Before this question gets marked as duplicate of this other question, I am trying to understand how the publisher works as it behaves in a way I do not expect.
Using the same example as the answer from the question previously stated:
// 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)
}
}
}
At this stage, all I wanted to do is my view model to publish the value directly. I don't want to have to declare the view will be receiving these sorts of values.
Ideally, I want to turn my publisher into a published properly... I though that the following code would work:
// 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)
}
}
}
What am I doing wrong for my assign
?
Why isn't triggering?
Edit: the environment object was set on the SceneDelegate
once the Clock view was created. The code excluded is attached below:
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()
}
}
This is a bit different to your original but nothing important is changed I hope.
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: $0) }
.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: $0)
}
)
}
}
I made viewModel an ObservedObject just to simplify the code.
The Timer.publish method along with autoconnect make Timer easier to use. I have found that using the same publisher with multiple subscribers causes problems as the first cancel kills the publisher.
I removed the deinit() as the cancel seems to be implicit for subscribers.
There was an interference between updates from onReceive and viewModel but changing the onReceive to 0.9 fixed that.
Finally I have discovered that the print() method in Combine is very useful for watching pipelines.
First try Text("Timer: \(date, style:.timer)")
which gives you a counting timer automatically. Also, consider the advantage of Timer.publisher
that it is a struct that can be stored in @State
so you don't even need 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)
}
}
}
The advantage to having the Timer
as @State
is if the Clock is no longer displayed for some reason then the timer stops and is discarded too.
If however, you did decide to use ObservableObject
then you could simply do:
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())")
}
}
}
And here is another way that gives a more accurate change to the date string, however I don't like this sample because the date formatting needs to be done in a Text
inside body so that the label updates if the region settings change.
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)")
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With