Let me preface this by saying I'm VERY new to Swift 2 and am building my first app which calls an api (php) for data (JSON). The problem I'm running into is when I make the call to the api the other functions ran before the api can send back the data.
I've researched some type of a onComplete to call a functions after the api response is done. I'm sure for most of you this is easy, but I cant seem to figure it our.
Thanks in advance!
class ViewController: UIViewController {
var Selects = [Selectors]()
var list = [AnyObject]()
var options = [String]()
var index = 0
#IBOutlet var Buttons: [UIButton]!
override func viewDidLoad() {
super.viewDidLoad()
self.API()
self.Render()
}
func API() {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
print("request failed \(error)")
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
}
}
task.resume()
}
func BuildOptions() {
// BuildOptions stuff happens here
}
func Render() {
// I do stuff here with the data
}
}
So I assume your Render() method is called before data gets back from the api? Keeping your api-calling code in the view controllers is bad design, but as you're new i won't expand on that. In your case it's as simple as not calling your Render() method in viewDidLoad() - call it after you're done with parsing the data from JSON (after the self.Selects = [Selectors... line). NSURLSession.sharedSession().dataTaskWithRequest(request) method is called asynchronously , and the callback block with data, response, error parameters is executed after this method is done with fetching your data, so it can happen after the viewDidLoad is long done and intially had no data to work on as the asynchronous method was still waiting for response from the API.
Edit - speaking of handling api calls, it's a wise thing to keep them separated from specific view controllers to maintain a clean reusable code base. You should call the API and wait for a callback from it, so i'd just do that to your API function, it would look like this:
static func callAPI(callback: [AnyObject]? -> Void ) {
let url = NSURL(string: "http:api.php")
let request = NSMutableURLRequest(URL: url!)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
if data == nil {
completion(nil)
}
do {
var list = [AnyObject]()
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let songs = json["songs"] as? [[String: AnyObject]] {
for song in songs {
self.list.append(song)
}
}
completion(list)
}
catch let error as NSError {
print("json error: \(error.localizedDescription)")
completion(nil)
}
}
task.resume()
}
Generally speaking methods should do one specific thing - in your case call the api and return data or error. Initialize your selectors in the view controllers on callback. Your view controller's viewDidLoad would look like this using the code above:
override func viewDidLoad() {
super.viewDidLoad()
YourApiCallingClass.callApi() {
result in
if let list = result {
self.list = list
self.Selects = [Selectors(Name: self.list[self.index]["name"] as? String, Options: self.BuildOptions(), Correct: 2)]
self.Render()
} else {
//Handle situation where no data will be returned, you can add second parameter to the closue in callApi method that will hold your custom errors just as the dataTaskWithRequest does :D
}
}
}
Now you have a nice separation of concerns, API method is reusable and view controller just handles what happens when it gets the data. It'd be nice if you slapped an UIActivityIndicator in the middle of the screen while waiting, it'd look all neat and professional then :P
Related
I'm creating a basic application in swift where I get data from an api and show it. My Model class handles the fetching data part and I use a delegate to communicate data with the ViewController.
However when fetched data is passed into the controller it is nill for some reason.
Here are the things I know/have tried.
Extracting the JSON is working. The data is correctly fetched.
I tried using viewWillAppear as well as viewDidLoad
I have set the delegate to self in the View Controller
Abstracted code is below.
//Model Class
protocol DataDetailDelegate {
func datadetailFetched(_ datadetail: DataDetailItem)
}
class Model {
var datadetaildelegate: DataDetailDelegate?
func fetchData(ID: String) {
let url = URL(string: Constants.Data_URL + ID)
guard url != nil else {
print("Invalid URL!")
return
}
let session = URLSession.shared.dataTask(with: url!) { data, response, error in
if error != nil && data == nil {
print("There was a data retriving error!")
return
}
let decoder = JSONDecoder()
do {
let response = try decoder.decode(MealDetail.self, from: data!)
if response != nil {
DispatchQueue.main.async {
self.datadetaildelegate?.datadetailFetched(response)
}
}
} catch {
print(error.localizedDescription)
}
}
session.resume()
}
}
//View Controller
import UIKit
class DetailViewController: UIViewController, DataDetailDelegate {
#IBOutlet weak var instruction: UITextView!
var model = Model()
var datadetail : DataDetailItem?
override func viewDidLoad() {
super.viewDidLoad()
model.datadetaildelegate = self
model.fetchData(ID: data.id)
if self.datadetail == nil {
print("No data detail available")
return
}
self.instruction.text = datadetail.instruction
}
func datadetailFetched(_ datadetail: DataDetailItem) {
self.datadetail = datadetail
}
}
As mentioned in the comments, there are many problems with the above code, but the main one being not recognising the asynchronous nature of the fetchData.
The below is a rough solution that addresses the critical issues with your code, rather than being best practice and addressing all of them. For example you could also check the contents of error and response codes, and pass them back through the completion handler (maybe as a Result type).
For starters, streamline your fetchData so that it gets the data and then handles that data via a completion handler in an asynchronous manner. Also, don't call it a Model as it's really not.
class SessionManager {
func fetchData(withID id: String, completion: #escaping (data) -> Void) {
guard let url = URL(string: Constants.Data_URL + ID) else {
print("Invalid URL!")
return
}
let session = URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil, let data = data else { //could be done far better by checking error and response codes
print("There was a data retriving error!")
return
}
completion(data)
}
session.resume()
}
Then adapt your view conroller so that it creates the session manager and fetches the data, but processes it in the completion handler. I've only added the relevant content.
class DetailViewController: UIViewController, DataDetailDelegate {
lazy var sessionManager = SessionManager()
var datadetail : DataDetailItem?
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.fetchData(withID: data.id){ [weak self] data in
let decoder = JSONDecoder()
do {
let response = try decoder.decode(MealDetail.self, from: data)
DispatchQueue.main.async {
self?.datadetail = response
self?.instruction.text = datadetail.instruction
}
} catch {
print(error.localizedDescription)
}
}
}
// rest of view controller
}
That should be enough to get you going. There are many best practice examples/tutorials scattered around that would be worth a look so you can refine things further.
Note: this has been written in here without a compiler to hand, so there may be some syntax that needs tweaking.
I am loading some JSON data into a UITableView. Each cell has a delete button and when this button is pressed the object is successfully deleted from the server. However, the page doesn't update to reflect this change. I want the table view to reload but without the deleted object. I have tried calling viewDidLoad() and viewDidAppear() as well as several other tricks I've found online. Currently, I am using a workaround that sends the user back to the home page. From there when you click on the page with the table view, it has updated to reflect the changes. However, I cannot seem to make this happen without leaving the view controller and coming back to it. I know that the API calls are working correctly, the page just isn't "refreshing". What should I try to make this work?
Thank you very much! My code for the entire class is below (I've added a couple comments to point out the places I am having problems with)
import UIKit
class Garage: UIViewController, UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.vehicles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell1", for: indexPath) as UITableViewCell
cell.textLabel?.text = vehicles[indexPath.row].year + " " + vehicles[indexPath.row].make + " " + vehicles[indexPath.row].model
let button = UIButton(type: .custom)
button.backgroundColor = UIColor.green
button.sizeToFit()
cell.accessoryType = .detailButton
return cell
}
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
print("tapped")
let alert = UIAlertController(title: "Would you like to delete this vehicle?", message: "This action cannot be undone.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
self.present(alert, animated: true)
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { action in
print("DELETED")
//
//THIS IS WHERE THE DELETE ACTION IS CALLED
//
var id = self.vehicles[indexPath.row].id
print("got here 1")
let api = "https://api.myapi.com/"
let parameters: [String: String] = ["id": id]
guard let url = URL(string: api + id) else { return }
var request = URLRequest(url: url)
let headers: [String: String] = [
"Content-Type": "application/json"
]
request.allHTTPHeaderFields = headers
request.httpMethod = "DELETE"
let requestBody = try? JSONSerialization.data(withJSONObject: parameters, options: [])
if let requestBody = requestBody {
request.httpBody = requestBody
}
//
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
self.viewDidLoad()
self.viewWillAppear(true)
URLSession.shared.dataTask(with: request) { (data, response, error) in
// print("Data, response, error", data, response, error)
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
print("json", json)
}
DispatchQueue.main.async {
self.tableVehicles.reloadData()
self.view.setNeedsLayout()
self.performSegue(withIdentifier: "GarageToHome", sender: self)
}
}.resume()
//
//THIS IS MY CURRENT WORK-AROUND
//
self.performSegue(withIdentifier: "GarageToHome", sender: self)
}))
}
#IBOutlet weak var label1: UILabel!
struct Vehicle {
var make: String
var model: String
var year: String
var Trim: String
var id: String
init(_ dictionary: [String: Any]) {
self.make = dictionary["make"] as? String ?? ""
self.model = dictionary["model"] as? String ?? ""
self.year = dictionary["year"] as? String ?? ""
self.Trim = dictionary["trim"] as? String ?? ""
self.id = dictionary["id"] as? String ?? ""
}
}
var vehicles = [Vehicle]()
#IBOutlet weak var tableVehicles: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// print("login name:", username)
self.tableVehicles.delegate = self
self.tableVehicles.dataSource = self
self.tableVehicles.reloadData()
}
override func viewWillAppear(_ animated: Bool) {
//vehicles = [Vehicle]()
let defaults = UserDefaults.standard
defaults.synchronize()
let token = UserDefaults.standard.string(forKey: "token")
let isSignedIn = UserDefaults.standard.bool(forKey: "isUserLoggedIn")
var username = UserDefaults.standard.string(forKey: "loginName")
defaults.synchronize()
DispatchQueue.main.async {
self.label1.text = "Welcome, " + username! + "!"
defaults.synchronize()
}
guard let url = URL(string: "https://api.myapi.com") else {return}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data,
error == nil else {
print(error?.localizedDescription ?? "Response Error")
return }
do{
let jsonResponse = try JSONSerialization.jsonObject(with:
dataResponse, options: [])
//print(jsonResponse) //Response result
guard let jsonArray = jsonResponse as? [[String: Any]] else {
return
}
for dic in jsonArray{
self.vehicles.append(Vehicle(dic))
}
print(self.vehicles)
DispatchQueue.main.async {
self.tableVehicles.delegate = self
self.tableVehicles.dataSource = self
self.tableVehicles.reloadData()
}
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
}
}
First of all, never ever call delegate methods containing will, did and should yourself. Don't do that. Those methods are exclusively called by the framework.
There is a quite easy solution. You know the index path of the deleted row so if the data task succeeds remove the item from the data source array and delete the row in the table view. Reloading the entire table view and updating the data source array from the downloaded data is not necessary.
Anyway you need a clear indicator that the server operation was successful, I don't know if if let data = data in the dataTask is sufficient.
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
URLSession.shared.dataTask(with: request) { (data, response, error) in
// print("Data, response, error", data, response, error)
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
print("json", json)
DispatchQueue.main.async {
self.vehicles.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
self.performSegue(withIdentifier: "GarageToHome", sender: self)
}
}
}.resume()
While you are calling both -viewDidLoad and -viewWillAppear(which are reloading your table view and performing your API request to fetch your data and populate your model respectively) one reason you might not be seeing your underlying data model appear to update is that you are calling both before you actually try to send the deletion request to your API endpoint. If you move your call to -viewWillAppear into the completion block of your delete request then that will ensure that your request to fetch the updated data from your API occurs after the deletion is finished.
That is the very least you could do to potentially get this working like you expect, however I would strongly recommend taking a little time to factor out your business and networking logic. You should not manually be calling -viewDidLoad or -viewWillAppear yourself. These are UIViewController lifecycle methods that are called by UIKit for you during the lifecycle of your view controller. Instead I'd recommend (at the very least) factoring out some methods or methods into other classes that handle things like fetching your data model from the server or deleting specific records. Then, you can call those methods from your table view delegate or UIAlertAction in a more re-usable and isolated manner without worrying about any unintended side effects of calling your UIViewController lifecycle methods just to fetch some data again.
So, while I really don't recommend you end up with this as your final code, instead of this:
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
self.viewDidLoad()
self.viewWillAppear(true)
URLSession.shared.dataTask(with: request) { (data, response, error) in
You could do this:
URLSession.shared.dataTask(with: request) { (data, response, error) in
//THIS IS WHERE I TRY TO RELOAD THE PAGE
//
print("reloading...")
self.viewDidLoad()
self.viewWillAppear(true)
Again, I highly recommend, at the very least, extracting your fetch and delete API logic.
You also have several options for how you go about performing the delete action and making it appear to the user that the record was deleted.
As described above, the minimal changes required to what you have would just to ensure that your updated data model is only fetched once the deletion request succeeds.
Alternatively you can optimistically make it appear that your delete request has succeeded by updating your backing data model locally and either deleting the affected row or reloading your table.
Since the former approach is subject to connectivity and networking latency issues it may appear really laggy to the user when they try to delete a record and you lose the ability to take advantage of UITableView animations that make it clear that a row has been deleted.
The latter approach will appear more responsive to the user. For this approach you would need to remove deleted object from your array where you are currently calling -viewDidLoad and -viewWillAppear: and then remove the corresponding row. Something like:
self.vehicles.removeAt(indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
You have a choice of animation types.
One challenge of optimistically deleting the record and row locally in your app is that if the API request fails for some reason then your user experience could appear inconsistent the next time your data model is fetched - it'll appear as if the record was not deleted (which is actually the case). So depending on the importance of consistency and how much error handling you care to do then you'll need to reconcile any errors that come about as part of your deletion request. Here again, you have many options. You can try the delete again behind the scenes, you could alert your user and then add the row back so they can try again, or just do nothing.
By using RxSwift, the purpose of my project is whenever an user types a city in search bar, it will make a call to wrap the current temperature. Currently, I have viewModel which contains
var searchingTerm = Variable<String>("") // this will be binded to search text from view controller
var result: Observable<Weather>! // this Observable will emit the result based on searchingTerm above.
In api service, I'm wrapping a network call using RxSwift by following
func openWeatherMapBy(city: String) -> Observable<Weather> {
let url = NSURL(string: resourceURL.forecast.path.stringByReplacingOccurrencesOfString("EnterYourCity", withString: city))
return Observable<WeatherModel>.create({ observer -> Disposable in
let downloadTask = self.session.dataTaskWithURL(url!, completionHandler: { (data, response, error) in
if let err = error {
observer.onError(err)
}
else {
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as! [String: AnyObject]
let weather = Weather(data: json)
observer.onNext(weather)
observer.onCompleted()
}
catch {
}
}
})
downloadTask.resume()
return AnonymousDisposable {
downloadTask.cancel()
}
})
}
As long as the model created, I'll send it to an observer and complete
At view controller, I'm doing
viewModel.result
.subscribe( onNext: { [weak self] model in
self?.weatherModel = model
dispatch_async(dispatch_get_main_queue(), {
self?.cityLabel.text = model.cityName
self?.temperatureLabel.text = model.cityTemp?.description
})
},
onError: { (error) in
print("Error is \(error)")
},
onCompleted:{
print("Complete")
}
)
{ print("Dealloc")}
.addDisposableTo(disposeBag)
}
It works as expected, UI is updated and show me what I want. However, I have just realized that onCompleted never gets called. I assume if I do everything right, I must have it printed out.
Any ideas about this issue. All comments are welcomed here.
result seems to be derived from searchingTerm, which is a Variable.
Variable only complete when they are being deallocated (source) so it makes sense that result does not receive onCompleted.
It makes sense that the behavior is this one. An observable will never emit new values after onCompleted. And you don't want it to stop updating after the first search result is presented.
I am working on a simple Flickr app that gets some data from their API and displays it on a tableview instance. Here's a piece of the code for the TableViewController subclass.
var photos = [FlickrPhotoModel]()
override func viewDidLoad() {
super.viewDidLoad()
getFlickrPhotos()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
private func getFlickrPhotos() {
DataProvider.fetchFlickrPhotos { (error: NSError?, data: [FlickrPhotoModel]?) in
//data is received
dispatch_async(dispatch_get_main_queue(), {
if error == nil {
self.photos = data!
self.tableView.reloadData()
}
})
}
}
The application does not seem to load the data if the { tableView.reloadData() } line is removed. Does anyone know why this would happen since I call getFlickrPhotos() within viewDidLoad(). I believe I am also dispatching from the background thread in the appropriate place. Please let me know what I am doing incorrectly.
EDIT -- Data Provider code
class func fetchFlickrPhotos(onCompletion: FlickrResponse) {
let url: NSURL = NSURL(string: "https://api.flickr.com/services/rest/?method=flickr.photos.getRecent&api_key=\(Keys.apikey)&per_page=25&format=json&nojsoncallback=1")!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) in
if error != nil {
print("Error occured trying to fetch photos")
onCompletion(error, nil)
return
}
do {
let jsonResults = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as? NSDictionary
let photosContainer = jsonResults!["photos"] as? NSDictionary
let photoArray = photosContainer!["photo"] as? [NSDictionary]
let flickrPhoto: [FlickrPhotoModel] = photoArray!.map{
photo in
let id = photo["id"] as? String ?? ""
let farm = photo["farm"] as? Int ?? 0
let secret = photo["secret"] as? String ?? ""
let server = photo["server"] as? String ?? ""
var title = photo["title"] as? String ?? "No title available"
if title == "" {
title = "No title available"
}
let model = FlickrPhotoModel(id: id, farm: farm, server: server, secret: secret, title: title)
return model
}
//the request was successful and flickrPhoto contains the data
onCompletion(nil, flickrPhoto)
} catch let conversionError as NSError {
print("Error parsing json results")
onCompletion(conversionError, nil)
}
}
task.resume()
}
I'm not familiar with that API, but it looks like the fetchFlickrPhotos method is called asynchronously on a background thread. That means that the rest of the application will not wait for it to finish before moving on. viewDidLoad will call the method, but then move on without waiting for it to finish.
The completion handler that you provide is called after the photos are done downloading which, depending on the number and size of the photos, could be seconds later. So reloadData is necessary to refresh the table view after the photos are actually done downloading.
I want to get the result of a webservice call as a block callback so I have added the method fetchOpportunities below to my Opportunity model class now I want to call this method from my UIViewController like that:
class HomeViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
Opportunity.fetchOpportunities(
success (data) {
}
)
}
}
I guess blocks is not present in Swift but how to replicate a similar behavior ? The call is asynchronous but I need to get the data when the call is completed in my TableViewController to update the TableViewand it can't work with my actual implementation in Objective-C I used to use blocks but what to do with Swift2
Here is the implementation of fetchOpportunities
class func fetchOpportunities() {
let urlPath = "http://www.MY_API_HERE.com"
guard let endpoint = NSURL(string: urlPath) else { print("Error creating endpoint");return }
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) -> Void in
do {
guard let dat = data else { throw JSONError.NoData }
guard let json = try NSJSONSerialization.JSONObjectWithData(dat, options: []) as? NSDictionary else { throw JSONError.ConversionFailed }
var ops = [Opportunity]()
if let dataArray = json["data"] as? [[String:AnyObject]] {
for op in dataArray {
ops.append( Opportunity(op) )
}
}
print(ops)
} catch let error as JSONError {
print(error.rawValue)
} catch {
print(error)
}
}.resume()
}
Just use closure. The method declaration will be:
class func fetchOpportunities(callback: ([Opportunity]) -> ()) {
// Do all the work here and then send the data like :
callback(ops)
}
In UIViewController:
Opportunity.fetchOpportunities { (data) -> () in
print(data)
}
Efficient JSON in Swift with Functional Concepts and Generics
Learn NSURLSession using Swift Part 1