diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 615aedc..9d21b8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ Please make sure you read the documentation (inline in the source files) and [RE ## Project structure -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. +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. ## Conclusion diff --git a/README.md b/README.md index 09e1cf0..93767bc 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,15 @@ If you prefer not to use SPM, you can also add **Loadability** as a normal frame ## Usage -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)) for views to load their data and show placeholders while loading. +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. + +The library has extensive inline documentation in all files. 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. *Note that the code snippets in this section have some code omitted for brevity; see the [Examples](examples) section for complete code examples.* ### Networking -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). +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). 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`. @@ -82,8 +84,7 @@ class YourLoader: Loader { } ``` -As noted previously, loaders automatically conform to `ObservableObject`; you can observe the published properties yourself or use the [SwiftUI integration](swiftui). - +As noted previously, loaders automatically conform to `ObservableObject`; you can observe the published properties yourself or use the [SwiftUI integration](swiftui-integration). #### Specific Loaders @@ -150,12 +151,73 @@ let object = cache[key] // YourObject?, Access cached value cache[key] = nil // Remove value ``` -`SerializableCache` subclasses the base `Cache` class to add support for saving caches to disk, which allows you to +`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`. -## Examples +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. + +```swift +let sCache = SerializableCache.load(name: "Name") +``` + +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. + +#### Shared Caches + +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. + +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. + +You create a shared cache by subclassing either `SharedCache` or `SharedSerializableCache`, depending on what features you want. + +```swift +struct YourCache: SharedSerializableCache { + typealias Key = YourKey + typealias Value = YourValue + static let shared = SerializableCache.load(name: "Name") +} +``` + +Instead of creating multiple cache instances, use the `shared` instance every time you need that cache. + +#### Using Caches with Loaders + +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. -### []() +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`). + +### SwiftUI Integration + +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. + +You only need to implement some basic requirements; creating a loader type, passing the key, and implementing the placeholder and body methods. + +The following shows an example of creating a loadable view. Note a few key points: +- You must ensure that your loader variable is annotated by `@StateObject` or the loader's publisher will *not* update the view as expected +- Because of Swift's behaviour with protocol conformance, your views must implement the `body` variable directly, passing in the `loaderView` generated by `LoadableView` + +```swift +struct YourView: View, LoadableView { + @StateObject var loader = YourLoader() // must be annotated by @StateObject + + var body: some View { + loaderView // required + } + + func body(with object: YourObject) -> some View { + /* create a body with the object */ + } + + func placeholder() -> some View { + ProgressView("Loading...") + } +} +``` + +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. + +## Examples +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. ## Contributing diff --git a/Sources/Loadability/UI/Load.swift b/Sources/Loadability/UI/Load.swift index 423ba8b..5ee4605 100644 --- a/Sources/Loadability/UI/Load.swift +++ b/Sources/Loadability/UI/Load.swift @@ -28,13 +28,6 @@ public struct Load - /// - key: <#key description#> - /// - objectKeyPath: <#objectKeyPath description#> - /// - contentBuilder: <#contentBuilder description#> - /// - placeholderContentBuilder: <#placeholderContentBuilder description#> public init(with loader: Loader, key: Loader.Key, objectKeyPath: KeyPath, @@ -75,7 +68,7 @@ public struct Load + /// The content shown. @ViewBuilder private var bodyContent: some View { if let value = value { contentBuilder(value)