Repository pattern support library for Swift with Concurrency.
Available for iOS or any Swift projects.
This library provides remote and local cache abstraction and observation with Swift Concurrency.
Created according to the following 5 policies.
- Repository pattern
- Abstract remote and local data acquisition.
- Single Source of Truth
- Looks like a single source from the user side.
- Observer pattern
- Observing data with Combine Framework.
- Return value as soon as possible
- Representing the state of data
The following is the class structure of Repository pattern using this library.
The following is an example of screen display using LoadingState
.
Install as Swift Package Manager exchanging *.*.*
for the latest tag.
dependencies: [
.package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "*.*.*"),
],
There are only 2 things you have to implement:
- Create a class to manage the in-app cache.
- Create a class to get data from origin server.
First, create a class that inherits Cacher<PARAM: Hashable, DATA>
.
Put the type you want to use as a param in <PARAM: Hashable>
. If you don't need the param, put in the UnitHash
.
class UserCacher: Cacher<UserId, UserData> {
static let shared = UserCacher()
private override init() {}
}
Cacher<PARAM: Hashable, DATA>
needs to be used in Singleton pattern.
Next, create a class that implements Fetcher
.
Put the type you want to use as a Data in DATA
associatedtype.
An example is shown below.
struct UserFetcher : Fetcher {
typealias PARAM = UserId
typealias DATA = UserData
private let userApi = UserApi()
func fetch(param: UserId) async throws -> UserData {
userApi.fetch(userId: param)
}
}
You need to prepare the API access class.
In this case, UserApi
classe.
After that, you can get the StoreFlowable<DATA>
class from the AnyStoreFlowable.from()
method.
Be sure to go through the created StoreFlowable<DATA>
class when getting / updating data.
let userFlowable: AnyStoreFlowable<UserData> = AnyStoreFlowable.from(cacher: userCacher, fetcher: userFetcher, param: userId)
let userStateSequence: LoadingStateSequence<UserData> = userFlowable.publish()
You can get the data in the form of LoadingStateSequence<DATA>
(Same as AsyncSequence<LoadingState<DATA>>
) by using the publish()
method.
LoadingState
is a enum that holds raw data.
You can observe the data by for-in AsyncSequence
.
and branch the data state with doAction()
method or switch
statement.
for await userState in userStateSequence {
userState.doAction(
onLoading: { (content: UserData?) in
...
},
onCompleted: { (content: UserData, _, _) in
...
},
onError: { (error: Error) in
...
}
)
}
Refer to the example module for details. This module works as an Android app.
See GithubMetaCacher + GithubMetaFetcher or GithubUserCacher + GithubUserFetcher.
This example accesses the Github API.
Get data without LoadingState enum
If you don't need value flow and LoadingState
enum, you can use requireData()
or getData()
.
requireData()
throws an Error if there is no valid cache and fails to get new data.
getData()
returns nil instead of Error.
public extension StoreFlowable {
func getData(from: GettingFrom = .both) async -> DATA?
func requireData(from: GettingFrom = .both) async throws -> DATA
}
GettingFrom
parameter specifies where to get the data.
public enum GettingFrom {
// Gets a combination of valid cache and remote. (Default behavior)
case both
// Gets only remotely.
case origin
// Gets only locally.
case cache
}
However, use requireData()
or getData()
only for one-shot data acquisition, and consider using publish()
if possible.
If you want to ignore the cache and get new data, add forceRefresh
parameter to publish()
.
public extension StoreFlowable {
func publish(forceRefresh: Bool = false) -> LoadingStateSequence<DATA>
}
Or you can use refresh()
if you are already observing the Publisher
.
public protocol StoreFlowable {
func refresh() async
}
Use validate()
if you want to verify that the local cache is valid.
If invalid, get new data remotely.
public protocol StoreFlowable {
func validate() async
}
If you want to update the local cache, use the update()
method.
Publisher
observers will be notified.
public protocol StoreFlowable {
func update(newData: DATA?) async
}
Use mapContent(transform)
to transform content in LoadingStateSequence<DATA>
.
let state: LoadingStateSequence<Int> = ...
let mappedState: LoadingStateSequence<String> = state.mapContent { value: Int in
value.toString()
}
Use combineState(state, transform)
to combine multiple LoadingStateSequence<DATA>
.
let state1: LoadingStateSequence<Int> = ...
let state2: LoadingStateSequence<Int> = ...
let combinedState: LoadingStateSequence<Int> = state1.combineState(state2) { value1: Int, value2: Int in
value1 + value2
}
You can easily set the cache expiration time. Override expireSeconds variable in your Cacher<PARAM: Hashable, DATA>
class.
The default value is TimeInterval.infinity
(= will NOT expire).
class UserCacher: Cacher<UserId, UserData> {
static let shared = UserCacher()
private override init() {}
override var expireSeconds: TimeInterval {
get { TimeInterval(60 * 30) } // expiration time is 30 minutes.
}
}
If you want to make the cached data persistent, override the method of your Cacher<PARAM: Hashable, DATA>
class.
class UserCacher: Cacher<UserId, UserData> {
static let shared = UserCacher()
private override init() {}
override var expireSeconds: TimeInterval {
get { TimeInterval(60 * 30) } // expiration time is 30 minutes.
}
// Save the data for each parameter in any store.
override func saveData(data: GithubMeta?, param: UnitHash) async {
...
}
// Get the data from the store for each parameter.
override func loadData(param: UnitHash) async -> GithubMeta? {
...
}
// Save the epoch time for each parameter to manage the expiration time.
// If there is no expiration time, no override is needed.
override func saveDataCachedAt(epochSeconds: Double, param: UnitHash) async {
...
}
// Get the date for managing the expiration time for each parameter.
// If there is no expiration time, no override is needed.
override func loadDataCachedAt(param: UnitHash) async -> Double? {
...
}
}
This library includes Pagination support.
Inherit PaginationCacher<PARAM: Hashable, DATA>
& PaginationFetcher
instead of Cacher<PARAM: Hashable, DATA>
& Fetcher
.
An example is shown below.
class UserListCacher: PaginationCacher<UnitHash, UserData> {
static let shared = UserListCacher()
private override init() {}
}
struct UserListFetcher : PaginationFetcher {
typealias PARAM = UnitHash
typealias DATA = UserData
private let userListApi = UserListApi()
func fetch(param: UnitHash) async throws -> PaginationFetcher.Result<UserData> {
let fetched = userListApi.fetch(pageToken: nil)
return PaginationFetcher.Result(data: fetched.data, nextKey: fetched.nextPageToken)
}
func fetchNext(nextKey: String, param: UnitHash) async throws -> PaginationFetcher.Result<UserData> {
let fetched = userListApi.fetch(pageToken: nextKey)
return PaginationFetcher.Result(data: fetched.data, nextKey: fetched.nextPageToken)
}
}
You need to additionally implements fetchNext(nextKey: String, param: PARAM)
.
And then, You can get the state of additional loading from the next
parameter of onCompleted {}
.
let userFlowable = AnyStoreFlowable.from(cacher: userListCacher, fetcher: userListFetcher)
for await loadingState in userFlowable.publish() {
loadingState.doAction(
onLoading: { (content: UserData?) in
// Whole (Initial) data loading.
},
onCompleted: { (content: UserData, next: AdditionalLoadingState, _) in
// Whole (Initial) data loading completed.
next.doAction(
onFixed: { (canRequestAdditionalData: Bool) in
// No additional processing.
},
onLoading: {
// Additional data loading.
},
onError: { (error: Error) in
// Additional loading error.
}
)
},
onError: { (error: Error) in
// Whole (Initial) data loading error.
}
)
}
To display in the UITableView
, Please use the difference update function. See also UITableViewDiffableDataSource
.
You can request additional data for paginating using the requestNextData()
method.
public extension PaginationStoreFlowable {
func requestNextData(continueWhenError: Bool = true) async
}
The GithubOrgsCacher + GithubOrgsFetcher or GithubReposCacher + GithubReposFetcher classes in example module implement pagination.
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.