Swiftpack.co -  KazaKago/StoreFlowable.swift as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
KazaKago/StoreFlowable.swift
Repository pattern support library for Swift with Combine framework.
.package(url: "https://github.com/KazaKago/StoreFlowable.swift.git", from: "1.0.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/103459433-bc7e8f80-4d52-11eb-8c3d-c885d76565fc.jpg

The following is an example of screen display using State.

https://user-images.githubusercontent.com/7742104/103459431-bab4cc00-4d52-11eb-983a-2087dd3100a0.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<KEY: Hashable>.
Put the type you want to use as a key in <KEY: Hashable>. If you don't need the key, put in the UnitHash.

class UserStateManager: FlowableDataStateManager<UserId> {
    static let shared = UserStateManager()
    private override init() {}
}

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

2. Create StoreFlowableCallback class

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

An example is shown below.

struct UserFlowableCallback : StoreFlowableCallback {

    typealias KEY = UserId
    typealias DATA = UserData

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

    init(userId: UserId) {
        key = userId
    }

    // Set the key for input / output of data. If you don't need the key, put in the UnitHash.
    let key: UserId

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

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

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

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

    // Whether the cache is valid.
    func needRefresh(cachedData: UserData) -> 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<KEY: Hashable, DATA> class from the StoreFlowableCallback.create() method, and use it to build the Repository class.
Be sure to go through the created AnyStoreFlowable<KEY: Hashable, DATA> class when getting / updating data.

struct UserRepository {

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

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

You can get the data in the form of StatePublisher<DATA> (Same as AnyPublisher<State<DATA>, Never>) by using the publish() method.
State 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(
                onFixed: {
                    state.content.doAction(
                        onExist: { value in
                            ...
                        },
                        onNotExist: {
                            ...
                        }
                    )
                },
                onLoading: {
                    state.content.doAction(
                        onExist: { value in
                            ...
                        },
                        onNotExist: {
                            ...
                        }
                    )
                },
                onError: { error in
                    state.content.doAction(
                        onExist: { value in
                            ...
                        },
                        onNotExist: {
                            ...
                        }
                    )
                }
            )
        }
        .store(in: &cancellableSet)
}

Example

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

This example accesses the Github API.

Advanced Usage

Get data without State enum

If you don't need value flow and State 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 = .mix) -> AnyPublisher<DATA?, Never>
    func requireData(from: GettingFrom = .mix) -> 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 mix
    // Gets only remotely.
    case fromOrigin
    // Gets only locally.
    case fromCache
}

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) -> StatePublisher<DATA>
}

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

public extension StoreFlowable {
    func refresh(clearCacheWhenFetchFails: Bool = true, continueWhenError: Bool = true) -> 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 PagnatingStoreFlowableCallback<KEY: Hashable, DATA> instead of StoreFlowableCallback<KEY: Hashable, DATA>.

An example is shown below.

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

    typealias KEY = UnitHash
    typealias DATA = [UserData]

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

    let key: UnitHash = UnitHash()

    let flowableDataStateManager: FlowableDataStateManager<UnitHash> = UserListStateManager.shared

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

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

    func saveDataToCache(cachedData: [UserData]?, newData: [UserData]) -> AnyPublisher<Void, Never> {
        let mergedData = (cachedData ?? []) + newData
        return userListCache.save(data: mergedData)
    }

    func fetchDataFromOrigin() -> AnyPublisher<FetchingResult<[UserData]>, Error> {
        userListApi.fetch(page: 1)
    }

    func fetchAdditionalDataFromOrigin(cachedData: [UserData]?) -> AnyPublisher<FetchingResult<[UserData]>, Error> {
        let page = ((cachedData?.count ?? 0) / 10 + 1)
        return userListApi.fetch(page: page)
    }

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

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

The GithubOrgsFlowableCallback and GithubReposFlowableCallback classes in example project implement pagination.

Request additional data

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

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

License

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

GitHub

link
Stars: 0
Last commit: 1 week ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

1.0.0
1 week ago

Initial release.

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