SwiftUI-Widget 使用及避坑指南

iOS Widget簡(jiǎn)單介紹( 只介紹iOS 14 以后Widget相關(guān)內(nèi)容):

Widget 是 iOS 14 重磅推出的新功能,使得用戶(hù)可以在主屏幕添加小組件,快速瀏覽 app 提供的重要信息。
用戶(hù)可以通過(guò) Widget 對(duì)主屏幕進(jìn)行個(gè)性化定制,但是 iOS 14 的 Widget 跟其他系統(tǒng)上的小組件有很大的區(qū)別。在 Widget 的設(shè)計(jì)上蘋(píng)果也保持了一貫的克制,定位于輕量化、僅用作關(guān)鍵信息的展示。比如系統(tǒng)自帶 Widget 中的股票、天氣、電量、運(yùn)動(dòng)信息,他們的共同特征是更新頻率高、提供的信息重要,讓用戶(hù)不用打開(kāi) app 就可以瀏覽關(guān)心的內(nèi)容。

相關(guān)限制:

蘋(píng)果基于上面的設(shè)計(jì)定位,同時(shí)也為了節(jié)省系統(tǒng)資源保證續(xù)航,對(duì) Widget 的做了一些限制:
不支持動(dòng)畫(huà),僅支持靜態(tài)頁(yè)面展示。
更新頻率由系統(tǒng)通過(guò)機(jī)器學(xué)習(xí)來(lái)動(dòng)態(tài)分配。
不支持拖拽、滾動(dòng)等復(fù)雜的交互,不支持 Switch 等控件。
用戶(hù)點(diǎn)擊 Widget 一定會(huì)跳轉(zhuǎn)到 App。
支持三種不同大小的樣式

適應(yīng)不同的屏幕尺寸

iOS Widget簡(jiǎn)單介紹( 只介紹iOS 14 以后Widget相關(guān)內(nèi)容):

Widget 是 iOS 14 重磅推出的新功能,使得用戶(hù)可以在主屏幕添加小組件,快速瀏覽 app 提供的重要信息。
用戶(hù)可以通過(guò) Widget 對(duì)主屏幕進(jìn)行個(gè)性化定制,但是 iOS 14 的 Widget 跟其他系統(tǒng)上的小組件有很大的區(qū)別。在 Widget 的設(shè)計(jì)上蘋(píng)果也保持了一貫的克制,定位于輕量化、僅用作關(guān)鍵信息的展示。比如系統(tǒng)自帶 Widget 中的股票、天氣、電量、運(yùn)動(dòng)信息,他們的共同特征是更新頻率高、提供的信息重要,讓用戶(hù)不用打開(kāi) app 就可以瀏覽關(guān)心的內(nèi)容。

相關(guān)限制:

蘋(píng)果基于上面的設(shè)計(jì)定位,同時(shí)也為了節(jié)省系統(tǒng)資源保證續(xù)航,對(duì) Widget 的做了一些限制:
不支持動(dòng)畫(huà),僅支持靜態(tài)頁(yè)面展示。
更新頻率由系統(tǒng)通過(guò)機(jī)器學(xué)習(xí)來(lái)動(dòng)態(tài)分配。
不支持拖拽、滾動(dòng)等復(fù)雜的交互,不支持 Switch 等控件。
用戶(hù)點(diǎn)擊 Widget 一定會(huì)跳轉(zhuǎn)到 App。
支持三種不同大小的樣式

適應(yīng)不同的屏幕尺寸

屏幕尺寸 - portrait 小部件-systemSmall 中型部件-systemMedium 大部件-systemLarge
414x896 pt (XR/XsMax/11/11ProMax) 169x169pt 360x169pt 360x379pt
375x812 pt (X/Xs/11 Pro) 155x155 pt 329x155 pt 329x345 pt
414x736 pt (6p/6sp/7p) 159x159 pt 348x159 pt 348x357 pt
375x667 pt (6/6s/7/8) 148x148 pt 321x148 pt 321x324 pt
320x568 pt (5/5s/SE) 141x141 pt 292x141 pt 292x311 pt

開(kāi)發(fā)要求:

開(kāi)發(fā)工具 Xcode 12 以上版本
開(kāi)發(fā)語(yǔ)言 Swift和SwiftUI
手機(jī)系統(tǒng)要求 14以上

Widget 創(chuàng)建:

1.Widget作為項(xiàng)目的一個(gè)組件,創(chuàng)建之前需要先創(chuàng)建一個(gè)iOS的項(xiàng)目,項(xiàng)目創(chuàng)建成功后點(diǎn)擊:File->New->Target添加Widget Extension Target 點(diǎn)擊Next。

2.輸入Widget組件名,取消勾選,點(diǎn)擊Finish就可以了。Include Configuration Intent:是否支持用戶(hù)配置。

