onCompleted never gets called - ios

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.

Related

Delegate returns nill in Swift (Data flow between Model and ViewController)

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.

Completion Handler not working Properly in swift

im trying to populate two arrays with the data i get from the firestore database. im getting the data successfully however it was late and when i printed them in viewDidLoad it printed empty arrays. so i decided to implement a completion handler however it still shows and empty array. can anyone tell me why my print statement runs before the functions even though im using escaping
func yourFunctionName(finished: #escaping () -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries.append(document.documentID)
}
}
}
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries2.append(document.documentID)
}
}
}
finished()
}
viewDidLoad(){
yourFunctionName {
print(self.countries)
print(self.countries2)
}
}
i get the empty arrays in the output although the arrays should have been filled before i called print though im using #escaping. please someone help me here
You are actually not escaping the closure.
For what I know the "#escaping" is a tag that the developper of a function use to signify the person using the function that the closure he/she is passing will be stored and call later (after the function ends) for asynchronicity and memory management. In your case you call the closure passed immediately in the function itself. Hence the closure is not escaping.
Also the firebase database is asynchronous. Meaning that you don't receive the result immediately
This part :
{ (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
self.countries.append(document.documentID)
}
}
}
is itself a closure, that will be executed later when the result of the query is produced. As you can see in the doc, the function is escaping the closure : https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/Query.html#getdocumentssource:completion:
func getDocuments(source: FirestoreSource, completion: #escaping FIRQuerySnapshotBlock)
So to summarise :
The code for the firebase query will be call later (but you don't know when), and your closure "finished" is called immediately after having define the firebase callback, thus before it has been called.
You should call your finished closure inside the firebase callback to have it when the arrays are populated.
I think your main problem here is not about to populate your arrays, your problem is how to get it better.
I did an example of how you could do that in a better way.
First, break your big function in two, and populate it out of your function.
Look at this code and observe the viewDidLoad implementation.
func countries(withCapital capital: String, completionHandler: (Result<Int, Error>) -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: capital)
.getDocuments { (snapshot, error) in
guard error == nil else {
completionHandler(.failure(error!))
return
}
let documents = snapshot!.documents
let ids = documents.map { $0.documentID }
completionHandler(.success(ids))
}
}
func countries(withClimate climate: String, completionHandler: (Result<Int, Error>) -> Void) {
db.collection("countries")
.whereField("climate", isEqualTo: climate)
.getDocuments { (snapshot, error) in
guard error == nil else {
completionHandler(.failure(error!))
return
}
let documents = snapshot!.documents
let ids = documents.map { $0.documentID }
completionHandler(.success(ids))
}
}
func viewDidLoad(){
countries(withClimate: "pleasant") { (result) in
switch result {
case .success(let countries):
print(countries)
self.countries2 = countries
default:
break
}
}
countries(withCapital: "washington") { (result) in
switch result {
case .success(let countries):
print(countries)
self.countries = countries
default:
break
}
}
}
If you have to call on main thread call using it
DispathQueue.main.async {
// code here
}
I hope it helped you.
They are returning empty arrays because Firebase's function is actually asynchronous (meaning it can run after the function "yourFunctionName" has done its work)
in order for it to work as intended (print the filled arrays)
All you need to do is call it inside Firebase's closure itself, like so:
func yourFunctionName(finished: #escaping () -> Void) {
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
self.countries.append(document.documentID)
finished() //<<< here
}
}
}
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
self.countries2.append(document.documentID)
finished() //<<< and here
}
}
}
}
I has been sometime since I encountered that problem but I beleve the issue is that you are calling the completion handler to late. What I mean is that you can try to call it directly after you have lopped throught the documents. One idea could be to return it in the compltion or just do as you do. Try this instead:
func yourFunctionName(finished: #escaping ([YourDataType]?) -> Void) {
var countires: [Your Data Type] = []
db.collection("countries")
.whereField("capital", isEqualTo: "washington")
.getDocuments { (snapshot, error) in
if error == nil{
for document in snapshot!.documents {
let documentData = document.data()
//print(document.documentID)
//print(documentData)
countries.append(document.documentID)
}
finished(countries)
return
}
}
}
func yourSecondName(finished: #escaping([YouDataType]?) -> Void) {
var countries: [Your data type] = []
db.collection("countries")
.whereField("climate", isEqualTo: "pleasant")
.getDocuments { (snapshot, error) in
if error == nil {
for document in snapshot!.documents{
let documentData = document.data()
//print(document.documentID)
//print(documentData)
countires.append(document.documentID)
}
finished(countires)
return
}
}
func load() {
yourFunctionName() { countries in
print(countires)
}
yourSecondName() { countries in
print(countries)
}
}
viewDidLoad(){
load()
}
What this will do is that when you call the completion block that is of type #escaping as well as returning after it you won't respond to that completely block any longer and therefore will just use the data received and therefore not care about that function anymore.
I good practice according to me, is to return the object in the completion block and use separate functions to be easier to debug and more efficient as well does it let you return using #escaping and after that return.
You can use a separate method as I showed to combine both methods and to update the UI. If you are going to update the UI remember to fetch the main queue using:
DispathQueue.main.async {
// Update the UI here
}
That should work. Greate question and hope it helps!

Firestore: Calls with of async function with SnapshotListener and in a cycle with DispatchGroup cause a crash

I have an issue with using DispatchGroup (as it was recommended here) with FireStore snapshotListener
In my example I have two functions. The first one is being called by the ViewController and should return array of objects to be displayed in the View.
The second one is a function to get child object from FireStore for each array member. Both of them must be executed asynchronously. The second one should be called in cycle.
So I used DispatchGroup to wait till all executions of the second function are completed to call the UI update. Here is my code (see commented section):
/// Async function returns all tables with active sessions (if any)
class func getTablesWithActiveSessionsAsync(completion: #escaping ([Table], Error?) -> Void) {
let tablesCollection = userData
.collection("Tables")
.order(by: "name", descending: false)
tablesCollection.addSnapshotListener { (snapshot, error) in
var tables = [Table]()
if let error = error {
completion (tables, error)
}
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let firebaseID = document.documentID
let tableName = data["name"] as! String
let tableCapacity = data["capacity"] as! Int16
let table = Table(firebaseID: firebaseID, tableName: tableName, tableCapacity: tableCapacity)
tables.append(table)
}
}
// Get active sessions for each table.
// Run completion only when the last one is processed.
let dispatchGroup = DispatchGroup()
for table in tables {
dispatchGroup.enter()
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(tables, nil)
}
}
}
/// Async function returns table session for table or nil if no active session is opened.
class func getActiveTableSessionAsync (forTable table: Table, completion: #escaping (TableSession?, Error?) -> Void) {
let tableSessionCollection = userData
.collection("Tables")
.document(table.firebaseID!)
.collection("ActiveSessions")
tableSessionCollection.addSnapshotListener { (snapshot, error) in
if let error = error {
completion(nil, error)
return
}
if let snapshot = snapshot {
guard snapshot.documents.count != 0 else { completion(nil, error); return }
// some other code
}
completion(nil,nil)
}
}
Everything works fine till the moment when the snapshot is changed because of using a snapshotListener in the second function. When data is changed, the following closure is getting called:
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
dispatchGroup.leave()
})
And it fails on the dispatchGroup.leave() step, because at the moment group is empty.
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
All dispatchGroup.enter() and dispatchGroup.leave() are already done on this step. And this closure was called by listener separately.
I tried to find the way how to check if the DispatchGroup is empty to do not call leave() method. But did not find any native solution.
The only similar solution I've found is in the following answer. But it looks too hacky and not sure if will work properly.
Is there any way to check if DispatchGroup is empty? According to this answer, there is no way to do it. But probably something changed during last 2 years.
Is there any other way to fix this issue and keep snapshotListener in place?
For now I implemented some kind of workaround solution - to use a counter.
I do not feel it's the best solution, but at least work for now.
// Get active sessions for each table.
// Run completion only when the last one is processed.
var counter = tables.count
for table in tables {
DBQuery.getActiveTableSessionAsync(forTable: table, completion: { (tableSession, error) in
if let error = error {
completion([], error)
return
}
table.tableSession = tableSession
counter = counter - 1
if (counter <= 0) {
completion(tables, nil)
}
})
}

