Raül Raül - 26 days ago 26
Swift Question

Modify View Controller Properties Before View Controller is Displayed

I'm about to finish my first iOS app, but I'm facing a problem that I don't know how to solve.

The initial ViewController of the app contains a MKMapView, that has few annotations. When clicking any annotation, a popup appears with some info and then, there's an info button that leads to a new ViewController, with detailed information related to the selected annotation.

The thing is that second ViewController(DetailedViewController), has few labels (title, description), an image and few links, which should be loaded before the ViewController itself is shown. I'm reading this values from a JSON but there's no problem with that.

I'm reading all values and setting them in the ViewDidLoad()

So the thing is, when that second ViewController(DetailedViewController) is loaded in the simulator or in the physical iPhone, all fields has their default value (the one set in the storyboard) and spends few seconds to update with the desired values, even they're read so fast from the JSON (according to logs).

Also, an error appears in the console:

This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.
Stack:(
0 CoreFoundation 0x000000018a0ca1d8 <redacted> + 148
1 libobjc.A.dylib 0x0000000188b0455c objc_exception_throw + 56
2 CoreFoundation 0x000000018a0ca108 <redacted> + 0
3 Foundation 0x000000018acb1ea4 <redacted> + 192
4 Foundation 0x000000018acb1be4 <redacted> + 76
5 Foundation 0x000000018aafd54c <redacted> + 112
6 Foundation 0x000000018acb0880 <redacted> + 112
7 UIKit 0x000000018ff2140c <redacted> + 1688
8 QuartzCore 0x000000018d3e1188 <redacted> + 148
9 UIKit 0x000000019056de90 <redacted> + 64
10 QuartzCore 0x000000018d3d5e64 <redacted> + 292
11 QuartzCore 0x000000018d3d5d24 <redacted> + 32
12 QuartzCore 0x000000018d3527ec <redacted> + 252
13 QuartzCore 0x000000018d379c58 <redacted> + 512
14 QuartzCore 0x000000018d37a124 <redacted> + 660
15 libsystem_pthread.dylib 0x000000018915efbc <redacted> + 572
16 libsystem_pthread.dylib 0x000000018915ece4 <redacted> + 200
17 libsystem_pthread.dylib 0x000000018915e378 pthread_mutex_lock + 0
18 libsystem_pthread.dylib 0x000000018915dda4 start_wqthread + 4
)


I've read similar issues and they're talking about something like this:

dispatch_async(dispatch_get_main_queue(){
// code here
})


But I don't know exactly how to use it :(

Can anybody help me?

Thank you very much in advance :)

EDIT:

I'll add the code of my detailed view controller, to explain more accurately what happens:

class BarDetailsViewController: UIViewController {

@IBOutlet var BarDetailsView: UIView!
@IBOutlet weak var BarNameLabel: UILabel!
@IBOutlet weak var BarDescriptionLabel: UILabel!

@IBOutlet weak var BarImage: UIImageView!

var barPhoneNumber: AnyObject! = "anyBarPhoneNumber"
var barWebsite: AnyObject! = "anyBarWebsite"
var barName: AnyObject! = "anyBarName"

var pinCoordinates : CLLocationCoordinate2D!
var pinId : String!

override func viewDidLoad() {
super.viewDidLoad()

let jsonURL: String = "http://www.craftbr.me/services/getbiz/index.php?barid=" + self.pinId
let requestURL: NSURL = NSURL(string: jsonURL)!

let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in

let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode

if (statusCode == 200) {
do{
let json: AnyObject! = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)

let barName: AnyObject! = json["name"]
self.BarNameLabel.text = String(barName)
self.barName = String(barName)

self.BarDescriptionLabel.text = (json["description"] as! String)

self.barPhoneNumber = json["telf"]

self.barWebsite = json["website"] as! String

let barImgUrl: AnyObject! = json["imgurl"]
let barImgUrlString: String! = String(barImgUrl)

if let imageUrl = NSURL(string: barImgUrlString),
let data = NSData(contentsOfURL: imageUrl)
{
self.BarImage.image = UIImage(data: data)
}

}catch {
print("Error with Json: \(error)")
}
}
}
task.resume()
}

}


