CoreLocation 是iOS中用于设备定位的框架,通过这个框架可以知道经度,纬度 海拔信息等
1 CLLocationManager 定位管理器
2 CLLocationManagerDelegate 定位管理代理
3 CLLocation 包含定位信息 如海拔 经纬度 海拔
4 CLPlacemark 包含国家 城市 街道
5 CLGeocoder 地理编码 正向地理编码 给出位置描述信息,获取位置信息 CLPlaceMark, 反向地理编码 根据给定的经纬度等信息,获取地理信息
开发流程
授权
>1 requestWhenInUseAuthorization 和 requestAlwaysAuthorization 发起定位授权
前台定位 需要设置 info.plist Privacy - Location When In Use Usage Description
程序中调用 requestWhenInUseAuthorization 进行授权
后台定位
需要勾选
Capabilities —> Background Modes —> Location updates
程序中后台定位
locationManager.allowsBackgroundLocationUpdates = true
程序中允许后台定位
locationManager.allowsBackgroundLocationUpdates = true
此时授权分为 2 种情况:
(1)Privacy - Location When In Use Usage Description + requestWhenInUseAuthorization:可以后台定位,但会在设备顶部出现蓝条(刘海屏设备会出现在左边刘海)。
(2)Privacy - Location When In Use Usage Description + Privacy - Location Always and When In Use Usage Description + requestAlwaysAuthorization:可以后台定位,不会出现蓝条。这种方式会出现 2 次授权对话框:第一次和前台定位一样,在同意使用While Using App模式后,继续使用定位才会弹出第二次,询问是否切换到Always模式
精度控制
iOS 14 新增了一种定位精度控制,在定位授权对话框中有一个精度切换开关,可以切换精确和模糊定位(默认精确)。
可以通过CLLocationManager的accuracyAuthorization属性获取当前的定位精度权限。
当已经获得定位权限且当前用户选择的是模糊定位,则可以使用CLLocationManager的requestTemporaryFullAccuracyAuthorization(withPurposeKey purposeKey: String, completion: ((Error?) -> Void)? = nil)方法申请一次临时精确定位权限,其中purposeKey为 Info.plist 中配置的Privacy - Location Temporary Usage Description Dictionary字段下某个具体原因的 key,可以设置多个 key 以应对不同的定位使用场景。
requestTemporaryFullAccuracyAuthorization方法并不能用于申请定位权限,只能用于从模糊定位升级为精确定位;如果没有获得定位权限,直接调用此 API 无效。
如果不想使用精确定位,则可以在 Info.plist 中配置Privacy - Location Default Accuracy Reduced为YES,此时申请定位权限的小地图中不再有精度切换开关。
如果发现该字段不是 Bool 型,需要以源码形式打开 Info.plist,然后手动修改<key>NSLocationDefaultAccuracyReduced</key>为 Bool 型的值,否则无法生效。
配置该字段后,如果 Info.plist 中还配置了Privacy - Location Temporary Usage Description Dictionary,则仍可以通过requestTemporaryFullAccuracyAuthorization申请临时的精确定位权限,会再次弹出授权对话框进行确认。
注意点
> 1
iOS 11 之后如果申请后台定位,Info.plist 需要同时配置Privacy - Location When In Use Usage Description和Privacy - Location Always and When In Use Usage Description
> 2
2019 年下半年起如果 Info.plist 中不包含Privacy - Location When In Use Usage Description和Privacy - Location Always and When In Use Usage Description,则在代码中不能出现符号requestAlwaysAuthorization,否则上架审核不通过
可以通过模拟器模拟定位
由于定位需要 GPS,一般情况下需要真机进行测试。但对于模拟器,也可以进行虚拟定位,主要有 3 种方式
1 新建gpx ,然后编辑定位信息到XML对应的位置,然后选择 Edit Scheme,然后在options中选择自己的gpx文件,然后选择
Debug -> Simulate Location
2 运行程序开始定位 —> 模拟器菜单 —> Features —> Location —> Custom Location —> 输入经纬度。
3 运行程序开始定位 —> 打开 macOS 的地图 App —> 右上角图片按钮 —> Simulator —> 发送。
地图代码开发
import CoreLocation
import UIKit
class ViewController: UIViewController {
// CLLocationManager
lazy var locationManager = CLLocationManager()
// CLGeocoder
lazy var gecoder = CLGeocoder()
override func viewDidLoad() {
super.viewDidLoad()
setupManager()
}
func setupManager() {
// 默认情况下每当位置改变时LocationManager就调用一次代理。通过设置distanceFilter可以实现当位置改变超出一定范围时LocationManager才调用相应的代理方法。这样可以达到省电的目的。
locationManager.distanceFilter = 300
// 精度 比如为10 就会尽量达到10米以内的精度
locationManager.desiredAccuracy = kCLLocationAccuracyBest
// 代理
locationManager.delegate = self
// 第一种:能后台定位但是会在顶部出现大蓝条(打开后台定位的开关)
// 允许后台定位
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestWhenInUseAuthorization()
// 第二种:能后台定位并且不会出现大蓝条
// locationManager.requestAlwaysAuthorization()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// 发起位置更新(定位)会一直轮询,耗电
locationManager.startUpdatingLocation()
}
}
extension ViewController: CLLocationManagerDelegate {
// 定位成功
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
// 反地理编码转换成具体的地址
gecoder.reverseGeocodeLocation(location) { placeMarks, _ in
// CLPlacemark -- 国家 城市 街道
if let placeMark = placeMarks?.first {
print(placeMark)
// print("\(placeMark.country!) -- \(placeMark.name!) -- \(placeMark.locality!)")
}
}
}
// 停止位置更新
locationManager.stopUpdatingLocation()
}
// 定位失败
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
}
import MapKit
class ViewController: UIViewController {
@IBOutlet var mapView: MKMapView!
lazy var locationManager: CLLocationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
setupMapView()
}
func setupManager() {
locationManager.requestWhenInUseAuthorization()
// 不需要发起定位
}
func setupMapView() {
// 设置定位
setupManager()
// 地图类型
mapView.mapType = .hybridFlyover
// 显示兴趣点
mapView.showsPointsOfInterest = true
// 显示指南针
mapView.showsCompass = true
// 显示交通
mapView.showsTraffic = true
// 显示建筑
mapView.showsBuildings = true
// 显示级别
mapView.showsScale = true
// 用户跟踪模式
mapView.userTrackingMode = .followWithHeading
}
}
缩放级别
// 设置“缩放级别”
func setRegion() {
if let location = location {
// 设置范围,显示地图的哪一部分以及显示的范围大小
let region = MKCoordinateRegion(center: mapView.userLocation.coordinate, latitudinalMeters: 500, longitudinalMeters: 500)
// 调整范围
let adjustedRegion = mapView.regionThatFits(region)
// 地图显示范围
mapView.setRegion(adjustedRegion, animated: true)
}
}
截屏
import MapKit
import UIKit
class ViewController: UIViewController {
// 地图显示的经纬度
let location: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 31.29065118, longitude: 118.3623587)
// 地图的跨度
var span: CLLocationDegrees = 0.01
// 截图后显示
var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width * 0.5, height: UIScreen.main.bounds.size.height * 0.5))
imageView.center = view.center
view.addSubview(imageView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
generateSnapshot(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)
}
}
extension ViewController {
func generateSnapshot(width: CGFloat, height: CGFloat) {
// 设置地图的Region
let region = MKCoordinateRegion(
center: location,
span: MKCoordinateSpan(
latitudeDelta: span,
longitudeDelta: span
)
)
// 截屏选项
let mapOptions = MKMapSnapshotter.Options()
mapOptions.region = region
mapOptions.size = CGSize(width: width, height: height)
mapOptions.showsBuildings = true
// 开始截屏
let snapshotter = MKMapSnapshotter(options: mapOptions)
snapshotter.start { snapshotOrNil, errorOrNil in
// 出现错误
if let error = errorOrNil {
print(error)
return
}
// 显示截屏内容
if let snapshot = snapshotOrNil {
self.imageView.image = snapshot.image
}
}
}
}
class MapFlag: NSObject, MKAnnotation {
// 标题
let title: String?
// 副标题
let subtitle: String?
// 经纬度
let coordinate: CLLocationCoordinate2D
// 附加信息
let urlString: String
init(title: String?, subtitle: String?, coordinate: CLLocationCoordinate2D, urlString: String) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
self.urlString = urlString
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let flag = MapFlag(title: "标题", subtitle: "副标题", coordinate: CLLocationCoordinate2D(latitude: 31.2906511800, longitude: 118.3623587000), urlString: "https://www.baidu.com")
mapView.addAnnotation(flag)
}
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MapFlag else {
return nil
}
// 如果是用户的位置,使用默认样式
if annotation == mapView.userLocation {
return nil
}
// 标注的标识符
let identifier = "marker"
// 获取AnnotationView
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKMarkerAnnotationView
// 判空
if annotationView == nil {
annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
// 显示气泡
annotationView?.canShowCallout = true
// 左边显示的辅助视图
annotationView?.leftCalloutAccessoryView = UIImageView(image: UIImage(systemName: "heart"))
// 右边显示的辅助视图
let button = UIButton(type: .detailDisclosure, primaryAction: UIAction(handler: { _ in
print(annotation.urlString)
}))
annotationView?.rightCalloutAccessoryView = button
}
return annotationView
}
}
extension ViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MapFlag else {
return nil
}
// 如果是用户的位置,使用默认样式
if annotation == mapView.userLocation {
return nil
}
// 标注的标识符
let identifier = "custom"
// 标注的自定义图片
let annotationImage = ["pin.circle.fill", "car.circle.fill", "airplane.circle.fill", "cross.circle.fill"]
// 获取AnnotationView
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
// 判空
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
// 图标,每次随机取一个
annotationView?.image = UIImage(systemName: annotationImage.randomElement()!)
// 显示气泡
annotationView?.canShowCallout = true
// 左边显示的辅助视图
annotationView?.leftCalloutAccessoryView = UIImageView(image: UIImage(systemName: "heart"))
// 右边显示的辅助视图
let button = UIButton(type: .detailDisclosure, primaryAction: UIAction(handler: { _ in
print(annotation.urlString)
}))
annotationView?.rightCalloutAccessoryView = button
// 弹出的位置偏移
annotationView?.calloutOffset = CGPoint(x: -5.0, y: 5.0)
}
return annotationView
}
}
// 点击地图插入一个标注,标注的标题和副标题显示的是标注的具体位置
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchPoint = touches.first?.location(in: mapView)
// 将坐标转换成为经纬度,然后赋值给标注
let coordinate = mapView.convert(touchPoint!, toCoordinateFrom: mapView)
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
let gecoder = CLGeocoder()
// 反地理编码转换成具体的地址
gecoder.reverseGeocodeLocation(location) { placeMarks, _ in
let placeMark = placeMarks?.first
if let placeMark = placeMark {
let flag = MapFlag(title: placeMark.locality, subtitle: placeMark.subLocality, coordinate: coordinate, urlString: "https://www.baidu.com")
self.mapView.addAnnotation(flag)
}
}
}