Executing a ViewModel function before setting variables in ViewController - ios

I have an issue with code order in a ItemsViewController.swift
When I run my code it starts the for items loop before my api returns the values for the items. This is done in the line: self.viewModel/getItemsTwo... Therefore it thinks that items is nil by the time the loop starts, so it errors with:
fatal error: unexpectedly found nil while unwrapping an Optional value
How can I start the loop only after items has been filled by the api call/function call?
class ItemsViewController: UIViewController {
private let viewModel : ItemsViewModel = ItemsViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
var items = self.viewModel.items
for item in items! {
print(item)
}
}
...
The getItemsTwo function in the viewModel sets the viewModel.items variable when it is called
EDIT 1
ItemsViewModel.swift
...
var items : JSON?
...
func getItemsTwo(user: MYUser) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
}
...
EDIT 2
I am trying to do this:
just change it in the ViewController to:
var items = self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
and the ViewModel to:
func getItemsTwo(user: MYUser) -> JSON {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
return self.items
}
But the return statement still errors as if self.items in nil.

Maybe you could expand your getItemsTwo method to take a callback closure, something like:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void)
Meaning that you have a parameter called callback which is a closure function that returns Void and takes an array of JSON items as an input parameter.
Once you have added newdata to self.items you could call your callback closure like so:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = new data
//Items are now populated, call callback
callback(items: self.items)
}
}
And then, in your ItemsViewController you could say:
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser()) { items in
for item in items {
print(item)
}
}
}
Notice that if you add a closure as the last parameter you can use a so called "Trailing Closure" and place it "outside" or "after" your function as described in this chapter of "The Swift Programming Language".
Hope that helps you (I haven't checked in a compiler so you might get some errors, but then well look at them OK :)).

Related

Extracting data from API (JSON format) doesn't save data outside of function call

I am trying to get an array of temperatures in a given time period from an API in JSON format. I was able to retrieve the array through a completion handler but I can't save it to another variable outside the function call (one that uses completion handler). Here is my code. Please see the commented area.
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "api address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
temps.append(tempF)
winds.append(windspeed)
}
}
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
I am calling this method from my view controller class. Here is the code.
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
}
//Here it prints out an empty array
print(weatherData)
}
The issue is that API takes some time to return the data, when the data is return the "Completion Listener" is called and it goes inside the "getWeather" method implementation, where it prints the data of array. But when your outside print method is called the API hasn't returned the data yet. So it shows empty array. If you will try to print the data form "weatherData" object after sometime it will work.
The best way I can suggest you is to update your UI with the data inside the "getWeather" method implementation like this:
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30") { (weatherhandler: [[Double]]) in
//It prints out the correct array here
print(weatherhandler[0])
weatherData = weatherhandler[0]
// Update your UI here.
}
//Here it prints out an empty array
print(weatherData)
}
It isn't an error, when your controller get loaded the array is still empty because your getWeather is still doing its thing (meaning accessing the api, decode the json) when it finishes the callback will have data to return to your controller.
For example if you were using a tableView, you will have reloadData() to refresh the UI, after you assign data to weatherData
Or you could place a property Observer as you declaring your weatherData property.
var weatherData:[Double]? = nil {
didSet {
guard let data = weatherData else { return }
// now you could do soemthing with the data, to populate your UI
}
}
now after the data is assigned to wheaterData, didSet will be called.
Hope that helps, and also place your jsonParsing logic into a `struct :)

Swift Function returning a value from asynchronous firebase call

I am writing a function that takes a groupchatID (String) and returns a list of Recipients ([String]) for that group chat. I am struggling with the asynchronous part of the function however. When I run the function, it correctly prints to the console the array of usernames I was looking for. Although, when I call the function and try to print the returned value, it is always an empty array because the function returns the array before the firebase call has finished. I am trying to use a callback, but I do not quite understand the syntax of it all. Please take a look and let me know what needs to be changed.
The Function:
func GetRecipientsFor(GroupChatID :String , completion: #escaping ([String]) -> ()) {
var returnArray: [String] = [""]
rootRef.child("chatMembers").child(GroupChatID).observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children.allObjects {
var append = child as! FIRDataSnapshot
returnArray.append((append.key as String))
print("Return Array Currently Contains: \(returnArray)")
//The above printout works properly and when the for loop finishes, the array is exactly as I want it
}
completion(returnArray)
//BUT, this portion returns an empty array
})
}
How I call the function:
GetRecipientsFor(GroupChatID: gchatID) { (result) -> () in
print(result)
}
NEW Function Call
var recipients : [String] = [""]
DispatchQueue.main.async {
GetRecipientsFor(GroupChatID: gchatID) { result in
print(result) //PRINTS CORRECTLY!!!
recipients = result
}
}
print(recipients) //PRINTS A BLANK ARRAY
The problem with
var recipients : [String] = [""]
DispatchQueue.main.async {
GetRecipientsFor(GroupChatID: gchatID) { result in
print(result)
recipients = result
}
}
print(recipients) // Completes before recipients = result
is that the last line is happening before the async call.
To explain futher print(recipients) happens before recipients = result. All logic using recipients needs to happen within that completion block. All you need to do is
func getRecipients(completion: #escaping ([String]) -> ()) {
var recipients : [String] = [""]
DispatchQueue.main.async {
GetRecipientsFor(GroupChatID: gchatID) { result in
print(result)
completion(result)
}
}
}
if you want to have further logic included you can call a function within the completion i.e. handleResults(result). I think it would be very beneficial to read more about closures/completion blocks/and async calls.
You also can simplify that and use the firebase observer async task adding other param to your function like this:
//controller is where you need to get the result
func GetRecipientsFor(GroupChatID :String , controller: UIViewController){
rootRef.observeSingleEvent(of: .value) { (snapshot) in
//here you haver your snapshot. do the stuff and
controller.setDataForRecipe(dataFromYourSnapshot)
}
}
And in your controller:
public func setDataForRecipe (arrayIngredients: [String]){
//whatever you want. example:
self.data = arrayIngredients
self.tableView.reloadData()
}

Swift: Lazy load variable vs MVC model?

I'm building an app with MVC Model.
I use lazy load technical to fill up a variable. (Model)
And this variable is being by one UIViewController (Controller)
But i don't know how to reload or trigger the view controller when the model action is finished. Here is my code
Model (lazy load data)
class func allQuotes() -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
}
return quotes
}
And the part of view controller
class Index:
UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
var quotes = IndexQuotes.allQuotes()
var collectionView:UICollectionView!
override func viewDidLoad() {
This is really serious question, i'm confusing what technic will be used to full fill my purpose?
Since Alamofire works asynchronously you need a completion block to return the data after being received
class func allQuotes(completion: ([IndexQuotes]) -> Void)
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for photoDict in (response.result.value as! [NSDictionary]) {
let photo = IndexQuotes(dictionary: photoDict)
quotes.append(photo)
}
}
completion(quotes)
}
}
Or a bit "Swiftier"
... {
let allPhotos = response.result.value as! [NSDictionary]
quotes = allPhotos.map {IndexQuotes(dictionary: $0)}
}
I'd recommend also to use native Swift collection types rather than NSArray and NSDictionary
In viewDidLoad in your view controller call allQuotes and reload the table view in the completion block on the main thread.
The indexQuotes property starting with a lowercase letter is assumed to be the data source array of the table view
var indexQuotes = [IndexQuotes]()
override func viewDidLoad() {
super.viewDidLoad()
IndexQuotes.allQuotes { (quotes) in
self.indexQuotes = quotes
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
First of all call the function from inside the viewdidLoad. Secondly use blocks or delegation to pass the control back to ViewController. I would prefer the blocks approch. You can have completion and failure blocks. In completions block you can reload the views and on failure you can use alertcontroller or do nothing.
You can see AFNetworking as an example for blocks.
It's async action, just use a callback here:
class func allQuotes(callback: () -> Void) -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
callback()
}
return quotes
}
In your UIViewController:
var quotes = IndexQuotes.allQuotes() {
self.update()
}
var collectionView:UICollectionView!
override func viewDidLoad() {
update()
}
private func update() {
// Update collection view or whatever.
}
Actually, I strongly don't recommend to use class functions in this case (and many other cases too), it's not scalable and difficult to maintain after some time.

Swift array does not change even thought using its reference (using inout) when passing it to a function

I call a function to get data from REST api and fill array of objects with result data, I use (inout parameter) as mentioned in apple documentation But the array does not change
My code
static func getData(tab : String, inout listItems : [Item], table: UITableView) -> (){
listItems.removeAll();
Alamofire.request(.GET, baseURL + currencyAPI, parameters: ["tab":tab]).responseArray { (response: Response<[Item], NSError>) in
let items = response.result.value
if let items = items {
listItems.appendContentsOf(items)
table.reloadData()
}
}
}
and function call
APIManager.getData("syp", listItems: &listItems, table: sypTable)
Update: when I put API request inside viewDidLoad function it works correctly
override func viewDidLoad() {
super.viewDidLoad()
//APIManager.getData("syp", listItems: &listItems, table: sypTable)
Alamofire.request(.GET, baseURL + currencyAPI, parameters: ["tab":"syp"]).responseArray { (response: Response<[Item], NSError>) in
let items = response.result.value
if let items = items {
self.listItems = items
self.sypTable.reloadData()
}
}
}

Empty return value from swift function containing closure

I created a function that should return a dictionary filled with data that are retrieved (using json, based on Ray Wenderlich tut) online. That code is in a closure. The problem is that an empty dictionary is returned first, and only afterwards it gets filled. Don't know if this is related to some delay in getting the remote data, but obviously I need the dictionary to be filled first before returning it. Here is the code.
func getStatusFromRemoteSource() -> [StatusModel] {
var statusUpdates = [StatusModel]()
println("statusUpdates after initialization: \(statusUpdates)") // 1
DataManager.getStatusDataWithSuccess { (statusData) -> Void in
let json = JSON(data: statusData)
if let jsonArray = json.array {
for jsonItem in jsonArray {
var statusVersion: String? = jsonItem["version"].string
var statusDescription: String? = jsonItem["message"].string
var statusCode: Int? = jsonItem["code"].string!.toInt()
var update = StatusModel(version: statusVersion, message: statusDescription, code: statusCode)
statusUpdates.append(update)
println("statusUpdates after appending update: \(statusUpdates)") // 3 (after other function call)
}
let item = 0
println("Version \(statusUpdates[item].version) has status \(statusUpdates[item].message)")
// println("Status code: \(statusUpdates[item].code)")
}
}
println("Status updates before return: \(statusUpdates)") // 2
return statusUpdates
}
So //1 prints first, then //2 (still empty) and then the other function (that calls this one) is called. Only then //3 is printed (correctly) with the content that should be retrieved.
How can I fill the statusUpdates dictionary before returning it?
You should use Closures in method to return statusUpdates as its Async method.
The empty statusUpdates will be returned immediately in your code but when using closures, you can wait till DataManager.getStatusDataWithSuccess is finished:
typealias RemoteStatusHandler = (status:[StatusModel]) -> Void
func getStatusFromRemoteSource(handler:RemoteStatusHandler){
var statusUpdates = [StatusModel]()
println("statusUpdates after initialization: \(statusUpdates)") // 1
DataManager.getStatusDataWithSuccess { (statusData) -> Void in
let json = JSON(data: statusData)
if let jsonArray = json.array {
for jsonItem in jsonArray {
var statusVersion: String? = jsonItem["version"].string
var statusDescription: String? = jsonItem["message"].string
var statusCode: Int? = jsonItem["code"].string!.toInt()
var update = StatusModel(version: statusVersion, message: statusDescription, code: statusCode)
statusUpdates.append(update)
println("statusUpdates after appending update: \(statusUpdates)") // 3 (after other function call)
}
let item = 0
println("Version \(statusUpdates[item].version) has status \(statusUpdates[item].message)")
// println("Status code: \(statusUpdates[item].code)")
}
handler(status: statusUpdates)
}
}
Then your function can be called like this:
getStatusFromRemoteSource { (status) -> Void in
//use status here, this function is void.
}

Resources