So here, inside of the ViewDidLoad() I modify the labels content created in the Storyboard with the info got from the json.

The thing is getting the info from the json is so fast, but then, modifying the labels is the thing that is slow and throws the error.

So @fragilecat, where should I add the function? And how do I declare the DispatchQueue? I've tried it but it doesn't work. And this function, where should be called?

Thanks again for your help :)

Answer

Apple uses queues to represent threads, as @Adam says in the comments you you should look at Concurrency Programming Guide to get a better understanding of your app's behavior.

However it might be useful to explain what is going on with your app that it creates your crash as it is a very common pattern in iOS.

You are making a request to some web service. Unless explicitly this reuqest is going to happen on a background queue. If this was not the case your app's UI would freeze while the request to the web service was made. The point to remember here is that ALL ui work must be done on the main queue! @Leon Guo has shown you how to access the main queue from a different queue.

So how does this effect your web service request. Well when the response comes back and you parse it and you are ready to assign the data to ui elements such as labels and textfields, you want to do that on the main queue like so:

// comment: example method on DetailedViewController class
func prepareUI(data: MyDataType){
    // Swift 3
    DispatchQueue.main.async {
        titleLabel.text = data.titleString
        descriptionLabel.text = data.descriptionText
    }
}

Or other wise you will be accessing the ui elements in the DetailedViewController from a queue other than the main queue, hence your crash. So to solve your problem right now wrap your assign of the UI elements in the DispatchQueue.main.async block of code where ever you are doing it. This should get you back up and running again.

In the long term you need to develop an understanding of how concurrency is implemented in iOS. The App Programming Guide for iOS is a great place to start!

Edit

Here is your code using the DispatchQueue.main.async function. I would state that this is just to get you up and running...

import UIKit

class BarDetailsViewController: UIViewController {

    @IBOutlet var barDetailsView: UIView!
    @IBOutlet weak var barNameLabel: UILabel!
    @IBOutlet weak var barDescriptionLabel: UILabel!

    @IBOutlet weak var BarImage: UIImageView!

    var barPhoneNumber  = "anyBarPhoneNumber"
    var barWebsite = "anyBarWebsite"
    var barName = "anyBarName"

    var pinCoordinates : CLLocationCoordinate2D!
    var pinId : String!

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // For example purposes only
        networkCall()

    }



    func networkCall() {
        let jsonURL: String = "http://www.craftbr.me/services/getbiz/index.php?barid=" + self.pinId
        let requestURL: NSURL = NSURL(string: jsonURL)!

        let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithRequest(urlRequest) {
            (data, response, error) -> Void in

            let httpResponse = response as! NSHTTPURLResponse
            let statusCode = httpResponse.statusCode

            if (statusCode == 200) {
                do{

                    let json: AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)

                    // Here we get back on the main queue since we are altering UI elements
                    dispatch_async(dispatch_get_main_queue()) {
                        let barName: AnyObject! = json["name"]
                        self.barNameLabel.text = String(barName)
                        self.barName = String(barName)

                        if let text = json["description"] as? String {
                            self.barDescriptionLabel.text = text
                        }

                        if phoneNumber = json["telf"] as? String {
                            self.barPhoneNumber = phoneNumber
                        }

                        if webSite = json["website"] as? String {
                            self.barWebsite =  barWebsite
                        }

                        let barImgUrl: AnyObject = json["imgurl"]
                        let barImgUrlString: String = String(barImgUrl)

                        if let imageUrl  = NSURL(string: barImgUrlString), let data = NSData(contentsOfURL: imageUrl) {
                            self.BarImage.image = UIImage(data: data)
                        }
                    }


                }catch {
                    print("Error with Json: \(error)")
                }
            }
        }
        task.resume()
    }

}
Comments