SwiftUI Published variable doesn't trigger UI update - ios

I have an app with a navigation view list that doesn't update when new elements get added later on in the app. the initial screen is fine and everything get triggered at this moment no matter how I code them, but beyond that, it stays that way. At some point I had my "init" method as an .onappear, and dynamic elements wouldn't come in, but the static ones would get added multiple times when I would go back and forth in the app, this is no longer part of my code now though.
here what my content view look like, I tried to move the navigation view part to the class that has the published var, in case it help, visually it dint change anything, dint help either.
struct ContentView: View {
#ObservedObject var diceViewList = DiceViewList()
var body: some View {
VStack{
Text("Diceimator").padding()
diceViewList.body
Text("Luck Selector")
}
}
}
and the DiceViewList class
import Foundation
import SwiftUI
class DiceViewList: ObservableObject {
#Published var list = [DiceView]()
init() {
list.append(DiceView(objectID: "Generic", name: "Generic dice set"))
list.append(DiceView(objectID: "Add", name: "Add a new dice set"))
// This insert is a simulation of what add() does with the same exact values. it does get added properly
let pos = 1
let id = 1
self.list.insert(DiceView(objectID: String(id), dice: Dice(name: String("Dice"), face: 1, amount: 1), name: "Dice"), at: pos)
}
var body: some View {
NavigationView {
List {
ForEach(self.list) { dView in
NavigationLink(destination: DiceView(objectID: dView.id, dice: dView.dice, name: dView.name)) {
HStack { Text(dView.name) }
}
}
}
}
}
func add(dice: Dice) {
let pos = list.count - 1
let id = list.count - 1
self.list.insert(DiceView(objectID: String(id), dice: dice, name: dice.name), at: pos)
}
}
I'm working on the latest Xcode 11 in case it matter
EDIT: Edited code according to suggestions, problem didnt change at all
struct ContentView: View {
#ObservedObject var vm: DiceViewList = DiceViewList()
var body: some View {
NavigationView {
List(vm.customlist) { dice in
NavigationLink(destination: DiceView(dice: dice)) {
Text(dice.name)
}
}
}
}
}
and the DiceViewList class
class DiceViewList: ObservableObject {
#Published var customlist: [Dice] = []
func add(dice: Dice) {
self.customlist.append(dice)
}
init() {
customlist.append(Dice(objectID: "0", name: "Generic", face: 1, amount: 1))
customlist.append(Dice(objectID: "999", name: "AddDice", face: 1, amount: 1))
}
}

