View refreshing not triggered when ObservableObject is inherited in SwiftUI - ios

ContentView2 view is not refreshed when model.value changes, if Model conforms to ObservableObject directly instead of inheriting SuperModel then it works fine
class SuperModel: ObservableObject {
}
class Model: SuperModel {
#Published var value = ""
}
struct ContentView2: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Text(model.value)
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
}
}
}

Here is working variant of your example. See that to be able to work, not only chaining the publishers is required, but at least one Published property. So or so, it could help in some scenario.
import SwiftUI
class SuperModel: ObservableObject {
// this is workaround but not real trouble.
// without any value in supermodel there is no real usage of SuperModel at all
#Published var superFlag = false
}
class Model: SuperModel {
#Published var value = ""
override init() {
super.init()
_ = self.objectWillChange.append(super.objectWillChange)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Text(model.value)
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
changing the code to
var body: some View {
VStack {
Text(model.value)
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
Text(model.superFlag.description)
Button("change super flag") {
self.model.superFlag.toggle()
}
}
}
you can see how to use even your supermodel at the same time

Use ObjectWillChange to solve the problem specified.
Here is the working code:
import SwiftUI
class SuperModel: ObservableObject {
}
class Model: SuperModel {
var value: String = "" {
willSet { self.objectWillChange.send() }
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Text("Model Value1: \(model.value)")
Button("change value") {
self.model.value = "\(Int.random(in: 1...10))"
}
Text("Model Value2: \(model.value)")
}
}
}

This really looks like heavy defect.
class SuperModel: ObservableObject {
}
class Model: SuperModel {
#Published var value = ""
}
as I see the value is changed and keep new one as expected, but DynamicProperty feature does not work
The following variant works for me (Xcode 11.2 / iOS 13.2)
class SuperModel: ObservableObject {
#Published private var stub = "" // << required !!!
}
class Model: SuperModel {
#Published var value = "" {
willSet { self.objectWillChange.send() } // < works only if above
}
}
Also such case is possible for consideration:
class SuperModel {
}
class Model: SuperModel, ObservableObject {
#Published var value = ""
}

Related

Why does SwiftUI not update a view here?

