### README (README.md) # Delta > Delta is an all-in-one classic video game emulator for non-jailbroken iOS devices. [](https://swift.org/) [](https://www.gnu.org/licenses/agpl-3.0) [](http://makeapullrequest.com) Delta is an iOS application that allows you to emulate and play video games for several classic video game systems, including Game Boy Advance, Nintendo 64, and Nintendo DS. Delta is the spiritual successor to [GBA4iOS](http://www.gba4iosapp.com) — a Game Boy Advance emulator for iOS devices [Paul Thorsen](https://twitter.com/pau1thor) and I made while in high school together — rebuilt from the ground up with modern iOS features and support for more systems.


Mario and Pokémon are properties of Nintendo Co., Ltd. and are not associated with Delta or AltStore LLC.

## Supported Systems - Nintendo Entertainment System (NES) - Super Nintendo Entertainment System (SNES) - Nintendo 64 (N64) - Game Boy / Game Boy Color (GBC) - Game Boy Advance (GBA) - Nintendo DS (DS) - Sega Genesis / Mega Drive (GEN) **(in beta)** ## Features - Accurate, full speed emulation thanks to mature underlying emulator cores. - NES: [Nestopia](https://github.com/0ldsk00l/nestopia) - SNES: [Snes9x](https://github.com/snes9xgit/snes9x) - N64: [mupen64plus](https://github.com/mupen64plus/mupen64plus-core) - GBC: [Gambatte](https://github.com/sinamas/gambatte) - GBA: [visualboyadvance-m](https://github.com/visualboyadvance-m/visualboyadvance-m) - DS: [melonDS](https://github.com/Arisotura/melonDS) - GEN: [Genesis Plus GX](https://github.com/ekeeke/Genesis-Plus-GX) - Beautiful, native UI. - Browse and play your favorite games with a UI designed from the ground up for iOS. - Automatically displays appropriate box art for imported games. - Change a game’s artwork to anything you want, or select from the built-in game artwork database. - Controller Support - Supports PS4, PS5, Xbox One S, Xbox Series X, and MFi game controllers. - Supports bluetooth (and wired) keyboards, as well as the Apple Smart Keyboard. - Completely customize button mappings on a per-system, per-controller basis. - Map buttons to special “Quick Save”, “Quick Load,” and “Fast Forward” actions. - Custom Controller Skins - Beautiful built-in controller skins for all systems. - Import controller skins made by others, or even make your own to share with the world! - Save States - Save and load save states for any game from the pause menu. - Lock save states to prevent them from being accidentally overwritten. - Automatically makes backup save states to ensure you never lose your progress. - Support for “Quick Saves,” save states that can be quickly saved/loaded with a single button press (requires external controller). - Fast Forwarding - Speed through slower parts of games by running the game much faster than normal. - Easily enable or disable from the pause menu, or optionally with a mapped button on an external controller. - Delta Sync - Sync your games, game saves, save states, cheats, controller skins, and controller mappings between devices. - View version histories of everything you sync and optionally restore them to earlier versions. - Supports both Google Drive and Dropbox. - Hold Button - Choose buttons for Delta to hold down on your behalf, freeing up your thumbs to press other buttons instead. - Perfect for games that typically require one button be held down constantly (ex: run button in Mario games, or the A button in Mario Kart). - 3D/Haptic Touch - Use 3D or Haptic Touch to “peek” at games, save states, and cheat codes. - App icon shortcuts allow quick access to your most recently played games, or optionally customize the shortcuts to always include certain games. - Cheat Codes - NES - Game Genie - SNES: - Game Genie - Pro Action Replay - N64 - GameShark - GBC - Game Genie - GameShark - GBA - Action Replay - Code Breaker - GameShark - DS - Action Replay - Gyroscope support **(WarioWare: Twisted! only)** - Microphone support **(DS only)** ## Installation