SwiftUI is a paradigm shift from how you would build a UIKit app.
The idea is to separate the data that "drives" the view - which is the View model, from the View presentation concerns.
In other words, if you had a ParentView that shows a list of ChildView(foo:Foo), then the ParentView's view model should be an array of Foo objects - not ChildViews:
struct Foo { var v: String }
class ParentVM: ObservableObject {
#Published let foos = [Foo("one"), Foo("two"), Foo("three")]
}
struct ParentView: View {
#ObservedObject var vm = ParentVM()
var body: some View {
List(vm.foos, id: \.self) { foo in
ChildView(foo: foo)
}
}
}
struct ChildView: View {
var foo: Foo
var body = Text("\(foo.v)")
}
So, in your case, separate the logic of adding Dice objects from DiceViewList (I'm taking liberties with your specific logic for brevity):
class DiceListVM: ObservableObject {
#Published var dice: [Dice] = []
func add(dice: Dice) {
dice.append(dice)
}
}
struct DiceViewList: View {
#ObservedObject var vm: DiceListVM = DiceListVM()
var body: some View {
NavigationView {
List(vm.dice) { dice in
NavigationLink(destination: DiceView(for: dice)) {
Text(dice.name)
}
}
}
}
If you need more data than what's available in Dice, just create a DiceVM with all the other properties, like .name and .dice and objectId.
But the takeaway is: Don't store and vend out views. - only deal with the data.

While testing stuff I realized the problem. I Assumed declaring #ObservedObject var vm: DiceViewList = DiceViewList() in every other class and struct needing it would make them find the same object, but it doesn't! I tried to pass the observed object as an argument to my subview that contain the "add" button, and it now work as intended.

Related

How to pass by reference in Swift - number incrementing app using MVVM

I've just started learning swift and was going to build this number-incrementing sample app to understand MVVM. I don't understand why is my number on the view not updating upon clicking the button.
I tried to update the view everytime user clicks the button but the count stays at zero.
The View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The ViewModel
import SwiftUI
class CounterViewModel: ObservableObject {
#ObservedObject var model = Model()
func increment() {
self.model.count += 1
}
}
The Model
import Foundation
class Model : ObservableObject{
#Published var count = 0
}
Following should work:
import SwiftUI
struct Model {
var count = 0
}
class CounterViewModel: ObservableObject {
#Published var model = Model()
func increment() {
self.model.count += 1
}
}
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please note:
ObservableObject and #Published are designed to work together.
Only a value, that is in an observed object gets published and so the view updated.
A distinction between model and view model is not always necessary and the terms are somewhat misleading. You can just put the count var in the ViewModel. Like:
#Published var count = 1
It makes sense to have an own model struct (or class), when fx you fetch a record from a database or via a network request, than your Model would take the complete record.
Something like:
struct Adress {
let name: String
let street: String
let place: String
let email: String
}
Please also note the advantages (and disadvantages) of having immutable structs as a model. But this is another topic.
Hi it's a bad idea to use MVVM in SwiftUI because Swift is designed to take advantage of fast value types for view data like structs whereas MVVM uses slow objects for view data which leads to the kind of consistency bugs that SwiftUI's use of value types is designed to eliminate. It's a shame so many MVVM UIKit developers (and Harvard lecturers) have tried to push their MVVM garbage onto SwiftUI instead of learning it properly. Fortunately some of them are changing their ways.
When learning SwiftUI I believe it's best to learn value semantics first (where any value change to a struct is also a change to the struct itself), then the View struct (i.e. when body is called), then #Binding, then #State. e.g. have a play around with this:
// use a config struct like this for view data to group related vars
struct ContentViewConfig {
var count = 0 {
didSet {
// could do validation here, e.g. isValid = count < 10
}
}
// include other vars that are all related, e.g. you could have searchText and searchResults.
// use mutating func for logic that affects multiple vars
mutating func increment() {
count += 1
//othervar += 1
}
}
struct ContentView: View {
#State var config = ContentViewConfig() // normally structs are immutable, but #State makes it mutable like magic, so its like have a view model object right here, but better.
var body: some View {
VStack {
ContentView2(count: config.count)
ContentView3(config: $config)
}
}
}
// when designing a View first ask yourself what data does this need to do its job?
struct ContentView2: View {
let count: Int
// body is only called if count is different from the last time this was init.
var body: some View {
Text(count, format: .number)
}
}
struct ContentView3: View {
#Binding var config: ContentViewConfig
var body: some View {
Button(action: {
config.increment()
}) {
Text("Increment")
}
}
}
}
Then once you are comfortable with view data you can move on to model data which is when ObservableObject and singletons come into play, e.g.
struct Item: Identifiable {
let id = UUID()
var text = ""
}
class MyStore: ObservableObject {
#Published var items: [Item] = []
static var shared = MyStore()
static var preview = MyStore(preview: true)
init(preview: Bool = false) {
if preview {
items = [Item(text: "Test Item")]
}
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyStore.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject store: MyStore
var body: some View {
List($store.items) { $item in
TextField("Item", $item.text)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyStore.preview)
}
}
Note we use singletons because it would be dangerous to use #StateObject for model data because its lifetime is tied to something on screen we could accidentally lose all our model data which should have lifetime tied to the app running. Best to think of #StateObject when you need a reference type in a #State, i.e. involving view data.
When it comes to async networking use the new .task modifier and you can avoid #StateObject.

SwiftUI: Communication between different ViewModels

I have a parent view which contains two child views. Each child view gets passed a different EnvironmentObject from the parent view. As a representation for all kinds of different changes, the second child view contains a Button which can be used to call a function in its ViewModel which then is supposed to change a variable in the ViewModel of the first child view.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
var body: some View {
ZStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
...
}}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
var body: some View {
Button(action: {
viewModel_2.changeValue_in_ViewModel_1(value: 1)
}, label: {
Text("Tap to change value")
})
}}
class ViewModel_1: ObservableObject {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
}
}
Is there a way to make those two ViewModels able to communicate with each other?
Thanks!
It would be solved simply by ViewModel_2 referencing ViewModel_1.
However, it is not necessary to refer to all ViewModel_1, so you can separate only the desired logic using protocol and let ViewModel_2 own it.
This is the sample code for the above explanation.
Searching for dependency injection can yield a lot of information about it.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
init() {
let viewModel_1 = ViewModel_1()
_viewModel_1 = StateObject(wrappedValue: viewModel_1)
_viewModel_2 = StateObject(wrappedValue: ViewModel_2(changeValue: viewModel_1 as! ChangeValue))
}
var body: some View {
VStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}
}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
Text("\(viewModel_1.someValue)")
}
}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
#State var count: Int = 0
var body: some View {
Button(action: {
count = count + 1
viewModel_2.changeValue_in_ViewModel_1(value: count)
}, label: {
Text("Tap to change value")
})
}
}
protocol ChangeValue {
func changeValue(value: Int)
}
class ViewModel_1: ObservableObject, ChangeValue {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
private let changeValue: ChangeValue
init (changeValue: ChangeValue) {
self.changeValue = changeValue
}
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
changeValue.changeValue(value: value)
}
}
We don't actually use view model objects in SwiftUI. The View struct is a view model already, being a value type it's more efficient and less error prone than an object but the property wrappers make it behave like an object, SwiftUI diffs the View struct and it creates/updates actual UIView/NSViews on screen for us. If you use actual view model objects you'll get bugs and face the problems that you are experiencing.
You can group related #State vars into their own struct and use mutating func for logic. That way it can be tested independently but the best thing is any chance to a property of the struct is detected by SwiftUI as a change to the whole struct which makes its dependency tracking super fast.
environmentObject is designed to hold a store object that contains the model structs (usually in arrays) in #Published properties. There isn't usually more than one environmentObject. This object is usually responsible for persisting or syncing the model data.

