美文网首页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