Capture the Very Last Change of a Binding in SwiftUI / Combine - swift

When I have a Binding in SwiftUI and I want to save whenever the binding changes I do (e.g. on a TextField)
var myText: String { /* value is derived */ }
func save(_ text: String) { /* doing the save stuff */ }
TextFiel(text: .init(get: { myText }, set: { save($0) })
Doing this, save() gets called whenever the binding changes. In some cases this might not be ideal, e.g. when save() makes a server call or some expensive computations. So what I'm looking for, is to get notified whenever the binding changes for the last time.
Maybe some kind of delayed observer that fires x seconds after the final change and get's invalidated if another change happens earlier than that threshold. Does Combine offer something like this?
Disclaimer: This question is about bindings in general and not just about TextFields in particular. The Textfield is only a coding example , so .onCommit is not the solution I'm looking for ;)

The debounce operator in Combine does this. It waits until a new value hasn't been pushed through the pipeline for X amount of time and then sends a signal.
In the case of a TextField, you'll still want a "normal" binding, because you'd want the user to see the characters appearing in real-time even if the data doesn't get sent to the server (or whatever other expensive operation) immediately.
import Combine
import SwiftUI
class ViewModel : ObservableObject {
#Published var text : String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$text
.debounce(for: .seconds(2), scheduler: RunLoop.main)
.sink { (newValue) in
//do expensive operation
print(newValue)
}
.store(in: &cancellables)
}
}
struct ContentView : View {
#StateObject var vm = ViewModel()
var body: some View {
TextField("", text: $vm.text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
Depending on what your operation does, you may also want to add a .receive(on:) operator. To update the UI, you'd want to receive on the main thread. But, for other expensive operations, you can send things to a background queue this way.

Related

Is there some thing like React.useEffect in SwiftUI?

I'm new to SwiftUI and was wondering if there is a concept similar to React.useEffect in SwiftUI.
Below is my code for listening keyboard events on macos.
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
var hello: String
#State var monitor: Any?
#State var text = ""
init(hello: String) {
self.hello = hello
print("ContentView init")
}
var body: some View {
VStack{
Text(hello)
.padding()
TextField("input", text: $text)
}
.onAppear {
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
print(hello)
return nil
}
}
}
}
struct MainView: View {
#State var hello: String = "h"
var body: some View {
ContentView(hello: hello)
Button(action: {
hello += "_h"
}) {
Text("tap me")
}
}
}
PlaygroundPage.current.setLiveView(MainView())
The playground output is as follows
ContentView init
h
h
ContentView init
h
h
h
Since onAppear trigger only once, even ContentView init multiple times. So the event callback here always prints the first captured value ("h").
So where should I add event listener and where should I remove it?
In React, you use useEffect from within a Component in order to declare a task or operation which causes side effects outside the rendering phase.
Well, SwiftUI is not exactly React, and there are problems and use cases which you would solve in a complete different approach. But, when trying to find something similar:
In SwiftUI you could call any function which is called from any "action" closure, for example from a SwiftUI Button. This function can modify #State variables, without disrupting the rendering phase.
Or you can use the Task Modifier, i.e. calling .task { ... } for a SwiftUI view, which comes probably closest.
Personally, I would not declare to use any task or operation which causes side effects to the AppState or Model within a SwiftUI View's body function. Rather, I prefer to send actions (aka "Intent", "Event") from the user to a Model or a ViewModel, or a Finite State Automaton. These events then get processed in a pure function, call it "update()", which performs the "logic", and this pure function may declare "Effects". These effects will then be called outside this pure update function, cause there side effects wherever they need to, and return a result which is materialised as an event, which itself gets fed into the pure update function again. That update function produces a "view state", which the view needs to render.
Now, I want to clarify some potential misconceptions:
"Since onAppear trigger only once, even ContentView init multiple times"
onAppear
This can be actually called several times for a view which you identify on the screen as a "view".
Usually, it is not always without issues to utilise onAppear for performing some sort of initialisation or setup. There are approaches to avoid these problem altogether, though.
"ContentView init"
You are better off viewing a SwiftUI View as a "function" (what?)
With that "function" you achieve two things:
Create an underlying view whose responsibility is to render pixels and also create (private) data for this view which it needs to render accordingly.
Modify this data or attributes of this underlying view.
For either action, you have to call the SwiftUI View's initialiser.
When either action is done, the SwiftUI View (a struct!) will diminish again. Usually, the struct's value, the SwiftUI View resides on the stack only temporarily.
Variables declared as #State and friends, are associated to the underlying view which is responsible to render the pixels. Their lifetime is bound to this renderable view which you can perceive on the screen.
Now, looking at your code, it should work as expected. You created a private #State variable for the event handler object. This seems to be the right approach. However, #State is meant as a private variable where a change would cause the view to render differently. Your event handler object is actually an "Any", i.e. a reference. This reference never changes: it will be setup at onAppear then it never changes anymore, except onAppear will be called again for the same underlying renderable view. There is probably a better solution than using #State and onAppear for your event handler object (see below later).
Now, when you want to render the event's value (aka mask as NSEvent.EventTypeMask) then you need another #State variable in your SwiftUI View of this type, which you set/update in the notification handler. The variable should be a struct or enum, not a reference!
SwiftUI then notifies the changes to this variable and in turn will call the body function where you explicitly render this value. Note, that you can update a #State variable from any thread.
Problems
According the documentation "You must call removeMonitor(_:) to stop the monitor."
Unfortunately, your #State variable which holds the reference to the event handler object will not call removeMonitor(_:) when the underlying renderable view gets deallocated.
Bummer!
What you have to do is, changing your design. What you need to do is to introduce a "Model" which is an ObservableObject. It should publish a value (a representation of what you receive in the notification handler) which will be rendered in the SwiftUI view accordingly.
This Model should also receive an event (say a function will be called for the Model from the SwiftUI view) when the view appears, where the Model then creates the event handler object, unless it has been created already (which completely solves your onAppear issues). Alternatively, just create the event handler once and only once in the Model's initialiser - which is arguable the better solution.
When the event handler's notification handler will be called, you update the published value of your Model accordingly.
Integrating the Model - an ObservableObject - properly into a SwiftUI view is a standard pattern in SwiftUI. Please look for help on SO, if you are uncertain how to accomplish this.
Now, since the Model is a class value, you can ensure to call removeMonitor(_:) in its deinit function.
Headstart
import SwiftUI
final class EventHandlerModel: ObservableObject {
private var monitor: Any!
#Published private(set) var viewState: String = ""
init() {
monitor = NSEvent.addLocalMonitorForEvents(
matching: .keyDown
) { event in
assert(Thread.isMainThread)
self.viewState = "\(event)"
return event
}
}
deinit {
guard let monitor = self.monitor else {
return
}
NSEvent.removeMonitor(monitor)
}
}
struct ContentView: View {
#StateObject private var model = EventHandlerModel()
var body: some View {
Text(verbatim: model.viewState)
}
}

Having an #EnvironmentObject in the view causes Tasks to be executed on the main thread

I ran into an issue where my async functions caused the UI to freeze, even tho I was calling them in a Task, and tried many other ways of doing it (GCD, Task.detached and the combination of the both). After testing it I figured out which part causes this behaviour, and I think it's a bug in Swift/SwiftUI.
Bug description
I wanted to calculate something in the background every x seconds, and then update the view, by updating a #Binding/#EnvironmentObject value. For this I used a timer, and listened to it's changes, by subscribing to it in a .onReceive modifier. The action of this modifer was just a Task with the async function in it (await foo()). This works like expected, so even if the foo function pauses for seconds, the UI won't freeze BUT if I add one #EnvironmentObject to the view the UI will be unresponsive for the duration of the foo function.
GIF of the behaviour with no EnvironmentVariable in the view:
GIF of the behaviour with EnvironmentVariable in the view:
Minimal, Reproducible example
This is just a button and a scroll view to see the animations. When you press the button with the EnvironmentObject present in the code the UI freezes and stops responding to the gestures, but just by removing that one line the UI works like it should, remaining responsive and changing properties.
import SwiftUI
class Config : ObservableObject{
#Published var color : Color = .blue
}
struct ContentView: View {
//Just by removing this, the UI freeze stops
#EnvironmentObject var config : Config
#State var c1 : Color = .blue
var body: some View {
ScrollView(showsIndicators: false) {
VStack {
HStack {
Button {
Task {
c1 = .red
await asyncWait()
c1 = .green
}
} label: {
Text("Task, async")
}
.foregroundColor(c1)
}
ForEach(0..<20) {x in
HStack {
Text("Placeholder \(x)")
Spacer()
}
.padding()
.border(.blue)
}
}
.padding()
}
}
func asyncWait() async{
let continueTime: Date = Calendar.current.date(byAdding: .second, value: 2, to: Date())!
while (Date() < continueTime) {}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Disclaimer
I am fairly new to using concurrency to the level I need for this project, so I might be missing something, but I couldn't find anything related to the searchwords "Task" and "EnvironmentObject".
Question
Is this really a bug? Or am I missing something?
As far as I can tell, your code, with or without the #EnvrionmentObject, should always block the main thread. The fact that it doesn't without #EnvironmentObject may be a bug, but not the other way around.
In your example, you block the main thread -- you call out to an async function that runs on the context inherited from its parent. Its parent is a View, and runs on the main actor.
Usually in this situation, there's confusion about what actually runs something outside of the inherited context. You mentioned using Task.detached, but as long as your function was still marked async on the parent, with no other modifications, in would still end up running on the main actor.
To avoid inheriting the context of the parent, you could, for example, mark it as nonisolated:
nonisolated func asyncWait() async {
let continueTime: Date = Calendar.current.date(byAdding: .second, value: 2, to: Date())!
while (Date() < continueTime) {}
}
Or, you could move the function somewhere (like to an ObservableObject outside of the View) that does not explicitly run on the main actor like the View does.
Note that there's also a little bit of deception here because you've marked the function as async, but it doesn't actually do any async work -- it just blocks the context that it's running on.
The issue is that Task { ... } adds the task to the current actor. If you have some slow, synchronous task, you never want that on the main actor. People often conflate Task { ... } with DispatchQueue.global().async { ... }, but they are not the same thing.
And you should also avoid putting anything slow and synchronous in an #MainActor isolated function.
If you want to get some slow and synchronous process off the current actor, you would generally use Task.detached { ... }. Or you could create a separate actor for the time consuming process.
But in this case, there is no need to do any of this. Instead, use Task.sleep, which is a rendition of sleep designed for Swift concurrency which “doesn’t block the underlying thread.”
Button {
Task {
c1 = .red
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
c1 = .green
}
} label: {
Text("Task, async")
}
.foregroundColor(c1)
Avoid spinning. Thread.sleep is a little better, but is still inadvisable. Use Task.sleep.

Is there a way to pass #Published object as a func argument UIKit?

I'm using UIKit not SwiftUI. I found solutions which all in SwiftUI but not solved my problem.
I have a #Published object like:
#Published var searchText = ""
I'm using that #Published object for search functionality like in following function. Also, I'm trying to reach that function outside of corresponding class which is final class MainViewModel
final class MainViewModel {
#Published var searchText = ""
//subscribes to the searchText 'Publisher'
func searchTextManipulation(searchText: String) {
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] (text) in //(text) is always empty string ""
...
I want to use that function parameter searchText: String as $searchText to search with text come from another screen something like:
final class MainViewController {
let viewModel = MainViewModel()
#Published var searchText = ""
override func viewDidLoad() {
viewModel.searchTextManipulation($searchText: searchText) //but it's not possible.
}
}
Can you show me a workaround for solving that crucial part?
Your code suggests that need to understand how Combine works a little bit better.
When you put together a publisher with a set of operators:
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in doSomething(text) }
It's like you're putting together a set of pipes. You only have to put them together once. When they are assembled, and you put something into the input side, the pipes will transform the value and deliver it to the output side. If you subscribe to the output side, using sink or assign for example, then you can catch the transformed value and do something with it.
But you only need to build the pipeline once.
After you build the pipeline, you also have to keep ahold of it.
Your searchTextManipulation function is building a pipeline that is immediately destroyed when you leave the function. To prevent that you have to store a subscription (in the form of an AnyCancellable). sink and assign return subscriptions and you should retain those to keep the pipeline from being destroyed.
An #Published property creates a Publisher, with the same name and a $ prefix, that emits a value when the property is initialized or changed. In other words, when you change searchText the system will put the value into the input of a pipeline named $searchText for you.
So a complete example of what it looks like you are trying to do is the playground below.
MainViewModel has two published properties. One is searchText and the other is searchResults. We set up the model so that when searchText changes (with a debounce and uniq) a new value of searchResults is published.
In init we build a pipeline that starts with $searchText. Through the pipeline we transform a search string into an array of search results. (The idea that each step in a pipeline transforms a value from one thing to another is the model I use in my head to decide how to chain operators.)
The assign at the end of the pipeline takes the transformed result and assigns it to the searchResults property.
Note: To ensure our pipeline is not destroyed when init ends, we have to capture and store the subscription done by assign.
With that pipeline in place, whenever code changes searchText the pipeline transform that value into an array of search results, and store the result in searchResults. Since MainViewModel owns the subscription, the pipeline will only be destroyed when the view model instance is destroyed.
So how do you use it? Well, the searchResults get published to a pipeline named $searchResults whenever they change. So all you have to do is listen that pipeline for changes.
In MainViewController we want to listen for those changes. In viewDidLoad we set up a short pipeline that starts with $searchResults, drops the first result (it's sent when searchResults is initialized) and uses sink` to print the results to the console.
Again note: we are constructing the pipeline once, keeping track of it by storing the subscription, and it will run automatically every time a new value is put into the seachResults.
I added a function called userDoesSomething that should be called whenever you want to kick off the search process.
userDoesSomething assigns some text value that came from the user to the model's searchText property.
Because it's #Published, the system sends the new value into the $searchText pipeline created by the view model's init function.
The pipeline transforms the text into and array of search results and assigns the array to searchResults
Because searchResults is #Published it sends the array into the pipeline called $searchResults
The MainViewController created a pipeline to listen to $searchResults and that pipeline runs with the new results.
That pipeline sends the value to a sink that simply prints it to the console.
The bit at the very end of the playground simulates a user sending a string with each of the letters in the English alphabet ('a'-'z') once every 0.2 seconds to userDoesSomething. That kicks of the numbered sequence I described above and gives you something interesting to look at on the Playground console.
import UIKit
import Combine
import PlaygroundSupport
let TheItems = ["The", "Quick", "Brown", "Fox", "Jumped", "Over", "the", "Lazy", "Dogs"]
final class MainViewModel {
#Published var searchText = ""
#Published private(set) var searchResults = []
var subscription: AnyCancellable! = nil
init() {
self.subscription = $searchText
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.removeDuplicates()
.map(findItems)
.assign(to: \.searchResults, on: self)
}
}
private func findItems(searchText: String) -> [String] {
if let firstChar = searchText.lowercased().first {
return TheItems.filter { $0.lowercased().contains(firstChar) }
}
return []
}
final class MainViewController: UIViewController {
var searchResultsSubscription : AnyCancellable! = nil
let viewModel = MainViewModel()
override func loadView() {
self.view = UIView(frame: CGRect(x: 0,y: 0,width: 200,height: 200))
}
override func viewDidLoad() {
searchResultsSubscription = viewModel.$searchResults
.dropFirst()
.sink{
// do what you like here when search results change
debugPrint($0)
}
}
func userDoesSomething(generates searchText: String) {
viewModel.searchText = searchText;
}
}
let model = MainViewModel()
let controller = MainViewController()
PlaygroundSupport.PlaygroundPage.current.liveView = controller
(UInt8(ascii: "a")...UInt8(ascii:"z"))
.enumerated()
.publisher
.sink { index, char in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200 * index)) {
controller.userDoesSomething(generates: String(UnicodeScalar(char)))
}
}
Yes, you can pass a #Published wrapper around, by changing the function signature to expect the projected value of the property wrapper:
func searchTextManipulation(searchText: Published<String>.Publisher) {
...
}
, and later in your viewDidLoad just use the $:
override func viewDidLoad() {
viewModel.searchTextManipulation(searchText: $searchText)
}
Everytime you write $someVar, what the compiler does is to de-sugar it into _someVar.projectedValue, and in case of Published, the projected value is of type Published<T>.Publisher. This is not something specific to SwiftUI, and it's part of the language, so you can freely use it in any parts of your code (assuming it makes sense, ofcourse).

How to detect key press and release in swiftUI (macOS)

Not much to say other than the title. I want to be able to take action in a swiftUI view when a key is pressed and when it is released (on macOS). Is there any good way to do this in swiftUI and if not is there any workaround?
Unfortunately keyboard event handling is one of those areas where it's painfully obvious that SwiftUI was designed first and foremost for iOS, with macOS being an afterthought.
If the key you're trying to detect is a modifier to a mouse click, such as cmd, option, or shift, you can use the .modifiers with onTapGesture to distinguish it from an unmodified onTapGesture. In that case, my experience with it is that you want the .onTapGesture call that uses .modifiers to precede the unmodified one.
Handling general key events for arbitrary views requires going outside of SwiftUI.
If you just need it for one View, one possibility is to implement that view with AppKit so you can receive the keyboard events via the ordinary Cocoa firstResponder mechanism, and then wrap that view in SwiftUI's NSViewRepresentable. In that case your wrapped NSView would update some #State property in NSViewRespresentable. A lot of developers using SwiftUI for macOS do it this way. While this is fine for a small number of views, if it turns out that you have to implement a lot of views in AppKit to make them usable in SwiftUI, then you're kind of defeating the point of using SwiftUI anyway. In that case, just make it an ordinary Cocoa app.
But there is another way...
You could use another thread that uses CGEventSource to poll the keyboard state actively in conjunction with a SwiftUI #EnvironmentObject or #StateObject to communicate keyboard state changes to the SwiftUI Views that are interested in them.
Let's say you want to detect when the up-arrow is pressed. To detect the key, I use an extension on CGKeyCode.
import CoreGraphics
extension CGKeyCode
{
// Define whatever key codes you want to detect here
static let kVK_UpArrow: CGKeyCode = 0x7E
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
}
Of course, you have to use the right key codes. I have a gist containing all of the old key codes. Rename them to be more Swifty if you like. The names listed go back to classic MacOS and were defined in Inside Macintosh.
With that extension defined, you can test if a key is pressed anytime you like:
if CGKeyCode.kVK_UpArrow.isPressed {
// Do something in response to the key press.
}
Note these are not key-up or key-down events. It's simply a boolean detecting if the key is pressed when you perform the check. To behave more like events, you'll need to do that part yourself by keeping track of key state changes.
There are multiple ways of doing this, and the following code is not meant to imply that this is the "best" way. It is simply a way. In any case, something like the following code would go (or be called from) wherever you do global initialization when you app starts.
// These will handle sending the "event" and will be fleshed
// out further down
func dispatchKeyDown(_ key: CGKeyCode) {...}
func dispatchKeyUp(_ key: CGKeyCode) {...}
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate let sleepSem = DispatchSemaphore(value: 0)
fileprivate let someConcurrentQueue = DispatchQueue(label: "polling", attributes: .concurrent)
someConcurrentQueue.async
{
while true
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
// Sleep long enough to avoid wasting CPU cycles, but
// not so long that you miss key presses. You may
// need to experiment with the .milliseconds value.
let_ = sleepSem.wait(timeout: .now() + .milliseconds(50))
}
}
The idea is just to have some code that periodically polls key states, compares them with previous states, dispatches an appropriate "event" when they change, and updates the previous states. The code above does that by running an infinite loop in a concurrent task. It requires creating a DispatchQueue with the .concurrent attribute. You can't use it on DispatchQueue.main because that queue is serial not concurrent, so the infinite loop would block the main thread, and the program would become unresponsive. If you already have a concurrent DispatchQueue you use for other reasons, you can just use that one instead of creating one just for polling.
However, any code that accomplishes the basic goal of periodic polling will do, so if you don't already have a concurrent DispatchQueue and would prefer not to create one just to poll for keyboard states, which would be a reasonable objection, here's an alternate version that uses DispatchQueue.main with a technique called "async chaining" to avoid blocking/sleeping:
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate func pollKeyStates()
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50))
{
// The infinite loop from previous code is replaced by
// infinite chaining.
pollKeyStates()
}
}
// Start up key state polling
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
pollKeyStates()
}
With code in place to detect when keys are pressed, you now need a way to communicate that to your SwiftUI Views. Again, there's more than one way to skin that cat. Here's an overly simplistic one that will update a View whenever the up-arrow is pressed, but you'll probably want to implement something a bit more sophisticated... probably something that allows views to specify what keys they're interested in responding to.
class UpArrowDetector: ObservableObject
{
#Published var isPressed: Bool = false
}
let upArrowDetector = UpArrowDetector()
func dispatchKeyDown(_ key: CGKeyCode)
{
if key == .kVK_UpArrow {
upArrowDetector.isPressed = true
}
}
func dispatchKeyUp(_ key: CGKeyCode) {
if key == .kVK_UpArrow {
upArrowDetector.isPressed = false
}
}
// Now we hook it into SwiftUI
struct UpArrowDetectorView: View
{
#StateObject var detector: UpArrowDetector
var body: some View
{
Text(
detector.isPressed
? "Up-Arrow is pressed"
: "Up-Arrow is NOT pressed"
)
}
}
// Use the .environmentObject() method of `View` to inject the
// `upArrowDetector`
struct ContentView: View
{
var body: some View
{
UpArrowDetectorView()
.environmentObject(upArrowDetector)
}
}
I've put a full, compilable, and working example at this gist patterned on code you linked to in comments. It's slightly refactored from the above code, but all the parts are there, including starting up the polling code.
I hope this points you in a useful direction.