MVVM Passing data from view to another view's viewModel

I'm new to MVVM and i am trying to pass a location data from my ContenView to DetailsView's viewModel which is DetailsViewViewModel.
My Opening View -> ContentView (My data is here)
Second View -> DetailsView
Data must be reach -> DetailsViewViewModel
Here is my sheet in ContentView
.sheet(item: $viewModel.selectedPlace) { place in
DetailsView(location: place) { newLocation in
viewModel.updateLocation(location: newLocation)
}
I know i'm trying to send my data to details view and it's wrong. It was like that before i convert the architecture to the MVVM and this is the only place that i couldn't convert.
Also here is my DetailsViewViewModel
extension DetailsView {
#MainActor class DetailsViewViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, failed
}
var location: Location
#Published var name: String
#Published var description: String
#Published var loadingState = LoadingState.loading
#Published var pages = [Page]()
init() {
self.location = // ??? how should i initialize?
self.name = location.name
self.description = location.description
}
What is the proper way to this. Using another views data in another views viewmodel.
Let me try to put in an example that uses the convenience of #EnvironmentObject:
Your view model is a class that conforms to ObservableObject, so you can have those nice variables #Published that change the state of the views.
Your main view - or also your App - must "own" the view model, meaning it needs to create the one and only instance of your view model that will be used by all views.
You pass the view model from one view to another using #StateObject and #ObservableObject, but in this example I prefer to use another approach. Make your main view inject the instance of your view model in the environment, so all other views will read from that. The main view uses .environmentObject(viewModel) to do that.
The other views read the view model from the environment by calling #EnvironmentObject. They create a variable specifying only the type - there can only be one instance per type in the environment.
This is the way with which all view will read from the same model. See below a functioning example:
Step 1:
class MyViewModel: ObservableObject {
#Published private(set) var color: Color = .blue
#Published var showSheet = false
func changeColorTo(_ color: Color) {
self.color = color
}
}
Steps 2 and 3:
struct Example: View {
#StateObject private var viewModel = MyViewModel() // Here is the step (2)
var body: some View {
OneView()
.environmentObject(viewModel) // Here is the step (3)
}
}
Step 4 in two different views:
struct OneView: View {
#EnvironmentObject var viewModel: MyViewModel // Here is step (4)
var body: some View {
VStack {
Text("Current color")
.padding()
.background(viewModel.color)
Button {
if viewModel.color == .blue {
viewModel.changeColorTo(.yellow)
} else {
viewModel.changeColorTo(.blue)
}
} label: {
Text("Change color")
}
Button {
viewModel.showSheet.toggle()
} label: {
Text("Now, show a sheet")
}
.padding()
}
.sheet(isPresented: $viewModel.showSheet) {
DetailView()
}
}
}
struct DetailView: View {
#EnvironmentObject var viewModel: MyViewModel // Here is step (4)
var body: some View {
VStack {
Text("The sheet is showing")
.padding()
Button {
viewModel.showSheet.toggle()
} label: {
Text("Now, stop showing the sheet")
}
}
}
}
since location data is your business layer data, you need a use-case to provide it to both view models, and to optimize it caching the response is the way to go.
-ViewModel is responsible to hold the latest view states and data
-The domain layer is responsible to handle business logic
-The data layer (networking, cache, persistence, or in-memory) is responsible for providing the most efficient data storage/retrieval solutions
So, if you are okay with these defenitions and think of writing test for these view models you know that it is not right to inject data from another ViewModel because you would not test that view model on making sure it passes the data to the next viewModel and it is not its responsibility, but you write many tests for you data layer to make sure service calls and caching systems are working properly.
#StateObject var viewModel = ViewModel()
struct ParentView: View {
var body: some View {
Button(action: {
}, label: {
Text("btn")
})
.sheet(item: $viewModel.selectedPlace) { place in
DetailView(name: place.name,
location: place.location,
description: place.description)
}
}
}
struct DetailView: View {
var name: String
var location: String
var description: String
var body: some View {
VStack {
Text(name)
Text(location)
Text(description)
}
}
}
You need to initialise DetailsViewModel from ContentView sheet when you are adding the DetailsView like below:
ContentView
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
Text("Hello, world!")
.sheet(item: $vm.selectedPlace,
onDismiss: didDismiss) {newLocation in
//Here Initialise the DetailViewModel with a location
DetailsView(detailsVM: DetailsViewModel(location: newLocation))
}
}
func didDismiss(){
}
}
DetailsView:
struct DetailsView: View {
#StateObject var detailsVM : DetailsViewModel
var body: some View {
Text("This is the DetailesView")
}
}
DetailsViewModel:
class DetailsViewModel:ObservableObject{
#Published var location:Location
init(location:Location){
self.location = location
}
}

