I can't change my view until my request and to find and fill my object.
I tried to put my code assync with GCD. That don't work
override func viewDidLoad() {
getHeartStroke()
NotificationCenter.default.addObserver(forName:NSNotification.Name("HeartStrokeNotification"), object: nil, queue: nil, using: notificationFinish)
}
func getHeartStroke() {
AF.request("http://localhost:8080/heartstroke", method: .get, headers: nil).responseJSON(completionHandler: {response in
if (response.error == nil)
{
let json = JSON(response.result.value!)
DispatchQueue.global(qos: .userInitiated).async {
guard let hearstrokeArray = try? JSONDecoder().decode([HeartStroke].self, from: json.rawData()) else{
debugPrint("An error has occurred")
return
}
NotificationCenter.default.post(name:NSNotification.Name("HeartStrokeNotification"), object: hearstrokeArray, userInfo: nil)
}
}else{
NotificationCenter.default.post(name:NSNotification.Name("HeartStrokeErrorNotification"), object: nil, userInfo: nil)
}
})
}
func notificationFinish(notification:Notification) -> Void{
if (notification.name.rawValue == "HeartStrokeNotification"){
arrayHeartstroke = notification.object as! [HeartStroke]
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
With this code I stay block on my page until the end of getHeartStroke(), I expect to navigate in my app in same time of the fetch.
What you need is a completion handler to handle this. Using the notification centre is just making your life difficult and complicated and could lead to unexpected behaviour. Here is some sample code:
func getHeartStroke(completionHandler: (_ heartStroke: [HeartStroke?], _ error: NSError?) -> ()) {
AF.request("http://localhost:8080/heartstroke", method: .get, headers: nil).responseJSON(completionHandler: {response in
if (response.error == nil)
{
let json = JSON(response.result.value!)
DispatchQueue.global(qos: .userInitiated).async {
guard let hearstrokeArray = try? JSONDecoder().decode([HeartStroke].self, from: json.rawData()) else{
debugPrint("An error has occurred")
return
}
completionHandler(hearstrokeArray, nil)
}
} else {
completionHandler(nil, response.error))
}
})
}
Then you can call it like so:
getHeartStroke { [weak self] (heartStrokeArray, error) in
guard let self = self else {return}
if error != nil {
self.processError(error)
} else {
self.processHeartStroke(heartStrokeArray)
}
}
processError and processHeartStroke will be functions that you should create to handle the heartStrokeArray and error objects.
These are standard callbacks or passing functions into functions. A lot of courses you find online seem to ignore callbacks but its definitely worth your time learning about them.
You can learn more about closures (completionHandler as one is named here) here: https://docs.swift.org/swift-book/LanguageGuide/Closures.html
Related
self.werteEintragen() should start after weatherManager.linkZusammenfuegen() is done. Right now I use DispatchQueue and let it wait two seconds. I cannot get it done with completion func because I dont know where to put the completion function.
This is my first Swift file:
struct DatenHolen {
let fussballUrl = "deleted="
func linkZusammenfuegen () {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString)
}
func perfromRequest(urlString: String)
{
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
if error != nil{
print(error!)
return
}
if let safeFile = gettingInfo {
self.parseJSON(datenEintragen: safeFile)
}
}
task.resume()
}
}
func parseJSON(datenEintragen: Data) {
let decoder = JSONDecoder()
do {
let decodedFile = try decoder.decode(JsonDaten.self, from: datenEintragen)
TeamOne = decodedFile.data[0].home_name
} catch {
print(error)
}
}
}
And this is my second Swift File as Viewcontroller.
class HauptBildschirm: UIViewController {
func werteEintragen() {
Tone.text = TeamOne
}
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [unowned self] in
self.werteEintragen()
}
}
}
How can I implement this and where?
func firstTask(completion: (_ success: Bool) -> Void) {
// Do something
// Call completion, when finished, success or faliure
completion(true)
}
firstTask { (success) in
if success {
// do second task if success
secondTask()
}
}
You can have a completion handler which will notify when a function finishes, also you could pass any value through it. In your case, you need to know when a function finishes successfully.
Here is how you can do it:
func linkZusammenfuegen (completion: #escaping (_ successful: Bool) -> ()) {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString, completion: completion)
}
func perfromRequest(urlString: String, completion: #escaping (_ successful: Bool) -> ()) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
guard error == nil else {
print("Error: ", error!)
completion(false)
return
}
guard let safeFile = gettingInfo else {
print("Error: Getting Info is nil")
completion(false)
return
}
self.parseJSON(datenEintragen: safeFile)
completion(true)
}
task.resume()
} else {
//can't create URL
completion(false)
}
}
Now, in your second view controller, call this func like this:
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen { [weak self] successful in
guard let self = self else { return }
DispatchQueue.main.async {
if successful {
self.werteEintragen()
} else {
//do something else
}
}
}
}
I highly recommend Google's Promises Framework:
https://github.com/google/promises/blob/master/g3doc/index.md
It is well explained and documented. The basic concept works like this:
import Foundation
import Promises
struct DataFromServer {
var name: String
//.. and more data fields
}
func fetchDataFromServer() -> Promise <DataFromServer> {
return Promise { fulfill, reject in
//Perform work
//This block will be executed asynchronously
//call fulfill() if your value is ready
//call reject() if an error occurred
fulfill(data)
}
}
func visualizeData(data: DataFromServer) {
// do something with data
}
func start() {
fetchDataFromServer
.then { dataFromServer in
visualizeData(data: dataFromServer)
}
}
The closure after "then" will always be executed after the previous Promise has been resolved, making it easy to fulfill asynchronous tasks in order.
This is especially helpful to avoid nested closures (pyramid of death), as you can chain promises instead.
I am trying to run a function X times in a for in loop but when all the functions have returned I want to run another function.
Currently I have it working by delaying the final function 1 second but I would really like to get Dispatch Group working.
I've been through various online examples and other questions but nothing I try seems to work, The code I have at the moment I know won't work as it is running dispatchGroup.leave() each time the for in functions are sent rather than when they return.
I've tried puting the DispatchGroup code in the function (which is in another file) but I'm stumped, however I think I am close to a solution.
I've also looked at semaphores and using count and incrementing a value each time the loop runs but I keep coming back to DispatchGroups.
My last resort is to ask a question!
ViewController code
#IBAction func removeDeviceBtn(_ sender: Any) {
let dispatchGroup = DispatchGroup()
for owner in arrOwnerList {
dispatchGroup.enter()
self.removeDevice(device: self.device, account: owner as! String, completion: self.completed)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
self.removeDeviceFromServer(device: self.device)
self.sendEmail(to:"gordon#example.co.uk", subject:self.device+" has been removed", text:self.device+" has been removed from the server, please check the sim for bar and termination")
})
Function code in other file as an extension
func completed(isSuccess: Bool) {
}
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader : [String:String] = ["username":Username,"password":Password]
let dictArray = [device]
WebHelper.requestPUTAPIRemoveDevice(BaseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { (response) in
if response.count == 0 {
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
}
}
else {
if response.count != 0 {
let isSuccess = true
completion(isSuccess)
}
else{
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.NoDataFound, on: self)
}
}
}
}) { (error) in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
}
}
}
Code from WebHelper file
class func requestPUTAPIRemoveDevice(_ strURL: String,header: Dictionary<String,String>,dictArray: Array<Any>, controllerView viewController: UIViewController, success: #escaping (_ response: [AnyHashable: Any]) -> Void, failure: #escaping (_ error: Error?) -> Void) {
if GlobalConstant.isReachable() {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.showHUD()
}
let loginString = String(format: "%#:%#", header["username"]!, header["password"]!)
let loginData: Data = loginString.data(using: String.Encoding.utf8)!
let base64LoginString = loginData.base64EncodedString(options: NSData.Base64EncodingOptions())
let headers = ["Authorization": "Basic "+base64LoginString, "Referer": "http://www.example.com"]
let postData = try? JSONSerialization.data(withJSONObject: dictArray, options: [])
let request = NSMutableURLRequest(url: NSURL(string: strURL)! as URL,
cachePolicy: .useProtocolCachePolicy,
timeoutInterval: 10.0)
request.httpMethod = "PUT"
request.allHTTPHeaderFields = headers
request.httpBody = postData
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
failure(error)
} else {
if let httpResponse = response as? HTTPURLResponse {
print("Server code \(httpResponse.statusCode)")
if httpResponse.statusCode == 200 || httpResponse.statusCode == 208 {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
let jsonResult = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers)
if (jsonResult is NSDictionary) {
success(jsonResult as! [AnyHashable : Any])
}
else if (jsonResult is NSArray) {
success(["response":jsonResult as! NSArray])
}
else{
success(["response":httpResponse.statusCode])
DispatchQueue.main.async {
}
}
}
else{
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
}
failure(error)
}
}
}
})
dataTask.resume()
}
else {
DispatchQueue.main.async {
LoadingIndicatorView.sharedInstance.hideHUD()
GlobalConstant.showAlertMessage(withOkButtonAndTitle: "", andMessage: "Internet not connected", on: viewController)
}
}
}
The final solution (apart from tidying up the various other issues) was to add success(["response":httpResponse.statusCode]) to the WebHelper file, corrected code above
Put the leave inside the completion handler:
for owner in arrOwnerList {
dispatchGroup.enter()
removeDevice(device: device, account: owner as! String) { [weak self] success in
self?.completed(isSuccess: success)
dispatchGroup.leave()
}
}
Or given that you’re not really doing anything in completed function, I’d just remove that:
for owner in arrOwnerList {
dispatchGroup.enter()
removeDevice(device: device, account: owner as! String) { _ in
dispatchGroup.leave()
}
}
I notice that you have paths of execution in removeDevice that aren’t calling the completion handler. Make sure every path of execution calls the completion handler or else your dispatch group will never get resolved.
func removeDevice(device: String, account: String, completion: #escaping (Bool) -> Void) {
let dictHeader = ["username": Username, "password": Password]
let dictArray = [device]
WebHelper.requestPUTAPIRemoveDevice(BaseURL+"rootaccount/removedevices/"+account+"?server=MUIR", header: dictHeader, dictArray: dictArray, controllerView: self, success: { response in
DispatchQueue.main.async {
if response.count == 0 {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: Messages.ServerError, on: self)
completion(false)
} else {
completion(true)
}
}
}, failure: { error in
DispatchQueue.main.async {
GlobalConstant.showAlertMessage(withOkButtonAndTitle: GlobalConstant.AppName, andMessage: error?.localizedDescription ?? Messages.ServerError, on: self)
completion(false)
}
})
}
By the way, I don’t know the name of the “failure” closure, so I assumed it was failure, but adjust as required by your requestPUTAPIRemoveDevice method. We generally avoid the multiple closure pattern in Swift, but if you’re going to do that, I’d avoid the trailing closure syntax. It makes the functional intent of this second closure a bit more explicit.
Or, this all begs the question as to why requestPUTAPIRemoveDevice is initiating UI updates at all. I’d probably put that in the view controller method. So requestPUTAPIRemoveDevice should just pass back enough information so the removeDeviceBtn routines knows what error to present. And this idea of presenting a separate error message for each failure is probably suspect, too. (E.g. if you have lost internet connection and are trying to remove a dozen devices, do you really want to show a dozen separate error messages?) But this is beyond the scope of this question.
I'm need to do a search operation in Swift and me using UISearchbar for it.
On textDidChange event, I need to call a web api, parse the response and then update the array and then begin the search on updated array.
But not sure my code does not work.
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let group = DispatchGroup()
group.enter()
// Perform some asynchronous operation
let queue1 = DispatchQueue(label: "abc")
queue1.async {
self.callWebAPI() // This function calls the web api and parses it’s response
group.leave()
}
DispatchQueue.global(qos: .utility).async {
DispatchQueue.main.async {
self.filteredCountry = self.arrCountry.filter({$0.name.prefix(searchText.count) == searchText})
self.searching = true
self.tableView.reloadData()
}
}
}
func callWebAPI() {
let urlString = URL(string: "https://restcountries.eu/rest/v2/all")
if let url = urlString {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
} else {
if let usableData = data {
do{
//here dataResponse received from a network request
let jsonResponse = try JSONSerialization.jsonObject(with:
data!, options: [])
print(jsonResponse) //Response result
guard let jsonArray = jsonResponse as? [[String: Any]] else {
return
}
print(jsonArray)
print("done")
} catch let parsingError {
print("Error", parsingError)
}
}
}
}
task.resume()
}
}
Please guide on my above code as not sure where I'm wrong
The issue is that callWebAPI is asynchronous (it returns immediately before the request is done), so you are calling leave immediately. You could give this method a completion handler and call leave in that. And you would also call the UI update in a notify block for your group, not just dispatch it.
Easier, just retire the DispatchGroup entirely and just update your UI in the completion handler you supply to callWebAPI.
For example, give callWebAPI a completion handler:
func callWebAPI(completionHandler: #escaping ([[String: Any]]?, Error?) -> Void) {
let urlString = URL(string: "https://restcountries.eu/rest/v2/all")
if let url = urlString {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, error == nil else {
completionHandler(nil, error)
return
}
do {
let jsonResponse = try JSONSerialization.jsonObject(with:
data)
completionHandler(jsonResponse as? [[String: Any]], nil)
} catch let parsingError {
completionHandler(nil, parsingError)
}
}
task.resume()
}
}
And then, you can eliminate the dispatch groups, the global queues (because it’s already an asynchronous method, you don’t need to invoke this from background queue), etc., and it’s simplified to just:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
callWebAPI { jsonResponse, error in
guard let jsonResponse = jsonResponse, error == nil else {
print("Error:", error ?? "Response was not correct format")
return
}
print(jsonResponse)
// Note, you don’t appear to be using `jsonResponse` at all,
// so I presume you’d update the relevant model objects.
DispatchQueue.main.async {
self.filteredCountry = self.arrCountry.filter({$0.name.prefix(searchText.count) == searchText})
self.searching = true
self.tableView.reloadData()
}
}
}
As an aside, nowadays we use JSONDecoder to parse JSON to populate model objects directly, but that’s beyond the scope of this question.
I'm attempting to print/dump and array of type Music outside of a function it's created in. I can successfully dump the musicItems array inside of the getMusicData function but when I set the musicItems array outside of the scope, it won't print anything. What am I doing wrong with the scope here? I have a feeling it's super simple but I just can't figure it out. Thanks in advance for taking the time to read this.
edit: It's giving me "0 elements" in the console when I attempt to dump the musicItems array in the ViewController class. Well, the function is in the same class as well so I guess I don't know what to call the first array. The parent array?
struct MusicResults: Decodable {
let results: [Music]?
}
struct Music: Decodable {
let trackName: String?
let collectionName: String?
let artworkUrl30: String?
}
class ViewController: UITableViewController, UISearchBarDelegate {
var musicItems: [Music] = []
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData()
dump(musicItems)
}
Here is the function.
func getMusicData() {
var musicItems: [Music] = []
guard let searchTerm = searchString else {return}
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
// print(results.trackName!)
musicItems.append(results)
}
//dump(musicItems)
self.musicItems = musicItems
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
Fixed Code
#IBAction func musicButton(_ sender: UIBarButtonItem) {
getMusicData {
music in
dump(music)
}
function:
func getMusicData(completionHandler: #escaping (_ music: [Music]) -> ()) {
...
let music = try JSONDecoder().decode(MusicResults.self, from: data)
for results in music.results! {
musicItems.append(results)
}
completionHandler(musicItems)
...
Your 'getMusicData' function is asynchronous which means that when it executes, it queues data task in a background queue and proceeds the execution and since there are no more institutions it simply returns control to its calling site - 'musicButton()' action, which in its turn executes the next instruction - prints the 'musicItems' array which might (and most likely, is) still not populated as the network call haven’t yet completed. One of the options that you have here is to pass a completion block to your 'getMusicData' function, that runs it after data task gets the results.
Another option is to use Property Observers
var musicItems: [Music] = [] {
didSet {
dump(self.musicItems)
/// This is where I would do the...
// DispatchQueue.main.async {
// self.tableView.reloadData()
// }
}
}
and then
func getMusicData() {
guard let searchTerm = searchString else { print("no search"); return }
let newString = searchTerm.replacingOccurrences(of: " ", with: "+", options: .literal, range: nil)
let jsonUrlString = "https://itunes.apple.com/search?media=music&term=\(newString)"
guard let url = URL(string: jsonUrlString) else { print("url error"); return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { print(err ?? "unknown"); return }
do {
let music = try JSONDecoder().decode(MusicResults.self, from: data)
if let results = music.results {
self.musicItems.append(contentsOf: results)
}
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
I got a getJSON Function with url parameter:
func getJsons(jsonUrl: String) {
guard let url = URL(string: jsonUrl) else { return }
URLSession.shared.dataTask(with: url:) { (data, response, err) in
if err != nil {
print("ERROR: \(err!.localizedDescription)")
let alertController = UIAlertController(title: "Error", message:
err!.localizedDescription, preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.default,handler: nil))
var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while ((topController.presentedViewController) != nil) {
topController = topController.presentedViewController!;
}
topController.present(alertController, animated: true, completion: nil)
}
guard let data = data else { return }
do {
let test = try JSONDecoder().decode([ArticleStruct].self, from: data)
DispatchQueue.main.async {
self.myArticles = test
print(self.myArticles?.count ?? 0 )
self.myTableView.reloadData()
}
} catch let jsonErr {
print("Error:", jsonErr)
}
}.resume()
}
now i want to move the function to another class (network class).
what must I do to add a completionHandler to the function and how do I call it from other classes.
i want to return the json to the caller class.
My plan:
in MainActivity -> viewDidLoad: call network completionHandler(getJsons(192.168.178.100/getPicture.php))
on completion -> myJsonDataMainActivity = (json data from completionHandler)
-> MainActivity.TableView.reload
in otherClass -> call network completionHandler(getJsons(192.168.178.100/getData.php))
on completion -> myJsonDataOtherClass = (json data from completionHandler)
-> otherClass.TableView.reload
Thanks for your help!
You can use delegate.
myJsonDataOtherClass:
protocol NetworkDelegate {
func didFinish(result: Data)
}
class myJsonDataOtherClass {
var delegate: NetworkDelegate? = nil
...
func getJsons(jsonUrl: String) {
...
URLSession.shared.dataTask(with: url:) { (data, response, err) in
...
delegate?.didFinish(data)
}.resume()
}
}
and set delegate at MainActivity
class MainActivity: UIViewController, NetworkDelegate{
...
let jsonClass = myJsonDataOtherClass()
jsonClass.delegate = self
jsonClass.getJsons(jsonUrl:url)
func didFinish(result:Data) {
// process data
}
}
You should add a completion handler in your function and pass the JSON object.
func getJsons(jsonUrl: String, completion:#escaping (_ success: Bool,_ json: [String: Any]?) -> Void) {
...
completion(true, json)
}