ZHANGYU.dev

October 14, 2023

SwiftUI 开发时钟小组件笔记

Swift12.3 min to read

SwiftUI 开发时钟小组件笔记

最近自学了SwiftUI,并且上线了一款简单的时钟小组件应用,从零开始的过程中遇见一些困难,简略的记录下问题的解决方式。

时钟走起来的方式

小组件的数据更新是需要实现继承TimelineProviderstruct中的getTimeline方法,像时钟这种东西,用定时器在App里是没有任何问题的,一旦放到小组件就没搞了。

小组件里的数据更新是和它的时间轴相关的,时间轴也就是一个数组,WidgetKit会根据其中元素的date字段所对应的时间来渲染小组件,而时间小组件的渲染方式就得把未来的时间先存入数组,接下来的时间里就根据这个时间来渲染小组件,下面的代码是渲染接下来15分钟的时间的时间轴代码。

// func getTimeline ...let times = 15 // 时间太长就运行就会报错let currentDate = Date()let updatesDate = Calendar.current.date(byAdding: .minute, value: times, to: currentDate)!var entries = [WidgetEntry]()for offset in 0 ..< 60 * times {  let entryDate = Calendar.current.date(byAdding: .second, value: offset, to: currentDate)!  entries.append(WidgetEntry(date: entryDate))}let timeline = Timeline(entries: entries, policy: .after(updatesDate))// ...

Timeline方法里的policy字段则是决定这段时间走完以后,重新获取新时间轴的策略,.after是在某一时间后,.end是在当前时间轴走完后触发。实际上我发现这个更新策略的优先级还是得系统来决定,所以时钟小组件经常会出现走的不准,卡住的问题,这个问题我还没有找到解决方式。

可配置的小组件

小组件分中的配置项分为StaticConfigurationIntentConfiguration,只有IntentConfiguration是可配置的,可配置也就是可以通过长按小组件点击编辑小组件来选择一些配置项目。

IntentConfiguration配置首先得在Widget Extension里新建一个xxx.intentdefinition的文件,再在里面填内容,配置项里可以选择指定的数据类型,也可以自定数据类型,都是在xxx.intentdefinition这个intent文件里配置。配置项目是可以通过代码来动态获取的,需要钩上对应数据的Dynamic Options选项。

当勾上了Dynamic Options选项以后,就需要新创建一个Intents Extension来实现了,只需要实现其中IntentHander.swift中的2个固定方法

// 这个ClocksWidgetIntentHandling是根据之前的intentdefinition文件由xcode自动创建的class IntentHandler: INExtension, ClocksWidgetIntentHandling {  	// 获取动态的数据  	// 这个方法名也是xcode自动的    func provideCurrentWidgetOptionsCollection(for _: ClocksWidgetIntent, with completion: @escaping (INObjectCollection<WidgetConfig>?, Error?) -> Void) {      	// WidgetConfig类型就是intentdefinition文件中自定的类型        let collection = INObjectCollection(items: [])        completion(collection, nil)    }    func defaultCurrentWidget(for _: ClocksWidgetIntent) -> WidgetConfig? {      // 默认的选项    }	// ...}

小组件和App的数据共享

数据共享方式我使用的是UserDefaults,不过需要先在xcode里新创建一个App Groups,都勾上相同的group才行

UserDefaults(suiteName: "group.xxx.xxx") // 填写之前的group

本地文件共享同样需要用到App Groups

FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.xxx.xxx")

关于小组件的背景

小组件里不能有网络请求,所以链接形式的图片是读取不出来的,包括本地路径,所以在getTimeline时间轴这块儿的时候就必须传递的是UIImage或者直接Image

透明小组件

小组件本来是不能直接透明的,动画也不能使用,想要透明的唯一方式就是刚好截小组件所在背景的那一块图作为小组件的背景,在视觉上就是透明的效果了,而且这让我很头大,因为我是个新手,根本不知道有没有相关的API可以获取小组件在屏幕上的位置,我想可能是没有的,所以我就按照苹果的设计文档,把所有机型的小组件位置都截图下来自己测量的。

自己只做了小、中组件,UIDevice().type这个方法是我在stackoverflow找的,用来判断机型

小组件的大小

// 小组件的大小enum DeviceWidgetSize {    static var small: CGSize {        switch UIDevice().type {        case .iPhone12ProMax:            return CGSize(width: 170, height: 170)        case .iPhone12Pro, .iPhone12:            return CGSize(width: 158, height: 158)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGSize(width: 169, height: 169)        case .iPhone12Mini, .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGSize(width: 155, height: 155)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGSize(width: 159, height: 159)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGSize(width: 148, height: 148)        case .iPhoneSE, .iPod7:            return CGSize(width: 141, height: 144)        default:            return CGSize(width: 169, height: 169)        }    }    static var meduim: CGSize {        switch UIDevice().type {        case .iPhone12ProMax:            return CGSize(width: 364, height: 170)        case .iPhone12Pro, .iPhone12:            return CGSize(width: 338, height: 158)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGSize(width: 360, height: 169)        case .iPhone12Mini, .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGSize(width: 329, height: 155)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGSize(width: 348, height: 159)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGSize(width: 322, height: 148)        case .iPhoneSE, .iPod7:            return CGSize(width: 291, height: 144)        default:            return CGSize(width: 360, height: 169)        }    }}