The background color and text in ReadyDashboardView below don't update when isConnected is updated. Obviously I want to update the view when the connection is made. I was expecting that publishing everything in the chain to that variable would make it update instantly in swiftui. Instead, it's always rendering using the value provided when the variable is instantiated. Here's a very simplified look at the situation:
Is there another swiftui feature i should be using or am I going to have to make some sweeping changes to my codebase?
import SwiftUI
#main
struct TestApp: App {
#StateObject private var env = PrinterEnv()
var body: some Scene {
WindowGroup {
ReadyDashboardView()
.environmentObject(env)
}
}
}
struct ReadyDashboardView: View {
#EnvironmentObject var env: PrinterEnv
var body: some View {
VStack {
HStack {
Spacer()
VStack {
Text(env.selectedPrinter?.isConnected ?? false ? "Printer Ready" : "Not Connected")
.padding(.bottom)
}
Spacer()
}
.background(env.selectedPrinter?.isConnected ?? false ? .green : .red)
Button("Connect") { env.selectedPrinter?.isConnected = true }
Button("Disconnect") { env.selectedPrinter?.isConnected = false }
}
}
}
class PrinterEnv: ObservableObject {
#Published var configuredPrinters: [Printer] = []
#Published var selectedPrinter: Printer?
init() {
configuredPrinters.append(contentsOf: [Printer()])
selectedPrinter = configuredPrinters.first
}
}
class Printer: ObservableObject {
#Published var isConnected = false
}
I suggest you do not nest ObservableObject, it does not work very well.
Try a Printer struct for example, such as:
struct Printer: Identifiable {
let id = UUID()
var isConnected = false
}
I do not think the problem is specifically related to nested observable objects. Nesting them is working fine and is even recommended by community members as the best way to manage app wide states and ensure performance is acceptable.
See this: https://www.fivestars.blog/articles/app-state/
That said, I believe it's probably related to the wrapping of the observable object with #Published property wrapper.
Try this in a playground:
import SwiftUI
class AppState: ObservableObject {
let fooState = FooState()
let barState = BarState()
}
class FooState: ObservableObject {
#Published var foo: Int = 42
}
class BarState: ObservableObject {
#Published var bar: String = Date().debugDescription
}
struct FooView: View {
#EnvironmentObject var fooState: FooState
var body: some View {
Text("foo: \(fooState.foo)")
}
}
struct BarView: View {
#EnvironmentObject var barState: BarState
var body: some View {
Text("bar: \(barState.bar)")
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Spacer()
FooView()
.environmentObject(appState.fooState)
Button("update foo") {
appState.fooState.foo = Int.random(in: 1...100)
}
Spacer()
BarView()
.environmentObject(appState.barState)
Button("update bar") {
appState.barState.bar = Date().debugDescription
}
Spacer()
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(
rootView: ContentView()
.frame(width: 320, height: 414)
.environmentObject(AppState())
)

SwiftUI Bind to #ObservableObject in array

How do I pass a bindable object into a view inside a ForEach loop?
Minimum reproducible code below.
class Person: Identifiable, ObservableObject {
let id: UUID = UUID()
#Published var healthy: Bool = true
}
class GroupOfPeople {
let people: [Person] = [Person(), Person(), Person()]
}
public struct GroupListView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
//MARK: Other properties
let group: GroupOfPeople = GroupOfPeople()
//MARK: Body
public var body: some View {
ForEach(group.people) { person in
//ERROR: Cannot find '$person' in scope
PersonView(person: $person)
}
}
//MARK: Init
}
public struct PersonView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
#Binding var person: Person
//MARK: Other properties
//MARK: Body
public var body: some View {
switch person.healthy {
case true:
Text("Healthy")
case false:
Text("Not Healthy")
}
}
//MARK: Init
init(person: Binding<Person>) {
self._person = person
}
}
The error I get is Cannot find '$person' in scope. I understand that the #Binding part of the variable is not in scope while the ForEach loop is executing. I'm looking for advice on a different pattern to accomplish #Binding objects to views in a List in SwiftUI.
The SwiftUI way would be something like this:
// struct instead of class
struct Person: Identifiable {
let id: UUID = UUID()
var healthy: Bool = true
}
// class publishing an array of Person
class GroupOfPeople: ObservableObject {
#Published var people: [Person] = [
Person(), Person(), Person()
]
}
struct GroupListView: View {
// instantiating the class
#StateObject var group: GroupOfPeople = GroupOfPeople()
var body: some View {
List {
// now you can use the $ init of ForEach
ForEach($group.people) { $person in
PersonView(person: $person)
}
}
}
}
struct PersonView: View {
#Binding var person: Person
var body: some View {
HStack {
// ternary instead of switch
Text(person.healthy ? "Healthy" : "Not Healthy")
Spacer()
// Button to change, so Binding makes some sense :)
Button("change") {
person.healthy.toggle()
}
}
}
}
You don't need Binding. You need ObservedObject.
for anyone still wondering... it looks like this has been added
.onContinuousHover(perform: { phase in
switch phase {
case .active(let location):
print(location.x)
case .ended:
print("ended")
}
})

SwiftUI view not getting updated on changing #ObservedObject

Here is a simple MVVM based TestView:
import SwiftUI
public struct Test: View {
#ObservedObject public var viewModel = TestViewModel()
public init() {
}
public var body: some View {
VStack {
Text(viewModel.model.stri)
Button(action: {
self.viewModel.change()
}) {
Text("Change")
}
}.padding(50)
}
}
public class TestModel {
#Published public var condition: Bool = false
#Published var stri = "Random numbers"
}
public class TestViewModel: ObservableObject {
#Published var model = TestModel()
func change() {
model.condition.toggle()
model.stri = "\(Int.random(in: 1...10))"
}
}
The view does not get updated when the model is updated from inside the view model.
The text should finally produce some random number between 1 and 10. Please let me know where I am going wrong.
It is because your Test view observes viewModel but not viewModel.model which is not changed in your scenario because it is a reference-type
The following is a solution
func change() {
model.condition.toggle()
model.stri = "\(Int.random(in: 1...10))"
self.objectWillChange.send() // << this one !!
}

How to run a method in ContentView when an ObservedObject changes in other class [Swift 5 iOS 13.4]