IOS Swift How can I get value from an Async method [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 4 years ago.
I am new to Swift and was wondering how can I get a value from an Async task. I have a function that gets Json data from an API on return I would like to get the value of a particular field outside of the async task... My code is below essentially I have a variable called status I want to get the value of status after the async called is returned then I want to check if the value is 1 . In the code below the value returned is 1 however it seems like the async called is executed before the line if Status == 1 {} . If the value is One then I want to navigate to a different ViewController . Any suggestions would be great ... I obviously can not put the code to go to a different ViewController inside the Async code since that is called many times .
func GetData() {
var status = 0
// Code that simply contains URL and parameters
URLSession.shared.dataTask(with:request, completionHandler: {(data, response, error) in
if error != nil {
print("Error")
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String:Any]
DispatchQueue.main.async {
if let Replies = parsedData["Result"] as? [AnyObject] {
for Stream in Replies {
if let myvalue = Stream["status"] as? Int {
status = myvalue
}
}
}
}
} catch let error as NSError {
print(error)
}
}
}).resume()
if status == 1 {
// This code is executed before the async so I don't get the value
let nextViewController = self.storyboard?.instantiateViewController(withIdentifier: "Passed") as! Passed
self.present(nextViewController, animated:false, completion:nil)
}
}
You can use a callback function for that like this:
func GetData(callback: (Int) -> Void) {
//Inside async task, Once you get the values you want to send in callback
callback(status)
}
You will get a callback from where you called the function.
For your situation, Anbu's answer will work too.

Swift 2 api call takes longer than other functions run

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

Resources