WidgetKit时间轴和工作流程
WidgetKit时间轴和工作流程

WidgetKit时间轴和工作流程

在 WidgetKit 中,小组件的核心是时间轴(Timeline时间轴提供者(TimelineProvider)

WidgetKit 工作流程

在 WidgetKit 中,数据通过 TimelineProvider 提供,TimelineProvider 负责生成一个或多个时间点(Entries),每个时间点对应着小组件应该显示的数据。

1、组件的数据流

TimelineProvider 是负责提供数据的核心组件,它将时间和数据结合,定义小组件在不同时间点的显示内容。

struct SimpleProvider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleProviderEntry {
        SimpleProviderEntry(date: Date())
    }
    
    func getSnapshot(in context: Context, completion: @escaping (SimpleProviderEntry) -> ()) {
        let entry = SimpleProviderEntry(date: Date())
        completion(entry)
    }
    
    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleProviderEntry>) -> ()) {
        var entries: [SimpleProviderEntry] = []
        
        // 获取当前时间
        let currentDate = Date()
        
        // 创建多个时间点(这里假设每隔一小时更新一次)
        for hourOffset in 0..<5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            var entry = SimpleProviderEntry(date: Date())
            entry.date = entry
            entries.append(entry)
        }

        // 定义更新时间策略为“每次到期后”更新
        let timeline = Timeline(entries: entries, policy: .atEnd)
        
        // 返回生成的时间线
        completion(timeline)
    }
}

它会通过以下三个方法提供数据:

1)placeholder(in:)

该方法提供一个初始的“占位数据”。

它主要用于在小组件未加载或正在加载时,显示一个默认的或占位的数据。通常只在 Widget 刚被添加到屏幕或首次加载时使用。

在真机的测试过程中,没有查看到placeholder实际的显示。

可能指的是,在没有数据的情况下显示的空白状态。

2)getSnapshot(in:completion:)

快照方法会获取一个小组件在特定时刻的快照数据。这个方法返回的数据通常用于小组件的预览(例如小组件在 Widget Gallery 中的展示)。

这个数据只在需要快速显示的情况下使用。

在getSnapshot方法中使用 completion(entry) 而不是直接返回 SimpleProviderEntry 是因为 getSnapshot 是一个异步操作的回调方法。

func placeholder(in context: Context) -> SimpleProviderEntry {
    SimpleProviderEntry(date: Date())   // 返回 SimpleProviderEntry
}

func getSnapshot(in context: Context, completion: @escaping (SimpleProviderEntry) -> ()) {
    let entry = SimpleProviderEntry(date: Date())
    completion(entry)   // 返回 completion 闭包
}

getSnapshot 方法的目的是在提供小组件的快照数据时,给系统一个机会来异步地准备这些数据。通常,系统会异步地请求小组件的快照,以确保它在UI线程之外获取数据并渲染。为了能够处理异步的操作(例如从网络或数据库加载数据),系统使用了回调(completion 闭包)来将处理后的数据返回给调用方。

异步操作:如果需要进行一些数据加载、计算或其他操作,使用 completion 让系统能够在数据准备好时通过回调将其传递回来。例如,从网络或数据库读取数据时,不能保证数据是立即可用的,因此需要一个回调来处理数据。

为什么不直接返回数据?

Swift 的异步操作设计要求不能直接返回结果,而是通过闭包或其他异步机制传递结果。在 getSnapshot 方法中,返回一个 completion 闭包是告诉系统:“我已经准备好数据了,现在将数据通过回调传回”。这样可以让系统在不阻塞主线程的情况下继续处理其他任务,直到数据准备好。

如果直接返回数据,而不通过回调,就会违反异步操作的设计模式,可能导致数据处理阻塞主线程,甚至引发 UI 更新的问题。

3)getTimeline(in:completion:)

该方法最重要,它返回一个 Timeline,这是包含时间点数据的数组。每个 TimelineEntry 表示小组件在某个时间点应该展示的数据。

小组件通过 getTimeline 定义数据的更新时间,例如每隔一定时间更新一次。它会告诉系统小组件在什么时候(例如在某个时间点)需要显示什么数据。

