Skip to content

Repository files navigation

CacheStore

SwiftUI State Management

What is CacheStore?

CacheStore is a SwiftUI state management framework that uses a dictionary as the state. Scoping creates a single source of truth for the parent state. CacheStore uses c, which a simple composition framework. c has the ability to create transformations that are either unidirectional or bidirectional.

CacheStore Basic Idea

A [AnyHashable: Any] can be used as the single source of truth for an app. Scoping can be done by limiting the known keys. Modification to the scoped value or parent value should be reflected throughout the app.

Objects

  • CacheStore: An object that needs defined Keys to get and set values.
  • Store: An object that needs defined Keys, Actions, and Dependencies. (Preferred)
    • TestStore: A testable wrapper around Store to make it easy to write XCTestCases

Store

A Store is an object that you send actions to and read state from. Stores use a CacheStore to manage state behind the scenes. All state changes must be defined in a StoreActionHandler where the state gets modified depending on an action.

TestStore

When creating tests you should use TestStore to send and receive actions while making expectations. If any expectation is false it will be reported in a XCTestCase. If there are any effects left at the end of the test, there will be a failure as all effects must be completed and all resulting actions handled. TestStore uses a FIFO (first in first out) queue to manage the effects.

Basic Usage

Store Example
import CacheStore
import SwiftUI
structPost:Codable,Hashable{varid:IntvaruserId:Intvartitle:Stringvarbody:String}enumStoreKey{case url
case posts
case isLoading
}enumAction{case fetchPosts
case postsResponse(Result<[Post],Error>)}extensionString:Error{}structDependency{varfetchPosts:(URL)async->Result<[Post],Error>}extensionDependency{staticvarmock:Dependency{Dependency(
fetchPosts:{ _ insleep(1)return.success([Post(id:1, userId:1, title:"Mock", body:"Post")])})}staticvarlive:Dependency{Dependency{ url indo{let(data, _)=tryawaitURLSession.shared.data(from: url)return.success(tryJSONDecoder().decode([Post].self, from: data))}catch{return.failure(error)}}}}letactionHandler=StoreActionHandler<StoreKey,Action,Dependency>{ cacheStore, action, dependency inswitch action {case.fetchPosts:structFetchPostsID:Hashable{}guardlet url = cacheStore.get(.url, as:URL.self)else{returnActionEffect(.postsResponse(.failure("Key `.url` was not a URL")))}
cacheStore.set(value:true, forKey:.isLoading)returnActionEffect(id:FetchPostsID()){.postsResponse(await dependency.fetchPosts(url))}caselet.postsResponse(.success(posts)):
cacheStore.set(value:false, forKey:.isLoading)
cacheStore.set(value: posts, forKey:.posts)caselet.postsResponse(.failure(error)):
cacheStore.set(value:false, forKey:.isLoading)}return.none
}structContentView:View{@ObservedObjectvarstore:Store<StoreKey,Action,Dependency>=.init(
initialValues:[.url:URL(string:"https://jsonplaceholder.typicode.com/posts")!
],
actionHandler: actionHandler,
dependency:.live
).debug
privatevarisLoading:Bool{
store.get(.isLoading, as:Bool.self)??true}varbody:someView{if
!isLoading,let posts = store.get(.posts, as:[Post].self){List(posts, id: \.self){ post inText(post.title)}}else{ProgressView().onAppear{
store.handle(action:.fetchPosts)}}}}
Testing
import CacheStore
import XCTest
@testableimport CacheStoreDemo
classCacheStoreDemoTests:XCTestCase{func testExample_success()throws{letstore=TestStore(
initialValues:[.url:URL(string:"https://jsonplaceholder.typicode.com/posts")asAny],
actionHandler: actionHandler,
dependency:.mock
)
store.send(.fetchPosts){ cacheStore in
cacheStore.set(value:true, forKey:.isLoading)}
store.send(.fetchPosts){ cacheStore in
cacheStore.set(value:true, forKey:.isLoading)}letexpectedPosts:[Post]=[Post(id:1, userId:1, title:"Mock", body:"Post")]
store.receive(.postsResponse(.success(expectedPosts))){ cacheStore in
cacheStore.set(value:false, forKey:.isLoading)
cacheStore.set(value: expectedPosts, forKey:.posts)}}func testExample_failure()throws{letstore=TestStore(
initialValues:[:],
actionHandler: actionHandler,
dependency:.mock
)
store.send(.fetchPosts, expecting:{ _ in})
store.receive(.postsResponse(.failure("Key `.url` was not a URL"))){ cacheStore in
cacheStore.set(value:false, forKey:.isLoading)}}}

Acknowledgement of Dependencies

Inspiration