If the entire views access the same model in an app, I think the Singleton pattern is enough. Am I right?
For example, if MainView and ChildView access the same model(e.g. AppSetting) like below, I cannot find any reason to use EnvironmentObject instead of the Singleton pattern. Is there any problem if I use like this? If it is okay, then when I should use EnvironmentObject instead of the Singleton pattern?
class AppSetting: ObservableObject {
static let shared = AppSetting()
private init() {}
#Published var userName: String = "StackOverflow"
}
struct MainView: View {
#ObservedObject private var appSetting = AppSetting.shared
var body: some View {
Text(appSetting.userName)
}
}
struct ChildView: View {
#ObservedObject private var appSetting = AppSetting.shared
var body: some View {
Text(appSetting.userName)
}
}
Thanks in advance.
You are correct there is no reason in this case to use an EnvironmentObject. Apple even encourages to make no excessive use of EnvironmentObjects.
Nevertheless an EnvironmentObject can be great too, if you use an object in many views, because then you don't have to pass it from View A to B, from B to C and so on.
Often you find yourself in a situation where even #State and #Binding will be enough to share and update data in a view and between two views.
I think when your app supports multiple windows via Scene (example Document based apps) maybe the singleton is not a solution and the EnvironmentObject is better.
Example you want to share selectedColor. When you use a singleton, the selectedColor will be same in entire app, in every scene (in every view in window). But if you want to use separated settings the EnvironmentObject is convenient.
I found singleton easier because you can use it in a class vs passing it in the class via .onAppear().
Singleton:
class ViewModel {
private let appSetting = AppSetting.shared
}
EnvironmentObject:
class SomeClass {
private var appSetting : AppSetting?
func addAppSetting(_ appSetting : AppSetting) {
self.appSetting = appSetting
}
}
struct MyView: View {
#EnvironmentObject var appSetting : AppSetting
#StateObject var someClass = SomeClass()
var body: some View {
ZStack {
}
.onAppear {
someClass.addAppSetting(appSetting)
}
}
}
Related
I have a pretty complicated UI so I broke my view models into multiple observable object classes that manage each part of the UI.
The 2 view model instances are created inside a "main" class called Manager. Manager contains methods that manipulate the published properties inside the view models.
I want those methods to be available to be used from my views but since the Manager class does not conform to ObservableObject protocol as it doesn't have any published properties, where do I create an instance of the Manager class so that I can use its methods from MULTIPLE views?
Important secondary question: According to Paul Hudson from HackingWithSwift, he recommended to mark all classes that are ObservableObjects with #MainActor attribute. But if I do that, the Manager class' methods won't be able to manipulate the published properties of the view models since not all methods from Manager have to run on the main queue. What is the solution?
import SwiftUI
class ViewModel1: ObservableObject {
#Published var duration = 0.0
}
class ViewModel2: ObservableObject {
#Published var currentTime = 0.0
}
class Manager {
var vm1 = ViewModel1()
var vm2 = ViewModel2()
func play() {
//Code that changes the published properties of the 2 view models
}
func pause() {
//Code that changes the published properties of the 2 view models
}
}
struct ContentView: View {
#EnvironmentObject var manager: Manager //Won't work since Manager doesn't conform to ObservableObject
var body: some View {
//View code
}
}
A possible approach is to create manager as regular property, but inject view models as environment, like
struct ContentView: View {
private var manager = Manager()
var body: some View {
SomeRootView()
.environmentObject(manager.vm1)
.environmentObject(manager.vm2)
}
}
or in-place of each subview depending in which subview hierarchy corresponding view model is needed.
If manager is needed somewhere in deep child view, then it possible to transfer it there by injecting Environment value, like in https://stackoverflow.com/a/61847419/12299030. Or to make it singleton and use via Manager.shared.
Complete findings and variants code
When updating child classes' properties you'll need to trigger a change event manually using objectWillChange.send.
class ViewModel1: ObservableObject {
#Published var duration = 0.0
}
class Manager: ObservableObject {
var vm1 = ViewModel1()
func play() {
vm1.duration += 10
objectWillChange.send()
}
}
struct ContentView: View {
#ObservedObject var manager = Manager() // init here only for testing
var body: some View {
Button {
manager.play()
} label: {
Text (String(manager.vm1.duration))
}
}
}
I understand that EnvironmentObject property wrapper can be used to pass around objects to views. I have a session object which I am passing around to my views. Now I have a requirement to pass this into one of my model classes (i.e., non-view). Ideally, this model (receiving the session object) is instantiated as a StateObject.
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var transactionsModel = TransactionsModel(token: session.token)
The code above will not work (understandably) because:
cannot use instance member 'session' within property initializer; property initializers run before 'self' is available
Any suggestions on how I can pass in the session into TransactionsModel?
Try initializing the StateObject in an .onAppear() prop to a child view, like this:
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var transactionsModel: TransactionModel?
var body: some View {
SomeChildView()
.onAppear(perform: {
transactionModel = TransactionModel(token: session.token)
})
}
}
This way, the variable is initialized when the view renders on the screen. It doesn't matter much which child view you add the onAppear prop to, as long as it is rendered as soon as the parent does.
The best way that I've found to do this (because you cannot have an optional StateObject) is:
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var localModel = LocalModel()
var body: some View {
SomeChildView()
.onAppear {
localModel.transactionModel = TransactionModel(token: session.token)
}
}
class LocalModel: ObservableObject {
#Published transactionModel: TransactionModel?
}
}
This is an incorrect answer. Please check out the chosen answer above.
You can access session object in init. In this case, transactionsModel should be done to be already initialized in any ways.
#EnvironmentObject var session: Session
#StateObject var transactionsModel = TransitionalModel(token: "")
init() {
let token = self.session.token
_transactionsModel = StateObject(wrappedValue: TransitionalModel(token: token))
}
Although it's out of the question, I am not sure if it's good way to pass something between them who look like being in different levels in the level of View.
I have an issue with propagating changes that happen to objects in the view model that are kept in an array.
I understand that #Published for a collection would work if the collection itself changes (eg. if elements were struct not class). Assuming that I need to preserve classes as classes. Is there a way to propagate events to a view, so that it knows it should be refreshed.
I have been trying all nasty ways like implementing ObservableCollection or ObservableArray but nothing seems to work.
Below an example of what I am struggling with.
Toggle is changing internally element of an array which has all the ObservableObject conformance and #Published annotation but still Text is not being refreshed.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
Text(vm.texts.first!.text)
.padding()
Button("Toggle") {
vm.texts.first?.toggle()
}
}
}
class ViewModel: ObservableObject {
#Published var texts: [TextHolder] = [.init(), .init()]
}
class TextHolder: ObservableObject {
#Published var text: String = ""
func toggle() {
text = UUID().uuidString
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Problem with your approach is that TextHolder is a class, which is reference type, if you change any value in it, changes won't reflect to array that's why SwiftUI view is not updated.
Approach 1:
You can change TextHolder from class to struct, if you change any value in struct a new copy is created, your array will get the change to as your SwiftUI view.
Please try below code
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
Text(vm.texts.first!.text)
.padding()
Button("Toggle") {
vm.texts[0].toggle()
}
}
}
class ViewModel: ObservableObject {
#Published var texts: [TextHolder] = [.init(), .init()]
}
struct TextHolder {
var text: String = ""
mutating func toggle() {
text = UUID().uuidString
}
}
Approach 2:
After changing value you have to manually tell your viewModel that something is changed, please refresh.
Button("Toggle") {
vm.texts[0].toggle()
vm.objectWillChange.send()
}
Hope it will help you to understand.
Note: this is based on the requirement you listed of "Assuming that I need to preserve classes as classes" -- otherwise, making your model a struct gives you all of this behavior for free.
You can call objectWillChange.send() manually on the ObservableObject. For example:
Button("Toggle") {
vm.texts.first?.toggle()
vm.objectWillChange.send()
}
Major downsides include having to add code to call this at each mutation site and actually remembering to do this. You could do things to compartmentalize the code a little more like moving toggle to the parent object and passing an index to it -- then, you could keep all of the objectWillChange calls in the parent. Also, you could experiment with KVO to watch the properties of the child objects and call objectWillChange when you see one of them change.
If you are not able to convert the class to a struct, an approach you can take is to subscribe to all objectWillChange publishers of the items in the array, and emit one for the main model, when one of those objects change:
#Published var texts: [TextHolder] = [.init(), .init()] {
didSet {
updateTextsSubscriptions()
}
}
private var textsSubscriptions = [AnyCancellable]()
private func updateTextsSubscriptions() {
textsSubscriptions = texts.map {
$0.objectWillChange.sink(receiveValue: {
self.objectWillChange.send()
})
}
}
You will also need to call updateTextsSubscriptions from within the initializer(s), to make sure any initial values for the texts array are monitored:
init() {
updateTextsSubscriptions()
}
i'm trying to do a simple login screen using swiftUI.
i put my isLogin into a class and set it as an EnviromentObject Variable.
How do I use it in a fullscreen cover?
My class
class AuthUser:ObservableObject {
#Published var isLogin:Bool = false
#Published var isCorrect:Bool = false
}
My View :
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
#State var showLogin:Bool = false
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $showLogin, content: AfterLogin.init)
}
}
You can use binding directly to property of environment object, like
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
The structure you're attempting to follow is called MVVM or Model-View-View-Model, which is nearly a requirement for swiftUI I believe the only thing that you're missing is the actual usage of that ViewModel, or in your case the AuthUser. So let's dig into that.
View Model
class AuthUser: ObservableObject {
#Published var isLogin = false
//Any other code, methods, constructors you want.
}
View
struct UIController: View {
#ObservedObject var userAuth = AuthUser()
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
What did I change? I changed your #EnvironmentObject to a ObservedObject because in your case you're not likely to actually be using an environment object. What are those special tags?
ObservableObject, this means that it can be observed for state changes.
ObservedObject, an object that is watching for state changes.
Published, an object that has public availability.
EnvironmentObject, an object that is created for the environment and accessible wherever in the app. This particular object is not immediately disposed which is why we removed it in your example.
So what's the purpose of doing things this way? Well it has to do with abstraction and readability. MVVM frameworks provide a way to Bind a view to an object. That binding means that any changes on the view change the data, and any changes to the data change the view. Swift makes this concept simple enough. In your case we're binding the isPresented to the ObservedObject and checking that objects isLogin state. If it changes, the view is also changed.
In the code below (a stripped-down version of some code in a project) I'm using a MVVM pattern with two views:
ViewA - displays a value stored in an ObservableObject ViewModel;
ViewB - displays the same value and has a Slider that changes that value, which is passed to the view using Binding.
Inside of ViewModelA I have a computed property which serves both to avoid the View from accessing the Model directly and to perform some other operations when the value inside the model (the one being displayed) is changed.
I'm also passing that computed value to a ViewModelB, using Binding, which acts as a StateObject for ViewB. However, when dragging the Slider to change that value, the value changes on ViewA but doesn't change on ViewB and the slider itself doesn't slide. As expected, when debugging, the wrappedValue inside the Binding is not changing. But how is the change propagated upwards (through the Binding's setters, I imagine) but not downwards back to ViewB?? I imagine this can only happen if the variable is being duplicated somewhere and changed only in one place, but I can't seem to understand where or if that's what's actually happening.
Thanks in advance!
Views:
import SwiftUI
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.value)
}
}
}
struct ViewA: View {
let value: Double
var body: some View {
Text("\(value)").padding()
}
}
struct ViewB: View {
#StateObject var viewModelB: ViewModelB
init(value: Binding<Double>){
_viewModelB = StateObject(wrappedValue: ViewModelB(value: value))
}
var body: some View {
VStack{
Text("\(viewModelB.value)")
Slider(value: $viewModelB.value, in: 0...1)
}
}
}
ViewModels:
class ViewModelA: ObservableObject {
#Published var model = Model()
var value: Double {
get {
model.value
}
set {
model.value = newValue
// perform other checks and operations
}
}
}
class ViewModelB: ObservableObject {
#Binding var value: Double
init(value: Binding<Double>){
self._value = value
}
}
Model:
struct Model {
var value: Double = 0
}
If you only look where you can't go, you might just miss the riches below
Breaking single source of truth, and breaching local (private) property of #StateObjectby sharing it via Binding are two places where you can't go.
#EnvironmentObject or more generally the concept of "shared object" between views are the riches below.
This is an example of doing it without MVVM nonsense:
import SwiftUI
final class EnvState: ObservableObject {#Published var value: Double = 0 }
struct ContentView: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
ViewA()
ViewB()
}
}
}
struct ViewA: View {
#EnvironmentObject var eos: EnvState
var body: some View {
Text("\(eos.value)").padding()
}
}
struct ViewB: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
Text("\(eos.value)")
Slider(value: $eos.value, in: 0...1)
}
}
}
Isn't this easier to read, cleaner, less error-prone, with fewer overheads, and without serious violation of fundamental coding principles?
MVVM does not take value type into consideration. And the reason Swift introduces value type is so that you don't pass shared mutable references and create all kinds of bugs.
Yet the first thing MVVM devs do is to introduce shared mutable references for every view and pass references around via binding...
Now to your question:
the only options I see are either using only one ViewModel per Model, or having to pass the Model (or it's properties) between ViewModels through Binding
Another option is to drop MVVM, get rid of all view models, and use #EnvironmentObject instead.
Or if you don't want to drop MVVM, pass #ObservedObject (your view model being a reference type) instead of #Binding.
E.g.;
struct ContentView: View {
#ObservedObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA)
ViewB(value: viewModelA)
}
}
}
On a side note, what's the point of "don't access model directly from view"?
It makes zero sense when your model is value type.
Especially when you pass view model reference around like cookies in a party so everyone can have it.
Really it looks like broken single-source or truth concept. Instead the following just works (ViewModelB might probably be needed for something, but not for this case)
Tested with Xcode 12 / iOS 14
Only modified parts:
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.model.value)
}
}
}
struct ViewB: View {
#Binding var value: Double
var body: some View {
VStack{
Text("\(value)")
Slider(value: $value, in: 0...1)
}
}
}