SwiftUI body not reevalutated after binding changed - swift

A bad case of SwiftUI magic not working for me, and I am loosing my sanity here. Why is the text not updating its value here? Why is the body not reevaluated after each increment() call?
class ReadingStateVM: ObservableObject {
#Published var value = 0
func increment() {
value = value + 1
print("value \(value)")
}
}
struct ReadingStateView: View {
var viewModel = ReadingStateVM()
var body: some View {
Text("State \(viewModel.value)")
.onTapGesture {
self.viewModel.increment()
}
}
}

You need to add the #ObservedObject property wrapper so when changes happen to your view model, the view will also update.
#ObservedObject var viewModel = ReadingStateVM()

In SwiftUI we use structs and mutating funcs for data, not objects. This is because SwiftUI takes advantage of value-semantics for tracking changes. To learn this watch WWDC 2020 Data Essentials in SwiftUI at 3:58. With this in mind, your code should now look like this:
struct ContentViewConfig {
var counter = 0
var anotherRelatedVar = 0
mutating func increment() {
counter = counter + 1
print("counter: \(counter)")
}
}
struct ContentView: View {
#State var config = ContentViewConfig()
var body: some View {
Button("State \(config.counter)"){
config.increment()
}
}
}
Furthermore, ObservableObject is part of the Combine framework so you usually only use it when you want to use assign to set the result of a Combine pipeline to an #Published var. And as the other answer states, this object needs to be declared with #ObservedObject for the body to be called when an #Published property changes.

Related

MVVM model in SwiftUI

I want to separate view from view model according to MVVM. How would I create a model in SwiftUI? I read that one should use struct rather than class.
As an example I have a model for a park where you can plant trees in:
// View Model
struct Park {
var numberOfTrees = 0
func plantTree() {
numberOfTrees += 1 // Cannot assign to property: 'self' is immutable
}
}
// View
struct ParkView: View {
var park: Park
var body: some View {
// …
}
}
Read things about #State in such things, that make structs somewhat mutable, so I tried:
struct Park {
#State var numberOfTrees = 0 // Enum 'State' cannot be used as an attribute
func plantTree() {
numberOfTrees += 1 // Cannot assign to property: 'self' is immutable
}
}
I did use #State successfully directly in a View. This doesn’t help with separating the view model code though.
I could use class:
class Park: ObservableObject {
var numberOfTrees = 0
func plantTree() {
numberOfTrees += 1
}
}
…but then I would have trouble using this view model nested in another one, say City:
struct City {
#ObservedObject var centerPark: Park
}
Changes in centerPark wouldn’t be published as Park now is reference type (at least not in my tests or here). Also, I would like to know how you solve this using a struct.
as a starting point:
// Model
struct Park {
var numberOfTrees = 0
mutating func plantTree() { // `mutating`gets rid of your error
numberOfTrees += 1
}
}
// View Model
class CityVM: ObservableObject {
#Published var park = Park() // creates a Park and publishes it to the views
// ... other #Published things ...
// Intents:
func plantTree() {
park.plantTree()
}
}
// View
struct ParkView: View {
// create the ViewModel, which creates the model(s)
// usually you would do this in the App struct and make available to all views by .environmentObject
#StateObject var city = CityVM()
var body: some View {
VStack {
Text("My city has \(city.park.numberOfTrees) trees.")
Button("Plant one more") {
city.plantTree()
}
}
}
}
mutating func is the fix but I thought I'd include some other info below:
We don't use MVVM with SwiftUI because we don't use classes for transient view state and we don't control the View in the MVVM/MVC sense. SwiftUI creates and updates the real view objects automatically for us, i.e. UILabels, UITableView etc. The SwiftUI View structs are essentially the view model already, so if you were to recreate that as an object not only will you be needlessly make your code more complex but also would introduce object reference bugs SwiftUI is trying to eliminate by using structs. With property wrappers like #State and #Binding SwiftUI is doing some magic to make the struct behave like an object it is not a good idea to ignore that. To make your View structs more testable you can extract related vars into a struct and use mutating funcs like this:
// View Model
struct ParkConfig {
var numberOfTrees = 0
mutating func plantTree() {
numberOfTrees += 1
}
}
struct ContentView {
#State var parkConfig = ParkConfig()
var body: some View {
ParkView(config: $parkConfig)
}
}
// View
struct ParkView: View {
#Binding var config: ParkConfig
var body: some View {
Button("Click Me") {
config.plantTree()
}
}
}
You can see Apple demonstrate this pattern in Data Essentials in SwiftUI WWDC 2020 at 4:18 where he says "EditorConfig can maintain invariants on its properties and be tested independently. And because EditorConfig is a value type, any change to a property of EditorConfig, like its progress, is visible as a change to EditorConfig itself."

