Swiftpack.co - KazaKago/StoreFlowable.swift as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by KazaKago.
KazaKago/StoreFlowable.swift 3.2.0
Repository pattern support library for Swift with Combine framework.
⭐️ 6
🕓 4 weeks ago
iOS macOS watchOS tvOS
.package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "3.2.0")

StoreFlowable.swift

GitHub tag (latest by date) Test License

Repository pattern support library for Swift with Combine Framework.
Available for iOS or any Mac/tvOS/watchOS projects.

Overview

This library provides remote and local data abstraction and observation with Combine Framework.
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.

https://user-images.githubusercontent.com/7742104/126891014-ce95b223-5b13-4290-8b58-faf1dcebf49e.jpg

The following is an example of screen display using LoadingState.

https://user-images.githubusercontent.com/7742104/125714730-381eee65-4126-4ee8-991a-7fc64dfb325c.jpg

Requirement

  • iOS 13.0 or later
  • MacOS 10.15 or later
  • tvOS 13.0 or later
  • watchOS 6.0 or later

Install

Install as Swift Package Manager exchanging x.x.x for the latest tag.

dependencies: [
    .package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "x.x.x"),
],

Basic Usage

There are only 5 things you have to implement:

  • Create data state management class
  • Get data from local cache
  • Save data to local cache
  • Get data from remote server
  • Whether the cache is valid

1. Create FlowableDataStateManager class

First, create a class that inherits FlowableDataStateManager<PARAM: Hashable>.
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 UserStateManager: FlowableDataStateManager<UserId> {
    static let shared = UserStateManager()
    private override init() {}
}

FlowableDataStateManager<PARAM: Hashable> needs to be used in Singleton pattern.

2. Create StoreFlowableFactory class

Next, create a class that implements StoreFlowableFactory.
Put the type you want to use as a Data in DATA associatedtype.

An example is shown below.

struct UserFlowableFactory : StoreFlowableFactory {

    typealias PARAM = UserId
    typealias DATA = UserData

    private let userApi = UserApi()
    private let userCache = UserCache()

    // Create data state management class.
    let flowableDataStateManager: FlowableDataStateManager<UserId> = UserStateManager.shared

    // Get data from local cache.
    func loadDataFromCache(param: UserId) -> AnyPublisher<UserData?, Never> {
        userCache.load(userId: param)
    }

    // Save data to local cache.
    func saveDataToCache(newData: UserData?, param: UserId) -> AnyPublisher<Void, Never> {
        userCache.save(userId: param, data: newData)
    }

    // Get data from remote server.
    func fetchDataFromOrigin(param: UserId) -> AnyPublisher<UserData, Error> {
        userApi.fetch(userId: param)
    }

    // Whether the cache is valid.
    func needRefresh(cachedData: UserData, param: UserId) -> AnyPublisher<Bool, Never> {
        cachedData.isExpired()
    }
}

You need to prepare the API access class and the cache access class.
In this case, UserApi and UserCache classes.

3. Create Repository class

After that, you can get the AnyStoreFlowable<DATA> class from the StoreFlowableFactory.create() method, and use it to build the Repository class.
Be sure to go through the created AnyStoreFlowable<DATA> class when getting / updating data.

struct UserRepository {

    func followUserData(userId: UserId) -> LoadingStatePublisher<UserData> {
        let userFlowable: AnyStoreFlowable<UserData> = UserFlowableFactory().create(userId)
        return userFlowable.publish()
    }

    func updateUserData(userData: UserData) -> AnyPublisher<Void, Never> {
        let userFlowable: AnyStoreFlowable<UserData> = UserFlowableFactory().create(userId)
        return userFlowable.update(newData: userData)
    }
}

You can get the data in the form of LoadingStatePublisher<DATA> (Same as AnyPublisher<LoadingState<DATA>, Never>) by using the publish() method.
LoadingState is a enum that holds raw data.

4. Use Repository class

You can observe the data by sink AnyPublisher.
and branch the data state with doAction() method or switch statement.

private func subscribe(userId: UserId) {
    userRepository.followUserData(userId: userId)
        .receive(on: DispatchQueue.main)
        .sink { state in
            state.doAction(
                onLoading: { (content: UserData?) in
                    ...
                },
                onCompleted: { (content: UserData, _, _) in
                    ...
                },
                onError: { (error: Error) in
                    ...
                }
            )
        }
        .store(in: &cancellableSet)
}