Here is my basic ContentView
struct ContentView: View
{
#ObservedObject var model = Model()
init(model: Model)
{
self.model = model
}
// How to observe model.networkInfo's value over here and run "runThis()" whenever the value changes?
func runThis()
{
// Function that I want to run
}
var body: some View
{
VStack
{
// Some widgets here
}
}
}
}
Here is my model
class Model: ObservableObject
{
#Published var networkInfo: String
{
didSet
{
// How to access ContentView and run "runThis" method from there?
}
}
}
I'm not sure if it is accessible ? Or if I can observe ObservableObject changes from View and run any methods?
Thanks in advance!
There are a number of ways to do this. If you want to runThis() when the
networkInfo changes then you could use something like this:
class Model: ObservableObject {
#Published var networkInfo: String = ""
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button(action: {
self.model.networkInfo = "test"
}) {
Text("change networkInfo")
}
}.onReceive(model.$networkInfo) { _ in self.runThis() }
}
func runThis() {
print("-------> runThis")
}
}
another global way is this:
class Model: ObservableObject {
#Published var networkInfo: String = "" {
didSet {
NotificationCenter.default.post(name: NSNotification.Name("runThis"), object: nil)
}
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
Button(action: {
self.model.networkInfo = "test"
}) {
Text("change networkInfo")
}
}.onReceive(
NotificationCenter.default.publisher(for: NSNotification.Name("runThis"))) { _ in
self.runThis()
}
}
func runThis() {
print("-------> runThis")
}
}

For a List-Detail interface - Data is updated in the Detail View, and the Data is changed but not immediately reflected in the Detail view

I am using SwiftUI on the Apple Watch and trying to use #ObservableObject, #ObservedObject, and #Binding correctly. I'm updating a value in a DetailView, and I want to have it reflected locally, as well as have the data changed globally. The code below works, but I am using a kludge to force the DetailView to redraw itself:
Is there a better way?
-------------- ContentView.swift ---------------
import Combine
import SwiftUI
struct person: Identifiable {
var id:Int = 0
var name:String
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
class AppData: ObservableObject {
#Published var people:[person] = [person(id:0, name:"John"),
person(id:1, name:"Bret"),
person(id:2,name:"Sue"),
person(id:3,name:"Amy")]
}
var gAppData = AppData()
struct ContentView: View {
#ObservedObject var model:AppData
var body: some View {
List( model.people.indices ){ index in
NavigationLink(destination: DetailView(person:self.$model.people[index])) { Text(self.model.people[index].name) }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(model:gAppData)
}
}
-------------- DetailView.swift ---------------
import SwiftUI
struct DetailView: View {
#Binding var person: person
// Created an unnecessary var to force a redreaw of the view
#State var doRedraw:Bool = true
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob"; self.doRedraw = false }) {
Text("Set Name to Bob")
}
}
}
}
struct DestView_Previews: PreviewProvider {
static var previews: some View {
DetailView(person:.constant(person( id:0, name:"John"))) // what does ".constant" actually do?
}
}
The problem here is because your view redraws only when you changes the #State or #Binding variable. Here you do not change the Person variable, but its property, which should not affect the user interface (because you didn't say to do this). I changed your code for a little for showing how to achieve this effect, you can go ahead from this point. You need to remember, what exactly affect UI:
class Person: Identifiable, ObservableObject { // better to assign struct/class names using UpperCamelCase
#Published var name:String // now change of this variable will affect UI
var id:Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
// changes in DetailView
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob" }) {
Text("Set Name to Bob")
}
}
}
}
// preview
struct DetailViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(Person(id: 1, name: "John"))
}
}
update: full code for List and Detail
import SwiftUI
class Person: Identifiable, ObservableObject { // better to assign type names using UpperCamelCase
#Published var name: String //{
var id: Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
func changeName(_ newName: String) {
self.name = newName
}
}
class AppData: ObservableObject {
#Published var people: [Person] = [Person(id:0, name:"John"),
Person(id:1, name:"Bret"),
Person(id:2,name:"Sue"),
Person(id:3,name:"Amy")]
}
struct ContentViewWithoutGlobalVar: View {
#EnvironmentObject var model: AppData
var body: some View {
NavigationView { // you forget something to navigate between views
List(model.people.indices) { index in
NavigationLink(destination: DetailView()
.environmentObject(self.model.people[index])) {
PersonRow(person: self.$model.people[index])
}
}
}
}
}
struct PersonRow: View {
#Binding var person: Person // this struct will see changes in Person and show them
var body: some View {
Text(person.name)
}
}
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(self.person.name)
Button(action:{ self.person.changeName("Bob") }) {
Text("Set Name to Bob")
}
}
}
}
struct ContentViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentViewWithoutGlobalVar()
.environmentObject(AppData())
DetailView()
.environmentObject(Person(id: 0, name: "John"))
}
}
}

Resources