通过 TimelinePolicy(例如 .atEnd,.after 等)来告诉系统如何处理时间点更新的频率。

func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleProviderEntry>) -> ()) {
    var entries: [SimpleProviderEntry] = []
    
    // 获取当前时间
    let currentDate = Date()
    
    // 创建多个时间点(这里假设每隔一小时更新一次)
    for hourOffset in 0..<5 {
        let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
        var entry = SimpleProviderEntry(date: Date())
        entry.date = entry
        entries.append(entry)
    }

    // 定义更新时间策略为“每次到期后”更新
    let timeline = Timeline(entries: entries, policy: .atEnd)
    
    // 返回生成的时间线
    completion(timeline)
}
Timeline<SimpleProviderEntry>是什么意思?

Timeline< SimpleProviderEntry > 是一个泛型类型,它代表了一个时间线,包含了一系列的 TimelineEntry 数据。具体来说,Timeline< SimpleProviderEntry > 中的 SimpleProviderEntry是定义的数据类型,表示每个时间点的小组件数据。

Timeline: 时间线对象,它管理了多个 TimelineEntry(在这个例子中是 SimpleProviderEntry)。它不仅仅是一个单纯的数据结构,还包含了时间更新的策略,告诉系统何时更新小组件。

TimelineEntry: 每个时间点的数据对象,包含了在特定时刻需要展示的数据。这个数据通常是通过 TimelineProvider 提供的。

所以,Timeline< SimpleProviderEntry > 表示一个时间线,它包含了多个 SimpleProviderEntry类型的数据条目。

为什么需要设计一个 entries 数组?

entries 数组用于保存一系列在未来时刻需要展示的数据。小组件不仅仅会展示当前的数据,还会展示一段时间内的数据。通过将多个 SimpleProviderEntry加入 entries 数组,系统可以按时间顺序呈现这些数据。

SimpleProviderEntry(date: Date())只是返回一个 SimpleProviderEntry的数据,但在 getTimeline 中,通常需要为多个时间点准备数据。比如希望小组件在接下来的几小时内,每小时更新一次内容。这样你就需要为每个小时生成一个 SimpleProviderEntry,并将它们存储在 entries 数组中。

直接返回SimpleProviderEntry(date: Date()) 只会得到一个数据,它不会为多个时间点生成数据,这样就无法形成一个时间线(Timeline)。

let timeline = Timeline(entries: entries, policy: .atEnd) 这段代码是什么意思?

这段代码创建了一个 Timeline 对象,它包含了 entries 数组中的多个条目,并设置了时间更新的策略。

entries: entries: 这里传入准备好的多个 BankletWidgetEntry 数据(也就是时间点的数据),这些数据会按照时间顺序在小组件中显示。

policy: .atEnd: 这个参数设置了小组件更新时间的策略。在这个例子中,.atEnd 表示小组件会在时间线的最后一个条目到期后自动更新。如果设置为 .after(Date),小组件将在指定的时间点更新。选择 .atEnd 是为了让系统在最后一个条目到期后自动更新整个时间线。

重复的时间点问题
// 获取当前时间
let currentDate = Date()
    
// 创建多个时间点(这里假设每隔一小时更新一次)
for hourOffset in 0..<5 {
    let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
    var entry = SimpleProviderEntry(date: Date())
    entry.date = entry
    entries.append(entry)
}

// 定义更新时间策略为“每次到期后”更新
let timeline = Timeline(entries: entries, policy: .atEnd)

在这个代码中,需要理解的是通过hourOffset创建了五个时间点,假设设置每小时更新一次,创建 5 个时间点,分别是:

当前时间(例如:2025年2月16日 10:00)

当前时间 + 1 小时(例如:2025年2月16日 11:00)

当前时间 + 2 小时(例如:2025年2月16日 12:00)

当前时间 + 3 小时(例如:2025年2月16日 13:00)

当前时间 + 4 小时(例如:2025年2月16日 14:00)

这些时间点的更新顺序是由 getTimeline 方法中定义的。系统会逐个展示这些时间点的数据,直到最后一个时间点到期。

