在日常开发中,经常会有展示计时类的定时更新类任务,比如要展示当前时间,比较常见的方式是通过Timer:
swift 代码解读复制代码struct DisplayTime: View {
@State private var date = Date.now
@State private var timerToken: AnyCancellable?
private var formatter = DateFormatter()
var body: some View {
List {
Section("Timer") {
Text(formatDate(date))
.font(.largeTitle)
}
}
.onAppear {
timerToken = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
date = .now
}
}
}
private func formatDate(_ date: Date) -> String {
formatter.timeStyle = .medium
return formatter.string(from: date)
}
}
在iOS 15以后,SwiftUI引入了一个新的组件叫TimelineView,可以更快捷的实现上述功能:
swift 代码解读复制代码TimelineView(.periodic(from: .now, by: 1)) { context in
Text(formatDate(context.date))
.font(.largeTitle)
}
这个组件可以让使用者省去管理Timer的工作。
TimelineView
TimelineView是一个本身没有视图的View,可以按照使用者提供的调度策略来自动刷新内容视图。
swift 代码解读复制代码extension TimelineView : View where Content : View {
public init(_ schedule: Schedule, @ViewBuilder content: @escaping (TimelineViewDefaultContext) -> Content)
}
使用TimelineView需要两部分内容:调度策略和视图的展示逻辑
调度策略
SwiftUI提供了几种可以直接使用的调度策略:
periodic
swift 代码解读复制代码public static func periodic(from startDate: Date, by interval: TimeInterval) -> PeriodicTimelineSchedule
periodic调度策略会按照指定的时间间隔定期执行
everyMinute
swift 代码解读复制代码public static var everyMinute: EveryMinuteTimelineSchedule { get }
每分钟开始的时候执行
explicit
swift 代码解读复制代码public static func explicit<S>(_ dates: S) -> ExplicitTimelineSchedule<S> where Self == ExplicitTimelineSchedule<S>, S : Sequence, S.Element == Date
按照指定的时间序列执行展示逻辑, 如下所示,指定了一个立即,5s,10s更新界面的策略
swift 代码解读复制代码 TimelineView(.explicit([
.now,
.now.addingTimeInterval(5),
.now.addingTimeInterval(10)
])) { context in
Text(formatDate(context.date))
.font(.largeTitle)
}
而之前的两种可以看做是时间序列可以不断刷新的explicit的特例
animation
swift 代码解读复制代码public static func animation(minimumInterval: Double? = nil, paused: Bool = false) -> AnimationTimelineSchedule
public static var animation: AnimationTimelineSchedule { get }
animation策略如其名字所示,是为了用于完成一些动画效果的,刷新频率和屏幕的刷新频率一致。如下所示可以创建一个简易的字母循环滚动的效果。
swift 代码解读复制代码 GeometryReader { reader in
TimelineView(.animation) { context in
let _ = { dataModel.offset -= 1
if dataModel.offset <= -reader.size.width {
dataModel.offset = 0
}
}()
Text("Hello TimelineView")
.font(.largeTitle)
.offset(x: dataModel.offset)
}
}
调度策略 | 说明 | 场景 |
---|---|---|
periodic | 定期执行,间隔可控 | 显示动态数据,比如倒计时 |
everyMinute | 每分钟开始时触发 | 展示当前时间或刷新分钟级数据 |
explicit | 根据指定的时间序列触发 | 需要在特定时刻更新视图 |
animation | 为动画效果设计,刷新的频率与屏幕刷新一致 | 实现滚动或其他高帧率效果的动画 |
展示逻辑
展示逻辑接收一个context参数,context包含两个属性
swift 代码解读复制代码/// 触发刷新的时间点
public let date: Date
/// 系统建议的刷新频率
public let cadence: TimelineView<Schedule, Content>.Context.Cadence
简易跑马灯
在前面的介绍里,实现了一个简易跑马灯,但是有两个问题:
- 正常的跑马灯应该是从屏幕的一侧消失的内容会从屏幕的另一侧出来
- 更新位移是在TimelineView的每个更新周期里固定加了1,但是可能这个更新间隔可能不能保证完全一样 所以可以对上面的代码做些修改,变成一个简易的走马灯组件:
swift 代码解读复制代码inal class SimpleMarqueeDataContext {
var offset: CGFloat = 0
var contentWidth: CGFloat = 0
private var lastTimeInterval: TimeInterval?
var containerWidth: CGFloat = 0
public func updateOffset(_ timeInterval: TimeInterval) {
if let lastTimeInterval = lastTimeInterval {
let diff = timeInterval - lastTimeInterval
offset += diff * 50
}
if offset >= containerWidth {
offset = 0
}
lastTimeInterval = timeInterval
}
}
struct SimpleMarquee<Content>: View where Content: View {
private var content: () -> Content
private var dataContext: SimpleMarqueeDataContext = .init()
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
GeometryReader { reader in
TimelineView(.animation) { context in
let _ = {
dataContext.updateOffset(context.date.timeIntervalSince1970)
}()
HStack(spacing: 0) {
content()
.measureWidth { width in
dataContext.contentWidth = width
}
content()
.offset(x: reader.size.width - dataContext.contentWidth)
}
.offset(x: -dataContext.offset)
}
.onAppear {
dataContext.containerWidth = reader.size.width
}
}
}
}
改动的点主要是:
- 组件通过context上的date和上次date的差值来计算位移的增加量
- 为了实现从屏幕另一侧出现的效果,多增加了一个内容视图的实例 这个组件可以这么使用:
swift 代码解读复制代码struct SimpleMarqueeView: View {
var body: some View {
VStack {
SimpleMarquee {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, TimelineView!")
}
.padding()
.border(.red)
}
}
}
}
最终的效果:
评论记录:
回复评论: