The issue is now resolved. See the last edit for details!
I have an UIViewController with an audio visualisation and a button. When a button is pressed, the following function is fired:
func use(sender:UIButton!) {
// Analyse the audio
let analysisQueue = dispatch_queue_create("analysis", DISPATCH_QUEUE_CONCURRENT)
dispatch_async(analysisQueue, {
// Initialise the analysis controller
let analysisController = AnalysisController()
analysisController.analyseAudio(global_result.filePath, completion: {
// When analysis is complete, navigate to another VC
dispatch_async(dispatch_get_main_queue(), {
let mainStoryboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ResultDetailViewController") as UIViewController
let navigationController = self.navigationController
// Pop the current VC before pushing the new one
navigationController?.popViewControllerAnimated(false)
navigationController?.pushViewController(vc, animated: false)
})
})
})
}
Here, I create a background queue and start a very lengthy signal processing operation. Once the processing is complete, I perform navigation to another view controller using the main queue.
This causes the ResultDetailViewController
to appear on the screen with all the relevant data and visualisation fully loaded.
However, for the first 2-3 seconds after the VC has been loaded, none of the buttons work! If I click any button within this initial period, the action would be fired once that initial period is over.
When I perform this transition from any other VC, the ResultDetailViewController
is loaded smoothly, and everything works.
What am I doing wrong? Any help would be much appreciated!
EDIT 1
I will add more details about my set-up:
In AnalysisController
, I am doing the following:
- Process the signal using FFT's and such
- Update a the properties of a
global_result
- Trigger a method of
global_result
, which stores the result in CoreData and uses Alamofire to POST the data to my server - Once the first POST succeeds, the callback updates
global_result
's id, and fires few more POST requests. - Completion handler is triggered, which then causes the transition
global_result
, is my custom global Object which is initialised as a public var
.
In theory, the completion handler should be triggered once the processing completes, results are saved in CoreData, and the first POST request is dispatched.
In ResultDetailViewController
's viewDidLoad
function, I am copying the global_result
into a local variable and creating the UI elements using data from global_result
.
Now, I suspected that the lag occurs due to background thread using global_result
when ResultDetailViewController
is already loaded, so I tried to create a new instance of Result
class, instead of copying the global_result
, but that also didn't help.
And here's the Result
class:
import Foundation
import CoreData
import Alamofire
import CryptoSwift
public class Result {
var localID: Int
var id: Int
var filePath: NSURL!
var result: Double
var origSound: [Double]
init(localID: Int, id: Int, filePath: NSURL, result: Double, origSound: [Double]) {
// Initialize stored properties.
self.localID = localID
self.id = id
self.filePath = filePath
self.result = result
self.origSound = origSound
}
func store() {
self.storeLocal()
// Serialise data
let parameters = [
"localID": self.localID,
"id": self.id,
"result": self.result
]
// Prepare request
let request = NSMutableURLRequest(URL: NSURL(string: "my_server/script.php")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: parameters)
// POST the data to server
Alamofire.request(request)
.responseJSON { response in
if let JSON = response.result.value {
if let myID = JSON as? Int {
self.id = myID
// Store the global ID in CoreData
updateIDLocal(self.localID, id: self.id)
// Serialise and POST array
let param = [
"origSound": self.origSound
]
// Prepare request
var request = NSMutableURLRequest(URL: NSURL(string: "my_server/script2.php?id=\(self.id)")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: param)
// POST the data to server
Alamofire.request(request)
// Upload the file
let upURL = "my_server/script3.php?id=\(self.id)"
// Prepare request
request = NSMutableURLRequest(URL: NSURL(string: upURL)!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encrypt the file
request.HTTPBody = fileToEncryptedData(rsaKey, filePath: self.filePath)
// POST the data to server
Alamofire.request(request)
}
}
}
}
// Store the object in CoreData
func storeLocal() {
// Create a local id
if let oldID = NSUserDefaults.standardUserDefaults().integerForKey("localID") as Int? {
// Increment the ID
NSUserDefaults.standardUserDefaults().setInteger(oldID + 1, forKey: "localID")
self.localID = oldID + 1
}
else {
// First object in CoreData
NSUserDefaults.standardUserDefaults().setInteger(0, forKey: "localID")
}
// Store data in CoreData
var resultDatas = [NSManagedObject]()
//1
let appDelegate =
UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
//2
let entity = NSEntityDescription.entityForName("Result",
inManagedObjectContext:managedContext)
let resultData = NSManagedObject(entity: entity!,
insertIntoManagedObjectContext: managedContext)
// Store data
resultData.setValue(localID, forKey: "localID")
resultData.setValue(id, forKey: "id")
resultData.setValue(filePath.path!, forKey: "url")
resultData.setValue(result, forKey: "result")
// Store the array
var data = NSData(bytes: origSound, length: origSound.count * sizeof(Double))
resultData.setValue(data, forKey: "origSound")
//4
do {
try managedContext.save()
//5
resultDatas.append(resultData)
} catch _ {
print("Could not save")
}
}
}
Within the AnalysisController
, I am calling global_result.store()
EDIT 2
What I thought was happening, was this:
- Created a background thread
- Done heavy processing on that thread
- Sent a POST request on that thread
- HTTP response was processed, and lots of data was encrypted on that background thread
- Jumped to the main thread
- Performed the transition on the main thread
In reality, this happened:
- Created a background thread
- Done heavy processing on that thread
- Sent a POST request on that thread
- Jumped to main thread
- Performed the transition on the main thread
- HTTP responses suddenly came back to the main thread, and it became blocked until tons of data finished encrypting.
Thanks to alexcurylo's suggestion, and this SO thread, I realised that the Alamofire's response handling occurs on the main thread, thus it was necessary to use one cool Alamofire feature to push the response handling onto a concurrent thread, so not to block the main queue.
For anyone's future reference, I implemented it like so:
// Prepare request
let request = NSMutableURLRequest(URL: NSURL(string: "my_server/script.php")!)
request.HTTPMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// Encode parameters in JSON and encrypt them
request.HTTPBody = dictToEncryptedJSONData(rsaKey, parameters: parameters)
// Create a concurrent queue
let queue = dispatch_queue_create("DT.response-queue", DISPATCH_QUEUE_CONCURRENT)
// POST the data to server
Alamofire.request(request)
.response(
queue: queue,
responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments),
completionHandler: { response in
// Perform response-handling HERE
})
Thanks everyone for your help! Even though it didn't directly solve this problem, Sandeep Bhandari's background queue creation tip, and Igor B's method for Storyboard referencing represent a better coding practice, which should be adopted instead of my original code.