Delta was originally developed under the impression Apple would allow it into the App Store. Unfortunately Apple later changed their minds, leaving me no choice but to find a new way to distribute Delta. Long story short, this led me to create [AltStore](https://github.com/rileytestut/AltStore), which now serves as the official way to install Delta onto your device. To install Delta with AltStore: 1. Download AltServer for Mac or PC from https://altstore.io 2. Connect your iOS device to your computer via lightning cable (or USB-C for iPads). 3. [Follow these instructions](https://altstore.io/faq/) to install AltStore onto your device with AltServer. 2. Open AltStore on your device, then navigate to the "Browse" tab. 3. Find Delta, then press the `FREE` button to start installing the app. Once you've installed Delta with AltStore, **you'll need to refresh it at least once every 7 days to prevent it from expiring** and requiring a re-installation. AltStore will periodically attempt to refresh your apps in the background when on the same WiFi as AltServer, but you can also manually refresh apps by pressing "Refresh All" in AltStore. AltStore will also let you know whenever a new update is released, allowing you to update Delta directly within AltStore. Alternatively, you are welcome to download the compiled `.ipa`'s from [Releases](https://github.com/rileytestut/Delta/releases) and sideload them using whatever sideloading method you prefer, but you will not receive automatic updates and will have to manually update Delta by re-sideloading each new version. ## Project Overview Delta was designed from the beginning to be modular, and for that reason each "Delta Core" has its own GitHub repo and is added as a submodule to the main Delta project. Additionally, Delta uses two of my own private frameworks I use to share common functionality between my apps: Roxas and Harmony. [**Delta**](https://github.com/rileytestut/Delta) Delta is just a regular, sandboxed iOS application. The Delta app repo (aka this one) contains all the code specific to the Delta app itself, such as storyboards, app-specific view controllers, database logic, etc. [**DeltaCore**](https://github.com/rileytestut/DeltaCore) DeltaCore serves as the “middle-man” between the high-level app code and the specific emulation cores. By working with this framework, you have access to all the core Delta features, such as emulation, controller skins, save states, cheat codes, etc. Other potential emulator apps will use this framework extensively. [**Roxas**](https://github.com/rileytestut/Roxas) Roxas is my own framework used across my projects, developed to simplify a variety of common tasks used in iOS development. [**Harmony**](https://github.com/rileytestut/Harmony) Harmony is my personal syncing framework designed to sync Core Data databases. Harmony listens for changes to an app's persistent store, then syncs any changes with a remote file service (such as Google Drive or Dropbox). **Delta Cores** Each system in Delta is implemented as its own "Delta Core", which serves as a standard emulation API Delta can understand regardless of the underlying core. For the most part, you don't interact directly with specific Delta Cores, but rather indirectly through `DeltaCore`. - [NESDeltaCore](https://github.com/rileytestut/NESDeltaCore) - [SNESDeltaCore](https://github.com/rileytestut/SNESDeltaCore) - [N64DeltaCore](https://github.com/rileytestut/N64DeltaCore) - [GBCDeltaCore](https://github.com/rileytestut/GBCDeltaCore) - [GBADeltaCore](https://github.com/rileytestut/GBADeltaCore) - [MelonDSDeltaCore](https://github.com/rileytestut/MelonDSDeltaCore) - [GPGXDeltaCore](https://github.com/rileytestut/GPGXDeltaCore) ## Minimum Project Requirements - Xcode 15 - Swift 5.9 - iOS 14.0 ## Compilation Instructions > Delta uses Git LFS to manage large files, so first make sure you have [installed Git LFS](https://git-lfs.github.com). 1. Clone this repository by running the following command in Terminal* ```bash $ git clone https://github.com/rileytestut/Delta.git ``` 2. Update Git submodules ```bash $ cd Delta $ git submodule update --init --recursive ``` 3. Open `Systems/Systems.xcworkspace` and select the "Systems" project in the project navigator (a.k.a. the left sidebar). 4. Select "Systems" under `Targets`, then click the `Signing & Capabilities` tab. 5. Change `Team` from "Yvette Testut" to your own account. 6. Close `Systems/Systems.xcworkspace`, then open `Delta.xcworkspace`. 6. Repeat steps 4 & 5 with the "Delta" target. 7. Change Delta's `Bundle Identifier` to something unique, such as by appending your GitHub username (ex: `com.rileytestut.Delta.MyGitHubUsername`). 8. Build + run app! 🎉 ## Licensing The Delta codebase is distributed under the **AGPLv3 license**. ## Contact Us * Email: delta@altstore.io * Mastodon: [@mattaltgear@mstdn.social](https://mstdn.social/mattaltgear) * Threads: [@mattaltgear](https://www.threads.com/@mattaltgear) * Bluesky: [@mattaltgear.bsky.social](https://bsky.app/profile/mattaltgear.bsky.social) --- ### CONTRIBUTING (CONTRIBUTING.md) Thank you for your interest in contributing to Delta! Delta is an open-source project that aims to provide a seamless and enjoyable experience for playing classic video games on iOS devices. We welcome contributions from anyone who shares our vision and passion for emulation. # How to Contribute We are currently accepting these types of contributions for Delta: - Experimental Features - Bug fixes All features added by third-party contributors will be considered *experimental* at first, and are disabled by default unless specifically enabled by the user from the Experimental Features section in Delta's settings. Experimental Features are only available in the beta version of Delta, but once a feature has been sufficiently tested we may choose to "graduate" it into an official Delta feature, at which point it will become available to all users. For more specific instructions regarding contributing features to Delta, see [ExperimentalFeatures.md](Docs/ExperimentalFeatures.md). ## Contribution Guidelines **Check out our project board first!** We have categorized issues and pull requests to highlight areas where help is most needed. You are welcome to contribute something new, but keep in mind that we may focus on more highly-requested items first. **Keep changes small** The smaller a pull request is the more likely it will be merged. Make sure your PRs are limited to just the relevant changes and nothing else. Avoid pure formatting changes to code that has not been modified otherwise. **Make sure contributions are 100% complete** We can't accept unfinished pull requests, so please make sure your contribution is ready to be merged as-is. **Extensively test your changes** Make sure your changes work as expected on different devices and iOS versions, and doesn't result in additional bugs. **Commit only relevant changes of changed files** Some files, such as Storyboards and Xcode projects, often contain changes that are unrelated to your specific change. **You should almost never commit an entire Storyboard or Xcode project as-is.** **Not all contributions will be accepted** To maximize your chances of getting your pull request accepted, make sure to read all the guidelines carefully and familiarize yourself with the project structure, coding style, and best practices. ## Code Style and Conventions **Please make sure your code follows these guidelines and is free of compiler warnings before submitting a pull request.** * All contributions should be pure Swift, no C++ or Objective-C (unless absolutely necessary) * Follow the [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/) for writing clean, consistent, and expressive Swift code * Use Allman (a.k.a. brackets on new line) brace style (exceptions: closures and single-line statements) * Use 4 spaces for indentation. Do not use tabs or mix tabs and spaces. * Use whitespace liberally to make code easier to read * Use descriptive variable names * Lean on the side of commenting (more comments make it more likely to be approved) * Prefer structs over classes. Classes should be used only if you need a reference type, or to interoperate with ObjC. When editing existing code, please preserve the original formatting of the code as much as possible (e.g. brace style). Do not make unnecessary changes to whitespace, indentation, line breaks or comments. This helps keep the diffs clean and easy to review. For example, when editing code that uses the Allman brace style (a.k.a. brackets on new line), make sure any `if`/`else` statements you write also place the bracket on a new line. ## Submitting Pull Requests All pull requests must adhere to the PR template, filling out each section as appropriate. **Only PRs that follow this template will be accepted.** Once you've submitted your PR, we will review it and provide feedback as soon as we can. --- ### Docs/ExperimentalFeatures (Docs/ExperimentalFeatures.md) # Overview Features contributed to Delta by third-party developers are considered "experimental", and are available for testing in the beta version of Delta. Once a feature has been sufficiently tested, we may choose to "graduate" it into an official Delta feature, at which point it will become available to all users. Every Experimental Feature can be thought of as a binary flag: it's either enabled or disabled. When disabled, a feature should have no impact on the rest of the app. This allows us to accept contributions freely without affecting the overall stability of Delta. If a feature requires more state than binary On/Off, you can define as many "options" as needed. Options store additional data required by your feature's implementation. Options can be "hidden" (the default), but can also be automatically exposed in Delta's settings where they can be changed directly by users. # Guidelines For Experimental Features Keep the following in mind when contributing new Experimental Features: * When a feature is disabled, it should have *no* noticeable impact on the rest of the app. * Avoid touching the core emulation logic. * Isolate your changes as much as possible from the rest of the app, preferably in separate files. We recommend using Swift extensions to add functionality to existing types (e.g. `GameViewController+ExperimentalFastForward.swift`) * If your change requires modifying `DeltaCore` or specific cores, make sure the naming and "shape" of any public API follows existing conventions as much as possible. In general, Experimental Features that require modifying cores have a higher bar for acceptance. # Adding a New Experimental Feature 1. Open `Delta/Experimental Features/ExperimentalFeatures.swift` 2. Add a new property to the `ExperimentalFeatures` struct, annotated with the `@Feature` property wrapper. You do not need to define the property's type as it will be inferred by @Feature. > The property name (e.g. `variableFastForward`) will be used internally as the `UserDefaults` key for persisting data. 3. Pass in the name of your feature to `@Feature`'s initializer, and optionally a description. Once you've defined your feature, you can check whether or not it's enabled at runtime via `ExperimentalFeatures.shared.[feature].isEnabled`. Here's a complete implementation for a new Experimental Feature called "Show Status Bar": ```swift // ExperimentalFeatures.swift struct ExperimentalFeatures { @Feature(name: "Show Status Bar", description: "Show the Status Bar during gameplay.") var showStatusBar } // GameViewController+ShowStatusBar.swift extension GameViewController { override var prefersStatusBarHidden: Bool { return !ExperimentalFeatures.shared.showStatusBar.isEnabled } } ``` # Adding Options to a Feature Some features require additional configuration beyond being enabled or disabled. These are referred to as "options", and you can define as many options for a feature as necessary. Whenever an option's value changes, it is automatically persisted to `UserDefaults`. The option’s underlying type must conform to `OptionValue`. Automatic conformance is provided for all standard property list types, but if you want to use your own type, it must either: * Conform to `RawRepresentable`, where its raw type is a valid property list type (e.g. enums with string backing), or * Conform to `Codable` To declare a feature with options: 1. Create a new Swift file in `Delta/Experimental Features/Features` and name it after your feature (e.g. `VariableFastForward.swift`) 2. Define a new struct named `[FeatureName]Options` (e.g. `VariableFastForwardOptions`) 3. For each configurable value, define a new property on your Options struct with `@Options` property wrapper. > The property name (e.g. `speed`) will be combined with the feature's property name and used internally as the `UserDefaults` key for persisting data. 4. **If the option represents a non-optional value, you must provide an initial value**. This will be used as the default value if the option has not been configured by user. 5. Follow the above instructions for declaring a feature, but pass in an instance of your `Options` struct to the `options:` parameter in the `@Feature` initializer. Heres's an example feature "Game Gestures" that shows an instruction alert the first time it is enabled. It uses an `@Option` to store whether the alert has already been shown or not. ```swift // GameGestures.swift struct GameGesturesOptions { @Option // No parameters = "Hidden" option var didShowGestureAlert: Bool = false } // ExperimentalFeatures.swift struct ExperimentalFeatures { @Feature(name: "Game Gestures", options: GameGesturesOptions()) var gameGestures } ``` # User-Facing Options By default, Options are hidden, which means their values can only be changed programmatically. However, options can also be user-facing, which we generally recommend. User-facing options will automatically appear in the `Experimental Features` section of Delta's settings, where they can be configured manually by users. To define a user-facing option, pass in a value for `name` in the `@Option` initializer, and optionally a `description`. Because user-facing options are meant to be seen by users, the underlying type must conform to `LocalizedOptionValue`. This protocol refines `OptionValue` with two new methods: * `localizedDescription`, used to display the value in a human-readable manner. * `localizedNilDescription`, used to represent the `nil` value in a human-readable manner. The default implementation returns "None". Heres's an example feature "Game Screenshots" that defines options so users can choose whether to save screenshots to the Photo Library, the Files app, or both. Unlike the above "Game Gestures" example, "Save to Files" and "Save to Photos" will be exposed in the `Experimental Features` section of Delta's settings, where they will appear as switches that the user can toggle. ```swift // GameScreenshots.swift struct GameScreenshotsOptions { @Option(name: "Save to Files", description: "Save the screenshot to the app's directory in Files.") var saveToFiles: Bool = true @Option(name: "Save to Photos", description: "Save the screenshot to the Photo Library.") var saveToPhotos: Bool = false } // ExperimentalFeatures.swift struct ExperimentalFeatures { @Feature(name: "Game Screenshots", options: GameScreenshotsOptions()) var gameScreenshots } ``` ## Types of User-Facing Options Delta supports 3 types of user-facing options: * Bool options * "Picker" options (e.g. array of values) * Custom options (any other type) Here's an example feature "VariableFastForward" that uses all 3 types of user-facing options: ```swift // VariableFastForward.swift enum FastForwardSpeed: Double, CaseIterable, CustomStringConvertible { case x2 = 2 case x3 = 3 case x4 = 4 case x8 = 8 var description: String { return "\(self.rawValue)x" } } extension FastForwardSpeed: LocalizedOptionValue { var localizedDescription: Text { Text(self.description) } static var localizedNilDescription: Text { Text("Maximum") } } struct VariableFastForwardOptions { // Bool option (will appear as inline UISwitch) @Option(name: "Allow Unrestricted Speeds", description: "Allow speeds that exceed the maximum speed of a system.") var allowUnrestrictedSpeeds: Bool = false // "Custom" option (will appear as full-screen view with text field) @Option(name: "Maximum Speed", description: "Change the maximum fast forward speed across all systems.", detailView: { TextField("", value: $0, formatter: NumberFormatter()) .keyboardType(.numberPad) }) var maxSpeed: Int? // "Picker" options (will appear as standard UIMenu picker) @Option(name: "Nintendo Entertainment System", values: FastForwardSpeed.allCases) var nes: FastForwardSpeed? @Option(name: "Super Nintendo", values: FastForwardSpeed.allCases) var snes: FastForwardSpeed? @Option(name: "Nintendo 64", values: FastForwardSpeed.allCases) var n64: FastForwardSpeed? // Etc. } ``` Each type of user-facing option has slightly different requirements, which are detailed below: ### Bool Option If the property annotated with @Option is a `Bool`, there is nothing more you need to do. Delta will automatically show a toggle on the feature's detail page that can be used by user to update this value. Example: ```swift @Option(name: "showStatusBar") var showStatusBar: Bool = false ``` ### "Picker" Option If there is a known, finite number of supported values for your option, you can pass a `Collection` of them to the `values:` parameter in the `@Option` initializer. Delta will automatically show an inline picker on the feature's detail page that will allow users to select from the preset values. > > If the option is an optional type, the picker will automatically include a `nil` option in the picker. You can customize the name used to represent the `nil` option by overriding `LocalizedOptionValue.localizedNilDescription`. > Example: ```swift enum Planet: String { mercury, venus, earth, ... } extension Planet: LocalizedOptionValue { static var localizedNilDescription: Text { Text("No Favorite Planet") } } @Option(name: "Current Planet", values: Planet.allCases) var currentPlanet: Planet = .earth @Option(name: "Favorite Planet", values: Planet.allCases) var favoritePlanet: Planet? // Optional, so Delta will include `nil` option in picker, displayed as "No Favorite Planet". ``` ### "Custom" Option Every user-facing `@Option` requires some UI in order to be configured by users in Delta's settings. If your option is not one of the ones listed above, you'll need to provide your own `SwiftUI` view. This can be as simple as just an inline `TextField` (e.g. for `String` options), or a completely custom full screen SwiftUI view with access to the entire SwiftUI API (e.g. a full color picker for `Color` options). To provide your own SwiftUI view, pass in a closure that returns your custom `View` to the `detailView:` parameter in `@Option`'s initializer. The closure passes in a `Binding` to the option's underlying value, which can then be passed into any SwiftUI control that takes a `Binding` (e.g. `Picker`, `Toggle`, `TextField`, etc.) to automatically update the option's value. However this is just a convenience, and you are welcome to update your `@Option` value from your custom view however works best. By default, custom options will present their SwiftUI views full-screen when tapped. However, if you want your custom view to appear inline (like Bool and "picker" options), you can apply the `displayInline()` modifier to your view. Example: ```swift // Inline text field @Option(name: "Custom Nickname", detailView: { TextField("", text: $0) .displayInline() }) var nickname: String ``` ## Using Features All Experimental Features can be selectively enabled or disabled by the user in the "Experimental Features" section of Delta's settings. To check whether a feature is enabled at runtime, call `ExperimentalFeatures.shared.[feature].isEnabled`. **Your feature implementation must respect this flag and have no noticeable effect on the rest of the app when disabled.** You can access individual feature options via `ExperimentalFeatures.shared.[feature].[option]`. To access `@Option`-specific properties, such as its `settingsKey`, use the `@Option`'s projected value by prepending the property with a `$` (e.g. `ExperimentalFeatures.shared.[feature].$[option].settingsName`). Delta will automatically post a `Settings.didChangeNotification` notification whenever a feature is enabled, disabled, or one of its options changes. The `userInfo` dictionary will contain either `Feature.settingsName` or `Option.settingsName` under the `SettingsUserInfoKey.name` key, as well as the new value under the `SettingsUserInfoKey.value` key. Example: ```swift // Handler for Settings.didChangeNotification func settingsDidChange(_ notification: Notification) { guard let name = notification.userInfo?[SettingsUserInfoKey.name] as? Settings.Name else { return } switch name { case ExperimentalFeatures.shared.showStatusBar.settingsKey: // Update status bar self.setNeedsStatusBarAppearanceUpdate() case ExperimentalFeatures.shared.customTintColor.settingsKey: fallthrough case ExperimentalFeatures.shared.customTintColor.$color.settingsKey: // Update tint color if feature itself is enabled/disabled OR tint color changes. self.updateTintColor() default: break } } ``` --- ### Docs/Pull Request Template (Docs/pull_request_template.md) Mark the type contribution you are making: - [ ] Experimental feature (new functionality that can be selectively enabled/disabled) - [ ] Bug fix (non-breaking change which fixes an issue) # Description Summary of your changes, including: * Why is this change necessary? * Why did you decide on this solution? # Testing List all iOS versions and devices you've tested this change on. **Example Configurations**: - iPhone 14, iOS 16.3.1 - iPhone X, iOS 15.7.4 # Checklist **General (All PRs)** - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] My changes generate no new warnings - [ ] I've tested my changes with different device + OS version configurations **Experimental Feature-specific** - [ ] Added property to `ExperimentalFeatures` struct annotated with `@Feature` - [ ] Uses `@Option`'s to persist all feature-related data - [ ] Locked *all* behavior changes behind `ExperimentalFeatures.shared.[feature].isEnabled` runtime check - [ ] Isolates changes to separate files as much as possible (e.g. via Swift extensions) --- ### Pods/Exported Symbols (Pods/exported_symbols.txt) _ConfigGetSharedDataFilepath_Boogie _ConfigGetUserConfigPath _ConfigGetUserCachePath _ConfigGetUserDataPath _ConfigOpenSection _ConfigDeleteSection _ConfigSaveSection _ConfigSaveFile _ConfigSetDefaultInt _ConfigSetDefaultFloat _ConfigSetDefaultBool _ConfigSetDefaultString _ConfigGetParamInt _ConfigGetParamFloat _ConfigGetParamBool _ConfigGetParamString _ConfigExternalGetParameter _ConfigExternalOpen _ConfigExternalClose _CoreGetAPIVersions _ConfigGetParameter _ConfigSetParameter _CoreDoCommand _RSP_PluginStartup _RSP_PluginShutdown _RSP_PluginGetVersion _RSP_RomClosed _DoRspCycles _InitiateRSP _Video_PluginStartup _Video_PluginShutdown _Video_PluginGetVersion _Video_RomOpen _Video_RomClosed _VidExt_Init _VidExt_Quit _VidExt_ListFullscreenModes _VidExt_SetVideoMode _VidExt_SetCaption _VidExt_ToggleFullScreen _VidExt_ResizeWindow _VidExt_GL_GetProcAddress _VidExt_GL_SetAttribute _VidExt_GL_GetAttribute _VidExt_GL_SwapBuffers _ChangeWindow _InitiateGFX _MoveScreen _ProcessDList _ProcessRDPList _ShowCFB _UpdateScreen _ViStatusChanged _ViWidthChanged _ReadScreen2 _SetRenderingCallback _FBRead _FBWrite _FBGetFrameBufferInfo _ResizeVideoOutput --- ### Pods/Alamofire/README (Pods/Alamofire/README.md) [](https://travis-ci.org/Alamofire/Alamofire) [](https://img.shields.io/cocoapods/v/Alamofire.svg) [](https://github.com/Carthage/Carthage) [](https://alamofire.github.io/Alamofire) [](https://twitter.com/AlamofireSF) [](https://gitter.im/Alamofire/Alamofire?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) Alamofire is an HTTP networking library written in Swift. - [Features](#features) - [Component Libraries](#component-libraries) - [Requirements](#requirements) - [Migration Guides](#migration-guides) - [Communication](#communication) - [Installation](#installation) - [Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md) - **Intro -** [Making a Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#making-a-request), [Response Handling](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-handling), [Response Validation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-validation), [Response Caching](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-caching) - **HTTP -** [HTTP Methods](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-methods), [Parameter Encoding](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#parameter-encoding), [HTTP Headers](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#http-headers), [Authentication](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#authentication) - **Large Data -** [Downloading Data to a File](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#downloading-data-to-a-file), [Uploading Data to a Server](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#uploading-data-to-a-server) - **Tools -** [Statistical Metrics](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#statistical-metrics), [cURL Command Output](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#curl-command-output) - [Advanced Usage](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md) - **URL Session -** [Session Manager](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-manager), [Session Delegate](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#session-delegate), [Request](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#request) - **Routing -** [Routing Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#routing-requests), [Adapting and Retrying Requests](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests) - **Model Objects -** [Custom Response Serialization](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#custom-response-serialization) - **Connection -** [Security](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security), [Network Reachability](https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#network-reachability) - [Open Radars](#open-radars) - [FAQ](#faq) - [Credits](#credits) - [Donations](#donations) - [License](#license) ## Features - [x] Chainable Request / Response Methods - [x] URL / JSON / plist Parameter Encoding - [x] Upload File / Data / Stream / MultipartFormData - [x] Download File using Request or Resume Data - [x] Authentication with URLCredential - [x] HTTP Response Validation - [x] Upload and Download Progress Closures with Progress - [x] cURL Command Output - [x] Dynamically Adapt and Retry Requests - [x] TLS Certificate and Public Key Pinning - [x] Network Reachability - [x] Comprehensive Unit and Integration Test Coverage - [x] [Complete Documentation](https://alamofire.github.io/Alamofire) ## Component Libraries In order to keep Alamofire focused specifically on core networking implementations, additional component libraries have been created by the [Alamofire Software Foundation](https://github.com/Alamofire/Foundation) to bring additional functionality to the Alamofire ecosystem. - [AlamofireImage](https://github.com/Alamofire/AlamofireImage) - An image library including image response serializers, `UIImage` and `UIImageView` extensions, custom image filters, an auto-purging in-memory cache and a priority-based image downloading system. - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - Controls the visibility of the network activity indicator on iOS using Alamofire. It contains configurable delay timers to help mitigate flicker and can support `URLSession` instances not managed by Alamofire. ## Requirements - iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+ - Xcode 8.3+ - Swift 3.1+ ## Migration Guides - [Alamofire 4.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%204.0%20Migration%20Guide.md) - [Alamofire 3.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%203.0%20Migration%20Guide.md) - [Alamofire 2.0 Migration Guide](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Alamofire%202.0%20Migration%20Guide.md) ## Communication - If you **need help**, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire). (Tag 'alamofire') - If you'd like to **ask a general question**, use [Stack Overflow](https://stackoverflow.com/questions/tagged/alamofire). - If you **found a bug**, open an issue. - If you **have a feature request**, open an issue. - If you **want to contribute**, submit a pull request. ## Installation ### CocoaPods [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: ```bash $ gem install cocoapods ``` > CocoaPods 1.1+ is required to build Alamofire 4.0+. To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`: ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target '' do pod 'Alamofire', '~> 4.7' end ``` Then, run the following command: ```bash $ pod install ``` ### Carthage [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. You can install Carthage with [Homebrew](https://brew.sh/) using the following command: ```bash $ brew update $ brew install carthage ``` To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl github "Alamofire/Alamofire" ~> 4.7 ``` Run `carthage update` to build the framework and drag the built `Alamofire.framework` into your Xcode project. ### Swift Package Manager The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but Alamofire does support its use on supported platforms. Once you have your Swift package set up, adding Alamofire as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`. #### Swift 3 ```swift dependencies: [ .Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4) ] ``` #### Swift 4 ```swift dependencies: [ .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.0.0") ] ``` ### Manually If you prefer not to use any of the aforementioned dependency managers, you can integrate Alamofire into your project manually. #### Embedded Framework - Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository: ```bash $ git init ``` - Add Alamofire as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: ```bash $ git submodule add https://github.com/Alamofire/Alamofire.git ``` - Open the new `Alamofire` folder, and drag the `Alamofire.xcodeproj` into the Project Navigator of your application's Xcode project. > It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter. - Select the `Alamofire.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. - Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar. - In the tab bar at the top of that window, open the "General" panel. - Click on the `+` button under the "Embedded Binaries" section. - You will see two different `Alamofire.xcodeproj` folders each with two different versions of the `Alamofire.framework` nested inside a `Products` folder. > It does not matter which `Products` folder you choose from, but it does matter whether you choose the top or bottom `Alamofire.framework`. - Select the top `Alamofire.framework` for iOS and the bottom one for OS X. > You can verify which one you selected by inspecting the build log for your project. The build target for `Alamofire` will be listed as either `Alamofire iOS`, `Alamofire macOS`, `Alamofire tvOS` or `Alamofire watchOS`. - And that's it! > The `Alamofire.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device. ## Open Radars The following radars have some effect on the current implementation of Alamofire. - [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in test case - `rdar://26870455` - Background URL Session Configurations do not work in the simulator - `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest` - [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+ ## Resolved Radars The following radars have been resolved over time after being filed against the Alamofire project. - [`rdar://26761490`](http://www.openradar.me/radar?id=5010235949318144) - Swift string interpolation causing memory leak with common usage (Resolved on 9/1/17 in Xcode 9 beta 6). ## FAQ ### What's the origin of the name Alamofire? Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu.edu/wildseed/alamofire.html), a hybrid variant of the Bluebonnet, the official state flower of Texas. ### What logic belongs in a Router vs. a Request Adapter? Simple, static data such as paths, parameters and common headers belong in the `Router`. Dynamic data such as an `Authorization` header whose value can changed based on an authentication system belongs in a `RequestAdapter`. The reason the dynamic data MUST be placed into the `RequestAdapter` is to support retry operations. When a `Request` is retried, the original request is not rebuilt meaning the `Router` will not be called again. The `RequestAdapter` is called again allowing the dynamic data to be updated on the original request before retrying the `Request`. ## Credits Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases. ### Security Disclosure If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker. ## Donations The [ASF](https://github.com/Alamofire/Foundation#members) is looking to raise money to officially stay registered as a federal non-profit organization. Registering will allow us members to gain some legal protections and also allow us to put donations to use, tax free. Donating to the ASF will enable us to: - Pay our yearly legal fees to keep the non-profit in good status - Pay for our mail servers to help us stay on top of all questions and security issues - Potentially fund test servers to make it easier for us to test the edge cases - Potentially fund developers to work on one of our projects full-time The community adoption of the ASF libraries has been amazing. We are greatly humbled by your enthusiasm around the projects, and want to continue to do everything we can to move the needle forward. With your continued support, the ASF will be able to improve its reach and also provide better legal safety for the core members. If you use any of our libraries for work, see if your employers would be interested in donating. Any amount you can donate today to help us reach our goal would be greatly appreciated. [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=W34WPEE74APJQ) ## License Alamofire is released under the MIT license. [See LICENSE](https://github.com/Alamofire/Alamofire/blob/master/LICENSE) for details. --- ### Pods/AppAuth/README (Pods/AppAuth/README.md) [](https://github.com/openid/AppAuth-iOS/actions/workflows/tests.yml) [](https://codecov.io/gh/openid/AppAuth-iOS) [](https://github.com/Carthage/Carthage) [](https://swift.org/package-manager) [](https://cocoapods.org/pods/AppAuth) [](https://github.com/openid/AppAuth-iOS/blob/master/LICENSE) [](https://cocoapods.org/pods/AppAuth) [](https://developer.apple.com/documentation/xcode/creating_a_mac_version_of_your_ipad_app) AppAuth for iOS and macOS, and tvOS is a client SDK for communicating with [OAuth 2.0](https://tools.ietf.org/html/rfc6749) and [OpenID Connect](http://openid.net/specs/openid-connect-core-1_0.html) providers. It strives to directly map the requests and responses of those specifications, while following the idiomatic style of the implementation language. In addition to mapping the raw protocol flows, convenience methods are available to assist with common tasks like performing an action with fresh tokens. It follows the best practices set out in [RFC 8252 - OAuth 2.0 for Native Apps](https://tools.ietf.org/html/rfc8252) including using `SFAuthenticationSession` and `SFSafariViewController` on iOS for the auth request. `UIWebView` and `WKWebView` are explicitly *not* supported due to the security and usability reasons explained in [Section 8.12 of RFC 8252](https://tools.ietf.org/html/rfc8252#section-8.12). It also supports the [PKCE](https://tools.ietf.org/html/rfc7636) extension to OAuth, which was created to secure authorization codes in public clients when custom URI scheme redirects are used. The library is friendly to other extensions (standard or otherwise), with the ability to handle additional params in all protocol requests and responses. For tvOS, AppAuth implements [OAuth 2.0 Device Authorization Grant ](https://tools.ietf.org/html/rfc8628) to allow for tvOS sign-ins through a secondary device. ## Specification ### iOS #### Supported Versions AppAuth supports iOS 7 and above. iOS 9+ uses the in-app browser tab pattern (via `SFSafariViewController`), and falls back to the system browser (mobile Safari) on earlier versions. #### Authorization Server Requirements Both Custom URI Schemes (all supported versions of iOS) and Universal Links (iOS 9+) can be used with the library. In general, AppAuth can work with any authorization server that supports native apps, as documented in [RFC 8252](https://tools.ietf.org/html/rfc8252), either through custom URI scheme redirects, or universal links. Authorization servers that assume all clients are web-based, or require clients to maintain confidentiality of the client secrets may not work well. ### macOS #### Supported Versions AppAuth supports macOS (OS X) 10.9 and above. #### Authorization Server Requirements AppAuth for macOS supports both custom schemes; a loopback HTTP redirects via a small embedded server. In general, AppAuth can work with any authorization server that supports native apps, as documented in [RFC 8252](https://tools.ietf.org/html/rfc8252); either through custom URI schemes, or loopback HTTP redirects. Authorization servers that assume all clients are web-based, or require clients to maintain confidentiality of the client secrets may not work well. ### tvOS #### Supported Versions AppAuth supports tvOS 9.0 and above. Please note that while it is possible to run the standard AppAuth library on tvOS, the documentation below describes implementing [OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628) (AppAuthTV). #### Authorization Server Requirements AppAuthTV is designed for servers that support the device authorization flow as documented in [RFC 8628](https://tools.ietf.org/html/rfc8628). ## Try Want to try out AppAuth? Just run: pod try AppAuth Follow the instructions in [Examples/README.md](Examples/README.md) to configure with your own OAuth client (you need to update three configuration points with your client info to try the demo). ## Setup AppAuth supports four options for dependency management. ### CocoaPods With [CocoaPods](https://guides.cocoapods.org/using/getting-started.html), add the following line to your `Podfile`: pod 'AppAuth' Then, run `pod install`. **tvOS:** Use the `TV` subspec: pod 'AppAuth/TV' ### Swift Package Manager With [Swift Package Manager](https://swift.org/package-manager), add the following `dependency` to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.3.0")) ] ``` **tvOS:** Use the `AppAuthTV` target. ### Carthage With [Carthage](https://github.com/Carthage/Carthage), add the following line to your `Cartfile`: github "openid/AppAuth-iOS" "master" Then, run `carthage bootstrap`. **tvOS:** Use the `AppAuthTV` framework. ### Static Library You can also use AppAuth as a static library. This requires linking the library and your project, and including the headers. Here is a suggested configuration: 1. Create an Xcode Workspace. 2. Add `AppAuth.xcodeproj` to your Workspace. 3. Include libAppAuth as a linked library for your target (in the "General -> Linked Framework and Libraries" section of your target). 4. Add `AppAuth-iOS/Source` to your search paths of your target ("Build Settings -> "Header Search Paths"). *Note: There is no static library for AppAuthTV.* ## Auth Flow AppAuth supports both manual interaction with the authorization server where you need to perform your own token exchanges, as well as convenience methods that perform some of this logic for you. This example uses the convenience method, which returns either an `OIDAuthState` object, or an error. `OIDAuthState` is a class that keeps track of the authorization and token requests and responses, and provides a convenience method to call an API with fresh tokens. This is the only object that you need to serialize to retain the authorization state of the session. ### Configuration You can configure AppAuth by specifying the endpoints directly: Objective-C ```objc NSURL *authorizationEndpoint = [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"]; NSURL *tokenEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"]; OIDServiceConfiguration *configuration = [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint]; // perform the auth request... ``` Swift ```swift let authorizationEndpoint = URL(string: "https://accounts.google.com/o/oauth2/v2/auth")! let tokenEndpoint = URL(string: "https://www.googleapis.com/oauth2/v4/token")! let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, tokenEndpoint: tokenEndpoint) // perform the auth request... ``` **tvOS** Objective-C ```objc NSURL *deviceAuthorizationEndpoint = [NSURL URLWithString:@"https://oauth2.googleapis.com/device/code"]; NSURL *tokenEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"]; OIDTVServiceConfiguration *configuration = [[OIDTVServiceConfiguration alloc] initWithDeviceAuthorizationEndpoint:deviceAuthorizationEndpoint tokenEndpoint:tokenEndpoint]; // perform the auth request... ``` Or through discovery: Objective-C ```objc NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"]; [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { if (!configuration) { NSLog(@"Error retrieving discovery document: %@", [error localizedDescription]); return; } // perform the auth request... }]; ``` Swift ```swift let issuer = URL(string: "https://accounts.google.com")! // discovers endpoints OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in guard let config = configuration else { print("Error retrieving discovery document: \(error?.localizedDescription ?? "Unknown error")") return } // perform the auth request... } ``` **tvOS** Objective-C ```objc NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"]; [OIDTVAuthorizationService discoverServiceConfigurationForIssuer:issuer completion:^(OIDTVServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { if (!configuration) { NSLog(@"Error retrieving discovery document: %@", [error localizedDescription]); return; } // perform the auth request... }]; ``` ### Authorizing – iOS First, you need to have a property in your `UIApplicationDelegate` implementation to hold the session, in order to continue the authorization flow from the redirect. In this example, the implementation of this delegate is a class named `AppDelegate`, if your app's application delegate has a different name, please update the class name in samples below accordingly. Objective-C ```objc @interface AppDelegate : UIResponder // property of the app's AppDelegate @property(nonatomic, strong, nullable) id currentAuthorizationFlow; @end ``` Swift ```swift class AppDelegate: UIResponder, UIApplicationDelegate { // property of the app's AppDelegate var currentAuthorizationFlow: OIDExternalUserAgentSession? } ``` And your main class, a property to store the auth state: Objective-C ```objc // property of the containing class @property(nonatomic, strong, nullable) OIDAuthState *authState; ``` Swift ```swift // property of the containing class private var authState: OIDAuthState? ``` Then, initiate the authorization request. By using the `authStateByPresentingAuthorizationRequest` convenience method, the token exchange will be performed automatically, and everything will be protected with PKCE (if the server supports it). AppAuth also lets you perform these requests manually. See the `authNoCodeExchange` method in the included Example app for a demonstration: Objective-C ```objc // builds authentication request OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration clientId:kClientID scopes:@[OIDScopeOpenID, OIDScopeProfile] redirectURL:kRedirectURI responseType:OIDResponseTypeCode additionalParameters:nil]; // performs authentication request AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; appDelegate.currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request presentingViewController:self callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { if (authState) { NSLog(@"Got authorization tokens. Access token: %@", authState.lastTokenResponse.accessToken); [self setAuthState:authState]; } else { NSLog(@"Authorization error: %@", [error localizedDescription]); [self setAuthState:nil]; } }]; ``` Swift ```swift // builds authentication request let request = OIDAuthorizationRequest(configuration: configuration, clientId: clientID, clientSecret: clientSecret, scopes: [OIDScopeOpenID, OIDScopeProfile], redirectURL: redirectURI, responseType: OIDResponseTypeCode, additionalParameters: nil) // performs authentication request print("Initiating authorization request with scope: \(request.scope ?? "nil")") let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: self) { authState, error in if let authState = authState { self.setAuthState(authState) print("Got authorization tokens. Access token: " + "\(authState.lastTokenResponse?.accessToken ?? "nil")") } else { print("Authorization error: \(error?.localizedDescription ?? "Unknown error")") self.setAuthState(nil) } } ``` *Handling the Redirect* The authorization response URL is returned to the app via the iOS openURL app delegate method, so you need to pipe this through to the current authorization session (created in the previous session): Objective-C ```objc - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { _currentAuthorizationFlow = nil; return YES; } // Your additional URL handling (if any) goes here. return NO; } ``` Swift ```swift func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) { self.currentAuthorizationFlow = nil return true } // Your additional URL handling (if any) return false } ``` ### Authorizing – MacOS On macOS, the most popular way to get the authorization response redirect is to start a local HTTP server on the loopback interface (limited to incoming requests from the user's machine only). When the authorization is complete, the user is redirected to that local server, and the authorization response can be processed by the app. AppAuth takes care of managing the local HTTP server lifecycle for you. > #### :bulb: Alternative: Custom URI Schemes > Custom URI schemes are also supported on macOS, but some browsers display > an interstitial, which reduces the usability. For an example on using custom > URI schemes with macOS, See `Example-Mac`. To receive the authorization response using a local HTTP server, first you need to have an instance variable in your main class to retain the HTTP redirect handler: Objective-C ```objc OIDRedirectHTTPHandler *_redirectHTTPHandler; ``` Then, as the port used by the local HTTP server varies, you need to start it before building the authorization request, in order to get the exact redirect URI to use: Objective-C ```objc static NSString *const kSuccessURLString = @"http://openid.github.io/AppAuth-iOS/redirect/"; NSURL *successURL = [NSURL URLWithString:kSuccessURLString]; // Starts a loopback HTTP redirect listener to receive the code. This needs to be started first, // as the exact redirect URI (including port) must be passed in the authorization request. _redirectHTTPHandler = [[OIDRedirectHTTPHandler alloc] initWithSuccessURL:successURL]; NSURL *redirectURI = [_redirectHTTPHandler startHTTPListener:nil]; ``` Then, initiate the authorization request. By using the `authStateByPresentingAuthorizationRequest` convenience method, the token exchange will be performed automatically, and everything will be protected with PKCE (if the server supports it). By assigning the return value to the `OIDRedirectHTTPHandler`'s `currentAuthorizationFlow`, the authorization will continue automatically once the user makes their choice: ```objc // builds authentication request OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration clientId:kClientID clientSecret:kClientSecret scopes:@[ OIDScopeOpenID ] redirectURL:redirectURI responseType:OIDResponseTypeCode additionalParameters:nil]; // performs authentication request __weak __typeof(self) weakSelf = self; _redirectHTTPHandler.currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { // Brings this app to the foreground. [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; // Processes the authorization response. if (authState) { NSLog(@"Got authorization tokens. Access token: %@", authState.lastTokenResponse.accessToken); } else { NSLog(@"Authorization error: %@", error.localizedDescription); } [weakSelf setAuthState:authState]; }]; ``` ### Authorizing – tvOS Ensure that your main class is a delegate of `OIDAuthStateChangeDelegate`, `OIDAuthStateErrorDelegate`, implement the corresponding methods, and include the following property and instance variable: Objective-C ```objc // property of the containing class @property(nonatomic, strong, nullable) OIDAuthState *authState; // instance variable of the containing class OIDTVAuthorizationCancelBlock _cancelBlock; ``` Then, build and perform the authorization request. Objective-C ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ### Making API Calls AppAuth gives you the raw token information, if you need it. However, we recommend that users of the `OIDAuthState` convenience wrapper use the provided `performActionWithFreshTokens:` method to perform their API calls to avoid needing to worry about token freshness: Objective-C ```objc [_authState performActionWithFreshTokens:^(NSString *_Nonnull accessToken, NSString *_Nonnull idToken, NSError *_Nullable error) { if (error) { NSLog(@"Error fetching fresh tokens: %@", [error localizedDescription]); return; } // perform your API request using the tokens }]; ``` Swift ```swift let userinfoEndpoint = URL(string:"https://openidconnect.googleapis.com/v1/userinfo")! self.authState?.performAction() { (accessToken, idToken, error) in if error != nil { print("Error fetching fresh tokens: \(error?.localizedDescription ?? "Unknown error")") return } guard let accessToken = accessToken else { return } // Add Bearer token to request var urlRequest = URLRequest(url: userinfoEndpoint) urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] // Perform request... } ``` ### Custom User-Agents (iOS and macOS) Each OAuth flow involves presenting an external user-agent to the user, that allows them to interact with the OAuth authorization server. Typical examples of a user-agent are the user's browser, or an in-app browser tab incarnation like `ASWebAuthenticationSession` on iOS. AppAuth ships with several implementations of an external user-agent out of the box, including defaults for iOS and macOS suitable for most cases. The default user-agents typically share persistent cookies with the system default browser, to improve the chance that the user doesn't need to sign-in all over again. It is possible to change the user-agent that AppAuth uses, and even write your own - all without needing to fork the library. All implementations of the external user-agent, be they included or created by you need to conform to the [`OIDExternalUserAgent`](http://openid.github.io/AppAuth-iOS/docs/latest/protocol_o_i_d_external_user_agent-p.html) protocol. Instances of the `OIDExternalUserAgent`are passed into [`OIDAuthState.authStateByPresentingAuthorizationRequest:externalUserAgent:callback`](http://openid.github.io/AppAuth-iOS/docs/latest/interface_o_i_d_auth_state.html#ac762fe2bf95c116f0b437419be211fa1) and/or [`OIDAuthorizationService.presentAuthorizationRequest:externalUserAgent:callback:`](http://openid.github.io/AppAuth-iOS/docs/latest/interface_o_i_d_authorization_service.html#ae551f8e6887366a46e49b09b37389b8f) rather than using the platform-specific convenience methods (which use the default user-agents for their respective platforms), like [`OIDAuthState.authStateByPresentingAuthorizationRequest:presentingViewController:callback:`](http://openid.github.io/AppAuth-iOS/docs/latest/category_o_i_d_auth_state_07_i_o_s_08.html#ae32fd0732cd3192cd5219f2655a4c85c). Popular use-cases for writing your own user-agent implementation include needing to style the user-agent in ways not supported by AppAuth, and implementing a fully custom flow with your own business logic. You can take one of the existing implementations as a starting point to copy, rename, and customize to your needs. #### Custom Browser User-Agent AppAuth for iOS includes a few extra user-agent implementations which you can try, or use as a reference for your own implementation. One of them, [`OIDExternalUserAgentIOSCustomBrowser`](http://openid.github.io/AppAuth-iOS/docs/latest/interface_o_i_d_external_user_agent_i_o_s_custom_browser.html) enables you to use a different browser for authentication, like Chrome for iOS or Firefox for iOS. Here's how to configure AppAuth to use a custom browser using the `OIDExternalUserAgentIOSCustomBrowser` user agent: First, add the following array to your [Info.plist](https://github.com/openid/AppAuth-iOS/blob/135f99d2cb4e9d18d310ac2588b905e612461561/Examples/Example-iOS_ObjC/Source/Info.plist#L34) (in XCode, right click -> Open As -> Source Code) ``` LSApplicationQueriesSchemes googlechromes opera-https firefox ``` This is required so that AppAuth can test for the browser and open the app store if it's not installed (the default behavior of this user-agent). You only need to include the URL scheme of the actual browser you intend to use. Objective-C ```objc // performs authentication request AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; id userAgent = [OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome]; appDelegate.currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:userAgent callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { if (authState) { NSLog(@"Got authorization tokens. Access token: %@", authState.lastTokenResponse.accessToken); [self setAuthState:authState]; } else { NSLog(@"Authorization error: %@", [error localizedDescription]); [self setAuthState:nil]; } }]; ``` Swift ``` guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { self.logMessage("Error accessing AppDelegate") return } let userAgent = OIDExternalUserAgentIOSCustomBrowser.customBrowserChrome() appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, externalUserAgent: userAgent) { authState, error in if let authState = authState { self.setAuthState(authState) self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")") } else { self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")") self.setAuthState(nil) } } ``` That's it! With those two changes (which you can try on the included sample), AppAuth will use Chrome iOS for the authorization request (and open Chrome in the App Store if it's not installed). ⚠️**Note: the `OIDExternalUserAgentIOSCustomBrowser` user-agent is not intended for consumer apps**. It is designed for advanced enterprise use-cases where the app developers have greater control over the operating environment and have special requirements that require a custom browser like Chrome. You don't need to stop with the included external user agents either! Since the [`OIDExternalUserAgent`](http://openid.github.io/AppAuth-iOS/docs/latest/protocol_o_i_d_external_user_agent-p.html) protocol is part of AppAuth's public API, you can implement your own versions of it. In the above example, `userAgent = [OIDExternalUserAgentIOSCustomBrowser CustomBrowserChrome]` would be replaced with an instantiation of your user-agent implementation. ## API Documentation Browse the [API documentation](http://openid.github.io/AppAuth-iOS/docs/latest/annotated.html). ## Included Samples Sample apps that explore core AppAuth features are available for iOS, macOS and tvOS; follow the instructions in [Examples/README.md](Examples/README.md) to get started. --- ### Pods/GoogleAPIClientForREST/README (Pods/GoogleAPIClientForREST/README.md) # Google APIs Client Library for Objective-C for REST # **Project site**
**Discussion group** [](https://travis-ci.org/google/google-api-objectivec-client-for-rest) Written by Google, this library is a flexible and efficient Objective-C framework for accessing JSON APIs. This is the recommended library for accessing JSON-based Google APIs for iOS and Mac OS X applications. The library is compatible with applications built for iOS 7 and later, and Mac OS X 10.9 and later. **To get started** with Google APIs and the Objective-C client library, Read the [wiki](https://github.com/google/google-api-objectivec-client-for-rest/wiki). See [BuildingTheLibrary](https://github.com/google/google-api-objectivec-client-for-rest/wiki/BuildingTheLibrary) for how to add the library to a Mac or iPhone application project, it covers directly adding sources or using CocoaPods. Study the [example applications](https://github.com/google/google-api-objectivec-client-for-rest/tree/master/Examples). Generated interfaces for Google APIs are in the [GeneratedServices folder](https://github.com/google/google-api-objectivec-client-for-rest/tree/master/Source/GeneratedServices). In addition to the pre generated classes included with the library, you can generate your own source for other services that have a [discovery document](https://developers.google.com/discovery/v1/reference/apis#resource-representations) by using the [ServiceGenerator](https://github.com/google/google-api-objectivec-client-for-rest/wiki/ServiceGenerator). **If you have a problem** or want a new feature to be included in the library, please join the [discussion group](http://groups.google.com/group/google-api-objectivec-client). Be sure to include [http logs](https://github.com/google/google-api-objectivec-client-for-rest/wiki#logging-http-server-traffic) for requests and responses when posting questions. Bugs may also be submitted on the [issues list](https://github.com/google/google-api-objectivec-client-for-rest/issues). **Externally-included projects**: The library is built on top of code from the separate project [GTM Session Fetcher](https://github.com/google/gtm-session-fetcher). To work with some remote services, it also needs [Authentication/Authorization](https://github.com/google/google-api-objectivec-client-for-rest/wiki#authentication-and-authorization). **Google Data APIs**: The much older library for XML-based APIs is [still available](https://github.com/google/gdata-objectivec-client). Other useful classes for Mac and iOS developers are available in the [Google Toolbox for Mac](https://github.com/google/google-toolbox-for-mac). --- ### Pods/GoogleSignIn/README (Pods/GoogleSignIn/README.md) [](https://cocoapods.org/pods/GoogleSignIn) [](https://cocoapods.org/pods/GoogleSignIn) [](https://cocoapods.org/pods/GoogleSignIn) [](https://github.com/google/GoogleSignIn-iOS/actions/workflows/tests.yml) # Google Sign-In for iOS and macOS Get users into your apps quickly and securely, using a registration system they already use and trust—their Google account. Visit [our developer site](https://developers.google.com/identity/sign-in/ios/) for integration instructions, documentation, support information, and terms of service. ## Getting Started Try either the [Objective-C](Samples/ObjC) or [Swift](Samples/Swift) sample app. For example, to demo the Objective-C sample project, you have three options: 1. Using [CocoaPods](https://cocoapods.org/)'s `try` method: ``` pod try GoogleSignIn ``` Note, this will default to providing you with the Objective-C sample app. 2. Using CocoaPod's `install` method: ``` git clone https://github.com/google/GoogleSignIn-iOS cd GoogleSignIn-iOS/Samples/ObjC/SignInSample/ pod install open SignInSampleForPod.xcworkspace ``` 3. Using [Swift Package Manager](https://swift.org/package-manager/): ``` git clone https://github.com/google/GoogleSignIn-iOS open GoogleSignIn-iOS/Samples/ObjC/SignInSample/SignInSample.xcodeproj ``` If you would like to see a Swift example, take a look at [Samples/Swift/DaysUntilBirthday](Samples/Swift/DaysUntilBirthday). * Add Google Sign-In to your own app by following our [getting started guides](https://developers.google.com/identity/sign-in/ios/start-integrating). * Take a look at the [API reference](https://developers.google.com/identity/sign-in/ios/api/). ## Google Sign-In on macOS Google Sign-In allows your users to sign-in to your native macOS app using their Google account and default browser. When building for macOS, the `signInWithConfiguration:` and `addScopes:` methods take a `presentingWindow:` parameter in place of `presentingViewController:`. Note that in order for your macOS app to store credientials via the Keychain on macOS, you will need to [sign your app](https://developer.apple.com/support/code-signing/). ### Mac Catalyst Google Sign-In also supports iOS apps that are built for macOS via [Mac Catalyst](https://developer.apple.com/mac-catalyst/). In order for your Mac Catalyst app to store credientials via the Keychain on macOS, you will need to [sign your app](https://developer.apple.com/support/code-signing/). ## Using the Google Sign-In Button There are several ways to add a 'Sign in with Google' button to your app, which path you choose will depend on your UI framework and target platform. ### SwiftUI (iOS and macOS) Creating a 'Sign in with Google' button in SwiftUI can be as simple as this: ``` GoogleSignInButton { GIDSignIn.sharedInstance.signIn( with: configuration, presenting: yourViewController) { user, error in // check `error`; do something with `user` } } ``` This example takes advantage of the initializer's [default argument for the view model](GoogleSignInSwift/Sources/GoogleSignInButton.swift#L39). The default arguments for the view model will use the light scheme, the standard button style, and the normal button state. You can supply an instance of [`GoogleSignInButtonViewModel`](GoogleSignInSwift/Sources/GoogleSignInButtonViewModel.swift) with different values for these properties to customize the button. [This convenience initializer](GoogleSignInSwift/Sources/GoogleSignInButton.swift#L56) provides parameters that you can use to set these values as needed. ### UIKit (iOS) If you are not using SwiftUI to build your user interfaces, you can either create `GIDSignInButton` programmatically, or in a Xib/Storyboard. If you are writing programmatic UI code, it will look something like this: `let button = GIDSignInButton(frame: CGRect())` ### AppKit (macOS) Given that `GIDSignInButton` is implemented as a subclass of `UIControl`, it will not be available on macOS. You can instead use the SwiftUI Google sign-in button. Doing so will require that you wrap the SwiftUI button in a hosting view so that it will be available for use in AppKit. ``` let signInButton = GoogleSignInButton { GIDSignIn.sharedInstance.signIn( with: configuration, presenting: yourViewController) { user, error in // check `error`; do something with `user` } } let hostedButton = NSHostingView(rootView: signInButton) ``` --- ### Pods/GTMAppAuth/README (Pods/GTMAppAuth/README.md) [](https://cocoapods.org/pods/GTMAppAuth) [](https://cocoapods.org/pods/GTMAppAuth) [](https://cocoapods.org/pods/GTMAppAuth) [](https://github.com/google/GTMAppAuth/actions/workflows/tests.yml) # GTMAppAuth for Apple Platforms GTMAppAuth enables you to use [AppAuth](https://github.com/openid/AppAuth-iOS) with the [Google Toolbox for Mac - Session Fetcher](https://github.com/google/gtm-session-fetcher) and [Google APIs Client Library for Objective-C For REST](https://github.com/google/google-api-objectivec-client-for-rest) libraries on iOS, macOS, tvOS, and watchOS by providing an implementation of [`GTMFetcherAuthorizationProtocol`](https://github.com/google/gtm-session-fetcher/blob/2a3b5264108e80d62003b770ff02eb7364ff1365/Source/GTMSessionFetcher.h#L660) for authorizing requests with AppAuth. GTMAppAuth is an alternative authorizer to [GTMOAuth2](https://github.com/google/gtm-oauth2) . The key differentiator is the use of the user's default browser for the authorization, which is more secure, more usable (the user's session can be reused) and follows modern OAuth [best practices for native apps](https://datatracker.ietf.org/doc/html/rfc8252). Compatibility methods for GTMOAuth2 are offered allowing you to migrate from GTMOAuth2 to GTMAppAuth preserving previously serialized authorizations (so users shouldn't need to re-authenticate). ## Setup If you use [CocoaPods](https://guides.cocoapods.org/using/getting-started.html), simply add: pod 'GTMAppAuth' To your `Podfile` and run `pod install`. ## Usage ### Configuration To configure GTMAppAuth with the OAuth endpoints for Google, you can use the convenience method: ```objc OIDServiceConfiguration *configuration = [GTMAppAuthFetcherAuthorization configurationForGoogle]; ``` Alternatively, you can configure GTMAppAuth by specifying the endpoints directly: ```objc NSURL *authorizationEndpoint = [NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"]; NSURL *tokenEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"]; OIDServiceConfiguration *configuration = [[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint tokenEndpoint:tokenEndpoint]; // perform the auth request... ``` Or through discovery: ```objc NSURL *issuer = [NSURL URLWithString:@"https://accounts.google.com"]; [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuer completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { if (!configuration) { NSLog(@"Error retrieving discovery document: %@", [error localizedDescription]); return; } // perform the auth request... }]; ``` ### Authorizing First, you need to have a way for your UIApplicationDelegate to continue the authorization flow session from the incoming redirect URI. Typically you could store the in-progress OIDAuthorizationFlowSession instance in a property: ```objc // property of the app's UIApplicationDelegate @property(nonatomic, nullable) id currentAuthorizationFlow; ``` And in a location accessible by all controllers that need authorization, a property to store the authorization state: ```objc // property of the containing class @property(nonatomic, nullable) GTMAppAuthFetcherAuthorization *authorization; ``` Then, initiate the authorization request. By using the `authStateByPresentingAuthorizationRequest` method, the OAuth token exchange will be performed automatically, and everything will be protected with PKCE (if the server supports it). ```objc // builds authentication request OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:configuration clientId:kClientID clientSecret:kClientSecret scopes:@[OIDScopeOpenID, OIDScopeProfile] redirectURL:redirectURI responseType:OIDResponseTypeCode additionalParameters:nil]; // performs authentication request self.appDelegate.currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { if (authState) { // Creates the GTMAppAuthFetcherAuthorization from the OIDAuthState. GTMAppAuthFetcherAuthorization *authorization = [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState]; self.authorization = authorization; NSLog(@"Got authorization tokens. Access token: %@", authState.lastTokenResponse.accessToken); } else { NSLog(@"Authorization error: %@", [error localizedDescription]); self.authorization = nil; } }]; ``` ### Handling the Redirect The authorization response URL is returned to the app via the platform-specific application delegate method, so you need to pipe this through to the current authorization session (created in the previous session). #### macOS Custom URI Scheme Redirect Example ```objc - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Other app initialization code ... // Register for GetURL events. NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; } - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; NSURL *URL = [NSURL URLWithString:URLString]; [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL]; } ``` #### iOS Custom URI Scheme Redirect Example ```objc - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { // Sends the URL to the current authorization flow (if any) which will // process it if it relates to an authorization response. if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { _currentAuthorizationFlow = nil; return YES; } // Your additional URL handling (if any) goes here. return NO; } ``` ### Making API Calls The goal of GTMAppAuth is to enable you to authorize HTTP requests with fresh tokens following the Session Fetcher pattern, which you can do like so: ```objc // Creates a GTMSessionFetcherService with the authorization. // Normally you would save this service object and re-use it for all REST API calls. GTMSessionFetcherService *fetcherService = [[GTMSessionFetcherService alloc] init]; fetcherService.authorizer = self.authorization; // Creates a fetcher for the API call. NSURL *userinfoEndpoint = [NSURL URLWithString:@"https://www.googleapis.com/oauth2/v3/userinfo"]; GTMSessionFetcher *fetcher = [fetcherService fetcherWithURL:userinfoEndpoint]; [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { // Checks for an error. if (error) { // OIDOAuthTokenErrorDomain indicates an issue with the authorization. if ([error.domain isEqual:OIDOAuthTokenErrorDomain]) { self.authorization = nil; NSLog(@"Authorization error during token refresh, clearing state. %@", error); // Other errors are assumed transient. } else { NSLog(@"Transient error during token refresh. %@", error); } return; } // Parses the JSON response. NSError *jsonError = nil; id jsonDictionaryOrArray = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; // JSON error. if (jsonError) { NSLog(@"JSON decoding error %@", jsonError); return; } // Success response! NSLog(@"Success: %@", jsonDictionaryOrArray); }]; ``` ### Saving to the Keychain You can easily save `GTMAppAuthFetcherAuthorization` instances to the Keychain using the included `GTMAppAuthFetcherAuthorization+Keychain` category. ```objc // Save to Keychain [GTMAppAuthFetcherAuthorization saveAuthorization:_authorization toKeychainForName:kGTMAppAuthExampleAuthorizerKey]; // Restore from Keychain GTMAppAuthFetcherAuthorization* authorization = [GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kGTMAppAuthExampleAuthorizerKey]; // Remove from Keychain [GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kGTMAppAuthExampleAuthorizerKey]; ``` #### Keychain Storage `GTMAppAuthFetcherAuthorization` instances are stored using Keychain items of the [`kSecClassGenericPassword`](https://developer.apple.com/documentation/security/ksecclassgenericpassword?language=objc) class with a [`kSecAttrAccount`](https://developer.apple.com/documentation/security/ksecattraccount?language=objc) value of "OAuth" and a developer supplied value for [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice?language=objc). For this use of generic password items, the combination of account and service values acts as the [primary key](https://developer.apple.com/documentation/security/1542001-security_framework_result_codes/errsecduplicateitem?language=objc) of the Keychain items. The [`kSecAttrAccessible`](https://developer.apple.com/documentation/security/ksecattraccessible?language=objc) key is set to [`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`](https://developer.apple.com/documentation/security/ksecattraccessibleafterfirstunlockthisdeviceonly?language=objc) in order to allow background access after initial device unlock following a restart. A [keyed archive](https://developer.apple.com/documentation/foundation/nskeyedarchiver?language=objc) representation of the relevant `GTMAppAuthFetcherAuthorization` instance is supplied as the value for [`kSecValueData`](https://developer.apple.com/documentation/security/ksecvaluedata?language=objc) and this is encrypted and stored by [Keychain Services](https://developer.apple.com/documentation/security/keychain_services?language=objc). For macOS, two Keychain storage options are available: the traditional file-based Keychain storage which uses access control lists and the more modern [data protection keychain storage](https://developer.apple.com/documentation/security/ksecusedataprotectionkeychain?language=objc) which uses Keychain access control groups. By default, GTMAppAuth uses the file-based Keychain storage on macOS. You may opt into using data protection keychain storage by using the parameter `useDataProtectionKeychain:YES` in your method calls. Note that Keychain items stored via one storage type will not be available via the other and macOS apps that choose to use the data protection Keychain will need to be signed in order for Keychain operations to succeed. #### GTMOAuth2 Compatibility To assist the migration from GTMOAuth2 to GTMAppAuth, GTMOAuth2-compatible Keychain methods are provided in `GTMOAuth2KeychainCompatibility`. ```objc // Restore from Keychain GTMAppAuthFetcherAuthorization *auth = [GTMOAuth2KeychainCompatibility authForGoogleFromKeychainForName:kKeychainItemName clientID:clientID clientSecret:clientSecret]; // Remove from Keychain [GTMOAuth2KeychainCompatibility removeAuthFromKeychainForName:kKeychainItemName]; ``` You can also save to GTMOAuth2 format, though this is discouraged (you should save in GTMAppAuth format as described above). ```objc // Save to Keychain [GTMOAuth2KeychainCompatibility saveAuthToKeychainForName:kKeychainItemName authentication:authorization]; ``` ## Included Samples Try out one of the included sample apps under [Examples](Examples). In the apps folder run `pod install`, then open the resulting `xcworkspace` file. Be sure to follow the instructions in [Example-iOS/README.md](Examples/Example-iOS/README.md) or [Example-macOS/README.md](Examples/Example-macOS/README.md) to configure your own OAuth client ID for use with the example. ## Differences with GTMOAuth2 ### Authorization Method GTMAppAuth uses the browser to present the authorization request, while GTMOAuth2 uses an embedded web-view. Migrating to GTMAppAuth will require you to change how you authorize the user. Follow the instructions above to get the authorization. You can then create a `GTMAppAuthFetcherAuthorization` object with the `initWithAuthState:authState` initializer. Once you have the `GTMAppAuthFetcherAuthorization` you can continue to make REST calls as before. ### Error Handling GTMAppAuth's error handling is also different. There are no notifications, instead you need to inspect NSError in the callback. If the error domain is `OIDOAuthTokenErrorDomain`, it indicates an authorization error, you should clear your authorization state and consider prompting the user to authorize again. Other errors are generally considered transient, meaning that you should retry the request after a delay. ### Serialization The serialization format is different between GTMOAuth2 and GTMAppAuth, though we have methods to help you migrate from one to the other without losing any data. ## Migrating from GTMOAuth2 ### OAuth Client Registration Typically, GTMOAuth2 clients are registered with Google as type "Other". This is correct for macOS, but on iOS clients should be registered with the type "iOS". If you're migrating an iOS client, in the *same project as your existing client*, [register a new iOS client](https://console.developers.google.com/apis/credentials?project=_) to be used with GTMAppAuth. ### Changing your Authorization Flows Both GTMOAuth2 and GTMAppAuth support the `GTMFetcherAuthorizationProtocol` allowing you to use the authorization with the session fetcher. Where you previously had a property like `GTMOAuth2Authentication *authorization` change the type to reference the protocol instead, i.e.: `id authorization`. This allows you to switch the authorization implementation under the hood to GTMAppAuth. Then, follow the instructions above to replace authorization request (where you ask the user to grant access) with the GTMAppAuth approach. If you created a new OAuth client, use that for these requests. ### Serialization & Migrating Existing Grants GTMAppAuth has a new data format and APIs for serialization. Unlike GTMOAuth2, GTMAppAuth serializes the configuration and history of the authorization, including the client id, and a record of the authorization request that resulted in the authorization grant. The client ID used for GTMAppAuth is [different](#oauth-client-registration) to the one used for GTMOAuth2. In order to keep track of the different client ids used for new and old grants, it's recommended to migrate to the new serialization format, which will store that for you. [GTMOAuth2-compatible serialization](#gtmoauth2-compatible-serialization) is also offered, but not fully supported. Change how you serialize your `authorization` object using the new methods using the following example. ```objc // Serialize to Keychain [GTMAppAuthFetcherAuthorization saveAuthorization:(GTMAppAuthFetcherAuthorization *)authorization toKeychainForName:kNewKeychainName]; ``` Be sure to use a *new* name for the keychain. Don't reuse your old one! For deserializing, we can preserve all existing grants (so users who authorized your app in GTMOAuth2 don't have to authorize it again). Remember that when deserializing the *old* data you need to use your *old* keychain name, and the old client id and client secret (if those changed), and that when serializing to the *new* format, use the *new* keychain name. Once again, pay particular care to use the old details when deserializing the GTMOAuth2 keychain, and the new details for all other GTMAppAuth calls. Keychain migration example: ```objc // Attempt to deserialize from Keychain in GTMAppAuth format. id authorization = [GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kNewKeychainName]; // If no data found in the new format, try to deserialize data from GTMOAuth2 if (!authorization) { // Tries to load the data serialized by GTMOAuth2 using old keychain name. // If you created a new client id, be sure to use the *previous* client id and secret here. authorization = [GTMOAuth2KeychainCompatibility authForGoogleFromKeychainForName:kPreviousKeychainName clientID:kPreviousClientID clientSecret:kPreviousClientSecret]; if (authorization) { // Remove previously stored GTMOAuth2-formatted data. [GTMOAuth2KeychainCompatibility removeAuthFromKeychainForName:kPreviousKeychainName]; // Serialize to Keychain in GTMAppAuth format. [GTMAppAuthFetcherAuthorization saveAuthorization:(GTMAppAuthFetcherAuthorization *)authorization toKeychainForName:kNewKeychainName]; } } ``` --- ### Pods/GTMSessionFetcher/README (Pods/GTMSessionFetcher/README.md) # Google Toolbox for Mac - Session Fetcher **Project site**
**Discussion group** [](https://github.com/google/gtm-session-fetcher/actions/workflows/swiftpm.yml) [](https://github.com/google/gtm-session-fetcher/actions/workflows/cocoapods.yml) `GTMSessionFetcher` makes it easy for Cocoa applications to perform http operations. The fetcher is implemented as a wrapper on `NSURLSession`, so its behavior is asynchronous and uses operating-system settings. Features include: - Simple to build; only one source/header file pair is required - Simple to use: takes just two lines of code to fetch a request - Supports upload and download sessions - Flexible cookie storage - Automatic retry on errors, with exponential backoff - Support for generating multipart MIME upload streams - Easy, convenient logging of http requests and responses - Supports plug-in authentication such as with GTMAppAuth - Easily testable; self-mocking - Automatic rate limiting when created by the `GTMSessionFetcherService` factory class - Fully independent of other projects **To get started** please read [USING.md](https://github.com/google/google-api-objectivec-client-for-rest/blob/main/USING.md) for detailed information. --- ### Pods/Local Podspecs/Harmony.Podspec.Json (Pods/Local Podspecs/Harmony.podspec.json) { "name": "Harmony", "version": "0.1", "summary": "iOS Syncing Framework", "description": "iOS framework that automatically syncs Core Data databases across different backends.", "homepage": "https://github.com/rileytestut/Harmony", "platforms": { "ios": "14.0" }, "source": { "git": "https://github.com/rileytestut/Harmony.git" }, "authors": { "Riley Testut": "riley@rileytestut.com" }, "social_media_url": "https://twitter.com/rileytestut", "source_files": "Harmony/**/*.{h,m,swift}", "public_header_files": "Harmony/Harmony.h", "header_mappings_dir": "", "resources": [ "Harmony/**/*.xcdatamodeld", "Harmony/**/*.xcmappingmodel" ], "dependencies": { "Roxas": [] }, "subspecs": [ { "name": "Harmony-Dropbox", "source_files": "Backends/Dropbox/Harmony-Dropbox/**/*.swift", "dependencies": { "SwiftyDropbox": [ "~> 5.0.0" ] } }, { "name": "Harmony-Drive", "source_files": "Backends/Drive/Harmony-Drive/**/*.swift", "dependencies": { "GoogleAPIClientForREST/Drive": [ "~> 1.3.0" ], "GoogleSignIn": [ "~> 6.2" ] } } ] } --- ### Pods/Local Podspecs/Roxas.Podspec.Json (Pods/Local Podspecs/Roxas.podspec.json) { "name": "Roxas", "version": "0.1", "summary": "Private iOS Development Framework", "description": "Private iOS Development Framework used by me in my projects.", "homepage": "https://github.com/rileytestut/roxas", "platforms": { "ios": "12.0" }, "source": { "git": "http://github.com/rileytestut/Roxas.git" }, "authors": { "Riley Testut": "riley@rileytestut.com" }, "social_media_url": "https://twitter.com/rileytestut", "source_files": "Roxas/*.{h,m}", "public_header_files": "Roxas/*.h", "private_header_files": "Roxas/RSTCellContentDataSource_Subclasses.h", "prefix_header_file": "Roxas/Roxas-Prefix.pch", "resources": "Roxas/*.xib" } --- ### Pods/SDWebImage/README (Pods/SDWebImage/README.md) Web Image ========= [](https://travis-ci.org/rs/SDWebImage) [](http://cocoadocs.org/docsets/SDWebImage/) [](http://cocoadocs.org/docsets/SDWebImage/) [](https://www.apache.org/licenses/LICENSE-2.0.html) [](https://www.versioneye.com/objective-c/sdwebimage/3.3) [](https://www.versioneye.com/objective-c/sdwebimage/references) [](https://github.com/rs/SDWebImage) This library provides a category for UIImageView with support for remote images coming from the web. It provides: - An `UIImageView` category adding web image and cache management to the Cocoa Touch framework - An asynchronous image downloader - An asynchronous memory + disk image caching with automatic cache expiration handling - Animated GIF support - WebP format support - A background image decompression - A guarantee that the same URL won't be downloaded several times - A guarantee that bogus URLs won't be retried again and again - A guarantee that main thread will never be blocked - Performances! - Use GCD and ARC - Arm64 support NOTE: Version 3.8 of SDWebImage requires iOS 7 or later (because of NSURLSession). Versions 3.7 to 3.0 requires iOS 5.1.1. If you need iOS < 5.0 support, please use the last [2.0 version](https://github.com/rs/SDWebImage/tree/2.0-compat). [How is SDWebImage better than X?](https://github.com/rs/SDWebImage/wiki/How-is-SDWebImage-better-than-X%3F) Who Uses It ---------- Find out [who uses SDWebImage](https://github.com/rs/SDWebImage/wiki/Who-Uses-SDWebImage) and add your app to the list. How To Use ---------- API documentation is available at [CocoaDocs - SDWebImage](http://cocoadocs.org/docsets/SDWebImage/) ### Using UIImageView+WebCache category with UITableView Just #import the UIImageView+WebCache.h header, and call the sd_setImageWithURL:placeholderImage: method from the tableView:cellForRowAtIndexPath: UITableViewDataSource method. Everything will be handled for you, from async downloads to caching management. ```objective-c #import ... - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *MyIdentifier = @"MyIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease]; } // Here we use the new provided sd_setImageWithURL: method to load the web image [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]]; cell.textLabel.text = @"My Text"; return cell; } ``` ### Using blocks With blocks, you can be notified about the image download progress and whenever the image retrieval has completed with success or not: ```objective-c // Here we use the new provided sd_setImageWithURL: method to load the web image [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { ... completion code here ... }]; ``` Note: neither your success nor failure block will be call if your image request is canceled before completion. ### Using SDWebImageManager The SDWebImageManager is the class behind the UIImageView+WebCache category. It ties the asynchronous downloader with the image cache store. You can use this class directly to benefit from web image downloading with caching in another context than a UIView (ie: with Cocoa). Here is a simple example of how to use SDWebImageManager: ```objective-c SDWebImageManager *manager = [SDWebImageManager sharedManager]; [manager downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) { // progression tracking code } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { if (image) { // do something with image } }]; ``` ### Using Asynchronous Image Downloader Independently It's also possible to use the async image downloader independently: ```objective-c SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader]; [downloader downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) { // progression tracking code } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) { if (image && finished) { // do something with image } }]; ``` ### Using Asynchronous Image Caching Independently It is also possible to use the async based image cache store independently. SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed asynchronous so it doesn't add unnecessary latency to the UI. The SDImageCache class provides a singleton instance for convenience but you can create your own instance if you want to create separated cache namespace. To lookup the cache, you use the `queryDiskCacheForKey:done:` method. If the method returns nil, it means the cache doesn't currently own the image. You are thus responsible for generating and caching it. The cache key is an application unique identifier for the image to cache. It is generally the absolute URL of the image. ```objective-c SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"]; [imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) { // image is not nil if image was found }]; ``` By default SDImageCache will lookup the disk cache if an image can't be found in the memory cache. You can prevent this from happening by calling the alternative method `imageFromMemoryCacheForKey:`. To store an image into the cache, you use the storeImage:forKey: method: ```objective-c [[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey]; ``` By default, the image will be stored in memory cache as well as on disk cache (asynchronously). If you want only the memory cache, use the alternative method storeImage:forKey:toDisk: with a negative third argument. ### Using cache key filter Sometime, you may not want to use the image URL as cache key because part of the URL is dynamic (i.e.: for access control purpose). SDWebImageManager provides a way to set a cache key filter that takes the NSURL as input, and output a cache key NSString. The following example sets a filter in the application delegate that will remove any query-string from the URL before to use it as a cache key: ```objective-c - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { SDWebImageManager.sharedManager.cacheKeyFilter = ^(NSURL *url) { url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path]; return [url absoluteString]; }; // Your app init code... return YES; } ``` Common Problems --------------- ### Using dynamic image size with UITableViewCell UITableView determines the size of the image by the first image set for a cell. If your remote images don't have the same size as your placeholder image, you may experience strange anamorphic scaling issue. The following article gives a way to workaround this issue: [http://www.wrichards.com/blog/2011/11/sdwebimage-fixed-width-cell-images/](http://www.wrichards.com/blog/2011/11/sdwebimage-fixed-width-cell-images/) ### Handle image refresh SDWebImage does very aggressive caching by default. It ignores all kind of caching control header returned by the HTTP server and cache the returned images with no time restriction. It implies your images URLs are static URLs pointing to images that never change. If the pointed image happen to change, some parts of the URL should change accordingly. If you don't control the image server you're using, you may not be able to change the URL when its content is updated. This is the case for Facebook avatar URLs for instance. In such case, you may use the `SDWebImageRefreshCached` flag. This will slightly degrade the performance but will respect the HTTP caching control headers: ``` objective-c [imageView sd_setImageWithURL:[NSURL URLWithString:@"https://graph.facebook.com/olivier.poitrey/picture"] placeholderImage:[UIImage imageNamed:@"avatar-placeholder.png"] options:SDWebImageRefreshCached]; ``` ### Add a progress indicator See this category: https://github.com/JJSaccolo/UIActivityIndicator-for-SDWebImage Installation ------------ There are three ways to use SDWebImage in your project: - using CocoaPods - copying all the files into your project - importing the project as a static library ### Installation with CocoaPods [CocoaPods](http://cocoapods.org/) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries in your projects. See the [Get Started](http://cocoapods.org/#get_started) section for more details. #### Podfile ``` platform :ios, '7.0' pod 'SDWebImage', '~>3.8' ``` If you are using Swift, be sure to add `use_frameworks!` and set your target to iOS 8+: ``` platform :ios, '8.0' use_frameworks! ``` #### Subspecs There are 3 subspecs available now: `Core`, `MapKit` and `WebP` (this means you can install only some of the SDWebImage modules. By default, you get just `Core`, so if you need `WebP`, you need to specify it). Podfile example: ``` pod 'SDWebImage/WebP' ``` ### Installation with Carthage (iOS 8+) [Carthage](https://github.com/Carthage/Carthage) is a lightweight dependency manager for Swift and Objective-C. It leverages CocoaTouch modules and is less invasive than CocoaPods. To install with carthage, follow the instruction on [Carthage](https://github.com/Carthage/Carthage) #### Cartfile ``` github "rs/SDWebImage" ``` #### Usage Swift If you installed using CocoaPods: ``` import SDWebImage ``` If you installed manually: ``` import WebImage ``` Objective-C ``` @import WebImage; ``` ### Installation by cloning the repository In order to gain access to all the files from the repository, you should clone it. ``` git clone --recursive https://github.com/rs/SDWebImage.git ``` ### Add the SDWebImage project to your project - Download and unzip the last version of the framework from the [download page](https://github.com/rs/SDWebImage/releases) - Right-click on the project navigator and select "Add Files to "Your Project": - In the dialog, select SDWebImage.framework: - Check the "Copy items into destination group's folder (if needed)" checkbox ### Add dependencies - In you application project app’s target settings, find the "Build Phases" section and open the "Link Binary With Libraries" block: - Click the "+" button again and select the "ImageIO.framework", this is needed by the progressive download feature: ### Add Linker Flag Open the "Build Settings" tab, in the "Linking" section, locate the "Other Linker Flags" setting and add the "-ObjC" flag: Alternatively, if this causes compilation problems with frameworks that extend optional libraries, such as Parse, RestKit or opencv2, instead of the -ObjC flag use: ``` -force_load SDWebImage.framework/Versions/Current/SDWebImage ``` If you're using Cocoa Pods and have any frameworks that extend optional libraries, such as Parsen RestKit or opencv2, instead of the -ObjC flag use: ``` -force_load $(TARGET_BUILD_DIR)/libPods.a ``` and this: ``` $(inherited) ``` ### Import headers in your source files In the source files where you need to use the library, import the header file: ```objective-c #import ``` ### Build Project At this point your workspace should build without error. If you are having problem, post to the Issue and the community can help you solve it. Future Enhancements ------------------- - LRU memory cache cleanup instead of reset on memory warning ## Licenses All source code is licensed under the [MIT License](https://raw.github.com/rs/SDWebImage/master/LICENSE). --- ### Pods/SMCalloutView/README (Pods/SMCalloutView/README.md) Overview -------- SMCalloutView aims to be an exact replica of the private UICalloutView system control. We all love those "bubbles" you get when clicking pins in MKMapView. But sadly, it's impossible to present this bubble-style "Callout" UI anywhere outside MKMapView. Phooey! So this class _painstakingly_ recreates this handy control for your pleasure. Usage ----- To use SMCalloutView in your own projects, simply copy the files `SMCalloutView.h` and `SMCalloutView.m`. SMCalloutView, by default, will render in the new style introduced with iOS 7. If you need the old style, simply include `SMClassicCalloutView.h` and `SMClassicCalloutView.m` in your project as well. There is a special class constructor, `+[SMCalloutView platformCalloutView]` which will automatically select the appropriate callout class for the current platform. The comments in `SMCalloutView.h` do a lot of explaining on how to use the class, but the main function you'll need is `presentCalloutFromRect:`. You'll specify the view you'd like to add the callout to, as well as the rect defining the "target" that the popup should point at. The target rect should be _in the coordinate system of the target view_ (just like the similarly- named `UIPopover` method). Most likely this will be `target.frame` if you're adding the callout view as a sibling of the target view, or it would be `target.bounds` if you're adding the callout view to the target itself. You can study the included project's UIViewController subclasses for a working example. Questions --------- #### How do I change the height of the callout? If you use only the `title/titleView/subtitle/subtitleView` properties, the callout will always be the "system standard" height. If you assign the `contentView` property however, then the callout will size to fit the `contentView` and the other properties are ignored. [#29]: https://github.com/nfarina/calloutview/issues/29 #### Can I customize the background graphics? Yes, the callout background is an instance of `SMCalloutBackgroundView`. You can set your own custom `View` subclass to be the background, or you can use one of the built-in subclasses: - `SMCalloutMaskedBackgroundView` renders an iOS-7 style background. - `SMCalloutImageBackgroundView` lets you specify each of the image "fragments" that make up a horizontally-stretchable background. - `SMCalloutDrawnBackgroundView` draws the background at any size using CoreGraphics methods. You can copy the `-drawRect` method and change the parameters to suit your needs. #### Can I use the callout with the Google Maps iOS SDK? Check out [ryanmaxwell's demo project][googlemaps] for an example of one way to do this. ([More discussion on this topic][#25]) [googlemaps]: https://github.com/ryanmaxwell/GoogleMapsCalloutView [#25]: https://github.com/nfarina/calloutview/issues/25 #### Have you recreated more of MapKit? Nope, but other intrepid coders have! - For an awesome replacement of the pulsing blue "Current Location" dot, check out [Sam Vermette's SVPulsingAnnotationView][dot]. - And for the outdoor map data and tiles themselves, check out [Mapbox's iOS SDK][mapbox], a complete open-source solution for custom maps. They even use `SMCalloutView` out of the box! [dot]: https://github.com/samvermette/SVPulsingAnnotationView [mapbox]: https://www.mapbox.com/mobile/ More Info --------- You can read more info if you wish in the [blog post][]. [blog post]: http://nfarina.com/post/78014139253/smcalloutview-for-ios-7 --- ### Pods/SQLite.Swift/README (Pods/SQLite.swift/README.md) # SQLite.swift [![Build Status][TravisBadge]][TravisLink] [![CocoaPods Version][CocoaPodsVersionBadge]][CocoaPodsVersionLink] [![Swift5 compatible][Swift5Badge]][Swift5Link] [![Platform][PlatformBadge]][PlatformLink] [![Carthage compatible][CartagheBadge]][CarthageLink] [![Join the chat at https://gitter.im/stephencelis/SQLite.swift][GitterBadge]][GitterLink] A type-safe, [Swift][]-language layer over [SQLite3][]. [SQLite.swift][] provides compile-time confidence in SQL statement syntax _and_ intent. ## Features - A pure-Swift interface - A type-safe, optional-aware SQL expression builder - A flexible, chainable, lazy-executing query layer - Automatically-typed data access - A lightweight, uncomplicated query and parameter binding interface - Developer-friendly error handling and debugging - [Full-text search][] support - [Well-documented][See Documentation] - Extensively tested - [SQLCipher][] support via CocoaPods - Active support at [StackOverflow](http://stackoverflow.com/questions/tagged/sqlite.swift), and [Gitter Chat Room](https://gitter.im/stephencelis/SQLite.swift) (_experimental_) [SQLCipher]: https://www.zetetic.net/sqlcipher/ [Full-text search]: Documentation/Index.md#full-text-search [See Documentation]: Documentation/Index.md#sqliteswift-documentation ## Usage ```swift import SQLite let db = try Connection("path/to/db.sqlite3") let users = Table("users") let id = Expression("id") let name = Expression("name") let email = Expression("email") try db.run(users.create { t in t.column(id, primaryKey: true) t.column(name) t.column(email, unique: true) }) // CREATE TABLE "users" ( // "id" INTEGER PRIMARY KEY NOT NULL, // "name" TEXT, // "email" TEXT NOT NULL UNIQUE // ) let insert = users.insert(name <- "Alice", email <- "alice@mac.com") let rowid = try db.run(insert) // INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com') for user in try db.prepare(users) { print("id: \(user[id]), name: \(user[name]), email: \(user[email])") // id: 1, name: Optional("Alice"), email: alice@mac.com } // SELECT * FROM "users" let alice = users.filter(id == rowid) try db.run(alice.update(email <- email.replace("mac.com", with: "me.com"))) // UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com') // WHERE ("id" = 1) try db.run(alice.delete()) // DELETE FROM "users" WHERE ("id" = 1) try db.scalar(users.count) // 0 // SELECT count(*) FROM "users" ``` SQLite.swift also works as a lightweight, Swift-friendly wrapper over the C API. ```swift let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)") for email in ["betty@icloud.com", "cathy@icloud.com"] { try stmt.run(email) } db.totalChanges // 3 db.changes // 1 db.lastInsertRowid // 3 for row in try db.prepare("SELECT id, email FROM users") { print("id: \(row[0]), email: \(row[1])") // id: Optional(2), email: Optional("betty@icloud.com") // id: Optional(3), email: Optional("cathy@icloud.com") } try db.scalar("SELECT count(*) FROM users") // 2 ``` [Read the documentation][See Documentation] or explore more, interactively, from the Xcode project’s playground. For a more comprehensive example, see [this article][Create a Data Access Layer with SQLite.swift and Swift 2] and the [companion repository][SQLiteDataAccessLayer2]. [Create a Data Access Layer with SQLite.swift and Swift 2]: http://masteringswift.blogspot.com/2015/09/create-data-access-layer-with.html [SQLiteDataAccessLayer2]: https://github.com/hoffmanjon/SQLiteDataAccessLayer2/tree/master ## Installation > _Note:_ Version 0.12 requires Swift 5 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.2) or greater. Version 0.11.6 requires Swift 4.2 (and [Xcode](https://developer.apple.com/xcode/downloads/) 10.1) or greater. ### Carthage [Carthage][] is a simple, decentralized dependency manager for Cocoa. To install SQLite.swift with Carthage: 1. Make sure Carthage is [installed][Carthage Installation]. 2. Update your Cartfile to include the following: ```ruby github "stephencelis/SQLite.swift" ~> 0.12.0 ``` 3. Run `carthage update` and [add the appropriate framework][Carthage Usage]. [Carthage]: https://github.com/Carthage/Carthage [Carthage Installation]: https://github.com/Carthage/Carthage#installing-carthage [Carthage Usage]: https://github.com/Carthage/Carthage#adding-frameworks-to-an-application ### CocoaPods [CocoaPods][] is a dependency manager for Cocoa projects. To install SQLite.swift with CocoaPods: 1. Make sure CocoaPods is [installed][CocoaPods Installation]. (SQLite.swift requires version 1.6.1 or greater.) ```sh # Using the default Ruby install will require you to use sudo when # installing and updating gems. [sudo] gem install cocoapods ``` 2. Update your Podfile to include the following: ```ruby use_frameworks! target 'YourAppTargetName' do pod 'SQLite.swift', '~> 0.12.0' end ``` 3. Run `pod install --repo-update`. [CocoaPods]: https://cocoapods.org [CocoaPods Installation]: https://guides.cocoapods.org/using/getting-started.html#getting-started ### Swift Package Manager The [Swift Package Manager][] is a tool for managing the distribution of Swift code. 1. Add the following to your `Package.swift` file: ```swift dependencies: [ .package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.12.0") ] ``` 2. Build your project: ```sh $ swift build ``` [Swift Package Manager]: https://swift.org/package-manager ### Manual To install SQLite.swift as an Xcode sub-project: 1. Drag the **SQLite.xcodeproj** file into your own project. ([Submodule][], clone, or [download][] the project first.) 2. In your target’s **General** tab, click the **+** button under **Linked Frameworks and Libraries**. 3. Select the appropriate **SQLite.framework** for your platform. 4. **Add**. Some additional steps are required to install the application on an actual device: 5. In the **General** tab, click the **+** button under **Embedded Binaries**. 6. Select the appropriate **SQLite.framework** for your platform. 7. **Add**. [Xcode]: https://developer.apple.com/xcode/downloads/ [Submodule]: http://git-scm.com/book/en/Git-Tools-Submodules [download]: https://github.com/stephencelis/SQLite.swift/archive/master.zip ## Communication [See the planning document] for a roadmap and existing feature requests. [Read the contributing guidelines][]. The _TL;DR_ (but please; _R_): - Need **help** or have a **general question**? [Ask on Stack Overflow][] (tag `sqlite.swift`). - Found a **bug** or have a **feature request**? [Open an issue][]. - Want to **contribute**? [Submit a pull request][]. [See the planning document]: /Documentation/Planning.md [Read the contributing guidelines]: ./CONTRIBUTING.md#contributing [Ask on Stack Overflow]: http://stackoverflow.com/questions/tagged/sqlite.swift [Open an issue]: https://github.com/stephencelis/SQLite.swift/issues/new [Submit a pull request]: https://github.com/stephencelis/SQLite.swift/fork ## Author - [Stephen Celis](mailto:stephen@stephencelis.com) ([@stephencelis](https://twitter.com/stephencelis)) ## License SQLite.swift is available under the MIT license. See [the LICENSE file](./LICENSE.txt) for more information. ## Related These projects enhance or use SQLite.swift: - [SQLiteMigrationManager.swift][] (inspired by [FMDBMigrationManager][]) ## Alternatives Looking for something else? Try another Swift wrapper (or [FMDB][]): - [Camembert](https://github.com/remirobert/Camembert) - [GRDB](https://github.com/groue/GRDB.swift) - [SQLiteDB](https://github.com/FahimF/SQLiteDB) - [Squeal](https://github.com/nerdyc/Squeal) - [SwiftData](https://github.com/ryanfowler/SwiftData) - [SwiftSQLite](https://github.com/chrismsimpson/SwiftSQLite) [Swift]: https://swift.org/ [SQLite3]: http://www.sqlite.org [SQLite.swift]: https://github.com/stephencelis/SQLite.swift [TravisBadge]: https://img.shields.io/travis/stephencelis/SQLite.swift/master.svg?style=flat [TravisLink]: https://travis-ci.org/stephencelis/SQLite.swift [CocoaPodsVersionBadge]: https://cocoapod-badges.herokuapp.com/v/SQLite.swift/badge.png [CocoaPodsVersionLink]: http://cocoadocs.org/docsets/SQLite.swift [PlatformBadge]: https://cocoapod-badges.herokuapp.com/p/SQLite.swift/badge.png [PlatformLink]: http://cocoadocs.org/docsets/SQLite.swift [CartagheBadge]: https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat [CarthageLink]: https://github.com/Carthage/Carthage [GitterBadge]: https://badges.gitter.im/stephencelis/SQLite.swift.svg [GitterLink]: https://gitter.im/stephencelis/SQLite.swift [Swift5Badge]: https://img.shields.io/badge/swift-5-orange.svg?style=flat [Swift5Link]: https://developer.apple.com/swift/ [SQLiteMigrationManager.swift]: https://github.com/garriguv/SQLiteMigrationManager.swift [FMDB]: https://github.com/ccgus/fmdb [FMDBMigrationManager]: https://github.com/layerhq/FMDBMigrationManager --- ### Pods/SQLite.Swift/LICENSE (Pods/SQLite.swift/LICENSE.txt) (The MIT License) Copyright (c) 2014-2015 Stephen Celis () Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- ### Pods/SwiftyDropbox/README (Pods/SwiftyDropbox/README.md) # Dropbox for Swift The Official Dropbox Swift SDK for integrating with Dropbox [API v2](https://www.dropbox.com/developers/documentation/http/documentation) on iOS or macOS. Full documentation [here](http://dropbox.github.io/SwiftyDropbox/api-docs/latest/). --- ## Table of Contents * [System requirements](#system-requirements) * [Get started](#get-started) * [Register your application](#register-your-application) * [Obtain an OAuth 2.0 token](#obtain-an-oauth-20-token) * [SDK distribution](#sdk-distribution) * [CocoaPods](#cocoapods) * [Carthage](#carthage) * [Manually add subproject](#manually-add-subproject) * [Configure your project](#configure-your-project) * [Application `.plist` file](#application-plist-file) * [Handling the authorization flow](#handling-the-authorization-flow) * [Initialize a `DropboxClient` instance](#initialize-a-dropboxclient-instance) * [Begin the authorization flow](#begin-the-authorization-flow) * [Handle redirect back into SDK](#handle-redirect-back-into-sdk) * [Try some API requests](#try-some-api-requests) * [Dropbox client instance](#dropbox-client-instance) * [Handle the API response](#handle-the-api-response) * [Request types](#request-types) * [RPC-style request](#rpc-style-request) * [Upload-style request](#upload-style-request) * [Download-style request](#download-style-request) * [Handling responses and errors](#handling-responses-and-errors) * [Route-specific errors](#route-specific-errors) * [Generic network request errors](#generic-network-request-errors) * [Response handling edge cases](#response-handling-edge-cases) * [Customizing network calls](#customizing-network-calls) * [Configure network client](#configure-network-client) * [Specify API call response queue](#specify-api-call-response-queue) * [`DropboxClientsManager` class](#dropboxclientsmanager-class) * [Single Dropbox user case](#single-dropbox-user-case) * [Multiple Dropbox user case](#multiple-dropbox-user-case) * [Examples](#examples) * [Documentation](#documentation) * [Stone](#stone) * [Modifications](#modifications) * [Bugs](#bugs) --- ## System requirements - iOS 9.0+ - macOS 10.11+ - Xcode 10.0+ - Swift 4.2+ ## Get Started ### Register your application Before using this SDK, you should register your application in the [Dropbox App Console](https://dropbox.com/developers/apps). This creates a record of your app with Dropbox that will be associated with the API calls you make. ### Obtain an OAuth 2.0 token All requests need to be made with an OAuth 2.0 access token. An OAuth token represents an authenticated link between a Dropbox app and a Dropbox user account or team. Once you've created an app, you can go to the App Console and manually generate an access token to authorize your app to access your own Dropbox account. Otherwise, you can obtain an OAuth token programmatically using the SDK's pre-defined auth flow. For more information, [see below](https://github.com/dropbox/SwiftyDropbox#handling-the-authorization-flow). --- ## SDK distribution You can integrate the Dropbox Swift SDK into your project using one of several methods. ### CocoaPods To use [CocoaPods](http://cocoapods.org), a dependency manager for Cocoa projects, you should first install it using the following command: ```bash $ gem install cocoapods ``` Then navigate to the directory that contains your project and create a new file called `Podfile`. You can do this either with `pod init`, or open an existing Podfile, and then add `pod 'SwiftyDropbox'` to the main loop. Your Podfile should look something like this: ```ruby use_frameworks! target '' do pod 'SwiftyDropbox' end ``` Then, run the following command to install the dependency: ```bash $ pod install ``` Once your project is integrated with the Dropbox Swift SDK, you can pull SDK updates using the following command: ```bash $ pod update ``` **Note**: SwiftyDropbox requires CocoaPods 1.0.0+ when using Alamofire 4.0.0+. Because of this requirement, the CocoaPods App (which uses CocoaPods 1.0.0) cannot be used. --- ### Carthage You can also integrate the Dropbox Swift SDK into your project using [Carthage](https://github.com/Carthage/Carthage), a decentralized dependency manager for Cocoa. Carthage offers more flexibility than CocoaPods, but requires some additional work. You can install Carthage (with Xcode 7+) via [Homebrew](http://brew.sh/): ```bash brew update brew install carthage ``` To install the Dropbox Swift SDK via Carthage, you need to create a `Cartfile` in your project with the following contents: ``` # SwiftyDropbox github "https://github.com/dropbox/SwiftyDropbox" ~> 5.0.0 ``` Then, run the following command to install the dependency to checkout and build the Dropbox Swift SDK repository: ##### iOS ```bash carthage update --platform iOS ``` In the Project Navigator in Xcode, select your project, and then navigate to **General** > **Linked Frameworks and Libraries**, then drag and drop `SwiftyDropbox.framework` (from `Carthage/Build/iOS`). Then to add SwiftyDropbox's Alamofire dependency, drag and drop `Alamofire.framework` (from `Carthage/Build/iOS`) to **Linked Frameworks and Libraries**, as well. Then, navigate to **Build Phases** > **+** > **New Run Script Phase**. In the newly-created **Run Script** section, add the following code to the script body area (beneath the "Shell" box): ``` /usr/local/bin/carthage copy-frameworks ``` Then, navigate to the **Input Files** section and add the following path: ``` $(SRCROOT)/Carthage/Build/iOS/SwiftyDropbox.framework $(SRCROOT)/Carthage/Build/iOS/Alamofire.framework ``` ##### macOS ```bash carthage update --platform Mac ``` In the Project Navigator in Xcode, select your project, and then navigate to **General** > **Embedded Binaries**, then drag and drop `SwiftyDropbox.framework` (from `Carthage/Build/Mac`). Then to add SwiftyDropbox's Alamofire dependency, drag and drop `Alamofire.framework` (from `Carthage/Build/Mac`) to **Linked Frameworks and Libraries**, as well. Then navigate to **Build Phases** > **+** > **New Copy Files Phase**. In the newly-created **Copy Files** section, click the **Destination** drop-down menu and select **Products Directory**, then drag and drop `SwiftyDropbox.framework.dSYM` (from `Carthage/Build/Mac`). --- ### Manually add subproject Finally, you can also integrate the Dropbox Swift SDK into your project manually with the help of Carthage. Please take the following steps: Create a `Cartfile` in your project with the same contents as the Cartfile listed in the [Carthage](#carthage) section of the README. Then, run the following command to checkout and build the Dropbox Swift SDK repository: ##### iOS ```bash carthage update --platform iOS ``` Once you have checked-out out all the necessary code via Carthage, drag the `Carthage/Checkouts/SwiftyDropbox/Source/SwiftyDropbox/SwiftyDropbox.xcodeproj` file into your project as a subproject. Then, in the Project Navigator in Xcode, select your project, and then navigate to your project's build target > **General** > **Embedded Binaries** > **+** and then add the `SwiftyDropbox.framework` file for the iOS platform. Finally, to retrieve SwiftyDropbox's Alamofire dependency, drag the `Carthage/Checkouts/Alamofire/Alamofire.xcodeproj` project into your project (as you did with `SwiftyDropbox.xcodeproj`). Then, in the Project Navigator in Xcode, select your project, and then navigate to your project's build target > **General** > **Linked Frameworks and Libraries** > **+** and then add the `Alamofire.framework` file for the iOS platform. ##### macOS ```bash carthage update --platform Mac ``` Once you have checked-out out all the necessary code via Carthage, drag the `Carthage/Checkouts/SwiftyDropbox/Source/SwiftyDropbox/SwiftyDropbox.xcodeproj` file into your project as a subproject. Then, in the Project Navigator in Xcode, select your project, and then navigate to your project's build target > **General** > **Embedded Binaries** > **+** and then add the `SwiftyDropbox.framework` file for the macOS platform. Finally, to retrieve SwiftyDropbox's Alamofire dependency, drag the `Carthage/Checkouts/Alamofire/Alamofire.xcodeproj` project into your project (as you did with `SwiftyDropbox.xcodeproj`). Then, in the Project Navigator in Xcode, select your project, and then navigate to your project's build target > **General** > **Linked Frameworks and Libraries** > **+** and then add the `Alamofire.framework` file for the macOS platform. --- ## Configure your project Once you have integrated the Dropbox Swift SDK into your project, there are a few additional steps to take before you can begin making API calls. ### Application `.plist` file If you are compiling on iOS SDK 9.0, you will need to modify your application's `.plist` to handle Apple's [new security changes](https://developer.apple.com/videos/wwdc/2015/?id=703) to the `canOpenURL` function. You should add the following code to your application's `.plist` file: ``` LSApplicationQueriesSchemes dbapi-8-emm dbapi-2 ``` This allows the Swift SDK to determine if the official Dropbox iOS app is installed on the current device. If it is installed, then the official Dropbox iOS app can be used to programmatically obtain an OAuth 2.0 access token. Additionally, your application needs to register to handle a unique Dropbox URL scheme for redirect following completion of the OAuth 2.0 authorization flow. This URL scheme should have the format `db-`, where `` is your Dropbox app's app key, which can be found in the [App Console](https://dropbox.com/developers/apps). You should add the following code to your `.plist` file (but be sure to replace `` with your app's app key): ``` CFBundleURLTypes CFBundleURLSchemes db- CFBundleURLName ``` After you've made the above changes, your application's `.plist` file should look something like this:

Info .plist Example

--- ### Handling the authorization flow There are three methods to programmatically retrieve an OAuth 2.0 access token: * **Direct auth** (iOS only): This launches the official Dropbox iOS app (if installed), authenticates via the official app, then redirects back into the SDK * **Safari view controller auth** (iOS only): This launches a `SFSafariViewController` to facillitate the auth flow. This is desirable because it is safer for the end-user, and pre-existing session data can be used to avoid requiring the user to re-enter their Dropbox credentials. * **Redirect to external browser** (macOS only): This launches the user's default browser to facillitate the auth flow. This is also desirable because it is safer for the end-user, and pre-existing session data can be used to avoid requiring the user to re-enter their Dropbox credentials. To facilitate the above authorization flows, you should take the following steps: --- #### Initialize a `DropboxClient` instance From your application delegate: ##### iOS ```Swift import SwiftyDropbox func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { DropboxClientsManager.setupWithAppKey("") return true } ``` ##### macOS ```Swift import SwiftyDropbox func applicationDidFinishLaunching(_ aNotification: Notification) { DropboxClientsManager.setupWithAppKeyDesktop("") } ``` --- #### Begin the authorization flow You can commence the auth flow by calling `authorizeFromController:controller:openURL` method in your application's view controller. From your view controller: ##### iOS ```Swift import SwiftyDropbox func myButtonInControllerPressed() { DropboxClientsManager.authorizeFromController(UIApplication.shared, controller: self, openURL: { (url: URL) -> Void in UIApplication.shared.openURL(url) }) } ``` ##### macOS ```Swift import SwiftyDropbox func myButtonInControllerPressed() { DropboxClientsManager.authorizeFromController(sharedWorkspace: NSWorkspace.shared, controller: self, openURL: { (url: URL) -> Void in NSWorkspace.shared.open(url) }) } ``` Beginning the authentication flow via in-app webview will launch a window like this:

Auth Flow Init Example

--- #### Handle redirect back into SDK To handle the redirection back into the Swift SDK once the authentication flow is complete, you should add the following code in your application's delegate: ##### iOS ```Swift import SwiftyDropbox func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if let authResult = DropboxClientsManager.handleRedirectURL(url) { switch authResult { case .success: print("Success! User is logged into Dropbox.") case .cancel: print("Authorization flow was manually canceled by user!") case .error(_, let description): print("Error: \(description)") } } return true } ``` ##### macOS ```Swift import SwiftyDropbox func applicationDidFinishLaunching(_ aNotification: Notification) { ...... // code outlined above goes here NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleGetURLEvent), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) } func handleGetURLEvent(_ event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) { if let aeEventDescriptor = event?.paramDescriptor(forKeyword: AEKeyword(keyDirectObject)) { if let urlStr = aeEventDescriptor.stringValue { let url = URL(string: urlStr)! if let authResult = DropboxClientsManager.handleRedirectURL(url) { switch authResult { case .success: print("Success! User is logged into Dropbox.") case .cancel: print("Authorization flow was manually canceled by user!") case .error(_, let description): print("Error: \(description)") } } // this brings your application back the foreground on redirect NSApp.activate(ignoringOtherApps: true) } } } ``` After the end user signs in with their Dropbox login credentials via the in-app webview, they will see a window like this:

Auth Flow Approval Example

If they press **Allow** or **Cancel**, the `db-` redirect URL will be launched from the webview, and will be handled in your application delegate's `application:handleOpenURL` method, from which the result of the authorization can be parsed. Now you're ready to begin making API requests! --- ## Try some API requests Once you have obtained an OAuth 2.0 token, you can try some API v2 calls using the Swift SDK. ### Dropbox client instance Start by creating a reference to the `DropboxClient` or `DropboxTeamClient` instance that you will use to make your API calls. ```Swift import SwiftyDropbox // Reference after programmatic auth flow let client = DropboxClientsManager.authorizedClient ``` or ```Swift import SwiftyDropbox // Initialize with manually retrieved auth token let client = DropboxClient(accessToken: "") ``` --- ### Handle the API response The Dropbox [User API](https://www.dropbox.com/developers/documentation/http/documentation) and [Business API](https://www.dropbox.com/developers/documentation/http/teams) have three types of requests: RPC, Upload and Download. The response handlers for each request type are similar to one another. The arguments for the handler blocks are as follows: * **route result type** (`Void` if the route does not have a return type) * **network error** (either a route-specific error or generic network error) * **output content** (`URL` / `Data` reference to downloaded output for Download-style endpoints only) Note: Response handlers are required for all endpoints. Progress handlers, on the other hand, are optional for all endpoints. --- ### Request types #### RPC-style request ```Swift client.files.createFolder(path: "/test/path/in/Dropbox/account").response { response, error in if let response = response { print(response) } else if let error = error { print(error) } } ``` --- #### Upload-style request ```Swift let fileData = "testing data example".data(using: String.Encoding.utf8, allowLossyConversion: false)! let request = client.files.upload(path: "/test/path/in/Dropbox/account", input: TestData.fileData) .response { response, error in if let response = response { print(response) } else if let error = error { print(error) } } .progress { progressData in print(progressData) } // in case you want to cancel the request if someConditionIsSatisfied { request.cancel() } ``` --- #### Download-style request ```Swift // Download to URL let fileManager = FileManager.default let directoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] let destURL = directoryURL.appendingPathComponent("myTestFile") let destination: (URL, HTTPURLResponse) -> URL = { temporaryURL, response in return destURL } client.files.download(path: "/test/path/in/Dropbox/account", overwrite: true, destination: destination) .response { response, error in if let response = response { print(response) } else if let error = error { print(error) } } .progress { progressData in print(progressData) } // Download to Data client.files.download(path: "/test/path/in/Dropbox/account") .response { response, error in if let response = response { let responseMetadata = response.0 print(responseMetadata) let fileContents = response.1 print(fileContents) } else if let error = error { print(error) } } .progress { progressData in print(progressData) } ``` --- ### Handling responses and errors Dropbox API v2 deals largely with two data types: **structs** and **unions**. Broadly speaking, most route **arguments** are struct types and most route **errors** are union types. **NOTE:** In this context, "structs" and "unions" are terms specific to the Dropbox API, and not to any of the languages that are used to query the API, so you should avoid thinking of them in terms of their Swift definitions. **Struct types** are "traditional" object types, that is, composite types made up of a collection of one or more instance fields. All public instance fields are accessible at runtime, regardless of runtime state. **Union types**, on the other hand, represent a single value that can take on multiple value types, depending on state. We capture all of these different type scenarios under one "union object", but that object will exist only as one type at runtime. Each union state type, or **tag**, may have an associated value (if it doesn't, the union state type is said to be **void**). Associated value types can either be primitives, structs or unions. Although the Swift SDK represents union types as objects with multiple instance fields, at most one instance field is accessible at runtime, depending on the tag state of the union. For example, the [/delete](https://www.dropbox.com/developers/documentation/http/documentation#files-delete) endpoint returns an error, `Files.DeleteError`, which is a union type. The `Files.DeleteError` union can take on two different tag states: `path_lookup` (if there is a problem looking up the path) or `path_write` (if there is a problem writing -- or in this case deleting -- to the path). Here, both tag states have non-void associated values (of types `Files.LookupError` and `Files.WriteError`, respectively). In this way, one union object is able to capture a multitude of scenarios, each of which has their own value type. To properly handle union types, you should pass each union through a switch statement, and check each possible tag state associated with the union. Once you have determined the current tag state of the union, you can then access the value associated with that tag state (provided there exists an associated value type, i.e., it's not **void**). --- #### Route-specific errors ``` /* Detailed source-code truncated for AI context efficiency. */ ``` --- #### Generic network request errors In the case of a network error, errors are either specific to the endpoint (as shown above) or more generic errors. To determine if an error is route-specific or not, the error object should be cast as a `CallError`, and depending on the type of error, handled in the appropriate switch statement. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` --- #### Response handling edge cases Some routes return union types as result types, so you should be prepared to handle these results in the same way that you handle union route errors. Please consult the [documentation](https://www.dropbox.com/developers/documentation/http/documentation) for each endpoint that you use to ensure you are properly handling the route's response type. A few routes return result types that are **datatypes with subtypes**, that is, structs that can take on multiple state types like unions. For example, the [/delete](https://www.dropbox.com/developers/documentation/http/documentation#files-delete) endpoint returns a generic `Metadata` type, which can exist either as a `FileMetadata` struct, a `FolderMetadata` struct, or a `DeletedMetadata` struct. To determine at runtime which subtype the `Metadata` type exists as, pass the object through a switch statement, and check for each possible class, with the result casted accordingly. See below: ```Swift client.files.deleteV2(path: "/test/path/in/Dropbox/account").response { response, error in if let response = response { switch response { case let fileMetadata as Files.FileMetadata: print("File metadata: \(fileMetadata)") case let folderMetadata as Files.FolderMetadata: print("Folder metadata: \(folderMetadata)") case let deletedMetadata as Files.DeletedMetadata: print("Deleted entity's metadata: \(deletedMetadata)") } } else if let error = error { switch error as CallError { case .routeError(let boxed, let userMessage, let errorSummary, let requestId): // a route-specific error occurred // see handling above case .internalServerError(let code, let message, let requestId): .... .... // a not route-specific error occurred // see handling above .... .... .... } } } ``` This `Metadata` object is known as a **datatype with subtypes** in our API v2 documentation. Datatypes with subtypes are a way combining structs and unions. Datatypes with subtypes are struct objects that contain a tag, which specifies which subtype the object exists as at runtime. The reason we have this construct, as with unions, is so we can capture a multitude of scenarios with one object. In the above example, the `Metadata` type can exists as `FileMetadata`, `FolderMetadata` or `DeleteMetadata`. Each of these types have common instances fields like "name" (the name for the file, folder or deleted type), but also instance fields that are specific to the particular subtype. In order to leverage inheritance, we set a common supertype called `Metadata` which captures all of the common instance fields, but also has a tag instance field, which specifies which subtype the object currently exists as. In this way, datatypes with subtypes are a hybrid of structs and unions. Only a few routes return result types like this. --- ### Customizing network calls #### Configure network client It is possible to configure the networking client used by the SDK to make API requests. You can supply custom fields like a custom user agent or custom delegates to manage response handler code, or a custom server trust policy. See below: ##### iOS ```Swift import SwiftyDropbox let transportClient = DropboxTransportClient(accessToken: "", baseHosts: nil, userAgent: "CustomUserAgent", selectUser: nil, sessionDelegate: mySessionDelegate, backgroundSessionDelegate: myBackgroundSessionDelegate, serverTrustPolicyManager: myServerTrustPolicyManager) DropboxClientsManager.setupWithAppKey("", transportClient: transportClient) ``` ##### macOS ```Swift import SwiftyDropbox let transportClient = DropboxTransportClient(accessToken: "", baseHosts: nil, userAgent: "CustomUserAgent", selectUser: nil, sessionDelegate: mySessionDelegate, backgroundSessionDelegate: myBackgroundSessionDelegate, serverTrustPolicyManager: myServerTrustPolicyManager) DropboxClientsManager.setupWithAppKeyDesktop("", transportClient: transportClient) ``` #### Specify API call response queue By default, response/progress handler code runs on the main thread. You can set a custom response queue for each API call that you make via the `response` method, in the event want your response/progress handler code to run on a different thread: ```Swift let client = DropboxClientsManager.authorizedClient! client.files.listFolder(path: "").response(queue: DispatchQueue(label: "MyCustomSerialQueue")) { response, error in if let result = response { print(Thread.current) // Output: {number = 4, name = (null)} print(Thread.main) // Output: {number = 1, name = (null)} print(result) } } ``` --- ### `DropboxClientsManager` class The Swift SDK includes a convenience class, `DropboxClientsManager`, for integrating the different functions of the SDK into one class. #### Single Dropbox user case For most apps, it is reasonable to assume that only one Dropbox account (and access token) needs to be managed at a time. In this case, the `DropboxClientsManager` flow looks like this: * call `setupWithAppKey`/`setupWithAppKeyDesktop` (or `setupWithTeamAppKey`/`setupWithTeamAppKeyDesktop`) in integrating app's app delegate * client manager determines whether any access tokens are stored -- if any exist, one token is arbitrarily chosen to use * if no token is found, call `authorizeFromController`/`authorizeFromControllerDesktop` to initiate the OAuth flow * if auth flow is initiated, call `handleRedirectURL` (or `handleRedirectURLTeam`) in integrating app's app delegate to handle auth redirect back into the app and store the retrieved access token (using a `DropboxOAuthManager` instance) * client manager instantiates a `DropboxTransportClient` (if not supplied by the user) * client manager instantiates a `DropboxClient` (or `DropboxTeamClient`) with the transport client as a field The `DropboxClient` (or `DropboxTeamClient`) is then used to make all of the desired API calls. * call `unlinkClients` to logout Dropbox user and clear all access tokens #### Multiple Dropbox user case For some apps, it is necessary to manage more than one Dropbox account (and access token) at a time. In this case, the `DropboxClientsManager` flow looks like this: * access token uids are managed by the app that is integrating with the SDK for later lookup * call `setupWithAppKeyMultiUser`/`setupWithAppKeyMultiUserDesktop` (or `setupWithTeamAppKeyMultiUser`/`setupWithTeamAppKeyMultiUserDesktop`) in integrating app's app delegate * client manager determines whether an access token is stored with the`tokenUid` as a key -- if one exists, this token is chosen to use * if no token is found, call `authorizeFromController`/`authorizeFromControllerDesktop` to initiate the OAuth flow * if auth flow is initiated, call `handleRedirectURL` (or `handleRedirectURLTeam`) in integrating app's app delegate to handle auth redirect back into the app and store the retrieved access token (using a `DropboxOAuthManager` instance) * at this point, the app that is integrating with the SDK should persistently save the `tokenUid` from the `DropboxAccessToken` field of the `DropboxOAuthResult` object returned from the `handleRedirectURL` (or `handleRedirectURLTeam`) method * `tokenUid` can be reused either to authorize a new user mid-way through an app's lifecycle via `reauthorizeClient` (or `reauthorizeTeamClient`) or when the app initially launches via `setupWithAppKeyMultiUser`/`setupWithAppKeyMultiUserDesktop` (or `setupWithTeamAppKeyMultiUser`/`setupWithTeamAppKeyMultiUserDesktop`) * client manager instantiates a `DropboxTransportClient` (if not supplied by the user) * client manager instantiates a `DropboxClient` (or `DropboxTeamClient`) with the transport client as a field The `DropboxClient` (or `DropboxTeamClient`) is then used to make all of the desired API calls. * call `resetClients` to logout Dropbox user but not clear any access tokens * if specific access tokens need to be removed, use the `clearStoredAccessToken` method in `DropboxOAuthManager` --- ## Examples * [PhotoWatch](https://github.com/dropbox/PhotoWatch) - View photos from your Dropbox. Supports Apple Watch. --- ## Documentation * [Dropbox API v2 Swift SDK](http://dropbox.github.io/SwiftyDropbox/api-docs/latest/) * [Dropbox API v2](https://www.dropbox.com/developers/documentation/http/documentation) --- ## Stone All of our routes and data types are auto-generated using a framework called [Stone](https://github.com/dropbox/stone). The `stone` repo contains all of the Swift specific generation logic, and the `spec` repo contains the language-neutral API endpoint specifications which serve as input to the language-specific generators. --- ## Modifications If you're interested in modifying the SDK codebase, you should take the following steps: * clone this GitHub repository to your local filesystem * run `git submodule init` and then `git submodule update` * navigate to `TestSwifty_[iOS|macOS]` * check the CocoaPods version installed (via `pod --version`) is same as "locked" in `TestSwifty_[iOS|macOS]/Podfile.lock` * run `pod install` * open `TestSwifty_[iOS|macOS]/TestSwifty_[iOS|macOS].xcworkspace` in Xcode * implement your changes to the SDK source code. To ensure your changes have not broken any existing functionality, you can run a series of integration tests: * create a new app on https://www.dropbox.com/developers/apps/, with "Full Dropbox" access. Note the App key * open Info.plist and configure the "URL types > Item 0 (Editor) > URL Schemes > Item 0" key to db-"App key" * open AppDelegate.swift and replace "FULL_DROPBOX_APP_KEY" with the App key as well * run the test app on your device and follow the on-screen instructions --- ## Bugs Please post any bugs to the [issue tracker](https://github.com/dropbox/SwiftyDropbox/issues) found on the project's GitHub page. Please include the following with your issue: - a description of what is not working right - sample code to help replicate the issue Thank you! --- ### Resources/Profanity (Resources/Profanity.txt) 2g1c 2 girls 1 cup acrotomophilia anal anilingus anus arsehole ass asshole assmunch auto erotic autoerotic babeland baby batter ball gag ball gravy ball kicking ball licking ball sack ball sucking bangbros bareback barely legal barenaked bastardo bastinado bbw bdsm beaver cleaver beaver lips bestiality bi curious big black big breasts big knockers big tits bimbos birdlock bitch black cock blonde action blonde on blonde action blow j blow your l blue waffle blumpkin bollocks bondage boner boob boobs booty call brown showers brunette action bukkake bulldyke bullet vibe bung hole bunghole busty butt buttcheeks butthole camel toe camgirl camslut camwhore carpet muncher carpetmuncher chocolate rosebuds circlejerk cleveland steamer clit clitoris clover clamps clusterfuck cock cocks coprolagnia coprophilia cornhole cum cumming cunnilingus cunt darkie date rape daterape deep throat deepthroat dick dildo dirty pillows dirty sanchez dog style doggie style doggiestyle doggy style doggystyle dolcett domination dominatrix dommes donkey punch double dong double penetration dp action eat my ass ecchi ejaculation erotic erotism escort ethical slut eunuch faggot fecal felch fellatio feltch female squirting femdom figging fingering fisting foot fetish footjob frotting fuck fucker fucking fuck buttons fudge packer fudgepacker futanari g-spot gang bang gay sex genitals giant cock girl on girl on top girls gone wild goatcx goatse gokkun golden shower goo girl goodpoop goregasm grope group sex guro hand job handjob hard core hardcore hitler hentai homoerotic honkey hooker hot chick how to kill how to murder huge fat humping incest intercourse jack off jail bait jailbait jerk off jew jews jigaboo jiggaboo jiggerboo jizz juggs kill kike kinbaku kinkster kinky knobbing leather restraint leather straight jacket lemon party lolita lovemaking make me come male squirting masturbate menage a trois milf missionary position motherfucker mound of venus mr hands muff diver muffdiving murder nambla nawashi negro neonazi nig nog nigga nigger nimphomania nipple nipples nsfw images nude nudity nympho nymphomania octopussy omorashi one cup two girls one guy one jar orgasm orgy paedophile panties panty pedobear pedophile pegging penis phone sex piece of shit piss pig pissing pisspig playboy pleasure chest pole smoker ponyplay poof poop chute poopchute porn porno pornography prince albert piercing pthc pubes pussy queaf raghead raging boner rape raping rapist rectum reverse cowgirl rimjob rimming rosy palm rosy palm and her 5 sisters rusty trombone s&m sadism scat schlong scissoring semen sex sexo sexy shaved beaver shaved pussy shemale shibari shit shitcock shitty shota shrimping slanteye slut smut snatch snowballing sodomize sodomy spic spooge spread legs strap on strapon strappado strip club style doggy suck sucks suicide girls sultry women swastika swinger tainted love taste my tea bagging threesome throating tied up tight white tit tits titties titty tongue in a topless tosser towelhead tranny tribadism tub girl tubgirl tushy twat twink twinkie two girls one cup undressing upskirt urethra play urophilia vagina venus mound vibrator violet blue violet wand vorarephilia voyeur vulva wank wet dream wetback white power women rapping wrapping men wrinkled starfish xx xxx yaoi yellow showers yiffy zoophilia --- ### Resources/AppIcon.Icon/Icon.Json (Resources/AppIcon.icon/icon.json) { "fill" : { "linear-gradient" : [ "display-p3:0.54510,0.15686,0.96863,1.00000", "srgb:0.26667,0.00000,0.63137,1.00000" ] }, "groups" : [ { "layers" : [ { "image-name" : "delta_icon.svg", "name" : "delta_icon", "position" : { "scale" : 0.88, "translation-in-points" : [ 0, 0 ] } }, { "glass" : false, "image-name" : "delta_shadow.svg", "name" : "delta_shadow", "position" : { "scale" : 0.88, "translation-in-points" : [ 0, 0 ] } } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "circles" : [ "watchOS" ], "squares" : "shared" } } --- ### Resources/Assets.Xcassets/Contents.Json (Resources/Assets.xcassets/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/BoxArt.Imageset/Contents.Json (Resources/Assets.xcassets/BoxArt.imageset/Contents.json) { "images" : [ { "filename" : "BoxArt.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "BoxArt@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "BoxArt@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/BoxArtGlass.Imageset/Contents.Json (Resources/Assets.xcassets/BoxArtGlass.imageset/Contents.json) { "images" : [ { "filename" : "BoxArt_customGlass.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Colors/Contents.Json (Resources/Assets.xcassets/Colors/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Colors/DarkGray.Colorset/Contents.Json (Resources/Assets.xcassets/Colors/DarkGray.colorset/Contents.json) { "info" : { "version" : 1, "author" : "xcode" }, "colors" : [ { "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { "red" : "0.150", "alpha" : "1.000", "blue" : "0.150", "green" : "0.150" } } } ] } --- ### Resources/Assets.Xcassets/Colors/LightPurple.Colorset/Contents.Json (Resources/Assets.xcassets/Colors/LightPurple.colorset/Contents.json) { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xFF", "green" : "0xBC", "red" : "0xD6" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Colors/Purple.Colorset/Contents.Json (Resources/Assets.xcassets/Colors/Purple.colorset/Contents.json) { "info" : { "version" : 1, "author" : "xcode" }, "colors" : [ { "idiom" : "universal", "color" : { "color-space" : "srgb", "components" : { "red" : "139", "alpha" : "1.000", "blue" : "247", "green" : "40" } } } ] } --- ### Resources/Assets.Xcassets/Controller Icons/Contents.Json (Resources/Assets.xcassets/Controller Icons/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Controller Icons/DS.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/DS.imageset/Contents.json) { "images" : [ { "filename" : "DS.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Controller Icons/GBA.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/GBA.imageset/Contents.json) { "images" : [ { "filename" : "GBA.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Controller Icons/GBC.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/GBC.imageset/Contents.json) { "images" : [ { "filename" : "GBC.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Controller Icons/Genesis.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/Genesis.imageset/Contents.json) { "images" : [ { "filename" : "Genesis.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Controller Icons/N64.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/N64.imageset/Contents.json) { "images" : [ { "filename" : "N64.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Controller Icons/NES.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/NES.imageset/Contents.json) { "images" : [ { "filename" : "NES.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Controller Icons/SNES.Imageset/Contents.Json (Resources/Assets.xcassets/Controller Icons/SNES.imageset/Contents.json) { "images" : [ { "filename" : "SNES.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Custom Symbols/Contents.Json (Resources/Assets.xcassets/Custom Symbols/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Custom Symbols/Gamecontroller.Slash.Symbolset/Contents.Json (Resources/Assets.xcassets/Custom Symbols/gamecontroller.slash.symbolset/Contents.json) { "info" : { "author" : "xcode", "version" : 1 }, "symbols" : [ { "filename" : "gamecontroller.slash.svg", "idiom" : "universal" } ] } --- ### Resources/Assets.Xcassets/Delta.Imageset/Contents.Json (Resources/Assets.xcassets/Delta.imageset/Contents.json) { "images" : [ { "filename" : "DeltaSplash.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } } --- ### Resources/Assets.Xcassets/DeltaPlaceholder.Imageset/Contents.Json (Resources/Assets.xcassets/DeltaPlaceholder.imageset/Contents.json) { "images" : [ { "filename" : "DeltaPlaceholder.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "DeltaPlaceholder@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "DeltaPlaceholder@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/DS Home Screen.Imageset/Contents.Json (Resources/Assets.xcassets/DS Home Screen.imageset/Contents.json) { "images" : [ { "filename" : "DS.pdf", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Link.Imageset/Contents.Json (Resources/Assets.xcassets/Link.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "Link@1x.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "Link@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "Link@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/LinkOut.Symbolset/Contents.Json (Resources/Assets.xcassets/LinkOut.symbolset/Contents.json) { "info" : { "author" : "xcode", "version" : 1 }, "symbols" : [ { "filename" : "LinkOut.svg", "idiom" : "universal" } ] } --- ### Resources/Assets.Xcassets/Pause Icons/Contents.Json (Resources/Assets.xcassets/Pause Icons/Contents.json) { "info" : { "version" : 1, "author" : "xcode" } } --- ### Resources/Assets.Xcassets/Pause Icons/CheatCodes.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/CheatCodes.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "cheats@1x.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cheats@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cheats@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Pause Icons/FastForward.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/FastForward.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "FastForward.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "FastForward@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "FastForward@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Pause Icons/LoadSaveState.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/LoadSaveState.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "LoadSaveState.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LoadSaveState@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LoadSaveState@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Pause Icons/Lu.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/Lu.imageset/Contents.json) { "images" : [ { "filename" : "36x36_white3.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "72x72_white3.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "108x108_white3.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Pause Icons/Pause.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/Pause.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "Pause.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "Pause@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "Pause@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Pause Icons/SaveSaveState.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/SaveSaveState.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "SaveSaveState.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "SaveSaveState@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "SaveSaveState@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Pause Icons/Screenshot.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/Screenshot.imageset/Contents.json) { "images" : [ { "filename" : "screenshot.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "screenshot@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "screenshot@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Pause Icons/SustainButtons.Imageset/Contents.Json (Resources/Assets.xcassets/Pause Icons/SustainButtons.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "sustain@1x.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "sustain@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "sustain@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Riley.Imageset/Contents.Json (Resources/Assets.xcassets/Riley.imageset/Contents.json) { "images" : [ { "filename" : "riley.jpg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/SettingsButton.Imageset/Contents.Json (Resources/Assets.xcassets/SettingsButton.imageset/Contents.json) { "images" : [ { "idiom" : "universal", "filename" : "gear_1.pdf" } ], "info" : { "version" : 1, "author" : "xcode" }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Shane.Imageset/Contents.Json (Resources/Assets.xcassets/Shane.imageset/Contents.json) { "images" : [ { "filename" : "shane.jpeg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Social Media/Contents.Json (Resources/Assets.xcassets/Social Media/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Social Media/Bluesky.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/Bluesky.imageset/Contents.json) { "images" : [ { "filename" : "bluesky.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Social Media/BlueskyAppIcon.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/BlueskyAppIcon.imageset/Contents.json) { "images" : [ { "filename" : "Bluesky.jpg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Social Media/GitHub.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/GitHub.imageset/Contents.json) { "images" : [ { "filename" : "GitHub.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Social Media/GitHubAppIcon.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/GitHubAppIcon.imageset/Contents.json) { "images" : [ { "filename" : "github.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Social Media/Mastodon.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/Mastodon.imageset/Contents.json) { "images" : [ { "filename" : "mastodon.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Social Media/MastodonAppIcon.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/MastodonAppIcon.imageset/Contents.json) { "images" : [ { "filename" : "Mastodon.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Assets.Xcassets/Social Media/Threads.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/Threads.imageset/Contents.json) { "images" : [ { "filename" : "threads_icon.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } --- ### Resources/Assets.Xcassets/Social Media/ThreadsAppIcon.Imageset/Contents.Json (Resources/Assets.xcassets/Social Media/ThreadsAppIcon.imageset/Contents.json) { "images" : [ { "filename" : "Threads.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Contents.Json (Resources/Icons.xcassets/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Classic/Contents.Json (Resources/Icons.xcassets/Classic/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Classic/BrokenIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Classic/BrokenIcon.appiconset/Contents.json) { "images" : [ { "filename" : "BrokenDelta.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Classic/OriginalIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Classic/OriginalIcon.appiconset/Contents.json) { "images" : [ { "filename" : "original-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Modern/Contents.Json (Resources/Icons.xcassets/Modern/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Modern/DeltaRed.Appiconset/Contents.Json (Resources/Icons.xcassets/Modern/DeltaRed.appiconset/Contents.json) { "images" : [ { "filename" : "DeltaRed.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Modern/GBA4iOS.Appiconset/Contents.Json (Resources/Icons.xcassets/Modern/GBA4iOS.appiconset/Contents.json) { "images" : [ { "filename" : "GBA4iOS.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Modern/ModernIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Modern/ModernIcon.appiconset/Contents.json) { "images" : [ { "filename" : "Icon1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "AppIconDark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIconTinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons/Contents.Json (Resources/Icons.xcassets/Patrons/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons/DeltaGold.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons/DeltaGold.appiconset/Contents.json) { "images" : [ { "filename" : "DeltaGold.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons/Halogenide.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons/Halogenide.appiconset/Contents.json) { "images" : [ { "filename" : "Halide@72ppi.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons/KetchupIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons/KetchupIcon.appiconset/Contents.json) { "images" : [ { "filename" : "KetchupIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "KetchupIcon_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "KetchupIcon_Dark 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons/MicrochipIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons/MicrochipIcon.appiconset/Contents.json) { "images" : [ { "filename" : "MicrochipIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "Microchip - Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "Microchip - Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBAIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBAIcon.appiconset/Contents.json) { "images" : [ { "filename" : "GBA.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBA_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBCIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBCIcon.appiconset/Contents.json) { "images" : [ { "filename" : "GBC_Grape.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBCIcon Berry.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBCIcon_Berry.appiconset/Contents.json) { "images" : [ { "filename" : "GBC_Berry.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBCIcon Dandelion.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBCIcon_Dandelion.appiconset/Contents.json) { "images" : [ { "filename" : "GBC_Dandelion.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBCIcon Kiwi.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBCIcon_Kiwi.appiconset/Contents.json) { "images" : [ { "filename" : "GBC_Kiwi.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBCIcon Teal.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBCIcon_Teal.appiconset/Contents.json) { "images" : [ { "filename" : "GBC_Teal.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/GBIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/GBIcon.appiconset/Contents.json) { "images" : [ { "filename" : "GameBoy.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GameBoy_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/N64Icon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/N64Icon.appiconset/Contents.json) { "images" : [ { "filename" : "N64.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "N64_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/NESIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/NESIcon.appiconset/Contents.json) { "images" : [ { "filename" : "NES.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "NES_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Patrons Button Pack/SNESIcon.Appiconset/Contents.Json (Resources/Icons.xcassets/Patrons - Button Pack/SNESIcon.appiconset/Contents.json) { "images" : [ { "filename" : "SNES.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "SNES_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "AppIcon_Tinted.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Contents.Json (Resources/Icons.xcassets/Raw/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/AppIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/AppIcon.imageset/Contents.json) { "images" : [ { "filename" : "GlassIcon_Light.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GlassIcon_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Classic/Contents.Json (Resources/Icons.xcassets/Raw/Classic/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Classic/BrokenIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Classic/BrokenIcon.imageset/Contents.json) { "images" : [ { "filename" : "BrokenDelta.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Classic/OriginalIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Classic/OriginalIcon.imageset/Contents.json) { "images" : [ { "filename" : "original-1024.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Modern/Contents.Json (Resources/Icons.xcassets/Raw/Modern/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Modern/DeltaRed.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Modern/DeltaRed.imageset/Contents.json) { "images" : [ { "filename" : "DeltaRed.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Modern/GBA4iOS.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Modern/GBA4iOS.imageset/Contents.json) { "images" : [ { "filename" : "GBA4iOS.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "AppIcon_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Modern/ModernIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Modern/ModernIcon.imageset/Contents.json) { "images" : [ { "filename" : "Icon1024.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "AppIcon_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons/Contents.Json (Resources/Icons.xcassets/Raw/Patrons/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons/DeltaGold.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons/DeltaGold.imageset/Contents.json) { "images" : [ { "filename" : "DeltaGold.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons/Halogenide.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons/Halogenide.imageset/Contents.json) { "images" : [ { "filename" : "Halide@72ppi.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons/KetchupIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons/KetchupIcon.imageset/Contents.json) { "images" : [ { "filename" : "KetchupIcon.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "Ketchup_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons/MicrochipIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons/MicrochipIcon.imageset/Contents.json) { "images" : [ { "filename" : "MicrochipIcon.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "Microchip - Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/Contents.json) { "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBAIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBAIcon.imageset/Contents.json) { "images" : [ { "filename" : "GBA.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBA_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBCIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBCIcon.imageset/Contents.json) { "images" : [ { "filename" : "GBC_Grape.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBCIcon Berry.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBCIcon_Berry.imageset/Contents.json) { "images" : [ { "filename" : "GBC_Berry.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBCIcon Dandelion.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBCIcon_Dandelion.imageset/Contents.json) { "images" : [ { "filename" : "GBC_Dandelion.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBCIcon Kiwi.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBCIcon_Kiwi.imageset/Contents.json) { "images" : [ { "filename" : "GBC_Kiwi.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBCIcon Teal.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBCIcon_Teal.imageset/Contents.json) { "images" : [ { "filename" : "GBC_Teal.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GBC_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/GBIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/GBIcon.imageset/Contents.json) { "images" : [ { "filename" : "GameBoy.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "GameBoy_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/N64Icon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/N64Icon.imageset/Contents.json) { "images" : [ { "filename" : "N64.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "N64_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/NESIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/NESIcon.imageset/Contents.json) { "images" : [ { "filename" : "NES.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "NES_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/Icons.Xcassets/Raw/Patrons Button Pack/SNESIcon.Imageset/Contents.Json (Resources/Icons.xcassets/Raw/Patrons - Button Pack/SNESIcon.imageset/Contents.json) { "images" : [ { "filename" : "SNES.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "SNES_Dark.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } --- ### Resources/MicrochipIcon.Icon/Icon.Json (Resources/MicrochipIcon.icon/icon.json) { "color-space-for-untagged-svg-colors" : "display-p3", "fill-specializations" : [ { "value" : { "linear-gradient" : [ "display-p3:0.64706,0.39216,0.92549,1.00000", "display-p3:0.34118,0.12941,0.62353,1.00000" ] } }, { "appearance" : "dark", "value" : "system-dark" } ], "groups" : [ { "blend-mode-specializations" : [ { "value" : "plus-lighter" }, { "appearance" : "dark", "value" : "plus-lighter" }, { "appearance" : "tinted", "value" : "screen" } ], "blur-material-specializations" : [ { "appearance" : "dark", "value" : null }, { "appearance" : "tinted", "value" : 0.25 } ], "layers" : [ { "blend-mode-specializations" : [ { "value" : "plus-lighter" }, { "appearance" : "dark", "value" : "normal" }, { "appearance" : "tinted", "value" : "normal" } ], "fill-specializations" : [ { "value" : { "linear-gradient" : [ "display-p3:0.84314,0.84314,0.84314,1.00000", "display-p3:0.84314,0.84314,0.84314,0.55000" ] } }, { "appearance" : "dark", "value" : { "linear-gradient" : [ "display-p3:0.94868,0.85523,0.83495,1.00000", "display-p3:0.73573,0.65403,0.99808,0.79980" ] } }, { "appearance" : "tinted", "value" : { "linear-gradient" : [ "display-p3:1.00000,1.00000,1.00000,1.00000", "display-p3:0.86213,0.87172,0.87172,1.00000" ] } } ], "glass-specializations" : [ { "value" : true }, { "appearance" : "tinted", "value" : true } ], "image-name-specializations" : [ { "value" : "04_symbols.svg" }, { "idiom" : "square", "value" : "logo.svg" } ], "name" : "04_symbols-alt", "opacity-specializations" : [ { "value" : 1 }, { "appearance" : "tinted", "value" : 1 } ], "position-specializations" : [ { "idiom" : "square", "value" : { "scale" : 1, "translation-in-points" : [ 0, 0 ] } } ] }, { "blend-mode-specializations" : [ { "value" : "plus-lighter" }, { "appearance" : "dark", "value" : "normal" }, { "appearance" : "tinted", "value" : "normal" } ], "fill-specializations" : [ { "value" : { "solid" : "display-p3:0.86271,0.86271,0.86271,0.55000" } }, { "appearance" : "dark", "value" : { "solid" : "display-p3:0.76641,0.69055,0.97849,0.80000" } }, { "appearance" : "tinted", "value" : "none" } ], "glass-specializations" : [ { "value" : false }, { "appearance" : "tinted", "value" : true } ], "image-name-specializations" : [ { "value" : "04_symbols.svg" }, { "idiom" : "square", "value" : "delta-text.svg" } ], "name" : "04_symbols-alt", "opacity-specializations" : [ { "value" : 1 }, { "appearance" : "tinted", "value" : 1 } ], "position-specializations" : [ { "idiom" : "square", "value" : { "scale" : 0.88, "translation-in-points" : [ 0, 0 ] } } ] } ], "lighting-specializations" : [ { "value" : "individual" }, { "appearance" : "tinted", "value" : "combined" } ], "opacity-specializations" : [ { "value" : 0.65 }, { "appearance" : "dark", "value" : 0.6 }, { "appearance" : "tinted", "value" : 1 } ], "position-specializations" : [ { "idiom" : "square", "value" : { "scale" : 1, "translation-in-points" : [ 0, 2 ] } } ], "shadow-specializations" : [ { "value" : { "kind" : "none", "opacity" : 0.5 } }, { "appearance" : "tinted", "value" : { "kind" : "layer-color", "opacity" : 0.8 } } ], "specular-specializations" : [ { "value" : false }, { "appearance" : "dark", "value" : false }, { "appearance" : "tinted", "value" : true } ], "translucency-specializations" : [ { "value" : { "enabled" : false, "value" : 0.5 } }, { "appearance" : "dark", "value" : { "enabled" : false, "value" : 0 } }, { "appearance" : "tinted", "value" : { "enabled" : true, "value" : 0.65 } } ] }, { "blend-mode-specializations" : [ { "appearance" : "tinted", "value" : "plus-lighter" } ], "blur-material-specializations" : [ { "value" : 1 }, { "appearance" : "dark", "value" : 1 }, { "appearance" : "tinted", "value" : null } ], "hidden-specializations" : [ { "idiom" : "square", "value" : false } ], "layers" : [ { "blend-mode-specializations" : [ { "value" : "hard-light" }, { "appearance" : "tinted", "value" : "hard-light" } ], "glass-specializations" : [ { "value" : false }, { "appearance" : "tinted", "value" : false } ], "hidden" : false, "image-name-specializations" : [ { "value" : "texture.svg" }, { "idiom" : "square", "value" : "texture.svg" } ], "name" : "texture", "opacity-specializations" : [ { "value" : 0.07 }, { "appearance" : "tinted", "value" : 0.2 } ] }, { "blend-mode-specializations" : [ { "appearance" : "dark", "value" : "normal" }, { "appearance" : "tinted", "value" : "normal" } ], "fill-specializations" : [ { "value" : { "linear-gradient" : [ "display-p3:0.24706,0.24706,0.28627,1.00000", "display-p3:0.14826,0.14826,0.17246,1.00000" ] } }, { "appearance" : "dark", "value" : { "linear-gradient" : [ "display-p3:0.32549,0.22353,0.48235,1.00000", "display-p3:0.25490,0.16863,0.38431,1.00000" ] } }, { "appearance" : "tinted", "value" : { "solid" : "srgb:0.50000,0.50000,0.50000,1.00000" } } ], "glass-specializations" : [ { "value" : true }, { "appearance" : "tinted", "value" : true } ], "hidden" : false, "image-name-specializations" : [ { "value" : "02_chip.png" }, { "idiom" : "square", "value" : "02_chip.svg" } ], "name" : "02_chip", "opacity-specializations" : [ { "appearance" : "tinted", "value" : 1 } ] } ], "lighting-specializations" : [ { "value" : "individual" }, { "appearance" : "tinted", "value" : "individual" } ], "opacity-specializations" : [ { "value" : 1 }, { "appearance" : "tinted", "value" : 0.6 } ], "position-specializations" : [ { "idiom" : "square", "value" : { "scale" : 1, "translation-in-points" : [ 0, 0 ] } } ], "shadow-specializations" : [ { "value" : { "kind" : "layer-color", "opacity" : 0.35 } }, { "appearance" : "dark", "value" : { "kind" : "layer-color", "opacity" : 0.2 } }, { "appearance" : "tinted", "value" : { "kind" : "layer-color", "opacity" : 1 } } ], "specular-specializations" : [ { "value" : true }, { "appearance" : "tinted", "value" : true } ], "translucency-specializations" : [ { "value" : { "enabled" : true, "value" : 0.25 } }, { "appearance" : "dark", "value" : { "enabled" : true, "value" : 0.4 } }, { "appearance" : "tinted", "value" : { "enabled" : true, "value" : 0.71 } } ] }, { "blend-mode-specializations" : [ { "value" : "normal" }, { "appearance" : "dark", "value" : "normal" }, { "appearance" : "tinted", "value" : "plus-lighter" } ], "blur-material-specializations" : [ { "value" : null }, { "appearance" : "tinted", "value" : null } ], "hidden" : false, "layers" : [ { "blend-mode-specializations" : [ { "appearance" : "tinted", "value" : "normal" } ], "fill-specializations" : [ { "appearance" : "tinted", "value" : "automatic" } ], "glass-specializations" : [ { "appearance" : "tinted", "value" : true } ], "hidden" : false, "image-name-specializations" : [ { "value" : "01_connectors.png" }, { "idiom" : "square", "value" : "01_connectors.svg" } ], "name" : "01_connectors", "opacity-specializations" : [ { "appearance" : "tinted", "value" : 1 } ], "position-specializations" : [ { "idiom" : "square", "value" : { "scale" : 1, "translation-in-points" : [ 0, 0 ] } } ] } ], "lighting" : "individual", "opacity-specializations" : [ { "appearance" : "dark", "value" : 0.94 }, { "appearance" : "tinted", "value" : 1 } ], "shadow-specializations" : [ { "value" : { "kind" : "layer-color", "opacity" : 1 } }, { "appearance" : "dark", "value" : { "kind" : "layer-color", "opacity" : 0.4 } }, { "appearance" : "tinted", "value" : { "kind" : "layer-color", "opacity" : 1 } } ], "specular-specializations" : [ { "value" : true }, { "appearance" : "dark", "value" : true }, { "appearance" : "tinted", "value" : true } ], "translucency-specializations" : [ { "value" : { "enabled" : true, "value" : 0.4 } }, { "appearance" : "dark", "value" : { "enabled" : true, "value" : 0.75 } }, { "appearance" : "tinted", "value" : { "enabled" : true, "value" : 0.75 } } ] }, { "blend-mode-specializations" : [ { "appearance" : "tinted", "value" : "normal" } ], "blur-material-specializations" : [ { "value" : null }, { "appearance" : "tinted", "value" : null } ], "layers" : [ { "fill-specializations" : [ { "value" : { "solid" : "display-p3:0.89412,0.76471,0.37647,1.00000" } }, { "appearance" : "dark", "value" : { "solid" : "display-p3:0.89412,0.76471,0.37647,1.00000" } }, { "appearance" : "tinted", "value" : "none" } ], "glass-specializations" : [ { "appearance" : "dark", "value" : true }, { "appearance" : "tinted", "value" : false } ], "image-name" : "thnx.svg", "name" : "thnx", "opacity-specializations" : [ { "value" : 0.4 }, { "appearance" : "tinted", "value" : 0.4 } ], "position-specializations" : [ { "idiom" : "square", "value" : { "scale" : 1, "translation-in-points" : [ 0, 425 ] } } ] }, { "blend-mode" : "plus-darker", "fill-specializations" : [ { "value" : { "linear-gradient" : [ "display-p3:0.80000,0.80000,0.80000,0.50000", "display-p3:0.80000,0.80000,0.80000,0.80242" ] } }, { "appearance" : "dark", "value" : { "linear-gradient" : [ "display-p3:0.80000,0.80000,0.80000,0.25362", "display-p3:0.80000,0.80000,0.80000,0.80242" ] } }, { "appearance" : "tinted", "value" : { "linear-gradient" : [ "display-p3:0.80000,0.80000,0.80000,0.20092", "display-p3:0.80000,0.80000,0.80000,0.79910" ] } } ], "glass-specializations" : [ { "value" : true }, { "appearance" : "tinted", "value" : true } ], "hidden" : false, "image-name-specializations" : [ { "value" : "00_stripes.svg" }, { "idiom" : "square", "value" : "00_stripes.png" } ], "name" : "00_stripes", "opacity-specializations" : [ { "value" : 0.2 }, { "appearance" : "dark", "value" : 0.1 }, { "appearance" : "tinted", "value" : 0 } ] } ], "lighting" : "individual", "opacity" : 1, "shadow" : { "kind" : "none", "opacity" : 0.5 }, "specular-specializations" : [ { "value" : true }, { "appearance" : "dark", "value" : false }, { "appearance" : "tinted", "value" : true } ], "translucency-specializations" : [ { "value" : { "enabled" : false, "value" : 0.5 } }, { "appearance" : "dark", "value" : { "enabled" : false, "value" : 0.5 } }, { "appearance" : "tinted", "value" : { "enabled" : false, "value" : 0.5 } } ] } ], "supported-platforms" : { "squares" : "shared" } } ---