├── .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 | [![Build and test](https://github.com/JacobHearst/ScryfallKit/actions/workflows/build+test.yml/badge.svg)](https://github.com/JacobHearst/ScryfallKit/actions/workflows/build+test.yml) [![Swift Version Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FJacobHearst%2FScryfallKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/JacobHearst/ScryfallKit) [![Platform Support](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FJacobHearst%2FScryfallKit%2Fbadge%3Ftype%3Dplatforms)](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 | --------------------------------------------------------------------------------