当 Timeline 中的最后一个时间点(例如:2025年2月16日 14:00)到期后,系统会重新调用 getTimeline 方法来获取新的数据和时间点。

这个“重新调用”是非常重要的,它让小组件能够在新的时间点进行更新。这意味着并不是简单地每小时返回一个数据条目,而是每个时间点都有一个更新周期,并且每个周期结束后,系统会重新请求下一个时间段的数据。

为什么需要多个时间点而不是一个时间点

可以设置 1 个时间点来代表“每小时更新一次”,也可以设置 5 个时间点来代表 5 小时的更新。为什么要有多个时间点,而不是只设置一个?

周期性更新的优势:通过多个时间点,系统能够提前知道未来几个时间点需要更新什么数据,并且在这些时间点之间继续更新。如果设置了 1 个时间点,虽然系统知道需要更新,但是它并不知道在未来的某个时刻是否还需要继续更新,直到重新调用 getTimeline。

重复”调用 getTimeline 是正常的:完全可以每小时返回一个时间点,这和设置多个时间点没什么区别。本质上,每次返回一个新的时间点数据,系统都会调用 getTimeline 更新小组件,这就是每个时间点被“刷新”的本质。

此外,也可以设置成每隔10分钟更新一次,这里的时间点和时间间隔都是可以自定义的,如果想要每10分钟更新一次,只需要调整hourOffset的计算方式,将每个小时的间隔从“1小时”改为“10分钟”。

// 创建多个时间点(每隔 10 分钟更新一次)
for minuteOffset in 0..<6 {  // 假设你想生成 6 个时间点(即 60 分钟)
    let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset * 10, to: currentDate)!
    var entry = loadBankletData()
    entry.date = entryDate
    entries.append(entry)
}

如果想要实现每小时更新一次,可以一次性定义从当前时刻起的未来24小时的每个小时的时间点,可以生成 24 个时间点(例如,从当前时刻起的 24 小时内,每小时更新一次)。这种方式适合在较长时间周期内预先设定每小时的数据。

func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleProviderEntry>) -> ()) {
    var entries: [SimpleProviderEntry] = []
    
    // 获取当前时间
    let currentDate = Date()
    
    // 创建多个时间点(每隔一小时更新一次,共 24 个小时)
    for hourOffset in 0..<24 {  // 24小时内的每个小时
        let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
        var entry = SimpleProviderEntry(date: Date())
        entry.date = entryDate
        entries.append(entry)
    }

    // 定义更新时间策略为“每次到期后”更新
    let timeline = Timeline(entries: entries, policy: .atEnd)
    
    // 返回生成的时间线
    completion(timeline)
}

如果只希望每小时更新一次,而不需要在 getTimeline 中生成 24 个时间点,只需返回当前时间点,并通过更新策略让系统知道在每个小时到期后重新调用 getTimeline。

在这种情况下,将每次只生成当前时间点的数据,然后告诉系统每小时更新一次。

func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleProviderEntry>) -> ()) {
    var entries: [SimpleProviderEntry] = []
    
    // 获取当前时间
    let currentDate = Date()
    
    // 仅生成当前时间点的条目
    var entry = SimpleProviderEntry(date: Date())
    entry.date = currentDate
    entries.append(entry)

    // 定义更新时间策略为“每次到期后”更新
    let timeline = Timeline(entries: entries, policy: .after(Date().addingTimeInterval(3600)))
    
    // 返回生成的时间线
    completion(timeline)
}

policy: .after(Date().addingTimeInterval(3600))

这个配置指定了 时间线将在当前时间之后的一小时更新。

Date().addingTimeInterval(3600) 计算出当前时间加上 3600 秒(即 1 小时)。

这意味着 小组件将会在 1 小时后重新调用 getTimeline 来更新数据。

所以指定了一个明确的时间点,系统会在这个时间点后触发更新。

效果:系统将在当前时间之后的 1 小时自动更新(即触发下一个周期)。

注意:这里使用的是.after,表示小组件按时更新,如果设置为 .atEnd,则会根据条目的展示周期来触发更新,通常由每个条目展示的结束时刻来更新数据。

2、时间轴与时间点(Entries)

