Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Consume @Published values on the main thread?

Is there a way to specify that count should only publish on the main thread? I've seen docs that talk about setting up your Publisher using receive(on:), but in this case the @Publisher wrapper hides that logic.

import SwiftUI
import Combine

class MyCounter: ObservableObject {
  @Published var count = 0

  public static let shared = MyCounter()
  
  private init() { }
}

struct ContentView: View {
    @ObservedObject var state = MyCounter.shared
    var body: some View {
        return VStack {
            Text("Current count: \(state.count)")
            Button(action: increment) {
                HStack(alignment: .center) {
                    Text("Increment")
                        .foregroundColor(Color.white)
                        .bold()
                }
            }
        }
    }
    
    private func increment() {
        NetworkUtils.count()
    }
}

public class NetworkUtils {

    public static func count() {
        guard let url = URL.parse("https://www.example.com/counter") else {
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let response = response as? HTTPURLResponse {
                let statusCode = response.statusCode
                if statusCode >= 200 && statusCode < 300 {
                    do {
                        guard let responseData = data else {
                            return
                        }
                        
                        guard let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else {
                            return
                        }
                        
                        if let newCount = json["new_count"] as? Int{
                            MyCounter.shared.count = newCount
                        }
                    } catch  {
                        print("Caught error")
                    }
                }
            } 
        }
        task.resume()
    }   
}

As you can see from my updated example, This is a simple SwiftUI view that has a button that when clicked makes a network call. The network call is asynchronous. When the network call returns, the ObservableObject MyCounter is updated on a background thread. I would like to know if there is a way to make the ObservableObject publish the change on the main thread. The only way I know to accomplish this now is to wrap the update logic in the network call closure like this:

DispatchQueue.main.async {
    MyCounter.shared.count = newCount
}
like image 903
TALE Avatar asked Nov 26 '19 22:11

TALE


2 Answers

Instead of using URLSession.shared.dataTask(with: request), you can use URLSession.shared.dataTaskPublisher(for: request) (docs) which will allow you to create a Combine pipeline. Then you can chain the receive(on:) operator as part of your pipeline.

URLSession.shared.dataTaskPublisher(for: request)
  .map { response in ... }
  ...
  .receive(on: RunLoop.main)
  ...

Also check out heckj's examples, I've found them to be very useful.

like image 89
Gil Birman Avatar answered Oct 31 '22 07:10

Gil Birman


If you try to set value marked @Published from a background thread you will see this error:

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive

So you have to make sure anywhere you set the value that it this done on the main thread, the values will always be published on the main thread.

like image 30
LuLuGaGa Avatar answered Oct 31 '22 06:10

LuLuGaGa