├── .github ├── FUNDING.yml └── workflows │ └── documentation.yml ├── Examples └── README.md ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcuserdata │ └── julian.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Sources └── Loadability │ ├── Other │ ├── HasPlaceholder.swift │ ├── LocalizedError.swift │ └── ThrowsErrors.swift │ ├── Networking │ ├── GenericKey.swift │ ├── SimpleNetworkLoader.swift │ ├── CachedLoader.swift │ └── Loader.swift │ ├── Caching │ ├── AnySharedCache.swift │ ├── SharedCache.swift │ ├── SharedSerializableCache.swift │ ├── SerializableCache.swift │ └── Cache.swift │ ├── Extensions │ ├── View-ErrorAlert.swift │ └── Error-Description.swift │ └── UI │ ├── LoadableView.swift │ └── Load.swift ├── Package.swift ├── LICENSE ├── docs ├── SomeLoader │ └── index.html ├── AnyIdentifiableError │ └── index.html ├── Cache │ └── index.html ├── HasPlaceholder │ └── index.html ├── CachedLoader │ └── index.html ├── ThrowsErrors │ └── index.html ├── SharedCache │ └── index.html ├── SharedSerializableCache │ └── index.html ├── index.html ├── AnySharedCache │ └── index.html ├── SimpleNetworkLoader │ └── index.html ├── LoadableView │ └── index.html ├── GenericKey │ └── index.html ├── Load │ └── index.html ├── IdentifiableError │ └── index.html ├── SerializableCache │ └── index.html ├── Loader │ └── index.html └── all.css ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [julianschiavo] 2 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | See the [Loadability Examples](https://github.com/julianschiavo/loadability-examples) Github Repository for example projects with Loadability. 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/Loadability/Other/HasPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A view that has a placeholder 4 | public protocol HasPlaceholder: View { 5 | associatedtype Placeholder: View 6 | 7 | /// A placeholder for the view, optionally annotated with `redacted(reason:)` to use a system-default placeholder style on placeholder content. 8 | static var placeholder: Placeholder { get } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Loadability/Networking/GenericKey.swift: -------------------------------------------------------------------------------- 1 | /// A generic key, used when the loadable value is not keyed by anything. 2 | public enum GenericKey: String, Codable, Hashable, Identifiable { 3 | /// A generic key, used when the loadable value is not keyed by anything. 4 | case key 5 | 6 | /// The stable identity of the entity associated with this instance. 7 | public var id: String { rawValue } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Loadability/Caching/AnySharedCache.swift: -------------------------------------------------------------------------------- 1 | /// A shared cache. 2 | public protocol AnySharedCache { 3 | associatedtype Key: Hashable & Identifiable 4 | associatedtype Value 5 | 6 | // static subscript(key: Key) -> Value? { get set } 7 | static func value(for key: Key) async throws -> Value? 8 | static func update(key: Key, to value: Value) async throws 9 | static func removeValue(for key: Key) async throws 10 | static func isValueStale(_ key: Key) -> Bool 11 | } 12 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/julian.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Loadability.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | Loadability 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Loadability", 8 | platforms: [.iOS("15"), .macOS("12"), .watchOS("8")], 9 | products: [ 10 | .library(name: "Loadability", targets: ["Loadability"]), 11 | ], 12 | dependencies: [ 13 | // Dependencies declare other packages that this package depends on. 14 | // .package(url: /* package url */, from: "1.0.0"), 15 | ], 16 | targets: [ 17 | .target(name: "Loadability", dependencies: []), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Sources/Loadability/Other/LocalizedError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct _LocalizedError: LocalizedError { 4 | /// A localized message describing what error occurred. 5 | var errorDescription: String? { 6 | error.userVisibleTitle 7 | } 8 | 9 | /// A localized message describing the reason for the failure. 10 | var failureReason: String? { 11 | nil 12 | } 13 | 14 | /// A localized message describing how one might recover from the failure. 15 | var recoverySuggestion: String? { 16 | error.recoverySuggestion 17 | } 18 | 19 | /// A localized message providing "help" text if the user requests help. 20 | var helpAnchor: String? { 21 | nil 22 | } 23 | 24 | /// The underlying error object. 25 | var error: Error 26 | 27 | init(_ error: Error) { 28 | self.error = error 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Loadability/Extensions/View-ErrorAlert.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public extension View { 4 | /// Presents an alert to the user if an error occurs. 5 | /// - Parameters: 6 | /// - alertContent: Content to display in the alert. 7 | func errorAlert(isPresented: Binding, error optionalError: Error?, message: ((Error) -> String)? = nil, dismiss: @MainActor @escaping () -> Void = { }) -> some View { 8 | var error: _LocalizedError? 9 | if let optionalError = optionalError { 10 | error = _LocalizedError(optionalError) 11 | } 12 | 13 | return alert(isPresented: isPresented, error: error) { _ in 14 | Button("OK") { 15 | Task { 16 | await MainActor.run { 17 | dismiss() 18 | } 19 | } 20 | } 21 | } message: { error in 22 | Text(message?(error) ?? error.userVisibleTitle) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | # Controls when the action will run. 4 | on: [push] 5 | 6 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 7 | jobs: 8 | # This workflow contains a single job called "build" 9 | build: 10 | # The type of runner that the job will run on 11 | runs-on: ubuntu-latest 12 | 13 | # Steps represent a sequence of tasks that will be executed as part of the job 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v2 17 | 18 | - name: Generate Documentation 19 | uses: SwiftDocOrg/swift-doc@master 20 | with: 21 | module-name: Loadability 22 | output: "docs" 23 | format: "html" 24 | base-url: "/Loadability/" 25 | - name: Publish Documentation 26 | uses: mikeal/publish-to-github-action@master 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | BRANCH_NAME: 'main' 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Julian Schiavo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/Loadability/Extensions/Error-Description.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Error { 4 | /// The `NSError` representation of the error. 5 | private var nsError: NSError { 6 | self as NSError 7 | } 8 | 9 | /// The user visible title for the error. 10 | var userVisibleTitle: String { 11 | nsError.localizedFailureReason ?? "" 12 | } 13 | 14 | private var description: String? { 15 | guard !nsError.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return "" } 16 | return nsError.localizedDescription 17 | } 18 | 19 | var recoverySuggestion: String? { 20 | guard let suggestion = nsError.localizedRecoverySuggestion, 21 | !suggestion.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } 22 | return nsError.localizedRecoverySuggestion 23 | } 24 | 25 | /// The user visible description for the error. 26 | var userVisibleOverallDescription: String { 27 | [description, recoverySuggestion] 28 | .compactMap { $0 } 29 | .joined(separator: " ") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Loadability/Networking/SimpleNetworkLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type that can load data from over the network and throw errors. 4 | @MainActor 5 | public protocol SimpleNetworkLoader: Loader { 6 | /// Creates a `URLRequest` for a network loading request. 7 | /// - Parameter key: The key identifying the object to load. 8 | func createRequest(for key: Key) async -> URLRequest 9 | 10 | /// Decodes data received from a network request into the object. 11 | /// - Parameters: 12 | /// - data: The data received from the request. 13 | /// - key: The key identifying the object to load. 14 | func decode(_ data: Data, key: Key) async throws -> Object 15 | } 16 | 17 | public extension SimpleNetworkLoader { 18 | func loadData(key: Key) async throws -> Object { 19 | let request = await createRequest(for: key) 20 | let (data, _) = try await URLSession.shared.data(for: request) 21 | return try await self.decode(data, key: key) 22 | } 23 | } 24 | 25 | public extension SimpleNetworkLoader where Object: Codable { 26 | func decode(_ data: Data, key: Key) async throws -> Object { 27 | let handle = Task { () -> Object in 28 | let decoder = JSONDecoder() 29 | return try decoder.decode(Object.self, from: data) 30 | } 31 | return try await handle.value 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Loadability/Other/ThrowsErrors.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | /// A type that can throw errors that should be shown to the user. 5 | public protocol ThrowsErrors { 6 | /// An error, if one occurred. Must be annotated with a publisher property wrapper, such as `@State` or `@Published`, to work. 7 | @MainActor var error: Error? { get nonmutating set } 8 | } 9 | 10 | public extension ThrowsErrors { 11 | /// Attempts to execute a block, catching errors thrown by displaying the error to the user. 12 | /// - Parameter block: A throwing block. 13 | @MainActor func tryAndCatch(_ block: () throws -> Void) { 14 | do { 15 | try block() 16 | } catch { 17 | catchError(error) 18 | } 19 | } 20 | 21 | /// Catches a thrown error, updating the view state to display the error if a subscriber is subscribed to it (through `View.alert(errorBinding: $error)`). 22 | /// - Parameter error: The error that occurred. 23 | @MainActor func catchError(_ error: Error?) { 24 | self.error = error 25 | } 26 | 27 | /// Dismisses the current error, if there is one. 28 | @MainActor func dismissError() { 29 | self.error = nil 30 | } 31 | 32 | /// Catches an error thrown from a Combine Publisher. 33 | /// - Parameter completion: The completion received from the publisher. 34 | @MainActor func catchCompletion(_ completion: Subscribers.Completion) { 35 | guard case let .failure(error) = completion else { return } 36 | catchError(error) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/SomeLoader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - SomeLoader 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Typealias 37 | Some​Loader 38 |

39 | 40 |
public typealias SomeLoader = Loader
41 |
42 |
43 | 44 |
45 |

46 | Generated on using swift-doc 1.0.0-beta.5. 47 |

48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/AnyIdentifiableError/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - AnyIdentifiableError 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Typealias 37 | Any​Identifiable​Error 38 |

39 | 40 |
public typealias AnyIdentifiableError = Error & Identifiable
41 |
42 |
43 | 44 |
45 |

46 | Generated on using swift-doc 1.0.0-beta.5. 47 |

48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Loadability Contribution Guide 2 | 3 | *Adapted from the [Plot Contribution Guide](https://github.com/JohnSundell/Plot/blob/master/CONTRIBUTING.md)* 4 | 5 | Welcome to the **Loadability Contribution Guide**, which aims to give you all the information you need to contribute to Loadability. Thank you for your interest in contributing to this project! 6 | 7 | ## Bugs, feature requests and support 8 | 9 | ### I found a bug, how do I report it? 10 | 11 | If you find a bug, such as loading or caching not working correctly, please file a detailed Github Issue containing all of the information necessary to reproduce the bug. This helps us to quickly understand the bug and triage it. 12 | 13 | ### I have an idea for a feature request 14 | 15 | Awesome! You can either create a Github Issue describing your idea and how it would improve the project, or create a Pull Request with your feature. Creating a Pull Request contributes to the project and makes it more likely your idea will be added! 16 | 17 | ### I have a question 18 | 19 | Please make sure you read the documentation (inline in the source files) and [README](README.md) carefully. If your question is still not answered, file a Github Issue detailing your question and what steps you took already. 20 | 21 | ## Project structure 22 | 23 | Loadability is structured in the default way for Swift Packages. Source code is in the `Source/Loadability` directory, with each type in a separate file. The library has extensive documentation; please ensure that additive changes have high-quality documentation. 24 | 25 | ## Conclusion 26 | 27 | Hopefully this document helped you better understand how Loadability is structured and the best way to get help or contribute to the project. Thanks again for the interest! 28 | -------------------------------------------------------------------------------- /Sources/Loadability/Caching/SharedCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A singleton collection used to store key-value pairs as a wrapper to `Cache`. 4 | public protocol SharedCache: AnySharedCache { 5 | /// The shared `Cache`. 6 | static var shared: Cache { get } 7 | } 8 | 9 | public extension SharedCache { 10 | /// Accesses the value associated with the given key. 11 | /// - Parameter key: The key to find in the cache. 12 | /// - Returns: The value associated with `key` if `key` is in the cache; otherwise, `nil`. 13 | static func value(for key: Key) async throws -> Value? { 14 | shared[key] 15 | } 16 | 17 | /// Updates the cached value for the given key, or adds the key-value pair to the cache if the key does not exist. 18 | /// - Parameters: 19 | /// - value: The new value to add to the cache. 20 | /// - key: The key to associate with `value`. If `key` already exists in the cache, `value` replaces the existing associated value. If `key` isn’t already a key of the cache, the (`key`, `value`) pair is added. 21 | static func update(key: Key, to newValue: Value) async throws { 22 | shared[key] = newValue 23 | } 24 | 25 | /// Removes the given key and its associated value from the cache. 26 | /// - Parameter key: The key to remove along with its associated value. 27 | static func removeValue(for key: Key) async throws { 28 | shared[key] = nil 29 | } 30 | 31 | /// Whether the value associated with the `key` is stale. Returns `true` if the key is not in the cache. 32 | /// - Parameter key: The key to find in the cache. 33 | /// - Returns: Whether the value associated with the `key` is stale. 34 | static func isValueStale(_ key: Key) -> Bool { 35 | shared.isValueStale(forKey: key) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Loadability/Caching/SharedSerializableCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A singleton collection, with support for serialization and storage, used to store key-value pairs as a wrapper to `SerializableCache`. 4 | public protocol SharedSerializableCache: AnySharedCache where Key: Codable, Value: Codable { 5 | /// The shared `SerializableCache`. 6 | static var shared: SerializableCache { get } 7 | } 8 | 9 | public extension SharedSerializableCache { 10 | /// Accesses the value associated with the given key. 11 | /// - Parameter key: The key to find in the cache. 12 | /// - Returns: The value associated with `key` if `key` is in the cache; otherwise, `nil`. 13 | static func value(for key: Key) async throws -> Value? { 14 | shared[key] 15 | } 16 | 17 | /// Updates the cached value for the given key, or adds the key-value pair to the cache if the key does not exist. 18 | /// - Parameters: 19 | /// - value: The new value to add to the cache. 20 | /// - key: The key to associate with `value`. If `key` already exists in the cache, `value` replaces the existing associated value. If `key` isn’t already a key of the cache, the (`key`, `value`) pair is added. 21 | static func update(key: Key, to newValue: Value) async throws { 22 | shared[key] = newValue 23 | } 24 | 25 | /// Removes the given key and its associated value from the cache. 26 | /// - Parameter key: The key to remove along with its associated value. 27 | static func removeValue(for key: Key) async throws { 28 | shared[key] = nil 29 | } 30 | 31 | /// Whether the value associated with the `key` is stale. Returns `true` if the key is not in the cache. 32 | /// - Parameter key: The key to find in the cache. 33 | /// - Returns: Whether the value associated with the `key` is stale. 34 | static func isValueStale(_ key: Key) -> Bool { 35 | shared.isValueStale(forKey: key) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Loadability/Networking/CachedLoader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A type that can load data from a source with caching. 4 | @MainActor 5 | public protocol CachedLoader: Loader where Key == Cache.Key, Object == Cache.Value { 6 | /// The type of cache. 7 | associatedtype Cache: AnySharedCache 8 | 9 | /// The cache. 10 | var cache: Cache.Type { get } 11 | } 12 | 13 | public extension CachedLoader { 14 | @discardableResult func load(key: Key) async -> Object? { 15 | let task = Task { () -> Object in 16 | let object: Object 17 | 18 | let cached = await loadCachedData(key: key) 19 | if let cached = cached, !cache.isValueStale(key) { 20 | object = cached 21 | } else { 22 | object = try await loadData(key: key) 23 | } 24 | 25 | self.object = object 26 | await loadCompleted(key: key, object: object) 27 | return object 28 | } 29 | self.task = task 30 | 31 | do { 32 | let object = try await task.value 33 | self.task = nil 34 | return object 35 | } catch { 36 | catchError(error) 37 | return nil 38 | } 39 | } 40 | 41 | func refresh(key: Key) async { 42 | guard task == nil else { return } 43 | await cancel() 44 | try? await cache.removeValue(for: key) 45 | object = nil 46 | // await load(key: key) 47 | } 48 | 49 | /// Attempts to load data from the cache. 50 | /// - Parameter key: The key identifying the object to load. 51 | private func loadCachedData(key: Key) async -> Object? { 52 | let handle = Task { 53 | try? await self.cache.value(for: key) 54 | } 55 | return await handle.value 56 | } 57 | 58 | /// Attempts to fetch data from the cache. 59 | /// - Parameters: 60 | /// - key: The key identifying the object to load. 61 | /// - completion: A completion handler called with the object, or `nil` if no object was found. 62 | func getCachedData(key: Key) async -> Object? { 63 | let handle = Task { 64 | try? await self.cache.value(for: key) 65 | } 66 | return await handle.value 67 | } 68 | 69 | func loadCompleted(key: Key, object: Object) async { 70 | Task { 71 | try? await self.cache.update(key: key, to: object) 72 | } 73 | } 74 | } 75 | 76 | public extension CachedLoader where Key == GenericKey { 77 | @discardableResult func load() async -> Object? { 78 | await load(key: .key) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/Loadability/UI/LoadableView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A `View` that loads content through a `Loader`. 4 | public protocol LoadableView: View { 5 | /// The type of `View` representing the content view. 6 | associatedtype Content: View 7 | /// The type of loader. 8 | associatedtype Loader: SomeLoader 9 | /// The type of `View` representing the placeholder view. 10 | associatedtype Placeholder: View 11 | /// The type of value loaded by the `Loader`. 12 | associatedtype Value 13 | 14 | /// The type of `Load` view. 15 | typealias LoaderView = Load? 16 | 17 | /// The key path of the value on the loaded object. 18 | typealias ValueKeyPath = KeyPath 19 | 20 | /// The key identifying the object to load. 21 | var key: Loader.Key { get } 22 | 23 | /// The key path of the value on the loaded object, defaults to `nil`. 24 | var keyPath: ValueKeyPath? { get } 25 | 26 | /// The loader used to load content. 27 | var loader: Loader { get } 28 | 29 | /// The placeholder to show while loading. 30 | func placeholder() -> Placeholder 31 | 32 | /// Creates a view using loaded content. 33 | /// - Parameter value: Loaded content. 34 | func body(with value: Value) -> Content 35 | } 36 | 37 | public extension LoadableView { 38 | var keyPath: KeyPath? { 39 | nil 40 | } 41 | 42 | /// The `Load` wrapper used to handle the view state. 43 | @ViewBuilder var loaderView: LoaderView { 44 | if let keyPath = keyPath { 45 | Load(with: loader, key: key, objectKeyPath: keyPath, content: body, placeholder: placeholder) 46 | } 47 | } 48 | } 49 | 50 | public extension LoadableView where Loader.Key == GenericKey { 51 | var key: GenericKey { 52 | .key 53 | } 54 | 55 | /// The `Load` wrapper used to handle the view state. 56 | @ViewBuilder var loaderView: LoaderView { 57 | if let keyPath = keyPath { 58 | Load(with: loader, objectKeyPath: keyPath, content: body, placeholder: placeholder) 59 | } 60 | } 61 | } 62 | 63 | public extension LoadableView where Loader.Key == GenericKey, Loader.Object == Value { 64 | /// The `Load` wrapper used to handle the view state. 65 | @ViewBuilder var loaderView: LoaderView { 66 | Load(with: loader, content: body, placeholder: placeholder) 67 | } 68 | } 69 | 70 | public extension LoadableView where Loader.Object == Value { 71 | /// The `Load` wrapper used to handle the view state. 72 | @ViewBuilder var loaderView: LoaderView { 73 | Load(with: loader, key: key, content: body, placeholder: placeholder) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/Cache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - Cache 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Class 37 | Cache 38 |

39 | 40 |
public class Cache<Key: Hashable & Identifiable, Value>
41 |
42 |

A mutable collection used to store key-value pairs that are subject to eviction when resources are low.

43 | 44 |
45 | 46 |
47 |

Initializers

48 | 49 |
50 |

51 | init(should​Automatically​Remove​Stale​Items:​) 52 |

53 |
public init(shouldAutomaticallyRemoveStaleItems autoRemoveStaleItems: Bool = false)
54 |
55 |

Creates a new Cache

56 | 57 |
58 |

Parameters

59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 |
auto​Remove​Stale​ItemsBool

Whether to automatically remove stale items, false by default.

73 |
77 |
78 |
79 | 80 | 81 | 82 |
83 |
84 | 85 |
86 |

87 | Generated on using swift-doc 1.0.0-beta.5. 88 |

89 |
90 | 91 | 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at julian@schiavo.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Sources/Loadability/Networking/Loader.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | public typealias SomeLoader = Loader 5 | 6 | /// A type that can load data from a source and throw errors. 7 | @MainActor 8 | public protocol Loader: ObservableObject, ThrowsErrors { 9 | /// The type of key that identifies objects. 10 | associatedtype Key: Hashable 11 | /// The type of object to load. 12 | associatedtype Object 13 | 14 | /// Creates a new instance of the loader. 15 | init() 16 | 17 | /// A publisher for the object that is loaded. 18 | var object: Object? { get set } 19 | 20 | /// An ongoing request. 21 | var cancellable: AnyCancellable? { get set } 22 | 23 | /// An ongoing task. 24 | var task: Task? { get set } 25 | 26 | /// Begins loading the object. 27 | /// - Parameter key: The key identifying the object to load. 28 | @discardableResult func load(key: Key) async -> Object? 29 | 30 | /// Creates a publisher that loads the object. 31 | /// - Parameter key: The key identifying the object to load. 32 | func createPublisher(key: Key) -> AnyPublisher? 33 | 34 | /// Starts loading the object's data. If you implement `loadData(key:)`, do not implement `createPublisher(key:)`. 35 | /// - Parameter key: The key identifying the object to load. 36 | func loadData(key: Key) async throws -> Object 37 | 38 | /// Refreshes the data by re-loading it (this resets the cache). 39 | /// - Parameter key: The key identifying the object to load. 40 | func refresh(key: Key) async 41 | 42 | /// Called when the object has been loaded successfully. 43 | /// - Parameters: 44 | /// - key: The key identifying the object that was loaded. 45 | /// - object: The loaded object. 46 | func loadCompleted(key: Key, object: Object) async 47 | 48 | /// Cancels the current loading operation. 49 | func cancel() async 50 | } 51 | 52 | public extension Loader { 53 | @discardableResult func load(key: Key) async -> Object? { 54 | let task = Task { () -> Object in 55 | let object = try await loadData(key: key) 56 | self.object = object 57 | await loadCompleted(key: key, object: object) 58 | return object 59 | } 60 | self.task = task 61 | 62 | do { 63 | let object = try await task.value 64 | self.task = nil 65 | return object 66 | } catch { 67 | catchError(error) 68 | return nil 69 | } 70 | } 71 | 72 | func refresh(key: Key) async { 73 | guard task == nil else { return } 74 | await cancel() 75 | object = nil 76 | await load(key: key) 77 | } 78 | 79 | func loadData(key: Key) async throws -> Object { 80 | guard let publisher = createPublisher(key: key) else { 81 | fatalError("You must implement either loadData(key:) or createPublisher(key:).") 82 | } 83 | 84 | return try await withCheckedThrowingContinuation { continuation in 85 | var didFinish = false 86 | cancellable = publisher 87 | .subscribe(on: DispatchQueue.global(qos: .userInitiated)) 88 | .receive(on: DispatchQueue.main) 89 | .sink { completion in 90 | guard case let .failure(error) = completion, !didFinish else { return } 91 | didFinish = true 92 | continuation.resume(throwing: error) 93 | } receiveValue: { object in 94 | guard !didFinish else { return } 95 | didFinish = true 96 | continuation.resume(returning: object) 97 | } 98 | } 99 | } 100 | 101 | func createPublisher(key: Key) -> AnyPublisher? { 102 | // Default implementation does nothing. This method is optional if `loadData(key:)` is implemented. 103 | return nil 104 | } 105 | 106 | func loadCompleted(key: Key, object: Object) async { 107 | // Default implementation does nothing. This is used by the more advanced loaders to allow for inserting cache events. 108 | } 109 | 110 | /// Cancels the ongoing load. 111 | func cancel() async { 112 | task?.cancel() 113 | task = nil 114 | 115 | cancellable?.cancel() 116 | cancellable = nil 117 | } 118 | } 119 | 120 | public extension Loader where Key == GenericKey { 121 | @discardableResult func load() async -> Object? { 122 | await load(key: .key) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /docs/HasPlaceholder/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - HasPlaceholder 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Has​Placeholder 38 |

39 | 40 |
public protocol HasPlaceholder: View
41 |
42 |

A view that has a placeholder

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | HasPlaceholder 62 | 63 | 64 | HasPlaceholder 65 | 66 | 67 | 68 | 69 | 70 | View 71 | 72 | View 73 | 74 | 75 | 76 | HasPlaceholder->View 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |

Conforms To

87 |
88 |
View
89 |
90 |
91 | 92 | 93 | 94 |
95 |

Requirements

96 | 97 |
98 |

99 | placeholder 100 |

101 |
var placeholder: Placeholder
102 |
103 |

A placeholder for the view, optionally annotated with redacted(reason:) to use a system-default placeholder style on placeholder content.

104 | 105 |
106 |
107 |
108 |
109 |
110 | 111 |
112 |

113 | Generated on using swift-doc 1.0.0-beta.5. 114 |

115 |
116 | 117 | 118 | -------------------------------------------------------------------------------- /docs/CachedLoader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - CachedLoader 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Cached​Loader 38 |

39 | 40 |
public protocol CachedLoader: Loader
41 |
42 |

A type that can load data from a source with caching.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | CachedLoader 62 | 63 | 64 | CachedLoader 65 | 66 | 67 | 68 | 69 | 70 | Loader 71 | 72 | 73 | Loader 74 | 75 | 76 | 77 | 78 | 79 | CachedLoader->Loader 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |

Conforms To

90 |
91 |
Loader
92 |

A type that can load data from a source and throw errors.

93 |
94 |
95 |
96 | 97 | 98 | 99 |
100 |

Requirements

101 | 102 |
103 |

104 | cache 105 |

106 |
var cache: Cache.Type
107 |
108 |

The cache.

109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 |
117 |

118 | Generated on using swift-doc 1.0.0-beta.5. 119 |

120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/ThrowsErrors/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - ThrowsErrors 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Throws​Errors 38 |

39 | 40 |
public protocol ThrowsErrors
41 |
42 |

A type that can throw errors that should be shown to the user.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | ThrowsErrors 62 | 63 | 64 | ThrowsErrors 65 | 66 | 67 | 68 | 69 | 70 | Loader 71 | 72 | 73 | Loader 74 | 75 | 76 | 77 | 78 | 79 | Loader->ThrowsErrors 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |

Types Conforming to Throws​Errors

90 |
91 |
Loader
92 |

A type that can load data from a source and throw errors.

93 |
94 |
95 |
96 | 97 | 98 | 99 |
100 |

Requirements

101 | 102 |
103 |

104 | error 105 |

106 |
var error: IdentifiableError?
107 |
108 |

An error, if one occurred. Must be annotated with a publisher property wrapper, such as @State or @Published, to work.

109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 |
117 |

118 | Generated on using swift-doc 1.0.0-beta.5. 119 |

120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/SharedCache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - SharedCache 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Shared​Cache 38 |

39 | 40 |
public protocol SharedCache: AnySharedCache
41 |
42 |

A singleton collection used to store key-value pairs as a wrapper to Cache.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | SharedCache 62 | 63 | 64 | SharedCache 65 | 66 | 67 | 68 | 69 | 70 | AnySharedCache 71 | 72 | 73 | AnySharedCache 74 | 75 | 76 | 77 | 78 | 79 | SharedCache->AnySharedCache 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |

Conforms To

90 |
91 |
AnySharedCache
92 |

A shared cache.

93 |
94 |
95 |
96 | 97 | 98 | 99 |
100 |

Requirements

101 | 102 |
103 |

104 | shared 105 |

106 |
var shared: Cache<Key, Value>
107 |
108 |

The shared Cache.

109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 |
117 |

118 | Generated on using swift-doc 1.0.0-beta.5. 119 |

120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/SharedSerializableCache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - SharedSerializableCache 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Shared​Serializable​Cache 38 |

39 | 40 |
public protocol SharedSerializableCache: AnySharedCache
41 |
42 |

A singleton collection, with support for serialization and storage, used to store key-value pairs as a wrapper to SerializableCache.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | SharedSerializableCache 62 | 63 | 64 | SharedSerializableCache 65 | 66 | 67 | 68 | 69 | 70 | AnySharedCache 71 | 72 | 73 | AnySharedCache 74 | 75 | 76 | 77 | 78 | 79 | SharedSerializableCache->AnySharedCache 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |

Conforms To

90 |
91 |
AnySharedCache
92 |

A shared cache.

93 |
94 |
95 |
96 | 97 | 98 | 99 |
100 |

Requirements

101 | 102 |
103 |

104 | shared 105 |

106 |
var shared: SerializableCache<Key, Value>
107 |
108 |

The shared SerializableCache.

109 | 110 |
111 |
112 |
113 |
114 |
115 | 116 |
117 |

118 | Generated on using swift-doc 1.0.0-beta.5. 119 |

120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - Loadability 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |
36 |

Classes

37 |
38 |
39 | 40 | Cache 41 | 42 |
43 |
44 |

A mutable collection used to store key-value pairs that are subject to eviction when resources are low.

45 | 46 |
47 |
48 | 49 | Serializable​Cache 50 | 51 |
52 |
53 |

A mutable collection, with support for serialization and storage, used to store key-value pairs that are subject to eviction when resources are low.

54 | 55 |
56 |
57 |
58 |
59 |

Structures

60 |
61 |
62 | 63 | Identifiable​Error 64 | 65 |
66 |
67 |

A uniquely identifiable error.

68 | 69 |
70 |
71 | 72 | Load 73 | 74 |
75 |
76 |

A view that loads content using a Loader before displaying the content in a custom View.

77 | 78 |
79 |
80 |
81 |
82 |

Enumerations

83 |
84 |
85 | 86 | Generic​Key 87 | 88 |
89 |
90 |

A generic key, used when the loadable value is not keyed by anything.

91 | 92 |
93 |
94 |
95 |
96 |

Protocols

97 |
98 |
99 | 100 | Any​Shared​Cache 101 | 102 |
103 |
104 |

A shared cache.

105 | 106 |
107 |
108 | 109 | Shared​Cache 110 | 111 |
112 |
113 |

A singleton collection used to store key-value pairs as a wrapper to Cache.

114 | 115 |
116 |
117 | 118 | Shared​Serializable​Cache 119 | 120 |
121 |
122 |

A singleton collection, with support for serialization and storage, used to store key-value pairs as a wrapper to SerializableCache.

123 | 124 |
125 |
126 | 127 | Cached​Loader 128 | 129 |
130 |
131 |

A type that can load data from a source with caching.

132 | 133 |
134 |
135 | 136 | Loader 137 | 138 |
139 |
140 |

A type that can load data from a source and throw errors.

141 | 142 |
143 |
144 | 145 | Simple​Network​Loader 146 | 147 |
148 |
149 |

A type that can load data from over the network and throw errors.

150 | 151 |
152 |
153 | 154 | Has​Placeholder 155 | 156 |
157 |
158 |

A view that has a placeholder

159 | 160 |
161 |
162 | 163 | Throws​Errors 164 | 165 |
166 |
167 |

A type that can throw errors that should be shown to the user.

168 | 169 |
170 |
171 | 172 | Loadable​View 173 | 174 |
175 |
176 |

A View that loads content through a Loader.

177 | 178 |
179 |
180 |
181 |
182 |

Typealiases

183 |
184 |
185 | 186 | Some​Loader 187 | 188 |
189 |
190 | 191 |
192 |
193 | 194 | Any​Identifiable​Error 195 | 196 |
197 |
198 | 199 |
200 |
201 |
202 |
203 |
204 | 205 |
206 |

207 | Generated on using swift-doc 1.0.0-beta.5. 208 |

209 |
210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/AnySharedCache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - AnySharedCache 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Any​Shared​Cache 38 |

39 | 40 |
public protocol AnySharedCache
41 |
42 |

A shared cache.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | AnySharedCache 62 | 63 | 64 | AnySharedCache 65 | 66 | 67 | 68 | 69 | 70 | SharedCache 71 | 72 | 73 | SharedCache 74 | 75 | 76 | 77 | 78 | 79 | SharedCache->AnySharedCache 80 | 81 | 82 | 83 | 84 | 85 | SharedSerializableCache 86 | 87 | 88 | SharedSerializableCache 89 | 90 | 91 | 92 | 93 | 94 | SharedSerializableCache->AnySharedCache 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |

Types Conforming to Any​Shared​Cache

105 |
106 |
SharedCache
107 |

A singleton collection used to store key-value pairs as a wrapper to Cache.

108 |
109 |
SharedSerializableCache
110 |

A singleton collection, with support for serialization and storage, used to store key-value pairs as a wrapper to SerializableCache.

111 |
112 |
113 |
114 | 115 | 116 | 117 |
118 |

Requirements

119 | 120 |
121 |

122 | is​Value​Stale(_:​) 123 |

124 |
static func isValueStale(_ key: Key) -> Bool
125 |
126 |
127 |
128 |
129 | 130 |
131 |

132 | Generated on using swift-doc 1.0.0-beta.5. 133 |

134 |
135 | 136 | 137 | -------------------------------------------------------------------------------- /Sources/Loadability/Caching/SerializableCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A mutable collection, with support for serialization and storage, used to store key-value pairs that are subject to eviction when resources are low. 4 | public final class SerializableCache: Cache, Codable { 5 | /// The unique name for the cache. 6 | final var name: String 7 | 8 | /// The folder in which to store the cache. 9 | final var folderURL: URL 10 | 11 | /// A ledger of keys stored in the cache, used when serializing data to disk. 12 | private final let keyLedger = KeyLedger() 13 | 14 | /// Creates a new `SerializableCache`. 15 | /// - Parameters: 16 | /// - name: The unique name for the cache. 17 | /// - autoRemoveStaleItems: Whether to automatically remove stale items, defaults to `false`. 18 | /// - itemLifetime: How many milliseconds items are valid for, defaults to 3600. This is not used if `autoRemoveStaleItems` is equal to `false`. 19 | /// - folderURL: The folder in which to store the cache, defaults to the system cache directory. 20 | private init( 21 | name: String, 22 | shouldAutomaticallyRemoveStaleItems autoRemoveStaleItems: Bool = false, 23 | itemLifetime: TimeInterval = 3600, 24 | folderURL: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first 25 | ) { 26 | 27 | guard let folderURL = folderURL else { 28 | fatalError("Invalid Folder URL") 29 | } 30 | 31 | self.name = name 32 | self.folderURL = folderURL 33 | super.init(shouldAutomaticallyRemoveStaleItems: autoRemoveStaleItems, itemLifetime: itemLifetime) 34 | 35 | _cache.delegate = keyLedger 36 | } 37 | 38 | /// Decodes an encoded `SerializableCache`. 39 | /// - Parameter decoder: The decoder. 40 | /// - Throws: When there is an error in decoding. 41 | public required convenience init(from decoder: Decoder) throws { 42 | self.init(name: "") 43 | let container = try decoder.singleValueContainer() 44 | let entries = try container.decode([_Entry].self) 45 | entries.forEach { updateValue($0.value, forKey: $0.key, expirationDate: $0.expirationDate) } 46 | } 47 | 48 | // MARK: - Codable 49 | 50 | /// Encodes the cache and key-value pairs to disk. 51 | /// - Parameter encoder: The encoder. 52 | /// - Throws: When there is an error in encoding. 53 | public final func encode(to encoder: Encoder) throws { 54 | let entries = keyLedger.keys.compactMap(entry) 55 | var container = encoder.singleValueContainer() 56 | try container.encode(entries) 57 | } 58 | 59 | /// Encodes and saves the cache to disk. 60 | public final func save() { 61 | guard !name.isEmpty else { return } 62 | let fileURL = folderURL.appendingPathComponent(self.name + ".cache") 63 | 64 | DispatchQueue.global(qos: .default).async { 65 | do { 66 | let data = try JSONEncoder().encode(self) 67 | try data.write(to: fileURL) 68 | } catch { 69 | print("[Cache] Failed to save.", 70 | error.localizedDescription, 71 | (error as NSError).localizedRecoverySuggestion ?? "") 72 | } 73 | } 74 | } 75 | 76 | /// Loads a previously-serialized cache from disk. 77 | /// - Parameters: 78 | /// - name: The unique name of the cache. 79 | /// - shouldAutomaticallyRemoveStaleItems: Whether to automatically remove stale items, defaults to `false`. 80 | /// - itemLifetime: How many milliseconds items are valid for, defaults to 3600. This is not used if `autoRemoveStaleItems` is equal to `false`. 81 | /// - folderURL: The folder in which to store the cache, defaults to the system cache directory. 82 | /// - Returns: The loaded cache. 83 | public static func load( 84 | name: String, 85 | shouldAutomaticallyRemoveStaleItems autoRemoveStaleItems: Bool = false, 86 | itemLifetime: TimeInterval = 3600, 87 | folderURL: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first 88 | ) -> SerializableCache { 89 | 90 | guard let folderURL = folderURL else { 91 | fatalError("Invalid Folder URL") 92 | } 93 | 94 | do { 95 | let fileURL = folderURL.appendingPathComponent(name + ".cache") 96 | let data = try Data(contentsOf: fileURL) 97 | let cache = try JSONDecoder().decode(self, from: data) 98 | cache.name = name // Setting after decoding avoids re-saving cache to disk unnescessarily 99 | return cache 100 | } catch { 101 | print("[Cache] Failed to load (Name: \(name)).", 102 | error.localizedDescription, 103 | (error as NSError).localizedRecoverySuggestion ?? "") 104 | 105 | let empty = SerializableCache(name: name, shouldAutomaticallyRemoveStaleItems: autoRemoveStaleItems, itemLifetime: itemLifetime) 106 | empty.save() 107 | return empty 108 | } 109 | } 110 | 111 | // MARK: - Internal 112 | 113 | /// Updates the cached entry for the given key, or adds the entry to the cache if the key does not exist. 114 | /// - Parameters: 115 | /// - entry: The entry to add to the cache. 116 | /// - key: The key to associate with `entry`. If `key` already exists in the cache, `entry` replaces the existing entry. If `key` isn’t already a key of the cache, the entry is added. 117 | override final func updateEntry(_ entry: _Entry, forKey key: _Key) { 118 | _cache.setObject(entry, forKey: key) 119 | keyLedger.insert(entry.key, to: self) 120 | } 121 | 122 | /// Removes the given key and its associated value from the cache. 123 | /// - Parameter key: The key to remove along with its associated value. 124 | override final func removeValue(forKey key: Key) { 125 | _cache.removeObject(forKey: _Key(key)) 126 | keyLedger.remove(key, from: self) 127 | } 128 | 129 | /// A ledger that stores a list of keys, conforming to `NSCacheDelegate` to automatically remove evicted keys. 130 | final class KeyLedger: NSObject, NSCacheDelegate { 131 | /// The list of keys. 132 | private(set) final var keys = Set() 133 | 134 | final func insert(_ key: Key, to cache: SerializableCache) { 135 | guard keys.insert(key).inserted else { return } 136 | cache.save() 137 | } 138 | 139 | final func remove(_ key: Key, from cache: SerializableCache) { 140 | keys.remove(key) 141 | cache.save() 142 | } 143 | 144 | final func cache(_ cache: NSCache, willEvictObject object: Any) { 145 | guard let entry = object as? _Entry else { return } 146 | keys.remove(entry.key) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /docs/SimpleNetworkLoader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - SimpleNetworkLoader 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Simple​Network​Loader 38 |

39 | 40 |
public protocol SimpleNetworkLoader: Loader
41 |
42 |

A type that can load data from over the network and throw errors.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | SimpleNetworkLoader 62 | 63 | 64 | SimpleNetworkLoader 65 | 66 | 67 | 68 | 69 | 70 | Loader 71 | 72 | 73 | Loader 74 | 75 | 76 | 77 | 78 | 79 | SimpleNetworkLoader->Loader 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 |

Conforms To

90 |
91 |
Loader
92 |

A type that can load data from a source and throw errors.

93 |
94 |
95 |
96 | 97 | 98 | 99 |
100 |

Requirements

101 | 102 |
103 |

104 | create​Request(for:​) 105 |

106 |
func createRequest(for key: Key) -> URLRequest
107 |
108 |

Creates a URLRequest for a network loading request.

109 | 110 |
111 |

Parameters

112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 127 | 128 | 129 |
keyKey

The key identifying the object to load.

126 |
130 |
131 |
132 |

133 | decode(_:​key:​) 134 |

135 |
func decode(_ data: Data, key: Key) throws -> Object
136 |
137 |

Decodes data received from a network request into the object.

138 | 139 |
140 |

Parameters

141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 156 | 157 | 158 | 159 | 160 | 162 | 163 | 164 |
dataData

The data received from the request.

155 |
keyKey

The key identifying the object to load.

161 |
165 |
166 |
167 |
168 |
169 | 170 |
171 |

172 | Generated on using swift-doc 1.0.0-beta.5. 173 |

174 |
175 | 176 | 177 | -------------------------------------------------------------------------------- /Sources/Loadability/Caching/Cache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A mutable collection used to store key-value pairs that are subject to eviction when resources are low. 4 | public class Cache { 5 | 6 | /// Whether the cache automatically removes stale items. 7 | final let autoRemoveStaleItems: Bool 8 | 9 | /// How many milliseconds items are valid for, defaults to 3600. This is not used if `autoRemoveStaleItems` is equal to `false`. 10 | final let itemLifetime: TimeInterval 11 | 12 | /// The wrapped `NSCache`. 13 | final let _cache = NSCache<_Key, _Entry>() 14 | 15 | // MARK: - Public 16 | 17 | /// Creates a new `Cache` 18 | /// - Parameter autoRemoveStaleItems: Whether to automatically remove stale items, `false` by default. 19 | /// - Parameter itemLifetime: How many milliseconds items are valid for, defaults to 3600. This is not used if `autoRemoveStaleItems` is equal to `false`. 20 | public init(shouldAutomaticallyRemoveStaleItems autoRemoveStaleItems: Bool = false, itemLifetime: TimeInterval = 3600) { 21 | self.autoRemoveStaleItems = autoRemoveStaleItems 22 | self.itemLifetime = itemLifetime 23 | } 24 | 25 | /// Accesses the value associated with the given key for reading and writing. When you assign a value for a key and that key already exists, the cache overwrites the existing value. If the cache doesn’t contain the key, the key and value are added as a new key-value pair. If you assign `nil` as the value for the given key, the cache removes that key and its associated value. 26 | public final subscript(key: Key) -> Value? { 27 | get { 28 | value(for: key) 29 | } 30 | set { 31 | guard let value = newValue else { 32 | removeValue(forKey: key) 33 | return 34 | } 35 | 36 | updateValue(value, forKey: key) 37 | } 38 | } 39 | 40 | /// Whether the value associated with the `key` is stale. Returns `true` if the key is not in the cache. 41 | /// - Parameter key: The key to find in the cache. 42 | /// - Returns: Whether the value associated with the `key` is stale. 43 | final func isValueStale(forKey key: Key) -> Bool { 44 | guard let value = entry(for: key) else { 45 | return true 46 | } 47 | 48 | return Date() > value.expirationDate 49 | } 50 | 51 | // MARK: - Internal 52 | 53 | /// Accesses the value associated with the given key. 54 | /// - Parameter key: The key to find in the cache. 55 | /// - Returns: The value associated with `key` if `key` is in the cache; otherwise, `nil`. 56 | final func value(for key: Key) -> Value? { 57 | entry(for: key)?.value 58 | } 59 | 60 | /// Accesses the entry associated with the given key. 61 | /// - Parameter key: The key to find in the cache. 62 | /// - Returns: The entry associated with `key` if `key` is in the cache; otherwise, `nil`. 63 | final func entry(for key: Key) -> _Entry? { 64 | let key = _Key(key) 65 | return entry(for: key) 66 | } 67 | 68 | /// Accesses the entry associated with the given key. 69 | /// - Parameter key: The key to find in the cache. 70 | /// - Returns: The entry associated with `key` if `key` is in the cache; otherwise, `nil`. 71 | private final func entry(for key: _Key) -> _Entry? { 72 | _cache.object(forKey: key) 73 | } 74 | 75 | /// Updates the cached value for the given key, or adds the key-value pair to the cache if the key does not exist. 76 | /// - Parameters: 77 | /// - value: The new value to add to the cache. 78 | /// - key: The key to associate with `value`. If `key` already exists in the cache, `value` replaces the existing associated value. If `key` isn’t already a key of the cache, the (`key`, `value`) pair is added. 79 | /// - expirationDate: The date at which the entry will become stale, and be reloaded. 80 | final func updateValue(_ value: Value, forKey key: Key, expirationDate suggestedExpirationDate: Date? = nil) { 81 | let expirationDate = suggestedExpirationDate ?? Date().addingTimeInterval(itemLifetime) 82 | let _key = _Key(key) 83 | let entry = _Entry(key: key, value: value, expirationDate: expirationDate) 84 | updateEntry(entry, forKey: _key) 85 | } 86 | 87 | /// Updates the cached entry for the given key, or adds the entry to the cache if the key does not exist. 88 | /// - Parameters: 89 | /// - entry: The entry to add to the cache. 90 | /// - key: The key to associate with `entry`. If `key` already exists in the cache, `entry` replaces the existing entry. If `key` isn’t already a key of the cache, the entry is added. 91 | func updateEntry(_ entry: _Entry, forKey key: _Key) { 92 | _cache.setObject(entry, forKey: key) 93 | } 94 | 95 | /// Removes the given key and its associated value from the cache. 96 | /// - Parameter key: The key to remove along with its associated value. 97 | func removeValue(forKey key: Key) { 98 | let key = _Key(key) 99 | _cache.removeObject(forKey: key) 100 | } 101 | 102 | /// Removes the given key and its associated value from the cache. 103 | /// - Parameter key: The key to remove along with its associated value. 104 | // private final func removeValue(forKey key: _Key) { 105 | // 106 | // } 107 | 108 | /// Empties the cache. 109 | private final func removeAll() { 110 | _cache.removeAllObjects() 111 | } 112 | 113 | // MARK: - Wrapped Classes 114 | 115 | /// A wrapper for a `Key` 116 | final class _Key: NSObject { 117 | /// The wrapped `Key` 118 | let key: Key.ID 119 | 120 | /// Creates a new wrapper for a key. 121 | /// - Parameter key: The key to wrap 122 | init(_ key: Key) { 123 | self.key = key.id 124 | } 125 | 126 | override var hash: Int { 127 | key.hashValue 128 | } 129 | 130 | override func isEqual(_ object: Any?) -> Bool { 131 | guard let value = object as? Self else { return false } 132 | return value.key == key 133 | } 134 | } 135 | 136 | /// A wrapper for a key-value pair. 137 | final class _Entry { 138 | /// The key 139 | let key: Key 140 | 141 | /// The value 142 | let value: Value 143 | 144 | /// The expiration date of the object 145 | let expirationDate: Date 146 | 147 | /// Creates a new wrapper for a key-value pair. 148 | /// - Parameters: 149 | /// - key: The key 150 | /// - value: The value 151 | /// - expirationDate: The expiration date, at which point the data will be reloaded 152 | init(key: Key, value: Value, expirationDate: Date) { 153 | self.key = key 154 | self.value = value 155 | self.expirationDate = expirationDate 156 | } 157 | } 158 | } 159 | 160 | extension Cache._Entry: Codable where Key: Codable, Value: Codable {} 161 | -------------------------------------------------------------------------------- /docs/LoadableView/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - LoadableView 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Loadable​View 38 |

39 | 40 |
public protocol LoadableView: View
41 |
42 |

A View that loads content through a Loader.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | LoadableView 62 | 63 | 64 | LoadableView 65 | 66 | 67 | 68 | 69 | 70 | View 71 | 72 | View 73 | 74 | 75 | 76 | LoadableView->View 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |

Conforms To

87 |
88 |
View
89 |
90 |
91 | 92 | 93 | 94 |
95 |

Requirements

96 | 97 |
98 |

99 | key 100 |

101 |
var key: Loader.Key
102 |
103 |

The key identifying the object to load.

104 | 105 |
106 |
107 |
108 |

109 | key​Path 110 |

111 |
var keyPath: ValueKeyPath?
112 |
113 |

The key path of the value on the loaded object, defaults to nil.

114 | 115 |
116 |
117 |
118 |

119 | loader 120 |

121 |
var loader: Loader
122 |
123 |

The loader used to load content.

124 | 125 |
126 |
127 |
128 |

129 | placeholder() 130 |

131 |
func placeholder() -> Placeholder
132 |
133 |

The placeholder to show while loading.

134 | 135 |
136 |
137 |
138 |

139 | body(with:​) 140 |

141 |
func body(with value: Value) -> Content
142 |
143 |

Creates a view using loaded content.

144 | 145 |
146 |

Parameters

147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 162 | 163 | 164 |
valueValue

Loaded content.

161 |
165 |
166 |
167 |
168 |
169 | 170 |
171 |

172 | Generated on using swift-doc 1.0.0-beta.5. 173 |

174 |
175 | 176 | 177 | -------------------------------------------------------------------------------- /Sources/Loadability/UI/Load.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A view that loads content using a `Loader` before displaying the content in a custom `View`. 4 | public struct Load: View { 5 | 6 | /// The loader used to load content. 7 | @ObservedObject private var loader: Loader 8 | 9 | /// The key identifying the object to load. 10 | private let key: Loader.Key 11 | 12 | /// The key path of the value on the loaded object, defaults to `nil`. 13 | private var keyPath: KeyPath? 14 | 15 | /// Builds a view using a loaded object. 16 | private var contentBuilder: (Value) -> Content 17 | 18 | /// Builds a placeholder for the content. 19 | private var placeholderContentBuilder: () -> PlaceholderContent 20 | 21 | /// The loaded object, if loaded. 22 | private var value: Value? { 23 | guard let object = loader.object else { return nil } 24 | if let keyPath = keyPath { 25 | return object[keyPath: keyPath] 26 | } else { 27 | return object as? Value 28 | } 29 | } 30 | 31 | /// Whether an error alert is currently presented. 32 | @State private var isErrorAlertPresented = false 33 | 34 | public init(with loader: Loader, 35 | key: Loader.Key, 36 | objectKeyPath: KeyPath, 37 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content, 38 | @ViewBuilder placeholder placeholderContentBuilder: @escaping () -> PlaceholderContent) { 39 | 40 | self.loader = loader 41 | self.key = key 42 | self.keyPath = objectKeyPath 43 | self.placeholderContentBuilder = placeholderContentBuilder 44 | self.contentBuilder = contentBuilder 45 | } 46 | 47 | public init(with loader: Loader, 48 | key: Loader.Key, 49 | objectKeyPath: KeyPath, 50 | placeholderView: P.Type, 51 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content) 52 | where PlaceholderContent == P.Placeholder { 53 | 54 | self.loader = loader 55 | self.key = key 56 | self.keyPath = objectKeyPath 57 | self.contentBuilder = contentBuilder 58 | self.placeholderContentBuilder = { 59 | placeholderView.placeholder 60 | } 61 | } 62 | 63 | public var body: some View { 64 | bodyContent 65 | .task { 66 | await loader.load(key: key) 67 | } 68 | .onDisappear { 69 | Task { 70 | await loader.cancel() 71 | } 72 | } 73 | } 74 | 75 | /// The content shown. 76 | @ViewBuilder private var bodyContent: some View { 77 | if let value = value { 78 | contentBuilder(value) 79 | } else { 80 | placeholderContent 81 | } 82 | } 83 | 84 | /// The placeholder shown while loading. 85 | private var placeholderContent: some View { 86 | placeholderContentBuilder() 87 | } 88 | 89 | /// Presents an alert to the user if an error occurs. 90 | /// - Parameters: 91 | /// - message: Content to display in the alert. 92 | public func displayingErrors(message: ((Error) -> String)? = nil) -> some View { 93 | var error: _LocalizedError? 94 | if let loaderError = loader.error { 95 | error = _LocalizedError(loaderError) 96 | } 97 | 98 | return alert(isPresented: $isErrorAlertPresented, error: error) { _ in 99 | Button("OK") { 100 | loader.dismissError() 101 | } 102 | } message: { error in 103 | Text(message?(error) ?? error.userVisibleTitle) 104 | } 105 | } 106 | } 107 | 108 | // No Key 109 | public extension Load where Loader.Key == GenericKey { 110 | init(with loader: Loader, 111 | objectKeyPath: KeyPath, 112 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content, 113 | @ViewBuilder placeholder placeholderContentBuilder: @escaping () -> PlaceholderContent) { 114 | 115 | self.loader = loader 116 | self.key = .key 117 | self.keyPath = objectKeyPath 118 | self.placeholderContentBuilder = placeholderContentBuilder 119 | self.contentBuilder = contentBuilder 120 | } 121 | 122 | init(with loader: Loader, 123 | objectKeyPath: KeyPath, 124 | placeholderView: P.Type, 125 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content) where PlaceholderContent == P.Placeholder { 126 | 127 | self.loader = loader 128 | self.key = .key 129 | self.keyPath = objectKeyPath 130 | self.contentBuilder = contentBuilder 131 | self.placeholderContentBuilder = { 132 | placeholderView.placeholder 133 | } 134 | } 135 | } 136 | 137 | // No Key, Activity Indicator 138 | public extension Load where Loader.Key == GenericKey, PlaceholderContent == ProgressView { 139 | init(with loader: Loader, 140 | objectKeyPath: KeyPath, 141 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content) { 142 | 143 | self.loader = loader 144 | self.key = .key 145 | self.keyPath = objectKeyPath 146 | self.contentBuilder = contentBuilder 147 | self.placeholderContentBuilder = { 148 | ProgressView() 149 | } 150 | } 151 | } 152 | 153 | // No Key, Base Object 154 | public extension Load where Loader.Key == GenericKey, Loader.Object == Value { 155 | init(with loader: Loader, 156 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content, 157 | @ViewBuilder placeholder placeholderContentBuilder: @escaping () -> PlaceholderContent) { 158 | 159 | self.loader = loader 160 | self.key = .key 161 | self.contentBuilder = contentBuilder 162 | self.placeholderContentBuilder = placeholderContentBuilder 163 | } 164 | } 165 | 166 | // No Key, Base Object, Activity Indicator 167 | public extension Load where Loader.Key == GenericKey, Loader.Object == Value, PlaceholderContent == ProgressView { 168 | init(with loader: Loader, 169 | content contentBuilder: @escaping (Value) -> Content) { 170 | 171 | self.loader = loader 172 | self.key = .key 173 | self.contentBuilder = contentBuilder 174 | self.placeholderContentBuilder = { 175 | ProgressView() 176 | } 177 | } 178 | } 179 | 180 | // Base Object 181 | public extension Load where Value == Loader.Object { 182 | init(with loader: Loader, 183 | key: Loader.Key, 184 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content, 185 | @ViewBuilder placeholder placeholderContentBuilder: @escaping () -> PlaceholderContent) { 186 | 187 | self.loader = loader 188 | self.key = key 189 | self.contentBuilder = contentBuilder 190 | self.placeholderContentBuilder = placeholderContentBuilder 191 | } 192 | 193 | init(with loader: Loader, 194 | key: Loader.Key, 195 | placeholderView: P.Type, 196 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content) where PlaceholderContent == P.Placeholder { 197 | 198 | self.loader = loader 199 | self.key = key 200 | self.contentBuilder = contentBuilder 201 | self.placeholderContentBuilder = { 202 | placeholderView.placeholder 203 | } 204 | } 205 | } 206 | 207 | // Base Object, Activity Indicator 208 | public extension Load where Value == Loader.Object, PlaceholderContent == ProgressView { 209 | init(with loader: Loader, 210 | key: Loader.Key, 211 | content contentBuilder: @escaping (Value) -> Content) { 212 | 213 | self.loader = loader 214 | self.key = key 215 | self.contentBuilder = contentBuilder 216 | self.placeholderContentBuilder = { 217 | ProgressView() 218 | } 219 | } 220 | } 221 | 222 | // Activity Indicator 223 | public extension Load where PlaceholderContent == ProgressView { 224 | init(with loader: Loader, 225 | key: Loader.Key, 226 | objectKeyPath: KeyPath, 227 | @ViewBuilder content contentBuilder: @escaping (Value) -> Content) { 228 | 229 | self.loader = loader 230 | self.key = key 231 | self.keyPath = objectKeyPath 232 | self.contentBuilder = contentBuilder 233 | self.placeholderContentBuilder = { 234 | ProgressView() 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /docs/GenericKey/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - GenericKey 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Enumeration 37 | Generic​Key 38 |

39 | 40 |
public enum GenericKey
41 |
42 |

A generic key, used when the loadable value is not keyed by anything.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | GenericKey 62 | 63 | 64 | GenericKey 65 | 66 | 67 | 68 | 69 | 70 | Hashable 71 | 72 | Hashable 73 | 74 | 75 | 76 | GenericKey->Hashable 77 | 78 | 79 | 80 | 81 | 82 | Identifiable 83 | 84 | Identifiable 85 | 86 | 87 | 88 | GenericKey->Identifiable 89 | 90 | 91 | 92 | 93 | 94 | String 95 | 96 | String 97 | 98 | 99 | 100 | GenericKey->String 101 | 102 | 103 | 104 | 105 | 106 | Codable 107 | 108 | Codable 109 | 110 | 111 | 112 | GenericKey->Codable 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
122 |

Conforms To

123 |
124 |
Codable
125 |
Hashable
126 |
Identifiable
127 |
String
128 |
129 |
130 |
131 |

Enumeration Cases

132 | 133 |
134 |

135 | key 136 |

137 |
case key
138 |
139 |

A generic key, used when the loadable value is not keyed by anything.

140 | 141 |
142 |
143 |
144 |
145 |

Properties

146 | 147 |
148 |

149 | id 150 |

151 |
var id: String
152 |
153 |

The stable identity of the entity associated with this instance.

154 | 155 |
156 |
157 |
158 | 159 | 160 | 161 |
162 |
163 | 164 |
165 |

166 | Generated on using swift-doc 1.0.0-beta.5. 167 |

168 |
169 | 170 | 171 | -------------------------------------------------------------------------------- /docs/Load/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - Load 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Structure 37 | Load 38 |

39 | 40 |
public struct Load<Loader: SomeLoader, Value, Content: View, PlaceholderContent: View>: View
41 |
42 |

A view that loads content using a Loader before displaying the content in a custom View.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | Load 62 | 63 | 64 | Load 65 | 66 | 67 | 68 | 69 | 70 | View 71 | 72 | View 73 | 74 | 75 | 76 | Load->View 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 |

Conforms To

87 |
88 |
View
89 |
90 |
91 |
92 |

Initializers

93 | 94 |
95 |

96 | init(with:​key:​object​Key​Path:​content:​placeholder:​) 97 |

98 |
public init(with loader: Loader, key: Loader.Key, objectKeyPath: KeyPath<Loader.Object, Value?>, content contentBuilder: @escaping (Value) -> Content, placeholder placeholderContentBuilder: @escaping () -> PlaceholderContent)
99 |
100 |
101 |

102 | init(with:​key:​object​Key​Path:​placeholder​View:​content:​) 103 |

104 |
public init<P: HasPlaceholder>(with loader: Loader, key: Loader.Key, objectKeyPath: KeyPath<Loader.Object, Value?>, placeholderView: P.Type, content contentBuilder: @escaping (Value) -> Content) where PlaceholderContent == P.Placeholder
105 |
106 |
107 |
108 |

Properties

109 | 110 |
111 |

112 | body 113 |

114 |
var body: some View
115 |
116 |
117 |
118 |

Methods

119 | 120 |
121 |

122 | displaying​Errors(_:​) 123 |

124 |
public func displayingErrors(_ alertContent: ((Error) -> Alert)?) -> some View
125 |
126 |

Presents an alert to the user if an error occurs.

127 | 128 |
129 |

Parameters

130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 145 | 146 | 147 |
alert​Content((Error) -> Alert)?

Content to display in the alert.

144 |
148 |
149 |
150 | 151 | 152 | 153 |
154 |
155 | 156 |
157 |

158 | Generated on using swift-doc 1.0.0-beta.5. 159 |

160 |
161 | 162 | 163 | -------------------------------------------------------------------------------- /docs/IdentifiableError/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - IdentifiableError 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Structure 37 | Identifiable​Error 38 |

39 | 40 |
public struct IdentifiableError: Error, Equatable, Identifiable
41 |
42 |

A uniquely identifiable error.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | IdentifiableError 62 | 63 | 64 | IdentifiableError 65 | 66 | 67 | 68 | 69 | 70 | Equatable 71 | 72 | Equatable 73 | 74 | 75 | 76 | IdentifiableError->Equatable 77 | 78 | 79 | 80 | 81 | 82 | Identifiable 83 | 84 | Identifiable 85 | 86 | 87 | 88 | IdentifiableError->Identifiable 89 | 90 | 91 | 92 | 93 | 94 | Error 95 | 96 | Error 97 | 98 | 99 | 100 | IdentifiableError->Error 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 |

Conforms To

111 |
112 |
Equatable
113 |
Error
114 |
Identifiable
115 |
116 |
117 |
118 |

Initializers

119 | 120 |
121 |

122 | init?(_:​id:​) 123 |

124 |
public init?(_ underlyingError: Error?, id: String? = nil)
125 |
126 |

Creates a new IdentifiableError.

127 | 128 |
129 |

Parameters

130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 145 | 146 | 147 | 148 | 149 | 151 | 152 | 153 |
underlying​ErrorError?

The underlying Error.

144 |
idString?

An optional identifier for the error.

150 |
154 |
155 |
156 |
157 |

Properties

158 | 159 |
160 |

161 | id 162 |

163 |
var id: String
164 |
165 |

A unique identifier for the error.

166 | 167 |
168 |
169 |
170 |

171 | error 172 |

173 |
var error: Error
174 |
175 |

The underlying error object.

176 | 177 |
178 |
179 |
180 |
181 |

Methods

182 | 183 |
184 |

185 | ==(lhs:​rhs:​) 186 |

187 |
public static func ==(lhs: IdentifiableError, rhs: IdentifiableError) -> Bool
188 |
189 |

Checks whether two errors are the same

190 | 191 |
192 |

Parameters

193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 208 | 209 | 210 | 211 | 212 | 214 | 215 | 216 |
lhsIdentifiable​Error

An error

207 |
rhsIdentifiable​Error

Another error

213 |
217 |

Returns

218 |

Whether the errors are the same

219 | 220 |
221 |
222 | 223 | 224 | 225 |
226 |
227 | 228 |
229 |

230 | Generated on using swift-doc 1.0.0-beta.5. 231 |

232 |
233 | 234 | 235 | -------------------------------------------------------------------------------- /docs/SerializableCache/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - SerializableCache 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Class 37 | Serializable​Cache 38 |

39 | 40 |
public final class SerializableCache<Key: Codable & Hashable & Identifiable, Value: Codable>: Cache<Key, Value>, Codable
41 |
42 |

A mutable collection, with support for serialization and storage, used to store key-value pairs that are subject to eviction when resources are low.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | SerializableCache 62 | 63 | 64 | SerializableCache 65 | 66 | 67 | 68 | 69 | 70 | Cache<Key, Value> 71 | 72 | Cache<Key, Value> 73 | 74 | 75 | 76 | SerializableCache->Cache<Key, Value> 77 | 78 | 79 | 80 | 81 | 82 | Codable 83 | 84 | Codable 85 | 86 | 87 | 88 | SerializableCache->Codable 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |

Conforms To

99 |
100 |
Cache<Key, Value>
101 |
Codable
102 |
103 |
104 |
105 |

Initializers

106 | 107 |
108 |

109 | init(from:​) 110 |

111 |
public required convenience init(from decoder: Decoder) throws
112 |
113 |

Decodes an encoded SerializableCache.

114 | 115 |
116 |

Parameters

117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 132 | 133 | 134 |
decoderDecoder

The decoder.

131 |
135 |

Throws

136 |

When there is an error in decoding.

137 | 138 |
139 |
140 |
141 |

Methods

142 | 143 |
144 |

145 | encode(to:​) 146 |

147 |
public final func encode(to encoder: Encoder) throws
148 |
149 |

Encodes the cache and key-value pairs to disk.

150 | 151 |
152 |

Parameters

153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 168 | 169 | 170 |
encoderEncoder

The encoder.

167 |
171 |

Throws

172 |

When there is an error in encoding.

173 | 174 |
175 |
176 |

177 | save() 178 |

179 |
public final func save()
180 |
181 |

Encodes and saves the cache to disk.

182 | 183 |
184 |
185 |
186 |

187 | load(name:​should​Automatically​Remove​Stale​Items:​folder​URL:​) 188 |

189 |
public static func load(name: String, shouldAutomaticallyRemoveStaleItems: Bool = false, folderURL: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first) -> SerializableCache<Key, Value>
190 |
191 |

Loads a previously-serialized cache from disk.

192 | 193 |
194 |

Parameters

195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 210 | 211 | 212 | 213 | 214 | 216 | 217 | 218 | 219 | 220 | 222 | 223 | 224 |
nameString

The unique name of the cache.

209 |
should​Automatically​Remove​Stale​ItemsBool

Whether to automatically remove stale items, defaults to false.

215 |
folder​URLURL?

The folder in which to store the cache, defaults to the system cache directory.

221 |
225 |

Returns

226 |

The loaded cache.

227 | 228 |
229 |
230 | 231 | 232 | 233 |
234 |
235 | 236 |
237 |

238 | Generated on using swift-doc 1.0.0-beta.5. 239 |

240 |
241 | 242 | 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📩 Loadability 2 | ### Powerful, modern networking and caching with SwiftUI support 3 | #### v1.0.6 4 | 5 | **This package has changed since the documentation was updated to add support for newer language features (async/await). To support older versions of operating systems, see the [deprecated branch](https://github.com/julianschiavo/Loadability/tree/deprecated).** 6 | 7 |
8 | 9 | **Loadability** is an advanced networking and caching library for Swift that allows you to effortlessly implement a custom networking and/or caching stack in your app, with native support for `Combine` and `SwiftUI`. 10 | 11 | Loadability uses Apple's `Combine` framework to publish loaded objects and errors, and has built-in `SwiftUI` support to create views that load content and display placeholders while loading. 12 | 13 | ## Requirements 14 | 15 | **Loadability** supports **iOS 14+**, **macOS 11+**, and **watchOS 7+**. It does not have any dependencies. 16 | 17 | ## Installation 18 | 19 | You can use **Loadability** as a Swift Package, or add it manually to your project. 20 | 21 | ### Swift Package Manager (SPM) 22 | 23 | Swift Package Manager is a way to add dependencies to your app, and is natively integrated with Xcode. 24 | 25 | To add **Loadability** with SPM, click `File` ► `Swift Packages` ► `Add Package Dependency...`, then type in the URL to this Github repo. Xcode will then add the package to your project and perform all the necessary work to build it. 26 | 27 | ``` 28 | https://github.com/julianschiavo/Loadability 29 | ``` 30 | 31 | Alternatively, add the package to your `Package.swift` file. 32 | 33 | ```swift 34 | let package = Package( 35 | // ... 36 | dependencies: [ 37 | .package(url: "https://github.com/julianschiavo/Loadability.git", from: "1.0.0") 38 | ], 39 | // ... 40 | ) 41 | ``` 42 | 43 | *See [SPM documentation](https://github.com/apple/swift-package-manager/tree/master/Documentation) to learn more.* 44 | 45 | ### Manually 46 | 47 | If you prefer not to use SPM, you can also add **Loadability** as a normal framework by building the library from this repository. (See other sources for instructions on doing this.) 48 | 49 | ## Usage 50 | 51 | Loadability declares basic protocols and classes that you extend in your app to build loaders and caches. It also has an (optional) `SwiftUI` integration (see [below](#swiftui-integration)) for views to load their data and show placeholders while loading. 52 | 53 | The library has [extensive documentation](https://julianschiavo.github.io/Loadability/). If you are looking to gain an advanced understanding of the library, or to implement something not discussed below, please consult the inline documentation before filing an issue. 54 | 55 | *Note that the code snippets in this section have some code omitted for brevity; see the [Examples](#examples) section for complete code examples.* 56 | 57 | ### Networking 58 | 59 | Loadability networking in centered around the concept of loaders. A `Loader` loads data from some source and publishes it as an `ObservableObject`, which you can observe manually or integrate through the [`SwiftUI` support](#swiftui-integration). 60 | 61 | To create a loader, you can create a class that conforms to the abstract `Loader` protocol, or conform to one of the sub-protocols that implement default behaviour, such as `SimpleNetworkLoader` and `CachedLoader`. 62 | 63 | Loaders have some minimal shared requirements, including published properties for the object and a possible error while loading, and methods that load the data. You can extend loaders to conform to your own custom requirements. 64 | 65 | This is an example of a loader. Each loader has an associated type for the object type that is loaded, and the key that is used by the loader to identify the object to load. 66 | ```swift 67 | class YourLoader: Loader { 68 | @Published var object: YourObject? 69 | @Published var error: IdentifiableError? 70 | 71 | func createRequest(for key: YourKey) -> URLRequest { 72 | URLRequest(url: /* some URL */) 73 | } 74 | 75 | func createPublisher(key: YourKey) -> AnyPublisher? { 76 | let request = createRequest(for: key) 77 | return URLSession.shared 78 | .dataTaskPublisher(for: request) 79 | .retry(3) 80 | .tryMap { data, response in 81 | try self.decode(data, key: key) 82 | } 83 | .eraseToAnyPublisher() 84 | } 85 | 86 | private func decode(_ data: Data, key: YourKey) throws -> YourObject { 87 | /* decode the data, for example, using a JSONDecoder */ 88 | } 89 | } 90 | ``` 91 | 92 | As noted previously, loaders automatically conform to `ObservableObject`; you can observe the published properties yourself or use the [SwiftUI integration](#swiftui-integration). 93 | 94 | #### Specific Loaders 95 | 96 | Loadability has a few specific built-in loaders that implement common requirements, reducing the code required for some implementations. You can subclass a specific built-in loader instead of the general `Loader` protocol if it meets your needs. 97 | 98 | ##### `SimpleNetworkLoader` 99 | 100 | `SimpleNetworkLoader` implements a basic networking loader, with optional predefined `Codable` decoding support. You can use this loader for instances where you want a default network loader. 101 | 102 | An example of this loader is shown here; as you can see, it is simpler to implement than a full `Loader` subclass, handling the networking code for you using only a `URLRequest` you create. You can create a custom decoding implementation to decode the `Data` received from the network request, or, if your object type conforms to `Codable`, take advantage of the predefined decoding implementation which uses a `JSONDecoder`. 103 | ```swift 104 | class YourLoader: SimpleNetworkLoader { 105 | @Published var object: YourObject? 106 | @Published var error: IdentifiableError? 107 | 108 | func createRequest(for key: YourKey) -> URLRequest { 109 | URLRequest(url: /* some URL */) 110 | } 111 | 112 | func decode(_ data: Data, key: YourKey) throws -> YourObject { 113 | /* optional custom decoding implementation */ 114 | } 115 | } 116 | ``` 117 | 118 | ##### `CachedLoader` 119 | 120 | `CachedLoader` uses the caching system discussed below to implement a loader that caches loaded data. You are encouraged to use this loader when you want to cache your data instead of implementing your own caching loader, as it handles tasks such as pre-loading cached data and updating the cache asynchronously after loads. 121 | 122 | This is an example of a `CachedLoader` subclass (see the [Caching](#caching) section to learn how to create a `Cache`). 123 | ```swift 124 | class YourLoader: CachedLoader { 125 | @Published var object: YourObject? 126 | @Published var error: IdentifiableError? 127 | 128 | var cache = YourCache.self /* a SharedCache or SharedSerializableCache type */ 129 | 130 | func createRequest(for key: YourKey) -> URLRequest { 131 | URLRequest(url: /* some URL */) 132 | } 133 | 134 | func createPublisher(key: YourKey) -> AnyPublisher? { 135 | /* see the Loader example above */ 136 | } 137 | 138 | private func decode(_ data: Data, key: YourKey) throws -> YourObject { 139 | /* see the Loader example above */ 140 | } 141 | } 142 | ``` 143 | The code is similar to the `Loader` snippet, as the caching is handled for you internally by the library; you simply need to provide a `Cache` which will be used. 144 | 145 | ### Caching 146 | 147 | In addition to networking, Loadability has support for caching as a Swift implementation built on top of Apple's `NSCache` with a number of additional features including support for serialization (saving caches to disk for reuse). 148 | 149 | The basic `Cache` class is a basic wrapper for `NSCache`, supporting the same features as the Objective-C version. Create an instance of `Cache`, and use subscripts to access, modify, or delete values, as such: 150 | 151 | ```swift 152 | let cache = Cache() 153 | cache[key] = YourObject() // Add value 154 | cache[key] = YourObject(someParameter: true) // Modify value 155 | let object = cache[key] // YourObject?, Access cached value 156 | cache[key] = nil // Remove value 157 | ``` 158 | 159 | `SerializableCache` subclasses the base `Cache` class to add support for saving caches to disk, which allows you to reload an old cache on app restart or similar events. Each `SerializableCache` has a name, which must be unique per-app and the same on each initialization to identify the cache on disk. Note that the key and object types of serializable caches must conform to `Codable`. 160 | 161 | Instead of creating an instance directly, you use the class method `load(name:)`, which attempts to load an existing cache from disk, falling back to creating a new cache if an existing one is not valid or does not exist, such as on the first initialization. Future calls of the method (such as on subsequent app restarts) load and decode the previously saved cache from disk. 162 | 163 | ```swift 164 | let sCache = SerializableCache.load(name: "Name") 165 | ``` 166 | 167 | The cache values are accessed, modified, and deleted in the same way as with the normal `Cache`. The `save()` method encodes and writes the cache to disk; this is called automatically on writes and modifications, so it is not necessary to call this yourself in normal implementations. 168 | 169 | #### Shared Caches 170 | 171 | Creating new cache instances, especially of `SerializableCache`, is costly and energy intensive. Additionally, creating multiple cache instances will result in cached data not being shared around your app, causing unnecessary network requests. 172 | 173 | Loadability has support for "shared" caches, which are a singleton-like pattern that allows you to use the same cache throughout your app for each type of cache you have. 174 | 175 | You create a shared cache by subclassing either `SharedCache` or `SharedSerializableCache`, depending on what features you want. 176 | 177 | ```swift 178 | struct YourCache: SharedSerializableCache { 179 | typealias Key = YourKey 180 | typealias Value = YourValue 181 | static let shared = SerializableCache.load(name: "Name") 182 | } 183 | ``` 184 | 185 | Instead of creating multiple cache instances, use the `shared` instance every time you need that cache. 186 | 187 | #### Using Caches with Loaders 188 | 189 | As discussed above in the [CachedLoader](#cachedloader) section under Loaders, Loadability has an integration between caches and loaders. Although you can use cache instances directly with custom loaders, it is *strongly* recommended to use a shared cache and `CachedLoader`, as loaders are, by definition, created for each object that is loaded. 190 | 191 | Create a shared cache following the code in the previous section on shared caches, and set the `cache` variable in your loader to the class's type (e.g. `YourCache.self`). 192 | 193 | ### SwiftUI Integration 194 | 195 | Loadability has native support for SwiftUI; loading data in views is made extremely simple. After creating any type of Loader, make your views loadable by conforming to `LoadableView`. The protocol handles loading the data and displays your placeholder content while loading, then passes a `non-nil` object to a new `body(with:)` method you implement to create your body content. 196 | 197 | You only need to implement some basic requirements; creating a loader type, passing the key, and implementing the placeholder and body methods. 198 | 199 | The following shows an example of creating a loadable view. Note a few key points: 200 | - You must ensure that your loader variable is annotated by `@StateObject` or the loader's publisher will *not* update the view as expected 201 | - Because of Swift's behaviour with protocol conformance, your views must implement the `body` variable directly, passing in the `loaderView` generated by `LoadableView` 202 | 203 | ```swift 204 | struct YourView: View, LoadableView { 205 | @StateObject var loader = YourLoader() // must be annotated by @StateObject 206 | 207 | var body: some View { 208 | loaderView // required 209 | } 210 | 211 | func body(with object: YourObject) -> some View { 212 | /* create a body with the object */ 213 | } 214 | 215 | func placeholder() -> some View { 216 | ProgressView("Loading...") 217 | } 218 | } 219 | ``` 220 | 221 | Loadability also exposes `Load`, which is a view type used internally by `LoadableView`. If you require a custom implementation, use this view to gain more control over the loading behaviour. However, try `LoadableView` first. 222 | 223 | ## Examples 224 | 225 | The [Loadability Examples](https://github.com/julianschiavo/loadability-examples) repository contains real-world app examples demonstrating how to use the library. The `COVID-19` app project shows an implementation of Loadability with support for caching, and uses the cached and network loaders discussed above. 226 | 227 | ## Contributing 228 | 229 | Contributions and pull requests are welcomed by anyone! If you find an issue with **Loadability**, file a Github Issue, or, if you know how to fix it, submit a pull request. 230 | 231 | Please review our [Code of Conduct](CODE_OF_CONDUCT.md) and [Contribution Guidelines](CONTRIBUTING.md) before making a contribution. 232 | 233 | ## Credits & Sponsoring 234 | 235 | **Loadability** was originally created by [Julian Schiavo](https://twitter.com/julianschiavo) in his spare time, and made available under the [MIT License](LICENSE). If you find the library useful, please consider [sponsoring me on Github](https://github.com/sponsors/julianschiavo), which contributes to development and learning resources, and allows me to keep making cool stuff like this! 236 | 237 | Loadability is inspired by (and uses code by permission) from John Sundell's in depth blog posts on [Handling loading states within SwiftUI views](https://www.swiftbysundell.com/articles/handling-loading-states-in-swiftui/) and [Caching in Swift](https://swiftbysundell.com/articles/caching-in-swift/), so thank you to John for the inspiration and examples. If you want to gain a better understanding of loading or caching in Swift, I strongly recommend you check out the articles linked above, in addition to his many other articles! 238 | 239 | ## License 240 | 241 | Available under the MIT License. See the [License](LICENSE) for more info. 242 | -------------------------------------------------------------------------------- /docs/Loader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Loadability - Loader 7 | 8 | 9 | 10 |
11 | 12 | 13 | Loadability 14 | 15 | Documentation 16 | 17 | Beta 18 |
19 | 20 | 25 | 26 | 32 | 33 |
34 |
35 |

36 | Protocol 37 | Loader 38 |

39 | 40 |
public protocol Loader: ObservableObject, ThrowsErrors
41 |
42 |

A type that can load data from a source and throw errors.

43 | 44 |
45 |
46 | 47 |
48 | 49 | 51 | 53 | 54 | 56 | 57 | %3 58 | 59 | 60 | 61 | Loader 62 | 63 | 64 | Loader 65 | 66 | 67 | 68 | 69 | 70 | ThrowsErrors 71 | 72 | 73 | ThrowsErrors 74 | 75 | 76 | 77 | 78 | 79 | Loader->ThrowsErrors 80 | 81 | 82 | 83 | 84 | 85 | ObservableObject 86 | 87 | ObservableObject 88 | 89 | 90 | 91 | Loader->ObservableObject 92 | 93 | 94 | 95 | 96 | 97 | SimpleNetworkLoader 98 | 99 | 100 | SimpleNetworkLoader 101 | 102 | 103 | 104 | 105 | 106 | SimpleNetworkLoader->Loader 107 | 108 | 109 | 110 | 111 | 112 | CachedLoader 113 | 114 | 115 | CachedLoader 116 | 117 | 118 | 119 | 120 | 121 | CachedLoader->Loader 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 |

Conforms To

132 |
133 |
ThrowsErrors
134 |

A type that can throw errors that should be shown to the user.

135 |
136 |
ObservableObject
137 |
138 |

Types Conforming to Loader

139 |
140 |
CachedLoader
141 |

A type that can load data from a source with caching.

142 |
143 |
SimpleNetworkLoader
144 |

A type that can load data from over the network and throw errors.

145 |
146 |
147 |
148 | 149 | 150 | 151 |
152 |

Requirements

153 | 154 |
155 |

156 | object 157 |

158 |
var object: Object?
159 |
160 |

A publisher for the object that is loaded.

161 | 162 |
163 |
164 |
165 |

166 | cancellable 167 |

168 |
var cancellable: AnyCancellable?
169 |
170 |

An ongoing request.

171 | 172 |
173 |
174 |
175 |

176 | load(key:​) 177 |

178 |
func load(key: Key)
179 |
180 |

Begins loading the object.

181 | 182 |
183 |

Parameters

184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 199 | 200 | 201 |
keyKey

The key identifying the object to load.

198 |
202 |
203 |
204 |

205 | create​Publisher(key:​) 206 |

207 |
func createPublisher(key: Key) -> AnyPublisher<Object, Error>?
208 |
209 |

Creates a publisher that loads the object.

210 | 211 |
212 |

Parameters

213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 228 | 229 | 230 |
keyKey

The key identifying the object to load.

227 |
231 |
232 |
233 |

234 | load​Data(key:​) 235 |

236 |
func loadData(key: Key)
237 |
238 |

Starts loading the object's data.

239 | 240 |
241 |

Parameters

242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 257 | 258 | 259 |
keyKey

The key identifying the object to load.

256 |
260 |
261 |
262 |

263 | load​Completed(key:​object:​) 264 |

265 |
func loadCompleted(key: Key, object: Object)
266 |
267 |

Called when the object has been loaded successfully.

268 | 269 |
270 |

Parameters

271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 286 | 287 | 288 | 289 | 290 | 292 | 293 | 294 |
keyKey

The key identifying the object that was loaded.

285 |
objectObject

The loaded object.

291 |
295 |
296 |
297 |

298 | cancel() 299 |

300 |
func cancel()
301 |
302 |
303 |
304 |
305 | 306 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /docs/all.css: -------------------------------------------------------------------------------- 1 | :root{--system-red:#ff3b30;--system-orange:#ff9500;--system-yellow:#fc0;--system-green:#34c759;--system-teal:#5ac8fa;--system-blue:#007aff;--system-indigo:#5856d6;--system-purple:#af52de;--system-pink:#ff2d55;--system-gray:#8e8e93;--system-gray2:#aeaeb2;--system-gray3:#c7c7cc;--system-gray4:#d1d1d6;--system-gray5:#e5e5ea;--system-gray6:#f2f2f7;--label:#000;--secondary-label:#3c3c43;--tertiary-label:#48484a;--quaternary-label:#636366;--placeholder-text:#8e8e93;--link:#007aff;--separator:#e5e5ea;--opaque-separator:#c6c6c8;--system-fill:#787880;--secondary-system-fill:#787880;--tertiary-system-fill:#767680;--quaternary-system-fill:#747480;--system-background:#fff;--secondary-system-background:#f2f2f7;--tertiary-system-background:#fff;--system-grouped-background:#f2f2f7;--secondary-system-grouped-background:#fff;--tertiary-system-grouped-background:#f2f2f7}@supports (color:color(display-p3 1 1 1)){:root{--system-red:color(display-p3 1 0.2314 0.1882);--system-orange:color(display-p3 1 0.5843 0);--system-yellow:color(display-p3 1 0.8 0);--system-green:color(display-p3 0.2039 0.7804 0.349);--system-teal:color(display-p3 0.3529 0.7843 0.9804);--system-blue:color(display-p3 0 0.4784 1);--system-indigo:color(display-p3 0.3451 0.3373 0.8392);--system-purple:color(display-p3 0.6863 0.3216 0.8706);--system-pink:color(display-p3 1 0.1765 0.3333);--system-gray:color(display-p3 0.5569 0.5569 0.5765);--system-gray2:color(display-p3 0.6824 0.6824 0.698);--system-gray3:color(display-p3 0.7804 0.7804 0.8);--system-gray4:color(display-p3 0.8196 0.8196 0.8392);--system-gray5:color(display-p3 0.898 0.898 0.9176);--system-gray6:color(display-p3 0.949 0.949 0.9686);--label:color(display-p3 0 0 0);--secondary-label:color(display-p3 0.2353 0.2353 0.2627);--tertiary-label:color(display-p3 0.2823 0.2823 0.2901);--quaternary-label:color(display-p3 0.4627 0.4627 0.5019);--placeholder-text:color(display-p3 0.5568 0.5568 0.5764);--link:color(display-p3 0 0.4784 1);--separator:color(display-p3 0.898 0.898 0.9176);--opaque-separator:color(display-p3 0.7765 0.7765 0.7843);--system-fill:color(display-p3 0.4706 0.4706 0.502);--secondary-system-fill:color(display-p3 0.4706 0.4706 0.502);--tertiary-system-fill:color(display-p3 0.4627 0.4627 0.502);--quaternary-system-fill:color(display-p3 0.4549 0.4549 0.502);--system-background:color(display-p3 1 1 1);--secondary-system-background:color(display-p3 0.949 0.949 0.9686);--tertiary-system-background:color(display-p3 1 1 1);--system-grouped-background:color(display-p3 0.949 0.949 0.9686);--secondary-system-grouped-background:color(display-p3 1 1 1);--tertiary-system-grouped-background:color(display-p3 0.949 0.949 0.9686)}}:root{--large-title:600 32pt/39pt sans-serif;--title-1:600 26pt/32pt sans-serif;--title-2:600 20pt/25pt sans-serif;--title-3:500 18pt/23pt sans-serif;--headline:500 15pt/20pt sans-serif;--body:300 15pt/20pt sans-serif;--callout:300 14pt/19pt sans-serif;--subhead:300 13pt/18pt sans-serif;--footnote:300 12pt/16pt sans-serif;--caption-1:300 11pt/13pt sans-serif;--caption-2:300 11pt/13pt sans-serif;--icon-associatedtype:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M42 81.71V31.3H24.47v-13h51.06v13H58v50.41z' fill='%23fff'/%3E%3C/svg%3E");--icon-case:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32H64.19C63.4 35 58.09 30.11 51 30.11c-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25C32 82.81 20.21 70.72 20.21 50z' fill='%23fff'/%3E%3C/svg%3E");--icon-class:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%239b98e6' height='90' rx='8' stroke='%235856d6' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='m20.21 50c0-20.7 11.9-32.79 30.8-32.79 16 0 28.21 10.33 28.7 25.32h-15.52c-.79-7.53-6.1-12.42-13.19-12.42-8.79 0-14.37 7.52-14.37 19.82s5.54 20 14.41 20c7.08 0 12.22-4.66 13.23-12.09h15.52c-.74 15.07-12.43 25-28.78 25-19.01-.03-30.8-12.12-30.8-32.84z' fill='%23fff'/%3E%3C/svg%3E");--icon-enumeration:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5.17' y='5'/%3E%3Cpath d='M71.9 81.71H28.43V18.29H71.9v13H44.56v12.62h25.71v11.87H44.56V68.7H71.9z' fill='%23fff'/%3E%3C/svg%3E");--icon-extension:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23eca95b' height='90' rx='8' stroke='%23e89234' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M54.43 81.93H20.51V18.07h33.92v12.26H32.61v13.8h20.45v11.32H32.61v14.22h21.82zM68.74 74.58h-.27l-2.78 7.35h-7.28L64 69.32l-6-12.54h8l2.74 7.3h.27l2.76-7.3h7.64l-6.14 12.54 5.89 12.61h-7.64z'/%3E%3C/g%3E%3C/svg%3E");--icon-function:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M24.25 75.66A5.47 5.47 0 0130 69.93c1.55 0 3.55.41 6.46.41 3.19 0 4.78-1.55 5.46-6.65l1.5-10.14h-9.34a6 6 0 110-12h11.1l1.09-7.27C47.82 23.39 54.28 17.7 64 17.7c6.69 0 11.74 1.77 11.74 6.64A5.47 5.47 0 0170 30.07c-1.55 0-3.55-.41-6.46-.41-3.14 0-4.73 1.51-5.46 6.65l-.78 5.27h11.44a6 6 0 11.05 12H55.6l-1.78 12.11C52.23 76.61 45.72 82.3 36 82.3c-6.7 0-11.75-1.77-11.75-6.64z' fill='%23fff'/%3E%3C/svg%3E");--icon-method:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%235a98f8' height='90' rx='8' stroke='%232974ed' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M70.61 81.71v-39.6h-.31l-15.69 39.6h-9.22l-15.65-39.6h-.35v39.6H15.2V18.29h18.63l16 41.44h.36l16-41.44H84.8v63.42z' fill='%23fff'/%3E%3C/svg%3E");--icon-property:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2389c5e6' height='90' rx='8' stroke='%236bb7e1' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M52.31 18.29c13.62 0 22.85 8.84 22.85 22.46s-9.71 22.37-23.82 22.37H41v18.59H24.84V18.29zM41 51h7c6.85 0 10.89-3.56 10.89-10.2S54.81 30.64 48 30.64h-7z' fill='%23fff'/%3E%3C/svg%3E");--icon-protocol:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23ff6682' height='90' rx='8' stroke='%23ff2d55' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cg fill='%23fff'%3E%3Cpath d='M46.28 18.29c11.84 0 20 8.66 20 21.71s-8.44 21.71-20.6 21.71H34.87v20H22.78V18.29zM34.87 51.34H43c6.93 0 11-4 11-11.29S50 28.8 43.07 28.8h-8.2zM62 57.45h8v4.77h.16c.84-3.45 2.54-5.12 5.17-5.12a5.06 5.06 0 011.92.35V65a5.69 5.69 0 00-2.39-.51c-3.08 0-4.66 1.74-4.66 5.12v12.1H62z'/%3E%3C/g%3E%3C/svg%3E");--icon-structure:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%23b57edf' height='90' rx='8' stroke='%239454c2' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M38.38 63c.74 4.53 5.62 7.16 11.82 7.16s10.37-2.81 10.37-6.68c0-3.51-2.73-5.31-10.24-6.76l-6.5-1.23C31.17 53.14 24.62 47 24.62 37.28c0-12.22 10.59-20.09 25.18-20.09 16 0 25.36 7.83 25.53 19.91h-15c-.26-4.57-4.57-7.29-10.42-7.29s-9.31 2.63-9.31 6.37c0 3.34 2.9 5.18 9.8 6.5l6.5 1.23C70.46 46.51 76.61 52 76.61 62c0 12.74-10 20.83-26.72 20.83-15.82 0-26.28-7.3-26.5-19.78z' fill='%23fff'/%3E%3C/svg%3E");--icon-typealias:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M42 81.71V31.3H24.47v-13h51.06v13H58v50.41z' fill='%23fff'/%3E%3C/svg%3E");--icon-variable:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%237ac673' height='90' rx='8' stroke='%235bb74f' stroke-miterlimit='10' stroke-width='4' width='90' x='5' y='5'/%3E%3Cpath d='M39.85 81.71L19.63 18.29H38l12.18 47.64h.35L62.7 18.29h17.67L60.15 81.71z' fill='%23fff'/%3E%3C/svg%3E")}body,button,input,select,textarea{-moz-font-feature-settings:"kern";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;direction:ltr;font-synthesis:none;text-align:left}h1:first-of-type,h2:first-of-type,h3:first-of-type,h4:first-of-type,h5:first-of-type,h6:first-of-type{margin-top:0}h1 code,h2 code,h3 code,h4 code,h5 code,h6 code{font-family:inherit;font-weight:inherit}h1 img,h2 img,h3 img,h4 img,h5 img,h6 img{margin:0 .5em .2em 0;vertical-align:middle;display:inline-block}h1+*,h2+*,h3+*,h4+*,h5+*,h6+*{margin-top:.8em}img+h1{margin-top:.5em}img+h1,img+h2,img+h3,img+h4,img+h5,img+h6{margin-top:.3em}:is(h1,h2,h3,h4,h5,h6)+:is(h1,h2,h3,h4,h5,h6){margin-top:.4em}:matches(h1,h2,h3,h4,h5,h6)+:matches(h1,h2,h3,h4,h5,h6){margin-top:.4em}:is(p,ul,ol)+:is(h1,h2,h3,h4,h5,h6){margin-top:1.6em}:matches(p,ul,ol)+:matches(h1,h2,h3,h4,h5,h6){margin-top:1.6em}:is(p,ul,ol)+*{margin-top:.8em}:matches(p,ul,ol)+*{margin-top:.8em}ol,ul{margin-left:1.17647em}:matches(ul,ol) :matches(ul,ol){margin-bottom:0;margin-top:0}nav h2{color:#3c3c43;color:var(--secondary-label);font-size:1rem;font-feature-settings:"smcp";font-variant:small-caps;font-weight:600;text-transform:uppercase}nav ol,nav ul{margin:0;list-style:none}nav li li{font-size:smaller}a:link,a:visited{text-decoration:none}a:hover{text-decoration:underline}a:active{text-decoration:none}b,strong{font-weight:600}.discussion,.summary{font:300 14pt/19pt sans-serif;font:var(--callout)}article>.discussion{margin-bottom:2em}.discussion .highlight{background:transparent;border:1px solid #e5e5ea;border:1px solid var(--separator);font:300 11pt/13pt sans-serif;font:var(--caption-1);padding:1em;text-indent:0}cite,dfn,em,i{font-style:italic}:matches(h1,h2,h3) sup{font-size:.4em}sup a{color:inherit;vertical-align:inherit}sup a:hover{color:#007aff;color:var(--link);text-decoration:none}sub{line-height:1}abbr{border:0}:lang(ja),:lang(ko),:lang(th),:lang(zh){font-style:normal}:lang(ko){word-break:keep-all}form fieldset{margin:1em auto;max-width:450px;width:95%}form label{display:block;font-size:1em;font-weight:400;line-height:1.5em;margin-bottom:14px;position:relative;width:100%}input[type=email],input[type=number],input[type=password],input[type=tel],input[type=text],input[type=url],textarea{border-radius:4px;border:1px solid #e5e5ea;border:1px solid var(--separator);color:#333;font-family:inherit;font-size:100%;font-weight:400;height:34px;margin:0;padding:0 1em;position:relative;vertical-align:top;width:100%;z-index:1}input[type=email],input [type=email]:focus,input[type=number],input [type=number]:focus,input[type=password],input [type=password]:focus,input[type=tel],input [type=tel]:focus,input[type=text],input [type=text]:focus,input[type=url],input [type=url]:focus,textarea,textarea:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=email]:focus,input[type=number]:focus,input[type=password]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=url]:focus,textarea:focus{border-color:#08c;box-shadow:0 0 0 3px rgba(0,136,204,.3);outline:0;z-index:9}input[type=email]:-moz-read-only,input[type=number]:-moz-read-only,input[type=password]:-moz-read-only,input[type=tel]:-moz-read-only,input[type=text]:-moz-read-only,input[type=url]:-moz-read-only,textarea:-moz-read-only{background:none;border:none;box-shadow:none;padding-left:0}input[type=email]:read-only,input[type=number]:read-only,input[type=password]:read-only,input[type=tel]:read-only,input[type=text]:read-only,input[type=url]:read-only,textarea:read-only{background:none;border:none;box-shadow:none;padding-left:0}::-moz-placeholder{color:#8e8e93;color:var(--placeholder-text)}:-ms-input-placeholder{color:#8e8e93;color:var(--placeholder-text)}::placeholder{color:#8e8e93;color:var(--placeholder-text)}textarea{-webkit-overflow-scrolling:touch;line-height:1.4737;min-height:134px;overflow-y:auto;resize:vertical;transform:translateZ(0)}textarea,textarea:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}select{background:transparent;border-radius:4px;border:none;cursor:pointer;font-family:inherit;font-size:1em;height:34px;margin:0;padding:0 1em;width:100%}select,select:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}select:focus{border-color:#08c;box-shadow:0 0 0 3px rgba(0,136,204,.3);outline:0;z-index:9}input[type=file]{background:#fafafa;border-radius:4px;color:#333;cursor:pointer;font-family:inherit;font-size:100%;height:34px;margin:0;padding:6px 1em;position:relative;vertical-align:top;width:100%;z-index:1}input[type=file]:focus{border-color:#08c;outline:0;box-shadow:0 0 0 3px rgba(0,136,204,.3);z-index:9}button,button:focus,input[type=file]:focus,input[type=file]:focus:focus,input[type=reset],input[type=reset]:focus,input[type=submit],input[type=submit]:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}:matches(button,input[type=reset],input[type=submit]){background-color:#e3e3e3;background:linear-gradient(#fff,#e3e3e3);border-color:#d6d6d6;color:#0070c9}:matches(button,input[type=reset],input[type=submit]):hover{background-color:#eee;background:linear-gradient(#fff,#eee);border-color:#d9d9d9}:matches(button,input[type=reset],input[type=submit]):active{background-color:#dcdcdc;background:linear-gradient(#f7f7f7,#dcdcdc);border-color:#d0d0d0}:matches(button,input[type=reset],input[type=submit]):disabled{background-color:#e3e3e3;background:linear-gradient(#fff,#e3e3e3);border-color:#d6d6d6;color:#0070c9}body{background:#f2f2f7;background:var(--system-grouped-background);color:#000;color:var(--label);font-family:ui-system,-apple-system,BlinkMacSystemFont,sans-serif;font:300 15pt/20pt sans-serif;font:var(--body)}h1{font:600 32pt/39pt sans-serif;font:var(--large-title)}h2{font:600 20pt/25pt sans-serif;font:var(--title-2)}h3{font:500 18pt/23pt sans-serif;font:var(--title-3)}h4,h5,h6{font:500 15pt/20pt sans-serif;font:var(--headline)}a{color:#007aff;color:var(--link)}label{font:300 14pt/19pt sans-serif;font:var(--callout)}input,label{display:block}input{margin-bottom:1em}hr{border:none;border-top:1px solid #e5e5ea;border-top:1px solid var(--separator);margin:1em 0}table{width:100%;font:300 11pt/13pt sans-serif;font:var(--caption-1);caption-side:bottom;margin-bottom:2em}td,th{padding:0 1em}th{font-weight:600;text-align:left}thead th{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator)}tr:last-of-type td,tr:last-of-type th{border-bottom:none}td,th{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);color:#3c3c43;color:var(--secondary-label)}caption{color:#48484a;color:var(--tertiary-label);font:300 11pt/13pt sans-serif;font:var(--caption-2);margin-top:2em;text-align:left}.graph text,code{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-weight:300}.graph>polygon{display:none}.graph text{fill:currentColor!important}.graph ellipse,.graph path,.graph polygon,.graph rect{stroke:currentColor!important}body{width:90vw;max-width:1280px;margin:1em auto}body>header{font:600 26pt/32pt sans-serif;font:var(--title-1);padding:.5em 0}body>header a{color:#000;color:var(--label)}body>header span{font-weight:400}body>header sup{text-transform:uppercase;font-size:small;font-weight:300;letter-spacing:.1ch}body>footer,body>header sup{color:#3c3c43;color:var(--secondary-label)}body>footer{clear:both;padding:1em 0;font:300 11pt/13pt sans-serif;font:var(--caption-1)}@media screen and (max-width:768px){body{width:96vw;max-width:100%}body>header{font:500 18pt/23pt sans-serif;font:var(--title-3);text-align:left;padding:1em 0}body>nav{display:none}body>main{padding:0 1em}}@media screen and (max-width:768px){#relationships figure{display:none}section>[role=article][class] pre{margin-left:-1em;margin-right:-1em}section>[role=article][class] div{margin-left:-2em}}main,nav{overflow-x:auto}main{background:#fff;background:var(--system-background);border-radius:8px;padding:0 2em}main section{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);margin-bottom:2em;padding-bottom:1em}main section:last-of-type{border-bottom:none;margin-bottom:0}nav{float:right;margin-left:1em;max-height:100vh;overflow:auto;padding:0 1em 3em;position:sticky;top:1em;width:20vw}nav a{color:#3c3c43;color:var(--secondary-label)}nav ul a{color:#48484a;color:var(--tertiary-label)}nav ol,nav ul{padding:0}nav ul{font:300 14pt/19pt sans-serif;font:var(--callout);margin-bottom:1em}nav ol>li>a{display:block;font-size:smaller;font:500 15pt/20pt sans-serif;font:var(--headline);margin:.5em 0}nav li{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}blockquote{--link:var(--secondary-label);border-left:4px solid #e5e5ea;border-left:4px solid var(--separator);color:#3c3c43;color:var(--secondary-label);font-size:smaller;margin-left:0;padding-left:2em}blockquote a{text-decoration:underline}article{padding:2em 0 1em}article>.summary{border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);margin-bottom:2em;padding-bottom:1em}article>.summary:last-child{border-bottom:none}.parameters th{text-align:right}.parameters td{color:#3c3c43;color:var(--secondary-label)}.parameters th+td{text-align:center}dl{padding-top:1em}dt{font:500 15pt/20pt sans-serif;font:var(--headline)}dd{margin-left:2em;margin-bottom:1em}dd p{margin-top:0}.highlight{background:#f2f2f7;background:var(--secondary-system-background);border-radius:8px;font-size:.75em;margin-bottom:2em;overflow-x:auto;text-indent:-2em;padding:1em 1em 1em 3em;white-space:pre-wrap}.highlight .p{white-space:nowrap}.highlight .placeholder{color:#000;color:var(--label)}.highlight a{text-decoration:underline;color:#8e8e93;color:var(--placeholder-text)}.highlight .attribute,.highlight .keyword,.highlight .literal{color:#af52de;color:var(--system-purple)}.highlight .number{color:#007aff;color:var(--system-blue)}.highlight .declaration{color:#5ac8fa;color:var(--system-teal)}.highlight .type{color:#5856d6;color:var(--system-indigo)}.highlight .directive{color:#ff9500;color:var(--system-orange)}.highlight .comment{color:#8e8e93;color:var(--system-gray)}main summary:hover{text-decoration:underline}figure{margin:2em 0;padding:1em 0}figure svg{max-width:100%;height:auto!important;margin:0 auto;display:block}h1 small{font-size:.5em;line-height:1.5;display:block;font-weight:400;color:#636366;color:var(--quaternary-label)}dd code,li code,p code{font-size:smaller;color:#3c3c43;color:var(--secondary-label)}a code{text-decoration:underline}dl dt[class],nav li[class],section>[role=article][class]{background-image:var(--background-image);background-size:1em;background-repeat:no-repeat;background-position:left .25em;padding-left:3em}dl dt[class]{background-position-y:.125em}section>[role=article]{margin-bottom:1em;padding-bottom:1em;border-bottom:1px solid #e5e5ea;border-bottom:1px solid var(--separator);padding-left:2em!important}section>[role=article]:last-of-type{margin-bottom:0;padding-bottom:0;border-bottom:none}dl dt[class],nav li[class]{list-style:none;text-indent:-1em;margin-bottom:.5em}nav li[class]{padding-left:2.5em}.associatedtype{--background-image:var(--icon-associatedtype);--link:var(--system-pink)}.case,.enumeration_case{--background-image:var(--icon-case);--link:var(--system-teal)}.class{--background-image:var(--icon-class);--link:var(--system-indigo)}.enumeration{--background-image:var(--icon-enumeration)}.enumeration,.extension{--link:var(--system-orange)}.extension{--background-image:var(--icon-extension)}.function{--background-image:var(--icon-function);--link:var(--system-green)}.initializer,.method{--background-image:var(--icon-method);--link:var(--system-blue)}.property{--background-image:var(--icon-property);--link:var(--system-teal)}.protocol{--background-image:var(--icon-protocol);--link:var(--system-pink)}.structure{--background-image:var(--icon-structure);--link:var(--system-purple)}.typealias{--background-image:var(--icon-typealias)}.typealias,.variable{--link:var(--system-green)}.variable{--background-image:var(--icon-variable)}.unknown{--link:var(--quaternary-label);color:#007aff;color:var(--link)} --------------------------------------------------------------------------------