3.關(guān)于預(yù)覽:
本項(xiàng)目會(huì)提示需要升級(jí),新創(chuàng)建項(xiàng)目的沒(méi)有該問(wèn)題
(運(yùn)行widget target 模擬器調(diào)試打日志有時(shí)不顯示)

多個(gè)Widget和小、大、中頁(yè)面數(shù)據(jù)布局

如何定義多個(gè)Widget,并且小、中、大的布局完全不同?

iOS14中Widget是支持通過(guò)創(chuàng)建一個(gè)擴(kuò)展項(xiàng)目返回一個(gè)或多個(gè)小部件的,可以使您的應(yīng)用提供多種小部件選擇。并且在項(xiàng)目中視圖通過(guò)WidgetFamily的枚舉自定義自己想要的組件和布局。

WidgetFamily枚舉

public enum WidgetFamily : Int, RawRepresentable, CustomDebugStringConvertible, CustomStringConvertible {

    /// A small widget.
    case systemSmall

    /// A medium-sized widget.
    case systemMedium

    /// A large widget.
    case systemLarge
}

默認(rèn)模版代碼,只能支持展示一類(lèi)型種的一種樣式

@main //widget 主入口,系統(tǒng)從這里加載
struct WidgetTest: Widget {
  //kind的值是widget的唯一標(biāo)識(shí)
    let kind: String = "Widget"
    var body: some WidgetConfiguration {//初始化配置代碼
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
           WidgeEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")//編輯頁(yè)面展示的標(biāo)題
        .description("This is an example widget.")//編輯頁(yè)面展示的描述內(nèi)容
        .supportedFamilies([.systemSmall,.systemMedium,.systemLarge])// 如何實(shí)現(xiàn)預(yù)覽里面small樣式展示不同樣式
    }
}

可以通過(guò)修改原Widget入口文件方法添加更多配置來(lái)支持多個(gè)Widget,相同類(lèi)型不同樣式。

@main
struct SwiftWidgetsBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        Widget1()
        Widget2()
        Widget3()
        Widget4()
        ...
    }
}

與主應(yīng)用交互:

根據(jù)官方文檔的描述,點(diǎn)擊Widget窗口喚起APP進(jìn)行交互指定跳轉(zhuǎn)支持兩種方式:

widgetURL:點(diǎn)擊區(qū)域是Widget的所有區(qū)域,代碼如下。
if family == .systemSmall {  // 小
  VStack(alignment: .center, spacing: 20, content: {
      Text("\(entry.quotes.date) at \(entry.quotes.place) ")
          .font(.system(size: 9))
          .foregroundColor(.gray)
  })
  .widgetURL(URL(string: "https://www.baidu.com/small"))

}

Link:通過(guò)Link修飾,允許讓界面上不同元素產(chǎn)生點(diǎn)擊響應(yīng)。

if family == .systemMedium { // 中
  VStack(alignment: .center, spacing: 8, content: {
      Link(destination: URL(string: "https://www.baidu.com/medium/1")!) {
          Text(entry.quotes.content[0])
              .font(.system(size: 17))
              .foregroundColor(.black)
              .frame(maxWidth:.infinity, alignment: .leading)
      }
     
      Text("\(entry.quotes.date) at \(entry.quotes.place) ")
          .font(.system(size: 12))
          .foregroundColor(.gray)
          .frame(maxWidth:.infinity, alignment: .trailing)
          .frame(height: 20, alignment: .bottom)
  })
 
}

注?。簊ystemSmall(小組件)只支持widgetURL,而systemMedium(中組件)和 systemLarge(大組件)則都支持。Link:更希望的是不同元素的點(diǎn)擊響應(yīng)。

在主項(xiàng)目的SceneDelegate代理方法中接收回調(diào)

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
    /// 根據(jù)不同的URL回調(diào)做出響應(yīng)
    NSLog(@"%@",URLContexts);
}

或 AppDelegate 中的

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*,id> *)options{
//處理
    return YES;
}

數(shù)據(jù)更新時(shí)機(jī)

這里關(guān)于刷新策略,根據(jù)官方文檔來(lái)看,Timeline的刷新策略是會(huì)延遲的,并不一定根據(jù)你設(shè)定的時(shí)間來(lái)。同時(shí)官方規(guī)定每個(gè)配置的窗口小部件每天都接收有限數(shù)量的刷新,
(官方文檔:https://developer.apple.com/documentation/widgetkit/timelineprovider

導(dǎo)致無(wú)法預(yù)測(cè)何時(shí)更新Widget。即使設(shè)置了某個(gè)時(shí)間再次獲取時(shí)間軸本身進(jìn)行更新,也無(wú)法保證iOS會(huì)同時(shí)更新視圖。從而造成一定的Widget頁(yè)面更新延遲。

蘋(píng)果因?yàn)樘峁┝艘粋€(gè)單獨(dú)的方法,調(diào)用來(lái)重新加載所有窗口小部件
/// 控件的所有已配置小部件重新加載時(shí)間線
/// 包含應(yīng)用程序。

