MapKit框架详细解析(三) —— 基本使用简单示例(二)

版本记录

版本号 时间
V1.0 2018.10.14 星期日

前言

MapKit框架直接从您的应用界面显示地图或卫星图像,调出兴趣点,并确定地图坐标的地标信息。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. MapKit框架详细解析(一) —— 基本概览(一)
2. MapKit框架详细解析(二) —— 基本使用简单示例(一)

Parsing JSON Data into Artwork Objects - 将JSON数据解析为Artwork对象

现在您已了解如何在地图上显示一件artwork,以及如何从pin的标注信息按钮启动Maps应用程序,现在是时候将数据集解析为一个Artwork对象数组。 然后,您将它们作为annotations添加到地图视图中,以显示位于当前地图区域中的所有艺术品。

将这个可用的初始化程序添加到初始化程序下面的Artwork.swift

init?(json: [Any]) {
  // 1
  self.title = json[16] as? String ?? "No Title"
  self.locationName = json[12] as! String
  self.discipline = json[15] as! String
  // 2
  if let latitude = Double(json[18] as! String),
    let longitude = Double(json[19] as! String) {
  self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
  } else {
    self.coordinate = CLLocationCoordinate2D()
  }
}

这是你正在做的事情:

  • 1) json参数是表示艺术作品的数组之一 - Any对象的数组。 如果计算数组的元素,您将看到titlelocationName等位于此方法中指定的索引处。 某些artworkstitle字段为null,因此您为title值提供默认值。
  • 2) json数组中的纬度和经度值是字符串:如果可以从它们创建Double对象,则创建CLLocationCoordinate2D

换句话说,这个初始化器转换一个这样的数组:

[ 55, "8492E480-43E9-4683-927F-0E82F3E1A024", 55, 1340413921, "436621", 1340413921, "436621", "{\n}", "Sean Browne", "Gift of the Oahu Kanyaku Imin Centennial Committee", "1989", "Large than life-size bronze figure of King David Kalakaua mounted on a granite pedestal. Located at Waikiki Gateway Park.", "Waikiki Gateway Park", "http://hiculturearts.pastperfect-online.com/34250images/002/199103-3.JPG", "1991.03", "Sculpture", "King David Kalakaua", "Full", "21.283921", "-157.831661", [ null, "21.283921", "-157.831661", null, false ], null ]

进入您之前创建的Artwork对象:

  • locationName: “Waikiki Gateway Park”
  • discipline: “Sculpture”
  • title: “King David Kalakaua”
  • coordinate with latitude: 21.283921 longitude: -157.831661

要使用此初始化程序,请打开ViewController.swift,并将以下属性添加到类中 - 一个用于保存JSON文件中的Artwork对象的数组:

var artworks: [Artwork] = []

接下来,将以下辅助方法添加到类中:

func loadInitialData() {
  // 1
  guard let fileName = Bundle.main.path(forResource: "PublicArt", ofType: "json") 
    else { return }
  let optionalData = try? Data(contentsOf: URL(fileURLWithPath: fileName))

  guard
    let data = optionalData,
    // 2
    let json = try? JSONSerialization.jsonObject(with: data),
    // 3
    let dictionary = json as? [String: Any],
    // 4
    let works = dictionary["data"] as? [[Any]]
    else { return }
  // 5
  let validWorks = works.flatMap { Artwork(json: $0) }
  artworks.append(contentsOf: validWorks)
}

以下是您在此代码中所做的事情:

  • 1) 您将PublicArt.json文件读入Data对象。
  • 2) 您使用JSONSerialization来获取JSON对象。
  • 3) 您检查JSON对象是具有String键和Any值的字典。
  • 4) 您只对其键为data的JSON对象感兴趣。
  • 5) 您使用刚刚添加到Artwork类的可用初始化程序对这个数组进行flatmap,并将生成的validWorks附加到artwork数组中。

Plotting the Artworks - 绘制Artworks

您现在拥有数据集中所有公共艺术作品的数组,您将添加到地图中。

仍然在ViewController.swift中,在viewDidLoad()的末尾添加以下代码:

loadInitialData()
mapView.addAnnotations(artworks)

