SwiftUI receive custom Event - swift

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/

Related

SwiftUI #Published and main thread

Could someone explain why I get this warning: Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I'm know that if I wrap the changes in DispatchQueue.main.async the problem goes away. Why does it happen with some view modals and not others? I thought that since the variable has #Published it's automatically a publisher on main thread?
class VM: ObservableObject {
private let contactsRepo = ContactsCollection()
#Published var mutuals: [String]?
func fetch() {
contactsRepo.findMutuals(uid: uid, otherUid: other_uid, limit: 4) { [weak self] mutuals in
guard let self = self else { return }
if mutuals != nil {
self.mutualsWithHost = mutuals // warning...
} else {
self.mutualsWithHost = []
}
}
}
}
Evidently, contactsRepo.findMutuals can call its completion handler on a background thread. You need to ward that off by getting back onto the main thread.
The #Published property wrapper creates a publisher of the declared type, nothing more. The documentation may be able to provide further clarity.
As for it happening on some viewModels and not others, we wouldn't be able to tell here as we don't have the code. However it's always best practice to use DispatchQueue.main.async block or .receive(on: DispatchQueue.main) modifier for combine as you've already figured out when updating your UI.
The chances are your other viewModel is already using the main thread or the properties on the viewModel aren't being used to update the UI, again without the code we'll never be sure.

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.

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

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.

RxSwift `ActivityIndicator` Functionality in Combine

I've been working with RxSwift for a few years now, and am starting to explore Combine with SwiftUI and am having some trouble trying to replicate some functionality from RxSwift in Combine.
On the RxSwift GitHub there is an example in a file called ActivityIndicator.swift.
Basic usage is as follows:
class Foo {
let activityIndicator = ActivityIndicator()
lazy var activity = activityIndicator.asDriver()
var disposeBag = DisposeBag()
func doSomething() {
Observable
.just("this is something")
.trackActivity(activityIndicator)
.subscribe()
.disposed(by: disposeBag)
}
}
What this does is allow you to then drive off of the activity driver and it will emit boolean values every time something subscribes or a subscription completes.
You can then directly drive something like a UIActivityIndicatorView's isAnimating property using RxCocoa.
I've been trying to figure out how to create something similar to this in Combine but am not having any luck.
Say I have a viewModel that looks like this:
class ViewModel: ObservableObject {
#Published var isActive = false
func doSomething() -> AnyPublisher<Void, Never> {
Just(())
.delay(for: 2.0, scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
}
What I would like to do is create an operator for a Publisher that will function similarly to how the Rx operator worked where I can forward the events from the subscription through the chain, but change the isActive value every time something subscribes/completes/cancels.
In the SwiftUI View I would initiate the doSomething function and sink to it, while also being able to use the published isActive property to show/hide a ProgressView
Something similar to this:
struct SomeView: View {
let viewModel = ViewModel()
var body: some View {
var cancelBag = Set<AnyCancellable>()
VStack {
Text("This is text")
if viewModel.isActive {
ProgressView()
}
}
.onAppear(perform: {
viewModel
.doSomething()
.sink()
.store(in: &cancelBag)
})
}
}
Is there something that works like this already that I am just completely missing?
If not, how can I go about replicating the RxSwift functionality in Combine?
Thank you in advance for the help.
Looks like someone created a Combine version. I don't know if it has the same issue as discussed by #Daniel T. but it looks promising.
https://github.com/duyquang91/ActivityIndicator
Hmm... The key to the ActivityIndicator class is the Observable.using(_:observableFactory:) operator. Unfortunately, I don't believe there is an equivalent operator in Combine.
The using operator creates a resource when the Observable is subscribed to, and then disposes the resource when the Observable sends a stop event (complete or error.) This insures the resource's lifetime. In this particular case, the resource just increments an Int value on creation and decrements it on disposal.
I think you could kind of mimic the behavior with something like this:
extension Publisher {
func trackActivity(_ activityIndicator: CombineActivityIndicator) -> some Publisher {
return activityIndicator.trackActivity(of: self)
}
}
final class CombineActivityIndicator {
var counter = CurrentValueSubject<Int, Never>(0)
var cancelables = Set<AnyCancellable>()
func trackActivity<Source: Publisher>(of source: Source) -> some Publisher {
let sharedSource = source.share()
counter.value += 1
sharedSource
.sink(
receiveCompletion: { [unowned self] _ in
self.counter.value -= 1
},
receiveValue: { _ in }
)
.store(in: &cancelables)
return sharedSource
}
var asPublisher: AnyPublisher<Bool, Never> {
counter
.map { $0 > 0 }
.eraseToAnyPublisher()
}
}
However, the above class will heat up the Publisher and you might miss emitted values because of it. Use at your own risk, I do not recommend the above unless you are desperate.
Maybe someone has written a using operator for Publisher and will be willing to share.

Is there an alternative to Combine's #Published that signals a value change after it has taken place instead of before?

I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.