SwiftUI receive custom Event

I have an ObservableObject that publishes some values using #Published property wrappers. This object also holds a timer.
The question is, how can I fire an event as soon as the timer is executed and handle that event in a view in SwiftUI (I'd prefer using something like onReceive)?
Using the Combine framework for publishing changing values already, I'd like to implement this event triggering / handling properly. But all that I've read so far about Combine is always about handling value changes. But in my case it's rather a single simple event (without any values).
I know that I could simply use a closure and call that when the timer expires, and I will do that if there's no better, combine-like solution.
This is a conceptual question for a very simple problem so I think it's self explaining without me coming up with a code example?
The way SwiftUI works with Combine is via .onReceive, which expects a publisher. An object can expose a publisher - whether Timer or something else - as a property.
Combine publishers work by emitting values, and if you just need to signal that an event has happened, you can emit () aka Void values.
This object, by the way, need not be an ObservableObject, but it could be.
class Foo: ObservableObject {
let timer = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.map { _ in } // map to Void
.eraseToAnyPublisher() // optional, but a good practice
}
Now, you can use .onReceive to subscribe to the timer event:
struct ContentView: View {
#StateObject var foo = Foo()
#State var int: Int = 0
var body: some View {
Text("\(int)")
.onReceive(timer) {
self.int += 1
}
}
}
Of course, you're not restricted to a TimerPublisher. For example, if some random event happens, you can use a PassthroughSubject to publish a value:
class Foo {
let eventA: AnyPublisher<Void, Never>
private let subject = PassthroughSubject<Void, Never>()
init() {
eventA = subject.eraseToAnyPublisher()
let delay = Double.random(in: 10...100)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
// something random happened, notify
self?.subject.send()
}
}
}
Just an idea, but you could dedicate a specific thread to that Object? And then listen and fire things on that Queue specifically. I've never done this before but it seems like the right sort of idea.
https://developer.apple.com/documentation/dispatch/dispatchqueue
https://www.freecodecamp.org/news/ios-concurrency/