注意:请务必使用复数addAnnotations,而不是单数addAnnotation

注释或删除创建单个“King David Kalakaua”地图annotation的行 - 您现在不需要它们,现在loadInitialData创建artworks数组:

//    let artwork = Artwork(title: "King David Kalakaua",
//      locationName: "Waikiki Gateway Park",
//      discipline: "Sculpture",
//      coordinate: CLLocationCoordinate2D(latitude: 21.283921, longitude: -157.831661))
//    mapView.addAnnotation(artwork)

Build并运行您的应用程序并检查所有标记!

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第1张图片

移动地图以查看其他标记。 例如,在您的初始位置的北面,在1号高速公路上方,是Honolulu’s Pioneer Artesian Well

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第2张图片

注意:标记的西北部是Punahou学校,它声称一位前美国总统作为校友! 西部的标记是他出生的医院。

点击一个标记打开其callout气泡,然后点击其信息按钮启动Maps应用程序 - 是的,您使用King Kalakaua雕像所做的一切都适用于所有这些新作品!

注意:感谢Dave Mark指出 Apple recommends adding all the annotations right away,无论它们是否在地图区域中可见 - 当您移动地图时,它会自动显示可见annotations

就是这样! 您已经构建了一个应用程序,将JSON文件解析为一artworks数组,然后将其显示为注释标记(annotation markers),并带有一个用于启动Maps应用程序的callout info按钮!


Customizing Annotations - 自定义注释

1. Markers with Color-Coding & Text - 带有颜色编码和文本的标记

还记得Artwork类中的discipline属性吗? 它的价值观就像“Sculpture”“Mural” - 实际上,最多的disciplines是雕塑,牌匾,壁画和纪念碑。 对标记进行颜色编码很容易,因此这些disciplines具有不同颜色的标记,所有其他disciplines都有绿色标记。

Artwork.swift中,添加以下属性:

// markerTintColor for disciplines: Sculpture, Plaque, Mural, Monument, other
var markerTintColor: UIColor  {
  switch discipline {
  case "Monument":
    return .red
  case "Mural":
    return .cyan
  case "Plaque":
    return .blue
  case "Sculpture":
    return .purple
  default:
    return .green
  }
}

现在,您可以继续向mapView(_:viewFor :)添加代码,但这会使视图控制器变得混乱。 有一种更优雅的方式,类似于您可以为表视图单元格执行的操作。 创建一个名为ArtworkViews.swift的新Swift文件,并在import语句下面添加此代码:

import MapKit

class ArtworkMarkerView: MKMarkerAnnotationView {
  override var annotation: MKAnnotation? {
    willSet {
      // 1
      guard let artwork = newValue as? Artwork else { return }
      canShowCallout = true
      calloutOffset = CGPoint(x: -5, y: 5)
      rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
      // 2
      markerTintColor = artwork.markerTintColor
      glyphText = String(artwork.discipline.first!)
    }
  }
}

很快,您将此类注册为Artwork annotations的可重用annotation视图。 系统会将注释作为newValue传递给你,所以这就是你正在做的事情:

  • 1) 这些行与mapView(_:viewFor :)完成相同的操作,配置callout
  • 2) 然后设置标记的色调颜色,并将其pin icon(glyph)替换为annotation’s discipline的第一个字母。

现在切换到ViewController.swift,并在调用loadInitialData()之前将此行添加到viewDidLoad()

