iOS 14小組件開發(fā)總結(jié)

?最近項(xiàng)目有開發(fā)iOS小組件的需求,開始調(diào)研到實(shí)現(xiàn)踩了很多坑撮慨,借此記錄下來竿痰。
?iOS14系統(tǒng)發(fā)布后,桌面添加的新的"入口模式"(很多產(chǎn)品把這個功能當(dāng)做了App的一個快捷入口)Widget甫煞。Widget有幾個地方要說下
1.只支持SwiftUI進(jìn)行界面開發(fā)(意味著你要開始學(xué)習(xí)SwiftUI)

  1. 小組件刷新機(jī)制
  2. Configuration讓小組件可配置菇曲。

1.創(chuàng)建Widget

File->New-> Target->Widget Extension``->如果你的項(xiàng)目支持可配置的話需要勾選include Configuration Intent`

image.png

image.png

image.png

IDE創(chuàng)建會一個默認(rèn)模板

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

示例代碼主要包含三個重要點(diǎn):Entry,EntryView,Porvider。類比MVC的話抚吠,Entry相當(dāng)于Model負(fù)責(zé)數(shù)據(jù)的轉(zhuǎn)換,EntryView相當(dāng)于view負(fù)責(zé)頁面UI渲染展示弟胀,Porvider相當(dāng)于控制器負(fù)責(zé)邏輯處理楷力。

@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喊式,需要三個參數(shù)
  • kind:widget的唯一標(biāo)識,類似于id
  • intent:ConfigurationIntent類型,支持widget配置項(xiàng)
  • provider:繼承自IntentTimelineProvider的子類
supportedFamilies:小組件有默認(rèn)有Large萧朝,Small岔留,Medium三種樣式,可單獨(dú)指定某種模式检柬,筆者項(xiàng)目里設(shè)置的只支持systemMedium
IntentTimelineProvider

Widget通過provider處理業(yè)務(wù)邏輯
getTimeline :獲取時間線處理業(yè)務(wù)邏輯献联,通過completion回調(diào)timeline給系統(tǒng),系統(tǒng)重新繪制頁面何址。
timeline支持entryies集合里逆,如果我們知道自己小組件在未來哪些時刻需要刷新頁面,那我們可以事先定義好時間點(diǎn)集合回調(diào)給系統(tǒng)用爪,系統(tǒng)會在對應(yīng)時間進(jìn)行刷新(官方demo就是定義一個每隔5小時進(jìn)行頁面繪制的timeline)
當(dāng)然在這個函數(shù)里可以進(jìn)行數(shù)據(jù)請求或者其他業(yè)務(wù)處理原押。

SwiftUI構(gòu)建界面

以一個例子來簡單介紹下SwiftUI開發(fā)小組件


image.png

大致需要

  • 背景紅色
  • 第一行文字和圖片
  • 四行水平文字(widget不支持scroll,所以不能用List偎血,可以根據(jù)數(shù)據(jù)源創(chuàng)建對應(yīng)個數(shù)的)
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設(shè)置子元素之間的距離
        HStack(alignment: .center, spacing: 10, content: {
 //Spacer().frame(width: 10)占據(jù)10個像素點(diǎn)的位置類似于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)占據(jù)10個像素點(diǎn)的位置類似于right=10的操作
            Spacer().frame(width: 10)
        })
    }
}
var body: some View {
        GeometryReader(content: { geometry in
            ZStack {
                bgColor
  
                // 豎直方向填充4個元素spacing設(shè)置每個元素之間的距離
                VStack(alignment: .leading, spacing: 10, content: {
                    Item()
                    Item()
                    Item()
                    Item()
                })
                
                
            }
        })
image.png
var body: some View {
        GeometryReader(content: { geometry in
            ZStack {
                bgColor
// VStack將標(biāo)題和列表包裝起來
                VStack{
                    Spacer().frame(height: 10)
// HStack包裝標(biāo)題和副標(biāo)題
                    HStack{
                        Spacer().frame(width: 10)
                        Text("標(biāo)題")
                        Image("icon")
                            .frame(width: 20, height: 20)
                            .clipped()
                        Spacer()
                        Text("副標(biāo)題")
                        Spacer().frame(width: 10)
                    }
                    Spacer()
                    
                    VStack(alignment: .leading, spacing: 10, content: {
                        Item()
                        Item()
                        Item()
                        Item()
                    })
                    Spacer()
                }
            }
        })
    }
image.png

布局小tips:類似于筆者這樣的界面我比較喜歡用Spacer填充控件在控件之間,撐滿剩余空間颇玷。靈活使用發(fā)現(xiàn)在屏幕適配方面還是挺好用的笨农,還是要小小的吐槽一下HStack和VStack這樣的控件有借鑒前端FlexBox的布局思想,但是HStack對其方式是VerticalAlignment帖渠,VStack對其方式是HorizontalAlignment,最開始開發(fā)的時候讓我很不習(xí)慣磁餐。

網(wǎng)絡(luò)請求

getTimeline方法里進(jìn)行網(wǎng)絡(luò)請求

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
  // 定義返回?cái)?shù)據(jù)模型
    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
        // 請求回來的數(shù)據(jù)包裝成timeline,completion回調(diào)給系統(tǒng)阿弃,小組件界面進(jìn)行刷新操作
        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結(jié)束了才會去請求一個新的timeline
  • never:展示一個靜態(tài)的timeline诊霹,不再去主動請求
  • after:在指定的刷新時間去請求新的timeLine
    筆者的項(xiàng)目里用的是after模式設(shè)置1個小時去主動刷新一次timeline,同時宿主app業(yè)務(wù)邏輯變動會手動調(diào)用WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)觸發(fā)的因?yàn)閃idgetCenter不支持OC語言直接調(diào)用渣淳,如果宿主APP是OC開發(fā)的脾还,需要添加一個Swift文件進(jìn)行間接調(diào)用

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

數(shù)據(jù)共享

支持Usedefault和FileManager2種方式實(shí)現(xiàn)宿主APP和widget數(shù)據(jù)共享
宿主工程Target和widget中的target添加App Group,Group id保持一致


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

// widget里面調(diào)用實(shí)現(xiàn)數(shù)據(jù)同步
 let userDefault = UserDefaults.init(suiteName: "group.com.xxxxxxxx")
userDefault?.object(forKey: "key")
頁面跳轉(zhuǎn)
  • Link(destination: <#URL#>, label: <#() -> _#>)
  • widgetURL()
Link(destination: URL(string: "跳轉(zhuǎn)鏈接")) {
                    Text("標(biāo)題")
 }
Text("標(biāo)題").widgetURL( URL(string: "跳轉(zhuǎn)鏈接"))
// 宿主工程openURL方法中進(jìn)行處理
- (BOOL)application:(UIApplication *)app
            openURL:(NSURL *)url
            options:(NSDictionary<NSString *, id> *)options {
// 在這里處理跳轉(zhuǎn)邏輯入愧,跳轉(zhuǎn)對應(yīng)頁面
}
自定義配置

image.png

.intentdefinition文件 -->> Configuration -->>+按鈕
系統(tǒng)分配了很多類型鄙漏,也可以添加自定義的枚舉類型等
image.png

configuration里可以拿到具體回調(diào)值


image.png
Light和Dark mode適配與控制

我們產(chǎn)品提出了一個需求:支持用戶選擇2種模式
[模式1]官方模式:正常和暗黑模式背景色為白色,字體為黑色
[模式2]系統(tǒng)模式:系統(tǒng)正常模式背景色為白色棺蛛,字體為黑色怔蚌;暗黑模式背景色為黑色,字體為白色

e56c3fbb19cb0f06bbebbc872e19bc8b.gif

@Environment(\.colorScheme) var colorScheme可以監(jiān)聽到light和darkmodel的改變旁赊,可以根據(jù)不同的模式定義不同的UI踩身。widget會自動選擇當(dāng)前模式的UI進(jìn)行刷新
var bgColor: some View {
// .both表示當(dāng)前用戶選擇的[模式2]
// [模式1]背景色一直為白色
(theme == .both && colorScheme == .dark) ? Color.black : Color.white
}

image.png

github小demo

采坑集錦:
1.創(chuàng)建widget工程名的時候有工程前綴導(dǎo)致報錯


image.png
  1. Widget不支持Scroll宗弯,所以如果要創(chuàng)建類似于列表的界面不能使用List,可以通過數(shù)據(jù)源使用HStackVStack容器包裝
    image.png

    3.添加App Groups時品洛,證書簽名選擇的是Automic,xcode會默認(rèn)自動生成以XC開頭的證書,無法匹配到我們自己手動創(chuàng)建的證書
    image.png

    4.Widget沒有類似于viewWillAppear方法,不能做到每次出現(xiàn)widget頁面進(jìn)行數(shù)據(jù)更新,可以通過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的手機(jī)打包直接崩潰蝶涩,報錯原因是 不能打開某個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
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绿聘,一起剝皮案震驚了整個濱河市嗽上,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌斜友,老刑警劉巖炸裆,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鲜屏,居然都是意外死亡烹看,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門洛史,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惯殊,“玉大人,你說我怎么就攤上這事也殖⊥了迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵忆嗜,是天一觀的道長己儒。 經(jīng)常有香客問我,道長捆毫,這世上最難降的妖魔是什么闪湾? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮绩卤,結(jié)果婚禮上途样,老公的妹妹穿的比我還像新娘。我一直安慰自己濒憋,他們只是感情好何暇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凛驮,像睡著了一般裆站。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天遏插,我揣著相機(jī)與錄音捂贿,去河邊找鬼纠修。 笑死胳嘲,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扣草。 我是一名探鬼主播了牛,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辰妙!你這毒婦竟也來了鹰祸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤密浑,失蹤者是張志新(化名)和其女友劉穎蛙婴,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尔破,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡街图,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了懒构。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片餐济。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖胆剧,靈堂內(nèi)的尸體忽然破棺而出絮姆,到底是詐尸還是另有隱情,我是刑警寧澤秩霍,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布篙悯,位于F島的核電站,受9級特大地震影響铃绒,放射性物質(zhì)發(fā)生泄漏鸽照。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一匿垄、第九天 我趴在偏房一處隱蔽的房頂上張望移宅。 院中可真熱鬧,春花似錦椿疗、人聲如沸漏峰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浅乔。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間靖苇,已是汗流浹背席噩。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贤壁,地道東北人悼枢。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像脾拆,于是被迫代替她去往敵國和親馒索。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容