@Published Property Not Triggering Anything

@Published property not triggering anything

Try Observing you model

@StateObject var myModel = MyModel()

Without a Minimal Reproducible Example we are going in pieces here.

You aren't observing title and value either. put an @Published on them and then observe them in their own View

protocol MyProtocol:ObservableObject {
var codeDetected:CodeDetected { get set }
//var codeDetectedPublished: Published<CodeDetected> { get }
//var codeDetectedPublisher: Published<CodeDetected>.Publisher { get }
}

class CodeDetected:ObservableObject {
@Published var title: String
@Published var value:String

init(title:String = "init", value:String = "init"){
self.title = title
self.value = value
}
}
class MyModel: MyProtocol {

@Published var codeDetected:CodeDetected = CodeDetected()
//var codeDetectedPublished: Published<CodeDetected> { _codeDetected}
//var codeDetectedPublisher: Published<CodeDetected>.Publisher { $codeDetected }
}
struct CodeDetectedView: View {
@StateObject var myModel = MyModel()
var body: some View {
VStack{
//Can't see updates here unless you ask for them/tell the parent model there are changes
Text(myModel.codeDetected.title)
.onReceive(myModel.codeDetected.$title, perform: { _ in
myModel.objectWillChange.send()
})
Text(myModel.codeDetected.value)
.onReceive(myModel.codeDetected.$value, perform: { _ in
myModel.objectWillChange.send()
})
CodeDetectedSubView(codeDetected: myModel.codeDetected)
}
}
}
struct CodeDetectedSubView: View {
@ObservedObject var codeDetected: CodeDetected
var body: some View {
//Can see here
Text(codeDetected.title)
Text(codeDetected.value)
TextField("title", text: $codeDetected.title)
TextField("value", text: $codeDetected.value)
}
}

Published property not updating in SwiftUI

for loop will block the main queue so create a queue to run for i in 1...1000000 on another thread

    //
// ContentView.swift
// StackOverFlow
//
// Created by Mustafa T Mohammed on 12/31/21.
//

import SwiftUI

public class ViewSearch {
@ObservedObject var viewModel: ViewModel

/// Main initializer for instance.
/// - Parameter viewModel: The view model for searching.
public init(viewModel: ViewModel) {
self.viewModel = viewModel
}
func doSomething() {
let q = DispatchQueue.init(label: "doSomething")
// for loop will block the main queue so create a queue to run for i in 1...1000000
// on another thread
q.async { [weak self] in // weak self to prevent retain cycle
guard let self = self else { return }
for i in 1...1000000 {
if i % 250000 == 0 {
DispatchQueue.main.async { //update UI by coming back to main thread
self.viewModel.label = "value: \(i)"
}
}
}
DispatchQueue.main.async { //update UI by coming back to main thread
self.viewModel.label = "Done!"
}
}
}
}

public class ViewModel: ObservableObject {
@Published public var label = "initial value" {
didSet {
print("\(label)")
self.objectWillChange.send()
}
}
@Published public var searchText = ""

var search: ViewSearch? = nil
}

struct ContentView: View {
@ObservedObject var model: ViewModel

var body: some View {
TextField("Search", text: $model.searchText) { isEditing in
if isEditing {
model.label = "initial value"
}
} onCommit: {
if !model.searchText.isEmpty {
model.search = ViewSearch(viewModel: model)
model.search?.doSomething()
}
}

Text(model.label)
}
}

SwiftUI trigger function when model updates inside @Published property

Firstly - simplifying this code. Most of this code is unnecessary to reproduce the problem, and can't even be compiled. The code below is not the working code, but rather what we have to start and change later:

ContentView

struct ContentView: View {
var body: some View {
NavigationView {
PackagesView()
}
}
}

PackageService

class PackageService: ObservableObject {
let package: Package

init(package: Package) {
self.package = package
}

// Get placemarks from locations
func getPlacemarks() {
print("getPlacements called")
}
}

PackagesViewModel

class PackagesViewModel: ObservableObject {
@Published var results = [Package]()
}

