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;
- 从视图中抽象出复杂的线程逻辑,并将其保留在视图模型中;和
- 有一些过程可以让视图模型通知视图相关的变化。
我正在向地图添加一堆注释,当用户移动和平移到不同的国家/地区时,我会删除注释并添加更多注释。我面临的问题是,在我与地图进行交互(点击、捏合、平移或缩放)之前,新注释不会显示。
我已经尝试将 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;
- 从视图中抽象出复杂的线程逻辑,并将其保留在视图模型中;和
- 有一些过程可以让视图模型通知视图相关的变化。