Publishing changes to a collection of observable objects

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

SwiftUI - Observable Object initiated from Swift class does not update #ObservedObject on ContentView()

The ObservableObject class is being instantiated from both the ContentView() as well as another Swift class. When a function of the ObservableObject class is run by the Swift class, it does not update the #ObservedObject of the ContentView().
I am aware that this is due to me instantiating the ObservableObject class twice. What is the best practice to utilise #ObservedObject when the Observable Class is not/cannot be instantiated by the ContentView().
I haven't found a way to make #EnvironmentObject work with Swift classes.
I could use a global variable and run a Timer() to check for changes to it. However, this feels like an ugly way to do it?!?
Please see example code below. Please run on a device, to see the print statement.
import SwiftUI
struct ContentView: View {
#ObservedObject var observedClass: ObservedClass = ObservedClass()
// The callingObservedClass does not exist on the ContentView, but is called
// somewhere in the app with no reference to the ContentView.
// It is included here to better showcase the issue.
let callingObservedClass: CallingObservedClass = CallingObservedClass()
var body: some View {
VStack {
// This Text shall be updated, when
// self.callingObservedClass.increaseObservedClassCount() has been executed.
Text(String(observedClass.count))
Button(action: {
// This updates the count-variable, but as callingObservedClass creates
// a new instance of ObservedClass, the Text(observedClass.count) is not updated.
self.callingObservedClass.increaseObservedClassCount()
}, label: {
Text("Increase")
})
}
}
}
class CallingObservedClass {
let observedClass = ObservedClass()
func increaseObservedClassCount() {
// Returning an Int here to better showcase that count is increased.
// But not in the ObservedClass instance of the ContentView, as the
// Text(observedClass.count) remains at 0.
let printCount = observedClass.increaseCount()
print(printCount)
}
}
class ObservedClass: ObservableObject {
#Published var count: Int = 0
func increaseCount() -> Int {
count = count + 1
return count
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Edit: I suppose my question is how do I get data from a Swift class and update a SwiftUI view when the data changes when I am unable to instantiate the Swift class from the SwiftUI view.
A possible solution to this is to chain the ObservableObject classes. Unfortunately, as of iOS 13.6 this does not work out of the box.
I found the answer via:
How to tell SwiftUI views to bind to nested ObservableObjects
Adjusted & functioning example:
// Add Combine
import Combine
import SwiftUI
struct ContentView: View {
#ObservedObject var callingObservedClass: CallingObservedClass = CallingObservedClass()
var body: some View {
VStack {
// Calling the chained ObservableObject
Text(String(callingObservedClass.observedClass.count))
Button(action: {
self.callingObservedClass.increaseObservedClassCount()
}, label: {
Text("Increase")
})
}
}
}
class CallingObservedClass: ObservableObject {
// Chaining Observable Objects
#Published var observedClass = ObservedClass()
// ObservableObject-chaining does not work out of the box.
// The anyCancellable variable with the below init() will do the trick.
// Thanks to https://stackoverflow.com/questions/58406287/how-to-tell-swiftui-views-to-bind-to-nested-observableobjects
var anyCancellable: AnyCancellable? = nil
init() {
anyCancellable = observedClass.objectWillChange.sink { (_) in
self.objectWillChange.send()
}
}
func increaseObservedClassCount() {
let printCount = observedClass.increaseCount()
print(printCount)
}
}
class ObservedClass: ObservableObject {
#Published var count: Int = 0
func increaseCount() -> Int {
count = count + 1
return count
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am interested on how to access Swift Class data and update an SwiftUI view if ObservableObject-Chaining is not an option.
Please answer below if you have a solution to this.

SwiftUI: ObservableObject does not persist its State over being redrawn

Problem
In Order to achieve a clean look and feel of the App's code, I create ViewModels for every View that contains logic.
A normal ViewModel looks a bit like this:
class SomeViewModel: ObservableObject {
#Published var state = 1
// Logic and calls of Business Logic goes here
}
and is used like so:
struct SomeView: View {
#ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
This workes fine when the Views Parent is not being updated. If the parent's state changes, this View gets redrawn (pretty normal in a declarative Framework). But also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).
In my opinion, the ViewModel should stay, or the State should persist.
If I replace the ViewModel with a #State Property and use the int (in this example) directly it stays persisted and does not get recreated:
struct SomeView: View {
#State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
This does obviously not work for more complex States. And if I set a class for #State (like the ViewModel) more and more Things are not working as expected.
Question
Is there a way of not recreating the ViewModel every time?
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
Why is #State keeping the State over the redraw?
I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.
Duplicate Question
I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.
Edit (adding more detailed Example)
When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a NavigationLink you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then "redraw" the NavigationLink, which would then lead to a recreation of the ViewModel.
I know I could store the ViewModel in the ParentView / ParentView's ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated - there might be some side effects.
Finally, there is a Solution provided by Apple: #StateObject.
By replacing #ObservedObject with #StateObject everything mentioned in my initial post is working.
Unfortunately, this is only available in ios 14+.
This is my Code from Xcode 12 Beta (Published June 23, 2020)
struct ContentView: View {
#State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
#Published var title = 0
}
struct TestView2: View {
#StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
As you can see, the StateObject Keeps it value upon the redraw of the Parent View, while the ObservedObject is being reset.
I agree with you, I think this is one of many major problems with SwiftUI. Here's what I find myself doing, as gross as it is.
struct MyView: View {
#State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
#ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
You can either construct the view model in place or pass it in, and it gets you a view that will maintain your ObservableObject across reconstruction.
Is there a way of not recreating the ViewModel every time?
Yes, keep ViewModel instance outside of SomeView and inject via constructor
struct SomeView: View {
#ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
No needs. #ObservedObject is-a already DynamicProperty similarly to #State
Why is #State keeping the State over the redraw?
Because it keeps its storage, ie. wrapped value, outside of view. (so, see first above again)
You need to provide custom PassThroughSubject in your ObservableObject class. Look at this code:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//#ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
First, I using TextChanger to pass new value of .text to .onReceive(...) in CustomState View. Note, that onReceive in this case gets PassthroughSubject, not the ObservableObjectPublisher. In last case you will have only Publisher.Output in perform: closure, not the NewValue. state.text in that case would have old value.
Second, look at the ComplexState class. I made an objectWillChange property to make text changes send notification to subscribers manually. Its almost the same like #Published wrapper do. But, when the text changing it will send both, and objectWillChange.send() and textChanged.send(newValue). This makes you be able to choose in exact View, how to react on state changing. If you want ordinary behavior, just put the state into #ObservedObject wrapper in CustomStateContainer View. Then, you will have all the views recreated and this section will get updated values too:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
If you don't want all of them to be recreated, just remove #ObservedObject. Ordinary text View will stop updating, but CustomState will. With no recreating.
update:
If you want more control, you can decide while changing the value, who do you want to inform about that change.
Check more complex code:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// #Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
#ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
#State private var text: String = ""
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
I made a manual Binding to stop broadcasting objectWillChange. But you still need to gets new value in all the places you changing this value to stay synchronized. Thats why I modified TextInput too.
Is that what you needed?
My solution is use EnvironmentObject and don't use ObservedObject at view it's viewModel will be reset, you pass through hierarchy by
.environmentObject(viewModel)
Just init viewModel somewhere it will not be reset(example root view).

didSet for a #Binding var in Swift

Normally we can use didSet in swift to monitor the updates of a variable. But it didn't work for a #Binding variable. For example, I have the following code:
#Binding var text {
didSet {
......
}
}
But the didSet is never been called.Any idea? Thanks.
Instead of didSet you can always use onReceive (iOS 13+) or onChange (iOS 14+):
import Combine
import SwiftUI
struct ContentView: View {
#State private var counter = 1
var body: some View {
ChildView(counter: $counter)
Button("Increment") {
counter += 1
}
}
}
struct ChildView: View {
#Binding var counter: Int
var body: some View {
Text(String(counter))
.onReceive(Just(counter)) { value in
print("onReceive: \(value)")
}
.onChange(of: counter) { value in
print("onChange: \(value)")
}
}
}
You shouldn’t need a didSet observer on a #Binding.
If you want a didSet because you want to compute something else for display when text changes, just compute it. For example, if you want to display the count of characters in text:
struct ContentView: View {
#Binding var text: String
var count: Int { text.count }
var body: some View {
VStack {
Text(text)
Text(“count: \(count)”)
}
}
}
If you want to observe text because you want to make some other change to your data model, then observing the change from your View is wrong. You should be observing the change from elsewhere in your model, or in a controller object, not from your View. Remember that your View is a value type, not a reference type. SwiftUI creates it when needed, and might store multiple copies of it, or no copies at all.
The best way is to wrap the property in an ObservableObject:
final class TextStore: ObservableObject {
#Published var text: String = "" {
didSet { ... }
}
}
And then use that ObservableObject's property as a binding variable in your view:
struct ContentView: View {
#ObservedObject var store = TextStore()
var body: some View {
TextField("", text: $store.text)
}
}
didSet will now be called whenever text changes.
Alternatively, you could create a sort of makeshift Binding value:
TextField("", text: Binding<String>(
get: {
return self.text
},
set: { newValue in
self.text = newValue
...
}
))
Just note that with this second strategy, the get function will be called every time the view is updated. I wouldn't recommend using this approach, but nevertheless it's good to be aware of it.