简体   繁体   中英

How to use closure to receive async data from a class with delegate in SwiftUI?

My basic requirement in to listen to a swift package's variable's value when connected to a sever and change a published value of same type inside view-model.

The code below is a sample code constructed to explain scenario. This code does not work properly as isConnected value is not updated when server is connected.

In the sample code, a Swift package called ClientPackage with its class and delegate protocol. Then there is ViewModel class which SwiftUI uses to update state of UI and there is a LocalClient class which implements ClientPackage library. Basically LocalClient is used as an abstract local package to make implementation on same easier inside ViewModel. (I'm not sure if this is the right approach for this.) So I need to change isConnected boolean in ViewModel when server is connected inside ClientPackage .

So I want to fixed this code in order to update isConencted value updated. The connected variable in LocalClient is set correctly from delegate method.

How can I make the closure in ViewModel.connect() work?

PS In the real app, I am using Starscream 4.0.4 swift package for ClientPackage .

// SocketClient.swift
// Xcode 12.3

import Foundation

class ViewModel: ObservableObject {
    
    var client: LocalClient?

    @Published var isConnected: Bool?
    
    let token = "12345678" // This is fetched async in real scenario
    
    init() {
        client = LocalClient(token: token)
    }
    
    func connect() {
        client?.connect() { connected in
            self.isConnected = connected
        }
    }
    
}

// Assuming code below is from a local swift package and can be modified

class LocalClient: NSObject {
    
    var coreClient: ClientPackage?
    var connected = false
    
    init(token: String) {
        super.init()
        coreClient = ClientPackage(token: token)
        coreClient!.delegate = self
    }
    
    func connect(_ completion: @escaping (Bool) -> Void) {
        coreClient?.connectToServer()
        completion(connected)
    }
}

extension LocalClient: ClientPackageDelegate {
    func connectedToServer(_ client: ClientPackage) {
        connected = true
        print("Connected to server")
    }
    
    func disconnectedFromServer(_ client: ClientPackage) {
        connected = false
        print("Disconnected from server")
    }
}

// Assuming code below is from a swift package and cannot be modified

// import Starscream

open class ClientPackage {
    open weak var delegate: ClientPackageDelegate?
    open var socketConnected: Bool?
    open var token: String?
    
    public init(token: String) {
        self.token = token
        self.socketConnected = false
    }
    
    open func didConnect() {
        self.delegate?.connectedToServer(self)
        self.socketConnected = true
    }
    
    open func didDisconnect() {
        self.delegate?.disconnectedFromServer(self)
        self.socketConnected = false
    }
    
    open func connectToServer() {
        // This method connect client to server asynchronously over websocket
        // When connection stable, self.didConnect is called
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            self.didConnect()
        }
    }
    
    open func disconnectFromServer() {
        // This method disconnect the connection
        // When connection stable, self.didDisconnect is called
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.didDisconnect()
        }
    }
}

public protocol ClientPackageDelegate: NSObjectProtocol {
    func connectedToServer(_ client: ClientPackage)
    func disconnectedFromServer(_ client: ClientPackage)
}
// ContentView.swift

import SwiftUI

struct ContentView: View {
    @StateObject var vm = ViewModel()
    var connected: Bool { vm.isConnected ?? false }
    var body: some View {
        VStack {
            Button(connected ? "Disconnected" : "Connected"){
                connected ? vm.disconnect() : vm.connect()
            }
            Text(connected ? "Connected" : "Disconnected")
        }
    }
}

You simply need to make the connected property of LocalClient @Published , then update the isConnected property of your view model whenever client.connected is updated.

LocalClient is already conforming to ClientPackageDelegate and its connected property is updated via the delegate methods, so you simply need to propagate the value of connected to your view model, to update the isConnected property which your UI observes.

class ClientViewModel: ObservableObject {
    
    var client: LocalClient?

    @Published var isConnected: Bool = false
    
    let token = "12345678" // This is fetched async in real scenario
    
    init() {
        client = LocalClient(token: token)
        client?.$connected.assign(to: &$isConnected)
    }
    
    func connect() {
        client?.connect() { connected in
            self.isConnected = connected
        }
    }
    
}

// Assuming code below is from a local swift package and can be modified

class LocalClient: NSObject {
    
    var coreClient: ClientPackage?
    @Published var connected = false
    
    init(token: String) {
        super.init()
        coreClient = ClientPackage(token: token)
        coreClient!.delegate = self
    }
    
    func connect(_ completion: @escaping (Bool) -> Void) {
        coreClient?.connectToServer()
        completion(connected)
    }
}

If you want closure based behaviour rather than using Combine (which I think is the wrong way to go, since you're already using Combine in your view model anyways), you just need to inject a closure into LocalClient , which you execute from your delegate methods whenever the connectivity state changes.

class ClientViewModel: ObservableObject {
    
    var client: LocalClient?

    @Published var isConnected: Bool = false
    
    let token = "12345678" // This is fetched async in real scenario
    
    init() {
        client = LocalClient(token: token, connectionStateChanged: { [weak self] in self?.isConnected = $0 })
    }
    
    func connect() {
        client?.connect() { connected in
            self.isConnected = connected
        }
    }
    
}

// Assuming code below is from a local swift package and can be modified

class LocalClient: NSObject {
    
    let coreClient: ClientPackage
    var connected = false
    let connectionStateChanged: (Bool) -> Void
    
    init(token: String, connectionStateChanged: @escaping (Bool) -> Void) {
        self.connectionStateChanged = connectionStateChanged
        coreClient = ClientPackage(token: token)
        super.init()
        coreClient.delegate = self
    }
    
    func connect(_ completion: @escaping (Bool) -> Void) {
        coreClient.connectToServer()
        completion(connected)
    }
}

extension LocalClient: ClientPackageDelegate {
    func connectedToServer(_ client: ClientPackage) {
        self.connected = true
        connectionStateChanged(true)
        print("Connected to server")
    }
    
    func disconnectedFromServer(_ client: ClientPackage) {
        self.connected = false
        connectionStateChanged(false)
        print("Disconnected from server")
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM