Code only works within a DispatchQueue.main.async block, why? - ios

I wrote this code to select all the text when a user begins editing a UITextField:
#IBAction func onEditingBegin(_ sender: Any) {
print("editing began")
let textfield = sender as! UITextField
textfield.selectAll(nil)
}
But it wouldn't work until I enclosed the textfield.selectAll(nil) line in a DispatchQueue.main.async block:
DispatchQueue.main.async {
textfield.selectAll(nil)
}
Why is that?
I also printed out the name of the thread in onEditingBegin() and this was the result:
<NSThread: 0x60800006c880>{number = 1, name = main}
So it seems that it is already being fired on the main thread, but the code is still not working unless textfield.selectAll() is called inside of the DispatchQueue.main.async block.

The real effect of your call to DispatchQueue.main.async is to add a tiny, tiny delay. This delay, in particular, is exactly long enough to allow the current runloop to finish and the next runloop to start. The action that has caused an event to be sent to you, calling onEditingBegin, is thus permitted to complete. The text field now is editing, and so we are ready for the next step, namely to select its contents.
The trick you've discovered is actually something that is surprisingly often needed in iOS programming. Cocoa is a complicated framework, and manipulations of the interface sometimes can stumble over one another's feet, as here — while the user is starting to edit in the text field, you are trying to select the text field's text. Sometimes we just need the runloop to come around one more time in order to permit the interface to "settle down" before proceeding to the next step.

Related

Deinit not calling - Cannot find why something is retaining (code provided)

I have discovered that my UIViewcontroller is not calling deinit() under the following scenario.
I am using this code extension to make my life easier by adding tap gesture recognizers.
https://gist.github.com/saoudrizwan/548aa90be174320fbaa6b3e71f01f6ae
I've used this code in one of my VCs, which I've stripped down to the barest minimum amount of code:
and in viewDidLoad() I did this:
// When the user taps on a label, have its related textbox automatically get the caret so they can type
// Add tapping so when you tap on a label it makes the corresponding textbox first responder
lblSubject.addTapGestureRecognizer {
self.txtSubject.becomeFirstResponder()
}
It appears that the line:
self.txtSubject.becomeFirstResponder()
Is the problem - when I leave this line above in that closure, deinit() does not call in my VC.
When I take the above line out or replace it with something like print("hello world")
deinit() properly calls. txtSubject is #IBOutlet weak var txtSubject: UITextField!
I am not entirely sure what to do here. I read that when you trigger becomeFirstResponder() it's important you call resignFirstResponder(), but even if I don't tap the label (so as to not give becomeFirstResponder() a chance to even call) I still cannot hit deinit()
Any ideas where I can look further?
Thanks so much.
Change
self.txtSubject.becomeFirstResponder()
To
[unowned self] in self.txtSubject.becomeFirstResponder()
unowned is often feared as dangerous, but there is no danger here. If self ceases to exist, there will be nothing to tap and the code will never run.
This is a classic retain loop. The self. inside of the closure is there to remind you to think about this. I assume that self is retaining lblSubject, and (via a OBJC_ASSOCIATION_RETAIN associated key), lblSubject is retaining self because it's captured by this closure.
You don't really need self here, however. You just need txtSubject. So you can just capture that:
lblSubject.addTapGestureRecognizer { [txtSubject] in
txtSubject.becomeFirstResponder()
}
Alternately, you can fall back to the giant weak self hammer (though this tends to be greatly over-used):
lblSubject.addTapGestureRecognizer { [weak self] in
self?.txtSubject.becomeFirstResponder()
}
The best way to explore this kind of bug is with Xcode's Memory Graph.
It is also a good idea to review the Swift docs on Automatic Reference Counting.

IOS Swift 3 UILabels in View don't get rendered correctly

