iOS 14小组件开发总结

 最近项目有开发iOS小组件的需求,开始调研到实现踩了很多坑,借此记录下来。
 iOS14系统发布后,桌面添加的新的"入口模式"(很多产品把这个功能当做了App的一个快捷入口)Widget。Widget有几个地方要说下
1.只支持SwiftUI进行界面开发(意味着你要开始学习SwiftUI)

  1. 小组件刷新机制
  2. Configuration让小组件可配置。

1.创建Widget

File->New-> Target->Widget Extension``->如果你的项目支持可配置的话需要勾选include Configuration Intent`

image.png

image.png

image.png

IDE创建会一个默认模板

struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }

    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}

struct MSWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

示例代码主要包含三个重要点:Entry,EntryView,Porvider。类比MVC的话,Entry相当于Model负责数据的转换,EntryView相当于view负责页面UI渲染展示,Porvider相当于控制器负责逻辑处理。

@main
struct MSWidget: Widget {
    let kind: String = "MSWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            MSWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([,.systemMedium])
    }
}

@main说明是小组件的入口

IntentConfiguration,需要三个参数
  • kind:widget的唯一标识,类似于id
  • intent:ConfigurationIntent类型,支持widget配置项
  • provider:继承自IntentTimelineProvider的子类
supportedFamilies:小组件有默认有Large,Small,Medium三种样式,可单独指定某种模式,笔者项目里设置的只支持systemMedium
IntentTimelineProvider

Widget通过provider处理业务逻辑
getTimeline :获取时间线处理业务逻辑,通过completion回调timeline给系统,系统重新绘制页面。
timeline支持entryies集合,如果我们知道自己小组件在未来哪些时刻需要刷新页面,那我们可以事先定义好时间点集合回调给系统,系统会在对应时间进行刷新(官方demo就是定义一个每隔5小时进行页面绘制的timeline)
当然在这个函数里可以进行数据请求或者其他业务处理。

SwiftUI构建界面

以一个例子来简单介绍下SwiftUI开发小组件


image.png

大致需要

  • 背景红色
  • 第一行文字和图片
  • 四行水平文字(widget不支持scroll,所以不能用List,可以根据数据源创建对应个数的)
struct Page1: View {
    var bgColor : some View {
        Color.red
    }
    var body: some View {
// 获取屏幕自身尺寸
        GeometryReader(content: { geometry in
// ZStack叠加背景色和文字
            ZStack {
                bgColor
      
                Item()
            }
        })
    }
}



struct Item : View {
    var body: some View {
// HStack水平方向集合包装容器,spacing设置子元素之间的距离
        HStack(alignment: .center, spacing: 10, content: {
 //Spacer().frame(width: 10)占据10个像素点的位置类似于left=10的操作
            Spacer().frame(width: 10)
            Text("第1个Text")
                .font(.system(size: 14))
                .foregroundColor(.white)
            Text("第2个Text")
                .font(.system(size: 14))
                .foregroundColor(.white)
            Text("第3个Text")
                .font(.system(size: 14))
                .foregroundColor(.white)
        // Spacer()填充水平方向剩余空间
            Spacer()
            Text("第4个Text")
                .font(.system(size: 14))
                .foregroundColor(.white)
   //距离Spacer().frame(width: 10)占据10个像素点的位置类似于right=10的操作
            Spacer().frame(width: 10)
        })
    }
}
var body: some View {
        GeometryReader(content: { geometry in
            ZStack {
                bgColor
  
                // 竖直方向填充4个元素spacing设置每个元素之间的距离
                VStack(alignment: .leading, spacing: 10, content: {
                    Item()
                    Item()
                    Item()
                    Item()
                })
                
                
            }
        })
image.png
var body: some View {
        GeometryReader(content: { geometry in
            ZStack {
                bgColor
// VStack将标题和列表包装起来
                VStack{
                    Spacer().frame(height: 10)
// HStack包装标题和副标题
                    HStack{
                        Spacer().frame(width: 10)
                        Text("标题")
                        Image("icon")
                            .frame(width: 20, height: 20)
                            .clipped()
                        Spacer()
                        Text("副标题")
                        Spacer().frame(width: 10)
                    }
                    Spacer()
                    
                    VStack(alignment: .leading, spacing: 10, content: {
                        Item()
                        Item()
                        Item()
                        Item()
                    })
                    Spacer()
                }
            }
        })
    }
image.png

布局小tips:类似于笔者这样的界面我比较喜欢用Spacer填充控件在控件之间,撑满剩余空间。灵活使用发现在屏幕适配方面还是挺好用的,还是要小小的吐槽一下HStack和VStack这样的控件有借鉴前端FlexBox的布局思想,但是HStack对其方式是VerticalAlignment,VStack对其方式是HorizontalAlignment,最开始开发的时候让我很不习惯。

网络请求

getTimeline方法里进行网络请求

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
  // 定义返回数据模型
    let response : Any
}

          func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    
        let currentDate = Date()
        
        let session = URLSession.shared
        let url = URL(string: "https://")
        guard let u = url else { return  }
        var request = URLRequest(url: u)
        request.httpMethod = "GET"
        request.timeoutInterval = 20
        let dataTask = session.dataTask(with: request) { (data, response, error) in
        // 请求回来的数据包装成timeline,completion回调给系统,小组件界面进行刷新操作
        let currentDate = Date()
        let updateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
            let entry = SimpleEntry(date: currentDate, configuration: configuration,response: data)
    
            let timeline = Timeline(entries: [entry], policy:.after(updateDate))
            completion(timeline)
        }
        dataTask.resume()
    }

官方提供了3中刷新策略

  • atEnd:最近的timeline结束了才会去请求一个新的timeline
  • never:展示一个静态的timeline,不再去主动请求
  • after:在指定的刷新时间去请求新的timeLine
    笔者的项目里用的是after模式设置1个小时去主动刷新一次timeline,同时宿主app业务逻辑变动会手动调用WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)触发的因为WidgetCenter不支持OC语言直接调用,如果宿主APP是OC开发的,需要添加一个Swift文件进行间接调用

open class WidgetTool: NSObject {
//
@available(iOS 14, *)
@objc open func refreshWidget () {
WidgetCenter.shared.reloadTimelines(ofKind: "你的组件kind")
}
}

数据共享

支持Usedefault和FileManager2种方式实现宿主APP和widget数据共享
宿主工程Target和widget中的target添加App Group,Group id保持一致


image.png
self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.xxxxxxxx"];
[self.userDefaults setObject:value forKey:@"key"];

// widget里面调用实现数据同步
 let userDefault = UserDefaults.init(suiteName: "group.com.xxxxxxxx")
userDefault?.object(forKey: "key")
页面跳转
  • Link(destination: <#URL#>, label: <#() -> _#>)
  • widgetURL()
Link(destination: URL(string: "跳转链接")) {
                    Text("标题")
 }
Text("标题").widgetURL( URL(string: "跳转链接"))
// 宿主工程openURL方法中进行处理
- (BOOL)application:(UIApplication *)app
            openURL:(NSURL *)url
            options:(NSDictionary<NSString *, id> *)options {
// 在这里处理跳转逻辑,跳转对应页面
}
自定义配置

image.png

.intentdefinition文件 -->> Configuration -->>+按钮
系统分配了很多类型,也可以添加自定义的枚举类型等
image.png

configuration里可以拿到具体回调值


image.png
Light和Dark mode适配与控制

我们产品提出了一个需求:支持用户选择2种模式
[模式1]官方模式:正常和暗黑模式背景色为白色,字体为黑色
[模式2]系统模式:系统正常模式背景色为白色,字体为黑色;暗黑模式背景色为黑色,字体为白色

e56c3fbb19cb0f06bbebbc872e19bc8b.gif

@Environment(\.colorScheme) var colorScheme可以监听到light和darkmodel的改变,可以根据不同的模式定义不同的UI。widget会自动选择当前模式的UI进行刷新
var bgColor: some View {
// .both表示当前用户选择的[模式2]
// [模式1]背景色一直为白色
(theme == .both && colorScheme == .dark) ? Color.black : Color.white
}

image.png

github小demo

采坑集锦:
1.创建widget工程名的时候有工程前缀导致报错


image.png
  1. Widget不支持Scroll,所以如果要创建类似于列表的界面不能使用List,可以通过数据源使用HStackVStack容器包装
    image.png

    3.添加App Groups时,证书签名选择的是Automic,xcode会默认自动生成以XC开头的证书,无法匹配到我们自己手动创建的证书
    image.png

    4.Widget没有类似于viewWillAppear方法,不能做到每次出现widget页面进行数据更新,可以通过WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)手动刷新
    5.Configuration定义的时候,key不能有空格,我这里Date Component中间有个空格
    image.png

    image.png
  2. lazy symbol binding failed: can't resolve symbol
    给同事的iOS13的手机打包直接崩溃,报错原因是 不能打开某个dylid,查了很多原因后来突然想到小组件只支持iOS14以后,需要添加iOS14的版本判断
// swift
if #available(iOS 14.0, *) {

} else {
  // Earlier version of iOS
}
// OC
if (@available(iOS 14.0, *)) {
}else {
  // Earlier version of iOS
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容