如何正确链接 CADisplayLink 和 SwiftUI?

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 有现成的方法?

Rob*_*Rob 8

一些观察:

\n
    \n
  1. 至少,我会考虑用 标记它@MainActor,因为它需要在 a 上RunLoop(通常意味着RunLoop.main)。请谨慎使用RunLoop.current与@MainActor. 如果您没有显式指定主要参与者,如果您意外地从任何其他异步上下文启动它,它可能会默默地失败。使用 时@MainActor,编译器会警告我们错误的用法。

    \n
  2. \n
  3. 我个人不建议将其设为单例。

    \n
  4. \n
  5. 您需要确保invalidate在完成后有一个显示链接的机制。

    \n
  6. \n
\n
\n

在更高级的观察中,更自然的模式可能是异步序列,或者AsyncSequence更具体地说是AsyncStream:

\n
//\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}\n
Run Code Online (Sandbox Code Playgroud)\n

然后你可以做这样的事情:

\n
struct 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}\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,当该视图被关闭时,.task {\xe2\x80\xa6}修饰符会自动为我们取消,从而自动取消我们的异步序列。

\n

如果您不使用修饰符.task,则必须自己取消异步序列,例如:

\n
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}\n
Run Code Online (Sandbox Code Playgroud)\n

有关异步序列的更多信息,请参阅 WWDC 2021 视频Meet AsyncSequence。

\n


Rob*_*ert 2

我喜欢你的解决方案——效果很好。这是用闭包代替@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)