Clean up SimplePing code with RunLoop.current.run - ios

I found the SimplePing library from Apple and want to use it in a SwiftUI Project.
To use the library I code online which works fine. The start function is as follows:
public func start(hostName: String) {
let pinger = SimplePing(hostName: "192.168.178.20")
pinger.delegate = self
pinger.start()
var count = 5
repeat {
if (self.canStartPinging) {
pinger.send(with: nil)
count-=1
if count == 0{
self.canStartPinging = false
break
}
}
RunLoop.current.run(mode: RunLoop.Mode.default, before: NSDate.distantFuture)
} while(true)
I don't really understand why I need the RunLoop.current.run(mode: RunLoop.Mode.default, before: NSDate.distantFuture) line. When I remove it the delegates of SimplePing doesn't get called.
How can I simplify this code and use it without blocking the Main thread?

The run(mode:before:) is there to allow the run loop to process events while this repeat-while loop spins. It’s a way to make a blocking loop allow things to occur on the run loop.
You haven’t shared the code that is setting canStartPinging, but I’m guessing that, at the very least, SimplePingDelegate method didStartWithAddress sets it. So, if you’re spinning on the main thread without calling that run(mode:before:), the SimplePing delegate method probably never gets a chance to be called. By adding that run call, at least the delegate method can run.
Your suspicion about this this whole pattern of spinning and calling run(mode:before:) is warranted. It’s horribly inefficient pattern. It should be eliminated.
If this were a standard Swift project, I’d suggest just using the delegate-protocol pattern and you’d be done. Since this is Swift UI, I’d suggest refactoring this to be a Combine Publisher, which you can then integrate into your SwiftUI project.

Related

UI not updating (in Swift) during intensive function on main thread

I wondered if anyone could provide advice on how I can ‘force’ the UI to update during a particularly intensive function (on the main thread) in Swift.
To explain: I am trying to add an ‘import’ feature to my app, which would allow a user to import items from a backup file (could be anything from 1 - 1,000,000 records, say, depending on the size of their backup) which get saved to the app’s CodeData database. This function uses a ‘for in’ loop (to cycle through each record in the backup file), and with each ‘for’ in that loop, the function sends a message to a delegate (a ViewController) to update its UIProgressBar with the progress so the user can see the live progress on the screen. I would normally try to send this intensive function to a background thread, and separately update the UI on the main thread… but this isn't an option because creating those items in the CoreData context has to be done on the main thread (according to Swift’s errors/crashes when I initially tried to do it on a background thread), and I think this therefore is causing the UI to ‘freeze’ and not update live on screen.
A simplified version of the code would be:
class CoreDataManager {
var delegate: ProgressProtocol?
// (dummy) backup file array for purpose of this example, which could contain 100,000's of items
let backUp = [BackUpItem]()
// intensive function containing 'for in' loop
func processBackUpAndSaveData() {
let totalItems: Float = Float(backUp.count)
var step: Float = 0
for backUpItem in backUp {
// calculate Progress and tell delegate to update the UIProgressView
step += 1
let calculatedProgress = step / totalItems
delegate?.updateProgressBar(progress: calculatedProgress)
// Create the item in CoreData context (which must be done on main thread)
let savedItem = (context: context)
}
// loop is complete, so save the CoreData context
try! context.save()
}
}
// Meanwhile... in the delegate (ViewController) which updates the UIProgressView
class ViewController: UIViewController, ProgressProtocol {
let progressBar = UIProgressView()
// Delegate function which updates the progress bar
func updateProgressBar(progress: Float) {
// Print statement, which shows up correctly in the console during the intensive task
print("Progress being updated to \(progress)")
// Update to the progressBar is instructed, but isn't reflected on the simulator
progressBar.setProgress(progress, animated: false)
}
}
One important thing to note: the print statement in the above code runs fine / as expected, i.e. throughout the long ‘for in’ loop (which could take a minute or two), the console continuously shows all the print statements (showing the increasing progress values), so I know that the delegate ‘updateProgressBar’ function is definitely firing correctly, but the Progress Bar on the screen itself simply isn’t updating / doesn’t change… and I’m assuming it’s because the UI is frozen and hasn’t got ‘time’ (for want of a better word) to reflect the updated progress given the intensity of the main function running.
I am relatively new to coding, so apologies in advance if I ask for clarification on any responses as much of this is new to me. In case it is relevant, I am using Storyboards (as opposed to SwiftUI).
Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task.
You say "...Just really looking for any advice / tips on whether there are any (relatively easy) routes to resolve this and essentially 'force' the UI to update during this intensive task."
No. If you do time-consuming work synchronously on the main thread, you block the main thread, and UI updates will not take effect until your code returns.
You need to figure out how to run your code on a background thread. I haven't worked with CoreData in quite a while. I know it's possible to do CoreData queries on a background thread, but I no longer remember the details. That's what you're going to need to do.
As to your comment about print statements, that makes sense. The Xcode console is separate from your app's run loop, and is able to display output even if your code doesn't return. The app UI can't do that however.

Can I use actors in Swift to always call a function on the main thread?

I recently saw that Swift had introduced concurrency support with the Actor model in Swift 5.5. This model enables safe concurrent code to avoid data races when we have a shared, mutable state.
I want to avoid main thread data races in my app's UI. For this, I am wrapping DispatchQueue.main.async at the call site wherever I set a UIImageView.image property or a UIButton style.
// Original function
func setImage(thumbnailName: String) {
myImageView.image = UIImage(named: thumbnailName)
}
// Call site
DispatchQueue.main.async {
myVC.setImage(thumbnailName: "thumbnail")
}
This seems unsafe because I have to remember to dispatch the method manually on the main queue. The other solution looks like:
func setImage(thumbnailName: String) {
DispatchQueue.main.async {
myImageView.image = UIImage(named: thumbnailName)
}
}
But this looks like a lot of boilerplate, and I wouldn't say I like using this for complex functions with more than one level of nesting.
The release of Swift support for Actors looks like a perfect solution for this. So, is there a way to make my code safer, i.e. always call UI functions on the main thread using Actors?
Actors in Swift 5.5 🤹‍♀️
Actor isolation and re-entrancy are now implemented in the Swift stdlib. So, Apple recommends using the model for concurrent logic with many new concurrency features to avoid data races. Instead of lock-based synchronisation (lots of boilerplate), we now have a much cleaner alternative.
Some UIKit classes, including UIViewController and UILabel, now have out of the box support for #MainActor. So we only need to use the annotation in custom UI-related classes. For example, in the code above, myImageView.image would automatically be dispatched on the main queue. However, the UIImage.init(named:) call is not automatically dispatched on the main thread outside of a view controller.
In the general case, #MainActor is useful for concurrent access to UI-related state, and is the easiest to do even though we can manually dispatch too. I've outlined potential solutions below:
Solution 1
The simplest possible. This attribute could be useful in UI-Related classes. Apple have made the process much cleaner using the #MainActor method annotation:
#MainActor func setImage(thumbnailName: String) {
myImageView.image = UIImage(image: thumbnailName)
}
This code is equivalent to wrapping in DispatchQueue.main.async, but the call site is now:
await setImage(thumbnailName: "thumbnail")
Solution 2
If you have Custom UI-related classes, we can consider applying #MainActor to the type itself. This ensures that all methods and properties are dispatched on the main DispatchQueue.
We can then manually opt out from the main thread using the nonisolated keyword for non-UI logic.
#MainActor class ListViewModel: ObservableObject {
func onButtonTap(...) { ... }
nonisolated func fetchLatestAndDisplay() async { ... }
}
We don't need to specify await explicitly when we call onButtonTap within an actor.
Solution 3 (Works for blocks, as well as functions)
We can also call functions on the main thread outside an actor with:
func onButtonTap(...) async {
await MainActor.run {
....
}
}
Inside a different actor:
func onButtonTap(...) {
await MainActor.run {
....
}
}
If we want to return from within a MainActor.run, simply specify that in the signature:
func onButtonTap(...) async -> Int {
let result = await MainActor.run { () -> Int in
return 3012
}
return result
}
This solution is slightly less cleaner than the above two solutions which are most suited for wrapping an entire function on the MainActor. However, actor.run also allows for inter threaded code between actors in one func (thx #Bill for the suggestion).
Solution 4 (Block solution that works within non-async functions)
An alternative way to schedule a block on the #MainActor to Solution 3:
func onButtonTap(...) {
Task { #MainActor in
....
}
}
The advantage here over Solution 3 is that the enclosing func doesn't need to be marked as async. Do note however that this dispatches the block later rather than immediately as in Solution 3.
Summary
Actors make Swift code safer, cleaner and easier to write. Don't overuse them, but dispatching UI code to the main thread is a great use case. Note that since the feature is still in beta, the framework may change/improve further in the future.
Since we can easily use the actor keyword interchangeably with class or struct, I want to advise limiting the keyword only to instances where concurrency is strictly needed. Using the keyword adds extra overhead to instance creation and so doesn't make sense when there is no shared state to manage.
If you don't need a shared state, then don't create it unnecessarily. struct instance creation is so lightweight that it's better to create a new instance most of the time. e.g. SwiftUI.

How do I wait for an asynchronous call in Swift?

So I've recently come back to Swift & iOS after a hiatus and I've run into an issue with asynchronous execution. I'm using Giphy's iOS SDK to save myself a lot of work, but their documentation is pretty much nonexistent so I'm not sure what might be happening under the hood in their function that calls their API.
I'm calling my function containing the below code from the constructor of a static object (I don't think that's the problem as I've also tried calling it from a cellForItemAt method for a Collection View).
My issue is that my function is returning and execution continues before the API call is finished. I've tried utilizing DispatchQueue.main.async and removing Dispatch entirely, and DispatchGroups, to no avail. The one thing that worked was a semaphore, but I think I remember reading that it wasn't best practice?
Any tips would be great, I've been stuck on this for waaaaaay too long. Thanks so much in advance
GiphyCore.shared.gifByID(id) { (response, error) in
if let media = response?.data {
DispatchQueue.main.sync {
print(media)
ret = media
}
}
}
return ret
My issue is that my function is returning and execution continues before the API call is finished.
That's the whole point of asynchronous calls. A network call can take an arbitrary amount of time, so it kicks off the request in the background and tells you when it's finished.
Instead of returning a value from your code, take a callback parameter and call it when you know the Giphy call has finished. Or use a promise library. Or the delegate pattern.
The one thing that worked was a semaphore, but I think I remember reading that it wasn't best practice?
Don't do this. It will block your UI until the network call completes. Since you don't know how long that will take, your UI will be unresponsive for an unknown amount of time. Users will think your app has crashed on slow connections.
You could just add this inside a method and use a completion handler and therefore do you not need to wait for the response. You could do it like this:
func functionName(completion: #escaping (YOURDATATYPE) -> Void) {
GiphyCore.shared.gifByID(id) { (response, error) in
if let media = response?.data {
completion(media)
return
}
}
}
Call your method like this
functionName() { response in
DispatchQueue.main.async {
// UPDATE the UI here
}
}