mapView.register(ArtworkMarkerView.self, 
  forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

在这里,您使用地图视图的默认重用标识符注册新类。 对于具有更多annotation类型的应用程序,您将使用自定义标识符注册类。

向下滚动到扩展,并注释掉mapView(_:viewFor :)方法。

Build并运行您的应用程序,然后移动地图,以查看不同颜色和标记的标记:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第3张图片

在地图的这一部分中,实际上有比地图视图更多的artworks:它通过聚类太靠近的标记来减少混乱。 在下一节中,您将看到所有annotations

但首先,设置字形的图像而不是文本。 将以下属性添加到Artwork.swift

var imageName: String? {
  if discipline == "Sculpture" { return "Statue" }
  return "Flag"
}

来自icons8.com的这些图片已经在Images.xcassets中。

Build并运行您的应用程序以查看带有图像的不同颜色标记:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第4张图片

这是另一个自定义选项,你的下一个任务:用图像替换标记!

2. Images, Not Markers - 图像,而不是标记

ArtworkViews.swift中,添加以下类:

class ArtworkView: MKAnnotationView {
  override var annotation: MKAnnotation? {
    willSet {
      guard let artwork = newValue as? Artwork else {return}
      canShowCallout = true
      calloutOffset = CGPoint(x: -5, y: 5)
      rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

      if let imageName = artwork.imageName {
        image = UIImage(named: imageName)
      } else {
        image = nil
      }
    }
  }
}

现在,您使用的是普通的旧MKAnnotationView而不是MKMarkerAnnotationView,并且视图具有image属性。

回到ViewController.swift,在viewDidLoad()中,注册这个新类,而不是ArtworkMarkerView

mapView.register(ArtworkView.self, 
  forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

Build并运行您的应用程序以查看雕塑和标志:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第5张图片

现在,您没有看到标题,但地图视图显示了所有annotations

3. Custom Callout Accessory Views - 自定义标注附件视图

正确的标注附件是一个信息按钮,但点击它会打开Maps应用,所以现在您将更改按钮以显示地图图标。

ArtworkView中找到这一行:

rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

用下面代码替换上面那一行

let mapsButton = UIButton(frame: CGRect(origin: CGPoint.zero,
   size: CGSize(width: 30, height: 30)))
mapsButton.setBackgroundImage(UIImage(named: "Maps-icon"), for: UIControlState())
rightCalloutAccessoryView = mapsButton

在这里,您创建一个UIButton,将其背景图像设置为Images.xcassets中iconarchive.com的Maps图标,然后将视图右侧标注附件设置为此按钮。

Build并运行您的应用,然后点按视图以查看新的地图按钮:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第6张图片

最后的定制是细节标注附件(detail callout accessory):它是一行,足以用于短位置文本,但是如果你想要显示大量文本呢?

Artwork.swift中,在init(json :)中找到这一行:

self.locationName = json[12] as! String

用下面这行进行替换

self.locationName = json[11] as! String

在这里,您选择了artwork的长描述,这不适用于默认的单行细节标注附件。 现在您需要一个多行标签:将以下代码添加到ArtworkView

let detailLabel = UILabel()
detailLabel.numberOfLines = 0
detailLabel.font = detailLabel.font.withSize(12)
detailLabel.text = artwork.subtitle
detailCalloutAccessoryView = detailLabel

Build并运行您的应用,然后点按视图以查看详细说明:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第7张图片

Bonus Topic: User Location Authorization - 奖金主题:用户位置授权

此应用程序无需要求用户授权访问其位置,但您可能希望将其包含在其他基于MapKit的应用程序中。

ViewController.swift中,添加以下行:

let locationManager = CLLocationManager()
func checkLocationAuthorizationStatus() {
  if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
    mapView.showsUserLocation = true
  } else {
    locationManager.requestWhenInUseAuthorization()
  }
}

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  checkLocationAuthorizationStatus()
}

在这里,您可以创建一个CLLocationManager来跟踪应用程序的授权状态,以便访问用户的位置。 在checkLocationAuthorizationStatus()中,如果您的应用已获得授权,则tick地图视图的Shows-User-Location复选框;否则,您告诉locationManager请求用户授权。

注意:locationManager可以发出两种授权请求:requestWhenInUseAuthorizationrequestAlwaysAuthorization。第一个让你的应用程序在前台使用位置服务;第二个授权您的应用程序运行时。 Apple’s documentation不鼓励使用“Always”
由于对用户隐私的潜在负面影响,不鼓励请求“Always”授权。只有这样做才能为用户提供真正的好处,您才应该请求此级别的授权。

1. Info.plist item: important but easy to overlook! - Info.plist项目:重要但容易被忽视!

您还需要执行一项与授权相关的任务 - 如果不这样做,您的应用程序不会崩溃,但不会出现locationManager的请求。要使请求生效,您必须提供一条消息,向用户解释您的应用想要访问其位置的原因。

