Is calling `disposed(by:)` necessary in any case? - swift

As the title, is it necessary to call disposed(by:) in any case? If yes, why?
Consider a simple example like this:
class ViewController: UIViewController {
let button = UIButton()
override func viewDidLoad() {
button.rx.tap.bind(onNext: { _ in
print("Button tapped!")
})
// Does this make any retain cycle here?
}
}

No, it is not necessary to call .disposed(by:) in every case.
If you have an Observable that you know will eventually send a stop event, and you know that you want to keep listening to that observable until it does so, then there is no reason/need to dispose the subscription and therefore no need to insert the disposable into a dispose bag.
The reason .subscribe and its ilk return a Disposable is so that the calling code can end the subscription before the observable has completed. The calling code ends the subscription by calling dispose() on the disposable returned. Otherwise, the subscription will continue until the source observable sends a stop event (either completed or error.)
If the calling code doesn't dispose the subscription, and the source observable doesn't send a stop event, then the subscription will continue to operate even if all other code involved has lost all references to the objects involved in the subscription.
So for example, if you put this in a viewDidLoad:
_ = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
.subscribe(onNext: { print($0) })
The code above will continue to print values long after the view controller that created it ceases to exist.
In the example you presented, the UIButton object will emit a completed event when it is deinitialized, so if you want to listen to the button right up until that happens, putting the disposable in a dispose bag isn't necessary.
Ignoring disposables means you need to be very cognizant as to which Observables complete and which ones don't, but if you have that understanding, you can ignore them. Just remember that the next developer down the line, or future you, won't have as good an understanding of the code as you do.

Related

Alternative ways to receive result from async/await on Main Thread for Swift

Recently, I ran into a problem where I got a warning for updating UI on background task.
func didInit() async {
listOfTodo = await interactor.getTodos()
}
I tried to wrap the function body inside DispatchQue.main.async {}, but I got an error.
I then found a solution which I have to put #MainActor on top of my function, but I feel like there are other solutions that would make more sense, or this is the only way to work with async/await on Main Thread?
#MainActor
func didInit() async {
listOfTodo = await interactor.getTodos()
}
You're approaching this backwards. If interactor.getTodos() must be run on the main actor, then it should be marked #MainActor, not the caller. But if didInit is logically "a UI-updating method," then it's fine to mark it #MainActor as well.
Or you can use MainActor.run {...} to manually move this one call to the main actor. It all comes down to what you mean to express.
A better approach would be to create a function which takes responsibility of updating UI, something like below
#MainActor
func updateUI() async {
// Code to update your UI
}
Call this function inside your task function like below
func didInit() {
Task.detached { // or specify a priority with Task.detached(priority: .background)
listOfTodo = interactor.getTodos()
await self.updateUI()
}
}
Note: Code is not tested on Xcode so may be require some changes

swift async function is being run on the main thread in some cases when run from task