To start some information about my setup: I have a Storyboard with 3 Views and 3 ViewControllers, one attached to each of the Views.
I'm switching the Views from a completion handler of a DataTask (http request) with self.performSegue(withIdentifier: "bookingSuccess", sender: nil). To be able to pass some data I use the prepare function where I assign my ViewModel to the destination Controller.
After that i prepare some UI Stuff in the ViewDidLoad() function and assign Values to my Labels in the ViewWillAppear() Method.
This works fine, but when the view switches, half of the UI Elements don't show and the button text is somehow also not there. Under some circumstances, which I don't know (most time after just waiting a few seconds or reopen without closing the APP these Elements are suddenly showing up.
I tried a lot of different things and nothing works. Sometimes a label shown up at the beginning, but I haven't got any Idea why it's not displaying. My IOS Target Version is either IOS 10 or IOS 11.0.1/3
Here are two images of the screens:
Directly after switching the View
After waiting about 10 seconds
This sounds like a problem with doing UI work on the background thread. Since you are calling self.performSegue(withIdentifier: "bookingSuccess", sender: nil) from the completion handler of the DataTask and that will be on the background thread so do:
DispatchQueue.main.async {
self.performSegue(withIdentifier: "bookingSuccess", sender: nil)
}

Update UIView before calling other function?

I am working in Swift. When a user presses a UIButton it calls a function ButtonPressed(). I would like ButtonPressed() to do two things:
Update the UIView by removing the current buttons and texts, then uploading some new text.
Call function TimeConsumingCalculation(). TimeConsumingCalculation is the complicated part of my app and does some calculations which take about 20 seconds or so to complete.
Right now, I have the code in the basic order:
ButtonPressed(){
self.Button.removeFromSuperview()
TimeConsumingCalculation()
}
However, it will not remove the button or do any other UI updates or additions until after the TimeConsumingCalculation is complete. I have read and attempted a few guides on closures and asynchronous functions, but have had no luck. Is there a special property with UIView that is causing it to be updated last?
As a side note - I have already attempted putting all UI actions in a separate function and calling it first. It doesn't work. The time consuming function does not take any variables from the buttons or UI or anything like that.
Thanks!
It seems like timeConsumingCalculation() is blocking the main queue, which is in charge of UI updates. Try calling it like this instead and use the isHidden property to hide the button instead of removing it from the view completely.
ButtonPressed(){
self.Button.isHidden = true
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
self.timeConsumingCalculation()
}
}
here you call timeConsumingCalculation() asynchronously on a background thread. The quality of service we give it is userInitiated, read more about quality of service classes here

Checking the current view state after block/closure completion

Within a asynchronously executed block/closure, I want to get a check on my current state before I executed anything within that block.
A common example of where this presents itself is segueing to the next View Controller after a NSURLsession request.
Here's an example:
#IBAction func tappedButton(sender: UIButton) {
//This closure named fetchHistorical goes to the internet and fetches an array
//The response is then sent to the next view controller along with a segue
Order.fetchHistorical(orderID, completionHandler: { (resultEnum) -> () in
switch resultEnum {
case .Success(let result):
let orderItemsArray = result.orderItems!.allObjects
self.performSegueWithIdentifier("showExchanges", sender: orderItemsArray)
default:
let _ = errorModal(title: "Error", message: "Failed!")
}
})
}
Assume that the user has been impatient and tapped this button 3 times.
That would mean this function will be called three times and each time it would attempt to segue to the next view controller (iOS nicely blocks this issue with "Warning: Attempt to present on whose view is not in the window hierarchy!")
I wanted to know how do you folks tackle this problem? Is it something like ... within the closure, check if you are still in the present viewcontroller ... if you are, then segueing is valid. If not, you probably have already segued and don't execute the segue again.
***More generally, how are you checking the current state within the closure because the closure is executed asynchronously?
Since the closure isn't executing on the main thread the state would be in accurate if you check it here (as you stated). You can use GCD to go to the main thread and check the state there. There are a couple of ways you can keep this code from running multiple times. If it will take some time to perform the calculations you can use an acitivity indicator to let the user know the app is busy at the moment. If you want the user to still have the option of pressing the button you can put a tag like:
var buttonWasTapped:Bool = false //class property
#IBAction func tappedButton(sender: UIButton) {
if !self.buttonWasTapped{
self.buttonWasTapped = true
}
}
Then change it back to false on viewDidAppear so they can press once every time that page is shown.
When starting some task that will take some time to complete I would do two things;
Show some sort of activity indicator so that the user knows something is happening
Disable the button so that there is further indication that the request has been received and to prevent the user from tapping multiple times.
It is important that you consider not only the correct operation of your app but also providing a good user experience.

Swift Accelerometer keeps crashing with ScrollView