WidgetCenter.shared.reloadAllTimelines()

刷新次數(shù)限制

Refreshing Widgets Efficiently
Each configured widget receives a limited number of refreshes every day. Several factors affect how many refreshes a widget receives, such as whether the containing app is running in the foreground or background, how frequently the widget is shown onscreen, and what types of activities the containing app engages in.

每個(gè)配置的小部件每天都會(huì)收到有限的刷新次數(shù)。有幾個(gè)因素會(huì)影響小部件接收的刷新次數(shù),例如包含的應(yīng)用程序是在前臺(tái)還是后臺(tái)運(yùn)行,小部件在屏幕上顯示的頻率,以及包含的應(yīng)用程序參與的活動(dòng)類(lèi)型。

在Xcode中調(diào)試小部件時(shí),WidgetKit不會(huì)施加此限制。要驗(yàn)證小部件的行為是否正確,請(qǐng)?jiān)赬code的調(diào)試器之外測(cè)試應(yīng)用程序和小部件的行為。

當(dāng)你的應(yīng)用程序位于前臺(tái)、有活動(dòng)媒體會(huì)話或使用標(biāo)準(zhǔn)位置服務(wù)時(shí),刷新不計(jì)入小部件的每日限制。有關(guān)媒體會(huì)話和定位服務(wù)的更多信息,請(qǐng)參閱doc://com.apple.documentation/documentation/avfoundation/avaudiosession使用標(biāo)準(zhǔn)定位服務(wù)。

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

主要是使用App Group來(lái)實(shí)現(xiàn)。
如登錄態(tài)同步等

Swift 與OC 相互調(diào)用:

Widget 中的Swift 調(diào)用主項(xiàng)目的OC 調(diào)用
使用橋接方法,且引入的文件必須 在Target Membership 關(guān)聯(lián)對(duì)應(yīng)的 Widget Target。

注意:

  • 用戶(hù)初次安裝未啟動(dòng),無(wú)法添加Widget組件。
  • 圖片加載不支持異步,只能同步加載好后進(jìn)行顯示
  • 在創(chuàng)建文件時(shí)一般都會(huì)在Target Membership 進(jìn)行勾選相應(yīng)的Target,默認(rèn)生產(chǎn)的模版的入口不需要勾選主target ,否則會(huì)報(bào)錯(cuò)
duplicate symbol '_main' in:
    /Users/XXX/Library/Developer/Xcode/DerivedData/XXX-hghyirqieliknyckkefqaeomgvms/Build/Intermediates.noindex/XXX.build/Debug-iphoneos/XXX.build/Objects-normal/arm64/main.o
    /Users/XXX/Library/Developer/Xcode/DerivedData/XXX-hghyirqieliknyckkefqaeomgvms/Build/Intermediates.noindex/XXX.build/Debug-iphoneos/XXX.build/Objects-normal/arm64/XXXWidget.o
ld: 1 duplicate symbol for architecture arm64
clang_bk: error: linker command failed with exit code 1 (use -v to see invocation)
[$] waitpid = 64263 
[$] run clang_bk fail,not exist 
nagain clang exit 
Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang failed with exit code 255
  • Widget中支持的字體和主工程支持的字體不一樣,如果UI中設(shè)置的字體沒(méi)有達(dá)到預(yù)期效果建議打印一下支持的字體。
    swift:
var familNames:[String] = []
        familNames = UIFont.familyNames
        for fami in familNames {
            print("---familNames",fami)
            let namesArr = UIFont.fontNames(forFamilyName: fami)
            for name in namesArr {
                print("---famil fontname",name)
            }
        }
  print(familNames)
  • 埋點(diǎn)
    Widget 的曝光事件我們是無(wú)法感知的,由于點(diǎn)擊 Widget 會(huì)直接跳轉(zhuǎn)到主 app,所以我們?cè)谔D(zhuǎn)到主 app 的 URL 上增加了埋點(diǎn)參數(shù),主 app 解析 URL 中的參數(shù)調(diào)用 UT 來(lái)埋點(diǎn)。
  • 包大小
    主工程是使用OC,Widget開(kāi)發(fā)會(huì)使用Swift及SwiftUI 會(huì)引入新的庫(kù),會(huì)導(dǎo)致包有一定的增加。
  • swiftUI布局的坑點(diǎn):
    比如
    Image("imageName")
    .resizable()
    .frame(width: fitWidth(22), height: fitWidth(22))
    圖片設(shè)置大小要先設(shè)置resizable,否則不生效,
    SwiftUI 設(shè)置方法先后順序不同會(huì)導(dǎo)致不同的UI效果

參考:
如何用iOS14 Widget小組件自定義玩法

iOS14 Widget初體驗(yàn)

如何進(jìn)行 iOS Widget 開(kāi)發(fā)?
iOS小組件Widget踩坑

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容