Art*_*m V 5 animation ios swift swiftui
我正在创建一些动画,为了在各种设备上流畅,我使用 CADisplayLink,如下所示:
// DisplayLink.swift
import SwiftUI
class DisplayLink: NSObject, ObservableObject {
@Published var frameDuration: CFTimeInterval = 0
@Published var frameChange: Bool = false
static let sharedInstance: DisplayLink = DisplayLink()
func createDisplayLink() {
let displaylink = CADisplayLink(target: self, selector: #selector(frame))
displaylink.add(to: .current, forMode: RunLoop.Mode.default)
}
@objc func frame(displaylink: CADisplayLink) {
frameDuration = displaylink.targetTimestamp - displaylink.timestamp
frameChange.toggle()
}
}
Run Code Online (Sandbox Code Playgroud)
然后我在应用程序中初始化它一次:
// App.swift
import SwiftUI
@main
struct App: App {
init() {
DisplayLink.sharedInstance.createDisplayLink()
}
var body: some Scene {
WindowGroup {
CircleView()
}
}
}
Run Code Online (Sandbox Code Playgroud)
我在整个应用程序中使用它,如下所示:
// CircleView.swift
import SwiftUI
struct CircleView: View {
@ObservedObject var displayLink = DisplayLink.sharedInstance
//
var body: some View {
//
.onChange(of: displayLink.frameChange) { _ in
print(displayLink.frameDuration)
}
}
Run Code Online (Sandbox Code Playgroud)
一切都很好,但是使用这种方法的正确性如何?也许 SwiftUI 有现成的方法?
一些观察:
\n至少,我会考虑用 标记它@MainActor,因为它需要在 a 上RunLoop(通常意味着RunLoop.main)。请谨慎使用RunLoop.current与@MainActor. 如果您没有显式指定主要参与者,如果您意外地从任何其他异步上下文启动它,它可能会默默地失败。使用 时@MainActor,编译器会警告我们错误的用法。
我个人不建议将其设为单例。
\n您需要确保invalidate在完成后有一个显示链接的机制。
在更高级的观察中,更自然的模式可能是异步序列,或者AsyncSequence更具体地说是AsyncStream:
//\n// CADisplayLink+Sequence.swift\n//\n// Created by Robert Ryan on 2/20/23.\n//\n\nimport UIKit\n\n@MainActor\nextension CADisplayLink {\n static func events() -> AsyncStream<CADisplayLink> {\n AsyncStream { continuation in\n let displayLink = DisplayLink { displayLink in\n continuation.yield(displayLink)\n }\n\n continuation.onTermination = { _ in\n Task { await displayLink.stop() }\n }\n }\n }\n}\n\n// There is no need to expose this, so let\'s put it in a private extension\n\nprivate extension CADisplayLink {\n @MainActor\n private class DisplayLink: NSObject {\n private var displayLink: CADisplayLink!\n private let handler: (CADisplayLink) -> Void\n\n init(mode: RunLoop.Mode = .default, handler: @escaping (CADisplayLink) -> Void) {\n self.handler = handler\n super.init()\n\n displayLink = CADisplayLink(target: self, selector: #selector(handle(displayLink:)))\n displayLink.add(to: .main, forMode: mode)\n }\n\n func stop() {\n displayLink.invalidate()\n }\n\n @objc func handle(displayLink: CADisplayLink) {\n handler(displayLink)\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n然后你可以做这样的事情:
\nstruct ContentView: View {\n @State var timestamp: CFTimeInterval = 0\n\n var body: some View {\n VStack {\n Text("\\(timestamp, specifier: "%0.2f")")\n .monospacedDigit()\n }\n .task {\n for await event in CADisplayLink.events() {\n timestamp = event.targetTimestamp\n }\n }\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n请注意,当该视图被关闭时,.task {\xe2\x80\xa6}修饰符会自动为我们取消,从而自动取消我们的异步序列。
如果您不使用修饰符.task,则必须自己取消异步序列,例如:
struct ContentView: View {\n @ObservedObject var viewModel = ViewModel()\n\n var body: some View {\n VStack {\n Text(viewModel.string)\n .monospacedDigit()\n\n Button("Start") {\n viewModel.start()\n }\n\n Button("Stop") {\n viewModel.stop()\n }\n }\n .onDisappear {\n viewModel.stop()\n }\n }\n}\n\n@MainActor\nclass ViewModel: ObservableObject {\n @Published var string = "Not running"\n\n private var task: Task<Void, Never>?\n\n private var formatter: DateFormatter = {\n let formatter = DateFormatter()\n formatter.dateFormat = "HH:mm:ss.SS"\n return formatter\n }()\n\n func start() {\n task?.cancel()\n\n task = Task {\n for await _ in CADisplayLink.events() {\n string = formatter.string(from: Date())\n }\n }\n }\n\n func stop() {\n task?.cancel()\n task = nil\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n有关异步序列的更多信息,请参阅 WWDC 2021 视频Meet AsyncSequence。
\n我喜欢你的解决方案——效果很好。这是用闭包代替@Published 的替代方案。
class DisplayLink: NSObject, ObservableObject {
private var displaylink: CADisplayLink?
private var update: ((TimeInterval) -> Void)?
func start(update: @escaping (TimeInterval) -> Void) {
self.update = update
displaylink = CADisplayLink(target: self, selector: #selector(frame))
displaylink?.add(to: .current, forMode: .default)
}
func stop() {
displaylink?.remove(from: .current, forMode: .default)
update = nil
}
@objc func frame(displaylink: CADisplayLink) {
let frameDuration = displaylink.targetTimestamp - displaylink.timestamp
update?(frameDuration)
}
}
Run Code Online (Sandbox Code Playgroud)
在你的 swift UI 类中:
@State private var displayLink = DisplayLink()
...
displayLink.start { frameDuration in
update(frameDuration)
}
...
displayLink.stop()
Run Code Online (Sandbox Code Playgroud)