Verge - Neue (SwiftUI / UIKit)
⚠️ Currently in progress
Latest released Verge => master
branch
Verge - Neue is an unidirectional-data-flow framework.
Link to Demo Application Video
Architecture
The sources are in VergeNeue
directory.
Demo Application is in VergeNeueDemo
directory.
Demo implementations are super experimentally code. I think there are no best practices. So these may be updated.
- Store / ScopedStore
- Reducer
- Mutation
- Action
let rootStore = Store(
state: RootState(),
reducer: RootReducer()
)
struct RootState {
var count: Int = 0
var photos: [Photo.ID : Photo] = [:]
var comments: [Commenet.ID : Comment] = [:]
}
struct Photo
struct Photo: Identifiable {
let id: String
let url: URL
}
struct Comment
struct Comment: Identifiable {
let id: String
let photoID: Photo.ID
let body: String
}
Use State
struct HomeView: View {
@ObservedObject var store: Store<RootReducer>
var body: some View {
NavigationView {
List(store.state.photos) { (photo) in
NavigationLink(destination: PhotoDetailView(photoID: photo.id)) {
Cell(photo: photo, comments: self.sessionStore.state.comments(for: photo.id))
}
}
.navigationBarTitle("Home")
}
.onAppear {
self.sessionStore.dispatch { $0.fetchPhotos() }
}
}
}
PhotoDetailView
struct PhotoDetailView: View {
let photoID: Photo.ID
@EnvironmentObject var sessionStore: SessionStateReducer.StoreType
@State private var draftCommentBody: String = ""
private var photo: Photo {
sessionStore.state.photosStorage[photoID]!
}
var body: some View {
VStack {
Text("\(photo.id)")
TextField("Enter comment here", text: $draftCommentBody)
.padding(16)
Button(action: {
guard self.draftCommentBody.isEmpty == false else { return }
self.sessionStore.dispatch {
$0.submitComment(body: self.draftCommentBody, photoID: self.photoID)
}
self.draftCommentBody = ""
}) {
Text("Submit")
}
}
}
}
We can choose class or struct depends on use cases.
Store uses Reducer as an instance.
This means Reducer can have some dependencies. (e.g Database, API client)
Firstly, In order to implement Reducer, Use ReducerType
on the object.
And then, ReducerType
needs a type of state to update the state with type-safety.
class RootReducer: ReducerType {
typealias TargetState = RootState
Define Mutation
The only way to actually change state in a Verge store is by committing a mutation.
Define a function that returns Mutation
object. That expresses that function is Mutation
Mutation
object is simple struct that has a closure what passes current state to change it.
Mutation
does not run asynchronous operation.
extension RootReducer: ReducerType {
func syncIncrement(adding number: Int) -> Mutation {
return .init {
$0.count += number
}
}
}
Commit Mutation
store.commit { $0.syncIncrement() }
Define Action
Action
is similar to Mutation
.
Action
can contain arbitrary asynchronous operations.
To commit Mutations inside Action, Use context.commit
.
extension RootReducer: ReducerType {
func asyncIncrement() -> Action<Void> {
return .init { context in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
context.commit { $0.syncIncrement() }
}
}
}
}
Dispatch Action
store.dispatch { $0.asyncIncrement() }
More sample Reducer implementations
func submitComment(body: String, photoID: Photo.ID) -> Action<Void> {
return .init { context in
let comment = Comment(photoID: photoID, body: body)
context.commit { _ in
.init {
$0.commentsStorage[comment.id] = comment
}
}
}
}
Advanced Informations
ScopedStore
ScopedStore
is a node object detached from Store
It initializes with Store
as parent store and WritableKeyPath to take fragment of parent store's state.
Its side-effects dispatch and commit affects parent-store.
And receives parent-store's side-effects