-
-
Save chrishulbert/9a21635a581e044f86e3ccc1d56010a6 to your computer and use it in GitHub Desktop.
Previewable SwiftUI ViewModel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
// Every different screen's VM re-uses this one VM protocol: | |
protocol ViewModel<ViewEvent, ViewState>: ObservableObject { | |
associatedtype ViewEvent | |
associatedtype ViewState | |
// For comms in the VM -> View direction: | |
var viewState: ViewState { get set } | |
// For comms in the View -> VM direction: | |
func handle(event: ViewEvent) | |
} | |
// This is specific to one screen, and used to update what is displayed. | |
struct FooViewState: Equatable { | |
var text: String | |
var sheetIsPresented: Bool = false | |
} | |
// This is specific to one screen, and used by the view to communicate | |
// to the VM. | |
enum FooViewEvent { | |
case hello | |
case goodbye | |
case present | |
} | |
// This is an example of a screen with a generic VM, so you can use the | |
// normal VM, or you can preview it with a mock VM. | |
struct FooView<VM: ViewModel>: View | |
where VM.ViewEvent == FooViewEvent, | |
VM.ViewState == FooViewState | |
{ | |
@StateObject var viewModel: VM | |
var body: some View { | |
VStack { | |
Text(viewModel.viewState.text) | |
Button("Hello") { | |
viewModel.handle(event: .hello) | |
} | |
Button("Goodbye") { | |
viewModel.handle(event: .goodbye) | |
} | |
Button("Present modal sheet") { | |
viewModel.handle(event: .present) | |
} | |
} | |
.sheet(isPresented: $viewModel.viewState.sheetIsPresented) { | |
Text("This is a modal sheet!") | |
.presentationDetents([.medium]) | |
.presentationDragIndicator(.visible) | |
} | |
} | |
} | |
// This is the real VM for this screen. | |
class FooViewModel: ViewModel { | |
@Published var viewState: FooViewState | |
init() { | |
viewState = FooViewState( | |
text: "Nothing has happened yet." | |
) | |
} | |
func handle(event: FooViewEvent) { | |
switch event { | |
case .hello: | |
viewState.text = "👋" | |
case .goodbye: | |
viewState.text = "😢" | |
case .present: | |
viewState.sheetIsPresented = true | |
} | |
} | |
} | |
#if targetEnvironment(simulator) | |
// This is declared once and used by all screens you want to preview. | |
// Is 'preview view' a tautology? Should this be called 'PreviewModel' | |
// or 'PreViewModel' ? Flip a coin to decide... | |
class PreviewViewModel<ViewEvent, ViewState>: ViewModel { | |
@Published var viewState: ViewState | |
init(viewState: ViewState) { | |
self.viewState = viewState | |
} | |
func handle(event: ViewEvent) { | |
print("Handle event: \(event)") | |
} | |
} | |
#Preview("Preview viewmodel") { | |
FooView( | |
viewModel: PreviewViewModel( | |
viewState: FooViewState( | |
text: "This is a preview!" | |
) | |
) | |
) | |
} | |
#Preview("Real viewmodel") { | |
FooView(viewModel: FooViewModel()) | |
} | |
#endif |
99% inspired by @swhitty and @MightyFine I cannot take much credit!
Hehe team effort 😉 great write up!!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice — I really like this pattern!