change UIVIew visibility during a long func call - swift

I have a search page with uses OAuth to make an external call to a website for data. Sometimes the data is very quick and others quite long. So I have created a custom object (Searching) to display on screen to indicate that a search is happening (the custom object is just 2 UIImageViews in a UIView)
The problem is that the searching.isHidden = false doesn't actually happen until the end of the func which happens after it gets all the data, even though it is called first. Which is obviously too late.
I tried moving the isHidden to a background thread but get an error saying UIView calls must be on the main thread
I tried moving the display call to its own func with an #escaping callback and then run the search after it returns but it still does not update.
If I remove the search() line it displays properly.
I've also tried forcing a refresh on the object using
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
and it didn't work
class Search {
override func viewDidLoad() {
super.viewDidLoad()
searching.isHidden = true
}
#IBAction func search(_ sender: Any) {
if self.searching.isHidden == false {
self.searching.isHidden = true
}
else {
self.searching.isHidden = false
}
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
//I've also tried using an escaping func to call the isHidden and call back when complete
//self.searching.show() {
//self.view.setNeedsLayout()
//self.view.setNeedsDisplay()
//self.search()
//}
//I've tried an async call
// DispatchQueue.main.async {
// self.search()
// }
}
func search() {
keywordText.resignFirstResponder()
//perform OAuth external search
if results.count > 1 {
searching.isHidden = true
self.performSegue(withIdentifier: "results", sender: nil)
}
return
}
}

On iOS (and MacOS) UI updates don’t get displayed until your code returns and the event loop gets a chance to run. Code that makes UI changes and then immediately does a long-running task on the main thread will not see those changes on-screen until the long-running task completes.
One way to handle this is to change you UI and then use dispatchAsync to trigger the long-running task:
searching.isHidden = false
DispatchQueue.main.async {
// Put your long-running code here.)
}

Related

Creating an observer to check if MediaPlayer playbackState is paused or not

I have a music app and I wish to determine if playback has been paused while the app was closed (due to an event like a phone call or AirPods being taken out of ear etc)
My first approach was to run a func inside of viewWillAppear that checked
if mediaPlayer.playbackState == .paused {
...
}
If it was paused I updated the play/pause button image. However, this did not work, the play/pause button would still show Play even if it was paused.
Next, I tried adding an observer to the viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(self.wasSongInterupted(_:)), name: UIApplication.didBecomeActiveNotification, object: self.mediaPlayer)
The self.wasSongInterupted I call is
#objc func wasSongInterupted(_ notification: Notification) {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .paused {
print("paused")
self.isPlaying = false
self.playPauseSongButton.isSelected = self.isPlaying
} else if self.mediaPlayer.playbackState == .playing {
self.isPlaying = true
self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
However, I am still having the same issue.
What is the best way to determine if my music player is playing or paused when I reopen the app?
Edit 1: I Edited my code based on comments.
wasSongInterrupted was not being called, and through breakpoints and errors I discovered the code was mostly not needed. I changed my code to be
func wasSongInterrupted() {
DispatchQueue.main.async {
if self.mediaPlayer.playbackState == .interrupted {
var isPlaying: Bool { return self.mediaPlayer.playbackState == .playing }
print("Playback state is \(self.mediaPlayer.playbackState.rawValue), self.isPlaying Bool is \(self.isPlaying)")
self.playPauseSongButton.setImage(UIImage(named: "playIconLight"), for: .normal)
//self.playPauseSongButton.isSelected = self.isPlaying
}
}
}
and inside my AppDelegate's applicationDidBecomeActive I have
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
Now the code runs, however I have an issue.
If I run the following code:
if self.mediaPlayer.playbackState == .interrupted {
print("interrupted \(self.isPlaying)")
}
and then make a call and come back to the app it will hit the breakpoint. It will print out interrupted as well as false which is the Bool value for self.isPlaying
However if I try to update the UI by
self.playPauseSongButton.isSelected = self.isPlaying
or by
self.playPauseSongButton.setImage(UIImage(named: "playIconLight.png"), for: .normal)
I get an error message Thread 1: EXC_BREAKPOINT (code=1, subcode=0x104af9258)
You trying to update you player UI from viewWillAppear. From Apple Documentation:
viewWillAppear(_:)
This method is called before the view controller's view is about to be added to a view hierarchy and before any animations are configured for showing the view.
So if your app was suspended and the becomes active again, this method won't be called, because your UIViewController is already at Navigations Stack.
If you want to catch the moment when your app becomes active from suspended state, you need to use AppDelegate. From Apple Documentation:
applicationDidBecomeActive(_:)
This method is called to let your app know that it moved from the inactive to active state. This can occur because your app was launched by the user or the system.
So you need to use this method at your AppDelegate to handle app running and update your interface.
UPDATE
You saying the inside this AppDelegate method you're doing
let mediaPlayerVC = MediaPlayerViewController()
mediaPlayerVC.wasSongInterupted()
That's wrong because you're creating a new view controller. What you need to do, is to access you existing view controller from navigation stack and update it.
One of the possible solutions is to use NotificationCenter to send a notification. You view controller should be subscribed to this event of course.
At first, you need to create a notification name
extension Notification.Name {
static let appBecameActive = Notification.Name(rawValue: "appBecameActive")
}
Then in you AppDelegate add following code to post your notifications when app becomes active
func applicationDidBecomeActive(_ application: UIApplication) {
NotificationCenter.default.post(name: .appBecameActive, object: nil)
}
And finally in your view controller add to subscribe it on notifications
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(wakeUp),
name: .appBecameActive,
object: nil)
...
}
#objc func wakeUp() {
// Update your UI from here
}
Hope it helps you.

