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 = RootStateDefine 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.
Mutationdoes 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
