iOS桌面小外掛 Widget Extension

struggle_time發表於2022-03-06

iOS桌面小外掛 Widget Extension

  • 這個外掛時iOS14以後才出現的,基於SwiftUI
  • 舊專案新建時可能一堆錯誤,其中一個時要把外掛target 開發sdk版本設定為14.0以上

新建target

  • File - Target - Widget Extension

專案結構

  • @main 這裡是主入口,這裡可以設定小元件的 Provider以及 WidgetEntryView,以及長按後彈出框的 APP 資訊設定。
  • Provider:控制器,這裡可以用來做小元件的重新整理操作
  • SimpleEntry: 這個是資料模型,Provider 裡如果想更新資料到 WidgetEntryView,必須通過 SimpleEntry 來實現,當然命名隨意了,但是這個必須繼承 TimelineEntry。同時也可以新增引數,變數什麼的,用來傳遞自己需要的資料型別。
  • WidgetEntryView: 這就是主檢視了,在這裡自定義頁面用來顯示在手機桌面。

import WidgetKit
import SwiftUI
import Intents

// 控制器,類似Controller,這裡可以用來做小元件的重新整理操作
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)
    }
}

// 資料模型,資料顯示在View上必須經過這裡
struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}

// View,小元件的介面
struct WidgetExtensionEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
        
    }
}

// 程式入口,初始化相關資訊,如Provider,View等
@main
struct WidgetExtension: Widget {
    let kind: String = "WidgetExtension"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            WidgetExtensionEntryView(entry: entry)
        }
        .configurationDisplayName("小元件")
        .description("This is an 測試一下 widget.")
    }
}
// 自定義樣式
struct WidgetExtension_Previews: PreviewProvider {
    static var previews: some View {
        
        // 設定小元件尺寸 systemSmall systemMedium systemLarge
        WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

自定義UI

自定義小元件尺寸

// 自定義樣式
struct WidgetExtension_Previews: PreviewProvider {
    static var previews: some View {
        
        // 設定小元件尺寸 systemSmall systemMedium systemLarge
        WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

HStack、VStack、ZStack

  • HStack、VStack相當於UIStackView,H是水平方向,V是豎直方向。ZStack可以理解為相對於螢幕裡外方向,也就是相當於以前superView和subView的方式。
// View,小元件的介面
struct WidgetExtensionEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        // 深度佈局,螢幕深度
        ZStack(alignment: .center, content: {
            // 背景圖
            Image("2").resizable().aspectRatio(contentMode: .fit)
            //  水平
            HStack(alignment: .center, spacing: 5, content: {
                // 左側圖
                Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
                // 垂直
                VStack(alignment: .center, spacing: 5, content: {
                    // 右側文字
                    Text("小元件1").foregroundColor(.blue)
                    Text("小元件2").foregroundColor(.blue).lineLimit(2)
                })

            })
        })
    }
}

傳遞資料

  • 通過widgetURL 和Link
  • 在主應用新增 URL Types
// View,小元件的介面
struct WidgetExtensionEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        // 深度佈局,螢幕深度
        ZStack(alignment: .center, content: {
            // 背景圖
            Image("2").resizable().aspectRatio(contentMode: .fit)
            //  水平
            HStack(alignment: .center, spacing: 5, content: {
                // 左側圖
                Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
                // 垂直
                VStack(alignment: .center, spacing: 5, content: {
                    // 右側文字
                    Text("小元件1").foregroundColor(.blue)
                    Text("小元件2").foregroundColor(.blue).lineLimit(2)
                })

            })
        }).widgetURL(URL(string: "widgetExtensionDemo://test1"))
    }
}
  • 接受資料

  • 只能用SceneDelegate來接受資料,AppDelegate不行。

  • SceneDelegate

  • SceneDelegate中相應事件

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts{
    NSLog(@"%s",__FUNCTION__);
    UIOpenURLContext * context = URLContexts.allObjects.firstObject;
    NSLog(@"%@", context.URL);
}

適配不同尺寸小元件


// View,小元件的介面
struct WidgetExtensionEntryView : View {
    @Environment(\.widgetFamily) var family:WidgetFamily
    var entry: Provider.Entry

