我将注释数据存储在 Firebase 的数据库中。我发现只要注释没有自定义视图,我就可以下载 10,000 个注释的数据并将这些注释添加到我的地图中,而不会出现太大的延迟。
然而,对于我的应用程序,我需要使用自定义视图,每个注释视图都是由多个图像片段组成的图像。如果我使用自定义视图(即使自定义视图只是一个 UIImage),应用程序会冻结并最终收到错误“来自调试器的消息:由于内存问题而终止”。我的应用程序的最小缩放级别为 15,因此用户基本上只能看到周围的内容。
我的目标是下载用户周围 10 公里范围内的所有注释的注释数据(我将使用 geohashing 来完成此操作,尽管这不是这个问题的重点)。手机上的地图只能查看大约一公里左右的土地。
那么我要么只想
a) add annotations that are visible on the phone
or
b) only load the views for the annotations that are visible.
我希望注释在屏幕边界内后立即可见,这样,如果用户滚动地图,他们会立即看到这些注释。
我的视图控制器中有这个委托函数,它确定每个注释的视图,当我注释掉它时,添加注释会有轻微的延迟,但不是很多。
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if annotation is MGLUserLocation && mapView.userLocation != nil {
let view = CurrentUserAnnoView(reuseIdentifier: currentUser.uid!)
self.currentUserAnno = view
return view
}
else if annotation is UserAnnotation{
let anno = annotation as! UserAnnotation
let auid = anno.reuseIdentifier //The anno uid
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: auid) {
return annotationView
} else {
let annotationView = UserAnnotationView(reuseIdentifier: auid, size: CGSize(width: 45, height: 45), annotation: annotation)
annotationView.isUserInteractionEnabled = true
anno.view = annotationView
return annotationView
}
}
return MGLAnnotationView(annotation: annotation, reuseIdentifier: "ShouldntBeAssigned") //Should never happen
}
Example
如果您观看此 YouTube 视频,您会发现注释并不总是可见,只有当您缩放或在注释上移动时它们才会变得可见。
https://youtu.be/JWUFD48Od4M https://youtu.be/JWUFD48Od4M
地图视图控制器
class MapViewController: UIViewController {
@IBOutlet weak var newPostView: NewPostView!
@IBOutlet var mapView: MGLMapView!
var data: MapData?
var currentUserAnno: CurrentUserAnnoView?
var testCounter = 0
let geoFire = GeoFire(firebaseRef: Database.database().reference().child("/users/core"))
@IBAction func tap(_ sender: UITapGestureRecognizer) {
self.view.endEditing(true)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: "7")
self.startup()
}
func startup(){
if CLLocationManager.isOff(){
let popup = UIAlertController(title: "Location Services are Disabled", message: "Please enable location services in your 'Settings -> Privacy' if you want to use this app", preferredStyle: UIAlertController.Style.alert)
popup.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {(alert: UIAlertAction) in
self.startup()
}))
popup.view.layoutIfNeeded()
self.present(popup, animated: true, completion: nil)
}else{
self.mapView.userTrackingMode = .follow
self.data = MapData(delegate: self)
}
}
@IBAction func newHidea(_ sender: Any) {
newPostView.isHidden = false
}
}
extension MapViewController: MGLMapViewDelegate{
func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
print(testCounter)
testCounter = testCounter + 1
if annotation is MGLUserLocation && mapView.userLocation != nil {
let view = CurrentUserAnnoView(reuseIdentifier: currentUser.uid!)
self.currentUserAnno = view
return view
}
else if annotation is UserAnnotation{
let anno = annotation as! UserAnnotation
// let auid = anno.reuseIdentifier //The anno uid
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "UserAnnotationView") {
return annotationView
} else {
let annotationView = UserAnnotationView(reuseIdentifier: "UserAnnotationView", size: CGSize(width: 45, height: 45), annotation: annotation)
annotationView.isUserInteractionEnabled = true
//anno.view = annotationView
return annotationView
}
}
return MGLAnnotationView(annotation: annotation, reuseIdentifier: "ShouldntBeAssigned") //Should never happen
}
func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
/*The regular anno status box is replaced by one with buttons*/
let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
let viewFrame = CGRect(origin: CGPoint(x: 0, y: -10), size: CGSize(width: 180, height: 400))
var cView: AnnoCalloutView
if (annotation as! UserAnnotation).status != nil{
cView = StatusCallout(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
}else{
cView = ProfileCallout(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
}
return cView
}
func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
if (annotation is UserAnnotation) {
return true
}else{
return false
}
}
func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) {
mapView.deselectAnnotation(annotation, animated: true) // Hide the callout.
}
}
//TODO: Check if there's a better method than a delegate to do this, since it's Model -> Controller
extension MapViewController: MapDataDelegate{
func addAnnotation(_ anno: UserAnnotation) {
self.mapView?.addAnnotation(anno)
}
}
用户注释
class UserAnnotation: NSObject, MGLAnnotation {
//////////Ignore these, required for MGLAnnotation//////
var title: String?
var subtitle: String?
////////////////////////////////////////////////////////
var coordinate: CLLocationCoordinate2D
var status: Status?{
didSet{
//TODO: update annotation
}
}
var reuseIdentifier: String
var avatar: Avatar
var uid: String
//MARK: You could assign these when the profile is viewed once, so if they view it again you have it saved.
var uName: String?
var bio: String?
init(coordinate: CLLocationCoordinate2D, avatar: Avatar, reuseIdentifier: String?, uid: String) {
// init(coordinate: CLLocationCoordinate2D, reuseIdentifier uid: String?) {
self.coordinate = coordinate
self.title = "None"
self.subtitle = "None"
self.reuseIdentifier = reuseIdentifier!
self.uid = uid
self.avatar = avatar
super.init()
// self.setAvatar(avatar: avatar)
}
init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?){
print("This shouldn't be printing")
self.coordinate = coordinate
self.uName = "ShouldntBeSet"
self.title = "ShouldntBeSet"
self.subtitle = "ShouldntBeSet"
self.reuseIdentifier = "ShouldntBeAssigned"
self.uid = "ShouldntBeAssigned"
self.avatar = Avatar(withValues: [0])
}
}
用户注释视图
class UserAnnotationView: MGLAnnotationView {
var anno: UserAnnotation?
var statusView: UITextView?
var imageView: UIImageView?
var avatarImage: UIImage{
let ai = AvatarImage()
ai.update(with: (anno?.avatar.values)!)
return ai.image!
}
init(reuseIdentifier: String, size: CGSize, annotation: MGLAnnotation) {
super.init(reuseIdentifier: reuseIdentifier)
// Prevents view from changing size when view tilted
scalesWithViewingDistance = false
frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
self.anno = annotation as? UserAnnotation
self.setUpImageView(frame: frame, size: size, annotation: annotation)
if anno?.status != nil{
self.createStatus(status: (anno?.status?.status)!)
}
}
func reuseWithDifferentAnno(annotation: UserAnnotation){
self.anno = annotation
self.imageView!.image = UIImage(named: "Will")
// let av = AvatarImage.newAvatar(values: (anno?.avatar.values)!)
// self.imageView!.image = av.image
// if anno?.status != nil{
// self.createStatus(status: (anno?.status?.status)!)
// }else{
// if statusView != nil{
// deleteStatus()
// }
// }
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setUpImageView(frame: CGRect, size: CGSize, annotation: MGLAnnotation){
self.imageView = UIImageView(frame: frame)
self.imageView!.translatesAutoresizingMaskIntoConstraints = false
if annotation is UserAnnotation {
// let av = AvatarImage.newAvatar(values: (anno?.avatar.values)!)
// self.imageView!.image = av.image
self.imageView!.image = UIImage(named: "Will")
}else{
let image = UIImage()
self.imageView!.image = image
}
addSubview(self.imageView!)
imageViewConstraints(imageView: self.imageView!, size: size)
}
func setImage(to image: UIImage){
self.imageView!.image = image
}
func createStatus(status: String){
if (status == self.statusView?.text) && (self.subviews.contains(self.statusView!)){
return
}else if self.statusView != nil && self.subviews.contains(self.statusView!){
deleteStatus()
}
self.statusView = UITextView()
self.statusView!.text = status
self.statusView!.isHidden = false
self.adjustUITextViewHeight()
self.statusView!.translatesAutoresizingMaskIntoConstraints = false
self.statusView!.layer.cornerRadius = 5
self.statusView!.textAlignment = .center
addSubview(self.statusView!)
textViewConstraints(textView: self.statusView!, isAbove: self.imageView!)
}
func deleteStatus(){
self.statusView?.removeFromSuperview()
self.statusView = nil
}
private func adjustUITextViewHeight(){
self.statusView!.translatesAutoresizingMaskIntoConstraints = true
self.statusView!.sizeToFit()
self.statusView!.isScrollEnabled = false
}
private func imageViewConstraints(imageView: UIImageView, size: CGSize){
let widCon = NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: size.width)
let heightCon = NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: size.height)
let cenCon = NSLayoutConstraint(item: imageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
NSLayoutConstraint.activate([cenCon, widCon, heightCon])
}
private func textViewConstraints(textView status: UITextView, isAbove imageView: UIImageView){
let cenCon = NSLayoutConstraint(item: status, attribute: .centerX, relatedBy: .equal, toItem: imageView, attribute: .centerX, multiplier: 1, constant: 0)
let botCon = NSLayoutConstraint(item: status, attribute: .bottom, relatedBy: .equal, toItem: imageView, attribute: .top, multiplier: 1, constant: -10)
let widCon = NSLayoutConstraint(item: status, attribute: .width, relatedBy: .lessThanOrEqual, toItem: nil, attribute: .width, multiplier: 1, constant: 200)
NSLayoutConstraint.activate([cenCon, botCon, widCon])
}
}
MapData
class MapData {
var annotations = [String:UserAnnotation]()
var updateTimer: Timer?
var delegate: MapDataDelegate
init(delegate: MapDataDelegate){
self.delegate = delegate
self.startTimer()
}
@objc func getUsers(){
FBCore.getAllUsers(completion:{(users) in
for child in users {
let value = child.value as! NSDictionary
self.getDataFor(user: value, whoseUidIs: child.key)
}
})
}
func getDataFor(user: NSDictionary, whoseUidIs annoid: String){
if annoid != currentUser.uid! && (currentUser.blockedBy?[annoid] ?? false) != true && (currentUser.blocks?[annoid] ?? false) != true{
guard let (coord, status, avatar) = FBCoreUser.get(forQryVal: user)
else {return}
if let anno = self.annotations[annoid]{
anno.coordinate = coord
if status != nil{// && anno.view!.isSelected == false {
if ((status?.isExpired)!){
anno.status = nil
}else{
anno.status = status
}
}
if avatar.values != anno.avatar.values{
anno.avatar = avatar
}
}else{
let anno = UserAnnotation(coordinate: coord, avatar: avatar, reuseIdentifier: "UserAnnotation", uid: annoid)
if status != nil{
if ((status?.isExpired)!){
anno.status = nil
}else{
anno.status = status
}
}
self.annotations[annoid] = anno
//print(anno.reuseIdentifier)
delegate.addAnnotation(anno)
}
}
}
func startTimer(){
// Scheduling timer to Call the function "updateCounting" with the interval of 5 seconds
if updateTimer != nil{
updateTimer!.invalidate()
}
updateTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(getUsers), userInfo: nil, repeats: true)
}
}