Info.plist中,打开Information Property List。将光标悬停在上下箭头上,或单击列表中的任何项目,以显示+和 - 符号,然后单击+符号以创建新项目。向下滚动以选择Privacy – Location When In Use Usage Description,然后将其值设置为类似To show you cool things nearby:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第8张图片

Build并运行。 您会在启动时看到权限请求:

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第9张图片

有这样的用法说明,谁不允许访问?

注意:从iOS 11开始,如果不提供“When in Use”,则无法请求“Always”:如果您仅设置Privacy – Location Always Usage Description,则不会显示,并且您将收到错误消息“Info.plist must contain both NSLocationAlwaysAndWhenInUseUsageDescription and NSLocationWhenInUseUsageDescription keys…”

下面,location manager要求“Always”

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第10张图片

现在您已经了解了使用MapKit的基础知识,但您可以添加更多内容:地理编码,地理围栏,自定义地图叠加等。 Apple的Location and Maps Programming Guide是查找其他信息的好地方。

另请参阅WWDC 2017 Session 237 MapKit中的新功能,以查找他们在iOS 11中添加的更多酷炫功能。


源码

首先我们看一下工程文件

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第11张图片

接着看一下sb中的内容

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第12张图片

接着我们就看一下源码

1. Swift源码

1. ViewController.swift
import UIKit
import MapKit

class ViewController: UIViewController {

  // MARK: - Properties
  var artworks: [Artwork] = []
  @IBOutlet weak var mapView: MKMapView!
  let regionRadius: CLLocationDistance = 1000

  // MARK: - View life cycle

  override func viewDidLoad() {
    super.viewDidLoad()
    // set initial location in Honolulu
    let initialLocation = CLLocation(latitude: 21.282778, longitude: -157.829444)
    centerMapOnLocation(location: initialLocation)

    mapView.delegate = self
//    mapView.register(ArtworkMarkerView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
    mapView.register(ArtworkView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
    loadInitialData()
    mapView.addAnnotations(artworks)

    // show artwork on map
//    let artwork = Artwork(title: "King David Kalakaua",
//      locationName: "Waikiki Gateway Park",
//      discipline: "Sculpture",
//      coordinate: CLLocationCoordinate2D(latitude: 21.283921, longitude: -157.831661))
//    mapView.addAnnotation(artwork)
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    checkLocationAuthorizationStatus()
  }

  // MARK: - CLLocationManager

  let locationManager = CLLocationManager()
  func checkLocationAuthorizationStatus() {
    if CLLocationManager.authorizationStatus() == .authorizedAlways {
      mapView.showsUserLocation = true
    } else {
      locationManager.requestAlwaysAuthorization()
    }
//    if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
//      mapView.showsUserLocation = true
//    } else {
//      locationManager.requestWhenInUseAuthorization()
//    }
  }

  // MARK: - Helper methods

  func centerMapOnLocation(location: CLLocation) {
    let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate,
      regionRadius, regionRadius)
    mapView.setRegion(coordinateRegion, animated: true)
  }

  func loadInitialData() {
    // 1
    guard let fileName = Bundle.main.path(forResource: "PublicArt", ofType: "json")
      else { return }
    let optionalData = try? Data(contentsOf: URL(fileURLWithPath: fileName))

    guard
      let data = optionalData,
      // 2
      let json = try? JSONSerialization.jsonObject(with: data),
      // 3
      let dictionary = json as? [String: Any],
      // 4
      let works = dictionary["data"] as? [[Any]]
      else { return }
    // 5
    let validWorks = works.flatMap { Artwork(json: $0) }
    artworks.append(contentsOf: validWorks)
  }

}

// MARK: - MKMapViewDelegate

extension ViewController: MKMapViewDelegate {

//   1
//  func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
//    guard let annotation = annotation as? Artwork else { return nil }
//    // 2
//    let identifier = "marker"
//    var view: MKMarkerAnnotationView
//    if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
//      as? MKMarkerAnnotationView { // 3
//      dequeuedView.annotation = annotation
//      view = dequeuedView
//    } else {
//      // 4
//      view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
//      view.canShowCallout = true
//      view.calloutOffset = CGPoint(x: -5, y: 5)
//      view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
//    }
//    return view
//  }

