djk3000 / ME

4 stars 2 forks source link

iOS-Widget #123

Open djk3000 opened 1 year ago

djk3000 commented 1 year ago

Widget 小组件

小组件可以在主屏幕上实现内容展示和功能跳转。 系统会向小组件获取时间线,根据当前时间对时间线上的数据进行展示。点击正在展示的视觉元素可以跳转到APP内,实现对应的功能。

image

可以通过在主屏幕上长按点击编辑添加自己程序的小组件。

实现

通过视频学习了一下怎么实现这个小组件。

import SwiftUI
import WidgetKit

struct ContentView: View {
    //创建一个组来共享这个数据
    @AppStorage("streak", store: UserDefaults(suiteName: "group.com.shgbit.Widgetdemo")) var streak = 0

    var body: some View {
        ZStack {
            Color.black
                .ignoresSafeArea()

            VStack(spacing: 60) {
                ZStack {
                    Circle()
                        .stroke(.white.opacity(0.1), lineWidth: 20)

                    let pct = Double(streak) / 50

                    Circle()
                        .trim(from: 0, to: pct)
                        .stroke(.blue, style: StrokeStyle(lineWidth: 20, lineCap: .round,lineJoin: .round))
                        .rotationEffect(.degrees(-90))

                    VStack {
                        Text("Streak")
                            .font(.largeTitle)

                        Text(String(streak))
                            .font(.system(size: 70))
                            .bold()
                    }
                    .foregroundColor(.white)
                }
                .padding(50)

                Button {
                    streak += 1
                    //根据标识更新小部件
                    WidgetCenter.shared.reloadTimelines(ofKind: "WidgetExtension")
                } label: {
                    ZStack {
                        RoundedRectangle(cornerRadius: 20)
                            .foregroundColor(.blue)
                            .frame(height: 50)

                        Text("+1")
                            .foregroundColor(.white)
                    }
                }
            }
        }
    }
}
import WidgetKit
import SwiftUI

/**
一些方法和主程序联动的方法
*/
struct DataService {
    @AppStorage("streak", store: UserDefaults(suiteName: "group.com.shgbit.Widgetdemo")) var streak = 0

    func log() {
        streak += 1
    }

    func progress() -> Int {
        return streak
    }
}

struct Provider: TimelineProvider {
    let data = DataService()

    /**
     这是一个占位方法,用于返回定义的结构中的数据,以便在 Widget 预览时显示占位数据。
     */
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), streak: data.progress())
    }

    /**
     这是获取快照数据的方法。当 Widget 处于更新周期之外或因其他原因需要立即更新时,系统会调用此方法。
     */
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(),streak: data.progress())
        completion(entry)
    }

    /**
     生成一个时间线,该时间线来控制多久之后刷新这个组件,这里是一小时刷一次,也可以通过和主程序的联动来刷新小组件界面
     */
    func getTimeline(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, streak: data.progress())
            entries.append(entry)
        }

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

/**
 数据结构
 */
struct SimpleEntry: TimelineEntry {
    let date: Date
    let streak: Int
}

/**
 小组件的View
 */
struct WidgetExtensionEntryView : View {
    var entry: Provider.Entry
    let data = DataService()

    var body: some View {
        //        Text(entry.date, style: .time)
        //        Text(String(data.progress()))
        ZStack {
            Circle()
                .stroke(.white.opacity(0.1), lineWidth: 20)

            let pct = Double(data.progress()) / 50

            Circle()
                .trim(from: 0, to: pct)
                .stroke(.blue, style: StrokeStyle(lineWidth: 20, lineCap: .round,lineJoin: .round))
                .rotationEffect(.degrees(-90))

            VStack {
                Text("Streak")
                    .font(.largeTitle)

                Text(String(data.progress()))
                    .bold()
            }
            .foregroundColor(.white)
        }
        .padding(20)
    }
}

struct WidgetExtension: Widget {
    //标识用来和内部程序控制
    let kind: String = "WidgetExtension"

    var body: some WidgetConfiguration {
        //这个闭包小部件的视图
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            WidgetExtensionEntryView(entry: entry)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.black)
        }
        //编辑组件时的名称
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall,.systemLarge,.systemMedium]) //部件大小
    }
}

/**
 预览视图
 */
struct WidgetExtension_Previews: PreviewProvider {
    static var previews: some View {
        WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), streak: 1))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

知识点:

  1. 这里我们用AppStorage来存储一个简单的数据,如果需要和小组件共享这个数据的话需要在程序Signing&Capabilities中+Capabilities添加一个Add Group,在小组件和本程序同时选择这个GroupName就能够共享这个数据。
  2. 通过let kind: String = "WidgetExtension"这个kind来使用主程序来更新小组件界面WidgetCenter.shared.reloadTimelines(ofKind: "WidgetExtension")

参考资料