SwiftUI Is it ok to put an ObservableObject inside another ObservableObject? - ios

I have a view called PurchaseView. This view displays details about the purchase, what was purchased and who purchased it. What I'm doing is that in this view im putting both the ItemView and ClientView inside PurchaseView. ItemView and ClientView are shared and there are used in other parts of my app. They have their own ViewModels.
I have also tried to put ItemViewModel and ClientViewModel inside PurchaseViewModel but I do not know if it is ok to put an ObservableObject inside another ObservableObject. Is this a good approach or there should not be any ObservableObject inside an ObservableObject? Which one of the following is better?
This?
class PurchaseViewModel: ObservableObject {
let clientViewModel: ClientViewModel
let itemsViewModel: ItemViewModel
//
}
Or this?
struct PurchaseView: View {
#ObservedObject var purchaseViewModel: PurchaseViewModel
#ObservedObject var itemViewModel: ItemViewModel
#ObservedObject var clientViewModel: ClientViewModel
var body: some View {
//
}
}
Purchase model:
class Purchase {
let id: String
let total: Double
// ...
var item: Item?
var client: Client?
}

Your first solution will not work as the changes to the nested ObservableObjects aren't propagated upwards:
class PurchaseViewModel: ObservableObject {
let clientViewModel: ClientViewModel
let itemsViewModel: ItemViewModel
...
}
A workaround can be found here: How to tell SwiftUI views to bind to nested ObservableObjects.
Your second approach is right and will work for most cases:
struct PurchaseView: View {
#ObservedObject var purchaseViewModel: PurchaseViewModel
#ObservedObject var itemViewModel: ItemViewModel
#ObservedObject var clientViewModel: ClientViewModel
...
}
If you share an ObservableObject for many views you can inject it to the environment instead and access as an #EnvironmentObject.
Alternatively you can make your nested ObservableObjects to be structs:
class PurchaseViewModel: ObservableObject {
#Published var clientViewModel: ClientViewModel // <- add `#Published`
...
}
struct ClientViewModel { // <- use `struct` instead of `class`
...
}
Note that your ClientViewModel will become a new struct every time it (or any of its properties) changes - this solution shouldn't be overused (especially for complex ViewModels).

Just noting that I'm using the NestedObservableObject approach from How to tell SwiftUI views to bind to nested ObservableObjects.
Normally I'd avoid this but the nested object in question is actually a CoreData model so breaking things out into smaller views doesn't really work in this regard.
Since the world treats NSManagedObjects as (mostly) ObservableObjects, this solution seemed best.

Related

Integrate repeating code in swiftUI, like inherit or something

I have some SwiftUI View which are having lots of repeating part like below:
import SwiftUI
struct SomeView: View {
// start of the repeating part
#EnvironmentObject var mainViewModel: MainViewModel
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var viewModel: AuthViewModel
init(viewModel:AuthViewModel){
self.viewModel = viewModel
}
// end of the repeating part
var body: some View {
// some view
}
}
In java or Kotlin I can just simply inherit a class of its type,
but I've notice that struct can't be inherit.
So what would be the alternative way to achieve this?
Protocol and protocol extensions are the alternative of inheritance.
https://www.hackingwithswift.com/sixty/9/4/protocol-extensions
I guess in your case, you try to handle different authentication views consider using view composition.
https://www.hackingwithswift.com/books/ios-swiftui/view-composition

What is the difference between ObservedObject and StateObject in SwiftUI