  func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView,
               calloutAccessoryControlTapped control: UIControl) {
    let location = view.annotation as! Artwork
    let launchOptions = [MKLaunchOptionsDirectionsModeKey:
      MKLaunchOptionsDirectionsModeDriving]
    location.mapItem().openInMaps(launchOptions: launchOptions)
  }

}
2. Artwork.swift
import Foundation
import MapKit
import Contacts

class Artwork: NSObject, MKAnnotation {
  let title: String?
  let locationName: String
  let discipline: String
  let coordinate: CLLocationCoordinate2D

  init(title: String, locationName: String, discipline: String, coordinate: CLLocationCoordinate2D) {
    self.title = title
    self.locationName = locationName
    self.discipline = discipline
    self.coordinate = coordinate

    super.init()
  }

  init?(json: [Any]) {
    // 1
    if let title = json[16] as? String {
      self.title = title
    } else {
      self.title = "No Title"
    }
    // json[11] is the long description
    self.locationName = json[11] as! String
    // json[12] is the short location string
//    self.locationName = json[12] as! String
    self.discipline = json[15] as! String
    // 2
    if let latitude = Double(json[18] as! String),
      let longitude = Double(json[19] as! String) {
      self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    } else {
      self.coordinate = CLLocationCoordinate2D()
    }
  }

  var subtitle: String? {
    return locationName
  }

  // pinTintColor for disciplines: Sculpture, Plaque, Mural, Monument, other
  var markerTintColor: UIColor  {
    switch discipline {
    case "Monument":
      return .red
    case "Mural":
      return .cyan
    case "Plaque":
      return .blue
    case "Sculpture":
      return .purple
    default:
      return .green
    }
  }

  var imageName: String? {
    if discipline == "Sculpture" { return "Statue" }
    return "Flag"
  }

  // Annotation right callout accessory opens this mapItem in Maps app
  func mapItem() -> MKMapItem {
    let addressDict = [CNPostalAddressStreetKey: subtitle!]
    let placemark = MKPlacemark(coordinate: coordinate, addressDictionary: addressDict)
    let mapItem = MKMapItem(placemark: placemark)
    mapItem.name = title
    return mapItem
  }

}
3. ArtworkViews.swift
import Foundation
import MapKit

class ArtworkMarkerView: MKMarkerAnnotationView {

  override var annotation: MKAnnotation? {
    willSet {
      guard let artwork = newValue as? Artwork else { return }
      canShowCallout = true
      calloutOffset = CGPoint(x: -5, y: 5)
      rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

      markerTintColor = artwork.markerTintColor
//      glyphText = String(artwork.discipline.first!)
        if let imageName = artwork.imageName {
          glyphImage = UIImage(named: imageName)
        } else {
          glyphImage = nil
      }
    }
  }

}

class ArtworkView: MKAnnotationView {

  override var annotation: MKAnnotation? {
    willSet {
      guard let artwork = newValue as? Artwork else {return}

      canShowCallout = true
      calloutOffset = CGPoint(x: -5, y: 5)
      let mapsButton = UIButton(frame: CGRect(origin: CGPoint.zero,
        size: CGSize(width: 30, height: 30)))
      mapsButton.setBackgroundImage(UIImage(named: "Maps-icon"), for: UIControlState())
      rightCalloutAccessoryView = mapsButton
//      rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

      if let imageName = artwork.imageName {
        image = UIImage(named: imageName)
      } else {
        image = nil
      }

      let detailLabel = UILabel()
      detailLabel.numberOfLines = 0
      detailLabel.font = detailLabel.font.withSize(12)
      detailLabel.text = artwork.subtitle
      detailCalloutAccessoryView = detailLabel
    }
  }

}

下面看一下点击info按钮打开Maps应用的效果。

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第13张图片

后记

本篇主要讲述了基本使用简单示例,感兴趣的给个赞或者关注~~~

MapKit框架详细解析(三) —— 基本使用简单示例(二)_第14张图片

你可能感兴趣的:(MapKit框架详细解析(三) —— 基本使用简单示例(二))