SwiftUI MVVM: child view model re-initialized when parent view updated

I'm attempting to use MVVM in a SwiftUI app, however it appears that view models for child views (e.g. ones in a NavigationLink) are re-initialized whenever an ObservableObject that's observed by both the parent and child is updated. This causes the child's local state to be reset, network data to be reloaded, etc.
I'm guessing it's because this causes parent's body to be reevaluated, which contains a constructor to SubView's view model, but I haven't been able to find an alternative that lets me create view models that don't live beyond the life of the view. I need to be able to pass data to the child view model from the parent.
Here's a very simplified playground of what we're trying to accomplish, where incrementing EnvCounter.counter resets SubView.counter.
import SwiftUI
import PlaygroundSupport
class EnvCounter: ObservableObject {
#Published var counter = 0
}
struct ContentView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: .init())
}
.environmentObject(envCounter)
}
}
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Sub view")
Button(action: { self.viewModel.counter += 1 }) {
Text("SubView counter is at \(self.viewModel.counter)")
}
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
A new property wrapper is added to SwiftUI in Xcode 12, #StateObject. You should be able to fix it by simply changing #ObservedObject for #StateObject as follows.
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#StateObject var viewModel: ViewModel // change on this line
var body: some View {
// ...
}
}
To solve this problem I created a custom helper class called ViewModelProvider.
The provider takes a hash for your view, and a method that builds the ViewModel. It then either returns the ViewModel, or builds it if its the first time that it received that hash.
As long as you make sure the hash stays the same as long as you want the same ViewModel, this solves the problem.
class ViewModelProvider {
private static var viewModelStore = [String:Any]()
static func makeViewModel<VM>(forHash hash: String, usingBuilder builder: () -> VM) -> VM {
if let vm = viewModelStore[hash] as? VM {
return vm
} else {
let vm = builder()
viewModelStore[hash] = vm
return vm
}
}
}
Then in your View, you can use the ViewModel:
Struct MyView: View {
#ObservedObject var viewModel: MyViewModel
public init(thisParameterDoesntChangeVM: String, thisParameterChangesVM: String) {
self.viewModel = ViewModelProvider.makeViewModel(forHash: thisParameterChangesVM) {
MOFOnboardingFlowViewModel(
pages: pages,
baseStyleConfig: style,
buttonConfig: buttonConfig,
onFinish: onFinish
)
}
}
}
In this example, there are two parameters. Only thisParameterChangesVM is used in the hash. This means that even if thisParameterDoesntChangeVM changes and the View is rebuilt, the view model stays the same.
I was having the same problem, your guesses are right, SwiftUI computes all your parent body every time its state changes. The solution is moving the child ViewModel init to the parent's ViewModel, this is the code from your example:
class EnvCounter: ObservableObject {
#Published var counter = 0
#Published var subViewViewModel = SubView.ViewModel.init()
}
struct CounterView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: envCounter.subViewViewModel)
}
.environmentObject(envCounter)
}
}