每个 TimelineEntry 表示一个时间点及该时间点要显示的数据。TimelineProvider 负责创建这些条目,并定义它们在时间轴上的更新方式。时间点的创建是根据 TimelineProvider 的方法,特别是 getTimeline(in:completion:) 中的逻辑。

关键步骤

1、数据创建:在 getTimeline(in:completion:) 方法中创建数据。例如,根据当前时间、用户数据、网络请求等来生成这些数据。

2、数据返回:将数据封装在 TimelineEntry 中,并将它们通过 Timeline 返回。

3、数据展示:当系统需要更新小组件时,它会通过 TimelineEntry 提供的数据来更新视图。

3、数据更新策略

TimelinePolicy 定义了小组件数据更新的策略。通过不同的策略来控制小组件的刷新频率:

.atEnd:表示数据会在每个时间点的结束时更新。

.after:表示数据会在指定的时间间隔后更新。

.never:表示不进行更新。

4、数据流的具体实现

为了更清楚地了解数据流动的具体实现,下面将分步讲解如何通过 TimelineProvider 提供数据,并确保小组件正确地显示和更新:

1)定义 TimelineEntry 和数据结构

struct BankletWidgetEntry: TimelineEntry {
    var date: Date
    let piggyBankIcon: String
    let piggyBankName: String
    let piggyBankAmount: Double
    let piggyBankTargetAmount: Double
    let loopAnimation: String
    let background: String
}

这里我们定义了一个 TimelineEntry 的数据结构 BankletWidgetEntry,它包含了展示所需的所有数据(如存钱罐图标、名称、金额等)。

2)实现 TimelineProvider

struct BankletWidgetProvider: TimelineProvider {
    
    func placeholder(in context: Context) -> BankletWidgetEntry {
        loadBankletData()
    }
    
    func getSnapshot(in context: Context, completion: @escaping (BankletWidgetEntry) -> ()) {
        let entry = loadBankletData()
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<BankletWidgetEntry>) -> ()) {
        var entries: [BankletWidgetEntry] = []
        
        // 获取当前时间
        let currentDate = Date()
        
        // 创建多个时间点(这里假设每隔一小时更新一次)
        for hourOffset in 0..<5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            var entry = loadBankletData()
            entry.date = entryDate
            entries.append(entry)
        }

        // 定义更新时间策略为“每次到期后”更新
        let timeline = Timeline(entries: entries, policy: .atEnd)
        
        // 返回生成的时间线
        completion(timeline)
}

    private func loadBankletData() -> BankletWidgetEntry {
            // 从 UserDefaults 中获取存钱罐的数据
            let userDefaults = UserDefaults(suiteName: "group.com.fangjunyu.piglet")
            
            let piggyBankIcon = userDefaults?.string(forKey: "piggyBankIcon") ?? "dollarsign"
            let piggyBankName = userDefaults?.string(forKey: "piggyBankName") ?? "PiggyBank"
            let piggyBankAmount = userDefaults?.double(forKey: "piggyBankAmount") ?? 100.0
            let piggyBankTargetAmount = userDefaults?.double(forKey: "piggyBankTargetAmount") ?? 10.0
            let loopAnimation = userDefaults?.string(forKey: "LoopAnimation") ?? "Home0"
            let background = userDefaults?.string(forKey: "background") ?? "bg0"
            // 返回加载的数据
            return BankletWidgetEntry(
                date: Date(),
                piggyBankIcon: piggyBankIcon,
                piggyBankName: piggyBankName,
                piggyBankAmount: piggyBankAmount,
                piggyBankTargetAmount: piggyBankTargetAmount,
                loopAnimation: loopAnimation,
                background: background
            )
        }
}

placeholder(in:):当小组件刚加入屏幕时,提供一个占位数据。这里返回了一个默认的数据。

getSnapshot(in:completion:):提供一个快照数据,通常用于 Widget Gallery 预览时展示。

getTimeline(in:completion:):生成一个包含多个时间点(BankletWidgetEntry)的时间线,并定义更新时间策略。我们在这里每小时生成一个数据条目。

因为主应用中的数据是通过App Group共享数据,因此TimelineProvider可以通过App Group读取存储的数据,将其放入到BankletWidgetEntry中。