Proper use of Dispatch to show activity indicator during a long task

During a computationally intensive task, I wish to show the user an activity indicator. What is the best way to do this?
My task (contrived of course), lasts a couple of seconds:
func startThinking(howMany: Int) {
for i in 0...howMany {
let p:Double = Double(i)
let _ = p / Double.pi
}
delegate?.finishedThinking()
}
This is called on a button tap:
#IBAction func startTap(_ sender: Any) {
Thinker.sharedInstance.startThinking(howMany: 500000000)
myActivity.startAnimating()
}
And stopped when the thinking task is finished:
func finishedThinking() {
print ("finished thinking")
myActivity.stopAnimating()
}
But the activity indicator is not showing up; the UI is blocked by the difficult thinking task.
I've tried putting the startAnimating on the main thread:
DispatchQueue.main.async {
self.myActivity.startAnimating()
}
or the difficult task onto its own thread:
DispatchQueue.global().async {
Thinker.sharedInstance.startThinking(howMany: 500000000)
}
and various other combinations that I've run across in Stack. What am I doing wrong?
Firstly, I would move the call to start animating to before the thinker call, and verify that it works if you don't start thinking. You also need to stop the animation from the main thread.
#IBAction func startTap(_ sender: Any) {
myActivity.startAnimating()
DispatchQueue.global(qos: .userInitiated).async {
Thinker.sharedInstance.startThinking(howMany: 500000000)
}
}
func finishedThinking() {
DispatchQueue.main.async {
myActivity.stopAnimating()
}
}
I adjusted a few things:
moved the .startAnimating() call to be first. It is already on the main thread since it was called from the interface
specify the qos as .userInitiated
run the .stopAnimating() on the main thread

Swift 3 hide NSButton immediately after click

How can I have a button disappear after it's been clicked?
#IBAction func onClick(_ sender: NSButton) {
sender.isHidden = true;
//...a lot of blocking instructions below this line
}
The above works to a certain extent, as the "sender" / button is hidden only after all of the instructions in the function have been processed. I have some blocking IO in the function (socket connections etc.) and I want the button to disappear before all that happens.
I tried using both outlets and sender.
#IBAction func onClick(_ sender: NSButton) {
sender.isHidden = true;
DispatchQueue.main.async {
//...a lot of blocking instructions below this line
}
}
I managed to achieve the desired effect by putting the "blocking" piece of code in the following statement (and pushing the .isHidden setting through immediately, in a synchronous fashion):
DispatchQueue.main.async { /*code*/ }

Displaying Activity indicator with OCR and using threads

