Coding01

Coding 点滴

0%

学习 iOS Widgets 开发之天气小组件(二)

本文主要学习到:

  1. App Group 数据共享知识
  2. Widget 获取 Location 权限

为了达到如上图的位置信息,需要做到两点:

  1. 在主 App 上使用 Location;
  2. 在 Widget 小组件上使用 Location。

关于第一点,可以直接在 APP 的 Info.plist 增加:

1
2
<key>NSLocationWhenInUseUsageDescription</key>
<string>Use Location</string>

在 Widget target 的 Info.plist 增加:

1
2
<key>NSWidgetWantsLocation</key>
<true/>

Location

创建热门城市列表,这里需要实现函数:

1
func provideLocationOptionsCollection(for intent: WeatherIntent, with completion: @escaping (INObjectCollection<CLPlacemark>?, Error?) -> Void)

这里的 [CLPlacemark] 数组,需要用到 CLPlacemark 初始化函数,而且也只有在 Intents Framework 中才有。

init(location:name:postalAddress:)

App extensions built with the Intents framework can use this method to create a placemark from existing location and address data. For example, an app that offers a ride service might create a new placemark when resolving a user’s pickup or drop-off location. The returned placemark contains only the data that you provide.

按照之前的扩展,需要我们增加位置选择,罗列主要热门城市:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import Intents
import Contacts

class IntentHandler: INExtension, WeatherIntentHandling {
private let geocoder = CLGeocoder()
private let items = [
CLPlacemark(location: CLLocation(latitude: 39.91667, longitude: 116.41667), name: "北京", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 34.50000, longitude: 121.43333), name: "上海", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 39.13333, longitude: 117.20000), name: "天津", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 22.20000, longitude: 114.10000), name: "香港", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 23.16667, longitude: 113.23333), name: "广州", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 22.30000, longitude: 113.51667), name: "珠海", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 22.61667, longitude: 114.06667), name: "深圳", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 30.26667, longitude: 120.20000), name: "杭州", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 29.56667, longitude: 106.45000), name: "重庆", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 36.06667, longitude: 120.33333), name: "青岛", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 24.46667, longitude: 118.10000), name: "厦门", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 26.08333, longitude: 119.30000), name: "福州", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 36.03333, longitude: 103.73333), name: "兰州", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 26.56667, longitude: 106.71667), name: "贵阳", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 28.21667, longitude: 113.00000), name: "长沙", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 32.05000, longitude: 118.78333), name: "南京", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 28.68333, longitude: 115.90000), name: "南昌", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 41.80000, longitude: 123.38333), name: "沈阳", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 37.86667, longitude: 112.53333), name: "太原", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 30.66667, longitude: 104.06667), name: "成都", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 29.60000, longitude: 91.00000), name: "拉萨", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 43.76667, longitude: 87.68333), name: "乌鲁木齐", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 25.05000, longitude: 102.73333), name: "昆明", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 34.26667, longitude: 108.95000), name: "西安", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 36.56667, longitude: 101.75000), name: "西宁", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 38.46667, longitude: 106.26667), name: "银川", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 46.06667, longitude: 122.08333), name: "兰浩特", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 45.75000, longitude: 126.63333), name: "哈尔滨", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 43.88333, longitude: 125.35000), name: "长春", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 30.51667, longitude: 114.31667), name: "武汉", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 34.76667, longitude: 113.65000), name: "郑州", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 38.03333, longitude: 114.48333), name: "石家庄", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 18.20000, longitude: 109.50000), name: "三亚", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 20.01667, longitude: 110.35000), name: "海口", postalAddress: nil),
CLPlacemark(location: CLLocation(latitude: 22.20000, longitude: 113.50000), name: "澳门", postalAddress: nil)
]

func provideLocationOptionsCollection(for intent: WeatherIntent, with completion: @escaping (INObjectCollection<CLPlacemark>?, Error?) -> Void) {
completion(INObjectCollection(items: self.items), nil)
}
}

看看运行效果:

我的定位

获取地理位置,查看 Accessing Location Information in Widgets