In this case the async function reads a file and returns the parsed contents.
In my view I want to load the contents off of the main thread, and then update the view once complete.
I've used this pattern in various places and noticed that in some cases the async call is on the main thread (by debugging) while in others it is on the Thread 4 Queue : com.apple.root.user-initiated-qos.cooperative (concurrent) thread
For example:
struct MyView: View {
#State var data = "some data"
var body: some View {
Button("refresh") {
// when the button is pressed refresh it
Task {
await refresh()
}
}.task {
// when the view appears
await refresh()
}
Text("the data is \(data)") // write the data which was refreshed async
}
}
func refresh() async {
do {
let res = try await anotherAyncFunction()
data = res // this is presumably wrong and off the main thread - obviously might not be correct but leave here for debug as well
} catch {
print("got error \(error)")
}
}
I created several different views using a similar pattern (.task block calling async functions)
In some cases the functions are long running (reading from disk) and that is happening on the main thread
Change Task { to Task.detached {.
From the Swift Language Guide:
To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:) initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method.
When you call Task.init, the asynchronous code runs on the current actor, which, in this context is the main actor. This has the result of blocking the main thread.
By calling Task.detached, you allow the asynchronous work to happen off the main thread.
Using Swift structured concurrency, to run code on a background thread, use a detached task or (better) an actor.

XCUITest verify that ui interruption handler occurred

I'm very new to swift and xcuitest. I recently came across addUIInterruptionMonitor for handling alerts that can pop up. What I would like to know is how I can verify that the alert happened and that the handler was dealt with. Take the following for example
addUIInterruptionMonitorWithDescription("Location Services") {
(alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.buttons["Request Location"].tap()
app.tap() // need to interact with the app again for the handler to fire
// after this if the handler never gets called I want the test to fail
I would like to test that the alert actually happens, but as far as I understand, after my last tap() if the alert never gets triggered my handler wont be called. I need to test that the alert actually happened and then maybe add some assertions to the contents of the handler
I seem to have answered my own question on further investigation. For asynchronous testing with xcuitest I can use a XCTestExpectation which will when created, cause the test to wait until the expectation is fulfilled, or fail after a certain timeout. With that my above code becomes:
let expectation = XCTestExpectation(description: "Location Service Alert")
let locationMonitorToken = addUIInterruptionMonitorWithDescription("Location Services") {
(alert) -> Bool in
alert.buttons["Allow"].tap()
expectation.fulfill() // test waits for this before passing
return true
}
app.buttons["Request Location"].tap()
app.tap() // alert triggered here
wait(for: [expectation], timeout: 3.0)
removeUIInterruptionMonitor(locationMonitorToken)
Update: forgot to put in the wait(for: [expectation], timeout: 3.0) after alert triggered to ensure handler is called.

MacOS Quartz Event Tap listening to wrong events

I am trying to intercept mouse move events using the CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) method as shown below:
let cfMachPort = CGEvent.tapCreate(tap: CGEventTapLocation.cghidEventTap,
place: CGEventTapPlacement.headInsertEventTap,
options: CGEventTapOptions.defaultTap,
eventsOfInterest:CGEventMask(CGEventType.mouseMoved.rawValue),
callback: {(eventTapProxy, eventType, event, mutablePointer) -> Unmanaged<CGEvent>? in event
print(event.type.rawValue) //Breakpoint
return nil
}, userInfo: nil)
let runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, cfMachPort!, 0)
let runLoop = RunLoop.current
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddSource(cfRunLoop, runloopSource, CFRunLoopMode.defaultMode)
I pass as event type eventsOfInterest mouseMoved events with a raw value of 5 as seen in the documentation. But for some reason my print() is not executed unless I click with the mouse. Inspecting the send mouse event in the debugger gives me a raw value of 2, which according to the documentation is a leftMouseUp event.
In the documentation for CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) it says:
Event taps receive key up and key down events [...]
So it seems like the method ignores mouseMoved events in general?! But how am I supposed to listen to mouseMoved events? I am trying to prevent my cursor (custom cursor) from being replaced (for example when I hover over the application dock at the bottom of the screen).
You need to bitshift the CGEventType value used to create the CGEventMask parameter. In Objective-C, there is a macro to do this: CGEventMaskBit.
From the CGEventMask documentation:
to form the bit mask, use the CGEventMaskBit macro to convert each constant into an event mask and then OR the individual masks together
I don't know the equivalent mechanism in swift; but the macro itself looks like this:
*/ #define CGEventMaskBit(eventType) ((CGEventMask)1 << (eventType))
In your example, it's sufficient to just manually shift the argument; e.g.
eventsOfInterest:CGEventMask(1 << CGEventType.mouseMoved.rawValue),
I would point out that the code example given in the question is a little dangerous; as it creates a default event tap and then drops the events rather than allowing them to be processed. This messes up mouse click handling and it was tricky to actually terminate the application using the mouse. Anyone running the example could set the event tap type to CGEventTapOptions.listenOnly to prevent that.
Here is a way to listen for mouseMove global events (tested with Xcode 11.2+, macOS 10.15)
// ... say in AppDelegate
var globalObserver: Any!
var localObserver: Any!
func applicationDidFinishLaunching(_ aNotification: Notification) {
globalObserver = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("in background: {\(location.x), \(location.y)}")
}
localObserver = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("active: {\(location.x), \(location.y)}")
return event
}
...
There's another thing incorrect in your code, although you might be lucky and it isn't normally causing a problem.
As documented for the mode parameter to CFRunLoopAddSource: "Use the constant kCFRunLoopCommonModes to add source to the set of objects monitored by all the common modes."
That third parameter should instead be CFRunLoopMode.commonModes.
What you have, CFRunLoopMode.defaultMode aka kCFRunLoopDefaultMode, is instead for use when calling CFRunLoopRun.

RxSwift PublishSubject is being disposed

I bind Button pressed to PublishSubject in a router like so:
hambugerButton
.rx_tap
.bindTo(router.openMenu)
.addDisposableTo(disposeBag)
In my Router:
let openMenu = PublishSubject<Void>()
//...
openMenu
.map { _ in
menuNavigationController
}
.bindTo(mainNavigationController.rx_present())
.addDisposableTo(disposeBag)
However, when the controller is being deallocated, the button is sending 'complete' signal. When PublishSubject receives it, it won't react to signals from another controller (which is understandable: it is an Observable guarantee).
The only solution I came up with:
hambugerButton
.rx_tap
.subscribeNext {
self.router.openMenu.onNext()
}
.addDisposableTo(disposeBag)
Which looks ugly and kinda spoils the idea of a reactive interface.
My question is, is there a way to avoid propagation of the Completed event to PublishSubject? Can I make some Observer which will ignore such events?
If the view controller which owns the hamburgerButton is being deallocated, and thus the hamburgerButton is also being deallocated, why wouldn't you want the binding to router.openMenu to also be deallocated? Maybe it's not clear what your view controller hierarchy is from your question.
Also, in the first snippet, you shouldn't be making a binding without adding it to a DisposeBag like so:
hambugerButton
.rx_tap
.bindTo(router.openMenu)
.addDisposableTo(disposeBag)