简体   繁体   中英

Swift async await: how to use with several non async delegates?

I have created this simple example, it is a UITextField with an autocompletion ability, displaying a table view showing asynchronous data that evolves as the user types in the text field.

TextField

import UIKit

protocol TextFieldDelegate {
    func autocompletedComponents(
        _ textField: TextField,
        _ components: @escaping ([String]) -> Void
    )
}

class TextField: UITextField {
    var components: [String] = []
    var tableView = UITableView(frame: .zero)
    var autocompletionDelegate: TextFieldDelegate? { didSet { setupUI() } }
    
    // Actions
    @objc private func didUpdateText() {
        autocompletionDelegate?.autocompletedComponents(self) { [weak self] components in
            guard let weakSelf = self else {
                return
            }
            weakSelf.components = components
            weakSelf.tableView.reloadData()
            weakSelf.updateUI()
        }
    }
    
    // Event
    override func becomeFirstResponder() -> Bool {
        tableView.isHidden = false
        return super.becomeFirstResponder()
    }
    
    override func resignFirstResponder() -> Bool {
        tableView.isHidden = true
        return super.resignFirstResponder()
    }
    
    // Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        internalInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        internalInit()
    }
    
    private func internalInit() {
        addTarget(
            self,
            action: #selector(didUpdateText),
            for: .editingChanged
        )
    }
    
    // UI
    private func setupUI() {
        tableView.dataSource = self
        tableView.delegate = self
        tableView.showsVerticalScrollIndicator = false
        tableView.removeFromSuperview()
        superview?.addSubview(tableView)
    }
    
    private func updateUI() {
        let tableViewHeight = Double(min(5, max(0, components.count))) * 44.0
        tableView.frame = CGRect(
            origin: CGPoint(
                x: frame.origin.x,
                y: frame.origin.y + frame.size.height
            ),
            size: CGSize(
                width: frame.size.width,
                height: tableViewHeight
            )
        )
    }
}

extension TextField: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = components[indexPath.row]
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        44.0
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        components.count
    }
}

ViewController

import MapKit
import UIKit

class ViewController: UIViewController {
    private var completion: (([MKLocalSearchCompletion]) -> Void)?
    private var searchCompleter = MKLocalSearchCompleter()
    
    @IBOutlet weak var textField: TextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        searchCompleter.delegate = self
        textField.autocompletionDelegate = self
    }
}

extension ViewController: TextFieldDelegate {
    func autocompletedComponents(
        _ textField: TextField,
        _ components: @escaping ([String]) -> Void
    ) {
        if completion == nil {
            completion = { results in
                components(results.map { $0.title })
            }
        }
        searchCompleter.queryFragment = textField.text ?? ""
    }
}

extension ViewController: MKLocalSearchCompleterDelegate {
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        completion?(completer.results)
    }
}

In this example the view controller uses data from MapKit . Now I would like to get rid of the @escaping blocks and replace them with the new async/await syntax.

I have started rewriting the TextField code:

protocol TextFieldDelegate {
    func autocompletedComponents(
        _ textField: TextField
    ) async -> [String]
}

@objc private func didUpdateText() {
    Task {
        let autocompletedComponents = await autocompletionDelegate?.autocompletedComponents(self) ?? []
        components = autocompletedComponents
        tableView.reloadData()
        updateUI()
    }
}

However I am stuck in the ViewController, because I don't know what to do with the completion block I was using until now.

Thank you for your help

Here's an implementation using Combine 's PassThroughSubject to send the array from MKLocalSearchCompleterDelegate to your autocompletedComponents function which then returns that array to be used in TextField

In ViewController :

class ViewController: UIViewController {
    private var searchCompleter = MKLocalSearchCompleter()
    var cancellables = Set<AnyCancellable>()
    var publisher = PassthroughSubject<[String], Never>()
    @IBOutlet weak var textField: TextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        searchCompleter.delegate = self
        textField.autocompletionDelegate = self
    }
}

extension ViewController: TextFieldDelegate {
    func autocompletedComponents(
        _ textField: TextField
    ) async -> [String] {
        searchCompleter.queryFragment = textField.text ?? ""
        return await withCheckedContinuation { continuation in
            publisher
                .sink { array in
                    continuation.resume(returning: array)
                }.store(in: &cancellables)
        }
    }
}

extension ViewController: MKLocalSearchCompleterDelegate {
    func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
        publisher.send(completer.results.map({$0.title}))
    }
}

Or you could use your completion closure it would look like this:

func autocompletedComponents(
    _ textField: TextField,
    _ components: @escaping ([String]) -> Void
) {
    return await withCheckedContinuation { continuation in
        if completion == nil {
            completion = { results in
                continuation.resume(returning: results.map { $0.title })
            }
        }
        searchCompleter.queryFragment = textField.text ?? ""
    }
}

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