Why global variable not updating value in swift 3? - ios

This is the class where the value is
class CurrentWeather{
var _date:String!
var _cityName:String!
var _temp:Double!
var _weatherType:String!
var cityName:String{
if _cityName==nil{
_cityName = ""
}
return _cityName
}
var currentTemprature:Double{
if _temp==nil{
_temp = 0.0
}
return self._temp
}
var weathertype:String{
if _weatherType==nil{
_weatherType = ""
}
return _weatherType
}
var date:String{
if _date==nil{
_date = ""
}
let dateFormater=DateFormatter()
dateFormater.dateStyle = .long
dateFormater.timeStyle = .none
let currentDate = dateFormater.string(from: Date())
self._date="\(currentDate)"
return _date
}
func weatherDataDownload(completed : downloadComplete){
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON{response in
if let dict=response.result.value as? Dictionary<String,AnyObject>{
if let name=dict["name"] as? String{
self._cityName = name.capitalized
print(name.capitalized)
}
if let weather=dict["weather"] as? [Dictionary<String,AnyObject>]{
if let main=weather[0]["main"] as? String{
self._weatherType=main.capitalized
print(main.capitalized)
}
}
if let tempr=dict["main"] as? Dictionary<String,AnyObject>{
if let temp=tempr["temp"] as? Double{
let convertedTemp=Double(round(temp-273.15))
self._temp=convertedTemp
print(convertedTemp)
}
}
}
}
completed()
}}
This is the ViewController class
var currentWeatherOj = CurrentWeather()
override func viewDidLoad() {
super.viewDidLoad()
table.delegate=self
table.dataSource=self
currentWeatherOj.weatherDataDownload {
self.updateUIweather()
}
}
func updateUIweather () {
weatherType.text=currentWeatherOj.weathertype
presentDate.text=currentWeatherOj.date
presentLocation.text=currentWeatherOj.cityName
presentTemp.text="\(currentWeatherOj.currentTemprature)"
}
when I try to call in ViewController its showing the default value which I set inside of computed variable other than _date but I am able print values inside the func of weatherDataDownload.I am confused how variables in swift 3 works because of this.

See the comments in the following code sample. You need to move the call to "completed()"
func weatherDataDownload(#escaping completed : downloadComplete) {
let weatherUrl=URL(string: constant)!
Alamofire.request(weatherUrl , method:.get).responseJSON { response in
// ... leave your code here alone
// put the call to completed() here
completed()
}
// not here
}
When you make the all to Alamofire, it executes it's request on a background thread. When that request completes, it calls the closure that you've defined (the one that starts "response in..."). You don't want to call updateUIweather until that has been done so you put the call to "completed()" inside of the same completion handler.
When the call to completed was outside of that completion handler, it would be called right away... immediately after the Alamofire request was sent (but before it had finished on that background thread). None of the code in the completion handler has run yet so your variables aren't updated yet.
Finally because your completed closure was passed to a block that was then sent off to a background thread, that closure "escapes" the current function. You add the #escaping so that folks reading your code will know that the closure will live on beyond the life of that function.

Related

String interpolation warning in DispatchQueue closure