EDIT: I have simplified the code and added calls to NSThread.isMainThread(), to see if this was the problem. See more extensive edit below
I'm working on a fairly simple app to assist a professor in research over the summer. The app intends to determine word difficulty in sentences based on the accelerometer in the iPad.
Essentially, the user will tilt the iPad, thus creating a non-zero acceleration, and the text, which is situated in a UILabel placed within a scrollView will scroll accordingly.
This works excellently 99% of the time. In almost all of our tests, it works perfectly, it goes through the entire text without issue, and nothing bad happens. Very rarely however, it just breaks, throwing an error of EXC_BAD_ACCESS. I want to stress that on the rare occasions it does break, there is no apparent pattern, it sometimes happens in the middle of scrolling, near the end, or at the start.
Obviously I would like the app to be bug free, and this is a fairly major one which I just can't figure out, so any help you can give would be greatly appreciated.
Here is the total code for my ScrollingLabel Class (the bug always happens at the end of the startScrolling class).
import Foundation
import UIKit
import QuartzCore
import CoreMotion
public class ScrollingLabel {
//Instantiation of scroll view and label
var baseTextLabel:UILabel!
var baseScrollView:UIScrollView!
var frame:CGRect!
//Instantiation of accelerometer materials
var motionManager=CMMotionManager()
var queue=NSOperationQueue()
//To rectify the issue, I have changed this to:
//var queue=NSOperationQueue.mainQueue(), SEE EDIT BELOW
init(frame:CGRect) {
/*Initializes the object by calling 3 private setup functions,
each dealing one with a specific feature of the final label, and
finally calling the scroll function to activate the accelerometer
control*/
setupFrame(frame)
setupLabel()
setupScroll()
startScrolling()
}
private func startScrolling() {
//The main accelerometer control of the label
println(NSThread.isMainQueue) //THIS RETURNS TRUE
//Allows the start orientation to become default
var firstOrientation:Bool
var timeElapsed:Double=0
if letUserCreateDefaultOrientation {firstOrientation=true}
else {firstOrientation=false}
var standardAccel:Double=0
//Begins taking updates from the accelerometer
if motionManager.accelerometerAvailable{
motionManager.accelerometerUpdateInterval=updateTimeInterval
motionManager.startAccelerometerUpdatesToQueue(self.queue, withHandler: { (accelerometerData, error:NSError!) -> Void in
println(NSThread.isMainQueue) //THIS RETURNS FALSE
//Changes the input of acceleration depending on constant control variables
var accel:Double
if self.timerStarted {
timeElapsed+=Double(self.updateTimeInterval)
}
if !self.upDownTilt {
if self.invertTextMotion {accel = -accelerometerData.acceleration.y}
else {accel = accelerometerData.acceleration.y}
}
else {
if self.invertTextMotion {accel = -accelerometerData.acceleration.x}
else {accel = accelerometerData.acceleration.x}
}
//Changes default acceleration if allowed
if firstOrientation {
standardAccel=accel
firstOrientation=false
}
accel=accel-standardAccel
//Sets the bounds of the label to prevent nil unwrapping
var minXOffset:CGFloat=0
var maxXOffset=self.baseScrollView.contentSize.width-self.baseScrollView.frame.size.width
//If accel is greater than minimum, and label is not paused begin updates
if !self.pauseScrolling && fabs(accel)>=self.minTiltRequired {
//If the timer has not started, and accel is positive, begin the timer
if !self.timerStarted&&accel<0{
self.stopwatch.start()
self.timerStarted=true
}
//Stores the data, and moves the scrollview depending on acceleration and constant speed
if self.collectData {self.storeIndexAccelValues(accel,timeElapsed: timeElapsed)}
var targetX:CGFloat=self.baseScrollView.contentOffset.x-(CGFloat(accel) * self.speed)
if targetX>maxXOffset {targetX=maxXOffset;self.stopwatch.stop();self.doneWithText=true}
else if targetX<minXOffset {targetX=minXOffset}
self.baseScrollView.setContentOffset(CGPointMake(targetX,0),animated:true)
if self.baseScrollView.contentOffset.x>minXOffset&&self.baseScrollView.contentOffset.x<maxXOffset {
if self.PRIVATEDEBUG {
println(self.baseScrollView.contentOffset)
}
}
}
})
}
}
When it does crash, it happens at the end of the startScrolling, when I set the content Offset to target X. If you need more information I am happy to provide it, but as the bug happens so rarely I don't have anything to say about specifically when it occurs or anything like that... it just seems random.
EDIT: I have simplified the code to just the pertinent parts, and added the two locations where I called NSThread.isMainQueue(). When called on the first line of startScrolling, .isMainQueue() returns TRUE, but then when called inside motionManager.startAccelerometerUpdatesToQueue it returns FALSE.
To rectify this, I have changed self.queue from just a NSOperationQueue() to NSOperationQueue.mainQueue(), and after making this switch, the second .isMainThread() call (the one inside motionManager.startAccelerometerUpdatesToQueue) now returns TRUE as we hoped for.
That's a lot of code. I don't see anything in the line that sets your content offset based on targetX.
A couple of possibilities are
baseScrollView is getting deallocated and is a zombie (unlikely since it looks like you have it defined as a regular (strong) instance variable.)
You're calling startScrolling from a background thread. All bets are off if you update UI objects from a background thread. You can check for that using NSThread.isMainThread(). Put that code in your startScrolling method, and if it returns false, that is your problem.
EDIT:
Based on your comments below in response to my answer you are calling startScrolling from a background thread.
That is indeed very likely the problem. Edit your post to show the code that is being called from a background thread, including the context. (Your "accelerometer update cycle" code).
You can't manipulate UIKit objects from a background thread, so you likely need to wrap the UIKit changes in your "accelerometer update cycle" code in a call to dispatch_async that runs on the main thread.
EDIT #2:
You've finally posted enough information so that we can help you. Your original code had you receiving acellerometer updates on a background queue. You were doing UIKit calls from that code. That is a no-no, and the results of doing it are undefined. They can range from updates taking forever, to not happening at all, to crashing.
Changing your code to use NSOperationQueue.mainQueue() as the queue that processes updates should fix the problem.
If you need to do time-consuming processing in your handler for accelerometer updates then you could continue to use the background queue you were using before, but wrap your UIKit calls in a dispatch_async:
dispatch_async(dispatch_get_main_queue())
{
//UIKit code here
}
That way your time-consuming accelerometer update code doesn't bog down the main thread but UI updates are still done on the main thread.

Resources