Swift MapKit 注释在点击地图之前不会加载

Swift MapKit annotations not loading until map tapped

我正在向地图添加一堆注释,当用户移动和平移到不同的国家/地区时,我会删除注释并添加更多注释。我面临的问题是,在我与地图进行交互(点击、捏合、平移或缩放)之前,新注释不会显示。

我已经尝试将 map.addAnnotations() 放入 DispatchQueue 中,但这没有用,我还将构建的方法 loadNewCountry(country: String) 偏移到 dispatchGroup 中。 None 个有效!

注意:我有几千种不同类型的注释,所以将它们全部加载到内存中对旧设备不起作用:)

func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    
    checkIfLoadNewCountry()
}

func checkIfLoadNewCountry() {
    let visible  = map.centerCoordinate
    geocode(latitude: visible.latitude, longitude: visible.longitude) { placemark, error in
        if let error = error {
            print("\(error)")
            return
        } else if let placemark = placemark?.first {
            if let isoCountry = placemark.isoCountryCode?.lowercased() {
                self.loadNewCountry(with: isoCountry)
            }
        }
    }
}

func loadNewCountry(with country: String) {
    let annotationsArray = [
        self.viewModel1.array,
        self.viewModel2.array,
        self.viewModel3.array
        ] as [[MKAnnotation]]
    
    let annotations = map.annotations
    
    autoreleasepool {
        annotations.forEach {
            if !([=11=] is CustomAnnotationOne), !([=11=] is CustomAnnotationTwo) {
                self.map.removeAnnotation([=11=])
            }
        }
    }
    
    let group = DispatchGroup()
    let queue = DispatchQueue(label: "reload-annotations", attributes: .concurrent)
    
    group.enter()
    queue.async {
        self.viewModel1.load(country: country)
        group.leave()
    }
    
    group.enter()
    queue.async {
        self.viewModel2.load(country: country)
        group.leave()
    }
    
    group.wait()
    
    DispatchQueue.main.async {
        for annoArray in annotationsArray {
            self.map.addAnnotations(annoArray)
        }
    }
    
}

关键问题是代码正在使用当前视图模型结果初始化 [[MKAnnotation]],然后为新的 country 启动视图模型模型的 load,并且然后将旧视图模型注释添加到地图视图。

相反,[[MKAnnotation]] 重新加载完成后:

func loadNewCountry(with country: String) {
    let annotations = map.annotations

    annotations
        .filter { !([=10=] is CustomAnnotationOne || [=10=] is CustomAnnotationTwo || [=10=] is MKUserLocation) }
        .forEach { map.removeAnnotation([=10=]) }

    let group = DispatchGroup()
    let queue = DispatchQueue(label: "reload-annotations", attributes: .concurrent)

    queue.async(group: group) {
        self.viewModel1.load(country: country)
    }

    queue.async(group: group) {
        self.viewModel2.load(country: country)
    }

    group.notify(queue: .main) {
        let annotationsArrays: [[MKAnnotation]] = [
            self.viewModel1.array,
            self.viewModel2.array,
            self.viewModel3.array
        ]

        for annotations in annotationsArrays {
            self.map.addAnnotations(annotations)
        }
    }
}

与手头的问题无关,我还有:

  • 简化了 DispatchGroup 组语法;
  • 消除了 wait 因为你永远不应该阻塞主线程;
  • 删除了不必要的autoreleasepool;
  • 添加 MKUserLocation 到要排除的注释类型(即使您现在不显示用户位置,您可能会在将来的某个日期显示)...您永远不想手动删除 MKUserLocation 否则你会得到奇怪的用户体验;
  • 重命名 annotationArrays 以表明您正在处理一个数组数组。

顺便说一句,以上内容引起了 thread-safety 的关注。您似乎正在后台队列中更新您的视图模型。如果您在其他地方与这些视图模型交互,请确保同步您的访问。而且,除此之外,“视图模型”(例如与“演示者”模式相对)的激励思想是将它们连接起来,以便它们自己通知视图更改。

所以,您可以考虑:

  • 为视图模型提供异步 startLoad 方法;
  • 为视图模型提供一些机制,以在加载完成时通知视图(在主队列上)更改(无论是观察者、委托协议、闭包等)。
  • 确保视图模型与其属性同步交互(例如,array)。

例如,假设视图模型正在通过闭包更新视图:

typealias AnnotationBlock = ([MKAnnotation]) -> Void

protocol CountryLoader {
    var didAdd: AnnotationBlock? { get set }
    var didRemove: AnnotationBlock? { get set }
}

class ViewModel1: CountryLoader {
    var array: [CustomAnnotationX] = []
    var didAdd: AnnotationBlock?
    var didRemove: AnnotationBlock?

    func startLoad(country: String, completion: (() -> Void)? = nil) {
        DispatchQueue.global().async {
            let newArray: [CustomAnnotationX] = ...   // computationally expensive load process here (on background queue)

            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }

                self.didRemove?(self.array)           // tell view what was removed
                self.array = newArray                 // update model on main queue
                self.didAdd?(newArray)                // tell view what was added
                completion?()                         // tell caller that we're done
            }
        }
    }
}

这是一个 thread-safe 实现,它从任何复杂的异步进程中抽象出视图和视图控制器。然后视图控制器需要配置视图模型:

class ViewController: UIViewController {
    @IBOutlet weak var map: MKMapView!

    let viewModel1 = ViewModel1()
    let viewModel2 = ViewModel2()
    let viewModel3 = ViewModel3()

    override func viewDidLoad() {
        super.viewDidLoad()

        configureViewModels()
    }

    func configureViewModels() {
        viewModel1.didRemove = { [weak self] annotations in
            self?.map?.removeAnnotations(annotations)
        }
        viewModel1.didAdd = { [weak self] annotations in
            self?.map?.addAnnotations(annotations)
        }

        ...
    }
}

那么,“reload for country”就变成了:

func loadNewCountry(with country: String) {
    viewModel1.startLoad(country: country)
    viewModel2.startLoad(country: country)
    viewModel3.startLoad(country: country)
}

或者

func loadNewCountry(with country: String) {
    showLoadingIndicator()

    let group = DispatchGroup()

    group.enter()
    viewModel1.startLoad(country: country) {
        group.leave()
    }

    group.enter()
    viewModel2.startLoad(country: country) {
        group.leave()
    }

    group.enter()
    viewModel3.startLoad(country: country) {
        group.leave()
    }

    group.notify(queue: .main) { [weak self] in
        self?.hideLoadingIndicator()
    }
}

现在这只是一种模式。根据您实现视图模型的方式,实现细节可能会有很大差异。但想法是你应该:

  • 确保视图模型是 thread-safe;
  • 从视图中抽象出复杂的线程逻辑,并将其保留在视图模型中;和
  • 有一些过程可以让视图模型通知视图相关的变化。