PackagesView

struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()

var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(new)
}

Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}

packagesViewModel.results[randomIndex].title = "new title (\(Int.random(in: 1 ... 100)))"
}

List(packagesViewModel.results, id: \.self) { package in
NavigationLink(destination: PackageView(packageService: PackageService(package: package))) {
Text(package.title)
}
}
}
}
}

PackageView

struct PackageView: View {
@ObservedObject var packageService: PackageService

var body: some View {
VStack {
Text("Title: \(packageService.package.title)")

Text("Description: \(packageService.package.description)")
}
}
}

Package

struct Package: Identifiable, Hashable {
let id = UUID()
var title: String
let description: String
}

Now, solving the problem. I fixed this issue by detecting the results changing with onChange(of:perform:). However, from here there is no way to access the PackageServices used in the view body.

To prevent this issue, the PackageServices are actually stored in PackagesViewModel, which logically makes more sense for the data flow. Now with PackageService also being a struct so the @Published works on the array for results, this now works.

See the code below:

PackageService (updated)

struct PackageService: Hashable {
var package: Package

init(package: Package) {
self.package = package
}

// Get placemarks from locations
mutating func getPlacemarks() {
print("getPlacements called")

// This function is mutating, feel free to set any properties in here
}
}

PackagesViewModel (updated)

class PackagesViewModel: ObservableObject {
@Published var results = [PackageService]()
}

PackagesView (updated)

struct PackagesView: View {
@StateObject var packagesViewModel = PackagesViewModel()

var body: some View {
VStack {
Button("Add new package") {
let number = packagesViewModel.results.count + 1
let new = Package(title: "title \(number)", description: "description \(number)")
packagesViewModel.results.append(PackageService(package: new))
}

Button("Change random title") {
guard let randomIndex = packagesViewModel.results.indices.randomElement() else {
return
}

let newTitle = "new title (\(Int.random(in: 1 ... 100)))"
packagesViewModel.results[randomIndex].package.title = newTitle
}

List($packagesViewModel.results, id: \.self) { $packageService in
NavigationLink(destination: PackageView(packageService: $packageService)) {
Text(packageService.package.title)
}
}
}
.onChange(of: packagesViewModel.results) { _ in
for packageService in $packagesViewModel.results {
packageService.wrappedValue.getPlacemarks()
}
}
}
}

PackageView (updated)

struct PackageView: View {
@Binding var packageService: PackageService

var body: some View {
VStack {
Text("Title: \(packageService.package.title)")

Text("Description: \(packageService.package.description)")
}
}
}

@Published property not updating view from nested view model - SwiftUI

Alternate solution is to use separated view for your sub-model:

struct FilterView: View {
@ObservedObject var filterVM: FilterViewModel

var body: some View {
Button(action: { filterVM.changeSelected()}, label: {
Text(filterVM.name)
.background(Color(filterVM.backgroundColour))
})
}
}

so in parent view now we can just use it as

struct ContentView: View {

@ObservedObject var filterListVM = FilterListViewModel()

// ...

FilterView(filterVM: filterListVM[0])
}

@Published property wrapper not working on subclass of ObservableObject

Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.

NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>() on the subclass as that will again cause the state not to update properly.

I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.

Here is a fully working example:

    import SwiftUI

class MyTestObject: ObservableObject {
@Published var aString: String = ""

}

class MyInheritedObject: MyTestObject {
// Using @Published doesn't work on a subclass
// @Published var anotherString: String = ""

// If you add the following to the subclass updating the state also doesn't work properly
// let objectWillChange = PassthroughSubject<Void, Never>()

// But if you update the value you want to maintain state
// of using the objectWillChange.send() method provided by the
// baseclass the state gets updated properly... Jaayy!
var anotherString: String = "" {
willSet { self.objectWillChange.send() }
}
}

struct MyTestView: View {
@ObservedObject var myTestObject = MyTestObject()
@ObservedObject var myInheritedObject = MyInheritedObject()

var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")

TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}


Related Topics



Leave a reply



Submit