SwiftUI and MVVM: Is using multiple viewModels in one view 'valid'? - mvvm

This is more a general question: I'm working on my first SwiftUI project and using MVVM for the first time as well. After programming some views I realized that I need for almost each view two different view models. Often it's the view model for the current view and the view model of the previous/mother view. Is this "normal" or is this a hint that I've designed my project wrong and abusing MVVM?
For example:
I have a view where I list all flashcard decks. For this view I have a decksViewModelthat looks like this:
class DeckListViewModel: ObservableObject{
#Published var decks = [Deck]
#Published var showDeck = false // this value will be true if i tab on a deck and the deck will shown in a detailed view. This value is checked in the list
#Published var expandButton = false
#Published var showDownloadCenter = false
#Published var showCreateDeck = false
}
Now I have a deckDetailView for the detailed view of my deck. The deckDetailViewModel stores the selected item. But to remove this view I need to change the value of the showDeck? in decksViewModel`. So I need to pass this view model as well.

I wouldn't say that's a good way to use MVVM.
Instead of storing showDeck in DeckListViewModel, you could just have it as a local #State variable in whatever view you're using it in. Or if you're using a NavigationView, just use a NavigationLink, and there'll be no need for any state variable.
struct DecksView: View {
#ObservedObject var deckListVM = DeckListViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(deckListVM.decks) { deck in
NavigationLink(destination: DeckDetailView(deckDetailVM: DeckDetailViewModel(deck: deck))) {
// some view
}
}
}
}
}
}
I'm not sure how you implemented your DeckDetailViewModel, just guessing there.

Related

SwiftUI: Have the nth view from an array swapped out when the currentIndex value changes?

There is an array of Views, and whenever the currentPosition variable changes, the view must change. However it is not changing.
I have a global ObservableObject class that has an array of Views, and a currentPosition value. My goal is to have the view changed according to the currentPosition variable.
class AXGlobalValues: ObservableObject {
#Published var tabs: [AXTabItem] = [
.init(webView: AXWebView()),
.init(webView: AXWebView())
]
#Published var currentPosition: Int = 0
}
On the SwiftUI view, I wrote global.tabs[global.currentPosition].webView, and I was expecting the webView to change based on the global.currentPosition value. However it didn't change.
What I tried to do was add an onChange(of:), but since SwiftUI is a declarative language, I couldn't update the view.
struct AXTabView: View {
#EnvironmentObject private var global: AXGlobalValues
var body: some View {
Text("\(global.currentPosition)")
global.tabs[global.currentPosition].webView
.onChange(of: global.currentPosition) { newValue in
// Not allowed in SwiftUI :(
global.tabs[global.currentPosition].webView
}
}
}
Is there any way I can update the view based on the currentPosition variable?
Thanks in advance!
ObservableObject is part of the Combine framework and is designed to persist (or sync) a data model, it's not the right place to store SwiftUI View structs.
Also, for view data like currentPosition we usually use #State var with simple values or a custom struct when we want to group some related vars or have extra logic. body is recomputed when the #State changes.

SwiftUI: How do I share an ObservedObject between child views without affecting the parent view

TL;DR: If I have a view containing a NavigationSplitView(sidebar:detail:), with a property (such as a State or StateObject) tracking user selection, how should I make it so that the sidebar and detail views observe the user selection, but the parent view does not?
Using SwiftUI's new NavigationSplitView (or the deprecated NavigationView), a common paradigm is to have a list of selectable items in the sidebar view, with details of the selected item in the detail view. The selection, of course, needs to be observed, usually from within an ObservedObject.
struct ExampleView: View {
#StateObject private var viewModel = ExampleViewModel()
var body: some View {
NavigationSplitView {
SidebarView(selection: $viewModel.selection)
} detail: {
DetailView(item: viewModel.selection)
}
}
}
struct SidebarView: View {
let selectableItems: [Item] = []
#Binding var selection: Item?
var body: some View {
List(selectableItems, selection: $viewModel.selected) { item in
NavigationLink(value: item) { Text(item.name) }
}
}
}
struct DetailView: View {
let item: Item?
var body: some View {
// Details of the selected item
}
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
}
This, however, poses a problem: the ExampleView owns the ExampleViewModel used for tracking the selection, which means that it gets recalculated whenever the selection changes. This, in turn, causes its children SidebarView and DetailView to be redrawn.
Since we want those children to be recalculated, one might be forgiven for thinking that everything is as intended. However, the ExampleView itself should not be recalculated in my opinion, because doing so will not only update the child views (intended), but also everything in the parent view (not intended). This is especially true if its body is composed of other views, modifiers, or setup work. Case in point: in this example, the NavigationSplitView itself will also be recalculated, which I don't think is what we want.
Almost all tutorials, guides and examples I see online use a version of the above example - sometimes the viewModel is passed as an ObservedObject, or as an EnvironmentObject, but they all share the same trait in that the parent view containing the NavigationSplitView is observing the property that should only be observed by the children of NavigationSplitView.
My current solution is to initiate the viewmodel in the parent view, but not observe it:
struct ExampleView: View {
let viewModel = ExampleViewModel()
...
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
nonisolated init() { }
}
This way, the parent view will remain intact (at least in regards to user selection); however, this will cause the ExampleViewModel to be recreated if anything else would cause the ExampleView to be redrawn - effectively resetting our user selection. Additionally, we are unable to pass any of the viewModel's properties as bindings. So while it works for my current use-case, I don't consider this an effective solution.

Updating property wrapper like #StateObject, affects other view rendering that does not use that property

When using different property wrappers associated with view updates, changes in one place affect rendering of views that do not use that property.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
Text(thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
class MyViewModel: ObservableObject {
#Published var firstTitle = "firstTitle"
#Published var secondTitle = "secondTitle"
func chageFirstTitle() {
firstTitle = "hello world"
}
}
I understand that the reason why the Text exposing the viewModel.secondTitle is re-rendered is because the #StateObject varviewModel = MyViewModel() dependency changed when the `viewModel.firstTitle changed.
However, I don't know why Text using #State var thirdTitle = "thirdTitle" is re-rendered too. In WWDC21 session Demystify SwiftUI, I saw that the view is re-rendered only when the related dependency is updated according to the dependency graph. But, even though the thirdTitle is irrelevant to the change of the viewModel, third Text using that dependency is re-rendered and the background color is changed.
What's even more confusing is that if I seperate the third Text into a separate view ( ThirdView ) and receive the thirdTitle using #Binding, the background color does not change because it is not re-rendering at that time.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
ThirdView(text: $thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
Regarding the situation I explained, could you help me to understand the rendering conditions of the view?
To render SwiftUI calls body property of a view (it is computable, i.e. executes completely on call). This call is performed whenever any view dependency, i.e. dynamic property, is changed.
So, viewModel.chageFirstTitle() changes dependency for ContentView and ContentView.body is called and every primitive in it is rendered. ThirdView also created but as far as its dependency is not changed, its body is not called, so content is not re-rendered.
A few things wrong here. We don't use view model objects in SwiftUI for view data, it's quite inefficient/buggy to do so. Instead, use a struct with mutating funcs with an #State. Pass in params to sub-Views as lets for read access, #Binding is only when you need write access. In terms of rendering, first of all body is only called if the let property is different from the last time the sub-View is init, then it diffs the body from the last time it was called, if there are any differences then SwiftUI adds/removes/updates actual UIKit UIViews on your behalf, then actual rendering of those UIViews, e.g. drawRect, is done by CoreGraphics.
struct ContentViewConfig {
var firstTitle = "firstTitle"
var secondTitle = "secondTitle"
mutating func changeFirstTitle() {
firstTitle = "hello world"
}
}
struct ContentView: View {
#State var config = Config()
...
struct ThirdView: View {
let text: String
...
Combine's ObservableObject is usually only used when needing to use Combine, e.g. using combineLatest with multiple publishers or for a Store object to hold the model struct arrays in #Published properties that are not tied to a View's lifetime like #State. Your use case doesn't look like a valid use of ObservableObject.

SwiftUi Fullscreen cover without using state variable

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.

SwiftUI: #ObservedObject redraws every view

I try to implement the MVVM way correctly in SwiftUI, so I came up with this (simplified) Model and ViewModel:
struct Model {
var property1: String
var property2: String
}
class ViewModel: ObservableObject {
#Published var model = Model(property1: "this is", property2: "a test")
}
Using this in a View works fine, but I experienced some bad performance issues, as I extended the ViewModel with some computed properties and some functions (as well the Model itself is more complicated). But let's stay with this example, because it demonstrates perfectly, what I think is a big problem in SwiftUI itself.
Imagine, you have those views to display the data:
struct ParentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
print("redrawing ParentView")
return ChildView(viewModel: self.viewModel)
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
print("redrawing ChildView")
return VStack {
ViewForTextField(property: self.$viewModel.model.property1)
ViewForTextField(property: self.$viewModel.model.property2)
}
}
}
struct ViewForTextField: View {
#Binding var property: String
var body: some View {
print("redrawing textView of \(self.property)")
return TextField("...", text: self.$property)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Now entering text into one of the TextField leads to a redraw of every View in my window! The print output is:
redrawing ParentView
redrawing ChildView
redrawing textView of this is
redrawing textView of a test
redrawing ParentView
redrawing ChildView
redrawing textView of this isa
redrawing textView of a test
redrawing ParentView
redrawing ChildView
redrawing textView of this isab
redrawing textView of a test
...
As I can see, SwiftUI redraws every view, because every view is listening to the ObservedObject.
How can I tell SwiftUI, that it only should redraw those views, where really happened any changes?
Actually MVVM means own model for every view, which is updated on model changes, not one model for all views.
So there is no needs to observe viewModel in ParentView as it does not depend on it
struct ParentView: View {
var viewModel: ViewModel // << just member to pass down in child
var body: some View {
print("redrawing ParentView")
return ChildView(viewModel: self.viewModel)
}
}
Alternate is to decompose view model so every view has own view sub-model, which would manage updates of own view.
If all your views observe the same thing, and that thing changes, then all your views will re-render. This is by definition. There's no option to configure to optionally update certain views.
That being said, you can work around it by manipulating what changes you'd like to publish.
E.g.;
class ViewModel: ObservableObject {
#Published var model = Model(property1: "this is", property2: "a test")
var mytext = "some text" // pass this as binding instead of model.propertyX
}
Now when textfield changes, mytext changes, but this change won't be published thus won't trigger further view updates. And you still can access it from view model.
I personally would recommend using #State and #EnvironmentObject instead of "view model". They are the built-in way to handle binding and view updates with tons of safe-guards and supports.
Also you should use value type instead of reference type as much as possible. MVVM from other languages didn't take this into consideration.