1. Widget动画基础知识
1. iOS17之前是不支持动画的,iOS17之后支持了转场动画
2. 有私有方法,可以实现小组件动画。但是app上架很危险
参考(利用私有api):https://github.com/TopWidgets/SwingAnimation
2. Widget动画实践,iOS17+
2.1 利用转场动画,实现简单过渡
注意点
1. 代码创建了2000个entry,实际上在不同机型上表现不一,有可正常运行也有运行不起来的。具体原因没探究出来,猜测与内存占用有关。所以这里创建多少entry,需要根据项目实际情况调研
2. 动画仅在iOS17+有用
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), message: "")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
let entry = SimpleEntry(date: Date(), message: "")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
//1. 定义2000个entry,在一条timeline中。
//2. 每个entry间隔3秒
let date = Date()
var entries: [Entry] = []
for index in 0...2000 {
let entry = SimpleEntry(date: date + Double(index) * 3.0, message: "当前entry:\(index)")
entries.append(entry)
}
//.atEnd, 所有entry结束后刷新时间线
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let message: String
}
struct widgetEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.message)
.font(.title)
.foregroundColor(Color.red)
//转场动画配置
.id(entry.message)
.transition(.asymmetric(insertion: .move(edge: .bottom).combined(with: .opacity), removal: .move(edge: .top).combined(with: .opacity)))
.animation(.easeInOut(duration: 0.3), value: entry.message)
}
}
struct widget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "kind", provider: Provider()) { entry in
if #available(iOS 17.0, *) {
widgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
widgetEntryView(entry: entry)
.padding()
.background(Color.clear)
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
2.2 利用Toggle | Button, 加上.invalidatableContent(), 实现闪动的效果
注意点
.invalidatableContent(),看介绍不像是会有这种效果的。但是确实出现了这个效果,具体没深入探究
import WidgetKit
import SwiftUI
import AppIntents
/// 点击交互用AppIntent
@available(iOS 17.0, *)
struct TestIntent: AppIntent {
static var title: LocalizedStringResource = "button press"
static var description: IntentDescription? = IntentDescription(stringLiteral: "none")
func perform() async throws -> some IntentResult {
//等待5秒,模仿网络请求
await withCheckedContinuation { c in
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
c.resume()
}
}
return .result()
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), message: "")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
let entry = SimpleEntry(date: Date(), message: "")
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
let timeline = Timeline(entries: [SimpleEntry(date: Date(), message: "")], policy: .never)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let message: String
}
struct widgetEntryView : View {
var entry: Provider.Entry
var body: some View {
if #available(iOS 17, *) {
VStack(spacing: 5) {
Text("数据刷新中")
.font(.title).foregroundColor(Color.red)
.invalidatableContent()
Button("button press me", intent: TestIntent())
Toggle("toggle press me", isOn: false, intent: TestIntent())
}
}
}
}
struct widget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "kind", provider: Provider()) { entry in
if #available(iOS 17.0, *) {
widgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
widgetEntryView(entry: entry)
.padding()
.background(Color.clear)
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
2.3 官方介绍了一个,在小组件中显示动态日期。
利用Text特性,官方地址