Say I get below code, and it works fine.
override func viewDidLoad() {
super.viewDidLoad()
// 1. put loadLevel() in background queue
DispatchQueue.global().async { [weak self] in
self?.loadLevel()
}
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code here
DispatchQueue.main.async { [weak self] in
// 3. push some UI code back to main thread
}
However, when I move the background queue to inside loadLevel() and cover the heavy code and UI code, I get an issue that UI is updated with empty values when launching the app. So what is the different of this two ways?
override func viewDidLoad() {
super.viewDidLoad()
// 1. call loadLevel
loadLevel()
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
DispatchQueue.global().async { [weak self] in
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
Update the 2nd code with the heavy code inside.
I found the issue, it is not related with GCD actually. This issue is in line Bundle.main.url(forResource: "level\(self?.level)", which produces a String interpolation warning. And result resource load get nil I guess.
As I used weak reference [weak self] as capture list here, I need to put self? before the global variable level in case to use it in closure. If I give it a default value like \(self?.level ?? 0), then this issue is fixed.
But is it that the property way to deal with this String interpolation here? Or some better approach should be involved here?
override func viewDidLoad() {
super.viewDidLoad()
// 1. call loadLevel
loadLevel()
}
func loadLevel() {
var clueString = ""
var solutionString = ""
var letterBits = [String]()
DispatchQueue.global().async { [weak self] in
if let levelFileURL = Bundle.main.url(forResource: "level\(self?.level)", withExtension: "txt") {
if let levelContents = try? String(contentsOf: levelFileURL) {
var lines = levelContents.components(separatedBy: "\n")
lines.shuffle()
self?.correctGuess = 0
print("AAA")
for (index, line) in lines.enumerated() {
let parts = line.components(separatedBy: ": ")
let answer = parts[0]
let clue = parts[1]
clueString += "\(index + 1). \(clue)\n"
let solutionWord = answer.replacingOccurrences(of: "|", with: "")
solutionString += "\(solutionWord.count) letters\n"
self?.solutions.append(solutionWord)
let bits = answer.components(separatedBy: "|")
letterBits += bits
print("ABC")
}
}
}
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
You have a reference to:
let resource = Bundle.main.url(forResource: "level\(self?.level)" withExtension: ...)
The warning is
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
The compiler is warning you that you are performing string interpolation of an optional value.
Let's consider a simpler example, to show what happens when you do string interpolation with optionals:
print("\(self?.level)")
If level was xxx, it would print
Optional("xxx")
And obviously if self or level were optional, it would just say:
nil
Clearly, neither of these are quite what you want. So, unwrap the optional. E.g.
guard let level = self?.level else { return }
let resource = Bundle.main.url(forResource: "level\(level)" withExtension: ...)
Let me start off by saying, I have no idea, but I have an idea for you to test. Move DispatchQueue.global().async to the first line of loadLevel().
func loadLevel() {
DispatchQueue.global().async { [weak self] in
var clueString = ""
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}
This isolates the change to just calling loadLevel(). If this works as expected, then keep moving the DispatchQueue.global().async call down until it does break.
func loadLevel() {
var clueString = ""
DispatchQueue.global().async { [weak self] in
var solutionString = ""
var letterBits = [String]()
// 2. some heavy code in background queue
DispatchQueue.main.async {
// 3. push UI code back to main thread
}
}
}

Not sure how to append array outside of async call

I'm trying to get certain child nodes named City from Firebase using observeSingleEvent but I am having issues trying to pull it into the main thread. I have used a combination of completion handlers and dispatch calls but I am not sure what I am doing wrong, in addition to not being that great in async stuff. In viewDidLoad I'm trying to append my keys from the setupSavedLocations function and return it back to savedLocations I feel like I am close. What am I missing?
Edit: Clarity on question
import UIKit
import Firebase
class SavedLocationsViewController: UIViewController {
var userID: String?
var savedLocations: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
self.savedLocations = savedData
print("inside", self.savedLocations)
})
}
print("outside",savedLocations)
}
func setupSavedLocations(completion: #escaping ([String]) -> ()) {
guard let user = userID else { return }
let databaseRef = Database.database().reference(fromURL: "https://************/City")
var dataTest : [String] = []
databaseRef.observeSingleEvent(of: .value, with: {(snapshot) in
let childString = "Users/" + user + "/City"
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
dataTest.append(key)
}
completion(dataTest)
})
}
sample output
outside []
inside ["New York City", "San Francisco"]
The call to setupSavedLocations is asynchronous and takes longer to run than it does for the cpu to finish viewDidLoad that is why your data is not being shown. You can also notice from your output that outside is called before inside demonstrating that. The proper way to handle this scenario is to show the user that they need to wait for an IO call to be made and then show them the relevant information when you have it like below.
class SavedLocationsViewController: UIViewController {
var myActivityIndicator: UIActivityIndicatorView?
override func viewDidLoad() {
super.viewDidLoad()
setupSavedLocations() { (savedData) in
DispatchQueue.main.async(execute: {
showSavedLocations(locations: savedData)
})
}
// We don't have any data here yet from the IO call
// so we show the user an indicator that the call is
// being made and they have to wait
let myActivityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.gray)
myActivityIndicator.center = view.center
myActivityIndicator.startAnimating()
self.view.addSubview(myActivityIndicator)
self.myActivityIndicator = myActivityIndicator
}
func showSavedLocations(locations: [String]) {
// This function has now been called and the data is passed in.
// Indicate to the user that the loading has finished by
// removing the activity indicator
myActivityIndicator?.stopAnimating()
myActivityIndicator?.removeFromSuperview()
// Now that we have the data you can do whatever you want with it here
print("Show updated locations: \(locations)")
}

Cannot retrieve data from Firebase using Swift

As the title says I have a weird problem to retrieve simple data from Firebase, but I really can't figure out where I'd go wrong.
This is my schema:
And this the code:
import Firebase
let DB_BASE = Database.database().reference()
class FirebaseService {
static let instance = FirebaseService()
private var REF_BASE = DB_BASE
private var REF_SERVICE_STATUS = DB_BASE.child("Service_Status")
struct ServiceStatus {
var downloadStatus: Bool
var uploadStatus: Bool
}
func getServiceStatus() -> (ServiceStatus?) {
var serviceStatus: ServiceStatus?
REF_SERVICE_STATUS.observeSingleEvent(of: .value) { (requestSnapshot) in
if let unwrapped = requestSnapshot.children.allObjects as? [DataSnapshot] {
for status in unwrapped {
serviceStatus.downloadStatus = status.childSnapshot(forPath: "Download_Status").value as! Bool
serviceStatus.uploadStatus = status.childSnapshot(forPath: "Upload_Status").value as! Bool
}
// THANKS TO JAY FOR CORRECTION
return sponsorStatus
}
}
}
}
but at the end serviceStatus is nil. Any advice?
I think you may be able to simplify your code a bit to make it more manageable. Try this
let ssRef = DB_BASE.child("Service_Status")
ssRef.observeSingleEvent(of: .value) { snapshot in
let dict = snapshot.value as! [String: Any]
let down = dict["Download_Status"] ?? false
let up = dict["Upload_Status"] ?? false
}
the ?? will give the down and up vars a default value of false if the nodes are nil (i.e. don't exist)
Oh - and trying to return data from a Firebase asynchronous call (closure) isn't really going to work (as is).
Remember that normal functions propagate through code synchronously and then return a value to the calling function and that calling function then proceeds to the next line of code.
As soon as you call your Firebase function, your code is going to happily move on to the next line before Firebase has a chance to get the data from the server and populate the return var. In other words - don't do it.
There are always alternatives so check this link out
Run code only after asynchronous function finishes executing

Swift: Functions called in the wrong order?

So, I've been working on a Weather App with the following brief Data Model
class CurrentWeather
{
private var _cityName: String!
private var _date: String!
private var _weatherType: String!
private var _currentTemp: Double!
var cityName: String
{
if _cityName == nil
{
_cityName = ""
}
return _cityName
}
// Same idea for getters var date, var weatherType and
// var currentTemp (returns 0.0 if it is nil)
// Not showing that here
func downloadWeatherDetails(completed: DownloadComplete)
{
// Function which computes values though a url and stores in instance variables
// Not showing the entire actual function here
self._cityName = name.capitalized. // value computed earlier
print(self._cityName)
self._weatherType = main.capitalized // value computed earlier
print(self._weatherType)
self._currentTemp = currentTemp - 273.15 // value computed earlier
print(self._currentTemp)
completed()
}
}
where the type DownloadComplete is a type alias to ()->()
In the main ViewController.swift, I have created an object and called this function (with trailing closure syntax)
var currentWeather: CurrentWeather!
override func viewDidLoad()
{
super.viewDidLoad()
currentWeather = CurrentWeather()
currentWeather.downloadWeatherDetails {
self.updateMainUI() // I have created this function
}
}
func updateMainUI()
{
dateLabel.text = currentWeather.date
currentTempLabel.text = String(currentWeather.currentTemp)
locationLabel.text = currentWeather.cityName
currentWeatherTypeLabel.text = currentWeather.weatherType
currentWeatherImage.image = UIImage(named: currentWeather.weatherType)
print("Tested: \(currentWeather.currentTemp)")
print("Tested: \(currentWeather.cityName)")
print("Tested: \(currentWeather.weatherType)")
}
So the Expected output:
Logically,
I have created a CurrentWeather object
Called the downloadWeatherDetails function which should load the different computed values in the private vars.
Call the user defined updateMainUI function which displays the different values on my app's UI
So the output should be like
Birim. //cityname
Clear. //weatherType
29.134 //currentTemp
Tested: 29.134
Tested: Birim
Tested: Clear
But the output which I get is
Tested: 0.0
Tested: (indicating "")
Tested: (indicating "")
Birim
Clear
29.134
So, basically the functions downloadWeatherDetails and updateMainUI are called in the wrong order? Why is this so? Is this somehow related to asynchronous execution of functions?
I have tried not using the trailing closure, but it still doesn't work.
I also tried leaving the closure empty and calling updateMainUI after downloadWeatherDetails call like this
currentWeather.downloadWeatherDetails {
}
self.updateMainUI()
But this too doesn't work. Any ideas of why the functions are called in the wrong order?
UPDATE:
the underscore variables are private vars while the non-underscore variables are the getters like
var cityName: String
{
if _cityName == nil
{
_cityName = ""
}
return _cityName
}
// Same idea for getters var date, var weatherType and
// var currentTemp (returns 0.0 if it is nil)
// Not showing that here
UPDATE 2:
Project files are here(in case one may want to refer): https://github.com/danny311296/Weather-App
You must call your 'completed()' function within the Alamofire request callback. Since the request function is asynchronous it does not wait for it to finish before executing completed().
Alamofire.request(CURRENT_WEATHER_URL).responseJSON { response in
// handle response...
// when done call completed
completed()
}
I guess the issue is that your "updateMainUI" method is being called before the completion of download process. Although you have implemented the Completion Listener, this should be working but I don't what's wrong with that. Try to use some other methods like delegation or notification to observe the downloading processes.
Check this link to see other method to observe completion:
https://medium.com/ios-os-x-development/ios-three-ways-to-pass-data-from-model-to-controller-b47cc72a4336

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.

Resources