/
Watchface.swift
430 lines (371 loc) · 19.8 KB
/
Watchface.swift
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
import Foundation
struct Watchface {
var metadata: Metadata
struct Metadata: Codable {
var version: Int = 2
var device_size = 2 // 38mm, 42mm?
var complication_sample_templates: ComplicationPositionDictionary<ComplicationTemplate>
struct ComplicationPositionDictionary<Value: Codable>: Codable {
var top: Value?
var bottom: Value?
var top_left: Value?
var top_right: Value?
var bottom_center: Value?
private enum CodingKeys: String, CodingKey {
case top, bottom
case top_left = "top-left"
case top_right = "top-right"
case bottom_center = "bottom-center"
}
}
enum ComplicationTemplate: Codable {
case utilitarianSmallFlat(CLKComplicationTemplateUtilitarianSmallFlat)
case utilitarianLargeFlat(CLKComplicationTemplateUtilitarianLargeFlat)
case circularSmallSimpleText(CLKComplicationTemplateCircularSmallSimpleText)
case circularSmallSimpleImage(CLKComplicationTemplateCircularSmallSimpleImage)
init(from decoder: Decoder) throws {
if let t = ((try? CLKComplicationTemplateUtilitarianSmallFlat(from: decoder))
.flatMap {$0.class == "CLKComplicationTemplateUtilitarianSmallFlat" ? $0 : nil}) {
self = .utilitarianSmallFlat(t)
return
}
if let t = ((try? CLKComplicationTemplateUtilitarianLargeFlat(from: decoder))
.flatMap {$0.class == "CLKComplicationTemplateUtilitarianLargeFlat" ? $0 : nil}) {
self = .utilitarianLargeFlat(t)
return
}
if let t = ((try? CLKComplicationTemplateCircularSmallSimpleText(from: decoder))
.flatMap {$0.class == "CLKComplicationTemplateCircularSmallSimpleText" ? $0 : nil}) {
self = .circularSmallSimpleText(t)
return
}
if let t = ((try? CLKComplicationTemplateCircularSmallSimpleImage(from: decoder))
.flatMap {$0.class == "CLKComplicationTemplateCircularSmallSimpleImage" ? $0 : nil}) {
self = .circularSmallSimpleImage(t)
return
}
let anyTemplate = try? CLKComplicationTemplateAny(from: decoder)
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "unknown ComplicationTemplate type: \(anyTemplate?.class ?? "(unknown structure)")"))
}
func encode(to encoder: Encoder) throws {
switch self {
case .utilitarianSmallFlat(let t): try t.encode(to: encoder)
case .utilitarianLargeFlat(let t): try t.encode(to: encoder)
case .circularSmallSimpleText(let t): try t.encode(to: encoder)
case .circularSmallSimpleImage(let t): try t.encode(to: encoder)
}
}
private struct CLKComplicationTemplateAny: Codable {
var `class`: String
}
}
enum CLKTextProvider: Codable {
case date(CLKDateTextProvider)
case time(CLKTimeTextProvider)
case compound(CLKCompoundTextProvider)
case simple(CLKSimpleTextProvider)
init(from decoder: Decoder) throws {
if let p = ((try? CLKDateTextProvider(from: decoder))
.flatMap {$0.class == "CLKDateTextProvider" ? $0 : nil}) {
self = .date(p)
return
}
if let p = ((try? CLKTimeTextProvider(from: decoder))
.flatMap {$0.class == "CLKTimeTextProvider" ? $0 : nil}) {
self = .time(p)
return
}
if let p = ((try? CLKCompoundTextProvider(from: decoder))
.flatMap {$0.class == "CLKCompoundTextProvider" ? $0 : nil}) {
self = .compound(p)
return
}
if let p = ((try? CLKSimpleTextProvider(from: decoder))
.flatMap {$0.class == "CLKSimpleTextProvider" ? $0 : nil}) {
self = .simple(p)
return
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "unknown CLKTextProvider type"))
}
func encode(to encoder: Encoder) throws {
switch self {
case .date(let p): try p.encode(to: encoder)
case .time(let p): try p.encode(to: encoder)
case .compound(let p): try p.encode(to: encoder)
case .simple(let p): try p.encode(to: encoder)
}
}
}
struct CLKDateTextProvider: Codable {
var `class`: String = "CLKDateTextProvider"
var date: Date = .init()
var _uppercase: Bool = true
var calendarUnits: Int = 528
}
struct CLKTimeTextProvider: Codable {
var `class`: String = "CLKTimeTextProvider"
var date: Date = .init()
var timeZone: String = "US/Pacific"
}
struct CLKSimpleTextProvider: Codable {
var `class`: String = "CLKSimpleTextProvider"
var text: String = "サンフランシスコ"
}
struct CLKCompoundTextProvider: Codable {
var `class`: String = "CLKCompoundTextProvider"
var textProviders: [CLKTextProvider] = [.time(.init()), .simple(.init())]
var format_segments: [String] = ["", " ", ""]
private enum CodingKeys: String, CodingKey {
case `class`
case textProviders
case format_segments = "format segments"
}
}
struct CLKComplicationTemplateUtilitarianSmallFlat: Codable {
var `class`: String = "CLKComplicationTemplateUtilitarianSmallFlat"
var version: Int = 30000
var creationDate: Date = .init()
var textProvider: CLKTextProvider = .date(.init())
}
struct CLKComplicationTemplateUtilitarianLargeFlat: Codable {
var `class`: String = "CLKComplicationTemplateUtilitarianSmallFlat"
var version: Int = 30000
var creationDate: Date = .init()
var textProvider: CLKTextProvider = .date(.init())
}
struct CLKComplicationTemplateCircularSmallSimpleText: Codable {
var `class`: String = "CLKComplicationTemplateCircularSmallSimpleText"
var version: Int = 30000
var creationDate: Date = .init()
var textProvider: CLKTextProvider = .date(.init())
var tintColor: TintColor
}
struct CLKComplicationTemplateCircularSmallSimpleImage: Codable {
var `class`: String = "CLKComplicationTemplateCircularSmallSimpleImage"
var version: Int = 30000
var creationDate: Date = .init()
var imageProvider: ImageProvider
var tintColor: TintColor
struct ImageProvider: Codable {
var onePieceImage: OnePieceImage
struct OnePieceImage: Codable {
var file_name: String // "13CF2F31-40CD-4F66-8C55-72A03A46DDC3.png" where .watchface/complicationData/top-right/
var scale: Int // 3
var renderingMode: Int // 0
private enum CodingKeys: String, CodingKey {
case file_name = "file name"
case scale, renderingMode
}
}
}
}
struct TintColor: Codable {
var red: Double
var green: Double
var blue: Double
var alpha: Double
}
var complications_names: ComplicationPositionDictionary<String>
var complications_item_ids: ComplicationPositionDictionary<Int>
var complications_bundle_ids: ComplicationPositionDictionary<String>? // com.apple.weather.watchapp, com.apple.HeartRate, com.apple.NanoCalendar
}
var face: Face
struct Face: Codable {
var version: Int = 4
var face_type: FaceType
enum FaceType: String, Codable {
case photos // has [top, bottom]
case kaleidoscope // has [top-left, top-right, bottom-center]
}
var resource_directory: Bool = true
var customization: Customization
struct Customization: Codable {
var color: String? // photo: "none"
var content: String // photo: "custom", kaleidoscope: "asset custom"
var position: String? // "top"
var style: String? // kaleidoscope: "radial"
}
var complications: Complications?
struct Complications: Codable {
var top: Item?
var top_left: Item?
var top_right: Item?
var bottom_center: Item?
struct Item: Codable {
var app: String // "date", "weather", "heartrate"
}
private enum CodingKeys: String, CodingKey {
case top
case top_left = "top left"
case top_right = "top right"
case bottom_center = "bottom center"
}
}
private enum CodingKeys: String, CodingKey {
case version
case customization
case complications
case face_type = "face type"
case resource_directory = "resource directory"
}
}
var snapshot: Data
var no_borders_snapshot: Data
// var device_border_snapshot: Data?
var resources: Resources
struct Resources {
var images: Metadata
var files: [String: Data] // filename -> content
struct Metadata: Codable {
var imageList: [Item]
var version: Int = 1
struct Item: Codable {
struct Analysis: Codable {
var bgBrightness: Double
var bgHue: Double
var bgSaturation: Double
var coloredText: Bool
var complexBackground: Bool
var shadowBrightness: Double
var shadowHue: Double
var shadowSaturation: Double
var textBrightness: Double
var textHue: Double
var textSaturation: Double
var version: Int = 1
}
var topAnalysis: Analysis? // photos has some, kaleidoscope has none
var leftAnalysis: Analysis? // photos has some, kaleidoscope has none
var bottomAnalysis: Analysis? // photos has some, kaleidoscope has none
var rightAnalysis: Analysis? // photos has some, kaleidoscope has none
var imageURL: String
var irisDuration: Double = 3
var irisStillDisplayTime: Double = 0
var irisVideoURL: String
var isIris: Bool = true
/// required for watchface sharing... it seems like PHAsset local identifier "UUID/L0/001". an empty string should work anyway.
var localIdentifier: String
var modificationDate: Date = .init()
var cropH: Double = 480
var cropW: Double = 384
var cropX: Double = 0
var cropY: Double = 0
var originalCropH: Double
var originalCropW: Double
var originalCropX: Double
var originalCropY: Double
}
}
}
var complicationData: ComplicationData? = nil
struct ComplicationData {
var top_left: [String: Data]? // filename -> content
var top_right: [String: Data]? // filename -> content
var bottom_center: [String: Data]? // filename -> content
}
}
extension Watchface {
init(fileWrapper: FileWrapper) throws {
guard let metadata_json = fileWrapper.fileWrappers?["metadata.json"]?.regularFileContents else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "metadata.json not found"))
}
let metadata = try JSONDecoder().decode(Watchface.Metadata.self, from: metadata_json)
guard let face_json = fileWrapper.fileWrappers?["face.json"]?.regularFileContents else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "face.json not found"))
}
let face = try JSONDecoder().decode(Watchface.Face.self, from: face_json)
guard let snapshot = fileWrapper.fileWrappers?["snapshot.png"]?.regularFileContents else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "snapshot.png not found"))
}
guard let no_borders_snapshot = fileWrapper.fileWrappers?["no_borders_snapshot.png"]?.regularFileContents else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "no_borders_snapshot.png not found"))
}
// let device_border_snapshot = fileWrapper.fileWrappers?["device_border_snapshot.png"]?.regularFileContents
guard let resources = fileWrapper.fileWrappers?["Resources"]?.fileWrappers else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Resources/ not found"))
}
guard let resources_metadata_plist = resources["Images.plist"]?.regularFileContents else {
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Images.plist not found"))
}
let resources_metadata = try PropertyListDecoder().decode(Watchface.Resources.Metadata.self, from: resources_metadata_plist)
let complicationData = fileWrapper.fileWrappers?["complicationData"]?.fileWrappers
self.init(
metadata: metadata,
face: face,
snapshot: snapshot,
no_borders_snapshot: no_borders_snapshot,
// device_border_snapshot: device_border_snapshot,
resources: Watchface.Resources(images: resources_metadata, files: resources_metadata.imageList.flatMap {[$0.imageURL, $0.irisVideoURL]}.reduce(into: [:]) {$0[$1] = resources[$1]?.regularFileContents}), // TODO: .pathfinders for kaleidoscope
complicationData: complicationData.map {Watchface.ComplicationData(
top_left: $0["top-left"].flatMap {$0.fileWrappers}.map {$0.mapValues {$0.regularFileContents ?? Data()}},
top_right: $0["top-right"].flatMap {$0.fileWrappers}.map {$0.mapValues {$0.regularFileContents ?? Data()}},
bottom_center: $0["bottom-center"].flatMap {$0.fileWrappers}.map {$0.mapValues {$0.regularFileContents ?? Data()}})}
)
}
/// check a lossy reading
func isEqualToFileWrapper(anotherFileWrapper right: FileWrapper) -> Bool {
guard let left = try? FileWrapper(watchface: self) else { return false }
guard left.isDirectory == right.isDirectory else { return false }
func isEqualJSONFiles(left: FileWrapper, right: FileWrapper, filename: String) -> Bool {
let l = left.fileWrappers?[filename]?.regularFileContents.flatMap {try? JSONSerialization.jsonObject(with: $0, options: []) as? NSDictionary}
let r = right.fileWrappers?[filename]?.regularFileContents.flatMap {try? JSONSerialization.jsonObject(with: $0, options: []) as? NSDictionary}
if l != r {
NSLog("%@", "detect differences in \(filename):\nleft: \(l.debugDescription)\n\nright:\(r.debugDescription)")
}
return l == r
}
func isEqualPropertyListFiles(left: FileWrapper, right: FileWrapper, filename: String) -> Bool {
let l = left.fileWrappers?[filename]?.regularFileContents.flatMap {try? PropertyListSerialization.propertyList(from: $0, options: [], format: nil) as? NSDictionary}
let r = right.fileWrappers?[filename]?.regularFileContents.flatMap {try? PropertyListSerialization.propertyList(from: $0, options: [], format: nil) as? NSDictionary}
if l != r {
NSLog("%@", "detect differences in \(filename):\nleft: \(l.debugDescription)\n\nright:\(r.debugDescription)")
}
return l == r
}
func isEqualDataFiles(left: FileWrapper, right: FileWrapper, filename: String) -> Bool {
let l = left.fileWrappers?[filename]?.regularFileContents
let r = right.fileWrappers?[filename]?.regularFileContents
if l != r {
NSLog("%@", "detect differences in \(filename):\nleft: \(l.debugDescription)\n\nright:\(r.debugDescription)")
}
return l == r
}
guard left.fileWrappers?.count == right.fileWrappers?.count else { return false }
guard isEqualJSONFiles(left: left, right: right, filename: "face.json") else { return false }
guard isEqualJSONFiles(left: left, right: right, filename: "metadata.json") else { return false }
guard isEqualDataFiles(left: left, right: right, filename: "snapshot.png") else { return false }
guard isEqualDataFiles(left: left, right: right, filename: "no_borders_snapshot.png") else { return false }
guard let leftResources = left.fileWrappers?["Resources"],
let rightResources = right.fileWrappers?["Resources"] else { return false }
guard leftResources.fileWrappers?.count == rightResources.fileWrappers?.count else { return false }
guard isEqualPropertyListFiles(left: leftResources, right: rightResources, filename: "Images.plist") else { return false }
for (filename, _) in resources.files {
guard isEqualDataFiles(left: leftResources, right: rightResources, filename: filename) else { return false }
}
return true
}
}
extension FileWrapper {
convenience init(watchface: Watchface) throws {
self.init(directoryWithFileWrappers: [
"face.json": FileWrapper(regularFileWithContents: try JSONEncoder().encode(watchface.face)),
"metadata.json": FileWrapper(regularFileWithContents: try JSONEncoder().encode(watchface.metadata)),
"snapshot.png": FileWrapper(regularFileWithContents: watchface.snapshot),
"no_borders_snapshot.png": FileWrapper(regularFileWithContents: watchface.no_borders_snapshot),
// "device_border_snapshot.png": watchface.device_border_snapshot.map {FileWrapper(regularFileWithContents: $0)},
"Resources": try FileWrapper(resources: watchface.resources),
"complicationData": watchface.complicationData.map {FileWrapper(complicationData: $0)},
].compactMapValues {$0})
}
convenience init(resources: Watchface.Resources) throws {
self.init(directoryWithFileWrappers: resources.files.mapValues {FileWrapper(regularFileWithContents: $0)}.merging(
["Images.plist": FileWrapper(regularFileWithContents: try PropertyListEncoder().encode(resources.images))], uniquingKeysWith: {a,b in a}))
}
convenience init(complicationData: Watchface.ComplicationData) {
self.init(directoryWithFileWrappers: [
"top-left": complicationData.top_left.map {FileWrapper(directoryWithFileWrappers: $0.mapValues {FileWrapper(regularFileWithContents: $0)})},
"top-right": complicationData.top_right.map {FileWrapper(directoryWithFileWrappers: $0.mapValues {FileWrapper(regularFileWithContents: $0)})},
"bottom-center": complicationData.bottom_center.map {FileWrapper(directoryWithFileWrappers: $0.mapValues {FileWrapper(regularFileWithContents: $0)})}
].compactMapValues {$0})
}
}