3)配置小组件

struct BankletWidget: Widget {
    let kind: String = "BankletWidget"
    
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: BankletWidgetProvider()) { entry in
            BankletWidgetEntryView(entry: entry)
                .containerBackground(for: .widget) {
                    Image("WidgetBackground")
                }
        }
        .supportedFamilies([.systemSmall]) // 支持小尺寸
        .configurationDisplayName("Progress widget") // 小组件的显示名称
        .description("Shows the current progress percentage of the piggy bank.") // 小组件的描述
    }
}

StaticConfiguration 用来创建静态的小组件配置,使用 BankletWidgetProvider 来提供数据。

在这里,entry 会接收来自 TimelineProvider 的数据,并交给 BankletWidgetEntryView 来渲染视图。

4)小组件的视图渲染

struct BankletWidgetEntryView : View {
    var entry: BankletWidgetEntry
    
    var SavingProgress:Double {
        // 防止除以零的错误
        guard entry.piggyBankTargetAmount != 0 else { return 0 }
        return max(min(entry.piggyBankAmount / entry.piggyBankTargetAmount * 100,100),0)
    }
    
    var body: some View {
        GeometryReader { geometry in
            // 通过 `geometry` 获取布局信息
            let height = geometry.size.height * 0.5
            VStack(spacing: 0) {
                VStack(spacing: 0) {
                    // 显示存钱罐图标
                    Circle()
                        .frame(width: 24,height: 24)
                        .foregroundColor(.white)
                        .overlay {
                            Image(systemName: entry.piggyBankIcon)
                                .imageScale(.small)
                                .foregroundColor(Color(hex:"FF4B00"))
                        }
                    Spacer()
                        .frame(height: 5)
                    // 读取主应用存储的存钱罐数据
                    Text(entry.piggyBankName)
                        .foregroundColor(.white)
                        .widgetAccentable()
                }
                .frame(height: height)
                .frame(maxWidth: .infinity)
                Spacer()
                HStack(spacing: 0) {
                    Rectangle()
                        .frame(width: 60, height: 40)
                        .foregroundColor(Color(hex:"FF4B00"))
                        .cornerRadius(10)
                        .overlay {
                            Text("\(SavingProgress.formattedWithTwoDecimalPlaces()) %")
                                .foregroundColor(.white)
                        }
                    
                    RightTriangle()
                        .frame(width: 10,height:10)
                        .foregroundColor(Color(hex:"FF4B00"))
                    Image(entry.loopAnimation) // 显示存钱罐图标
                        .resizable()
                        .scaledToFit()
                        .imageScale(.large)
                }
            }
            .font(.footnote)
            .frame(maxWidth: .infinity,maxHeight: .infinity)
            .padding(0)
        }
    }
}

在 BankletWidgetEntryView 中,通过 entry 获取数据(如存钱罐的名称、金额等),然后根据这些数据渲染视图。

entry就是在第一步定义的数据结构 BankletWidgetEntry。

最后,在真机上运行正常。

总结

1、数据来源:数据通过 TimelineProvider 的 getTimeline 方法提供。每个 TimelineEntry 保存小组件在某一时刻的数据。

2、时间更新:TimelineProvider 还定义了数据更新时间的策略,如 .atEnd 表示每次时间结束时更新,.after 表示延迟更新。

3、视图渲染:视图渲染通过获取每个时间点的数据 (TimelineEntry),然后在视图中展示这些数据。

TimelineProvider 提供数据,而视图根据这些数据渲染,不要直接在视图中做数据操作,数据应该由 TimelineProvider 提供并传递给视图。

扩展知识

TimelineEntry 的实际应用

TimelineEntry 是用于将数据结构化并与时间绑定的。在小组件中,数据并不是一次性加载的,而是根据时间轴(Timeline)来逐步更新的。小组件的生命周期和更新机制跟普通的 iOS 应用有所不同,它并不是一直在后台实时运行,而是依据系统的策略和时间策略来更新。TimelineEntry 是每一个时间点的数据快照,它承载了小组件在某一时刻需要展示的内容。

小组件的更新主要分为两种类型:

