I'm writing a program that retrieves values from a database and displays it on the UI. The retrieval step executes asynchronously and as a result the result box gets updated before the db results come back. Right now I can get it to work by using a NSThread.sleepForTimeInterval(time) above the "ResultBox.text = globaldbresults" but I know that is a bad way to do it since the retrieval time will vary depending on the size of the retrieval etc. I much prefer to ensure a block (the UI textfield update) only runs after the retrieve has completed.
I've tried using dispatch_group_wait but I think I'm using it wrong.
[In AppDelegate.swift]
var globaldbresults
#UIApplicationMain
[END In Appdelegate.swift]
import UIKit
import Dispatch
class ViewController: UIViewController {
dbGetqueue = dispatch_group_create()
#IBAction func goClicked(sender: AnyObject) {
dispatch_group_enter(dbGetqueue)
mysqlget(Room1num, num2: Room2num)
dispatch_group_leave(dbGetqueue)
dispatch_group_wait(dbGetqueue, 5)
ResultBox.text = globaldbresults
}
func mysqlget(num1 :Int, num2 :Int) {
let myUrl = NSURL(string: "http://localhost/iOS_PHP_MySQL.php")
let myGetTask = NSURLSession.sharedSession().dataTaskWithRequest(myRequest, completionHandler: {(reqData, reqResponse, reqError) -> Void in
dbresults = (NSString(data: reqData, encoding: NSUTF8StringEncoding)!)
globaldbresults = String(dbresults)
})
myGetTask.resume()
}
}
I've just put the relevant lines of code. The globaldbresults which is bad way to deal with one of my problem solves the scope issue which occurs if I try to directly reference dbresults from my goClicked Action.
I would it should be:
dispatch_group(dbGetqueue) {
block to execute
}
[Some other code not dependent on block above]
dispatch_group_wait(dbGetqueue) {
code that doesn't start till dbGetqueue finished
}
but that is obviously not the case.
If you have only one request to get desired data this could be solved by a simple callback closure.
But if you want to do it with dispatch_group_ way here it could be done:
class ViewController: UIViewController {
dbGetqueue = dispatch_group_create()
#IBAction func goClicked(sender: AnyObject) {
mysqlget(Room1num, num2: Room2num)
dispatch_group_notify(dbGetqueue,dispatch_get_main_queue(),{
self.ResultBox.text = globaldbresults
})
}
func mysqlget(num1 :Int, num2 :Int) {
dispatch_group_enter(dbGetqueue) //Enter group here
let myUrl = NSURL(string: "http://localhost/iOS_PHP_MySQL.php")
let myGetTask = NSURLSession.sharedSession().dataTaskWithRequest(myRequest, completionHandler: {(reqData, reqResponse, reqError) -> Void in
dbresults = (NSString(data: reqData, encoding: NSUTF8StringEncoding)!)
globaldbresults = String(dbresults)
dispatch_group_leave(self.dbGetqueue) //Leave group when you are done getting results.
})
myGetTask.resume()
}
Another way of doing without dispatch_group_ is
class ViewController: UIViewController {
dbGetqueue = dispatch_group_create()
#IBAction func goClicked(sender: AnyObject) {
mysqlget(Room1num, num2: Room2num)
}
func mysqlget(num1 :Int, num2 :Int) {
let myUrl = NSURL(string: "http://localhost/iOS_PHP_MySQL.php")
let myGetTask = NSURLSession.sharedSession().dataTaskWithRequest(myRequest, completionHandler: {(reqData, reqResponse, reqError) -> Void in
dbresults = (NSString(data: reqData, encoding: NSUTF8StringEncoding)!)
globaldbresults = String(dbresults)
dispatch_async(dispatch_get_main_queue(), {
self.self.ResultBox.text = globaldbresults
})
})
myGetTask.resume()
}
}
}
Related
I have a scenario where I need to load data from a JSON object, I've created an helper class that does that and it looks like this
protocol JSONDumpHelperDelegate {
func helper(_: JSONDumpHelper, didFinishFetching: [Link])
func helper(_: JSONDumpHelper, completionProcess: Double)
}
struct JSONDumpHelper {
static let pointsOfInterest = OSLog(subsystem: "com.mattrighetti.Ulry", category: .pointsOfInterest)
var delegate: JSONDumpHelperDelegate?
func loadFromFile(
with filemanager: FileManager = .default,
from url: URL,
decoder: JSONDecoder = JSONDecoder(),
context: NSManagedObjectContext = CoreDataStack.shared.managedContext,
dataFetcher: DataFetcher = DataFetcher()
) {
os_signpost(.begin, log: JSONDumpHelper.pointsOfInterest, name: "loadFromFile")
let data = try! Data(contentsOf: url)
let dump = try! decoder.decode(Dump.self, from: data)
var tagsHash: [UUID:Tag] = [:]
if let tagsCodable = dump.tags {
for tagCodable in tagsCodable {
let tag = Tag(context: context)
tag.id = tagCodable.id
tag.name = tagCodable.name
tag.colorHex = tagCodable.colorHex
tagsHash[tag.id] = tag
}
}
var groupHash: [UUID:Group] = [:]
if let groupsCodable = dump.groups {
for groupCodable in groupsCodable {
let group = Group(context: context)
group.id = groupCodable.id
group.name = groupCodable.name
group.colorHex = groupCodable.colorHex
group.iconName = groupCodable.iconName
groupHash[group.id] = group
}
}
var links: [Link] = []
if let linksCodable = dump.links {
let total = linksCodable.count
var completed = 0.0
delegate?.helper(self, completionProcess: 0.0)
for linkCodable in linksCodable {
let link = Link(context: context)
link.id = linkCodable.id
link.url = linkCodable.url
link.createdAt = linkCodable.createdAt
link.updatedAt = linkCodable.updatedAt
link.colorHex = linkCodable.colorHex
link.note = linkCodable.note
link.starred = linkCodable.starred
link.unread = linkCodable.unread
if let uuidGroup = linkCodable.group?.id {
link.group = groupHash[uuidGroup]
}
if let tags = linkCodable.tags {
link.tags = Set<Tag>()
for tagUUID in tags.map({ $0.id }) {
link.tags?.insert(tagsHash[tagUUID]!)
}
}
links.append(link)
completed += 1
delegate?.helper(self, completionProcess: completed / Double(total))
}
}
os_signpost(.end, log: JSONDumpHelper.pointsOfInterest, name: "loadFromFile")
}
}
This could potentially be a very long running task, just imagine an array with 1k records that also need to fetch data from the internet (not shown in implementation, error still exist with posted code) and you can easily end up with 10s in execution time.
What I'm trying to achieve is to show the user an alert that will show him the progress of the import process, by updating the values with the delegate protocols below.
extension BackupViewController: JSONDumpHelperDelegate {
func helper(_: JSONDumpHelper, didFinishFetching: [Link]) {
DispatchQueue.main.async {
self.completionViewController.remove()
}
}
func helper(_: JSONDumpHelper, completionProcess: Double) {
DispatchQueue.main.async {
self.completionViewController.descriptionLabel.text = "Completed \(Int(completionProcess * 100))%"
}
}
}
The import method is fired from a UITableView, immediately after the user choses a file from a DocumentPickerViewController
extension BackupViewController: UIDocumentPickerDelegate {
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
dismiss(animated: true, completion: nil)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
self.initImport(for: urls)
}
}
private func initImport(for urls: [URL]) {
if let url = urls.first {
completionViewController.add(self, frame: self.view.bounds)
completionViewController.descriptionLabel.text = "Fetching"
DispatchQueue.global(qos: .userInteractive).async {
self.dumpHelper.loadFromFile(from: url)
}
}
}
The problem I am facing is that when the user initiates the import process, the UI is not updated until the process itself is finished.
If I place breakpoints both at the protocol implementations and at the delegate calls in the helper class I can see that the delegate is not called immediately but they all get fired when the process ended (but the alert controller does not update its values).
Just to place some more info I'm going to replicate an import of N elements from JSON:
User clicks import process
initImport is executed (nothing is shown on UI even if I add the custom vc to view)
JSONDumpHelper.loadFromFile is executed entirely, calling delegate N times, nothing called in the delegate implementation
loadFromFile finishes execution
delegate implementation of helper(_:, completionProcess) is executed N times, UI always shows "Completed 0%"
delegate implementation of helper(_:, didFinishFetching) is executed, controller is removed from view
Can anybody point out what is wrong with this implementation? It seems like the loadFromFile function is not working in a separate Queue and UI is stuck and can't update as expected.
I am beginner in programming. I actually have my own answer of this questions and the app worked as I am expected, but I am not sure if this is the correct way to to this.
This check out action will be triggered after the user click chechoutButton. but before before this chechoutButton.isEnabled , I have to make sure 3 parameters are available (not nil). before doing this check out action, I need 3 parameters :
get user's coordinate from GPS.
get user's location address from Google Place
API
Get current date time from server for verification.
method to get user location address from Google Place API will be triggered only if I get the coordinate from GPS, and as we know, fetching data from the internet (to take date and time) also takes time, it should be done asynchronously.
how do I manage this checkoutButton only enabled if those 3 parameters are not nil ? Is there a better way according to apple guideline to do this
the simplified code are below
class CheckoutTVC: UITableViewController {
#IBOutlet weak var checkOutButton: DesignableButton!
var checkinAndCheckoutData : [String:Any]? // from MainMenuVC
var dateTimeNowFromServer : String?
var userLocationAddress : String?
let locationManager = LocationManager()
var coordinateUser : Coordinate? {
didSet {
getLocationAddress()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// initial state
checkOutButton.alpha = 0.4
checkOutButton.isEnabled = false
getDateTimeFromServer()
getCoordinate()
}
#IBAction func CheckoutButtonDidPressed(_ sender: Any) {
}
}
extension CheckoutTVC {
func getDateTimeFromServer() {
activityIndicator.startAnimating()
NetworkingService.getDateTimeFromServer { (result) in
switch result {
case .failure(let error) :
self.activityIndicator.stopAnimating()
// show alert
case .success(let timeFromServer) :
let stringDateTimeServer = timeFromServer as! String
self.dateTimeNowFromServer = stringDateTimeServer
self.activityIndicator.stopAnimating()
}
}
}
func getCoordinate() {
locationManager.getPermission()
locationManager.didGetLocation = { [weak self] userCoordinate in
self?.coordinateUser = userCoordinate
self?.activateCheckOutButton()
}
}
func getLocationAddress() {
guard let coordinateTheUser = coordinateUser else {return}
let latlng = "\(coordinateTheUser.latitude),\(coordinateTheUser.longitude)"
let request = URLRequest(url: url!)
Alamofire.request(request).responseJSON { (response) in
switch response.result {
case .failure(let error) :// show alert
case .success(let value) :
let json = JSON(value)
let locationOfUser = json["results"][0]["formatted_address"].string
self.userLocationAddress = locationOfUser
self.locationAddressLabel.text = locationOfUser
self.activateNextStepButton()
}
}
}
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
}
I manage this by using this method, but I don't know if this is the correct way or not
func activateCheckoutButton() {
if dateTimeNowFromServer != nil && userLocationAddress != nil {
checkOutButton.alpha = 1
checkOutButton.isEnabled = true
}
}
You can use DispatchGroup to know when all of your asynchronous calls are complete.
func notifyMeAfter3Calls() {
let dispatch = DispatchGroup()
dispatch.enter()
API.call1() { (data1)
API.call2(data1) { (data2)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
}
dispatch.enter()
API.call3() { (data)
//DO SOMETHING WITH RESPONSE
dispatch.leave()
}
dispatch.notify(queue: DispatchQueue.main) {
finished?(dispatchSuccess)
}
}
You must have an equal amount of enter() and leave() calls. Once all of the leave() calls are made, the code in DispatchGroupd.notify will be called.
I am making two asynchronous network calls and would like to use a Dispatch Group to wait until the call complete and then resume. My program is freezing.
class CommentRatingViewController: UIViewController, UITextViewDelegate {
let myDispatchGroup = DispatchGroup()
#IBAction func saveRatingComment(_ sender: Any) {
rating = ratingView.rating
if rating != 0.0 {
myDispatchGroup.enter()
saveRating(articleID: post.articleID, userID: post.userID) //Network call
self.updatedRating = true
}
if commentsTextView.text != "" {
myDispatchGroup.enter()
saveComment(articleID: post.articleID, userID: post.userID, comment: commentsTextView.text!) //Network call self.addedComment = true
}
myDispatchGroup.wait()
DispatchQueue.main.async {
self.delegate?.didCommentOrRatePost(updatedRating: self.updatedRating, addedComment: self.addedComment)
}
}
And here is one of the network calls:
func saveRating (articleID: String, userID: String) {
let userPostURLRaw = "http://www.smarttapp.com/DesktopModules/DnnSharp/DnnApiEndpoint/Api.ashx?method=UpdatePostRating"
Alamofire.request(
userPostURLRaw,
method: .post,
parameters: ["articleID": articleID,
"newRating": self.rating,
"UserID": userID]
)
.responseString { response in
guard let myString = response.result.value else { return }
DispatchQueue.main.async {
self.myDispatchGroup.leave()
}
}
}
The network calls worked until I introduced Dispatch Group code.
I've resolved this.
The problem was that myDispatchGroup.enter() and self.myDispatchGroup.leave() where being called on different threads. I moved the call to the beginning and very end of the network requests and it works fine now.
I'm loading my UITableView from an Api call but although the data is retrieved fairly quickly, there is a significant time delay before it is loaded into the table. The code used is below
import UIKit
class TrackingInfoController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var table : UITableView?
#IBOutlet var indicator : UIActivityIndicatorView?
#IBOutlet var spinnerView : UIView?
var tableArrayList = Array<TableData>()
struct TableData
{
var dateStr:String = ""
var nameStr:String = ""
var codeStr:String = ""
var regionStr:String = ""
init(){}
}
override func viewDidLoad() {
super.viewDidLoad()
table!.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
spinnerView?.hidden = false
indicator?.bringSubviewToFront(spinnerView!)
indicator!.startAnimating()
downloadIncidents()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func BackToMain() {
performSegueWithIdentifier("SearchToMainSegue", sender: nil)
}
//#pragma mark - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1 //BreakPoint 2
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableArrayList.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell") as! CustomTableViewCell
cell.incidentDate.text = tableArrayList[indexPath.row].dateStr
cell.incidentText.text = tableArrayList[indexPath.row].nameStr
cell.incidentCode.text = tableArrayList[indexPath.row].codeStr
cell.incidentLoctn.text = tableArrayList[indexPath.row].regionStr
return cell //BreakPoint 4
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
AppDelegate.myGlobalVars.gIncName = tableArrayList[indexPath.row].nameStr
AppDelegate.myGlobalVars.gIncDMA = tableArrayList[indexPath.row].codeStr
performSegueWithIdentifier("SearchResultsToDetailSegue", sender: nil)
}
func alertView(msg: String) {
let dialog = UIAlertController(title: "Warning",
message: msg,
preferredStyle: UIAlertControllerStyle.Alert)
dialog.addAction(UIAlertAction(title: "Ok", style: .Default, handler: nil))
presentViewController(dialog,
animated: false,
completion: nil)
}
func downloadIncidents()
{
var event = AppDelegate.myGlobalVars.gIncName
var DMA = AppDelegate.myGlobalVars.gIncDMA
if event == "Enter Event Name" {
event = ""
}
if DMA == "Enter DMA" {
DMA = ""
}
let request = NSMutableURLRequest(URL: NSURL(string: "http://incident-tracker-api-uat.herokuapp.com/mobile/events?name=" + event)!,
cachePolicy: .UseProtocolCachePolicy,
timeoutInterval: 10.0)
request.HTTPMethod = "GET"
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
if error != nil {
self.alertView("Error - " + error!.localizedDescription)
}
else {
do {
var incidentList: TableData
if let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments) as? Array<Dictionary<String, AnyObject>> {
for item in json {
if let dict = item as? Dictionary<String, AnyObject> {
incidentList = TableData()
if let nameStr = dict["name"] as? String {
incidentList.nameStr = nameStr
}
if let codeStr = dict["dma"] as? String {
incidentList.codeStr = codeStr
}
if let dateStr = dict["supplyOutageStart"] as? String {
let tmpStr = dateStr
let index = tmpStr.startIndex.advancedBy(10)
incidentList.dateStr = tmpStr.substringToIndex(index)
}
if let regionStr = dict["region"] as? String {
incidentList.regionStr = regionStr
}
self.tableArrayList.append(incidentList)
}
}
self.spinnerView?.hidden = true
self.indicator?.stopAnimating()
self.table?.reloadData() //BreakPoint 3
}
}catch let err as NSError
{
self.alertView("Error - " + err.localizedDescription)
}
}
})
task.resume() //BreakPoint 1
}
When the class is run, it hits BreakPoint 1 first and then hits BreakPoint 2 and then quickly goes to BreakPoint 3, it then goes to BreakPoint 2 once more. Then there is a delay of about 20 to 30 seconds before it hits Breakpoint 4 in cellForRowAtIndexPath() and the data is loaded into the UITableView. The view is displayed quickly afterwards.
The data is retrieved quite quickly from the Web Service so why is there a significant delay before the data is then loaded into the tableView? Is there a need to thread the Web Service method?
You are getting server response in a background thread so you need to call the reloadData() function on the UI thread. I am suspecting that the wait time can vary depending on whether you interact with the app, which effectively calls the UI thread, and that's when the table actually displays the new data.
In a nutshell, you need to wrap the self.table?.reloadData() //BreakPoint 3 with
dispatch_async(dispatch_get_main_queue()) {
// update some UI
}
The final result would be
Pre Swift 3.0
dispatch_async(dispatch_get_main_queue()) {
self.table?.reloadData()
}
Post Swift 3.0
DispatchQueue.main.async {
print("This is run on the main queue, after the previous code in outer block")
}
The table view should begin to reload in a fraction of a second after you call tableView.reloadData().
If you make UI calls from a background thread, however, the results are "undefined". In practice, a common effect I've seen is for the UI changes to take an absurdly long time to actually take effect. The second most likely side-effect is a crash, but other, strange side-effects are also possible.
The completion handler for NSURLSession calls is run on a background thread by default. You therefore need to wrap all your UI calls in a call to dispatch_async(dispatch_get_main_queue()) (which is now DispatchQueue.main.async() in Swift 3.)
(If you are doing compute-intensive work like JSON parsing in your closure it's best to do that from the background so you don't block the main thread. Then make just the UI calls from the main thread.)
In your case you'd want to wrap the 3 lines of code marked with "breakpoint 3" (all UI calls) as well as the other calls to self.alertView()
Note that if you're sure the code in your completion closure is quick you can simply wrap the whole body of the closure in a call to dispatch_async(dispatch_get_main_queue()).
Just make sure you reload your tableview in inside the Dispatch main async, just immediately you get the data
I have two classes in Swift, one is a ViewController.swift, another has some business logic, called Brain.swift. In Brain.swift I have a class which contains a function called convert() which executes an NSTask.
In ViewController.swift all of the UI updating occurs.
What I would like to accomplish is getting the output of the convert()'s NSTask into a TextView in the ViewController.
I have implemented the solution from this answer, but I'm a bit of a novice so I'm unsure how to return it as a class property in real time to be accessible by other classes.
Brain.swift
import Foundation
internal func convert(chosenFile: NSURL, addText: (newText: String) -> Void) {
let bundle = NSBundle.mainBundle()
let task = NSTask()
let outputPipe = NSPipe()
task.standardOutput = outputPipe
let outHandle = outputPipe.fileHandleForReading
outHandle.readabilityHandler = { outputPipe in
if let line = String(data: outputPipe.availableData, encoding: NSUTF8StringEncoding) {
addText(newText: line)
} else {
print("Error decoding data: \(outputPipe.availableData)")
}
}
task.launch()
task.waitUntilExit()
}
ViewController.swift
#IBAction func Run(sender: AnyObject) {
let qualityOfServiceClass = QOS_CLASS_USER_INITIATED
let userInitiatedQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(userInitiatedQueue, {
self.btnConvert.enabled = false
self.btnSelect.enabled = false
self.activitySpinner.hidden = false
self.activitySpinner.startAnimation(self)
convert(self.inputFile.chosenFile) { newText in
self.statusText.stringValue = "\(newText)"
}
})
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.statusText.stringValue = "Done!"
self.activitySpinner.hidden = true
self.activitySpinner.stopAnimation(self)
self.btnSelect.enabled = true
})
}
This should work:
func convert(chosenFile: NSURL, addText: (newText: String) -> Void) {
let task = NSTask()
// Set up task
let pipe = NSPipe()
task.standardOutput = pipe
let outHandle = pipe.fileHandleForReading
outHandle.readabilityHandler = { pipe in
if let line = String(data: pipe.availableData, encoding: NSUTF8StringEncoding) {
addText(newText: line)
} else {
print("Error decoding data: \(pipe.availableData)")
}
}
task.launch()
}
You can call it like this:
convert(myURL) { newText in
print("New output available: \(newText)")
}
I made a function out of your class because there's no reason for a class when you're just gonna use it for a single function. The readabilityHandler approach also simplifies your code greatly.
I also added this way of getting updates to the mentioned question.