就如文章说的,因为我是多个 Widgets 在同一个 APP 中,所以采用后面一种方案:

Isolate Location Usage With Multiple Widget Extensions
If your app provides multiple widgets and only some of the widgets use location, separate your widgets into multiple extensions. Add the NSWidgetWantsLocation to the extension that contains widgets that use location. This allows the system to only prompt the user for the widgets that use location information, and makes it more contextually relevant for users.

How to manage location data access for iPhone and iPad widgets

A widget like Weather may requesting access to your location. “Widgets may use your location for up to 15 minutes after being viewed to stay up to date,” reads the permission prompt.

需要对 Location 做详细 Test:

Test Your Widget in Real-World Scenarios
Due to the timeline-based updates of widgets, and the variability of a widget’s in-use status, it’s important to test widgets that use location in real-world scenarios. For example, create test scenarios that:

  • Extend the app’s location authorization when adding the widget.

  • Don’t extend the app’s location authorization when adding the widget.

  • Change the app’s authorization in Settings > Privacy > Location Services after the widget is added.

  • Change the widget’s approval of location authorization in Settings > Privacy > Location Services after the widget is added.

  • Add the widget to Home screen pages that are both frequently and infrequently viewed.

Because widgets receive a limited number of refreshes every day, test your widget over multiple days.

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* authorizedForWidgetUpdates
*
* Discussion:
* Returns true if widgets of the calling application may be eligible to receive location updates.
*
* If the calling application has authorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse,
* and authorizedForWidgetUpdates == YES, then widgets will be able to get location updates if called upon
* to refresh within a few minutes of having been seen.
*
* If the calling application has authorizationStatus == kCLAuthorizationStatusAuthorizedAlways,
* then authorizedForWidgetUpdates will always be YES.
*/

这一步是整个 Widget 的重点。

数据共享

Sharing data with a Widget

在主 APP target 增加 App Group 能力:

增加容器:

这样就可以在 Widget target 增加 App Group 能力和选择容器。

扩展 FileManager 类:

1
2
3
4
5
6
7
8
9
import Foundation

extension FileManager {
static func sharedContainerURL() -> URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.your.prefix.app.contents"
)!
}
}

同时,增加读写方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static func saveLocation(data: [String: String], isMy: Bool) {
let fileName = isMy ? "mylocation.json" : "otherlocation.json"
let archiveURL = FileManager.sharedContainerURL().appendingPathComponent(fileName)
let encoder = JSONEncoder()
if let dataToSave = try? encoder.encode(data) {
do {
try dataToSave.write(to: archiveURL)
} catch {
// TODO: ("Error: Can't save Counters")
NSLog("save value error")
return;
}
}
}

static func getLocation(isMy: Bool) -> [String: String] {
let fileName = isMy ? "mylocation.json" : "otherlocation.json"
let defaultData = [
"latitude": "39.91667",
"longitude": "116.41667",
"date": Date().toString()
]

let archiveURL = FileManager.sharedContainerURL().appendingPathComponent(fileName)

guard let codeData = try? Data(contentsOf: archiveURL) else { return defaultData }

let decoder = JSONDecoder()

let locationData = (try? decoder.decode([String : String].self, from: codeData)) ?? defaultData

return locationData
}

有了以上的整理,接下来就是在 Location 获取后,存入文件,在需要的时候,直接读取,如在我们的获选区域里加入「我的位置」:

1
2
3
4
5
func provideLocationOptionsCollection(for intent: WeatherIntent, with completion: @escaping (INObjectCollection<CLPlacemark>?, Error?) -> Void) {
let locationData = FileManager.getLocation(isMy: true)
self.items.insert(CLPlacemark(location: CLLocation(latitude: Double(locationData["latitude"]!)!, longitude: Double(locationData["longitude"]!)!), name: "我的位置", postalAddress: nil), at: 0)
completion(INObjectCollection(items: self.items), nil)
}

381607765644_.pic

总结

到此,基本完成「天气」小组件的开发工作。

Welcome to my other publishing channels