1、定时更新:如每小时、每天更新一次数据。

2、事件触发更新:如用户交互后、系统事件触发时更新。

而Timeline和TimelineEntry的设计是为了在这些情况下提供一种高效的更新机制。

为什么需要 TimelineEntry?

1、与时间绑定

TimelineEntry 与时间轴(Timeline)紧密结合。一个 Timeline 由多个 TimelineEntry 组成,每个条目对应一个时间点。在不同的时间点,条目会显示不同的数据。这就是 TimelineEntry 的核心作用:它帮助描述某个时刻的“小组件状态”。

2、分时更新

不可能一次性展示所有数据,而是需要在不同的时刻更新小组件的内容。TimelineEntry 确保每个时间点展示的数据是独立且正确的,并且能支持动态更新。比如希望每小时更新一次存钱进度,TimelineEntry 就会根据每个小时的数据来提供新的条目。

3、提供数据

TimelineProvider 通过 getTimeline(in:completion:) 方法提供一个时间轴,时间轴由多个 TimelineEntry 构成。每个条目包含当前时间的相关数据,并被传递给视图进行展示。这里的数据结构和视图相分离,数据的变化是根据时间更新的,而视图会根据这些更新显示不同的内容。

为什么需要时间轴(Timeline)来逐步更新数据?

1、节省资源

小组件不会像普通应用一样持续运行,它会在系统允许的情况下刷新。每次刷新是由系统决定的(比如系统空闲时或者当显示该小组件的屏幕切换到前台时)。而不是每秒钟都实时拉取数据,否则可能造成过度的电池消耗和资源浪费。

如果小组件总是实时显示最新数据,它就会不断地请求数据源,这不仅对电池耗电有影响,且会导致频繁的数据加载,造成性能问题。通过时间轴(Timeline)来控制更新时间,就能减少不必要的频繁请求。

2、减少更新频率

小组件的内容并不需要时刻实时变化。例如,存钱进度每小时更新一次,这并不会对用户造成很大的影响,因此可以通过TimelineEntry来控制每小时更新一次。

Timeline提供了设定刷新间隔的能力。可以根据需求设置更新周期(例如每小时更新、每天更新或根据某些事件更新),而不是每次都拉取最新数据。

为什么说数据不是一次性加载的,而是逐步更新的?

每个 TimelineEntry 表示在某一时刻的“快照”数据。不需要为每一秒钟或每一瞬间的变化都提供数据,而是可以将这些变化按时间分批次更新。例如:

每小时或者每天系统会去刷新小组件,查看当前数据,更新相关显示。通过TimelineEntry,告诉系统下一个更新的时间和更新的内容。

Timeline 和 TimelineProvider 负责按时间给小组件提供新的数据,这样在每个时间点,小组件都能显示正确的信息。

TimelineEntry 和视图中定义数据结构的区别

TimelineEntry 负责“组织”与“时间绑定”的数据,而视图(例如 BankletWidgetEntryView)负责渲染和展示这些数据。

TimelineEntry

用于提供在特定时间点展示的数据。

包含了数据结构和时间信息。

每个条目都是不可变的,且会随着时间的推移而更新。

它是时间轴的一部分,不直接管理视图,而是通过时间轴为视图提供数据。

视图中的数据结构

主要用于视图的显示,不涉及时间更新。

在视图中,使用 TimelineEntry 提供的数据来展示内容,通常是 UI 的一部分。

具体的运作流程

1、TimelineProvider 提供数据

TimelineProvider 的 getTimeline(in:completion:) 方法返回一系列 TimelineEntry,这些条目带有数据,并且每个条目都带有一个时间(即该条目的 date 属性)。这意味着每个条目包含的数据是某个具体时间点的状态。

2、视图使用这些数据

视图组件(例如 BankletWidgetEntryView)通过传入的 TimelineEntry 数据来展示具体的 UI 内容。视图通过 entry 参数获取 TimelineEntry 的数据,进行 UI 渲染。

3、时间更新

随着时间的流逝,TimelineProvider 会返回新的 TimelineEntry,更新小组件的显示内容。这样,小组件就会根据时间的变化更新显示的数据。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注