Example

Refer to the example project for details. This module works as an iOS app.
See GithubMetaFlowableFactory and GithubUserFlowableFactory.

This example accesses the Github API.

Advanced Usage

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) -> AnyPublisher<DATA?, Never>
    func requireData(from: GettingFrom = .both) -> AnyPublisher<DATA, Error>
}

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.

Refresh data

If you want to ignore the cache and get new data, add forceRefresh parameter to publish().

public extension StoreFlowable {
    func publish(forceRefresh: Bool = false) -> LoadingStatePublisher<DATA>
}

Or you can use refresh() if you are already observing the Publisher.

public protocol StoreFlowable {
    func refresh() -> AnyPublisher<Void, Never>
}

Validate cache data

Use validate() if you want to verify that the local cache is valid.
If invalid, get new data remotely.

public protocol StoreFlowable {
    func validate() -> AnyPublisher<Void, Never>
}

Update cache data

If you want to update the local cache, use the update() method.
Publisher observers will be notified.

public protocol StoreFlowable {
    func update(newData: DATA?) -> AnyPublisher<Void, Never>
}

Pagination support

This library includes Pagination support.

Inherit PagnationStoreFlowableFactory<PARAM: Hashable, DATA> instead of StoreFlowableFactory<PARAM: Hashable, DATA>.

An example is shown below.

class UserListStateManager: FlowableDataStateManager<UnitHash> {
    static let shared = UserListStateManager()
    private override init() {}
}
struct UserListFlowableFactory : PaginationStoreFlowableFactory {

    typealias PARAM = UnitHash
    typealias DATA = [UserData]

    private let userListApi = UserListApi()
    private let userListCache = UserListCache()

    let flowableDataStateManager: FlowableDataStateManager<UnitHash> = UserListStateManager.shared

    func loadDataFromCache(param: UnitHash) -> AnyPublisher<[UserData]?, Never> {
        userListCache.load()
    }

    func saveDataToCache(newData: [UserData]?, param: UnitHash) -> AnyPublisher<Void, Never> {
        userListCache.save(data: newData)
    }

    func saveNextDataToCache(cachedData: [UserData], newData: [UserData], param: UnitHash) -> AnyPublisher<Void, Never> {
        userListCache.save(data: cachedData + newData)
    }

    func fetchDataFromOrigin(param: UnitHash) -> AnyPublisher<Fetched<[UserData]>, Error> {
        userListApi.fetch(pageToken: nil).map { fetchedData in
            Fetched(data: fetchedData, nextKey: fetchedData.nextPageToken)
        }.eraseToAnyPublisher()
    }

    func fetchNextDataFromOrigin(nextKey: String, param: UnitHash) -> AnyPublisher<Fetched<[UserData]>, Error> {
        userListApi.fetch(pageToken: nextKey).map { fetchedData in
            Fetched(data: fetchedData, nextKey: fetchedData.nextPageToken)
        }.eraseToAnyPublisher()
    }

    func needRefresh(cachedData: [UserData], param: UnitHash) -> AnyPublisher<Bool, Never> {
        cachedData.last.isExpired()
    }
}

You need to additionally implements saveNextDataToCache() and fetchNextDataFromOrigin().
When saving the data, combine the cached data and the new data before saving.

And then, You can get the state of additional loading from the next parameter of onCompleted {}.

let userFlowable = UserListFlowableFactory().create(UnitHash())
userFlowable.publish()
    .receive(on: DispatchQueue.main)
    .sink { state in
        state.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.
            }
        )
    }
    .store(in: &cancellableSet)

To display in the UITableView, Please use the difference update function. See also UITableViewDiffableDataSource.

Request additional data

You can request additional data for paginating using the requestNextData() method.

public extension PaginationStoreFlowable {
    func requestNextData(continueWhenError: Bool = true) -> AnyPublisher<Void, Never>
}

Pagination Example

The GithubOrgsFlowableFactory and GithubReposFlowableFactory classes in example project implement pagination.

License

This project is licensed under the Apache-2.0 License - see the LICENSE file for details.

GitHub

link
Stars: 6
Last commit: 4 weeks ago
jonrohan Something's broken? Yell at me @ptrpavlik. Praise and feedback (and money) is also welcome.

Release Notes

3.2.0
4 weeks ago

Bugfix Release

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics