iOS14 Widget开发踩坑(五)定位与地图的使用

  • 前言
  • 分析
  • 实现案例
    • 地图
    • 定位
  • 问题
    • 适配黑暗模式
      • 2022年12月15日已解决
    • 中心点偏移问题
      • 2022年12月19日已解决
    • 延迟问题
  • 本篇参考文献
  • 小组件参考文献总结

前言

最近又抽出时间来看小组件的问题了,产品也是想要实现一下系统应用 地图 的小组件的样子,并且研究一下小组件定位的实现。

分析

SwiftUI使用MKMapView是使用UIViewRepresentable协议将将MKMapView转化为View来进行使用的。但是在小组件中,无法使用UIViewRepresentable,也无法直接使用MKMapViewMapKit中有Map,但是在主程序可以使用,在小组件上却根本无法加载出来。我很怀疑苹果给自己的系统程序留了后门。

struct ContentView: View {@State var region = MKCoordinateRegion(center:CLLocationCoordinate2D(latitude: 31.203115, longitude: 121.598637), span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01))var body: some View {Map(coordinateRegion: $region, showsUserLocation: true)}
}


但是经过BaiduGoogle的帮助还是找到了一些蛛丝马迹以及参考文章,他们都详细的描述了如何加载地图和调用定位。我只是在这里做一下总结。

实现案例

地图

已经明确需要使用MKMapSnapshotter,以下是MKMapSnapshotter的说明:

extension MKMapSnapshotter {public typealias CompletionHandler = (MKMapSnapshotter.Snapshot?, Error?) -> Void
}
@available(iOS 7.0, *)
open class MKMapSnapshotter : NSObject {public init(options: MKMapSnapshotter.Options)open func start(completionHandler: @escaping MKMapSnapshotter.CompletionHandler) // defaults to the main queueopen func start() async throws -> MKMapSnapshotter.Snapshotopen func start(with queue: DispatchQueue, completionHandler: @escaping MKMapSnapshotter.CompletionHandler)open func start(with queue: DispatchQueue) async throws -> MKMapSnapshotter.Snapshotopen func cancel()open var isLoading: Bool { get }
}extension MKMapSnapshotter {@available(iOS 7.0, *)open class Options : NSObject, NSCopying {@NSCopying open var camera: MKMapCameraopen var mapRect: MKMapRectopen var region: MKCoordinateRegionopen var mapType: MKMapType@available(iOS 13.0, *)@NSCopying open var pointOfInterestFilter: MKPointOfInterestFilter?@available(iOS, introduced: 7.0, deprecated: 13.0, message: "Use pointOfInterestFilter")open var showsPointsOfInterest: Boolopen var showsBuildings: Boolopen var size: CGSize@available(iOS, introduced: 7.0, deprecated: 100000, message: "Use traitCollection.displayScale")open var scale: CGFloat@available(iOS 13.0, *)@NSCopying open var traitCollection: UITraitCollection}
}

使用options进行地图的相关配置,使用MKMapSnapshotter进行快照。闭包返回一个UIImageBool

let mapImageSize = 440.0
func getMapSnapshot(region:MKCoordinateRegion, completionHandler: @escaping (UIImage, Bool) -> Void) {let options = MKMapSnapshotter.Options()options.pointOfInterestFilter = MKPointOfInterestFilter.includingAlloptions.region = regionoptions.size = CGSize(width: mapImageSize, height: mapImageSize)options.showsBuildings = truelet snapshot = MKMapSnapshotter(options: options)snapshot.start { (snapshot, error) inif ((error) != nil) {completionHandler(UIImage(), false)} else {if (snapshot?.image != nil) {completionHandler(snapshot!.image, true)} else {completionHandler(UIImage(), false)}}}
}
// 方法2,可以将用户位置图片直接绘制到截图中,也可以将图片放到视图里再显示,仅供参考
func getMapSnapshot2(region:MKCoordinateRegion, completionHandler: @escaping (Image, Bool) -> Void) {let options = MKMapSnapshotter.Options()options.pointOfInterestFilter = MKPointOfInterestFilter.includingAlloptions.region = regionoptions.size = CGSize(width: mapImageSize, height: mapImageSize)options.showsBuildings = truelet snapshot = MKMapSnapshotter(options: options)snapshot.start { (snapshot, error) inif ((error) != nil) {completionHandler(Image(""), false)} else {if (snapshot?.image != nil) {let snapShotImage = snapshot!.imageif let pinImage = UIImage(named: "tkt_alltrack_startPoint") {UIGraphicsBeginImageContextWithOptions(snapShotImage.size, true, snapShotImage.scale)snapShotImage.draw(at: CGPoint.zero)let fixedPinPoint = CGPoint(x: (options.size.width - pinImage.size.width) / 2, y: (options.size.height - pinImage.size.height) / 2)pinImage.draw(at: fixedPinPoint)let mapImage = UIGraphicsGetImageFromCurrentImageContext()UIGraphicsEndImageContext()DispatchQueue.main.async {if (mapImage != nil) {completionHandler(Image(uiImage: mapImage!), true)} else {completionHandler(Image(""), false)}}} else {completionHandler(Image(""), false)}} else {completionHandler(Image(""), false)}}}
}

getTimeLine方法中进行调用,其中MKCoordinateSpan的数值越小,地图缩放等级越大。

    func getTimeline(in context: Context, completion: @escaping (Timeline<LocationWidgetEntry>) -> ()) {let currentDate = Date()let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!// 这里使用了一个固定的坐标,可以通过主程序将用户坐标传递过来let coordinate = CLLocationCoordinate2D(latitude: 31.203115, longitude: 121.598637)let region = MKCoordinateRegion(center:coordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))getMapSnapshot(region: region) { image, success inlet entry    = LocationWidgetEntry(date: currentDate,image: image, success: success)let timeline = Timeline(entries: [entry], policy: .after(refreshDate))completion(timeline)}}

再写一个简单的视图

struct LocationWidgetEntryView : View {var entry: LocationWidgetProvider.Entryvar body: some View {if (entry.success) {ZStack(alignment: .center) {Image(uiImage: entry.image).resizable().frame(width: mapImageSize, height: mapImageSize).scaledToFill()Image("tkt_alltrack_startPoint").resizable().frame(width: 20, height: 20).shadow(color: .gray, radius: 5)}} else {Text("截图失败了")}}
}

定位

使用CLLocationManager可以在小组件进行定位,前提是需要在小组件的Info.plist文件中添加权限

Widget Wants Location 为 YES 时程序位置权限中会出现“使用App或小组件期间”选项。需要请求权限时也会弹出相应提示。
当主程序定位权限为“始终”或“使用App或小组件期间”两个选项时,小组件都可以请求到位置。


判断小组件是否支持定位可以使用字段authorizedForWidgetUpdates

代码

var widgetLocationManager = WidgetLocationManager()func getTimeline(in context: Context, completion: @escaping (Timeline<LocationWidgetEntry>) -> ()) {let currentDate = Date()let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!widgetLocationManager.fetchLocation(handler: { location inprint(location)let entry    = LocationWidgetEntry(date: currentDate,coordinate: location.coordinate)let timeline = Timeline(entries: [entry], policy: .after(refreshDate))completion(timeline)})
}class WidgetLocationManager: NSObject, CLLocationManagerDelegate {var locationManager: CLLocationManager?private var handler: ((CLLocation) -> Void)?override init() {super.init()DispatchQueue.main.async {self.locationManager = CLLocationManager()self.locationManager!.delegate = selfif self.locationManager!.authorizationStatus == .notDetermined {self.locationManager!.requestWhenInUseAuthorization()}}}func fetchLocation(handler: @escaping (CLLocation) -> Void) {self.handler = handlerself.locationManager!.requestLocation()}func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {self.handler!(locations.last!)}func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {print(error)}
}struct LocationWidgetEntryView : View {var entry: LocationWidgetProvider.Entryvar body: some View {VStack() {Text("latitude:\(entry.coordinate.latitude)")Text("longitude:\(entry.coordinate.longitude)")}}
}

问题

目前,还有一下问题没有解决,如果有人知道请联系我,欢迎大家一起讨论解决方案!!!!!

适配黑暗模式

地图的小组件的地图是可以在黑暗模式时自动切换到黑暗模式的,但MKMapSnapshotter的截图只能有亮色模式的。目前还不知道怎么解决。

2022年12月15日已解决

对于MKMapSnapshotter有如下方法可以设置暗黑模式

let options = MKMapSnapshotter.Options()
options.pointOfInterestFilter = MKPointOfInterestFilter.includingAll
options.region = region
options.size = size
options.showsBuildings = true
options.traitCollection = UITraitCollection(traitsFrom: [options.traitCollection,UITraitCollection(userInterfaceStyle: .dark)
])

所以我们可以对截图方法稍微进行改造,使其可以接收是否黑暗模式的参数

func getMapSnapshot(region:MKCoordinateRegion, size: CGSize ,isDark:Bool ,completionHandler: @escaping (UIImage, Bool) -> Void) {let options = MKMapSnapshotter.Options()options.pointOfInterestFilter = MKPointOfInterestFilter.includingAlloptions.region = regionoptions.size = sizeoptions.showsBuildings = trueoptions.traitCollection = UITraitCollection(traitsFrom: [options.traitCollection,UITraitCollection(userInterfaceStyle: isDark ? .dark : .light)])let snapshot = MKMapSnapshotter(options: options)snapshot.start { (snapshot, error) inif ((error) != nil) {completionHandler(UIImage(), false)} else {if (snapshot?.image != nil) {completionHandler(snapshot!.image, true)} else {completionHandler(UIImage(), false)}}}
}
// 但是这样写的话,调用就会变成
getMapSnapshot(region: region, size: context.displaySize, isDark: false) { lightImage, lightSuccess ingetMapSnapshot(region: region, size: context.displaySize,isDark: true) { darkImage, darkSuccess inlet entry = LocationWidgetEntry(date: currentDate, lightImage: lightImage, darkImage: darkImage, success: (lightSuccess && darkSuccess))let timeline = Timeline(entries: [entry], policy: .after(refreshDate))completion(timeline)}
}

这样写不够优雅,我们可以用GCD包裹一下。

func getMapSnapshotWithGCD(region:MKCoordinateRegion, size: CGSize ,completionHandler: @escaping (_ lightImage: UIImage, _ darkImage: UIImage, _ success: Bool) -> Void) {let group = DispatchGroup()let queue = DispatchQueue(label: "com.widget.MKMapSnapshotter")var lightSuccess = falsevar darkSuccess = falsevar lightImage = UIImage()var darkImage = UIImage()group.enter()queue.async {getMapSnapshot(region: region, size: size, isDark: false) { image, success inlightImage = imagelightSuccess = successgroup.leave()}}group.enter()queue.async {getMapSnapshot(region: region, size: size, isDark: true) { image, success indarkImage = imagedarkSuccess = successgroup.leave()}}group.notify(queue: DispatchQueue.main) {let doubleSuccess = (lightSuccess && darkSuccess)completionHandler(lightImage, darkImage, doubleSuccess)}
}// 在TimeLine中调用就会更优雅一些
getMapSnapshotWithGCD(region: region, size: context.displaySize) { lightImage, darkImage, success inlet entry = LocationWidgetEntry(date: currentDate, lightImage: lightImage, darkImage: darkImage, success: success)let timeline = Timeline(entries: [entry], policy: .after(refreshDate))completion(timeline)
}// 视图使用环境变量进行判断
struct LocationWidgetEntryView : View {@Environment(\.widgetFamily) var family@Environment(\.colorScheme) var colorSchemevar entry: LocationWidgetProvider.Entryvar body: some View {if (family == .systemLarge) {if (entry.success) {ZStack(alignment: .center) {Image(uiImage: colorScheme == .dark ? entry.darkImage : entry.lightImage).resizable().scaledToFill()Ellipse().fill(Color.gray.opacity(0.8)).frame(width: 15, height: 8)ZStack(alignment: .top) {Image("nh_annotation_bg_boy").resizable().frame(width: 72, height: 80)Image("icon_upload_avatar_boy").resizable().frame(width: 60, height: 60).offset(y:6)}.offset(y:-40)}} else {VStack {Text("截图失败了")}}} else  {if (entry.success) {ZStack(alignment: .center) {Image(uiImage: colorScheme == .dark ? entry.darkImage : entry.lightImage).resizable().scaledToFill()Image("tkt_alltrack_startPoint").resizable().frame(width: 20, height: 20).shadow(color: .gray, radius: 5)}} else {VStack {Text("截图失败了")}}}}
}

这样写完,在切换系统的显示模式的时候,地图的截图也会跟随替换颜色了。

中心点偏移问题

MKMapSnapshotter截图出来,用户坐标会显示在中心,但是设计师想要将用户坐标进行偏移(小尺寸向下15,向右25;中尺寸向下15向右90),然而使用将图片放大,移动显示图片中心点的方法是不可行的,因为中尺寸需要宽度增加180,超出了截图的限制,导致失败。

2022年12月19日已解决

只能算出新的中心点的经纬度坐标去截图,再将大头针元素放置到原来的经纬度坐标上,这样就可以只截图小组件尺寸的图片。

struct LocationWidgetEntry: TimelineEntry {let date: Datelet offsetX:CGFloatlet offsetY:CGFloatlet lightImage: UIImagelet darkImage: UIImagelet success:Bool
}struct LocationWidgetProvider: TimelineProvider {var widgetLocationManager = WidgetLocationManager()let emptyEntry = LocationWidgetEntry(date: Date(),offsetX: 0,offsetY: 0,lightImage: UIImage(), darkImage: UIImage(), success: false)func placeholder(in context: Context) -> LocationWidgetEntry {emptyEntry}func getSnapshot(in context: Context, completion: @escaping (LocationWidgetEntry) -> ()) {completion(emptyEntry)}func getTimeline(in context: Context, completion: @escaping (Timeline<LocationWidgetEntry>) -> ()) {// 当前时间let currentDate = Date()// 刷新时间:15分钟后let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!// 地图精细度let delta = 0.01// 纬度let la = 31.203115// 经度let lo = 121.598637// Y轴偏移量let offsetY = 15.0// X轴偏移量var offsetX = 25.0if context.family == .systemMedium {offsetX = 90.0}// 用户坐标let coordinate = CLLocationCoordinate2D(latitude:la , longitude: lo)// 用户区域let region = MKCoordinateRegion(center:coordinate, span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta))// 计算用地图let map = MKMapView(frame: CGRect(x: 0, y: 0, width: context.displaySize.width, height: context.displaySize.height))map.region = region// 用户点let point = map.convert(coordinate, toPointTo: map)// 新中心点let cPoint = CGPoint(x: point.x - offsetX, y: point.y - offsetY)// 新中心坐标let cCoordinate = map.convert(cPoint, toCoordinateFrom: map)// 新中心区域let cRegion = MKCoordinateRegion(center:cCoordinate, span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta))// 截图getMapSnapshotWithGCD(region: cRegion, size: context.displaySize) { lightImage, darkImage, success inlet entry = LocationWidgetEntry(date: currentDate, offsetX: offsetX, offsetY: offsetY, lightImage: lightImage, darkImage: darkImage, success: success)let timeline = Timeline(entries: [entry], policy: .after(refreshDate))completion(timeline)}}
}struct LocationWidgetEntryView : View {@Environment(\.widgetFamily) var family@Environment(\.colorScheme) var colorSchemevar entry: LocationWidgetProvider.Entryvar body: some View {if (entry.success) {ZStack(alignment: .center) {Image(uiImage: colorScheme == .dark ? entry.darkImage : entry.lightImage) // 适配暗黑模式.resizable().scaledToFit()Ellipse().fill(Color.gray.opacity(0.8)).frame(width: 15, height: 8).offset(x: entry.offsetX, y: entry.offsetY) // 偏移到用户位置ZStack(alignment: .top) {Image("nh_annotation_bg_boy").resizable().frame(width: 72, height: 80)Image("icon_upload_avatar_boy").resizable().frame(width: 60, height: 60).offset(y:6)}.offset(x: entry.offsetX, y: (entry.offsetY - 40)) // 偏移到用户位置,并升高到自己的底部对齐}} else {VStack {Text("截图失败了")}}}
}

延迟问题

使用上面的代码请求定位,结果返回有5s-20s的延迟,不知道是什么原因。

本篇参考文献

《10 Tips on Developing iOS 14 Widgets》
《ShowingMapsInWidgets》
《SHOWING MAP PREVIEW WITH MKMAPSNAPSHOTTER》
《Fetching current location in iOS 14 Widget》
《Accessing Location Information in Widgets》

小组件参考文献总结

《Widgets》
《How to create Widgets in iOS 14 in Swift》
《Add configuration and intelligence to your widgets》
《Creating a Widget Extension》
《Keeping a Widget Up To Date》
《Making a Configurable Widget》
《SwiftUI-Text》
《混编之oc调用swift》
《从开发者的角度看 iOS 14 小组件》
《iOS14WidgetKit开发实战1-4》
《iOS14 Widget 开发相关及易报错地方处理》
《iOS小组件Widget踩坑》
《iOS小组件Widget从0到1开发》
《Swift-Realm数据库的使用详解》
《iOS14 WidgetKit小试牛刀-用户配置与intent》
【iOS14】仿网易云桌面小组件(一)
【iOS14】仿网易云桌面小组件(二)
【iOS14】仿网易云桌面小组件(三)
【iOS14】仿网易云桌面小组件(四)
《iOS14 Widget开发踩坑(一)修正版-初识别与刷新》
《iOS14 Widget开发踩坑(二)修正版-多个小组件》
《iOS14 Widget开发踩坑(三)数据通信与用户配置》
《iOS14 Widget开发踩坑(四)伪透明的实现和其他研究》

iOS14 Widget开发踩坑(五)定位与地图的使用相关推荐

  1. 微信开发踩坑系列一之Native支付

    微信开发踩坑系列一之Native支付 1.前言 1.1.文章说明 1.2.微信支付简介 1.3.项目技术栈 2.Native支付开发 2.1.官方描述 2.2.两种模式介绍 2.3.开发前准备工作 2 ...

  2. 【浙政钉】微信-专有钉钉小程序-开发踩坑实记

    文章目录 ⭐[浙政钉]微信-专有钉钉小程序-开发踩坑实记 ⭐ 创建项目 ⭐ 转化方案 ⭐ 政务钉钉调试 ⭐ 上传发布 ⭐[浙政钉]微信-专有钉钉小程序-开发踩坑实记 最近有个需求,要将微信小程序转为浙 ...

  3. 微信小程序开发踩坑合集

    微信搜索:凯小白学编程   回复 小程序   领取1000套小程序源码 本文分享一下开发小程序是遇到的一些问题.展示了曾经开发过的两个小程序中遇到的坑 下一篇文章预告:<Maven入门> ...

  4. mybatis mapper.xml dtd_全栈开发踩坑之路4-用MyBatis实现服务

    1.前言 上一篇文章介绍了如何设计后端的Mysql数据库:Alex Wang:全栈开发踩坑之路3-MySql数据库设计,本文介绍如何用MyBatis实现后端服务. 本后端项目的Github地址(撰写中 ...

  5. 微信vue路由跳转兼容_Vue微信公众号开发踩坑记录

    需求 微信授权登录(基于公众号的登录方案) 接入JS-SDK实现图片上传,分享等功能 现状及难点 采用的Vue框架,前后端分离模式(vue工程仅作为客户端),用户通过域名访问的是客户端,但是微信授权中 ...

  6. 微信开发踩坑之旅 之 开发准备及服务器配置

    在工作和兴趣的机缘巧合之下,我开始接触微信开发.在这里简单记述自己的微信开发踩坑之旅. 首先,由于本人标准的理工科生,记述的语言有所不足,我尽量说明准确和详细点. 本文记述主线 ·申请公众号 ·公众号 ...

  7. 乐视体感摄像头开发踩坑记录

    乐视三合一体感相机开发踩坑记录 第一次用Cmake,以下如有错误请大佬指正 开发环境: Linux ARM(树莓派4) AstraSDK-v2.1.3 Arm/Arm64(https://orbbec ...

  8. 「Java」基于Mirai的qq机器人开发踩坑笔记(其一)

    目录 0. 前置操作 I. 安装MCL II. MCL自动登录配置 III. 安装IDEA插件 1. 新建Mirai项目 2. 编写主类 3. 添加外部依赖 4. IDEA运行 5. 插件打包 6. ...

  9. 「Java」基于Mirai的qq机器人开发踩坑笔记(其二)

    目录 0. 配置机器人 1. onLoad方法 2. onEnable方法 3. 消息属性 4. 消息监听 I. 好友消息 II. 群聊消息 III. 无差别消息 5. 发送消息 I. 文本消息 II ...

最新文章

  1. Vbs脚本编程简明教程之十
  2. 由Linux内核bug引起SSH登录缓慢问题的排查与解决
  3. Linux下的buffer与cache
  4. 联想sr950配置raid卡_联想ThinkServerrd服务器raid卡设置教程LSIiraid卡设置教程
  5. 沃尔玛痛失世界最大零售商 电商凶猛!
  6. Python与C++动态链接库交互 win10平台
  7. UART接口算法移植加密芯片的调试技巧——算法调试
  8. ElementUI:tree鼠标浮动在某个节点背景色以及点击背景色修改
  9. 【转】Quartz.NET快速入门指南
  10. 浅谈微信卡券功能开发(2)
  11. 机器学习模型训练全流程!
  12. Linux电源管理(五)thermal【转】
  13. SparkEnv源码解读
  14. Android Unable to delete file: build\intermediates\manifests\full\debug\AndroidManifest.xm
  15. 利用正则表达式来验证邮箱
  16. slice,splice,split区别和作用
  17. 谷歌地图 替代_Google地图的替代品
  18. 国家铁塔最快3个月后挂牌 或导致资费上涨
  19. 泰拳的快感之二——我看《冬荫功》
  20. 2021消防设施操作员(中级)岗位考试模拟题库判断自动系统知识部分

热门文章

  1. html+css使用空白标签巧妙实现不同尺寸的图片在容器里垂直居中的方法
  2. 【测试】——软件测试的W模型和V模型
  3. 计算机网络 数据链路层 要解决的三个问题 差错检测
  4. Solidworks 与 ROS URDF
  5. win-nodejs安装
  6. Go语言编写并发小爬虫
  7. web day02 表格 表单及HTML常用的表单控件
  8. Android 多媒体之音频
  9. ChatGPT接入个人网站指导
  10. 计算机毕业设计-springboot课堂签到小程序-学生考勤打卡小程序