    var body: some View {
        switch family {
        case .systemSmall:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fill)
                //  水平
                HStack(alignment: .center, spacing: 5, content: {
                    // 左側圖
                    Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
                    // 垂直
                    VStack(alignment: .center, spacing: 5, content: {
                        // 右側文字
                        Text("小元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    })

                })
            }).widgetURL(URL(string: "widgetExtensionDemo://test1"))
        case .systemMedium:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fill)
                //  水平
                HStack(alignment: .top, spacing: 5, content: {
                    // 左側圖
                    Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue)
                    // 垂直
                    VStack(alignment: .trailing, spacing: 5, content: {
                        // 右側文字
                        Text("zh元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    }).foregroundColor(.gray)

                })
            }).widgetURL(URL(string: "widgetExtensionDemo://test2"))
        case .systemLarge:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fill)
                //  水平
                HStack(alignment: .center, spacing: 5, content: {
                    // 左側圖
                    Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading)
                    // 垂直
                    VStack(alignment: .center, spacing: 5, content: {
                        // 右側文字
                        Text("小元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    })

                }).foregroundColor(.blue)
            }).widgetURL(URL(string: "widgetExtensionDemo://test3"))
            
        default:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fit)
                //  水平
                HStack(alignment: .center, spacing: 5, content: {
                    // 左側圖
                    Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
                    // 垂直
                    VStack(alignment: .center, spacing: 5, content: {
                        // 右側文字
                        Text("小元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    })

                })
            }).widgetURL(URL(string: "widgetExtensionDemo://test1"))
        }

    }
}

更多小元件建立

  • 重寫@main入口
// 更多小元件
@main
struct Widgets:WidgetBundle {
    init() {
        
    }

    @WidgetBundleBuilder
    var body: some Widget{ // 最多建立5次,也就是15個小元件
        WidgetExtension()
        CustomWidget()
        CustomWidget()
        CustomWidget()
        CustomWidget()
    }
    
}

struct CustomWidget:Widget {
    var kind:String="自定義元件"
    var body: some WidgetConfiguration{
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            CustomEntryView(entry:entry)
        }
        .configurationDisplayName("自定義更多元件")
        .description("ios14自定義更多小元件")
    }
    
}
// 自定義Ui
struct CustomEntryView:View {
    @Environment(\.widgetFamily) var family:WidgetFamily
    var entry: Provider.Entry
    @ViewBuilder
    var body: some View {
        switch family {
        case .systemSmall:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fill)
                //  水平
                HStack(alignment: .center, spacing: 5, content: {
                    // 左側圖
                    Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
                    // 垂直
                    VStack(alignment: .center, spacing: 5, content: {
                        // 右側文字
                        Text("小元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    })

                })
            }).widgetURL(URL(string: "widgetExtensionDemo://test1"))
        case .systemMedium:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fill)
                //  水平
                HStack(alignment: .top, spacing: 5, content: {
                    // 左側圖
                    Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue)
                    // 垂直
                    VStack(alignment: .trailing, spacing: 5, content: {
                        // 右側文字
                        Text("zh元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    }).foregroundColor(.gray)

                })
            }).widgetURL(URL(string: "widgetExtensionDemo://test2"))
        case .systemLarge:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fill)
                //  水平
                HStack(alignment: .center, spacing: 5, content: {
                    // 左側圖
                    Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading)
                    // 垂直
                    VStack(alignment: .center, spacing: 5, content: {
                        // 右側文字
                        Text("小元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    })

                }).foregroundColor(.blue)
            }).widgetURL(URL(string: "widgetExtensionDemo://test3"))
            
        default:
            // 深度佈局,螢幕深度
            ZStack(alignment: .center, content: {
                // 背景圖
                Image("2").resizable().aspectRatio(contentMode: .fit)
                //  水平
                HStack(alignment: .center, spacing: 5, content: {
                    // 左側圖
                    Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
                    // 垂直
                    VStack(alignment: .center, spacing: 5, content: {
                        // 右側文字
                        Text("小元件1").foregroundColor(.blue)
                        Text("小元件2").foregroundColor(.blue).lineLimit(2)
                    })

                })
            }).widgetURL(URL(string: "widgetExtensionDemo://test1"))
        }

    }
}

參考1
參考2

相關文章