美文网首页小组件
iOS小组件Widget从0到1开发

iOS小组件Widget从0到1开发

作者: IOSMan | 来源:发表于2020-09-27 18:31 被阅读0次

    Widget支持iOS 14以上系统,UI必须为swift UI(不能调用UIKit桥接过来,会显示一个黄色背景的红色圆圈斜杠),其他可以是swift和OC混编

    如果要全面理解widget,需要学习(没时间的话只看自己需要的部分)
    1.swift 官方中文教程
    2.swift UI Apple官方文档

    但是如果只是完成业务需求,没有太多时间,只看下面widget介绍

    首先新建Widget Extention,步骤如下图:


    新建Widget Extention.png

    勾选Include Configuration Intent选项,会多生成一个.intentdefinition文件。


    勾选Include Configuration Intent选项.png
    生成的文件目录.png

    这个选项的作用是配置widget控件,比如系统自带的天气,长按显示编辑小组件

    长按编辑小组件.png 小组件编辑.png

    这个背景色可以在Assets.xcassets/WidgetBackground中设置
    交互按钮的字体颜色在Assets.xcassets/AccentColor中设置

    小组件编辑选择城市.png

    注意:如果勾选Include Configuration Intent选项,并且项目配置类前缀Class Prefix,则生成的.swift文件中ConfigurationIntent类,要手动添加类前缀

    先看自动生成的.swift文件,这是一个时钟的widget。编译一下,看能不能成功。

    可以看到里面都是struct:
    struct ConfigureWidget: Widgetwidget入口,也负责整体项目配置
    struct ConfigureWidgetEntryView : View负责UI相关
    struct Provider: TimelineProvider数据的提供者
    struct SimpleEntry: TimelineEntry数据模型
    struct ConfigureWidget_Previews: PreviewProviderxcode热重载配置文件
    可以看到整体是一个MVVM结构。
    这几个struct都可以重命名,对应更改里面的配套名字就可以,比如更改SimpleEntry数据模型,那么传递的数据模型都需要更改。

    1.入口和配置widget的不同样式

    入口是遵守Widget协议的struct

    @main
    struct ConfigureWidget: Widget {
        let kind: String = "ConfigureWidget"
    
        var body: some WidgetConfiguration {
            IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
                ConfigureWidgetEntryView(entry: entry)
            }
            .configurationDisplayName("My Widget")
            .description("This is an example widget.")
        }
    }
    

    一个widget Extention只能有一个@main入口。
    kind:widget的唯一标识
    provider:数据源
    return ConfigureWidgetEntryView 返回,UI界面

    .configurationDisplayName("Your Widget")添加widget界面,显示的标题
    .description("This is an example widget.")添加widget界面,显示的描述
    .supportedFamilies([.systemSmall])添加widget界面,显示可以添加的widget样式,默认是small,medium,large三种样式用一套适配,但是一般我们三种样式,会分别适配。有三种方式达到分别适配的效果

    1.在UI层配置三种模式显示不同的UI

    加载一个widget,然后根据小中大,匹配加载不同的UI,这种方式数据没加载出来有可能会黑屏,定制化差,不推荐这种方式

    struct ConfigureWidgetEntryView : View {
        var entry: Provider.Entry
    
        @Environment(\.widgetFamily) var family: WidgetFamily
    
            @ViewBuilder
            var body: some View {
                switch family {
                case .systemSmall: SmallView(entry:entry)
                case .systemMedium: MediumView(entry:entry)
                case .systemLarge: LargeView(entry:entry)
                default: SmallView(entry:entry)
                }
            }
    }
    
    2.返回不同的widget支持不同的样式

    不同widget可以返回多个相同样式,比如返回多个small样式的widget,每个widget单独加载,对应一个单独的UI,小中大可以添加个性化的添加widget话术,推荐这种方式

    @main
    struct YourWidget : WidgetBundle {
        @WidgetBundleBuilder
        var body: some Widget {
            SmallWidget()
            SmallWidget2()
            SmallWidget3()
            MediumWidget()
            LargeWidget()
        }
    }
    
    3.新建多个Widget Extention

    一个APP有几个完全不同的widget,可以用这种方式,会有多个bundleID,不推荐

    2.struct SimpleEntry: TimelineEntry数据模型

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

    date是必须要实现的,因为要遵守TimelineEntry协议,他的作用是多久刷新一次数据。
    另外自己要实现自己需要的数据模型。
    configuration是用户自定义小组件的配置模型,在.intentdefinition文件parameter中添加参数名,选择相应的类型,也可以自定义类型

    3.struct ConfigureWidgetEntryView : View负责UI相关

    struct ConfigureWidgetEntryView : View因为遵循View协议,所以在var body中用swift UI实现UI相关的代码,每次TimelineProvider中刷新UI,也会走这里的代码,所以这里会有一个数据模型的属性,也就是自己自定义的数据模型属性。

    4.struct Provider: TimelineProvider数据的提供者

    这里就是负责加载数据,有下面三个方法
    // 没加载到数据前的占位控件,但是经过测试并不会加载这的数据

        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)
        }
    
    getSnapshot方法展示数据的位置.png

    // 根据时间轴,获取网络真实数据

        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)
        }
    

    控制器有自己的生命周期,但widget需要定时获取数据;
    默认例子中是1小时执行一次getTimeline,每次加载5条SimpleEntry数据,(60/5=12min),12分钟更新一次UI
    Calendar.current.date(byAdding: .second, value: 5, to: currentDate)意思是返回当前时间+5秒钟的时间。

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

    entries中的数据执行完之后policy: .atEnd立刻completion(timeline)重新获取数据,执行func getTimeline。

    policy:.atEnd执行完这个entries,立刻刷新
    .never不刷新,可以调用WidgetCenter.shared.reloadAllTimelines()刷新所有widget,或者指定widget刷新
    .after指定时间刷新

    点击事件:
    systemSmall:只能是widgetURL方式跳转
    systemMedium、systemLarge:widgetURL、Link都可以
    widgetURL:

        var body: some View {
            VStack{
                
            }
            .widgetURL(URL(string: "www.myWidget.com"))
        }
    

    Link包裹:

    var body: some View {
          Link(destination: URL(string: "www.myWidget.com")!){
                VStack{
                    
                }
          }
    }
    

    处理事件点击,相当于处理URL Schemes,iOS14UI生命周期交给了scene

        func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
            for context in URLContexts {
                    print(context.url)
                }
        }
    

    和OC混编问题

    如果原项目是OC,或者现在widget target项目里面加入OC文件,需要加入桥接文件,如果加入.intentdefinition的配置文件,需要在桥接文件中,引入#import "ConfigurationIntent.h",否则会报错误Cannot find type 'ConfigurationIntent' in scope。

    如果需要用cocopod中的其他库,需要添加新的target,加入在widget扩展中需要用到的库,比如加入AFNetworking:

    target 'WidgetExtension' do
      # Comment the next line if you don't want to use dynamic frameworks
      use_frameworks!
    
      pod 'AFNetworking'
      # Pods for WidgetExtension
    
    end
    

    如果需要主项目中的文件,在需要用到的.m文件Target Membership中勾选widgetExtention,这样widget小组件才能用这个OC 文件。可能需要一些兼容性改动:引入一些缺少的头文件,勾选其他关联文件的Target Membership,添加widget关联。勾选后编译一下,没问题后把用到的OC文件添加到桥接文件中声明,这样swiftUI才能调用原来OC主项目的文件

    可以看下我写的另一篇,记录widget遇到的问题https://www.jianshu.com/p/a4d61a880bac

    相关文章

      网友评论

        本文标题:iOS小组件Widget从0到1开发

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