├── .spi.yml
├── SampleCode
└── ScryfallSearcher
│ ├── ScryfallSearcher
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── ScryfallSearcherApp.swift
│ ├── CardView.swift
│ ├── SearchView.swift
│ └── ContentView.swift
│ └── ScryfallSearcher.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── project.pbxproj
├── .gitignore
├── Sources
└── ScryfallKit
│ ├── Documentation.docc
│ ├── SearchTutorial
│ │ ├── Resources
│ │ │ ├── Code
│ │ │ │ ├── Section1BasicUI
│ │ │ │ │ ├── Step0CreateView.swift
│ │ │ │ │ ├── Step1AddInput.swift
│ │ │ │ │ ├── Step2AddLazyVGrid.swift
│ │ │ │ │ ├── Step3AddAsyncImage.swift
│ │ │ │ │ └── Step4AddSearch.swift
│ │ │ │ └── Section2RefineUX
│ │ │ │ │ ├── Step1ShowErrors.swift
│ │ │ │ │ ├── Step2LoadingIndicator.swift
│ │ │ │ │ ├── Step3DisableAutocorrect.swift
│ │ │ │ │ └── Step4ZeroState.swift
│ │ │ └── Media
│ │ │ │ ├── Section2RefineUX
│ │ │ │ ├── Step3ZeroState.png
│ │ │ │ ├── Step1ShowErrorPreview.png
│ │ │ │ └── Step2LoadingIndicatorPreview.png
│ │ │ │ └── Section1BasicUI
│ │ │ │ ├── BasicUIFinalProduct.mov
│ │ │ │ ├── Step4AddSearchPreview.png
│ │ │ │ ├── Step3AddAsyncImagePreview.png
│ │ │ │ └── BasicUIFinalProductSearchCrop.png
│ │ ├── SearchTutorial.tutorial
│ │ └── BasicUI.tutorial
│ ├── ScryfallKit.md
│ ├── MultiFacedCards.md
│ └── RetrievingCards.md
│ ├── Extensions
│ ├── MTGSet+date.swift
│ └── Card+helpers.swift
│ ├── Networking
│ ├── EndpointRequests
│ │ ├── CatalogRequests.swift
│ │ ├── SymbolRequests.swift
│ │ ├── SetRequests.swift
│ │ ├── RulingsRequests.swift
│ │ ├── EndpointRequest.swift
│ │ └── CardRequests.swift
│ └── NetworkService.swift
│ ├── Logger.swift
│ ├── Models
│ ├── Card
│ │ ├── Card+Preview.swift
│ │ ├── Card+Prices.swift
│ │ ├── Card+Legalities.swift
│ │ ├── Card+ManaCost.swift
│ │ ├── Card+Ruling.swift
│ │ ├── Card+RelatedCard.swift
│ │ ├── Card+ImageUris.swift
│ │ ├── Card+Symbol.swift
│ │ ├── Card+Face.swift
│ │ ├── Card+enums.swift
│ │ └── Card.swift
│ ├── Catalog.swift
│ ├── Enums.swift
│ ├── ObjectList.swift
│ ├── ScryfallError.swift
│ ├── MTGSet.swift
│ └── FieldFilter.swift
│ ├── ScryfallKitError.swift
│ ├── ScryfallClient+Async.swift
│ └── ScryfallClient.swift
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ └── xcschemes
│ └── ScryfallKit.xcscheme
├── Package.resolved
├── Package.swift
├── Package@swift-5.7.swift
├── Package@swift-5.9.swift
├── Tests
└── ScryfallKitTests
│ ├── ComparableTests.swift
│ └── SmokeTests.swift
├── .github
└── workflows
│ ├── build+test.yml
│ └── publishDocs.yml
├── LICENSE
└── README.md
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [ScryfallKit]
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section1BasicUI/Step0CreateView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SearchView: View {
4 | var body: some View {
5 | Text("Hello, World!")
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section2RefineUX/Step3ZeroState.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section2RefineUX/Step3ZeroState.png
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/BasicUIFinalProduct.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/BasicUIFinalProduct.mov
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/Step4AddSearchPreview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/Step4AddSearchPreview.png
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section2RefineUX/Step1ShowErrorPreview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section2RefineUX/Step1ShowErrorPreview.png
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/Step3AddAsyncImagePreview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/Step3AddAsyncImagePreview.png
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/BasicUIFinalProductSearchCrop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section1BasicUI/BasicUIFinalProductSearchCrop.png
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section2RefineUX/Step2LoadingIndicatorPreview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JacobHearst/ScryfallKit/HEAD/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Media/Section2RefineUX/Step2LoadingIndicatorPreview.png
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/ScryfallSearcherApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScryfallSearcherApp.swift
3 | // ScryfallSearcher
4 | //
5 | // Created by Jacob Hearst on 4/11/23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct ScryfallSearcherApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | SearchView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section1BasicUI/Step1AddInput.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SearchView: View {
4 | @State private var query = ""
5 |
6 | var body: some View {
7 | TextField("Search for Magic: the Gathering cards", text: $query)
8 | .textFieldStyle(.roundedBorder)
9 | .padding()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-docc-plugin",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-docc-plugin",
7 | "state" : {
8 | "revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
9 | "version" : "1.0.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Extensions/MTGSet+date.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MTGSet.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension MTGSet {
8 | /// A `Date` created from ``releasedAt``
9 | public var date: Date? {
10 | guard let releasedAt = releasedAt else { return nil }
11 |
12 | let formatter = DateFormatter()
13 | formatter.dateFormat = "yyyy-MM-dd"
14 |
15 | return formatter.date(from: releasedAt)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/EndpointRequests/CatalogRequests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CatalogEndpointRequest.swift
3 | //
4 |
5 | import Foundation
6 |
7 | struct GetCatalog: EndpointRequest {
8 | var catalogType: Catalog.`Type`
9 |
10 | var path: String {
11 | return "catalog/\(catalogType.rawValue)"
12 | }
13 |
14 | var queryParams: [URLQueryItem] = []
15 | var requestMethod: RequestMethod = .GET
16 | var body: Data?
17 | }
18 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "scryfallkit",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/jacobhearst/scryfallkit",
7 | "state" : {
8 | "revision" : "bc1f742718420a099663b8af1eae2abbeb615ad3",
9 | "version" : "5.2.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Logger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Logger.swift
3 | //
4 |
5 | import Foundation
6 | import OSLog
7 |
8 | @available(macOS 11.0, *)
9 | @available(iOS 14.0, *)
10 | extension Logger {
11 | static let subsystem = "dev.hearst.scryfallkit"
12 | static let main = Logger(subsystem: subsystem, category: "ScryfallKit")
13 | static let client = Logger(subsystem: subsystem, category: "ScryfallClient")
14 | static let network = Logger(subsystem: subsystem, category: "Network")
15 | static let decoder = Logger(subsystem: subsystem, category: "Decoder")
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/SearchTutorial.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorials(name: "ScryfallKit") {
2 | @Intro(title: "Building native macOS and iOS Magic: the Gathering tools with ScryfallKit") {
3 | Bring the power of Scryfall to your native macOS and iOS experiences
4 | }
5 |
6 | @Chapter(name: "Creating a basic search UI") {
7 | First, lets create a basic UI that will allow the user to perform two basic tasks: searching and browsing
8 |
9 | @Image(source: BasicUIFinalProduct, alt: "A screen capture of the final product of the tutorial")
10 |
11 | @TutorialReference(tutorial: "doc:BasicUI")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section1BasicUI/Step2AddLazyVGrid.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
6 |
7 | @State private var query = ""
8 | @State private var cards = [Card]()
9 |
10 | var body: some View {
11 | ScrollView {
12 | TextField("Search for Magic: the Gathering cards", text: $query)
13 | .textFieldStyle(.roundedBorder)
14 |
15 | LazyVGrid(columns: columns) {
16 | ForEach(cards) { card in
17 |
18 | }
19 | }
20 | }
21 | .padding()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ScryfallKit",
8 | platforms: [.macOS(.v10_13), .iOS(.v12)],
9 | products: [
10 | .library(
11 | name: "ScryfallKit",
12 | targets: ["ScryfallKit"])
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
16 | ],
17 | targets: [
18 | .target(
19 | name: "ScryfallKit"
20 | ),
21 | .testTarget(
22 | name: "ScryfallKitTests",
23 | dependencies: ["ScryfallKit"]
24 | ),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Package@swift-5.7.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ScryfallKit",
8 | platforms: [.macOS(.v10_13), .iOS(.v12)],
9 | products: [
10 | .library(
11 | name: "ScryfallKit",
12 | targets: ["ScryfallKit"])
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
16 | ],
17 | targets: [
18 | .target(
19 | name: "ScryfallKit"
20 | ),
21 | .testTarget(
22 | name: "ScryfallKitTests",
23 | dependencies: ["ScryfallKit"]),
24 | ]
25 | )
26 |
--------------------------------------------------------------------------------
/Package@swift-5.9.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "ScryfallKit",
8 | platforms: [.macOS(.v10_13), .iOS(.v12)],
9 | products: [
10 | .library(
11 | name: "ScryfallKit",
12 | targets: ["ScryfallKit"])
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
16 | ],
17 | targets: [
18 | .target(
19 | name: "ScryfallKit"
20 | ),
21 | .testTarget(
22 | name: "ScryfallKitTests",
23 | dependencies: ["ScryfallKit"]
24 | ),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+Preview.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+Preview.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// Metadata about a Magic card's preview
9 | public struct Preview: Codable, Hashable, Sendable {
10 | /// The name of the source that previewed this card.
11 | public var source: String
12 | /// A link to the preview for this card.
13 | public var sourceUri: String
14 | /// The date this card was previewed.
15 | public var previewedAt: String
16 |
17 | public init(source: String, sourceUri: String, previewedAt: String) {
18 | self.source = source
19 | self.sourceUri = sourceUri
20 | self.previewedAt = previewedAt
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/EndpointRequests/SymbolRequests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SymbolRequests.swift
3 | //
4 |
5 | import Foundation
6 |
7 | struct GetSymbology: EndpointRequest {
8 | var path = "symbology"
9 | var queryParams: [URLQueryItem] = []
10 | var requestMethod: RequestMethod = .GET
11 | var body: Data?
12 | }
13 |
14 | struct ParseManaCost: EndpointRequest {
15 | // Request params
16 | var cost: String
17 |
18 | // Protocol vars
19 | var path = "symbology/parse-mana"
20 | var queryParams: [URLQueryItem]
21 | var requestMethod: RequestMethod = .GET
22 | var body: Data?
23 |
24 | init(cost: String) {
25 | self.cost = cost
26 | self.queryParams = [
27 | URLQueryItem(name: "cost", value: cost)
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/EndpointRequests/SetRequests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetEndpointRequest.swift
3 | //
4 |
5 | import Foundation
6 |
7 | struct GetSets: EndpointRequest {
8 | var path = "sets"
9 | var queryParams: [URLQueryItem] = []
10 | var requestMethod: RequestMethod = .GET
11 | var body: Data?
12 | }
13 |
14 | struct GetSet: EndpointRequest {
15 | var identifier: MTGSet.Identifier
16 |
17 | var path: String {
18 | switch self.identifier {
19 | case .code(let code):
20 | return "sets/\(code)"
21 | case .scryfallID(let id):
22 | return "sets/\(id)"
23 | case .tcgPlayerID(let id):
24 | return "sets/tcglayer/\(id)"
25 | }
26 | }
27 | var queryParams: [URLQueryItem] = []
28 | var requestMethod: RequestMethod = .GET
29 | var body: Data?
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/ScryfallKitTests/ComparableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComparableTests.swift
3 | //
4 |
5 | import ScryfallKit
6 | import XCTest
7 |
8 | class ComparableTests: XCTestCase {
9 | func testColorSort() {
10 | // Given
11 | let colors: [Card.Color] = [.B, .C, .W, .U, .G, .R]
12 |
13 | // When
14 | let result = colors.sorted()
15 |
16 | // Then
17 | let expected: [Card.Color] = [.W, .U, .B, .R, .G, .C]
18 | XCTAssertEqual(result, expected)
19 | }
20 |
21 | func testRaritySort() {
22 | // Given
23 | let rarities: [Card.Rarity] = [.mythic, .common, .rare, .uncommon, .bonus, .special]
24 |
25 | // When
26 | let result = rarities.sorted()
27 |
28 | // Then
29 | let expected: [Card.Rarity] = [.bonus, .special, .common, .uncommon, .rare, .mythic]
30 | XCTAssertEqual(result, expected)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/EndpointRequests/RulingsRequests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RulingsRequests.swift
3 | //
4 |
5 | import Foundation
6 |
7 | struct GetRulings: EndpointRequest {
8 | var identifier: Card.Ruling.Identifier
9 |
10 | var path: String {
11 | switch identifier {
12 | case .multiverseID(let id):
13 | return "cards/multiverse/\(id)/rulings"
14 | case .mtgoID(let id):
15 | return "cards/mtgo/\(id)/rulings"
16 | case .arenaID(let id):
17 | return "cards/arena/\(id)/rulings"
18 | case .collectorNumberSet(let collectorNumber, let set):
19 | return "cards/\(set)/\(collectorNumber)/rulings"
20 | case .scryfallID(let id):
21 | return "cards/\(id)/rulings"
22 | }
23 | }
24 | var queryParams: [URLQueryItem] = []
25 | var requestMethod: RequestMethod = .GET
26 | var body: Data?
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section1BasicUI/Step3AddAsyncImage.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
6 |
7 | @State private var query = ""
8 | @State private var cards = [Card]()
9 |
10 | var body: some View {
11 | ScrollView {
12 | TextField("Search for Magic: the Gathering cards", text: $query)
13 | .textFieldStyle(.roundedBorder)
14 |
15 | LazyVGrid(columns: columns) {
16 | ForEach(cards) { card in
17 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
18 | image
19 | .resizable()
20 | .scaledToFit()
21 | } placeholder: {
22 | Text(card.name)
23 | ProgressView()
24 | }
25 | }
26 | }
27 | }
28 | .padding()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+Prices.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+Prices.swift
3 | //
4 |
5 | extension Card {
6 | /// Daily price information for a Magic card
7 | public struct Prices: Codable, Hashable, Sendable {
8 | /// The price of this card in tix
9 | public var tix: String?
10 | /// The price of this card in usd
11 | public var usd: String?
12 | /// The price of this card's foil printing in usd
13 | public var usdFoil: String?
14 | /// The price of this card's etched printing in usd
15 | public var usdEtched: String?
16 | /// The price of this card in eur.
17 | public var eur: String?
18 |
19 | public init(tix: String? = nil, usd: String? = nil, usdFoil: String? = nil, usdEtched: String? = nil, eur: String? = nil)
20 | {
21 | self.tix = tix
22 | self.usd = usd
23 | self.usdFoil = usdFoil
24 | self.usdEtched = usdEtched
25 | self.eur = eur
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/build+test.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Build and test
4 |
5 | # Controls when the workflow will run
6 | on:
7 | schedule:
8 | - cron: 0 0 * * 0
9 | push:
10 | branches: [ main ]
11 | pull_request:
12 | branches: [ main ]
13 | workflow_dispatch:
14 |
15 | jobs:
16 | build:
17 | runs-on: macos-latest
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | - name: Build package
22 | run: swift build -v
23 |
24 | test:
25 | runs-on: macos-latest
26 | steps:
27 | - uses: actions/checkout@v2
28 |
29 | - name: Smoke test package
30 | run: swift test -v
31 |
32 | - name: CINotify Email
33 | if: ${{ failure() }}
34 | uses: cinotify/github-action@v1.1.0
35 | with:
36 | # Recipient email address
37 | to: jhearst@protonmail.ch
38 | # Email subject
39 | subject: ScryfallKit failure
40 | # Email body
41 | body: A ScryfallKit test failed
42 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/ScryfallKitError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScryfallKitError.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// An error thrown by the `ScryfallKit` module
8 | public enum ScryfallKitError: LocalizedError, CustomStringConvertible {
9 | /// Internal error, a request was tried with an invalid URL
10 | case invalidUrl
11 | /// An error returned by Scryfall's API such as for an invalid search
12 | case scryfallError(ScryfallError)
13 | case singleFacedCard
14 | case noDataReturned
15 | case failedToCast(String)
16 |
17 | public var errorDescription: String? {
18 | description
19 | }
20 |
21 | public var description: String {
22 | switch self {
23 | case .invalidUrl:
24 | return "Invalid URL"
25 | case .scryfallError(let error):
26 | return error.details
27 | case .singleFacedCard:
28 | return "Tried to access card faces on card with a single face"
29 | case .noDataReturned:
30 | return "No data was returned by the server"
31 | case .failedToCast(let details):
32 | return "Failed to cast \(details)"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jacob Hearst
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/EndpointRequests/EndpointRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EndpointRequest.swift
3 | //
4 |
5 | import Foundation
6 | import OSLog
7 |
8 | protocol EndpointRequest {
9 | var path: String { get }
10 | var queryParams: [URLQueryItem] { get }
11 | var requestMethod: RequestMethod { get }
12 | var body: Data? { get }
13 | }
14 |
15 | extension EndpointRequest {
16 | var urlRequest: URLRequest? {
17 | var urlComponents = URLComponents(string: "https://api.scryfall.com/\(path)")
18 | if !queryParams.isEmpty {
19 | urlComponents?.queryItems = queryParams.compactMap { $0.value == nil ? nil : $0 }
20 | }
21 |
22 | guard let url = urlComponents?.url else {
23 | if #available(iOS 14.0, macOS 11.0, *) {
24 | Logger.main.error("Couldn't make url")
25 | } else {
26 | print("Couldn't make url")
27 | }
28 | return nil
29 | }
30 |
31 | var urlRequest = URLRequest(url: url)
32 | urlRequest.httpMethod = requestMethod.rawValue
33 | urlRequest.httpBody = body
34 | urlRequest.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
35 |
36 | return urlRequest
37 | }
38 | }
39 |
40 | enum RequestMethod: String {
41 | case GET, POST
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section1BasicUI/Step4AddSearch.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let client = ScryfallClient()
6 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
7 |
8 | @State private var query = ""
9 | @State private var cards = [Card]()
10 |
11 | var body: some View {
12 | ScrollView {
13 | TextField("Search for Magic: the Gathering cards", text: $query)
14 | .textFieldStyle(.roundedBorder)
15 | .onSubmit(search)
16 |
17 | LazyVGrid(columns: columns) {
18 | ForEach(cards) { card in
19 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
20 | image
21 | .resizable()
22 | .scaledToFit()
23 | } placeholder: {
24 | Text(card.name)
25 | ProgressView()
26 | }
27 | }
28 | }
29 | }
30 | .padding()
31 | }
32 |
33 | private func search() {
34 | Task {
35 | do {
36 | let results = try await client.searchCards(query: query)
37 | await MainActor.run {
38 | cards = results.data
39 | }
40 | } catch {
41 | print(error)
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/ScryfallKit.md:
--------------------------------------------------------------------------------
1 | # ``ScryfallKit``
2 |
3 | A simple Swift wrapper for the popular Magic: the Gathering API by Scryfall
4 |
5 | > Warning: This library does not implement the 10 requests/second rate limiting requested by the Scryfall team.
6 | >
7 | > "We encourage you to cache the data you download from Scryfall or process it locally in your own database, at least for 24 hours. Scryfall provides our entire database compressed for download in daily bulk data files" ([Scryfall](https://scryfall.com/docs/api#rate-limits-and-good-citizenship)).
8 |
9 | This library is largely a translation of Scryfall's REST API to a collection of Swift enums and structs. It is highly recommended that you read the [official Scryfall API docs](https://scryfall.com/docs/api) as the documentation in this project will not attempt to give in-depth explanations of how each of these endpoints work.
10 |
11 | Some recommended starting points:
12 | - [Lists and pagination](https://scryfall.com/docs/api/lists)
13 | - [Errors](https://scryfall.com/docs/api/errors)
14 | - [Card Imagery](https://scryfall.com/docs/api/images)
15 | - [Layouts and Faces](https://scryfall.com/docs/api/layouts)
16 |
17 | ## Topics
18 |
19 | ### Fundamentals
20 |
21 | -
22 | -
23 | -
24 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+Legalities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+Legalities.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// The legality of a Magic card in each format
9 | public struct Legalities: Codable, Hashable, Sendable {
10 | public let standard: Legality?
11 | public let historic: Legality?
12 | public let pioneer: Legality?
13 | public let modern: Legality?
14 | public let legacy: Legality?
15 | public let pauper: Legality?
16 | public let vintage: Legality?
17 | public let penny: Legality?
18 | public let commander: Legality?
19 | public let brawl: Legality?
20 |
21 | public init(
22 | standard: Legality?,
23 | historic: Legality?,
24 | pioneer: Legality?,
25 | modern: Legality?,
26 | legacy: Legality?,
27 | pauper: Legality?,
28 | vintage: Legality?,
29 | penny: Legality?,
30 | commander: Legality?,
31 | brawl: Legality?
32 | ) {
33 | self.standard = standard
34 | self.historic = historic
35 | self.pioneer = pioneer
36 | self.modern = modern
37 | self.legacy = legacy
38 | self.pauper = pauper
39 | self.vintage = vintage
40 | self.penny = penny
41 | self.commander = commander
42 | self.brawl = brawl
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Catalog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Catalog.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// A struct containing an array of Magic datapoints
8 | ///
9 | /// [Scryfall documentation](https://scryfall.com/docs/api/catalogs)
10 | public struct Catalog: Codable, Sendable {
11 | /// The catalog type. Each of these types represents a different `/catalogs` endpoint
12 | public enum `Type`: String, Codable, CaseIterable {
13 | case powers, toughnesses, loyalties, watermarks
14 | case cardNames = "card-names"
15 | case artistNames = "artist-names"
16 | case wordBank = "word-bank"
17 | case creatureTypes = "creature-types"
18 | case planeswalkerTypes = "planeswalker-types"
19 | case landTypes = "land-types"
20 | case artifactTypes = "artifact-types"
21 | case enchantmentTypes = "enchantment-types"
22 | case spellTypes = "spell-types"
23 | case keywordAbilities = "keyword-abilities"
24 | case keywordActions = "keyword-actions"
25 | case abilityWords = "ability-words"
26 | }
27 |
28 | /// The number of items in the `data` array
29 | public var totalValues: Int
30 |
31 | /// An array of data points
32 | public var data: [String]
33 |
34 | public init(totalValues: Int, data: [String]) {
35 | self.totalValues = totalValues
36 | self.data = data
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+ManaCost.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+ManaCost.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// The mana cost of a card
9 | ///
10 | /// Returned by ``ScryfallClient/parseManaCost(_:completion:)``
11 | ///
12 | /// [Scryfall documentation](https://scryfall.com/docs/api/colors)
13 | public struct ManaCost: Codable {
14 | /// The normalized cost, with correctly-ordered and wrapped mana symbols.
15 | public var cost: String
16 | /// The mana value. If you submit Un-set mana symbols, this decimal could include fractional parts.
17 | public var cmc: Double
18 | /// The colors of the given cost.
19 | public var colors: [Card.Color]
20 | /// True if the cost is colorless.
21 | public var colorless: Bool
22 | /// True if the cost is monocolored.
23 | public var monocolored: Bool
24 | /// True if the cost is multicolored.
25 | public var multicolored: Bool
26 |
27 | public init(
28 | cost: String, cmc: Double, colors: [Card.Color], colorless: Bool, monocolored: Bool,
29 | multicolored: Bool
30 | ) {
31 | self.cost = cost
32 | self.cmc = cmc
33 | self.colors = colors
34 | self.colorless = colorless
35 | self.monocolored = monocolored
36 | self.multicolored = multicolored
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/CardView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CardView.swift
3 | //
4 |
5 | import ScryfallKit
6 | import SwiftUI
7 |
8 | struct CardView: View {
9 | var card: Card
10 |
11 | var body: some View {
12 | VStack {
13 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
14 | image
15 | .resizable()
16 | .scaledToFit()
17 | } placeholder: {
18 | Text(card.name)
19 | ProgressView()
20 | }
21 |
22 | GroupBox {
23 | HStack {
24 | Text(card.name)
25 | Spacer()
26 | Text(card.manaCost ?? "")
27 | }
28 |
29 | Divider()
30 |
31 | Text(card.oracleText ?? "")
32 | .padding(.bottom)
33 | Text(card.flavorText ?? "")
34 | .italic()
35 |
36 | if let powerAndToughness = card.powerAndToughness {
37 | Divider()
38 | HStack {
39 | Spacer()
40 | Text(powerAndToughness)
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
48 | extension Card {
49 | var powerAndToughness: String? {
50 | guard let power, let toughness else { return nil }
51 | return "\(power)/\(toughness)"
52 | }
53 | }
54 |
55 | //struct SwiftUIView_Previews: PreviewProvider {
56 | // static var previews: some View {
57 | // SwiftUIView()
58 | // }
59 | //}
60 |
--------------------------------------------------------------------------------
/.github/workflows/publishDocs.yml:
--------------------------------------------------------------------------------
1 | name: Build and Publish Docs
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [ main ]
7 |
8 | # Kill any previous run still executing
9 | concurrency:
10 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | build_docs:
15 | name: Build and Archive Docs
16 | runs-on: macos-14
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 |
21 | - name: Generate docs
22 | run: |
23 | swift package \
24 | --allow-writing-to-directory github-pages \
25 | generate-documentation \
26 | --target ScryfallKit \
27 | --disable-indexing \
28 | --transform-for-static-hosting \
29 | --hosting-base-path ScryfallKit/ \
30 | --output-path github-pages
31 |
32 | - name: Upload docs archive
33 | uses: actions/upload-pages-artifact@v3
34 | with:
35 | path: github-pages
36 |
37 | deploy:
38 | name: Deploy Docs
39 | needs: build_docs
40 |
41 | permissions:
42 | pages: write
43 | id-token: write
44 |
45 | environment:
46 | name: github-pages
47 | url: ${{ steps.deployment.outputs.page_url }}
48 |
49 | runs-on: ubuntu-latest
50 | steps:
51 | - name: Deploy
52 | id: deployment
53 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Enums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Enums.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// Environments to play Magic: The Gathering in
8 | public enum Game: String, Codable, CaseIterable, Sendable {
9 | case paper, mtgo, arena
10 | }
11 |
12 | /// Comparison strategies for determining what makes a card "unique"
13 | ///
14 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards/search#unique-rollup-modes)
15 | public enum UniqueMode: String, Codable, CaseIterable, Sendable {
16 | case cards, art, prints
17 | }
18 |
19 | /// Fields that Scryfall can sort cards by
20 | ///
21 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards/search#sorting-cards)
22 | public enum SortMode: String, Codable, CaseIterable, Sendable {
23 | case name, set, released, rarity, color, usd, tix, eur, cmc, power, toughness, edhrec, artist
24 | }
25 |
26 | /// Directions that Scryfall can order cards in
27 | ///
28 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards/search#sorting-cards)
29 | public enum SortDirection: String, Codable, CaseIterable, Sendable {
30 | case auto, asc, desc
31 | }
32 |
33 | /// Formats for playing Magic: the Gathering
34 | public enum Format: String, CaseIterable, Sendable {
35 | case standard, historic, pioneer, modern, legacy, pauper, vintage, penny, commander, brawl
36 | }
37 |
38 | /// Currency types that Scryfall provides prices for
39 | public enum Currency: String, CaseIterable, Sendable {
40 | case usd, eur, tix, usdFoil, usdEtched
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/ObjectList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ObjectList.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// A sequence of Scryfall objects. May be paginated
8 | ///
9 | /// [Scryfall documentation](https://scryfall.com/docs/api/lists)
10 | public struct ObjectList: Codable, Sendable where T: Sendable {
11 | /// The data contained in the list
12 | public var data: [T]
13 | /// True if there's a next page, nil if there's only one page
14 | public var hasMore: Bool?
15 | /// If there is a page beyond the current page, this field will contain a full API URI to that page. You may submit a HTTP GET request to that URI to continue paginating forward on this List.
16 | public var nextPage: String?
17 | /// If this is a list of Card objects, this field will contain the total number of cards found across all pages.
18 | public var totalCards: Int?
19 | /// An array of human-readable warnings issued when generating this list, as strings.
20 | ///
21 | /// Warnings are non-fatal issues that the API discovered with your input. In general, they indicate that the List will not contain the all of the information you requested. You should fix the warnings and re-submit your request.
22 | public var warnings: [String]?
23 |
24 | public init(
25 | data: [T], hasMore: Bool? = nil, nextPage: String? = nil, totalCards: Int? = nil,
26 | warnings: [String]? = nil
27 | ) {
28 | self.data = data
29 | self.hasMore = hasMore
30 | self.nextPage = nextPage
31 | self.totalCards = totalCards
32 | self.warnings = warnings
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+Ruling.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+Ruling.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// An object representing a ruling on a specific card
9 | public struct Ruling: Codable, Identifiable, Sendable {
10 | /// A value or combination of values that can identify a ruling. Used to find rulings for specific cards
11 | public enum Identifier {
12 | case scryfallID(id: String)
13 | case mtgoID(id: Int)
14 | case multiverseID(id: Int)
15 | case arenaID(id: Int)
16 | case collectorNumberSet(collectorNumber: String, set: String)
17 | }
18 |
19 | /// A computer-readable string indicating which company produced this ruling
20 | public enum Source: String, Codable, Sendable {
21 | case scryfall
22 | case wotc
23 | }
24 |
25 | /// A computer-readable string indicating which company produced this ruling
26 | public var source: Source
27 | /// The date when the ruling or note was published.
28 | public var publishedAt: String
29 | /// The text of the ruling.
30 | public var comment: String
31 | /// The card's oracle id
32 | public var oracleId: String
33 |
34 | /// An id made by concatenating the oracle id and the comment itself
35 | public var id: String { oracleId + comment }
36 |
37 | public init(source: Source, publishedAt: String, comment: String, oracleId: String) {
38 | self.source = source
39 | self.publishedAt = publishedAt
40 | self.comment = comment
41 | self.oracleId = oracleId
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/ScryfallError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScryfallError.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// An error returned by the Scryfall REST API
8 | public struct ScryfallError: Codable, CustomStringConvertible, Sendable {
9 | /// An integer HTTP status code for this error.
10 | public var status: Int
11 | /// A computer-friendly string representing the appropriate HTTP status code.
12 | public var code: String
13 | /// A human-readable string explaining the error.
14 | public var details: String
15 | /// A computer-friendly string that provides additional context for the main error.
16 | ///
17 | /// For example, an endpoint many generate HTTP 404 errors for different kinds of input. This field will provide a label for the specific kind of 404 failure, such as ambiguous.
18 | public var type: String?
19 | /// If your input also generated non-failure warnings, they will be provided as human-readable strings in this array.
20 | public var warnings: [String]?
21 |
22 | public init(
23 | status: Int, code: String, details: String, type: String? = nil, warnings: [String]? = nil
24 | ) {
25 | self.status = status
26 | self.code = code
27 | self.details = details
28 | self.type = type
29 | self.warnings = warnings
30 | }
31 |
32 | public var description: String {
33 | return """
34 | Scryfall Error
35 | - Status: \(status)
36 | - Code: \(code)
37 | - Details: \(details)
38 | - Type?: \(String(describing: type))
39 | - Warnings?: \(String(describing: warnings))
40 | """
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section2RefineUX/Step1ShowErrors.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let client = ScryfallClient()
6 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
7 |
8 | @State private var query = ""
9 | @State private var cards = [Card]()
10 | @State private var error: String?
11 | @State private var showError = false
12 |
13 | var body: some View {
14 | ScrollView {
15 | TextField("Search for Magic: the Gathering cards", text: $query)
16 | .textFieldStyle(.roundedBorder)
17 | .onSubmit(search)
18 |
19 | LazyVGrid(columns: columns) {
20 | ForEach(cards) { card in
21 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
22 | image
23 | .resizable()
24 | .scaledToFit()
25 | } placeholder: {
26 | Text(card.name)
27 | ProgressView()
28 | }
29 | }
30 | }
31 | }
32 | .padding()
33 | .alert("Error", isPresented: $showError, presenting: error, actions: { _ in }) { error in
34 | Text(error)
35 | }
36 | }
37 |
38 | private func search() {
39 | error = nil
40 | showError = false
41 |
42 | Task {
43 | do {
44 | let results = try await client.searchCards(query: query)
45 | await MainActor.run {
46 | cards = results.data
47 | }
48 | } catch {
49 | self.error = error.localizedDescription
50 | self.showError = true
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+RelatedCard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+RelatedCard.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// A Magic card that's related to another Magic card
9 | ///
10 | /// - Note: In the documentation of this struct, "this card" will refer to the `RelatedCard` object while "the original card" will refer to the `Card` object that contains this object
11 | public struct RelatedCard: Codable, Hashable, Sendable {
12 | /// The type of relationship
13 | public enum Component: String, Codable, CaseIterable, Hashable, Sendable {
14 | /// This card is a token that's made by the original card
15 | case token
16 | /// This card melds with the original card
17 | case meldPart = "meld_part"
18 | /// This card is the result of melding the original card with its other half
19 | case meldResult = "meld_result"
20 | /// This card combos with the original card
21 | case comboPiece = "combo_piece"
22 | }
23 |
24 | /// The Scryfall ID of this card
25 | public var id: UUID
26 | /// The type of relationship this card has to the original card
27 | public var component: Component
28 | /// The name of this card
29 | public var name: String
30 | /// The space separated types of this card
31 | public var typeLine: String
32 | /// A URI where you can retrieve a full object describing this card on Scryfall’s API
33 | public var uri: String
34 |
35 | public init(
36 | id: UUID, component: RelatedCard.Component, name: String, typeLine: String, uri: String
37 | ) {
38 | self.id = id
39 | self.component = component
40 | self.name = name
41 | self.typeLine = typeLine
42 | self.uri = uri
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section2RefineUX/Step2LoadingIndicator.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let client = ScryfallClient()
6 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
7 |
8 | @State private var loading = false
9 | @State private var query = ""
10 | @State private var cards = [Card]()
11 | @State private var error: String?
12 | @State private var showError = false
13 |
14 | var body: some View {
15 | ScrollView {
16 | TextField("Search for Magic: the Gathering cards", text: $query)
17 | .textFieldStyle(.roundedBorder)
18 | .onSubmit(search)
19 |
20 | if loading {
21 | ProgressView()
22 | } else {
23 | LazyVGrid(columns: columns) {
24 | ForEach(cards) { card in
25 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
26 | image
27 | .resizable()
28 | .scaledToFit()
29 | } placeholder: {
30 | Text(card.name)
31 | ProgressView()
32 | }
33 | }
34 | }
35 | }
36 | }
37 | .padding()
38 | .alert("Error", isPresented: $showError, presenting: error, actions: { _ in }) { error in
39 | Text(error)
40 | }
41 | }
42 |
43 | private func search() {
44 | loading = true
45 | error = nil
46 | showError = false
47 |
48 | Task {
49 | do {
50 | let results = try await client.searchCards(query: query)
51 | await MainActor.run {
52 | cards = results.data
53 | }
54 | } catch {
55 | self.error = error.localizedDescription
56 | self.showError = true
57 | }
58 |
59 | loading = false
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section2RefineUX/Step3DisableAutocorrect.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let client = ScryfallClient()
6 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
7 |
8 | @State private var loading = false
9 | @State private var query = ""
10 | @State private var cards = [Card]()
11 | @State private var error: String?
12 | @State private var showError = false
13 |
14 | var body: some View {
15 | ScrollView {
16 | TextField("Search for Magic: the Gathering cards", text: $query)
17 | .textFieldStyle(.roundedBorder)
18 | .autocorrectionDisabled(true)
19 | .onSubmit(search)
20 |
21 | if loading {
22 | ProgressView()
23 | } else {
24 | LazyVGrid(columns: columns) {
25 | ForEach(cards) { card in
26 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
27 | image
28 | .resizable()
29 | .scaledToFit()
30 | } placeholder: {
31 | Text(card.name)
32 | ProgressView()
33 | }
34 | }
35 | }
36 | }
37 | }
38 | .padding()
39 | .alert("Error", isPresented: $showError, presenting: error, actions: { _ in }) { error in
40 | Text(error)
41 | }
42 | }
43 |
44 | private func search() {
45 | loading = true
46 | error = nil
47 | showError = false
48 |
49 | Task {
50 | do {
51 | let results = try await client.searchCards(query: query)
52 | await MainActor.run {
53 | cards = results.data
54 | }
55 | } catch {
56 | self.error = error.localizedDescription
57 | self.showError = true
58 | }
59 |
60 | loading = false
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/Resources/Code/Section2RefineUX/Step4ZeroState.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let client = ScryfallClient()
6 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
7 |
8 | @State private var loading = false
9 | @State private var query = ""
10 | @State private var cards = [Card]()
11 | @State private var error: String?
12 | @State private var showError = false
13 |
14 | var body: some View {
15 | ScrollView {
16 | TextField("Search for Magic: the Gathering cards", text: $query)
17 | .textFieldStyle(.roundedBorder)
18 | .autocorrectionDisabled(true)
19 | .onSubmit(search)
20 |
21 | if loading {
22 | ProgressView()
23 | } else if cards.isEmpty {
24 | Text("Perform a search to view cards")
25 | } else {
26 | LazyVGrid(columns: columns) {
27 | ForEach(cards) { card in
28 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
29 | image
30 | .resizable()
31 | .scaledToFit()
32 | } placeholder: {
33 | Text(card.name)
34 | ProgressView()
35 | }
36 | }
37 | }
38 | }
39 | }
40 | .padding()
41 | .alert("Error", isPresented: $showError, presenting: error, actions: { _ in }) { error in
42 | Text(error)
43 | }
44 | }
45 |
46 | private func search() {
47 | loading = true
48 | error = nil
49 | showError = false
50 |
51 | Task {
52 | do {
53 | let results = try await client.searchCards(query: query)
54 | await MainActor.run {
55 | cards = results.data
56 | }
57 | } catch {
58 | self.error = error.localizedDescription
59 | self.showError = true
60 | }
61 |
62 | loading = false
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+ImageUris.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+ImageUris.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// Image URIs for each ``Card/ImageType``
9 | public struct ImageUris: Codable, Hashable, Sendable {
10 | /// A link to a small full card image.
11 | /// - Note: Designed for use as thumbnail or list icon.
12 | public let small: String?
13 | /// A link to a medium-sized full card image
14 | public let normal: String?
15 | /// A link to a large full card image
16 | public let large: String?
17 | /// A link to a transparent, rounded full card PNG.
18 | /// - Note: This is the best image to use for videos or other high-quality content.
19 | public let png: String?
20 | /// A link to a rectangular crop of the card’s art only.
21 | /// - Note: Not guaranteed to be perfect for cards with outlier designs or strange frame arrangements
22 | public let artCrop: String?
23 | /// A link to a full card image with the rounded corners and the majority of the border cropped off.
24 | /// - Note: Designed for dated contexts where rounded images can’t be used.
25 | public let borderCrop: String?
26 |
27 | public init(
28 | small: String?, normal: String?, large: String?, png: String?, artCrop: String?,
29 | borderCrop: String?
30 | ) {
31 | self.small = small
32 | self.normal = normal
33 | self.large = large
34 | self.png = png
35 | self.artCrop = artCrop
36 | self.borderCrop = borderCrop
37 | }
38 |
39 | /// Get the URI for a specific image type
40 | /// - Parameter type: The image type to retrieve the URI for
41 | /// - Returns: The URI, if present
42 | public func uri(for type: Card.ImageType) -> String? {
43 | switch type {
44 | case .artCrop:
45 | return artCrop
46 | case .borderCrop:
47 | return borderCrop
48 | case .large:
49 | return large
50 | case .png:
51 | return png
52 | case .normal:
53 | return normal
54 | case .small:
55 | return small
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/SearchView.swift:
--------------------------------------------------------------------------------
1 | import ScryfallKit
2 | import SwiftUI
3 |
4 | struct SearchView: View {
5 | private let client = ScryfallClient()
6 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
7 |
8 | @State private var loading = false
9 | @State private var query = ""
10 | @State private var cards = [Card]()
11 | @State private var error: String?
12 | @State private var showError = false
13 |
14 | var body: some View {
15 | NavigationStack {
16 | ScrollView {
17 | TextField("Search for Magic: the Gathering cards", text: $query)
18 | .textFieldStyle(.roundedBorder)
19 | .autocorrectionDisabled(true)
20 | .onSubmit(search)
21 |
22 | if loading {
23 | ProgressView()
24 | } else if cards.isEmpty {
25 | Text("Perform a search to view cards")
26 | } else {
27 | LazyVGrid(columns: columns) {
28 | ForEach(cards) { card in
29 | NavigationLink(value: card) {
30 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
31 | image
32 | .resizable()
33 | .scaledToFit()
34 | } placeholder: {
35 | Text(card.name)
36 | ProgressView()
37 | }
38 | }
39 | }
40 | }.navigationDestination(for: Card.self) { CardView(card: $0) }
41 | }
42 | }
43 | }
44 | .padding()
45 | .alert("Error", isPresented: $showError, presenting: error, actions: { _ in }) { error in
46 | Text(error)
47 | }
48 | }
49 |
50 | private func search() {
51 | loading = true
52 | error = nil
53 | showError = false
54 |
55 | Task {
56 | do {
57 | let results = try await client.searchCards(query: query)
58 | await MainActor.run {
59 | cards = results.data
60 | }
61 | } catch {
62 | self.error = error.localizedDescription
63 | self.showError = true
64 | }
65 |
66 | loading = false
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/MultiFacedCards.md:
--------------------------------------------------------------------------------
1 | # Multi-Faced Cards
2 |
3 | Dealing with cards that have multiple faces
4 |
5 | @Metadata {
6 | @PageKind(sampleCode)
7 | }
8 |
9 | ## Overview
10 |
11 | Magic: the Gathering has a variety of card layouts that are considered "multi-face". These layouts are often identified by an accompanying game mechanic like Transform. Another, less obvious, example would be cards with a "split" layout such as [Armed // Dangerous](https://scryfall.com/card/dgm/122/armed-dangerous) or [Appeal // Authority](https://scryfall.com/card/hou/152/appeal-authority). As each card face is treated like a separate card, each face may have different values for core fields. Mana cost, card type, and power and toughness are some examples. Scryfall has written about this topic themselves [here](https://scryfall.com/docs/api/layouts#card-faces).
12 |
13 | ScryfallKit includes some built in helper functions that make dealing with multi-faced cards easier. For example, ``ScryfallKit/Card/getImageURL(type:getSecondFace:)`` and ``ScryfallKit/Card/getImageURL(types:getSecondFace:)`` both have a `getSecondFace` parameter that does exactly what it sounds like: if set to `true`, it will return a url to the second face's art.
14 |
15 | ```swift
16 | let client = ScryfallClient()
17 |
18 | let thingInTheIce = try await client.getCardByName(exact: "Thing in the Ice")
19 | if let imageURL = thingInTheIce.getImageURL(type: .normal, getSecondFace: true) {
20 | print(imageURL) // https://cards.scryfall.io/normal/back/3/5/359d1b13-6156-43b0-a9a7-6bfff36c1a91.jpg?1576384282
21 | }
22 | ```
23 |
24 | Similarly, ``ScryfallKit/Card/getAttributeForFace(keyPath:useSecondFace:)`` allows you to use key paths to get the field for either the front or the back face via the `useSecondFace` parameter
25 |
26 | ```swift
27 | let client = ScryfallClient()
28 |
29 | let thingInTheIce = try await client.getCardByName(exact: "Thing in the Ice")
30 | print(try thingInTheIce.getAttributeForFace(keyPath: \.name, useSecondFace: true)) // Awoken Horror
31 |
32 | let arclightPhoenix = try await client.getCardByName(exact: "Arclight Phoenix")
33 | // Will throw because Arclight is a single-faced card
34 | let cardBack = try arclightPhoenix.getAttributeForFace(keyPath: \.name, useSecondFace: true)
35 | ```
36 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // ScryfallSearcher
4 | //
5 |
6 | import ScryfallKit
7 | import SwiftUI
8 |
9 | struct ContentView: View {
10 | private let client = ScryfallClient()
11 | private let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
12 |
13 | @State private var loading = false
14 | @State private var query = ""
15 | @State private var cards = [Card]()
16 | @State private var error: String?
17 | @State private var showError = false
18 |
19 | var body: some View {
20 | ScrollView {
21 | TextField("Search for Magic: the Gathering cards", text: $query)
22 | .textFieldStyle(.roundedBorder)
23 | .autocorrectionDisabled(true)
24 | .textInputAutocapitalization(.never)
25 | .onSubmit {
26 | search(query: query)
27 | }
28 |
29 | if loading {
30 | ProgressView()
31 | } else if cards.isEmpty {
32 | Text("Perform a search to view cards")
33 | } else {
34 | LazyVGrid(columns: columns) {
35 | ForEach(cards) { card in
36 | AsyncImage(url: card.getImageURL(type: .normal)) { image in
37 | image
38 | .resizable()
39 | .scaledToFit()
40 | } placeholder: {
41 | Text(card.name)
42 | ProgressView()
43 | }
44 | }
45 | }
46 | }
47 |
48 | Spacer()
49 | }
50 | .alert("Error", isPresented: $showError, presenting: error, actions: { _ in }) { error in
51 | Text(error)
52 | }
53 | .refreshable {
54 | guard !query.isEmpty else { return }
55 | search(query: query)
56 | }
57 | .padding()
58 | }
59 |
60 | private func search(query: String) {
61 | error = nil
62 | loading = true
63 |
64 | Task {
65 | do {
66 | let results = try await client.searchCards(query: query)
67 | await MainActor.run {
68 | cards = results.data
69 | }
70 | } catch {
71 | await MainActor.run {
72 | showError = true
73 | self.error = error.localizedDescription
74 | }
75 | }
76 |
77 | await MainActor.run {
78 | loading = false
79 | }
80 | }
81 | }
82 | }
83 |
84 | struct ContentView_Previews: PreviewProvider {
85 | static var previews: some View {
86 | ContentView()
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+Symbol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+Symbol.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// A symbol that could appear on a Magic: the Gathering card
9 | public struct Symbol: Codable, Identifiable, Sendable {
10 | /// The textual representation of this symbol
11 | public var symbol: String
12 | /// A more loose variation of the symbol.
13 | ///
14 | /// Example: "{W}" has a `looseVariant` of "W"
15 | public var looseVariant: String?
16 | /// An english description of this symbol's meaning
17 | public var english: String
18 | /// True if this symbol is transposable
19 | ///
20 | /// Scryfall doesn't provide any more information on what this means but inspecting which symbols are marked as transposable reveals that only hybrid and half mana symbols are transposable.
21 | public var transposable: Bool
22 | /// True if this symbol is a mana symbol
23 | public var representsMana: Bool
24 | /// The amount that this symbol adds to a card's converted mana cost
25 | public var cmc: Double?
26 | /// True if this symbol _could_ appear in a card's mana cost
27 | public var appearsInManaCosts: Bool
28 | /// True if this symbol is funny
29 | public var funny: Bool
30 | /// The colors that make up this symbol
31 | public var colors: [Color]
32 | /// Alternate notations for this symbol that used on Wizards of the Coast's [Gatherer](https://gatherer.wizards.com/Pages/Default.aspx)
33 | public var gathererAlternates: [String]?
34 | /// A link to an SVG of this symbol
35 | public var svgUri: String?
36 |
37 | /// A computed ID for this symbol which is just the `symbol` property
38 | public var id: String { symbol }
39 |
40 | public init(
41 | symbol: String,
42 | looseVariant: String? = nil,
43 | english: String,
44 | transposable: Bool,
45 | representsMana: Bool,
46 | cmc: Double? = nil,
47 | appearsInManaCosts: Bool,
48 | funny: Bool,
49 | colors: [Color],
50 | gathererAlternates: [String]? = nil,
51 | svgUri: String? = nil
52 | ) {
53 | self.symbol = symbol
54 | self.looseVariant = looseVariant
55 | self.english = english
56 | self.transposable = transposable
57 | self.representsMana = representsMana
58 | self.cmc = cmc
59 | self.appearsInManaCosts = appearsInManaCosts
60 | self.funny = funny
61 | self.colors = colors
62 | self.gathererAlternates = gathererAlternates
63 | self.svgUri = svgUri
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScryfallKit
2 | [](https://github.com/JacobHearst/ScryfallKit/actions/workflows/build+test.yml) [](https://swiftpackageindex.com/JacobHearst/ScryfallKit) [](https://swiftpackageindex.com/JacobHearst/ScryfallKit)
3 |
4 | A Swift Package for accessing [Scryfall's REST API](https://scryfall.com/docs/api)
5 |
6 | ## Documentation
7 | This library is largely a translation of Scryfall's REST API to a collection of Swift enums and structs. It is highly recommended that you read the [official Scryfall API docs](https://scryfall.com/docs/api) as the documentation in this project will not attempt to give in-depth explanations of how each of these endpoints work.
8 |
9 | For the most up to date documentation, use the DocC pages published [here](https://jacobhearst.github.io/ScryfallKit/documentation/scryfallkit/)
10 |
11 | To generate these pages locally, use this command from [Apple's Swift DocC plugin](https://github.com/apple/swift-docc-plugin#previewing-documentation)
12 |
13 | `swift package --disable-sandbox preview-documentation --target ScryfallKit`
14 |
15 | ## Getting Started
16 | Add ScryfallKit to your project either through the Xcode UI, or through the process below for Swift Packages
17 | ```swift
18 | let package = Package(
19 | // ... Other Package.swift stuff goes here
20 | dependencies: [
21 | .package(url: "https://github.com/JacobHearst/ScryfallKit", from: "5.0.0"), // Add the library to your manifest
22 | ],
23 | targets: [
24 | .target(name: "MyPackage", dependencies: ["ScryfallKit"]), // Add it to your target's dependencies
25 | ]
26 | )
27 | ```
28 |
29 | ## Example
30 | ```swift
31 | import ScryfallKit
32 |
33 | let client = ScryfallClient()
34 |
35 | // Retrieve the Strixhaven Mystical Archive printing of Doom Blade
36 | do {
37 | let doomBlade = try await client.getCardByName(exact: "Doom Blade", set: "STA")
38 | print(doomBlade.cmc)
39 | } catch {
40 | print("Received error: \(error)")
41 | }
42 |
43 | // Or using a completion handler
44 | client.getCardByName(exact: "Doom Blade", set: "STA") { result in
45 | switch result {
46 | case .success(let doomBlade):
47 | print(doomBlade.cmc)
48 | case .failure(let error):
49 | print("Received error: \(error)")
50 | }
51 | }
52 | ```
53 |
54 | ## Network Logging
55 | The ScryfallClient has a configurable level of network logging with two options: minimal and verbose. Enabling verbose logging will print the HTTP body of each request and response. Minimal logging will log that a request was made (and the URL it's made to) as well as that a response was received.
56 |
57 | ## Contributing
58 | Contributions are always welcome, simply fork this repo, make and test your changes, and then open a pull request. I will try and review it within a reasonable amount of time.
59 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/RetrievingCards.md:
--------------------------------------------------------------------------------
1 | # Retrieving Cards
2 |
3 | How to retrieve one or more cards from Scryfall
4 |
5 | @Metadata {
6 | @PageKind(sampleCode)
7 | }
8 |
9 | ## By Searching
10 |
11 | ScryfallKit provides two ways to search for cards:
12 | - Using the ``ScryfallKit/CardFieldFilter`` enum
13 | - Using a string that contains valid [Scryfall Syntax](https://scryfall.com/docs/syntax)
14 |
15 | #### Using CardFieldFilter
16 | ```swift
17 | let client = ScryfallClient()
18 | let filters: [CardFieldFilter] = [
19 | CardFieldFilter.type("forest"),
20 | CardFieldFilter.type("creature")
21 | ]
22 |
23 | let cards = try await client.searchCards(filters: filters)
24 | print(cards.data[0].name) // Prints "Dryad Arbor"
25 | ```
26 |
27 | #### Using Scryfall Syntax
28 | ```swift
29 | let client = ScryfallClient()
30 |
31 | let cards = try await client.searchCards(query: "t:creature t:forest")
32 | print(cards.data[0].name) // Prints "Dryad Arbor"
33 | ```
34 |
35 | ## By Name
36 | Scryfall's API supports retrieving a single card by both fuzzy and exact matching on the card name. It's recommended you read [Scryfall's documentation](https://scryfall.com/docs/api/cards/named) on this endpoint for better understanding of how name matching works and what errors you can expect.
37 |
38 | #### Using an exact name
39 | ```swift
40 | let client = ScryfallClient()
41 | let narsetEnlightenedMaster = try await client.getCardByName(exact: "Narset, Enlightened Master")
42 | print(narsetEnlightenedMaster.collectorNumber) // Prints "190"
43 | ```
44 |
45 | #### Using a fuzzy name
46 | ```swift
47 | let client = ScryfallClient()
48 | let narsetEnlightenedMaster = try await client.getCardByName(fuzzy: "Narset, Master")
49 | print(narsetEnlightenedMaster.collectorNumber) // Prints "190"
50 | ```
51 |
52 | ## By Identifier
53 | Scryfall's data contains the unique IDs used by several other services and marketplaces. In addition, cards are uniquely identifiable by the combination of their set code, collector number, and language. These identifiers are reflected in ``Card/Identifier``
54 |
55 | You can use these identifiers to retrieve individual cards with ``ScryfallKit/ScryfallClient/getCard(identifier:completion:)`` or to retrieve up to 75 cards at once with ``ScryfallClient/getCardCollection(identifiers:completion:)``
56 |
57 | #### Retrieving a single card
58 | ```swift
59 | let client = ScryfallClient()
60 |
61 | // Using the card's Scryfall ID
62 | let flumphScryfallID: Card.Identifier = .scryfallID(id: "cdc86e78-8911-4a0d-ba3a-7802f8d991ef")
63 | let flumph = try await client.getCard(identifier: flumphScryfallID)
64 | print(flumph.setName) // Adventures in the Forgotten Realms
65 |
66 | // Using the cards set code and collector number
67 | let mysteryIdentifier: Card.Identifier = .setCodeCollectorNo(setCode: "mrd", collectorNo: "150")
68 | let mysteryCard = try await client.getCard(identifier: mysteryIdentifier)
69 | print(mysteryCard.name) // Chalice of the Void
70 | ```
71 |
72 | #### Retrieving a collection of cards
73 | ```swift
74 | let client = ScryfallClient()
75 | let identifiers: [Card.CollectionIdentifier] = [
76 | .scryfallID(id: "b2288f7b-05b1-4a6c-8d02-42ffcadc6f0b"),
77 | .name("Lotus Field"),
78 | .collectorNoAndSet(collectorNo: "12", set: "dgm")
79 | ]
80 |
81 | let cards = try await client.getCardCollection(identifiers: identifiers)
82 | let names = cards.data.map { $0.name }
83 | print(names) // ["Pore Over the Pages", "Lotus Field", "Hidden Strings"]
84 | ```
85 |
86 | ## See Also
87 | - ``ScryfallKit/ScryfallClient/getRandomCard(query:completion:)``
88 | - ``ScryfallKit/ScryfallClient/searchCards(filters:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)``
89 | - ``ScryfallKit/ScryfallClient/searchCards(query:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)``
90 | - ``ScryfallKit/ScryfallClient/getCardByName(fuzzy:set:completion:)``
91 | - ``ScryfallKit/ScryfallClient/getCardByName(exact:set:completion:)``
92 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+Face.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+Face.swift
3 | //
4 |
5 | import Foundation
6 |
7 | extension Card {
8 | /// A single face of a multi-faced card
9 | ///
10 | /// [Scryfall documentation](https://scryfall.com/docs/api/layouts#card-faces)
11 | public struct Face: Codable, Hashable, Sendable {
12 | /// The converted mana cost of this card face, now called the "mana value"
13 | public var cmc: Double?
14 | /// The identifier for this card face’s oracle identity.
15 | ///
16 | /// For more information on this property, see [Scryfall's documentation](https://scryfall.com/docs/api/cards#core-card-fields)
17 | public var oracleId: String?
18 | /// The name of the artist who illustrated this card
19 | public var artist: String?
20 | /// An array of the colors in this card’s color indicator or nil if it doesn't have one
21 | ///
22 | /// Color indicators are used to specify the color of a card that has no mana symbols
23 | public var colorIndicator: [Card.Color]?
24 | /// An array of the colors in this card's mana cost
25 | public var colors: [Card.Color]?
26 | /// This card's flavor text if any
27 | public var flavorText: String?
28 | /// An ID for this card face's art that remains consistent across reprints
29 | public var illustrationId: UUID?
30 | /// An object listing available imagery for this card.
31 | public var imageUris: ImageUris?
32 | /// This card's starting loyalty counters if it's a planeswalker
33 | public var loyalty: String?
34 | /// The mana cost for this card.
35 | ///
36 | /// This value will be any empty string "" if the cost is absent. Remember that per the game rules, a missing mana cost and a mana cost of {0} are different values.
37 | public var manaCost: String
38 | /// The name of this card
39 | ///
40 | /// If the card has multiple faces the names will be separated by a "//" such as "Wear // Tear"
41 | public var name: String
42 | /// The oracle text for this card
43 | public var oracleText: String?
44 | /// The power of this card if it's a creature
45 | public var power: String?
46 | /// The localized name printed on this card, if any.
47 | public var printedName: String?
48 | /// The localized text printed on this card, if any.
49 | public var printedText: String?
50 | /// The localized type line printed on this card, if any.
51 | public var printedTypeLine: String?
52 | /// The toughness of this card if it's a creature
53 | public var toughness: String?
54 | /// This card's types, separated by a space
55 | /// - Note: Tokens don't have type lines
56 | public var typeLine: String?
57 | /// This card's watermark, if any
58 | public var watermark: String?
59 |
60 | public init(
61 | artist: String? = nil,
62 | colorIndicator: [Card.Color]? = nil,
63 | colors: [Card.Color]? = nil,
64 | flavorText: String? = nil,
65 | illustrationId: UUID? = nil,
66 | imageUris: ImageUris? = nil,
67 | loyalty: String? = nil,
68 | manaCost: String,
69 | name: String,
70 | oracleText: String? = nil,
71 | power: String? = nil,
72 | printedName: String? = nil,
73 | printedText: String? = nil,
74 | printedTypeLine: String? = nil,
75 | toughness: String? = nil,
76 | typeLine: String? = nil,
77 | watermark: String? = nil
78 | ) {
79 | self.artist = artist
80 | self.colorIndicator = colorIndicator
81 | self.colors = colors
82 | self.flavorText = flavorText
83 | self.illustrationId = illustrationId
84 | self.imageUris = imageUris
85 | self.loyalty = loyalty
86 | self.manaCost = manaCost
87 | self.name = name
88 | self.oracleText = oracleText
89 | self.power = power
90 | self.printedName = printedName
91 | self.printedText = printedText
92 | self.printedTypeLine = printedTypeLine
93 | self.toughness = toughness
94 | self.typeLine = typeLine
95 | self.watermark = watermark
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/NetworkService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkService.swift
3 | //
4 |
5 | import Foundation
6 | import OSLog
7 |
8 | /// An enum representing the two available levels of log verbosity
9 | public enum NetworkLogLevel: Sendable {
10 | /// Only log when requests are made and errors
11 | case minimal
12 | /// Log the bodies of requests and responses
13 | case verbose
14 | }
15 |
16 | protocol NetworkServiceProtocol: Sendable {
17 | func request(
18 | _ request: EndpointRequest,
19 | as type: T.Type,
20 | completion: @Sendable @escaping (Result) -> Void
21 | )
22 | @available(macOS 10.15.0, *, iOS 13.0.0, *)
23 | func request(_ request: EndpointRequest, as type: T.Type) async throws -> T
24 | }
25 |
26 | struct NetworkService: NetworkServiceProtocol, Sendable {
27 | var logLevel: NetworkLogLevel
28 |
29 | func request(
30 | _ request: EndpointRequest, as type: T.Type,
31 | completion: @Sendable @escaping (Result) -> Void
32 | ) {
33 | guard let urlRequest = request.urlRequest else {
34 | if #available(macOS 11.0, iOS 14.0, *) {
35 | Logger.network.error("Invalid url request")
36 | } else {
37 | print("Invalid url request")
38 | }
39 | completion(.failure(ScryfallKitError.invalidUrl))
40 | return
41 | }
42 |
43 | if logLevel == .verbose, let body = urlRequest.httpBody,
44 | let JSONString = String(data: body, encoding: String.Encoding.utf8)
45 | {
46 | print("Sending request with body:")
47 | if #available(macOS 11.0, iOS 14.0, *) {
48 | Logger.network.debug("\(JSONString)")
49 | } else {
50 | print(JSONString)
51 | }
52 | }
53 |
54 | let task = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
55 | do {
56 | let result = try handle(dataType: type, data: data, response: response, error: error)
57 | completion(.success(result))
58 | } catch {
59 | completion(.failure(error))
60 | }
61 | }
62 |
63 | if #available(macOS 11.0, iOS 14.0, *) {
64 | Logger.network.debug(
65 | "Making request to: '\(String(describing: urlRequest.url?.absoluteString))'")
66 | } else {
67 | print("Making request to: '\(String(describing: urlRequest.url?.absoluteString))'")
68 | }
69 | task.resume()
70 | }
71 |
72 | func handle(dataType: T.Type, data: Data?, response: URLResponse?, error: Error?)
73 | throws -> T
74 | {
75 | if let error = error {
76 | throw error
77 | }
78 |
79 | guard let content = data else {
80 | throw ScryfallKitError.noDataReturned
81 | }
82 |
83 | guard let httpStatus = (response as? HTTPURLResponse)?.statusCode else {
84 | throw ScryfallKitError.failedToCast("httpStatus property of response to HTTPURLResponse")
85 | }
86 |
87 | let decoder = JSONDecoder()
88 | decoder.keyDecodingStrategy = .convertFromSnakeCase
89 |
90 | if (200..<300).contains(httpStatus) {
91 | if logLevel == .verbose {
92 | let responseBody = String(data: content, encoding: .utf8)
93 | if #available(macOS 11.0, iOS 14.0, *) {
94 | Logger.network.debug("\(responseBody ?? "Couldn't represent response body as string")")
95 | } else {
96 | print(responseBody ?? "Couldn't represent response body as string")
97 | }
98 | }
99 |
100 | return try decoder.decode(dataType, from: content)
101 | } else {
102 | let httpError = try decoder.decode(ScryfallError.self, from: content)
103 | throw ScryfallKitError.scryfallError(httpError)
104 | }
105 | }
106 |
107 | @available(macOS 10.15.0, *, iOS 13.0.0, *)
108 | func request(_ request: EndpointRequest, as type: T.Type) async throws -> T
109 | where T: Sendable {
110 | try await withCheckedThrowingContinuation { continuation in
111 | self.request(request, as: type) { result in
112 | continuation.resume(with: result)
113 | }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Extensions/Card+helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+helpers.swift
3 | // ScryfallKit
4 | //
5 | // Created by Jacob Hearst on 9/7/21.
6 | //
7 |
8 | import Foundation
9 | import OSLog
10 |
11 | extension Card {
12 | /// Get the legality of a card in a given format
13 | /// - Parameter format: The format to get legality for
14 | /// - Returns: The legality of this card for the given format
15 | public func getLegality(for format: Format) -> Legality {
16 | switch format {
17 | case .brawl:
18 | return legalities.brawl ?? .notLegal
19 | case .standard:
20 | return legalities.standard ?? .notLegal
21 | case .historic:
22 | return legalities.historic ?? .notLegal
23 | case .pioneer:
24 | return legalities.pioneer ?? .notLegal
25 | case .modern:
26 | return legalities.modern ?? .notLegal
27 | case .legacy:
28 | return legalities.legacy ?? .notLegal
29 | case .pauper:
30 | return legalities.pauper ?? .notLegal
31 | case .vintage:
32 | return legalities.vintage ?? .notLegal
33 | case .penny:
34 | return legalities.penny ?? .notLegal
35 | case .commander:
36 | return legalities.commander ?? .notLegal
37 | }
38 | }
39 |
40 | /// Get the price string for a given currency
41 | /// - Parameter type: The currency you want the price string for
42 | /// - Returns: The price string, if present. Nil if not
43 | public func getPrice(for currency: Currency) -> String? {
44 | switch currency {
45 | case .usd: return prices.usd
46 | case .eur: return prices.eur
47 | case .tix: return prices.tix
48 | case .usdFoil: return prices.usdFoil
49 | case .usdEtched: return prices.usdEtched
50 | }
51 | }
52 | }
53 |
54 | // Multifaced helpers
55 | extension Card {
56 | /// Get an attribute for a multifaced card
57 | /// - Parameters:
58 | /// - keyPath: A KeyPath for the desired attribute
59 | /// - useSecondFace: Whether or not the attribute from the second face should be used
60 | /// - Returns: The requested property from the desired face
61 | public func getAttributeForFace(keyPath: KeyPath, useSecondFace: Bool)
62 | throws -> PropType
63 | {
64 | guard let faces = cardFaces else { throw ScryfallKitError.singleFacedCard }
65 | return useSecondFace ? faces[1][keyPath: keyPath] : faces[0][keyPath: keyPath]
66 | }
67 |
68 | /// Get the URL for a specific image type
69 | /// - Parameters:
70 | /// - type: The desired image type
71 | /// - getSecondFace: Whether or not the second face of a card should be retrieved
72 | public func getImageURL(type: ImageType, getSecondFace: Bool = false) -> URL? {
73 | var cardImageUris = self.imageUris
74 |
75 | if let faces = cardFaces {
76 | // Some cards have multiple "faces" but don't have unique images for those faces
77 | // Flip and split cards are examples of this
78 | let faceImageUris = getSecondFace ? faces[1].imageUris : faces[0].imageUris
79 | if faceImageUris != nil {
80 | cardImageUris = faceImageUris
81 | }
82 | }
83 |
84 | guard let uris = cardImageUris else {
85 | return nil
86 | }
87 |
88 | guard let uri = uris.uri(for: type) else {
89 | if #available(iOS 14.0, macOS 11.0, *) {
90 | Logger.main.error("No URI for image type \(type.rawValue)")
91 | } else {
92 | print("No URI for image type \(type)")
93 | }
94 | return nil
95 | }
96 |
97 | return URL(string: uri)
98 | }
99 |
100 | /// Get an image URL with backups in case the card doesn't have the desired ImageType
101 | ///
102 | /// Example: If you want the normal sized image uri but are okay with the large size if normal isn't available, you would call this method as: `getImageURL(types: [.normal, .large])`
103 | ///
104 | /// - Parameters:
105 | /// - types: A list of ImageTypes ordered by preference
106 | /// - getSecondFace: Whether or not the second face of a card should be retrieved
107 | public func getImageURL(types: [ImageType], getSecondFace: Bool = false) -> URL? {
108 | for type in types {
109 | guard let url = getImageURL(type: type, getSecondFace: getSecondFace) else {
110 | continue
111 | }
112 |
113 | return url
114 | }
115 |
116 | return nil
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/ScryfallKit.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
70 |
73 |
74 |
75 |
76 |
77 |
83 |
84 |
90 |
91 |
92 |
93 |
95 |
96 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Networking/EndpointRequests/CardRequests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchCards.swift
3 | //
4 |
5 | import Foundation
6 | import OSLog
7 |
8 | struct SearchCards: EndpointRequest {
9 | var body: Data?
10 | var requestMethod: RequestMethod = .GET
11 | var path = "cards/search"
12 | var queryParams: [URLQueryItem]
13 |
14 | init(
15 | query: String,
16 | unique: UniqueMode? = nil,
17 | order: SortMode? = nil,
18 | dir: SortDirection? = nil,
19 | includeExtras: Bool? = nil,
20 | includeMultilingual: Bool? = nil,
21 | includeVariations: Bool? = nil,
22 | page: Int? = nil
23 | ) {
24 |
25 | self.queryParams = [
26 | URLQueryItem(name: "q", value: query),
27 | URLQueryItem(name: "unique", value: unique?.rawValue),
28 | URLQueryItem(name: "order", value: order?.rawValue),
29 | URLQueryItem(name: "dir", value: dir?.rawValue),
30 | URLQueryItem(name: "include_extras", value: includeExtras?.description),
31 | URLQueryItem(name: "include_multilingual", value: includeMultilingual?.description),
32 | URLQueryItem(name: "include_variations", value: includeVariations?.description),
33 | URLQueryItem(name: "page", value: page?.description),
34 | ]
35 | }
36 | }
37 |
38 | struct GetCardNamed: EndpointRequest {
39 | var body: Data?
40 | var requestMethod: RequestMethod = .GET
41 | var path = "cards/named"
42 | var queryParams: [URLQueryItem]
43 |
44 | init(exact: String? = nil, fuzzy: String? = nil, set: String? = nil) {
45 | queryParams = [
46 | URLQueryItem(name: "exact", value: exact),
47 | URLQueryItem(name: "fuzzy", value: fuzzy),
48 | URLQueryItem(name: "set", value: set),
49 | ]
50 | }
51 | }
52 |
53 | struct GetCardAutocomplete: EndpointRequest {
54 | var body: Data?
55 | var requestMethod: RequestMethod = .GET
56 | var path = "cards/autocomplete"
57 | var queryParams: [URLQueryItem]
58 |
59 | init(query: String, includeExtras: Bool? = nil) {
60 | self.queryParams = [
61 | URLQueryItem(name: "q", value: query),
62 | URLQueryItem(name: "include_extras", value: includeExtras?.description),
63 | ]
64 | }
65 | }
66 |
67 | struct GetRandomCard: EndpointRequest {
68 | var body: Data?
69 | var requestMethod: RequestMethod = .GET
70 | var path = "cards/random"
71 | var queryParams: [URLQueryItem]
72 |
73 | init(query: String?) {
74 | queryParams = [
75 | URLQueryItem(name: "q", value: query)
76 | ]
77 | }
78 | }
79 |
80 | struct GetCard: EndpointRequest {
81 | let identifier: Card.Identifier
82 |
83 | var path: String {
84 | switch identifier {
85 | case .scryfallID(let id):
86 | return "cards/\(id)"
87 | case .setCodeCollectorNo(let setCode, let collectorNumber, let lang):
88 | let pathStr = "cards/\(setCode)/\(collectorNumber)"
89 | guard let language = lang else { return pathStr }
90 | return "\(pathStr)/\(language)"
91 | default:
92 | // This guard should never trip. The only card identifier that doesn't have provider/id is the set code/collector
93 | guard let id = identifier.id else {
94 | if #available(iOS 14.0, macOS 11.0, *) {
95 | Logger.main.error("Provided identifier doesn't have a provider or doesn't have an id")
96 | } else {
97 | print("Provided identifier doesn't have a provider or doesn't have an id")
98 | }
99 |
100 | fatalError(
101 | "Encountered a situation that shouldn't be possible: Card identifier's id property was nil"
102 | )
103 | }
104 |
105 | return "cards/\(identifier.provider)/\(id)"
106 | }
107 | }
108 |
109 | var queryParams = [URLQueryItem]()
110 | var requestMethod: RequestMethod = .GET
111 | var body: Data?
112 | }
113 |
114 | struct GetCardCollection: EndpointRequest {
115 | var path = "cards/collection"
116 | var queryParams: [URLQueryItem] = []
117 | var requestMethod: RequestMethod = .POST
118 | var body: Data?
119 |
120 | init(identifiers: [Card.CollectionIdentifier]) {
121 | let identifierJSON = identifiers.map { $0.json }
122 | let requestBody: [String: [[String: String]]] = ["identifiers": identifierJSON]
123 |
124 | do {
125 | body = try JSONSerialization.data(withJSONObject: requestBody)
126 | } catch {
127 | if #available(iOS 14.0, macOS 11.0, *) {
128 | Logger.main.error("Errored serializing dict to JSON for GetCardCollection request")
129 | } else {
130 | print("Errored serializing dict to JSON for GetCardCollection request")
131 | }
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/MTGSet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MTGSet.swift
3 | //
4 |
5 | import Foundation
6 | import OSLog
7 |
8 | /// A set represents a group of related Magic cards
9 | ///
10 | /// [Scryfall documentation](https://scryfall.com/docs/api/sets)
11 | public struct MTGSet: Codable, Identifiable, Hashable, Sendable {
12 | /// An value that can be used to identify a set
13 | public enum Identifier {
14 | case code(code: String)
15 | case scryfallID(id: String)
16 | case tcgPlayerID(id: String)
17 |
18 | var identifier: String {
19 | switch self {
20 | case .code(let code):
21 | return code
22 | case .scryfallID(let id):
23 | return id
24 | case .tcgPlayerID(let id):
25 | return id
26 | }
27 | }
28 | }
29 |
30 | /// A machine-readable value describing the type of set this is.
31 | ///
32 | /// See [Scryfall's docs](https://scryfall.com/docs/api/sets#set-types) for more information on set types
33 | public enum `Type`: String, Codable, Sendable {
34 | // While "masters" is in fact not inclusive, it's also a name that we can't control
35 | // swiftlint:disable:next inclusive_language
36 | case core, expansion, masters, masterpiece, spellbook, commander, planechase, archenemy,
37 | vanguard, funny, starter, box, promo, token, memorabilia, arsenal, alchemy, minigame, unknown
38 | case fromTheVault = "from_the_vault"
39 | case premiumDeck = "premium_deck"
40 | case duelDeck = "duel_deck"
41 | case draftInnovation = "draft_innovation"
42 | case treasureChest = "treasure_chest"
43 |
44 | public init(from decoder: Decoder) throws {
45 | self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
46 | if self == .unknown, let rawValue = try? String(from: decoder) {
47 | if #available(iOS 14.0, macOS 11.0, *) {
48 | Logger.main.warning("Decoded unknown MTGSet Type: \(rawValue)")
49 | } else {
50 | print("Decoded unknown MTGSet Type: \(rawValue)")
51 | }
52 | }
53 | }
54 | }
55 |
56 | /// A unique ID for this set on Scryfall that will not change.
57 | public var id: UUID
58 | /// The unique three to five-letter code for this set.
59 | public var code: String
60 | /// The unique code for this set on MTGO, which may differ from the regular code.
61 | public var mtgoCode: String?
62 | /// This set’s ID on [TCGplayer’s API](https://docs.tcgplayer.com/docs), also known as the groupId.
63 | public var tcgplayerId: Int?
64 | /// The English name of the set.
65 | public var name: String
66 | /// A computer-readable classification for this set.
67 | public var setType: MTGSet.`Type`
68 | /// The date the set was released or the first card was printed in the set (in GMT-8 Pacific time).
69 | public var releasedAt: String?
70 | /// The block code for this set, if any.
71 | public var blockCode: String?
72 | /// The block or group name code for this set, if any.
73 | public var block: String?
74 | /// The set code for the parent set, if any. promo and token sets often have a parent set.
75 | public var parentSetCode: String?
76 | /// The number of cards in this set.
77 | public var cardCount: Int
78 | /// The denominator for the set’s printed collector numbers.
79 | public var printedSize: Int?
80 | /// True if this set was only released in a video game.
81 | public var digital: Bool
82 | /// True if this set contains only foil cards.
83 | public var foilOnly: Bool
84 | /// True if this set contains only nonfoil cards.
85 | public var nonfoilOnly: Bool
86 | /// A link to this set’s permapage on Scryfall’s website.
87 | public var scryfallUri: String
88 | /// A link to this set object on Scryfall’s API.
89 | public var uri: String
90 | /// A URI to an SVG file for this set’s icon on Scryfall’s CDN.
91 | ///
92 | /// - Note: Hotlinking this image isn’t recommended, because it may change slightly over time. You should download it and use it locally for your particular user interface needs.
93 | public var iconSvgUri: String
94 | /// A Scryfall API URI that you can request to begin paginating over the cards in this set.
95 | public var searchUri: String
96 |
97 | public init(
98 | id: UUID,
99 | code: String,
100 | mtgoCode: String? = nil,
101 | tcgplayerId: Int? = nil,
102 | name: String,
103 | setType: MTGSet.`Type`,
104 | releasedAt: String? = nil,
105 | blockCode: String? = nil,
106 | block: String? = nil,
107 | parentSetCode: String? = nil,
108 | cardCount: Int,
109 | printedSize: Int? = nil,
110 | digital: Bool,
111 | foilOnly: Bool,
112 | nonfoilOnly: Bool,
113 | scryfallUri: String,
114 | uri: String,
115 | iconSvgUri: String,
116 | searchUri: String
117 | ) {
118 | self.id = id
119 | self.code = code
120 | self.mtgoCode = mtgoCode
121 | self.tcgplayerId = tcgplayerId
122 | self.name = name
123 | self.setType = setType
124 | self.releasedAt = releasedAt
125 | self.blockCode = blockCode
126 | self.block = block
127 | self.parentSetCode = parentSetCode
128 | self.cardCount = cardCount
129 | self.printedSize = printedSize
130 | self.digital = digital
131 | self.foilOnly = foilOnly
132 | self.nonfoilOnly = nonfoilOnly
133 | self.scryfallUri = scryfallUri
134 | self.uri = uri
135 | self.iconSvgUri = iconSvgUri
136 | self.searchUri = searchUri
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Documentation.docc/SearchTutorial/BasicUI.tutorial:
--------------------------------------------------------------------------------
1 | @Tutorial(time: 10) {
2 | @XcodeRequirement(
3 | title: "Xcode 14",
4 | destination: "https://developer.apple.com/download/")
5 |
6 | @Intro(title: "Creating a basic search experience") {
7 | Scryfall's primary function is its search feature, with ScryfallKit you can bring that search to Apple platforms .
8 | }
9 |
10 | @Section(title: "Implementing basic search") {
11 | @ContentAndMedia {
12 | Search engines have two primary areas: the search bar and the results. Let's create a basic UI that covers both of these areas.
13 |
14 | @Image(source: BasicUIFinalProductSearchCrop, alt: "The top third of a screenshot of the final product of this tutorial")
15 | }
16 |
17 | @Steps {
18 | @Step {
19 | Create a new SwiftUI view
20 |
21 | @Code(name: "SearchView.swift", file: Step0CreateView)
22 | }
23 |
24 | @Step {
25 | Add a `TextField` to act as the search input.
26 |
27 | @Code(name: "SearchView.swift", file: Step1AddInput)
28 | }
29 |
30 | @Step {
31 | Wrap your `TextField` in a `ScrollView` and then add a 2-columned `LazyVGrid` below the search input to display the images of the cards returned by the search.
32 |
33 | @Code(name: "SearchView.swift", file: Step2AddLazyVGrid)
34 | }
35 |
36 | @Step {
37 | Retrieve the URL to the card image with ``ScryfallKit/Card/getImageURL(type:getSecondFace:)`` and display it with an `AsyncImage`.
38 |
39 | Exact image dimensions can be found in [Scryfall's docs](https://scryfall.com/docs/api/images) but `.resizable()` combined with `.scaledToFit()` should suffice for most image sizes.
40 |
41 | @Code(name: "SearchView.swift", file: Step3AddAsyncImage) {
42 | @Image(source: Step3AddAsyncImagePreview, alt: "A screenshot of the final product of step 1 in an iOS simulator")
43 | }
44 | }
45 |
46 | @Step {
47 | Create a `search` function that submits the `query` to a ``ScryfallKit/ScryfallClient`` instance and then sets `cards` to the first page of results.
48 |
49 | @Code(name: "SearchView.swift", file: Step4AddSearch) {
50 | @Image(source: Step4AddSearchPreview, alt: "A screenshot of the final product of step 2 in an iOS simulator")
51 | }
52 | }
53 |
54 | @Step {
55 | Run your app and try entering "type:creature" in your search bar. You should see the grid of cards populate.
56 |
57 | @Video(source: "BasicUIFinalProduct.mov", poster: "BasicUIFinalProduct.png")
58 | }
59 | }
60 | }
61 |
62 | @Section(title: "Refining UX") {
63 | While we can now search for and view cards, the user experience leaves something to be desired. There are two main issues with the current implementation: errors aren't ever presented to the user and there's no loading indicator when a search is submitted
64 |
65 | @Steps {
66 | @Step {
67 | We'll be displaying errors to the user via an alert. Create some variables to hold the error message and the open state of the alert. Then call `.alert` on the top level `ScrollView`.
68 |
69 | @Code(name: "SearchView.swift", file: Step1ShowErrors, previousFile: Step4Search) {
70 | @Image(source: "Step1ShowErrorPreview", alt: "A screenshot of an alert displaying an error in an iOS simulator")
71 | }
72 | }
73 |
74 | @Step {
75 | Add a `loading` property and then add a conditional statement to determine whether to show a `ProgressView` or the search results
76 |
77 | Set `loading` to `true` and run the app or perform a search to see the loading indicator in action
78 |
79 | @Code(name: "SearchView.swift", file: Step2LoadingIndicator) {
80 | @Image(source: "Step2LoadingIndicatorPreview", alt: "A screenshot of a loading indicator where the card grid was")
81 | }
82 | }
83 |
84 | @Step {
85 | You may have noticed in your testing that autocorrect can be quite annoying when entering Magic: the Gathering card names like "Fblthp, the Lost". Let's disable that with by calling `autocorrectionDisabled(true)` on our `TextField`
86 |
87 | @Code(name: "SearchView.swift", file: Step3DisableAutocorrect)
88 | }
89 |
90 | @Step {
91 | As a final refinement, we want to help the user understand what to do when they open our app. Let's add a message that will only show if we're not loading and we don't have search results
92 |
93 | @Code(name: "SearchView.swift", file: Step4ZeroState) {
94 | @Image(source: "Step3ZeroState", alt: "A screenshot of a message prompting the user to perform a search")
95 | }
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/ScryfallClient+Async.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScryfallClient+Async.swift
3 | //
4 |
5 | import Foundation
6 |
7 | @available(macOS 10.15.0, *, iOS 13.0.0, *)
8 | extension ScryfallClient {
9 | /// Equivalent to ``searchCards(filters:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)`` but with async/await syntax
10 | public func searchCards(
11 | filters: [CardFieldFilter],
12 | unique: UniqueMode? = nil,
13 | order: SortMode? = nil,
14 | sortDirection: SortDirection? = nil,
15 | includeExtras: Bool? = nil,
16 | includeMultilingual: Bool? = nil,
17 | includeVariations: Bool? = nil,
18 | page: Int? = nil
19 | ) async throws -> ObjectList {
20 | try await withCheckedThrowingContinuation { continuation in
21 | searchCards(
22 | filters: filters,
23 | unique: unique,
24 | order: order,
25 | sortDirection: sortDirection,
26 | includeExtras: includeExtras,
27 | includeMultilingual: includeMultilingual,
28 | includeVariations: includeVariations,
29 | page: page
30 | ) { result in
31 | continuation.resume(with: result)
32 | }
33 | }
34 | }
35 |
36 | /// Equivalent to ``searchCards(query:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:completion:)`` but with async/await syntax
37 | public func searchCards(
38 | query: String,
39 | unique: UniqueMode? = nil,
40 | order: SortMode? = nil,
41 | sortDirection: SortDirection? = nil,
42 | includeExtras: Bool? = nil,
43 | includeMultilingual: Bool? = nil,
44 | includeVariations: Bool? = nil,
45 | page: Int? = nil
46 | ) async throws -> ObjectList {
47 | let request = SearchCards(
48 | query: query,
49 | unique: unique,
50 | order: order,
51 | dir: sortDirection,
52 | includeExtras: includeExtras,
53 | includeMultilingual: includeMultilingual,
54 | includeVariations: includeVariations,
55 | page: page)
56 |
57 | return try await networkService.request(request, as: ObjectList.self)
58 | }
59 |
60 | /// Equivalent to ``getCardByName(exact:set:completion:)`` but with async/await syntax
61 | public func getCardByName(exact: String, set: String? = nil) async throws -> Card {
62 | let request = GetCardNamed(exact: exact, set: set)
63 | return try await networkService.request(request, as: Card.self)
64 | }
65 |
66 | /// Equivalent to ``getCardByName(fuzzy:set:completion:)`` but with async/await syntax
67 | public func getCardByName(fuzzy: String, set: String? = nil) async throws -> Card {
68 | let request = GetCardNamed(fuzzy: fuzzy, set: set)
69 | return try await networkService.request(request, as: Card.self)
70 | }
71 |
72 | /// Equivalent to ``getCardNameAutocomplete(query:includeExtras:completion:)`` but with async/await syntax
73 | public func getCardNameAutocomplete(query: String, includeExtras: Bool? = nil) async throws
74 | -> Catalog
75 | {
76 | let request = GetCardAutocomplete(query: query, includeExtras: includeExtras)
77 | return try await networkService.request(request, as: Catalog.self)
78 | }
79 |
80 | /// Equivalent to ``getRandomCard(query:completion:)`` but with async/await syntax
81 | public func getRandomCard(query: String? = nil) async throws -> Card {
82 | let request = GetRandomCard(query: query)
83 | return try await networkService.request(request, as: Card.self)
84 | }
85 |
86 | /// Equivalent to ``getCard(identifier:completion:)`` but with async/await syntax
87 | public func getCard(identifier: Card.Identifier) async throws -> Card {
88 | let request = GetCard(identifier: identifier)
89 | return try await networkService.request(request, as: Card.self)
90 | }
91 |
92 | /// Equivalent to ``getCardCollection(identifiers:completion:)`` but with async/await syntax
93 | public func getCardCollection(identifiers: [Card.CollectionIdentifier]) async throws
94 | -> ObjectList
95 | {
96 | let request = GetCardCollection(identifiers: identifiers)
97 | return try await networkService.request(request, as: ObjectList.self)
98 | }
99 |
100 | /// Equivalent to ``getCatalog(catalogType:completion:)`` but with async/await syntax
101 | public func getCatalog(catalogType: Catalog.`Type`) async throws -> Catalog {
102 | let request = GetCatalog(catalogType: catalogType)
103 | return try await networkService.request(request, as: Catalog.self)
104 | }
105 |
106 | /// Equivalent to ``getSets(completion:)`` but with async/await syntax
107 | public func getSets() async throws -> ObjectList {
108 | try await networkService.request(GetSets(), as: ObjectList.self)
109 | }
110 |
111 | /// Equivalent to ``getSet(identifier:completion:)`` but with async/await syntax
112 | public func getSet(identifier: MTGSet.Identifier) async throws -> MTGSet {
113 | let request = GetSet(identifier: identifier)
114 | return try await networkService.request(request, as: MTGSet.self)
115 | }
116 |
117 | /// Equivalent to ``getRulings(_:completion:)`` but with async/await syntax
118 | public func getRulings(_ identifier: Card.Ruling.Identifier) async throws -> ObjectList<
119 | Card.Ruling
120 | > {
121 | let request = GetRulings(identifier: identifier)
122 | return try await networkService.request(request, as: ObjectList.self)
123 | }
124 |
125 | /// Equivalent to ``getSymbology(completion:)`` but with async/await syntax
126 | public func getSymbology() async throws -> ObjectList {
127 | try await networkService.request(GetSymbology(), as: ObjectList.self)
128 | }
129 |
130 | /// Equivalent to ``parseManaCost(_:completion:)`` but with async/await syntax
131 | public func parseManaCost(_ cost: String) async throws -> Card.ManaCost {
132 | let request = ParseManaCost(cost: cost)
133 | return try await networkService.request(request, as: Card.ManaCost.self)
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/FieldFilter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FieldFilter.swift
3 | //
4 |
5 | /// A comparison operator for card metadata
6 | public enum ComparisonType: String, CaseIterable, Codable {
7 | case lessThan = "<"
8 | case lessThanOrEqual = "<="
9 | case greaterThan = ">"
10 | case greaterThanOrEqual = ">="
11 | case equal = "="
12 | case notEqual = "!="
13 | case including = ":"
14 | }
15 |
16 | /// An enum representing a search filter
17 | public enum CardFieldFilter {
18 | // Colors and Identity
19 | case colors(String, ComparisonType = .including)
20 | case colorIdentity(String, ComparisonType = .including)
21 |
22 | // Types and oracle text
23 | case type(String)
24 | case oracleText(String)
25 | case fullOracleText(String)
26 | case oracleId(String)
27 | case keyword(String)
28 |
29 | // Mana costs
30 | case mana(String, ComparisonType = .equal)
31 | case devotion(String, ComparisonType = .equal)
32 | case produces(String, ComparisonType = .equal)
33 | case cmc(String, ComparisonType = .equal)
34 |
35 | // Power, toughness, and Loyalty
36 | case power(String, ComparisonType = .equal)
37 | case toughness(String, ComparisonType = .equal)
38 | case loyalty(String, ComparisonType = .equal)
39 |
40 | // Sets and Blocks
41 | case set(String)
42 | case block(String)
43 | case collectorNumber(String, ComparisonType = .equal)
44 |
45 | // Format based filters
46 | case format(String)
47 | case banned(String)
48 | case restricted(String)
49 |
50 | // Prices
51 | case usd(String, ComparisonType = .equal)
52 | case tix(String, ComparisonType = .equal)
53 | case eur(String, ComparisonType = .equal)
54 |
55 | // Artist, flavor text, and watermark
56 | case artist(String)
57 | case flavor(String)
58 | case watermark(String)
59 |
60 | // Border, Frame
61 | case border(Card.BorderColor)
62 | case frame(Card.Frame)
63 |
64 | // Tagger tags
65 | case art(String)
66 | case function(String)
67 |
68 | // Misc
69 | case name(String)
70 | case cube(String)
71 | case game(Game)
72 | case year(String, ComparisonType = .equal)
73 | case language(String)
74 | case `is`(String)
75 | case not(String)
76 | case include(String)
77 | case rarity(String, ComparisonType = .equal)
78 | case `in`(String)
79 | case compoundOr([CardFieldFilter])
80 | case compoundAnd([CardFieldFilter])
81 |
82 | /// The Scryfall syntax query string representing the filter
83 | public var filterString: String {
84 | switch self {
85 | case .fullOracleText(let value):
86 | return "fo:\(value)"
87 | case .oracleId(let id):
88 | return "oracleid:\(id)"
89 | case .keyword(let value):
90 | return "keyword:\(value)"
91 | case .compoundOr(let filters):
92 | return "(\(filters.map { $0.filterString }.joined(separator: " or ")))"
93 | case .compoundAnd(let filters):
94 | return "(\(filters.map { $0.filterString }.joined(separator: " and ")))"
95 | case .name(let value):
96 | return "name:\(value)"
97 | case .colors(let value, let comparison):
98 | return "color\(comparison.rawValue)\(value)"
99 | case .colorIdentity(let value, let comparison):
100 | return "identity\(comparison.rawValue)\(value)"
101 | case .type(let value):
102 | return "type:\(value)"
103 | case .oracleText(let value):
104 | return "oracle:\(value)"
105 | case .mana(let value, let comparison):
106 | return "mana\(comparison.rawValue)\(value)"
107 | case .devotion(let value, let comparison):
108 | return "devotion\(comparison.rawValue)\(value)"
109 | case .produces(let value, let comparison):
110 | return "produces\(comparison.rawValue)\(value)"
111 | case .cmc(let value, let comparison):
112 | return "cmc\(comparison.rawValue)\(value)"
113 | case .power(let value, let comparison):
114 | return "power\(comparison.rawValue)\(value)"
115 | case .toughness(let value, let comparison):
116 | return "toughness\(comparison.rawValue)\(value)"
117 | case .loyalty(let value, let comparison):
118 | return "loyalty\(comparison.rawValue)\(value)"
119 | case .set(let value):
120 | return "set:\(value)"
121 | case .block(let value):
122 | return "block:\(value)"
123 | case .collectorNumber(let value, let comparison):
124 | return "collectorNumber\(comparison.rawValue)\(value)"
125 | case .format(let value):
126 | return "format:\(value)"
127 | case .banned(let value):
128 | return "banned:\(value)"
129 | case .restricted(let value):
130 | return "restricted:\(value)"
131 | case .usd(let value, let comparison):
132 | return "usd\(comparison.rawValue)\(value)"
133 | case .tix(let value, let comparison):
134 | return "tix\(comparison.rawValue)\(value)"
135 | case .eur(let value, let comparison):
136 | return "eur\(comparison.rawValue)\(value)"
137 | case .artist(let value):
138 | return "artist:\(value)"
139 | case .flavor(let value):
140 | return "flavor:\(value)"
141 | case .watermark(let value):
142 | return "watermark:\(value)"
143 | case .border(let value):
144 | return "border:\(value)"
145 | case .frame(let value):
146 | return "frame:\(value)"
147 | case .art(let value):
148 | return "art:\(value)"
149 | case .function(let value):
150 | return "function:\(value)"
151 | case .cube(let value):
152 | return "cube:\(value)"
153 | case .game(let value):
154 | return "game:\(value)"
155 | case .year(let value, let comparison):
156 | return "year\(comparison.rawValue)\(value)"
157 | case .language(let value):
158 | return "language:\(value)"
159 | case .is(let value):
160 | return "is:\(value)"
161 | case .not(let value):
162 | return "not:\(value)"
163 | case .include(let value):
164 | return "include:\(value)"
165 | case .rarity(let value, let comparison):
166 | return "rarity\(comparison.rawValue)\(value)"
167 | case .in(let value):
168 | return "in:\(value)"
169 | }
170 | }
171 |
172 | /// The Scryfall syntax query string representing the filter, negated
173 | public var negated: String {
174 | "-\(filterString)"
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Tests/ScryfallKitTests/SmokeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SmokeTests.swift
3 | //
4 |
5 | import XCTest
6 |
7 | @testable import ScryfallKit
8 |
9 | @available(iOS 13.0.0, *)
10 | final class SmokeTests: XCTestCase {
11 | var client: ScryfallClient!
12 |
13 | override func setUp() {
14 | self.client = ScryfallClient()
15 | }
16 |
17 | func testLayouts() async throws {
18 | // Verify that we can handle all layout types
19 | // Skip double sided because there aren't any double_sided or battle cards being returned by Scryfall
20 | for layout in Card.Layout.allCases where ![.doubleSided, .unknown, .battle].contains(layout) {
21 | let cards = try await client.searchCards(query: "layout:\(layout.rawValue)")
22 | checkForUnknowns(in: cards.data)
23 | }
24 | }
25 |
26 | func testTransformers() async throws {
27 | _ = try await client.getCardByName(fuzzy: "optimus prime hero")
28 | }
29 |
30 | func testSearchCardsWithFilters() async throws {
31 | let filters: [CardFieldFilter] = [.cmc("3", .greaterThan), .colorIdentity("WU")]
32 | _ = try await client.searchCards(filters: filters)
33 | }
34 |
35 | func testSearchCards() async throws {
36 | _ = try await client.searchCards(query: "Sigarda")
37 | }
38 |
39 | func testSearchCardsMultiplePages() async throws {
40 | let query = "a" // Some broad query that will return multiple pages
41 | let firstPage = try await client.searchCards(query: query)
42 | let secondPage = try await client.searchCards(query: query, page: 2)
43 |
44 | XCTAssertNotEqual(firstPage.data[0].name, secondPage.data[0].name)
45 | }
46 |
47 | func testGetCardByExactName() async throws {
48 | _ = try await client.getCardByName(exact: "Narset, Enlightened Master")
49 | }
50 |
51 | func testGetCardByFuzzyName() async throws {
52 | _ = try await client.getCardByName(fuzzy: "Narset, Master")
53 | }
54 |
55 | func testGetCardNameAutocomplete() async throws {
56 | let results = try await client.getCardNameAutocomplete(query: "Nars")
57 | XCTAssertFalse(results.data.isEmpty)
58 | }
59 |
60 | func testGetRandomCard() async throws {
61 | _ = try await client.getRandomCard()
62 | }
63 |
64 | func testGetCardById() async throws {
65 | // Flumph
66 | let identifier = Card.Identifier.scryfallID(id: "cdc86e78-8911-4a0d-ba3a-7802f8d991ef")
67 | _ = try await client.getCard(identifier: identifier)
68 | }
69 |
70 | func testGetCatalog() async throws {
71 | _ = try await client.getCatalog(catalogType: .cardNames)
72 | }
73 |
74 | func testGetSets() async throws {
75 | _ = try await client.getSets()
76 | }
77 |
78 | func testGetSetByCode() async throws {
79 | let identifier = MTGSet.Identifier.code(code: "afr")
80 | _ = try await client.getSet(identifier: identifier)
81 | }
82 |
83 | func testGetSet() async throws {
84 | // Ultimate Masters
85 | let identifier = MTGSet.Identifier.scryfallID(id: "2ec77b94-6d47-4891-a480-5d0b4e5c9372")
86 | _ = try await client.getSet(identifier: identifier)
87 | }
88 |
89 | func testGetRulings() async throws {
90 | let identifier = Card.Ruling.Identifier.scryfallID(id: "cdc86e78-8911-4a0d-ba3a-7802f8d991ef")
91 | _ = try await client.getRulings(identifier)
92 | }
93 |
94 | func testGetSymbology() async throws {
95 | _ = try await client.getSymbology()
96 | }
97 |
98 | func testParseManaCost() async throws {
99 | _ = try await client.parseManaCost("{X}{W}{U}{R}")
100 | }
101 |
102 | func testSearchWithFieldFilters() async throws {
103 | let filters: [CardFieldFilter] = [
104 | CardFieldFilter.type("forest"),
105 | CardFieldFilter.type("creature"),
106 | ]
107 | let cards = try await client.searchCards(filters: filters)
108 |
109 | XCTAssertEqual(cards.totalCards, 1)
110 | }
111 |
112 | func testSearchWithFieldFiltersWithComparison() async throws {
113 | let filters: [CardFieldFilter] = [
114 | CardFieldFilter.cmc("0", .lessThanOrEqual),
115 | CardFieldFilter.type("Creature"),
116 | CardFieldFilter.colors("0", .equal),
117 | ]
118 |
119 | let cards = try await client.searchCards(filters: filters)
120 | XCTAssert(cards.totalCards ?? 0 > 1)
121 | }
122 |
123 | func testSearchWithCompoundFieldFilters() async throws {
124 | let filters: [CardFieldFilter] = [
125 | CardFieldFilter.type("forest"),
126 | CardFieldFilter.type("creature"),
127 | ]
128 |
129 | let compoundFilter = CardFieldFilter.compoundOr(filters)
130 |
131 | let cards = try await client.searchCards(filters: [compoundFilter])
132 | XCTAssert(cards.totalCards ?? 0 > 1)
133 | }
134 |
135 | func testGetCardCollection() async throws {
136 | let identifiers: [Card.CollectionIdentifier] = [
137 | .scryfallID(id: "683a5707-cddb-494d-9b41-51b4584ded69"),
138 | .name("Ancient Tomb"),
139 | .collectorNoAndSet(collectorNo: "150", set: "mrd"),
140 | ]
141 |
142 | _ = try await client.getCardCollection(identifiers: identifiers)
143 | }
144 |
145 | func testAllNewCards() async throws {
146 | // Get sets that released in the past 30 days
147 | let sets = try await client.getSets().data.filter { mtgSet in
148 | guard let date = mtgSet.date else {
149 | print("Couldn't get release date for set: \(mtgSet.name)")
150 | return false
151 | }
152 |
153 | let distanceInSeconds = date.distance(to: Date())
154 | let distanceInDays = distanceInSeconds / 60 / 60 / 24
155 |
156 | return distanceInDays < 30
157 | }
158 |
159 | // Filter for cards that are in any of the sets
160 | let filter = CardFieldFilter.compoundOr(sets.map { .set($0.code) })
161 |
162 | // Search
163 | var results = try await client.searchCards(filters: [filter], unique: .prints)
164 | checkForUnknowns(in: results.data)
165 | var page = 1
166 |
167 | // Go through every page
168 | while results.hasMore ?? false {
169 | page += 1
170 | results = try await client.searchCards(filters: [filter], page: page)
171 | checkForUnknowns(in: results.data)
172 | usleep(500000) // Wait for 0.5 seconds
173 | }
174 | }
175 |
176 | private func checkForUnknowns(in cards: [Card]) {
177 | for card in cards {
178 | XCTAssertNotEqual(card.layout, .unknown, "Unknown layout on \(card.name)")
179 | XCTAssertNotEqual(card.setType, .unknown, "Unknown set type on \(card.name)")
180 | if let frameEffects = card.frameEffects {
181 | for effect in frameEffects {
182 | XCTAssertNotEqual(effect, .unknown, "Unknown frame effect on \(card.name) [\(card.set)]")
183 | }
184 | }
185 | }
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card+enums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card+enums.swift
3 | //
4 |
5 | import Foundation
6 | import OSLog
7 |
8 | extension Card {
9 | /// A value or combination of values that uniquely identify a Magic card
10 | public enum Identifier {
11 | case scryfallID(id: String)
12 | case mtgoID(id: Int)
13 | case multiverseID(id: Int)
14 | case arenaID(id: Int)
15 | case tcgPlayerID(id: Int)
16 | case cardMarketID(id: Int)
17 | case setCodeCollectorNo(setCode: String, collectorNo: String, lang: String? = nil)
18 |
19 | /// The name of the service that the identifer is linked to
20 | var provider: String {
21 | switch self {
22 | case .mtgoID:
23 | return "mtgo"
24 | case .multiverseID:
25 | return "multiverse"
26 | case .arenaID:
27 | return "arena"
28 | case .tcgPlayerID:
29 | return "tcgplayer"
30 | case .cardMarketID:
31 | return "cardmarket"
32 | default:
33 | return "scryfall"
34 | }
35 | }
36 |
37 | /// The id value of the identifier, if present. Only not present for set code + collector number
38 | var id: String? {
39 | switch self {
40 | case .scryfallID(let id):
41 | return id
42 | case .mtgoID(let id):
43 | return String(id)
44 | case .multiverseID(let id):
45 | return String(id)
46 | case .arenaID(let id):
47 | return String(id)
48 | case .tcgPlayerID(let id):
49 | return String(id)
50 | case .cardMarketID(let id):
51 | return String(id)
52 | default:
53 | return nil
54 | }
55 | }
56 | }
57 |
58 | /// A value or combination of values that uniquely identifies a Magic card for the purposes of retrieving a collection of cards.
59 | public enum CollectionIdentifier {
60 | case scryfallID(id: String)
61 | case mtgoID(id: Int)
62 | case multiverseID(id: Int)
63 | case oracleID(id: String)
64 | case illustrationID(id: String)
65 | case name(_: String)
66 | case nameAndSet(name: String, set: String)
67 | case collectorNoAndSet(collectorNo: String, set: String)
68 |
69 | var json: [String: String] {
70 | switch self {
71 | case .scryfallID(let id):
72 | return ["id": id]
73 | case .mtgoID(let id):
74 | return ["mtgo_id": "\(id)"]
75 | case .multiverseID(let id):
76 | return ["multiverse_id": "\(id)"]
77 | case .oracleID(let id):
78 | return ["oracle_id": id]
79 | case .illustrationID(let id):
80 | return ["illustration_id": id]
81 | case .name(let name):
82 | return ["name": name]
83 | case .nameAndSet(let name, let set):
84 | return ["name": name, "set": set]
85 | case .collectorNoAndSet(let collectorNo, let set):
86 | return ["collector_number": collectorNo, "set": set]
87 | }
88 | }
89 | }
90 |
91 | /// Finishes for a printed card
92 | public enum Finish: String, Codable, CaseIterable, Sendable {
93 | case nonfoil, foil, etched, glossy
94 | }
95 |
96 | /// Status of Scryfall's image asset for this card
97 | ///
98 | /// [Scryfall documentation](https://scryfall.com/docs/api/images#image-statuses)
99 | public enum ImageStatus: String, Codable, CaseIterable, Sendable {
100 | case missing, placeholder, lowres
101 | case highresScan = "highres_scan"
102 | }
103 |
104 | /// Types of images provided by Scryfall
105 | ///
106 | /// [Scryfall documentation](https://scryfall.com/docs/api/images)
107 | public enum ImageType: String, Codable, CaseIterable {
108 | case png, large, normal, small
109 | case artCrop = "art_crop"
110 | case borderCrop = "border_crop"
111 | }
112 |
113 | /// Card rarities
114 | public enum Rarity: String, Codable, CaseIterable, Comparable, Sendable {
115 | case common, uncommon, rare, special, mythic, bonus
116 |
117 | /// Order according to Scryfall
118 | public static func < (lhs: Card.Rarity, rhs: Card.Rarity) -> Bool {
119 | let order: [Card.Rarity] = [.bonus, .special, .common, .uncommon, .rare, .mythic]
120 | return order.firstIndex(of: lhs)! < order.firstIndex(of: rhs)!
121 | }
122 | }
123 |
124 | /// Layouts for a Magic card
125 | ///
126 | /// [Scryfall documentation](https://scryfall.com/docs/api/layouts)
127 | public enum Layout: String, CaseIterable, Codable, Sendable {
128 | case normal, split, flip, transform, meld, leveler, saga, adventure, planar, scheme, vanguard,
129 | token, emblem, augment, host, `class`, battle, `case`, mutate, prototype, unknown
130 | case modalDfc = "modal_dfc"
131 | case doubleSided = "double_sided"
132 | case doubleFacedToken = "double_faced_token"
133 | case artSeries = "art_series"
134 | case reversibleCard = "reversible_card"
135 |
136 | /// Codable initializer
137 | ///
138 | /// If this initializer fails to decode a value, instead of throwing an error, it will decode as the ``ScryfallKit/Card/Layout-swift.enum/unknown`` type and print a message to the logs.
139 | /// - Parameter decoder: The Decoder to try decoding a ``ScryfallKit/Card/Layout-swift.enum`` from
140 | public init(from decoder: Decoder) throws {
141 | self = (try? Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))) ?? .unknown
142 | if self == .unknown, let rawValue = try? String(from: decoder) {
143 | if #available(iOS 14.0, macOS 11.0, *) {
144 | Logger.decoder.error("Decoded unknown Layout: \(rawValue)")
145 | } else {
146 | print("Decoded unknown Layout: \(rawValue)")
147 | }
148 | }
149 | }
150 | }
151 |
152 | /// Machine-readable strings representing a card's legality in different formats
153 | public enum Legality: String, Codable, CaseIterable, Hashable, Sendable {
154 | /// This card is legal to be played in this format
155 | case legal
156 | /// This card is restricted in this format (players may only have one copy in their deck)
157 | case restricted
158 | /// This card has been banned in this format
159 | case banned
160 | /// This card is not legal in this format (ex: an uncommon is not legal in pauper)
161 | case notLegal = "not_legal"
162 |
163 | public var label: String {
164 | switch self {
165 | case .notLegal:
166 | return "Not Legal"
167 | default:
168 | return rawValue.capitalized
169 | }
170 | }
171 | }
172 |
173 | /// A string representing one of the colors (and colorless) in Magic
174 | public enum Color: String, Codable, CaseIterable, Comparable, Sendable {
175 | // swiftlint:disable:next identifier_name
176 | case W, U, B, R, G, C
177 |
178 | public static func < (lhs: Color, rhs: Color) -> Bool {
179 | let order: [Color] = [.W, .U, .B, .R, .G, .C]
180 | return order.firstIndex(of: lhs)! < order.firstIndex(of: rhs)!
181 | }
182 | }
183 |
184 | /// Card border colors
185 | public enum BorderColor: String, Codable, CaseIterable, Sendable {
186 | case black, borderless, gold, silver, white, yellow
187 | }
188 |
189 | /// Card frames
190 | ///
191 | /// [Scryfall documentation](https://scryfall.com/docs/api/frames)
192 | public enum Frame: String, Codable, CaseIterable, Sendable {
193 | case v1993 = "1993"
194 | case v1997 = "1997"
195 | case v2003 = "2003"
196 | case v2015 = "2015"
197 | case future
198 | }
199 |
200 | /// Effects applied to a Magic card frame
201 | ///
202 | /// [Scryfall documentation](https://scryfall.com/docs/api/frames#frame-effects)
203 | public enum FrameEffect: String, Codable, CaseIterable, Sendable {
204 | case legendary, miracle, nyxtouched, draft, devoid, tombstone, colorshifted, inverted,
205 | sunmoondfc, compasslanddfc, originpwdfc, mooneldrazidfc, waxingandwaningmoondfc, showcase,
206 | extendedart, companion, etched, snow, lesson, convertdfc, fandfc, battle, gravestone, fullart,
207 | vehicle, borderless, extended, spree, textless, unknown, enchantment, shatteredglass, upsidedowndfc
208 |
209 | public init(from decoder: Decoder) throws {
210 | self =
211 | try FrameEffect(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
212 | if self == .unknown, let rawValue = try? String(from: decoder) {
213 | if #available(iOS 14.0, macOS 11.0, *) {
214 | Logger.decoder.error("Decoded unknown FrameEffect: \(rawValue)")
215 | } else {
216 | print("Decoded unknown FrameEffect: \(rawValue)")
217 | }
218 | }
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/ScryfallClient.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScryfallClient.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// A client for interacting with the Scryfall API
8 | public final class ScryfallClient: Sendable {
9 | private let networkLogLevel: NetworkLogLevel
10 | let networkService: NetworkServiceProtocol
11 |
12 | /// Initialize an instance of the ScryfallClient
13 | /// - Parameter networkLogLevel: The desired logging level. See ``NetworkLogLevel``
14 | public init(networkLogLevel: NetworkLogLevel = .minimal) {
15 | self.networkLogLevel = networkLogLevel
16 | self.networkService = NetworkService(logLevel: networkLogLevel)
17 | }
18 |
19 | /// Perform a search using an array of ``CardFieldFilter`` objects.
20 | ///
21 | /// Performs a Scryfall search using the `/cards/search` endpoint. This method is simply a convenience wrapper around ``searchCards(query:unique:order:sortDirection:includeExtras:includeMultilingual:includeVariations:page:)``
22 | ///
23 | /// Full reference at: https://scryfall.com/docs/api/cards/search.
24 | ///
25 | /// - Parameters:
26 | /// - filters: Only include cards matching these filters
27 | /// - unique: The strategy for omitting similar cards. See ``UniqueMode``
28 | /// - order: The method to sort returned cards. See ``SortMode``
29 | /// - sortDirection: The direction to sort cards. See ``SortDirection``
30 | /// - includeExtras: If true, extra cards (tokens, planes, etc) will be included. Equivalent to adding include:extras to the fulltext search. Defaults to `false`
31 | /// - includeMultilingual: If true, cards in every language supported by Scryfall will be included. Defaults to `false`.
32 | /// - includeVariations: If true, rare care variants will be included, like the Hairy Runesword. Defaults to `false`.
33 | /// - page: The page number to return. Defaults to `1`
34 | /// - completion: A function/block to be called when the search is complete
35 | public func searchCards(
36 | filters: [CardFieldFilter],
37 | unique: UniqueMode? = nil,
38 | order: SortMode? = nil,
39 | sortDirection: SortDirection? = nil,
40 | includeExtras: Bool? = nil,
41 | includeMultilingual: Bool? = nil,
42 | includeVariations: Bool? = nil,
43 | page: Int? = nil,
44 | completion: @Sendable @escaping (Result, Error>) -> Void
45 | ) {
46 | let query = filters.map { $0.filterString }.joined(separator: " ")
47 | searchCards(
48 | query: query,
49 | unique: unique,
50 | order: order,
51 | sortDirection: sortDirection,
52 | includeExtras: includeExtras,
53 | includeMultilingual: includeMultilingual,
54 | includeVariations: includeVariations,
55 | page: page,
56 | completion: completion)
57 | }
58 |
59 | /// Perform a search using a string conforming to Scryfall query syntax.
60 | ///
61 | /// Full reference at: https://scryfall.com/docs/api/cards/search.
62 | ///
63 | /// - Parameters:
64 | /// - filters: Only include cards matching these filters
65 | /// - unique: The strategy for omitting similar cards. See ``UniqueMode``
66 | /// - order: The method to sort returned cards. See ``SortMode``
67 | /// - sortDirection: The direction to sort cards. See ``SortDirection``
68 | /// - includeExtras: If true, extra cards (tokens, planes, etc) will be included. Equivalent to adding include:extras to the fulltext search. Defaults to `false`
69 | /// - includeMultilingual: If true, cards in every language supported by Scryfall will be included. Defaults to `false`.
70 | /// - includeVariations: If true, rare care variants will be included, like the Hairy Runesword. Defaults to `false`.
71 | /// - page: The page number to return. Defaults to `1`
72 | /// - completion: A function/block to be called when the search is complete
73 | public func searchCards(
74 | query: String,
75 | unique: UniqueMode? = nil,
76 | order: SortMode? = nil,
77 | sortDirection: SortDirection? = nil,
78 | includeExtras: Bool? = nil,
79 | includeMultilingual: Bool? = nil,
80 | includeVariations: Bool? = nil,
81 | page: Int? = nil,
82 | completion: @Sendable @escaping (Result, Error>) -> Void
83 | ) {
84 |
85 | let request = SearchCards(
86 | query: query,
87 | unique: unique,
88 | order: order,
89 | dir: sortDirection,
90 | includeExtras: includeExtras,
91 | includeMultilingual: includeMultilingual,
92 | includeVariations: includeVariations,
93 | page: page)
94 |
95 | networkService.request(request, as: ObjectList.self, completion: completion)
96 | }
97 |
98 | /// Get a card with the exact name supplied
99 | ///
100 | /// Full reference at: https://scryfall.com/docs/api/cards/named
101 | ///
102 | /// - Parameters:
103 | /// - exact: The exact card name to search for, case insenstive.
104 | /// - set: A set code to limit the search to one set.
105 | /// - completion: A function/block to be called when the search is complete
106 | public func getCardByName(
107 | exact: String, set: String? = nil, completion: @Sendable @escaping (Result) -> Void
108 | ) {
109 | let request = GetCardNamed(exact: exact, set: set)
110 | networkService.request(request, as: Card.self, completion: completion)
111 | }
112 |
113 | /// Get a card with a name close to what was entered
114 | ///
115 | /// Full reference at: https://scryfall.com/docs/api/cards/named
116 | ///
117 | /// - Parameters:
118 | /// - fuzzy: The exact card name to search for, case insenstive.
119 | /// - set: A set code to limit the search to one set.
120 | /// - completion: A function/block to be called when the search is complete
121 | public func getCardByName(
122 | fuzzy: String, set: String? = nil, completion: @Sendable @escaping (Result) -> Void
123 | ) {
124 | let request = GetCardNamed(fuzzy: fuzzy, set: set)
125 | networkService.request(request, as: Card.self, completion: completion)
126 | }
127 |
128 | /// Retrieve up to 20 card name autocomplete suggestions for a given string.
129 | ///
130 | /// Full reference at: https://scryfall.com/docs/api/cards/autocomplete
131 | ///
132 | /// - Parameters:
133 | /// - query: The string to autocomplete
134 | /// - includeExtras: If true, extra cards (tokens, planes, vanguards, etc) will be included. Defaults to false.
135 | /// - completion: A function/block to be called when the search is complete
136 | /// - Returns: A ``Catalog`` of card names or an error
137 | public func getCardNameAutocomplete(
138 | query: String, includeExtras: Bool? = nil,
139 | completion: @Sendable @escaping (Result) -> Void
140 | ) {
141 | let request = GetCardAutocomplete(query: query, includeExtras: includeExtras)
142 | networkService.request(request, as: Catalog.self, completion: completion)
143 | }
144 |
145 | /// Get a single random card
146 | ///
147 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards/random)
148 | ///
149 | /// - Parameters:
150 | /// - query: An optional fulltext search query to filter the pool of random cards.
151 | /// - completion: A function/block to call when the request is complete
152 | public func getRandomCard(
153 | query: String? = nil, completion: @Sendable @escaping (Result) -> Void
154 | ) {
155 | let request = GetRandomCard(query: query)
156 | networkService.request(request, as: Card.self, completion: completion)
157 | }
158 |
159 | /// Get a single card using a Card identifier.
160 | ///
161 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards)
162 | ///
163 | /// See ``Card/Identifier`` for more information on identifiers
164 | ///
165 | /// - Parameters:
166 | /// - identifier: The identifier for the desired card
167 | /// - completion: A function/block to call when the request is complete
168 | public func getCard(
169 | identifier: Card.Identifier, completion: @Sendable @escaping (Result) -> Void
170 | ) {
171 | let request = GetCard(identifier: identifier)
172 | networkService.request(request, as: Card.self, completion: completion)
173 | }
174 |
175 | /// Bulk request up to 75 cards at a time.
176 | ///
177 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards/collection)
178 | ///
179 | /// - Parameters:
180 | /// - identifiers: The array of identifiers
181 | /// - completion: A function/block to call when the request is complete
182 | public func getCardCollection(
183 | identifiers: [Card.CollectionIdentifier],
184 | completion: @Sendable @escaping (Result, Error>) -> Void
185 | ) {
186 | let request = GetCardCollection(identifiers: identifiers)
187 | networkService.request(request, as: ObjectList.self, completion: completion)
188 | }
189 |
190 | /// Get a catalog of Magic datapoints (keyword abilities, artist names, spell types, etc)
191 | ///
192 | /// [Scryfall documentation](https://scryfall.com/docs/api/catalogs)
193 | ///
194 | /// - Parameters:
195 | /// - catalogType: The type of catalog to retrieve
196 | /// - completion: A function/block to call when the request is complete
197 | public func getCatalog(
198 | catalogType: Catalog.`Type`, completion: @Sendable @escaping (Result) -> Void
199 | ) {
200 | let request = GetCatalog(catalogType: catalogType)
201 | networkService.request(request, as: Catalog.self, completion: completion)
202 | }
203 |
204 | /// Get all MTG sets
205 | ///
206 | /// [Scryfall documentation](https://scryfall.com/docs/api/sets/all)
207 | ///
208 | /// - Parameter completion: A function/block to call when the request is complete
209 | public func getSets(completion: @Sendable @escaping (Result, Error>) -> Void) {
210 | networkService.request(GetSets(), as: ObjectList.self, completion: completion)
211 | }
212 |
213 | /// Get a specific MTG set
214 | ///
215 | /// [Scryfall documentation](https://scryfall.com/docs/api/sets)
216 | ///
217 | /// See ``MTGSet/Identifier`` for more information on set identifiers
218 | ///
219 | /// - Parameters:
220 | /// - identifier: The set's identifier
221 | /// - completion: A function/block to call when the request is complete
222 | public func getSet(
223 | identifier: MTGSet.Identifier, completion: @Sendable @escaping (Result) -> Void
224 | ) {
225 | let request = GetSet(identifier: identifier)
226 | networkService.request(request, as: MTGSet.self, completion: completion)
227 | }
228 |
229 | /// Get the rulings for a specific card.
230 | ///
231 | /// [Scryfall documentation](https://scryfall.com/docs/api/rulings)
232 | ///
233 | /// See ``Card/Ruling/Identifier`` for more information on ruling identifiers
234 | ///
235 | /// - Parameters:
236 | /// - identifier: An identifier for the ruling you wish to retrieve
237 | /// - completion: A function/block to call when the request is complete
238 | public func getRulings(
239 | _ identifier: Card.Ruling.Identifier,
240 | completion: @Sendable @escaping (Result, Error>) -> Void
241 | ) {
242 | let request = GetRulings(identifier: identifier)
243 | networkService.request(request, as: ObjectList.self, completion: completion)
244 | }
245 |
246 | /// Get all MTG symbology
247 | ///
248 | /// [Scryfall documentation](https://scryfall.com/docs/api/card-symbols/all)
249 | ///
250 | /// - Parameter completion: A function/block to call when the request is complete
251 | public func getSymbology(
252 | completion: @Sendable @escaping (Result, Error>) -> Void
253 | ) {
254 | networkService.request(GetSymbology(), as: ObjectList.self, completion: completion)
255 | }
256 |
257 | /// Parse a string representing a mana cost and retun Scryfall's interpretation
258 | ///
259 | /// [Scryfall documentation](https://scryfall.com/docs/api/card-symbols/parse-mana)
260 | ///
261 | /// - Parameters:
262 | /// - cost: The string to parse
263 | /// - completion: A function/block to call when the request is complete
264 | public func parseManaCost(
265 | _ cost: String, completion: @Sendable @escaping (Result) -> Void
266 | ) {
267 | let request = ParseManaCost(cost: cost)
268 | networkService.request(request, as: Card.ManaCost.self, completion: completion)
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/Sources/ScryfallKit/Models/Card/Card.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Card.swift
3 | //
4 |
5 | import Foundation
6 |
7 | /// A Magic: the Gathering card
8 | ///
9 | /// [Scryfall documentation](https://scryfall.com/docs/api/cards)
10 | public struct Card: Codable, Identifiable, Hashable, Sendable {
11 | // MARK: Core fields
12 | /// The identifier of this card on Scryfall
13 | public var id: UUID
14 | /// The identifier of this card on MTG Arena
15 | public var arenaId: Int?
16 | /// The identifier of this card on MTG Online
17 | public var mtgoId: Int?
18 | /// The identifier of the foil version of this card on MTG Online
19 | public var mtgoFoilId: Int?
20 | /// The identifiers of this card on Wizards of the Coast's [Gatherer](https://gatherer.wizards.com/Pages/Default.aspx)
21 | public var multiverseIds: [Int]?
22 | /// The identifier of this card on TCGPlayer
23 | public var tcgplayerId: Int?
24 | /// The identifier of the etched version of this card on TCGPlayer
25 | public var tcgplayerEtchedId: Int?
26 | /// The identifier of this card on Card Market
27 | public var cardMarketId: Int?
28 | /// The identifier for this card’s oracle identity.
29 | ///
30 | /// For more information on this property, see [Scryfall's documentation](https://scryfall.com/docs/api/cards#core-card-fields)
31 | /// - Note: Scryfall's documentation lists this as required but it's nil for reversible cards
32 | public var oracleId: String?
33 | /// The language that this specific printing was printed in
34 | public var lang: String
35 | /// A link to where you can begin paginating all re/prints for this card on Scryfall’s API.
36 | public var printsSearchUri: String
37 | /// A link to this card’s rulings list on Scryfall’s API.
38 | public var rulingsUri: String
39 | /// A link to this card’s permapage on Scryfall’s website.
40 | public var scryfallUri: String
41 | /// A link to this card object on Scryfall’s API.
42 | public var uri: String
43 |
44 | // MARK: Gameplay fields
45 | /// An array of related cards (tokens, meld parts, etc) or nil if there are none
46 | public var allParts: [RelatedCard]?
47 | /// An array of all the faces that a card has or nil if it's a single-faced card
48 | public var cardFaces: [Face]?
49 | /// The converted mana cost of a card, now called the "mana value"
50 | /// - Note: Scryfall's documentation lists this as required but it's nil for reversible cards
51 | public var cmc: Double?
52 | /// An array of colors representing this card's color identity.
53 | ///
54 | /// Color identity is used to determine what cards are legal in a Commander deck. See the [comprehensive rules](https://magic.wizards.com/en/rules) for more information
55 | public var colorIdentity: [Color]
56 | /// An array of the colors in this card’s color indicator or nil if it doesn't have one
57 | ///
58 | /// Color indicators are used to specify the color of a card that has no mana symbols
59 | public var colorIndicator: [Color]?
60 | /// An array of the colors in this card's mana cost
61 | public var colors: [Color]?
62 | /// This card’s overall rank/popularity on EDHREC. Not all cards are ranked.
63 | public var edhrecRank: Int?
64 | /// This card’s hand modifier, if it is Vanguard card. This value will contain a delta, such as -1.
65 | public var handModifier: String?
66 | /// An array of the keywords on this card (deathouch, first strike, etc)
67 | public var keywords: [String]
68 | /// This card's layout (normal, transform, split, etc)
69 | public var layout: Layout
70 | /// The formats that this card is legal in
71 | public var legalities: Legalities
72 | /// This card’s life modifier, if it is Vanguard card. This value will contain a delta, such as +2.
73 | public var lifeModifier: String?
74 | /// This card's starting loyalty counters if it's a planeswalker
75 | public var loyalty: String?
76 | /// The mana cost for this card.
77 | ///
78 | /// This value will be any empty string "" if the cost is absent. Remember that per the game rules, a missing mana cost and a mana cost of {0} are different values.
79 | public var manaCost: String?
80 | /// The name of this card
81 | ///
82 | /// If the card has multiple faces the names will be separated by a "//" such as "Wear // Tear"
83 | public var name: String
84 | /// The oracle text for this card
85 | public var oracleText: String?
86 | /// True if this card is an oversized card
87 | public var oversized: Bool
88 | /// The power of this card if it's a creature
89 | public var power: String?
90 | /// The colors of mana that this card _could_ produce
91 | public var producedMana: [Color]?
92 | /// True if this card is on the Reserved List
93 | public var reserved: Bool
94 | /// The toughness of this card if it's a creature
95 | public var toughness: String?
96 | /// This card's types, separated by a space
97 | /// - Note: Tokens don't have type lines
98 | public var typeLine: String?
99 |
100 | // MARK: Print fields
101 | /// The name of the artist who illustrated this card
102 | public var artist: String?
103 | /// True if this card was printed in booster packs
104 | public var booster: Bool
105 | /// The color of this card's border
106 | public var borderColor: BorderColor
107 | /// The Scryfall ID for the card back design present on this card.
108 | /// - Note: Scryfall's documentation lists this as required but it's nil for multi-faced cards
109 | public var cardBackId: UUID?
110 | /// This card's collector number
111 | public var collectorNumber: String
112 | /// True if you should consider avoiding use of this print downstream
113 | ///
114 | /// [Comment from Scryfall's blog](https://scryfall.com/blog/regarding-wotc-s-recent-statement-on-depictions-of-racism-220)
115 | public var contentWarning: Bool?
116 | /// Whether this card has been released digitally
117 | public var digital: Bool
118 | /// The different finishes that this card was printed in
119 | public var finishes: [Card.Finish]
120 | /// The just-for-fun name printed on the card (such as for Godzilla series cards).
121 | public var flavorName: String?
122 | /// This card's flavor text if any
123 | public var flavorText: String?
124 | /// The frame effects if any
125 | public var frameEffects: [FrameEffect]?
126 | /// The type of frame this card was printed with
127 | public var frame: Frame
128 | /// True if this card's art is larger than normal
129 | public var fullArt: Bool
130 | /// An array of the games this card has been released in
131 | public var games: [Game]
132 | /// True if Scryfall has a high-res image of this card
133 | public var highresImage: Bool
134 | /// An ID for this card's art that remains consistent across reprints
135 | public var illustrationId: UUID?
136 | /// A computer-readable indicator for the state of this card’s image
137 | public var imageStatus: ImageStatus
138 | /// An object listing available imagery for this card.
139 | public var imageUris: ImageUris?
140 | /// An object containing daily price information for this card
141 | public var prices: Prices
142 | /// The localized name printed on this card, if any.
143 | public var printedName: String?
144 | /// The localized text printed on this card, if any.
145 | public var printedText: String?
146 | /// The localized type line printed on this card, if any.
147 | public var printedTypeLine: String?
148 | /// True if this card is a promotional printing
149 | public var promo: Bool
150 | /// An array of strings describing what categories of promo cards this card falls into.
151 | public var promoTypes: [String]?
152 | /// A dictionary of URIs to this card’s listing on major marketplaces.
153 | public var purchaseUris: [String: String]?
154 | /// This card's rarity
155 | public var rarity: Rarity
156 | /// A dictionary of links to other MTG resources
157 | public var relatedUris: [String: String]
158 | /// This card's release date
159 | public var releasedAt: String
160 | /// True if this card has been printed before
161 | public var reprint: Bool
162 | /// Link to this card's set on Scryfall
163 | public var scryfallSetUri: String
164 | /// This card's full set name
165 | public var setName: String
166 | /// A link to this card's set on Scryfall
167 | public var setSearchUri: URL
168 | /// The type of set this card was printed in
169 | public var setType: MTGSet.`Type`
170 | /// A link to this card's set object on the Scryfall API
171 | public var setUri: String
172 | /// This card's set code
173 | public var set: String
174 | /// True if this was a story spotlight card
175 | public var storySpotlight: Bool
176 | /// True if this card doesn't have any text on it
177 | public var textless: Bool
178 | /// True if this card is a variation of another printing
179 | public var variation: Bool
180 | /// The id of the card this card is a variation of
181 | public var variationOf: UUID?
182 | /// This card's watermark, if any
183 | public var watermark: String?
184 | /// An object with information on when this card was previewed and by whom
185 | public var preview: Preview?
186 |
187 | // swiftlint:disable function_body_length
188 | public init(
189 | arenaId: Int? = nil,
190 | mtgoId: Int? = nil,
191 | mtgoFoilId: Int? = nil,
192 | multiverseIds: [Int]? = nil,
193 | tcgplayerId: Int? = nil,
194 | tcgplayerEtchedId: Int? = nil,
195 | cardMarketId: Int? = nil,
196 | id: UUID,
197 | oracleId: String,
198 | lang: String,
199 | printsSearchUri: String,
200 | rulingsUri: String,
201 | scryfallUri: String,
202 | uri: String,
203 | allParts: [RelatedCard]? = nil,
204 | cardFaces: [Face]? = nil,
205 | cmc: Double,
206 | colorIdentity: [Color],
207 | colorIndicator: [Color]? = nil,
208 | colors: [Color]? = nil,
209 | edhrecRank: Int? = nil,
210 | handModifier: String? = nil,
211 | keywords: [String],
212 | layout: Layout,
213 | legalities: Legalities,
214 | lifeModifier: String? = nil,
215 | loyalty: String? = nil,
216 | manaCost: String? = nil,
217 | name: String,
218 | oracleText: String? = nil,
219 | oversized: Bool,
220 | power: String? = nil,
221 | producedMana: [Color]? = nil,
222 | reserved: Bool,
223 | toughness: String? = nil,
224 | typeLine: String? = nil,
225 | artist: String? = nil,
226 | booster: Bool,
227 | borderColor: BorderColor,
228 | cardBackId: UUID? = nil,
229 | collectorNumber: String,
230 | contentWarning: Bool? = nil,
231 | digital: Bool,
232 | finishes: [Finish],
233 | flavorName: String? = nil,
234 | flavorText: String? = nil,
235 | frameEffects: [FrameEffect]? = nil,
236 | frame: Frame,
237 | fullArt: Bool,
238 | games: [Game],
239 | highresImage: Bool,
240 | illustrationId: UUID? = nil,
241 | imageStatus: ImageStatus,
242 | imageUris: ImageUris? = nil,
243 | prices: Prices,
244 | printedName: String? = nil,
245 | printedText: String? = nil,
246 | printedTypeLine: String? = nil,
247 | promo: Bool,
248 | promoTypes: [String]? = nil,
249 | purchaseUris: [String: String]? = nil,
250 | rarity: Card.Rarity,
251 | relatedUris: [String: String],
252 | releasedAt: String,
253 | reprint: Bool,
254 | scryfallSetUri: String,
255 | setName: String,
256 | setSearchUri: URL,
257 | setType: MTGSet.`Type`,
258 | setUri: String,
259 | set: String,
260 | storySpotlight: Bool,
261 | textless: Bool,
262 | variation: Bool,
263 | variationOf: UUID? = nil,
264 | watermark: String? = nil,
265 | preview: Preview? = nil
266 | ) {
267 | self.arenaId = arenaId
268 | self.mtgoId = mtgoId
269 | self.mtgoFoilId = mtgoFoilId
270 | self.multiverseIds = multiverseIds
271 | self.tcgplayerId = tcgplayerId
272 | self.tcgplayerEtchedId = tcgplayerEtchedId
273 | self.cardMarketId = cardMarketId
274 | self.id = id
275 | self.oracleId = oracleId
276 | self.lang = lang
277 | self.printsSearchUri = printsSearchUri
278 | self.rulingsUri = rulingsUri
279 | self.scryfallUri = scryfallUri
280 | self.uri = uri
281 | self.allParts = allParts
282 | self.cardFaces = cardFaces
283 | self.cmc = cmc
284 | self.colorIdentity = colorIdentity
285 | self.colorIndicator = colorIndicator
286 | self.colors = colors
287 | self.edhrecRank = edhrecRank
288 | self.handModifier = handModifier
289 | self.keywords = keywords
290 | self.layout = layout
291 | self.legalities = legalities
292 | self.lifeModifier = lifeModifier
293 | self.loyalty = loyalty
294 | self.manaCost = manaCost
295 | self.name = name
296 | self.oracleText = oracleText
297 | self.oversized = oversized
298 | self.power = power
299 | self.producedMana = producedMana
300 | self.reserved = reserved
301 | self.toughness = toughness
302 | self.typeLine = typeLine
303 | self.artist = artist
304 | self.booster = booster
305 | self.borderColor = borderColor
306 | self.cardBackId = cardBackId
307 | self.collectorNumber = collectorNumber
308 | self.contentWarning = contentWarning
309 | self.digital = digital
310 | self.finishes = finishes
311 | self.flavorName = flavorName
312 | self.flavorText = flavorText
313 | self.frameEffects = frameEffects
314 | self.frame = frame
315 | self.fullArt = fullArt
316 | self.games = games
317 | self.highresImage = highresImage
318 | self.illustrationId = illustrationId
319 | self.imageStatus = imageStatus
320 | self.imageUris = imageUris
321 | self.prices = prices
322 | self.printedName = printedName
323 | self.printedText = printedText
324 | self.printedTypeLine = printedTypeLine
325 | self.promo = promo
326 | self.promoTypes = promoTypes
327 | self.purchaseUris = purchaseUris
328 | self.rarity = rarity
329 | self.relatedUris = relatedUris
330 | self.releasedAt = releasedAt
331 | self.reprint = reprint
332 | self.scryfallSetUri = scryfallSetUri
333 | self.setName = setName
334 | self.setSearchUri = setSearchUri
335 | self.setType = setType
336 | self.setUri = setUri
337 | self.set = set
338 | self.storySpotlight = storySpotlight
339 | self.textless = textless
340 | self.variation = variation
341 | self.variationOf = variationOf
342 | self.watermark = watermark
343 | self.preview = preview
344 | }
345 | // swiftlint:enable function_body_length
346 | }
347 |
--------------------------------------------------------------------------------
/SampleCode/ScryfallSearcher/ScryfallSearcher.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2A07EE3D29E707C500D7747D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A07EE3C29E707C500D7747D /* SearchView.swift */; };
11 | 2A5E0FA729EEFB4F004056CB /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E0FA629EEFB4F004056CB /* CardView.swift */; };
12 | 2A90C68129E5A16C00DC591D /* ScryfallSearcherApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A90C68029E5A16C00DC591D /* ScryfallSearcherApp.swift */; };
13 | 2A90C68329E5A16C00DC591D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A90C68229E5A16C00DC591D /* ContentView.swift */; };
14 | 2A90C68529E5A16D00DC591D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A90C68429E5A16D00DC591D /* Assets.xcassets */; };
15 | 2A90C68829E5A16D00DC591D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A90C68729E5A16D00DC591D /* Preview Assets.xcassets */; };
16 | 2A90C69029E5A2E200DC591D /* ScryfallKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2A90C68F29E5A2E200DC591D /* ScryfallKit */; };
17 | 2AC7C1E329E70BB300D587E9 /* ScryfallKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2AC7C1E229E70BB300D587E9 /* ScryfallKit */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXFileReference section */
21 | 2A07EE3C29E707C500D7747D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; };
22 | 2A5E0FA629EEFB4F004056CB /* CardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; };
23 | 2A90C67D29E5A16C00DC591D /* ScryfallSearcher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScryfallSearcher.app; sourceTree = BUILT_PRODUCTS_DIR; };
24 | 2A90C68029E5A16C00DC591D /* ScryfallSearcherApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScryfallSearcherApp.swift; sourceTree = ""; };
25 | 2A90C68229E5A16C00DC591D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
26 | 2A90C68429E5A16D00DC591D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
27 | 2A90C68729E5A16D00DC591D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
28 | /* End PBXFileReference section */
29 |
30 | /* Begin PBXFrameworksBuildPhase section */
31 | 2A90C67A29E5A16C00DC591D /* Frameworks */ = {
32 | isa = PBXFrameworksBuildPhase;
33 | buildActionMask = 2147483647;
34 | files = (
35 | 2A90C69029E5A2E200DC591D /* ScryfallKit in Frameworks */,
36 | 2AC7C1E329E70BB300D587E9 /* ScryfallKit in Frameworks */,
37 | );
38 | runOnlyForDeploymentPostprocessing = 0;
39 | };
40 | /* End PBXFrameworksBuildPhase section */
41 |
42 | /* Begin PBXGroup section */
43 | 2A90C67429E5A16C00DC591D = {
44 | isa = PBXGroup;
45 | children = (
46 | 2A90C67F29E5A16C00DC591D /* ScryfallSearcher */,
47 | 2A90C67E29E5A16C00DC591D /* Products */,
48 | 2A90C68E29E5A2E200DC591D /* Frameworks */,
49 | );
50 | sourceTree = "";
51 | };
52 | 2A90C67E29E5A16C00DC591D /* Products */ = {
53 | isa = PBXGroup;
54 | children = (
55 | 2A90C67D29E5A16C00DC591D /* ScryfallSearcher.app */,
56 | );
57 | name = Products;
58 | sourceTree = "";
59 | };
60 | 2A90C67F29E5A16C00DC591D /* ScryfallSearcher */ = {
61 | isa = PBXGroup;
62 | children = (
63 | 2A5E0FA629EEFB4F004056CB /* CardView.swift */,
64 | 2A90C68029E5A16C00DC591D /* ScryfallSearcherApp.swift */,
65 | 2A90C68229E5A16C00DC591D /* ContentView.swift */,
66 | 2A90C68429E5A16D00DC591D /* Assets.xcassets */,
67 | 2A90C68629E5A16D00DC591D /* Preview Content */,
68 | 2A07EE3C29E707C500D7747D /* SearchView.swift */,
69 | );
70 | path = ScryfallSearcher;
71 | sourceTree = "";
72 | };
73 | 2A90C68629E5A16D00DC591D /* Preview Content */ = {
74 | isa = PBXGroup;
75 | children = (
76 | 2A90C68729E5A16D00DC591D /* Preview Assets.xcassets */,
77 | );
78 | path = "Preview Content";
79 | sourceTree = "";
80 | };
81 | 2A90C68E29E5A2E200DC591D /* Frameworks */ = {
82 | isa = PBXGroup;
83 | children = (
84 | );
85 | name = Frameworks;
86 | sourceTree = "";
87 | };
88 | /* End PBXGroup section */
89 |
90 | /* Begin PBXNativeTarget section */
91 | 2A90C67C29E5A16C00DC591D /* ScryfallSearcher */ = {
92 | isa = PBXNativeTarget;
93 | buildConfigurationList = 2A90C68B29E5A16D00DC591D /* Build configuration list for PBXNativeTarget "ScryfallSearcher" */;
94 | buildPhases = (
95 | 2A90C67929E5A16C00DC591D /* Sources */,
96 | 2A90C67A29E5A16C00DC591D /* Frameworks */,
97 | 2A90C67B29E5A16C00DC591D /* Resources */,
98 | );
99 | buildRules = (
100 | );
101 | dependencies = (
102 | );
103 | name = ScryfallSearcher;
104 | packageProductDependencies = (
105 | 2A90C68F29E5A2E200DC591D /* ScryfallKit */,
106 | 2AC7C1E229E70BB300D587E9 /* ScryfallKit */,
107 | );
108 | productName = ScryfallSearcher;
109 | productReference = 2A90C67D29E5A16C00DC591D /* ScryfallSearcher.app */;
110 | productType = "com.apple.product-type.application";
111 | };
112 | /* End PBXNativeTarget section */
113 |
114 | /* Begin PBXProject section */
115 | 2A90C67529E5A16C00DC591D /* Project object */ = {
116 | isa = PBXProject;
117 | attributes = {
118 | BuildIndependentTargetsInParallel = 1;
119 | LastSwiftUpdateCheck = 1430;
120 | LastUpgradeCheck = 1430;
121 | TargetAttributes = {
122 | 2A90C67C29E5A16C00DC591D = {
123 | CreatedOnToolsVersion = 14.3;
124 | };
125 | };
126 | };
127 | buildConfigurationList = 2A90C67829E5A16C00DC591D /* Build configuration list for PBXProject "ScryfallSearcher" */;
128 | compatibilityVersion = "Xcode 14.0";
129 | developmentRegion = en;
130 | hasScannedForEncodings = 0;
131 | knownRegions = (
132 | en,
133 | Base,
134 | );
135 | mainGroup = 2A90C67429E5A16C00DC591D;
136 | packageReferences = (
137 | 2AC7C1E129E70BB300D587E9 /* XCRemoteSwiftPackageReference "scryfallkit" */,
138 | );
139 | productRefGroup = 2A90C67E29E5A16C00DC591D /* Products */;
140 | projectDirPath = "";
141 | projectRoot = "";
142 | targets = (
143 | 2A90C67C29E5A16C00DC591D /* ScryfallSearcher */,
144 | );
145 | };
146 | /* End PBXProject section */
147 |
148 | /* Begin PBXResourcesBuildPhase section */
149 | 2A90C67B29E5A16C00DC591D /* Resources */ = {
150 | isa = PBXResourcesBuildPhase;
151 | buildActionMask = 2147483647;
152 | files = (
153 | 2A90C68829E5A16D00DC591D /* Preview Assets.xcassets in Resources */,
154 | 2A90C68529E5A16D00DC591D /* Assets.xcassets in Resources */,
155 | );
156 | runOnlyForDeploymentPostprocessing = 0;
157 | };
158 | /* End PBXResourcesBuildPhase section */
159 |
160 | /* Begin PBXSourcesBuildPhase section */
161 | 2A90C67929E5A16C00DC591D /* Sources */ = {
162 | isa = PBXSourcesBuildPhase;
163 | buildActionMask = 2147483647;
164 | files = (
165 | 2A90C68329E5A16C00DC591D /* ContentView.swift in Sources */,
166 | 2A07EE3D29E707C500D7747D /* SearchView.swift in Sources */,
167 | 2A5E0FA729EEFB4F004056CB /* CardView.swift in Sources */,
168 | 2A90C68129E5A16C00DC591D /* ScryfallSearcherApp.swift in Sources */,
169 | );
170 | runOnlyForDeploymentPostprocessing = 0;
171 | };
172 | /* End PBXSourcesBuildPhase section */
173 |
174 | /* Begin XCBuildConfiguration section */
175 | 2A90C68929E5A16D00DC591D /* Debug */ = {
176 | isa = XCBuildConfiguration;
177 | buildSettings = {
178 | ALWAYS_SEARCH_USER_PATHS = NO;
179 | CLANG_ANALYZER_NONNULL = YES;
180 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
182 | CLANG_ENABLE_MODULES = YES;
183 | CLANG_ENABLE_OBJC_ARC = YES;
184 | CLANG_ENABLE_OBJC_WEAK = YES;
185 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
186 | CLANG_WARN_BOOL_CONVERSION = YES;
187 | CLANG_WARN_COMMA = YES;
188 | CLANG_WARN_CONSTANT_CONVERSION = YES;
189 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
190 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
191 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
192 | CLANG_WARN_EMPTY_BODY = YES;
193 | CLANG_WARN_ENUM_CONVERSION = YES;
194 | CLANG_WARN_INFINITE_RECURSION = YES;
195 | CLANG_WARN_INT_CONVERSION = YES;
196 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
197 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
198 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
200 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
201 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
202 | CLANG_WARN_STRICT_PROTOTYPES = YES;
203 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
204 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
205 | CLANG_WARN_UNREACHABLE_CODE = YES;
206 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
207 | COPY_PHASE_STRIP = NO;
208 | DEBUG_INFORMATION_FORMAT = dwarf;
209 | ENABLE_STRICT_OBJC_MSGSEND = YES;
210 | ENABLE_TESTABILITY = YES;
211 | GCC_C_LANGUAGE_STANDARD = gnu11;
212 | GCC_DYNAMIC_NO_PIC = NO;
213 | GCC_NO_COMMON_BLOCKS = YES;
214 | GCC_OPTIMIZATION_LEVEL = 0;
215 | GCC_PREPROCESSOR_DEFINITIONS = (
216 | "DEBUG=1",
217 | "$(inherited)",
218 | );
219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
221 | GCC_WARN_UNDECLARED_SELECTOR = YES;
222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
223 | GCC_WARN_UNUSED_FUNCTION = YES;
224 | GCC_WARN_UNUSED_VARIABLE = YES;
225 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
226 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
227 | MTL_FAST_MATH = YES;
228 | ONLY_ACTIVE_ARCH = YES;
229 | SDKROOT = iphoneos;
230 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
231 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
232 | };
233 | name = Debug;
234 | };
235 | 2A90C68A29E5A16D00DC591D /* Release */ = {
236 | isa = XCBuildConfiguration;
237 | buildSettings = {
238 | ALWAYS_SEARCH_USER_PATHS = NO;
239 | CLANG_ANALYZER_NONNULL = YES;
240 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
242 | CLANG_ENABLE_MODULES = YES;
243 | CLANG_ENABLE_OBJC_ARC = YES;
244 | CLANG_ENABLE_OBJC_WEAK = YES;
245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
246 | CLANG_WARN_BOOL_CONVERSION = YES;
247 | CLANG_WARN_COMMA = YES;
248 | CLANG_WARN_CONSTANT_CONVERSION = YES;
249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
251 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
252 | CLANG_WARN_EMPTY_BODY = YES;
253 | CLANG_WARN_ENUM_CONVERSION = YES;
254 | CLANG_WARN_INFINITE_RECURSION = YES;
255 | CLANG_WARN_INT_CONVERSION = YES;
256 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
257 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
258 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
260 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
261 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
262 | CLANG_WARN_STRICT_PROTOTYPES = YES;
263 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
264 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
265 | CLANG_WARN_UNREACHABLE_CODE = YES;
266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
267 | COPY_PHASE_STRIP = NO;
268 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
269 | ENABLE_NS_ASSERTIONS = NO;
270 | ENABLE_STRICT_OBJC_MSGSEND = YES;
271 | GCC_C_LANGUAGE_STANDARD = gnu11;
272 | GCC_NO_COMMON_BLOCKS = YES;
273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
275 | GCC_WARN_UNDECLARED_SELECTOR = YES;
276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
277 | GCC_WARN_UNUSED_FUNCTION = YES;
278 | GCC_WARN_UNUSED_VARIABLE = YES;
279 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
280 | MTL_ENABLE_DEBUG_INFO = NO;
281 | MTL_FAST_MATH = YES;
282 | SDKROOT = iphoneos;
283 | SWIFT_COMPILATION_MODE = wholemodule;
284 | SWIFT_OPTIMIZATION_LEVEL = "-O";
285 | VALIDATE_PRODUCT = YES;
286 | };
287 | name = Release;
288 | };
289 | 2A90C68C29E5A16D00DC591D /* Debug */ = {
290 | isa = XCBuildConfiguration;
291 | buildSettings = {
292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
293 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
294 | CODE_SIGN_STYLE = Automatic;
295 | CURRENT_PROJECT_VERSION = 1;
296 | DEVELOPMENT_ASSET_PATHS = "\"ScryfallSearcher/Preview Content\"";
297 | DEVELOPMENT_TEAM = F8SAUD58HS;
298 | ENABLE_PREVIEWS = YES;
299 | GENERATE_INFOPLIST_FILE = YES;
300 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
301 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
302 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
303 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
304 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
305 | LD_RUNPATH_SEARCH_PATHS = (
306 | "$(inherited)",
307 | "@executable_path/Frameworks",
308 | );
309 | MARKETING_VERSION = 1.0;
310 | PRODUCT_BUNDLE_IDENTIFIER = dev.hearst.ScryfallSearcher;
311 | PRODUCT_NAME = "$(TARGET_NAME)";
312 | SWIFT_EMIT_LOC_STRINGS = YES;
313 | SWIFT_VERSION = 5.0;
314 | TARGETED_DEVICE_FAMILY = "1,2";
315 | };
316 | name = Debug;
317 | };
318 | 2A90C68D29E5A16D00DC591D /* Release */ = {
319 | isa = XCBuildConfiguration;
320 | buildSettings = {
321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
322 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
323 | CODE_SIGN_STYLE = Automatic;
324 | CURRENT_PROJECT_VERSION = 1;
325 | DEVELOPMENT_ASSET_PATHS = "\"ScryfallSearcher/Preview Content\"";
326 | DEVELOPMENT_TEAM = F8SAUD58HS;
327 | ENABLE_PREVIEWS = YES;
328 | GENERATE_INFOPLIST_FILE = YES;
329 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
330 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
331 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
333 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
334 | LD_RUNPATH_SEARCH_PATHS = (
335 | "$(inherited)",
336 | "@executable_path/Frameworks",
337 | );
338 | MARKETING_VERSION = 1.0;
339 | PRODUCT_BUNDLE_IDENTIFIER = dev.hearst.ScryfallSearcher;
340 | PRODUCT_NAME = "$(TARGET_NAME)";
341 | SWIFT_EMIT_LOC_STRINGS = YES;
342 | SWIFT_VERSION = 5.0;
343 | TARGETED_DEVICE_FAMILY = "1,2";
344 | };
345 | name = Release;
346 | };
347 | /* End XCBuildConfiguration section */
348 |
349 | /* Begin XCConfigurationList section */
350 | 2A90C67829E5A16C00DC591D /* Build configuration list for PBXProject "ScryfallSearcher" */ = {
351 | isa = XCConfigurationList;
352 | buildConfigurations = (
353 | 2A90C68929E5A16D00DC591D /* Debug */,
354 | 2A90C68A29E5A16D00DC591D /* Release */,
355 | );
356 | defaultConfigurationIsVisible = 0;
357 | defaultConfigurationName = Release;
358 | };
359 | 2A90C68B29E5A16D00DC591D /* Build configuration list for PBXNativeTarget "ScryfallSearcher" */ = {
360 | isa = XCConfigurationList;
361 | buildConfigurations = (
362 | 2A90C68C29E5A16D00DC591D /* Debug */,
363 | 2A90C68D29E5A16D00DC591D /* Release */,
364 | );
365 | defaultConfigurationIsVisible = 0;
366 | defaultConfigurationName = Release;
367 | };
368 | /* End XCConfigurationList section */
369 |
370 | /* Begin XCRemoteSwiftPackageReference section */
371 | 2AC7C1E129E70BB300D587E9 /* XCRemoteSwiftPackageReference "scryfallkit" */ = {
372 | isa = XCRemoteSwiftPackageReference;
373 | repositoryURL = "https://github.com/jacobhearst/scryfallkit";
374 | requirement = {
375 | kind = upToNextMajorVersion;
376 | minimumVersion = 5.0.0;
377 | };
378 | };
379 | /* End XCRemoteSwiftPackageReference section */
380 |
381 | /* Begin XCSwiftPackageProductDependency section */
382 | 2A90C68F29E5A2E200DC591D /* ScryfallKit */ = {
383 | isa = XCSwiftPackageProductDependency;
384 | productName = ScryfallKit;
385 | };
386 | 2AC7C1E229E70BB300D587E9 /* ScryfallKit */ = {
387 | isa = XCSwiftPackageProductDependency;
388 | package = 2AC7C1E129E70BB300D587E9 /* XCRemoteSwiftPackageReference "scryfallkit" */;
389 | productName = ScryfallKit;
390 | };
391 | /* End XCSwiftPackageProductDependency section */
392 | };
393 | rootObject = 2A90C67529E5A16C00DC591D /* Project object */;
394 | }
395 |
--------------------------------------------------------------------------------