SwiftUI List Rows Not Refreshed After Updating Binding In Other Screen

I have a simple watchOS SwiftUI Application. The application has three screens. The first screen consists of a List of items. When you press that item, it will redirect to another screen & when you tap a button there it will open up a .sheet View which allows you to edit the item in the list.
The first view looks like this:
class Object: NSObject {
var title: String
init(title: String) {
self.title = title
}
}
struct Row: View {
#Binding var object: Object
var body: some View {
Text(self.object.title)
}
}
struct ContentView: View {
#State private var objects = [Object]()
var body: some View {
NavigationView {
List {
ForEach(objects.indices, id: \.self) { idx in
NavigationLink(destination: SecondView(object: self.$objects[idx])) {
Row(object: self.$objects[idx])
}
}
}
}
.onAppear {
self.objects = [
Object(title: "Test 1"),
Object(title: "Test 2")
]
}
}
}
These are the second & third views:
struct SecondView: View {
#Binding var object: Object
#State private var showPicker: Bool = false
var body: some View {
VStack {
Text(object.title)
Button(action: {
self.showPicker.toggle()
}) {
Text("Press Here")
}
}
.sheet(isPresented: $showPicker) {
ThirdView(object: self.$object)
}
}
}
struct ThirdView: View {
#Binding var object: Object
var body: some View {
VStack {
Text(object.title)
Button(action: {
self.update()
}, label: {
Text("Tap here")
})
}
}
func update() {
let newObj = self.object
newObj.title = "Hello, World!"
self.object = newObj
}
}
I'd expect, whenever I tap the button in the third view, the Binding (and thus the State) get's updated with "Hello, World". However, that is not the case, although not immediately.
What I currently see happening is that when I tap the button in the third view, the Text in that view does not get updated. When I dismiss the third view and go back to the second view, I do see "Hello, World". But when I go back to the list, the row still has the old value.
One other thing I noticed is that, when I fill the array of objects directly, like so:
#State private var objects = [Object(title: "Test 1"), Object(title: "Test 2")]
and remove the filling of the array in .onAppear, this work totally how I'd expect it to (everything updates immediately to "Hello, World".
Does anyone one know what I'm doing wrong here or did I might hit a bug?
Thanks!
Complex objects need to be classes conforming to #ObservableObject.
Observed ivars need to be published.
class Object: ObservableObject {
#Published var title: String
[...]
}
Observing views would use them as #ObservedObject
struct Row: View {
#ObservedObject var object: Object
[...]
}
You might have to create an object wrapper for your lists
class ObjectList: ObservableObject {
#Published var objects: [Object]
[...]
}

Resources