1

I have the following code where I expect updates to the Published property label to be reflected in the UI but it is not.

  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()  {
    for i in 1...1000000 {
      if i % 250000  == 0 {
        viewModel.label = "value: \(i)"
      }
    }
    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)
  }
}

Updates are triggered on the commit of the textfield input. I expect the UI to show "value: 250000", "value: 500000", etc. The didSet observer shows the change but the UI is not updated. Why not?

Phantom59
  • 947
  • 1
  • 8
  • 19

1 Answers1

1

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)
    }
}
  • Nice ... I was halfway there having tried the DispatchQueue.main but if the for-loop is still running on the Main queue I suppose my use of DispatchQueue.main did nothing. – Phantom59 Jan 01 '22 at 00:34