美文网首页iOS开发-Swift
iOS 14小组件开发总结

iOS 14小组件开发总结

作者: Michale_Zuo | 来源:发表于2021-03-26 22:14 被阅读0次

     最近项目有开发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
    }
    

    相关文章

      网友评论

        本文标题:iOS 14小组件开发总结

        本文链接:https://www.haomeiwen.com/subject/rtzfhltx.html