I'm looking to create an EnvironmentObject that can be accessed by the View Model (not just the view).
The Environment object tracks the application session data, e.g. loggedIn, access token etc, this data will be passed into the view models (or service classes where needed) to allow calling of an API to pass data from this EnvironmentObjects.
I have tried to pass in the session object to the initialiser of the view model class from the view but get an error.
how can I access/pass the EnvironmentObject into the view model using SwiftUI?
You can do it like this:
struct YourView: View {
#EnvironmentObject var settings: UserSettings
#ObservedObject var viewModel = YourViewModel()
var body: some View {
VStack {
Text("Hello")
}
.onAppear {
self.viewModel.setup(self.settings)
}
}
}
For the ViewModel:
class YourViewModel: ObservableObject {
var settings: UserSettings?
func setup(_ settings: UserSettings) {
self.settings = settings
}
}
You shouldn't. It's a common misconception that SwiftUI works best with MVVM. MVVM has no place in SwiftUI. You are asking that if you can shove a rectangle to fit a triangle shape. It wouldn't fit.
Let's start with some facts and work step by step:
ViewModel is a model in MVVM.
MVVM does not take value types (e.g.; no such thing in Java) into consideration.
A value type model (model without state) is considered safer than reference type model (model with state) in the sense of immutability.
Now, MVVM requires you to set up a model in such way that whenever it changes, it updates the view in some pre-determined way. This is known as binding.
Without binding, you won't have nice separation of concerns, e.g.; refactoring out model and associated states and keeping them separate from view.
These are the two things most iOS MVVM developers fail:
iOS has no "binding" mechanism in traditional Java sense. Some would just ignore binding, and think calling an object ViewModel automagically solves everything; some would introduce KVO-based Rx, and complicate everything when MVVM is supposed to make things simpler.
Model with state is just too dangerous because MVVM put too much emphasis on ViewModel, too little on state management and general disciplines in managing control; most of the developers end up thinking a model with state that is used to update view is reusable and testable. This is why Swift introduces value type in the first place; a model without state.
Now to your question: you ask if your ViewModel can have access to EnvironmentObject (EO)?
You shouldn't. Because in SwiftUI a model that conforms to View automatically has reference to EO. E.g.;
struct Model: View {
#EnvironmentObject state: State
// automatic binding in body
var body: some View {...}
}
I hope people can appreciate how compact SDK is designed.
In SwiftUI, MVVM is automatic. There's no need for a separate ViewModel object that manually binds to view which requires an EO reference passed to it.
The above code is MVVM. E.g.; a model with binding to view. But because model is value type, so instead of refactoring out model and state as view model, you refactor out control (in protocol extension, for example).
This is official SDK adapting design pattern to language feature, rather than just enforcing it. Substance over form. Look at your solution, you have to use singleton which is basically global. You should know how dangerous it is to access global anywhere without protection of immutability, which you don't have because you have to use reference type model!
TL;DR
You don't do MVVM in java way in SwiftUI. And the Swift-y way to do it is no need to do it, it's already built-in.
Hope more developer see this since this seemed like a popular question.
Below provided approach that works for me. Tested with many solutions started with Xcode 11.1.
The problem originated from the way EnvironmentObject is injected in view, general schema
SomeView().environmentObject(SomeEO())
ie, at first - created view, at second created environment object, at third environment object injected into view
Thus if I need to create/setup view model in view constructor the environment object is not present there yet.
Solution: break everything apart and use explicit dependency injection
Here is how it looks in code (generic schema)
// somewhere, say, in SceneDelegate
let someEO = SomeEO() // create environment object
let someVM = SomeVM(eo: someEO) // create view model
let someView = SomeView(vm: someVM) // create view
.environmentObject(someEO)
There is no any trade-off here, because ViewModel and EnvironmentObject are, by design, reference-types (actually, ObservableObject), so I pass here and there only references (aka pointers).
class SomeEO: ObservableObject {
}
class BaseVM: ObservableObject {
let eo: SomeEO
init(eo: SomeEO) {
self.eo = eo
}
}
class SomeVM: BaseVM {
}
class ChildVM: BaseVM {
}
struct SomeView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: SomeVM
init(vm: SomeVM) {
self.vm = vm
}
var body: some View {
// environment object will be injected automatically if declared inside ChildView
ChildView(vm: ChildVM(eo: self.eo))
}
}
struct ChildView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: ChildVM
init(vm: ChildVM) {
self.vm = vm
}
var body: some View {
Text("Just demo stub")
}
}
Solution for: iOS 14/15+
Here's how you might interact with an Environment Object from a View Model, without having to inject it on instantiation:
Define the Environment Object:
import Combine
final class MyAuthService: ObservableObject {
#Published private(set) var isSignedIn = false
func signIn() {
isSignedIn = true
}
}
Create a View to own and pass around the Environment Object:
import SwiftUI
struct MyEntryPointView: View {
#StateObject var auth = MyAuthService()
var body: some View {
content
.environmentObject(auth)
}
#ViewBuilder private var content: some View {
if auth.isSignedIn {
Text("Yay, you're all signed in now!")
} else {
MyAuthView()
}
}
}
Define the View Model with methods that take the Environment Object as an argument:
extension MyAuthView {
#MainActor final class ViewModel: ObservableObject {
func signIn(with auth: MyAuthService) {
auth.signIn()
}
}
}
Create a View that owns the View Model, receives the Environment Object, and calls the appropriate method:
struct MyAuthView: View {
#EnvironmentObject var auth: MyAuthService
#StateObject var viewModel = ViewModel()
var body: some View {
Button {
viewModel.signIn(with: auth)
} label: {
Text("Sign In")
}
}
}
Preview it for completeness:
struct MyEntryPointView_Previews: PreviewProvider {
static var previews: some View {
MyEntryPointView()
}
}
I choose to not have a ViewModel. (Maybe time for a new pattern?)
I have setup my project with a RootView and some child views. I setup my RootView with a App object as the EnvironmentObject. Instead of the ViewModel accessing Models, all my views access classes on App. Instead of the ViewModel determining the layout, the view hierarchy determine the layout. From doing this in practice for a few apps, I've found my views are staying small and specific. As an over simplification:
class App: ObservableObject {
#Published var user = User()
let networkManager: NetworkManagerProtocol
lazy var userService = UserService(networkManager: networkManager)
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
convenience init() {
self.init(networkManager: NetworkManager())
}
}
struct RootView: View {
#EnvironmentObject var app: App
var body: some View {
if !app.user.isLoggedIn {
LoginView()
} else {
HomeView()
}
}
}
struct HomeView: View {
#EnvironmentObject var app: App
var body: some View {
VStack {
Text("User name: \(app.user.name)")
Button(action: { app.userService.logout() }) {
Text("Logout")
}
}
}
}
In my previews, I initialize a MockApp which is a subclass of App. The MockApp initializes the designated initializers with the Mocked object. Here the UserService doesn't need to be mocked, but the datasource (i.e. NetworkManagerProtocol) does.
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
Group {
HomeView()
.environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
}
}
}
The Resolver library does a nice job to get dependency injection for model classes. It provides a property wrapper #Injected which is very similar in spirit to #EnvironmentObject but works everywhere. So in a model, I would inject a ExampleService like this:
class ExampleModel: ObservableObject {
#Injected var service: ExampleService
// ...
}
This can also be used to resolve dependencies for Views:
struct ExampleView: View {
#ObservedObject var exampleModel: ExampleModel = Resolver.resolve()
var body: some View {
// ...
}
}
An alternative for Views is to use #EnvironmentObject in the SwiftUI view hierarchy, but this gets a little bit cumbersome because you'll have two dependency-injection containers, Resolver/#Injected for everything that's app-wide/service-like and SwiftUI/#EnvironmentObject in the view hierarchy for everything that relates to views/for view models.
Simply create a Singleton and use it wherever you want (view / class / struct / ObservableObject ...)
Creating Class should look like this:
class ApplicationSessionData
{
// this is the shared instance / local copy / singleton
static let singleInstance = ApplicationSessionData()
// save shared mambers/vars here
var loggedIn: Bool = false
var access: someAccessClass = someAccessClass()
var token: String = "NO TOKET OBTAINED YET"
...
}
Using Class/Struct/View should look like this:
struct SomeModel {
// obtain the shared instance
var appSessData = ApplicationSessionData.singleInstance
// use shared mambers/vars here
if(appSessData.loggedIn && appSessData.access.hasAccessToThisView) {
appSessData.token = "ABC123RTY..."
...
}
}
You need to be aware of the pitfalls that exist in Singletons, so you won't fall into one.
Read more here: https://matteomanferdini.com/swift-singleton
Related
Description
For programatic navigation you could previously use NavigationLink(isActive:, destination:, label:) which would fire navigation when the isActive param is true. In IOS 16 this became deprecated and NavigationStack, NavigationLink(value:, label:) and NavigationPath was introduced.
To read about the usage of these follow the links:
https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16 (search for NavigationStack)
My question is how should I use and maintain the array with the content of the navigation stack (like the NavigationPath object) if I'd like to use it in different Views and in their ViewModels?
As you can see in the code below I created a NavigationPath object to hold my navigation stack in the BaseView or BaseView.ViewModel. This way I can do programatic navigation from this BaseView to other pages (Page1, Page2), which is great.
But if I go to Page1 and try to navigate from there to Page2 programatically I need to have access to the original NavigationPath object object, the one that I use in BaseView.
What would be the best way to access this original object?
It is possible that I misunderstand the usage of this new feature but if you have any possible solutions for programatic navigation from a ViewModel I would be glad to see it :)
Code
What I've tried to do:
struct BaseView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationStack(path: $viewModel.paths) {
VStack {
Button("Page 1", action: viewModel.goToPage1)
Button("Page 2", action: viewModel.goToPage2)
}
.navigationDestination(for: String.self) { stringParam in
Page1(stringParam: stringParam)
}
.navigationDestination(for: Int.self) { intParam in
Page2(intParam: intParam)
}
}
}
}
extension BaseView {
#MainActor class ViewModel: ObservableObject {
#Published var paths = NavigationPath()
func goToPage1() {
let param = "Some random string" // gets the parameter from some calculation or async network call
paths.append(param)
}
func goToPage2() {
let param = 19 // gets the parameter from some calculation or async network call
paths.append(param)
}
}
}
struct Page1: View {
#StateObject var viewModel = ViewModel()
let stringParam: String
var body: some View {
VStack {
Button("Page 2", action: viewModel.goToPage2)
}
}
}
extension Page1 {
#MainActor class ViewModel: ObservableObject {
func goToPage2() {
// Need to add value to the original paths variable in BaseView.ViewModel
}
}
}
struct Page2: View {
#StateObject var viewModel = ViewModel()
let intParam: Int
var body: some View {
Text("\(intParam)")
}
}
extension Page2 {
#MainActor class ViewModel: ObservableObject {
}
}
There is no need for MVVM in SwiftUI because the View struct plus property wrappers is already equivalent to a view model object but faster and less error prone. Also in SwiftUI we don't even have access to the traditional view layer - it takes our View data structs, diffs them to create/update/remove UIView/NSView objects, using the best ones for the platform/context. If you use an object for view data instead, then you'll just have the same consistency problems that SwiftUI was designed to eliminate.
Sadly the web (and Harvard University) is filled with MVVM SwiftUI articles by people that didn't bother to learn it properly. Fortunately things are changing:
I was wrong! MVVM is NOT a good choice for building SwiftUI applications (Azam Sharp)
How MVVM devs get MVVM wrong in SwiftUI: From view model to state (Jim Lai)
Stop using MVVM for SwiftUI (Apple Developer Forums)
I'm looking to create an EnvironmentObject that can be accessed by the View Model (not just the view).
The Environment object tracks the application session data, e.g. loggedIn, access token etc, this data will be passed into the view models (or service classes where needed) to allow calling of an API to pass data from this EnvironmentObjects.
I have tried to pass in the session object to the initialiser of the view model class from the view but get an error.
how can I access/pass the EnvironmentObject into the view model using SwiftUI?
You can do it like this:
struct YourView: View {
#EnvironmentObject var settings: UserSettings
#ObservedObject var viewModel = YourViewModel()
var body: some View {
VStack {
Text("Hello")
}
.onAppear {
self.viewModel.setup(self.settings)
}
}
}
For the ViewModel:
class YourViewModel: ObservableObject {
var settings: UserSettings?
func setup(_ settings: UserSettings) {
self.settings = settings
}
}
You shouldn't. It's a common misconception that SwiftUI works best with MVVM. MVVM has no place in SwiftUI. You are asking that if you can shove a rectangle to fit a triangle shape. It wouldn't fit.
Let's start with some facts and work step by step:
ViewModel is a model in MVVM.
MVVM does not take value types (e.g.; no such thing in Java) into consideration.
A value type model (model without state) is considered safer than reference type model (model with state) in the sense of immutability.
Now, MVVM requires you to set up a model in such way that whenever it changes, it updates the view in some pre-determined way. This is known as binding.
Without binding, you won't have nice separation of concerns, e.g.; refactoring out model and associated states and keeping them separate from view.
These are the two things most iOS MVVM developers fail:
iOS has no "binding" mechanism in traditional Java sense. Some would just ignore binding, and think calling an object ViewModel automagically solves everything; some would introduce KVO-based Rx, and complicate everything when MVVM is supposed to make things simpler.
Model with state is just too dangerous because MVVM put too much emphasis on ViewModel, too little on state management and general disciplines in managing control; most of the developers end up thinking a model with state that is used to update view is reusable and testable. This is why Swift introduces value type in the first place; a model without state.
Now to your question: you ask if your ViewModel can have access to EnvironmentObject (EO)?
You shouldn't. Because in SwiftUI a model that conforms to View automatically has reference to EO. E.g.;
struct Model: View {
#EnvironmentObject state: State
// automatic binding in body
var body: some View {...}
}
I hope people can appreciate how compact SDK is designed.
In SwiftUI, MVVM is automatic. There's no need for a separate ViewModel object that manually binds to view which requires an EO reference passed to it.
The above code is MVVM. E.g.; a model with binding to view. But because model is value type, so instead of refactoring out model and state as view model, you refactor out control (in protocol extension, for example).
This is official SDK adapting design pattern to language feature, rather than just enforcing it. Substance over form. Look at your solution, you have to use singleton which is basically global. You should know how dangerous it is to access global anywhere without protection of immutability, which you don't have because you have to use reference type model!
TL;DR
You don't do MVVM in java way in SwiftUI. And the Swift-y way to do it is no need to do it, it's already built-in.
Hope more developer see this since this seemed like a popular question.
Below provided approach that works for me. Tested with many solutions started with Xcode 11.1.
The problem originated from the way EnvironmentObject is injected in view, general schema
SomeView().environmentObject(SomeEO())
ie, at first - created view, at second created environment object, at third environment object injected into view
Thus if I need to create/setup view model in view constructor the environment object is not present there yet.
Solution: break everything apart and use explicit dependency injection
Here is how it looks in code (generic schema)
// somewhere, say, in SceneDelegate
let someEO = SomeEO() // create environment object
let someVM = SomeVM(eo: someEO) // create view model
let someView = SomeView(vm: someVM) // create view
.environmentObject(someEO)
There is no any trade-off here, because ViewModel and EnvironmentObject are, by design, reference-types (actually, ObservableObject), so I pass here and there only references (aka pointers).
class SomeEO: ObservableObject {
}
class BaseVM: ObservableObject {
let eo: SomeEO
init(eo: SomeEO) {
self.eo = eo
}
}
class SomeVM: BaseVM {
}
class ChildVM: BaseVM {
}
struct SomeView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: SomeVM
init(vm: SomeVM) {
self.vm = vm
}
var body: some View {
// environment object will be injected automatically if declared inside ChildView
ChildView(vm: ChildVM(eo: self.eo))
}
}
struct ChildView: View {
#EnvironmentObject var eo: SomeEO
#ObservedObject var vm: ChildVM
init(vm: ChildVM) {
self.vm = vm
}
var body: some View {
Text("Just demo stub")
}
}
Solution for: iOS 14/15+
Here's how you might interact with an Environment Object from a View Model, without having to inject it on instantiation:
Define the Environment Object:
import Combine
final class MyAuthService: ObservableObject {
#Published private(set) var isSignedIn = false
func signIn() {
isSignedIn = true
}
}
Create a View to own and pass around the Environment Object:
import SwiftUI
struct MyEntryPointView: View {
#StateObject var auth = MyAuthService()
var body: some View {
content
.environmentObject(auth)
}
#ViewBuilder private var content: some View {
if auth.isSignedIn {
Text("Yay, you're all signed in now!")
} else {
MyAuthView()
}
}
}
Define the View Model with methods that take the Environment Object as an argument:
extension MyAuthView {
#MainActor final class ViewModel: ObservableObject {
func signIn(with auth: MyAuthService) {
auth.signIn()
}
}
}
Create a View that owns the View Model, receives the Environment Object, and calls the appropriate method:
struct MyAuthView: View {
#EnvironmentObject var auth: MyAuthService
#StateObject var viewModel = ViewModel()
var body: some View {
Button {
viewModel.signIn(with: auth)
} label: {
Text("Sign In")
}
}
}
Preview it for completeness:
struct MyEntryPointView_Previews: PreviewProvider {
static var previews: some View {
MyEntryPointView()
}
}
I choose to not have a ViewModel. (Maybe time for a new pattern?)
I have setup my project with a RootView and some child views. I setup my RootView with a App object as the EnvironmentObject. Instead of the ViewModel accessing Models, all my views access classes on App. Instead of the ViewModel determining the layout, the view hierarchy determine the layout. From doing this in practice for a few apps, I've found my views are staying small and specific. As an over simplification:
class App: ObservableObject {
#Published var user = User()
let networkManager: NetworkManagerProtocol
lazy var userService = UserService(networkManager: networkManager)
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
convenience init() {
self.init(networkManager: NetworkManager())
}
}
struct RootView: View {
#EnvironmentObject var app: App
var body: some View {
if !app.user.isLoggedIn {
LoginView()
} else {
HomeView()
}
}
}
struct HomeView: View {
#EnvironmentObject var app: App
var body: some View {
VStack {
Text("User name: \(app.user.name)")
Button(action: { app.userService.logout() }) {
Text("Logout")
}
}
}
}
In my previews, I initialize a MockApp which is a subclass of App. The MockApp initializes the designated initializers with the Mocked object. Here the UserService doesn't need to be mocked, but the datasource (i.e. NetworkManagerProtocol) does.
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
Group {
HomeView()
.environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
}
}
}
The Resolver library does a nice job to get dependency injection for model classes. It provides a property wrapper #Injected which is very similar in spirit to #EnvironmentObject but works everywhere. So in a model, I would inject a ExampleService like this:
class ExampleModel: ObservableObject {
#Injected var service: ExampleService
// ...
}
This can also be used to resolve dependencies for Views:
struct ExampleView: View {
#ObservedObject var exampleModel: ExampleModel = Resolver.resolve()
var body: some View {
// ...
}
}
An alternative for Views is to use #EnvironmentObject in the SwiftUI view hierarchy, but this gets a little bit cumbersome because you'll have two dependency-injection containers, Resolver/#Injected for everything that's app-wide/service-like and SwiftUI/#EnvironmentObject in the view hierarchy for everything that relates to views/for view models.
Simply create a Singleton and use it wherever you want (view / class / struct / ObservableObject ...)
Creating Class should look like this:
class ApplicationSessionData
{
// this is the shared instance / local copy / singleton
static let singleInstance = ApplicationSessionData()
// save shared mambers/vars here
var loggedIn: Bool = false
var access: someAccessClass = someAccessClass()
var token: String = "NO TOKET OBTAINED YET"
...
}
Using Class/Struct/View should look like this:
struct SomeModel {
// obtain the shared instance
var appSessData = ApplicationSessionData.singleInstance
// use shared mambers/vars here
if(appSessData.loggedIn && appSessData.access.hasAccessToThisView) {
appSessData.token = "ABC123RTY..."
...
}
}
You need to be aware of the pitfalls that exist in Singletons, so you won't fall into one.
Read more here: https://matteomanferdini.com/swift-singleton
I am trying to implement a design pattern like MVC in order to achieve low coupling between different parts of the code. There are few materials online that I personally didn't find helpful in relation to IOS or swift UI development and MVC pattern.
What I am trying to understand is how should the controller class control or render the UI in Swift UI ?
Following the MVC pattern for example - the View shouldn't know about how the model looks like, so sending an object back from the Data Base to the view in order to visually present it wouldn't be a good Idea..
Say we have the following View and Controller, how should I go about the interaction between the controller and view when sending the data back from the DB in order to visually present it in the view ?
View:
import SwiftUI
import Foundation
struct SwiftUIView: View {
var assignmentController = AssignmentController()
#State var assignmentName : String = ""
#State var notes : String = ""
var body: some View {
NavigationView {
VStack {
Form {
TextField("Assignment Name", text: $assignmentName)
TextField("Notes", text: $notes)
}
Button(action: {
self.assignmentController.retrieveFirstAssignment()
}) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
}
Controller
var assignmentModel = AssignmentModel()
func retrieveFirstAssignment()
{
var firstAssignment : Assignment
firstAssignment=assignmentModel.retrieveFirstAssignment()
}
For now, it does nothing with the object it found.
Model
An object in the model composed of two String fields : "assignmentName" and "notes".
*We assume the assignment Model have a working function that retrieves one task from the DB in order to present it in the view.
struct SwiftUIView: View {
#State m = AssignmentModel()
var body: some View {
// use m
}
func loadAssignmentFromDB() {
m.retrieveFirstAssignment()
}
}
This is what I called "enhanced MVC with built-in MVVM".
It satisfies your pursuit of MVC and possibly MVVM, at the same time, with much less effort.
Now I'll argue why:
function, or more specifically, mutation is Control. you don't need an object called "Controller" to have C in MVC. Otherwise we might as well stick to UIKit for 10 more years.
this makes sense when your model is value type. nothing can mutate it without your specific say-so, e.g.; #State annotation. since the only way to mutate model is via these designated endpoints, your Control takes effect only in the function that mutates these endpoints.
Quoting from another reply:
It is true that SwiftUI is a much closer match with MVVM than with MVC. However, almost all example code in the Apple documentation is so simple the ViewModel (and/or Controller in MVC) is left out completely. Once you start creating bigger projects the need for something to bridge your Views and Models arises. However, IMO, the SwiftUI documentation does not (yet) fully address this in a satisfying way.
SwiftUI, if anything, enhances MVC. What is the purpose of having ViewModel?
a.) to have model view binding, which is present in my code snippet above.
b.) to manage states associated with object. if #State does not give you the impression that it is for you to manage state, then i don't know what will. it's funny how many MVVM devs are just blind to this. Just as you don't need View Controller for Control, you don't need ViewModel for VM. Design pattern is a #State of mind. Not about specific naming and rigid structure.
c.) say i'm open to MVVM without being based. which code snippet do you think have more chance in scaling to larger projects? my compact one or that suggested in another reply?
hint: think how many extra files, view models, observableobjects, glue codes, pass-around-vm-as-parameters, init function to accept vm as parameters, you are going to have. and these are just for you to write some of the code in another object. it says nothing about reducing or simplifying the task at hand. hell it does even tell you how to refactor your control codes, so you are most likely to just repeat whatever you did wrong in MVC all over again. did i mention ViewModel is a shared, reference type object with implicit state managements? so what's the point of having a value type model when you are just going to override it with a reference type model?
it's funny how MVVM devs say SwiftUI in its base form is not scalable to larger projects. keeping things simple is the only way to scale.
This is what I observed as the roadmap of dev progression in 2020.
Day1: beginner
Day2: google some, drop MVC
Day3: google some more, SwiftUI not scalable
Day4: OK, I need MVVM+RxSwift+Coordinator+Router+DependencyInjection to avoid SDK short-comings.
My suggestion, due to this seems like a common beginner question, is to learn to walk before you run.
I've personally seen RxSwift developers move controller code to view so that controller appears "clean", and need 3 third-party libraries (one is a custom fork) to send a http GET.
Design pattern means nothing if you can't get simple things simple.
To me this is a very good question. It is true that SwiftUI is a much closer match with MVVM than with MVC. However, almost all example code in the Apple documentation is so simple the ViewModel (and/or Controller in MVC) is left out completely. Once you start creating bigger projects the need for something to bridge your Views and Models arises. However, IMO, the SwiftUI documentation does not (yet) fully address this in a satisfying way. I would love other developers to correct me or expand on this (I'm still learning), but here's what I found out so far:
For managing updating your views in non-example project you almost always want to use ObservableObject/ObservedObject.
Views should only observe an object if they need to be updated if it changes. It is better if you can delegate the updates to a child view.
It may be tempting to create a large ObservableObject and add #Published for all of its properties. However, this means that a view that observes that object gets updated (sometimes visibly) even if a property changes on which the view does not even depend.
Binding is the most natural interface for Views that represent controls that can modify data. Beware that a Binding does NOT trigger updating views. Updating the view should be managed either #State or #ObservedObject (this can done by the parent view of the control).
Constants are the natural interface for Views that only display data (and not modify it).
Here is how I would apply this to your example:
import SwiftUI
//
// Helper class for observing value types
//
class ObservableValue<Value: Hashable>: ObservableObject {
#Published var value: Value
init(initialValue: Value) {
value = initialValue
}
}
//
// Model
//
struct Assignment {
let name : String
let notes: String
}
//
// ViewModel?
//
// Usually a view model transforms data so it is usable by the view. Strings are already
// usable in our components. The only change here is to wrap the strings in an
// ObservableValue so views can listen for changes to the individual properties.
//
// Note: In Swift you often see transformations of the data implemented as extensions to
// the model rather than in a separate ViewModel.
class AssignmentModelView {
var name : ObservableValue<String>
var notes: ObservableValue<String>
init(assignment: Assignment) {
name = ObservableValue<String>(initialValue: assignment.name)
notes = ObservableValue<String>(initialValue: assignment.notes)
}
var assignment: Assignment {
Assignment(name: name.value, notes: notes.value)
}
}
//
// Controller
//
// Publish the first assignment so Views depending on it can update whenever we change
// the first assignment (**not** update its properties)
class AssignmentController: ObservableObject {
#Published var firstAssignment: AssignmentModelView?
func retrieveFirstAssignment() {
let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
firstAssignment = AssignmentModelView(assignment: assignment)
}
}
struct ContentView: View {
// In a real app you should use dependency injection here
// (i.e. provide the assignmentController as a parameter)
#ObservedObject var assignmentController = AssignmentController()
var body: some View {
NavigationView {
VStack {
// I prefer to use `map` instead of conditional views, since it
// eliminates the need for forced unwrapping
self.assignmentController.firstAssignment.map { assignmentModelView in
Form {
ObservingTextField(title: "Assignment Name", value: assignmentModelView.name)
ObservingTextField(title: "Notes", value: assignmentModelView.notes)
}
}
Button(action: { self.retrieveFirstAssignment() }) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
func retrieveFirstAssignment() {
assignmentController.retrieveFirstAssignment()
}
}
//
// Wrapper for TextField that correctly updates whenever the value
// changes
//
struct ObservingTextField: View {
let title: String
#ObservedObject var value: ObservableValue<String>
var body: some View {
TextField(title, text: $value.value)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This may be overkill for your app. There is a more straightforward version, but it has the
disadvantage that TextFields are updated even though their contents hasn't changed. In
this particular example I do not think that matters much. For larger projects it may
become important, not (just) for performance reasons, but updates are sometimes very
visible. For reference: here's the simpler version.
import SwiftUI
// Model
struct Assignment {
let name : String
let notes: String
}
// ViewModel
class AssignmentViewModel: ObservableObject {
#Published var name : String
#Published var notes: String
init(assignment: Assignment) {
name = assignment.name
notes = assignment.notes
}
}
// Controller
class AssignmentController: ObservableObject {
#Published var firstAssignment: AssignmentViewModel?
func retrieveFirstAssignment() {
let assignment = Assignment(name: "My First Assignment", notes: "Everyone has to start somewhere...")
firstAssignment = AssignmentViewModel(assignment: assignment)
}
}
struct ContentView: View {
// In a real app you should use dependency injection here
// (i.e. provide the assignmentController as a parameter)
#ObservedObject var assignmentController = AssignmentController()
var body: some View {
NavigationView {
VStack {
self.assignmentController.firstAssignment.map { assignmentModelView in
FirstAssignmentView(firstAssignment: assignmentModelView)
}
Button(action: { self.retrieveFirstAssignment() }) {
Text("Get The First Assignment !")
}
}
.navigationBarTitle("First Assignment")
}
}
func retrieveFirstAssignment() {
assignmentController.retrieveFirstAssignment()
}
}
struct FirstAssignmentView: View {
#ObservedObject var firstAssignment: AssignmentViewModel
var body: some View {
Form {
TextField("Assignment Name", text: $firstAssignment.name)
TextField("Notes", text: $firstAssignment.notes)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I had the very same question and I came accross an excellent paper by Matteo Manferdini in which he describes how to use the MVC model in SwiftUI.
I used his paper to refactor a Pizza app that uses CoreData. And even though I’m still a beginner in SwiftUI it gave me a very good understanding of how to implement MVC. You can find Matteo’s paper here.
I found this demo project that shows how to use clean swift with swiftU, so I tried to do something like this on my own. I came up with problem while using Core Data. As clean swift architecture only one source (appState in my case) can pass data to view. So I created injection container, it has interactor and appState.
Container:
struct DIContainer: EnvironmentKey {
#ObservedObject var appState: AppState
let interactors: Interactors
init(appState: AppState, interactors: Interactors) {
self.appState = appState
self.interactors = interactors
}
class AppState: ObservableObject, Equatable {
var userData = UserData()
}
extension AppState {
class UserData: ObservableObject, Equatable {
#Published var places:[Place] = []
}
And I inject enviroment like this:
#Environment(\.injected) private var injected: DIContainer
And finally view that uses it:
var body: some View {
NavigationView {
ZStack {
List {
ForEach(injected.$appState.userData.places.wrappedValue){ place in
PlaceRow(place: place)
}
....
Problem is that even array is loade, view doesnt react to that. It refreshes only when i navigate to other view. Also, when I delete or add new elements to array, view has no changes.
If you can give me some ideas it would be nice.
I googled for 4 days, everything I find is not working for me.
If you're injecting the DIContainer into your View like this:
#Environment(\.injected) private var injected: DIContainer
remember that your View will not refresh when the DIContainer properties will change. You have to manually observe them.
You can try one of these solutions:
1) Subscribe to the AppState properties' changes in .onReceive:
#Environment(\.injected) private var injected: DIContainer
var body: some View {
NavigationView { ... }
.onReceive(injected.appState.userData.$places) { places in
// do something with places,
// eg. assign to a #State variable to be used in a `ForEach` loop...
}
}
2) Inject the UserData directly as an #EnvironmentObject:
#EnvironmentObject var userData: UserData
ContentView().environmentObject(container.appState.userData)
As the UserData is an ObservableObject it will change whenever any of its #Published properties (eg. places) is modified.
3) Subscribe to the AppState/UserData in the ViewModel and observe the only the ViewModel in your View.
The more detailed explanation can be found in this version of the same demo project you linked in your question.
I have this view which takes as a param its own View Model. Inside this view I need to show another view which in order for it to be displayed, needs its own View Model. So I present the Purchase view and then inside the Purchase view I need to display the details of the actual item which was purchased. So I pass as a parameter also the ItemViewModel. Both PurchaseViewModel and ItemViewModel are used on other views also.
It could happen in the feature that I would also need ClientViewModel which would be used to display data about the actual buyer of this item. Does this mean that I would need to pass also #ObservedObject var clientViewModel: ClientViewModel as a parameter?
My question is: Is this approach good or there is a better way to do this ?
struct PurchaseView: View {
#ObservedObject var purchaseViewModel: PurchaseViewModel
#ObservedObject var itemViewModel: ItemViewModel
var body: some View {
VStack {
Text(purchaseViewModel.title)
ItemView(itemViewModel: itemViewModel)
}
}
}
class Purchase {
let senderID: String
let receiverID: String
var status: PurchaseStatus
var item: Item?
}
You could make the PurchaseViewModel contain the ItemViewModel and ClientViewModel as properties in the same way that the PurchaseView contains the ItemView and ClientView.
That would make PurchaseViewModel responsible for creating the instances of the other view models. The parent of PurchaseView would no longer need to know about the implementation of PurchaseView.
You should start by considering the relationship of the models, not the view models.
For example, you could make a case that a Purchase involves one or more items (Item) and a purchaser (Client). In this case you would actually create a model something like:
class Purchase {
var items = [Item]()
var client: Client
...
}
And your PurchaseViewModel would reflect this, containing the ItemViewModel and ClientViewModel
let clientViewModel: ClientViewModel
let itemsViewModels: [ItemViewModel]
init(purchase: Purchase) {
self.clientViewModel = ClientViewModel(client:purchase.client)
self.itemsViewModels = purchase.items.map { ItemViewModel(item:$0) }
}
If you have a hierarchy of views that need some unrelated models then you can consider using the environment, especially for models that are "global"
For example, say that the user was looking at an item detail, but they were not yet making a purchase (So there is no Purchase yet) and you want to have a button that shows them their Client detail.
You could pass the ItemViewModel explicitly to the ItemDetailView:
struct ItemListView {
...
Button(action: { ItemDetailView(item:someItemView) })
{ ... }
}
struct ItemDetailView {
...
Button(action: { ClientView() }) {
Text("Client detail")
}
}
struct ClientDetailView {
#EnvironmentObject client: ClientViewModel
...
}
You can inject the ClientViewModel into the environment at a suitable point, such as the root view or even scene delegate.
If you have a hierarchy of views that need these models, but not all views need all models, you could use the environment to provide the various models rather than directly injecting them via initialiser parameters.