Swift Combine:在可观察对象中使用定时器发布器
Swift Combine: Using timer publisher in an observable object
在这个问题被标记为 的重复之前,我试图了解发布者的工作方式,因为它的行为方式出乎我的意料。
使用与前面所述问题的答案相同的示例:
// Let's define the view model with my view...
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private let cancellable: AnyCancellable?
let intervalPublisher = Timer.TimerPublisher(
interval: 1.0,
runLoop: .main,
mode: .default)
init() {
self.cancellable = timerPublisher.connect() as? AnyCancellable
}
deinit {
self.cancellable?.cancel()
}
}
struct Clock : View {
@EnvironmentObject var viewModel: TimerViewModel
@State private var currentTime: String = "Initial"
var body: some View {
VStack {
Text(currentTime)
}
.onReceive(timer.intervalPublisher) { newTime in
self.currentTime = String(describing: newTime)
}
}
}
在这个阶段,我只想让我的视图模型直接发布值。我不想声明视图将接收这些类型的值。
理想情况下,我想把我的发布者变成一个正确发布的...我认为下面的代码可以工作:
// Let's define the view model with my view...
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private let cancellable: AnyCancellable?
private let assignCancellable: AnyCancellable?
let intervalPublisher = Timer.TimerPublisher(
interval: 1.0,
runLoop: .main,
mode: .default)
@Published var tick: String = "0:0:0"
init() {
cancellable = intervalPublisher.connect() as? AnyCancellable
assignCancellable = intervalPublisher
.map { new in String(describing: new) }
.assign(to: \TimerViewModel.tick, on: self)
}
deinit {
cancellable?.cancel()
assignCancellable?.cancel()
}
}
struct Clock : View {
@EnvironmentObject var viewModel: TimerViewModel
@State private var currentTime: String = "Initial"
var body: some View {
VStack {
Text(currentTime)
Text(viewModel.tick) // why doesn't this work?
}
.onReceive(timer.intervalPublisher) { newTime in
self.currentTime = String(describing: newTime)
}
}
}
我的 assign
做错了什么?
为什么不触发?
编辑:创建时钟视图后,在 SceneDelegate
上设置了环境对象。排除的代码附在下面:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let view = Clock().environmentObject(TimerViewModel())
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)
self.window = window
window.makeKeyAndVisible()
}
}
“@EnvironmentObject”必须在祖先视图上设置模型对象。
我没看到这个。
所以,我确实重写了你的代码。
import SwiftUI
import Combine
struct ContentView: View {
let timer = TimerViewModel()
var body: some View {
VStack {
Text("Hello World")
TimerView().environmentObject(timer)
}
}
}
struct TimerView: View {
@EnvironmentObject var timer: TimerViewModel
var body: some View {
Text(timer.time)
}
}
class TimerViewModel: ObservableObject {
@Published var time = "init"
private let innerTimer = Timer.TimerPublisher(interval: 1.0, runLoop: .main, mode: .default)
private let cancellable: Cancellable
private var anyCancellable: AnyCancellable?
init() {
cancellable = innerTimer.connect()
anyCancellable = innerTimer
.map({ [=10=].description })
.assign(to: \TimerViewModel.time, on: self)
}
deinit {
cancellable.cancel()
anyCancellable?.cancel()
}
}
这与你原来的有点不同,但我希望没有重要的改变。
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private var assignCancellable: AnyCancellable? = nil
@Published var tick: String = "0:0:0"
init() {
assignCancellable = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.map { String(describing: [=10=]) }
.assign(to: \TimerViewModel.tick, on: self)
}
}
struct ContentView: View {
@State private var currentTime: String = "Initial"
@ObservedObject var viewModel = TimerViewModel()
var body: some View {
VStack {
Text(currentTime)
Text(viewModel.tick) // why doesn't this work?
}
.onReceive(Timer.publish(every: 0.9, on: .main, in: .default).autoconnect(),
perform: {
self.currentTime = String(describing: [=10=])
}
)
}
}
我将 viewModel 设为 ObservedObject 只是为了简化代码。
Timer.publish 方法和自动连接使 Timer 更易于使用。我发现将同一个发布者与多个订阅者一起使用会导致问题,因为第一个取消会杀死发布者。
我删除了 deinit(),因为取消似乎对订阅者来说是隐含的。
onReceive 和 viewModel 的更新之间存在干扰,但将 onReceive 更改为 0.9 修复了该问题。
终于发现Combine中的print()方法对于监视流水线非常有用
首先尝试 Text("Timer: \(date, style:.timer)")
,它会自动为您提供一个计数计时器。这也是一个很好的演示,可以向那些使用 SwiftUI 走错路的人展示,例如MVVM 人群。
还要考虑 Timer.publisher
的优点,它是一个可以存储在 @State
中的结构,因此您甚至不需要 ObservableObject
.
import Combine
import SwiftUI
struct Clock : View {
@State private var timer = Timer.publish(every: 1, on: .main, in:.common).autoconnect()
@State private var currentTime: String = "Initial"
var body: some View {
VStack {
Text(currentTime)
}
.onReceive(timer) { newTime in
self.currentTime = String(describing: newTime)
}
}
}
将 Timer
设置为 @State
的好处是,如果由于某种原因不再显示时钟,则计时器会停止并被丢弃。
但是,如果您确实决定使用 ObservableObject
,那么您只需执行以下操作:
class MyTimer : ObservableObject {
var objectWillChange = Timer.publish(every: 1, on: .main, in:.common).autoconnect()
}
struct Clock2: View {
@StateObject var timer = MyTimer() // causes body to run every second
var body: some View {
VStack {
Text("Hello, World! \(Date())")
}
}
}
还有另一种方法可以更准确地更改日期字符串:
class MyTimer : ObservableObject {
var timer : Timer? = nil
@Published var timeString = ""
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.timeString = timer.fireDate.description
}
}
}
struct ContentView: View {
@StateObject var timer = MyTimer()
var body: some View {
VStack {
Text("Hello, World! \(timer.timeString)")
}
}
}
在这个问题被标记为
使用与前面所述问题的答案相同的示例:
// Let's define the view model with my view...
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private let cancellable: AnyCancellable?
let intervalPublisher = Timer.TimerPublisher(
interval: 1.0,
runLoop: .main,
mode: .default)
init() {
self.cancellable = timerPublisher.connect() as? AnyCancellable
}
deinit {
self.cancellable?.cancel()
}
}
struct Clock : View {
@EnvironmentObject var viewModel: TimerViewModel
@State private var currentTime: String = "Initial"
var body: some View {
VStack {
Text(currentTime)
}
.onReceive(timer.intervalPublisher) { newTime in
self.currentTime = String(describing: newTime)
}
}
}
在这个阶段,我只想让我的视图模型直接发布值。我不想声明视图将接收这些类型的值。
理想情况下,我想把我的发布者变成一个正确发布的...我认为下面的代码可以工作:
// Let's define the view model with my view...
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private let cancellable: AnyCancellable?
private let assignCancellable: AnyCancellable?
let intervalPublisher = Timer.TimerPublisher(
interval: 1.0,
runLoop: .main,
mode: .default)
@Published var tick: String = "0:0:0"
init() {
cancellable = intervalPublisher.connect() as? AnyCancellable
assignCancellable = intervalPublisher
.map { new in String(describing: new) }
.assign(to: \TimerViewModel.tick, on: self)
}
deinit {
cancellable?.cancel()
assignCancellable?.cancel()
}
}
struct Clock : View {
@EnvironmentObject var viewModel: TimerViewModel
@State private var currentTime: String = "Initial"
var body: some View {
VStack {
Text(currentTime)
Text(viewModel.tick) // why doesn't this work?
}
.onReceive(timer.intervalPublisher) { newTime in
self.currentTime = String(describing: newTime)
}
}
}
我的 assign
做错了什么?
为什么不触发?
编辑:创建时钟视图后,在 SceneDelegate
上设置了环境对象。排除的代码附在下面:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let view = Clock().environmentObject(TimerViewModel())
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: view)
self.window = window
window.makeKeyAndVisible()
}
}
“@EnvironmentObject”必须在祖先视图上设置模型对象。
我没看到这个。
所以,我确实重写了你的代码。
import SwiftUI
import Combine
struct ContentView: View {
let timer = TimerViewModel()
var body: some View {
VStack {
Text("Hello World")
TimerView().environmentObject(timer)
}
}
}
struct TimerView: View {
@EnvironmentObject var timer: TimerViewModel
var body: some View {
Text(timer.time)
}
}
class TimerViewModel: ObservableObject {
@Published var time = "init"
private let innerTimer = Timer.TimerPublisher(interval: 1.0, runLoop: .main, mode: .default)
private let cancellable: Cancellable
private var anyCancellable: AnyCancellable?
init() {
cancellable = innerTimer.connect()
anyCancellable = innerTimer
.map({ [=10=].description })
.assign(to: \TimerViewModel.time, on: self)
}
deinit {
cancellable.cancel()
anyCancellable?.cancel()
}
}
这与你原来的有点不同,但我希望没有重要的改变。
import Combine
import SwiftUI
class TimerViewModel: ObservableObject {
private var assignCancellable: AnyCancellable? = nil
@Published var tick: String = "0:0:0"
init() {
assignCancellable = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.map { String(describing: [=10=]) }
.assign(to: \TimerViewModel.tick, on: self)
}
}
struct ContentView: View {
@State private var currentTime: String = "Initial"
@ObservedObject var viewModel = TimerViewModel()
var body: some View {
VStack {
Text(currentTime)
Text(viewModel.tick) // why doesn't this work?
}
.onReceive(Timer.publish(every: 0.9, on: .main, in: .default).autoconnect(),
perform: {
self.currentTime = String(describing: [=10=])
}
)
}
}
我将 viewModel 设为 ObservedObject 只是为了简化代码。
Timer.publish 方法和自动连接使 Timer 更易于使用。我发现将同一个发布者与多个订阅者一起使用会导致问题,因为第一个取消会杀死发布者。
我删除了 deinit(),因为取消似乎对订阅者来说是隐含的。
onReceive 和 viewModel 的更新之间存在干扰,但将 onReceive 更改为 0.9 修复了该问题。
终于发现Combine中的print()方法对于监视流水线非常有用
首先尝试 Text("Timer: \(date, style:.timer)")
,它会自动为您提供一个计数计时器。这也是一个很好的演示,可以向那些使用 SwiftUI 走错路的人展示,例如MVVM 人群。
还要考虑 Timer.publisher
的优点,它是一个可以存储在 @State
中的结构,因此您甚至不需要 ObservableObject
.
import Combine
import SwiftUI
struct Clock : View {
@State private var timer = Timer.publish(every: 1, on: .main, in:.common).autoconnect()
@State private var currentTime: String = "Initial"
var body: some View {
VStack {
Text(currentTime)
}
.onReceive(timer) { newTime in
self.currentTime = String(describing: newTime)
}
}
}
将 Timer
设置为 @State
的好处是,如果由于某种原因不再显示时钟,则计时器会停止并被丢弃。
但是,如果您确实决定使用 ObservableObject
,那么您只需执行以下操作:
class MyTimer : ObservableObject {
var objectWillChange = Timer.publish(every: 1, on: .main, in:.common).autoconnect()
}
struct Clock2: View {
@StateObject var timer = MyTimer() // causes body to run every second
var body: some View {
VStack {
Text("Hello, World! \(Date())")
}
}
}
还有另一种方法可以更准确地更改日期字符串:
class MyTimer : ObservableObject {
var timer : Timer? = nil
@Published var timeString = ""
init() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.timeString = timer.fireDate.description
}
}
}
struct ContentView: View {
@StateObject var timer = MyTimer()
var body: some View {
VStack {
Text("Hello, World! \(timer.timeString)")
}
}
}