I am trying to display an activity indicator whilst some text recognition happens. If i just start and stop[ the indicator around the recognition code it never shows. The issue i have is that if use:
activityIndicator.startAnimating()
DispatchQueue.main.async( execute: {
self.performTextRecognition()
})
activityIndicator.stopAnimating()
self.performSegue(withIdentifier: "segueToMyBills", sender: self)
The indicator never shows as it performs the segue and the table view in the next view controller shows no information because the text recognition hasn't completed. I've never touched on threads until now so a little insight on what to do would be much appreciated. Thanks!
Well your problem is that your OCR happens on the main thread. That blocks the thread thus never having time to draw the activity indicator. Try to modify your code into this:
activityIndicator.startAnimating()
DispatchQueue.global(qos: .background).async { [weak weaKSelf = self] in
// Use a weak reference to self here to make sure no retain cycle is created
weakSelf?.performTextRecognition()
DispatchQueue.main.async {
// Make sure weakSelf is still around
guard let weakSelf = weakSelf else { return }
weakSelf.activityIndicator.stopAnimating()
weakSelf.performSegue(withIdentifier: "segueToMyBills", sender: weakSelf)
}
}
Try to add a completion handler to your self.performTextRecognition() function in this way
function performTextRecognition(completion: ((Error?) -> Void)? = .none) {
//you can replace Error with Any type or leave it nil
//do your logic here
completion?(error)
}
and then call the function like this :
performTextRecognition(completion: { error in
activityIndicator.stopAnimating()
self.performSegue(withIdentifier: "segueToMyBills", sender: self)
})

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?
I want the view to run this asynchronous thread. However, as soon as the view disappears, I want that thread to stop running. What's the best way to do this? I'm not sure where to start and might be thinking about this the wrong way. Nevertheless, what I described is how I want it to behave to the user.
You can use NSOperation to achieve what you want, NSOperation and NSOperationQueue are built on top of GCD. As a very general rule, Apple recommends using the highest-level abstraction, and then dropping down to lower levels when measurements show they are needed.
For example, You want to download images asynchronously when the view is loaded and cancel the task when the view is disappeared. First create a ImageDownloader object subclass to NSOperation. Notice that we check if the operation is cancelled twice, this is because the NSOperation has 3 states: isReady -> isExecuting -> isFinish and when the operation starts executing, it won't be cancelled automatically, we need to do it ourself.
class ImageDownloader: NSOperation {
//1
var photoRecord: NSURL = NSURL(string: "fortest")!
//2
init(photoRecord: NSURL) {
self.photoRecord = photoRecord
}
//3
override func main() {
//4
if self.cancelled {
return
}
//5
let imageData = NSData(contentsOfURL:self.photoRecord)
//6
if self.cancelled {
return
}
}
}
Then you can use it like: downloader.cancel(), downloader.start(). Notice that we need to check if the operation is cancelled in the completion block.
import UIKit
class ViewController: UIViewController {
let downloder = ImageDownloader(photoRecord: NSURL(string: "test")!)
override func viewDidLoad() {
super.viewDidLoad()
downloder.completionBlock = {
if self.downloder.cancelled {
return
}
print("image downloaded")
}
//Start the task when the view is loaded
downloder.start()
}
override func viewWillDisappear(animated: Bool) {
//Cancel the task when the view will disappear
downloder.cancel()
}
}
Once DetailViewController is presented, the asyncOperation method will be executed asynchronously.
Note: currently the asyncOperation method is executed every second so if you want the method to be called only once, you must change the repeats property to false.
class DetailViewController: UIViewController {
// timer that will execute
// asynchronously an operation
var timer: NSTimer!
// counter used in the async operation.
var counter = 0
// when view is about to appear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// setting up the timer
timer = NSTimer.scheduledTimerWithTimeInterval(
1.0,
target: self,
selector: #selector(asyncOperation),
userInfo: nil,
repeats: true //set up false if you don't want the operation repeats its execution.
)
}
// when view is about to disappear
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// stopping the timer
timer.invalidate()
}
// async operation that will
// be executed
func asyncOperation() {
counter += 1
print("counter: \(counter)")
}
}
Source: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/
Result: