Communicating/reacting to a specific state change between views for two separate yet semi-associated view models - swift

I have a view that consists of two subviews with 1) separate internal states but 2) a high-level state that’s noteworthy to any associated views i.e. each other. That is, a change in one’s high-level state should trigger the other to reconsider/change its own. Imagine – in the interest of keeping it simple/generalisable - a header that has a set of buttons that change based on the option selected in the footer. On the flip side, there’s a button in the header that exits the active state and reset it back to its original state (and therefore the footer’s too).
I’ve managed to conveniently achieve this by adopting a shared universal instance for each view’s view model. The header for example was observing changes in the footer’s view model, and therefore is able react to a change in the footer’s high-level state. However, considering the footer had other internal states (i.e. irrelevant to the header but necessary for the footer view that was observing changes to its own view model), the header view was being re-rendered every time there was any change, regardless of relevance, to the footer view. In reality, at least to my eyes, I wouldn’t notice any visual hiccups but I’m aware this is a fundamentally inefficient (and wrong) way to manage the flow of states. And that if this misbehavior was widespread, then performance is likely to become compromised (visible).
What I have is something along the lines of:
struct HeaderView: View {
// MARK: Observers
#ObservedObject var viewModel: HeaderViewModel = HeaderViewModel.shared
#ObservedObject var footerViewModel: FooterViewModel = FooterViewModel.shared
// MARK: -
var body: some View {
VStack {
switch footerViewModel.state {
case 1: HeaderState1View()
case 2: HeaderState2View()
case 3: HeaderState3View()
}
Button("EXIT", action: {viewModel.reset()})
}
}
}
struct FooterView: View {
// MARK: Observers
#ObservedObject var viewModel: FooterViewModel = FooterViewModel.shared
// MARK: -
var body: some View {
Button("State 1", action: {viewModel.state(to: 1)})
.background(viewModel.state == 1 ? Color.red : Color.clear)
Button("State 2", action: {viewModel.reset()})
.background(viewModel.state == 2 ? Color.red : Color.clear)
Button("State 3", action: {viewModel.reset()})
.background(viewModel.state == 3 ? Color.red : Color.clear)
}
}
class HeaderViewModel: ObservableObject {
// ...
func reset() {
FooterViewModel.shared.reset()
// and other internal state changes
}
// ...
}
class FooterViewModel: ObservableObject {
#Published var state: Int = 1
// ...
func state(to state: Int) {
self.state = state
}
func reset() {
self.state = 1
}
// ...
}
The view models for each is created inside a root-level ViewModel responsible for creating both (and others).
class ViewModel {
var footer = FooterViewModel()
var header = HeaderViewModel()
// etc.
}
That’s created in #main root struct:
#main
struct myApp: App {
// MARK: Variables
var viewModel = ViewModel()
// MARK: Layout
var body: some Scene {
return WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
struct ContentView: View {
// MARK: -
var body: some View {
VStack {
header
/// other views
footer
}
}
Without depending on shared instances of both header and footers’ view models, as I have above, what would the most appropriate/efficient way to achieve what I’m after?
To be more specific/clear, if, for example, the Header and Footer views create a #StateObject of their respective view models i.e.
#StateObject var viewModel: FooterViewModel = FooterViewModel() /// in FooterView.swift
#StateObject var viewModel: HeaderViewModel = HeaderViewModel() /// in HeaderView.swift
How would I be able let any change in HeaderView communicate back to the instance of FooterViewModel that FooterView created?
EDIT:
I imagine one way to possibly achieve this is if I create a third view model that consists of the published shared state and that it’s passed into both views when they’re initialised. But imagine there was more than one published property that needed to be shared between the views, and that there was another view in the mix that needed access to only one of them – would I create two shared view models then: one with publishers for shared properties between all 3 and another with ones for the 2? Doesn’t seem a practical strategy.

Related

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 and MVVM: Is using multiple viewModels in one view 'valid'?

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.

How to reset a subview in SwiftUI?

Below is a simplified version of the code that I'm using. But whenever I resetKeyboard() it still shows the previous keyboard. Is there anyway to make it so when I call resetKeyboard() it replaces the keyboard with a fresh KeyboardView?
struct GameView: View {
#State var myKeyboard = KeyboardView()
var body: some View {
VStack{
Button("Change Keyboard") {
myKeyboard.change()
}
myKeyboard
Button("Reset Keyboard") {
resetKeyboard()
}
}
}
func resetKeyboard(){
self.myKeyboard = KeyboardView()
}
}
SwiftUI constructs a view tree from View objects in body of their parents.
So, what SwiftUI got was the initial copy (remember, it's a value-type struct) of myKeyboard, not the copy you are changing.
Under normal usage, you don't keep instances of various View stored as variables (I mean, you can, but you'd need to understand in depth what's going on).
What you probably want is to change the data that drives the child view. Where does (should) this data live? It depends on what you want to do.
In most cases the parent "owns" the state, i.e. has the source of truth of some data that the child relies on. Then it's trivial to change the state and pass the state to the child via its init:
struct ChildView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
struct ParentView: View {
#State var random: Int = Int.random(1...100)
var body: some View {
VStack() {
ChildView(number: random)
Button("randomize") {
self.random = Int.random(1...100)
}
}
}
}
But, say, the parent doesn't want to do the randomization - i.e. the child should deal with it.
The proper approach is to create a view model for the child, which the parent (or the parent's own view model) could own and pass via init, and then the view model would deal with nuances of randomization.
class ChildVM: ObservableObject {
#Published var random = Int.random(1...100)
func change() {
random = Int.random(1...100)
}
}
The parent creates an instance of ChildVM and passes it to the child:
struct ParentVuew: View {
let childVM = ChildVM()
var body: some View {
VStack() {
ChildView(vm: childVm)
Button("randomize") {
self.childVM.change()
}
}
}
}
And the child view is simply driven by the view model:
struct ChildView: View {
#ObservedObject let vm: ChildVM
var body: some View {
Text("\(vm.random)")
}
}
Obviously, this is a simplified example that could have been achieved in any number of ways.
And there are different ways for the parent to "message" the child.
But the general takeaway should be that Views should be thought of as declarative structures - not living instances - and the data is what drives the changes in those views. You need to decide who is best to own the source of truth.

SwiftUI - #Binding to a computed property which accesses value inside ObservableObject property duplicates the variable?

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)
}
}
}