Stop arbitrary function execution

For the purposes of this question, assume that I need to run some function on some object and that function will take a long time to execute (minutes). Also assume that I have no control over this function (*). How do I now cancel this function's execution?
I want to run it in a background thread to keep the main thread free and I could do that with GCD, NSOperation or NSThread. However, as far as I know, none of these support forced stopping. They can all be cancelled, but this cancellation must be implemented in the function itself - but I don't have access to that function, so I can't do that. The closest I got was using NSThread and exit(), but unfortunately it can't be applied to a instance variable (see the code example). My current plan is to try to send a notification and observe that within the object/function and kill the thread from within using Thread.exit(). I'm justing wondering if there is a "cleaner" or easier way, either built-in or 3rd party.
let someObject = Object()
// Using GCD
dispatchQueue.async { someObject.expensiveFunction() }
// Using NSOperation
operationQueue.addOperation { someObject.expensiveFunction() }
// Using NSThread
let thread = Thread { someObject.expensiveFunction() }
thread.exit() // exit is not available on an instance
(*) In this case I do have control over the function and could implement an actual cancellation, but due to the libraries I'm using, this would require a lot of refactoring.

Swift SceneKit: SCNAudioPlayer completion block (didFinishPlayback) syntax?

I'm not sure why I can't work this out, but In the documentation for the SCNAudioPlayer class, there are 2 blocks used for playback about to start and playback being completed.
https://developer.apple.com/reference/scenekit/scnaudioplayer/1522818-didfinishplayback
I routinely create and use closures but I can't seem to get this one working:
let player = SCNAudioPlayer(source: source)
player.didFinishPlayback({()->void in
//code here
})
Or variants of the same thing makes swift 3 complain.
I was thinking this might be a NSNotification strategy but it seems a complicated procedure for something seemingly simple.
You are trying to call didFinishPlayback like a function. But as your own reference link shows, it is a variable that you must set.
player.didFinishPlayback = { /* ... */ }

Resources