If I have an ObservableObject in SwiftUI I can refer to it as an #ObservedObject:
class ViewModel: ObservableObject {
#Published var someText = "Hello World!"
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.someText)
}
}
Or as a #StateObject:
class ViewModel: ObservableObject {
#Published var someText = "Hello World!"
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.someText)
}
}
But what's the actual difference between the two? Are there any situations where one is better than the other, or they are two completely different things?
#ObservedObject
When a view creates its own #ObservedObject instance it is recreated every time a view is discarded and redrawn:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
}
On the contrary a #State variable will keep its value when a view is redrawn.
#StateObject
A #StateObject is a combination of #ObservedObject and #State - the instance of the ViewModel will be kept and reused even after a view is discarded and redrawn:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
}
Performance
Although an #ObservedObject can impact the performance if the View is forced to recreate a heavy-weight object often, it should not matter much when the #ObservedObject is not complex.
When to use #ObservedObject
It might appear there is no reason now to use an #ObservedObject, so when should it be used?
You should use #StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with #ObservedObject.
Note there are too many use-cases possible and sometimes it may be desired to recreate an observable property in your View. In that case it's better to use an #ObservedObject.
Useful links:
What’s the difference between #StateObject and #ObservedObject?
What’s the difference between #ObservedObject, #State, and #EnvironmentObject?
What is the #StateObject property wrapper?
Apple documentation did explain why initializing with ObservedObject is unsafe.
SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view.
The solution is StateObject.
At the same time, the documentation showed us how we should create data models in a view (or app/scene) when it can hold on to the truth, and pass it to another view.
struct LibraryView: View {
#StateObject var book = Book() // Hold on to the 1 truth
var body: some View {
BookView(book: book) // Pass it to another view
}
}
struct BookView: View {
#ObservedObject var book: Book // From external source
}
Even though pawello2222's answer have nicely explained the differences when the view itself creates its view model, it's important to note the differences when the view model is injected into the view.
When you inject the view model into the view, as long as the view model is a reference type, there are no differences between #ObservedObject and #StateObject, since the object that injected the view model into your view should hold a reference to view model as well, hence the view model isn't destroyed when the child view is redrawn.
class ViewModel: ObservableObject {}
struct ParentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
ChildView(viewModel: viewModel) // You inject the view model into the child view
}
}
// Even if `ChildView` is discarded/redrawn, `ViewModel` is kept in memory, since `ParentView` still holds a reference to it - `ViewModel` is only released and hence destroyed when `ParentView` is destroyed/redrawn.
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
}
Here is an example to illustrate the difference.
Every time you click the Refresh button the StateObjectClass is recreated from scratch only for CountViewObserved. This means it's #Published count property gets the default value of 0 when this happens.
The difference between #StateObject and #ObservedObject is clear. The #StateObject version of the observed StateObjectClass preserves its state since it is never deinitted. The #ObservedObject version does not as it is recreated. So you should use #StateObject for the owner of an ObservableObject.
import SwiftUI
class StateObjectClass: ObservableObject {
enum ObserverType: String {
case stateObject
case observedObject
}
#Published var count = 0
let type: ObserverType
let id = UUID()
init(type: ObserverType) {
self.type = type
}
deinit {
print(#function, "type: \(type.rawValue) id: \(id)")
}
}
struct CountViewState: View {
#StateObject var state = StateObjectClass(type: .stateObject)
var body: some View {
VStack {
Text("#StateObject's count: \(state.count)")
Button("ADD 1"){
state.count += 1
}
}
}
}
struct CountViewObserved: View {
#ObservedObject var state = StateObjectClass(type: .observedObject)
var body: some View {
VStack {
Text("#ObservedObject's count: \(state.count)")
Button("Add 1") {
state.count += 1
}
}
}
}
struct ContentView: View {
#State private var count = 0
var body: some View {
VStack {
Text("Refresh CounterView's count: \(count)")
Button("Refresh") {
count += 1
}
CountViewState()
.padding()
CountViewObserved()
.padding()
}
}
}
#StateObject is a state of a given view, thus the instance of it is retained by SwiftUI across body updates. It is not retained though when running in Preview.
#ObservedObject on the other hand is just an object being observed by given View, thus is not retained by SwiftUI (it has to be retained outside of the View).
In other words - it looks like SwiftUI keeps a strong reference of #StateObject and unowned reference of #ObservedObject.
Retained vs non-retained source, Previews behavior source, around ~8:30.
The difference between let's say :
#ObservedObject var book: BookModel
And
#StateObject var book: BookModel
#ObservedObject does NOT own the instance book, its your responsibility to manage the life cycle of the instance..
But when you want to tie the life cycle of your observable object book to your view like in #State you can use #StateObject.
In this case SwiftUI will OWN the observable object and the creation and destruction will be tied to the view's life cycle
SwiftUI will keep the object alive for the whole life cycle of the view
This is great for expensive resources, you do not need to fiddle with onDisappear anymore to release resources.
This clarification is taken from WWDC2020 Data essentials in SwiftUI

SwiftUI MVVM Pass EnvironmentObject into ViewModel [duplicate]

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

SwiftUI cant bind injected value

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.

SwiftUI - How to pass EnvironmentObject into View Model?

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

Resources