小组件在屏幕的位置

// 小组件在屏幕上的位置struct DeviceWidgetPosition {    // 小组件 左上    static var smallTopLeft: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 32, y: 82)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 26, y: 77)        case .iPhone12Mini:            return CGPoint(x: 23, y: 77)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 27, y: 76)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 23, y: 71)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 33, y: 38)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 28, y: 30)        case .iPhoneSE, .iPod7:            return CGPoint(x: 14, y: 30)        default:            return CGPoint(x: 27, y: 76)        }    }    // 小组件 右上    static var smallTopRight: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 226, y: 82)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 206, y: 77)        case .iPhone12Mini:            return CGPoint(x: 197, y: 77)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 218, y: 76)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 197, y: 71)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 224, y: 38)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 200, y: 30)        case .iPhoneSE, .iPod7:            return CGPoint(x: 165, y: 30)        default:            return CGPoint(x: 218, y: 76)        }    }    // 小组件 中左    static var smallCenterLeft: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 32, y: 294)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 26, y: 273)        case .iPhone12Mini:            return CGPoint(x: 23, y: 267)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 27, y: 286)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 23, y: 261)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 33, y: 232)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 27, y: 206)        case .iPhoneSE, .iPod7:            return CGPoint(x: 14, y: 200)        default:            return CGPoint(x: 218, y: 76)        }    }    // 小组件 中右    static var smallCenterRight: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 226, y: 294)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 206, y: 273)        case .iPhone12Mini:            return CGPoint(x: 197, y: 267)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 218, y: 286)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 197, y: 261)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 224, y: 232)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 200, y: 206)        case .iPhoneSE, .iPod7:            return CGPoint(x: 165, y: 200)        default:            return CGPoint(x: 218, y: 76)        }    }    // 小组件 下左    static var smallBottomLeft: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 32, y: 506)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 26, y: 469)        case .iPhone12Mini:            return CGPoint(x: 23, y: 457)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 27, y: 495.3333)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 23, y: 451)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 33, y: 426)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 27, y: 382)        case .iPhoneSE, .iPod7:            return CGPoint(x: 0, y: 0)        default:            return CGPoint(x: 218, y: 76)        }    }    // 小组件 下右    static var smallBottomRight: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 226, y: 506)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 206, y: 469)        case .iPhone12Mini:            return CGPoint(x: 197, y: 457)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 218, y: 495.3333)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 197, y: 451)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 224, y: 426)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 200, y: 382)        case .iPhoneSE, .iPod7:            return CGPoint(x: 0, y: 0)        default:            return CGPoint(x: 218, y: 76)        }    }    // 中组件 上方    static var mediumTop: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 32, y: 82)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 26, y: 77)        case .iPhone12Mini:            return CGPoint(x: 23, y: 77)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 27, y: 76)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 23, y: 71)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 33, y: 38)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 28, y: 30)        case .iPhoneSE, .iPod7:            return CGPoint(x: 14, y: 30)        default:            return CGPoint(x: 218, y: 76)        }    }    // 中组件 中间    static var mediumCenter: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 32, y: 294)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 26, y: 273)        case .iPhone12Mini:            return CGPoint(x: 23, y: 267)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 27, y: 286)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 23, y: 261)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 33, y: 232)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 27, y: 206)        case .iPhoneSE, .iPod7:            return CGPoint(x: 14, y: 200)        default:            return CGPoint(x: 218, y: 76)        }    }    // 中组件 下方    static var mediumBottom: CGPoint {        switch UIDevice().type {        case .iPhone12ProMax:            return CGPoint(x: 32, y: 506)        case .iPhone12Pro, .iPhone12:            return CGPoint(x: 26, y: 469)        case .iPhone12Mini:            return CGPoint(x: 23, y: 457)        case .iPhone11ProMax, .iPhone11, .iPhoneXSMax, .iPhoneXR:            return CGPoint(x: 27, y: 495.3333)        case .iPhone11Pro, .iPhoneXS, .iPhoneX:            return CGPoint(x: 23, y: 451)        case .iPhone6SPlus, .iPhone7Plus, .iPhone8Plus:            return CGPoint(x: 33, y: 426)        case .iPhone6S, .iPhone7, .iPhone8, .iPhoneSE2:            return CGPoint(x: 27, y: 382)        case .iPhoneSE, .iPod7:            return CGPoint(x: 0, y: 0)        default:            return CGPoint(x: 218, y: 76)        }    }}

总结

因为辞职在家里,决定学点其他技能,自从m1出了我就彻底变果粉了,所以我觉得学swift来搞点东西,刚好自己也挺需要这样一个时钟小组件的功能,自己开发一个得了

从零开始学习swift和swift ui,虽然也就是api搬运,不过也学习了一些不同的代码思想,我想在写前端里也能给我一些思路。比如苹果的动画真的是太好用了,短短一句代码,我甚至在思考如何将withAnimation这种方法带入到前端

我这个App也毫无设计,连介绍图都自己截图的,这让我不禁在思考,如果想成为一个真正的自由开发者,岂不是还得会设计?然后上架也忒烦人了,首先得花688,然后一堆操作,最后还得审核,我审核了3次才通过😭

最后,我开发的时钟小组件在App Store上架了,同时代码也是开源的

AppStore

Github