I have updateHeight, updateWeight, and updateBMI methods in my HealthAlgorithm class. I then try to call them in order in ViewController.swift
HealthAlgorithm.swift:
//MARK: Properties
var healthManager:HealthManager?
var kUnknownString = "Unknown"
var bmi:Double?
var height:HKQuantitySample?
var weight:HKQuantitySample?
func updateHeight() {
// 1. Construct an HKSampleType for weight
let sampleType = HKSampleType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHeight)
// 2. Call the method to read the most recent weight sample
HealthManager().readMostRecentSample(sampleType!, completion: { (mostRecentHeight, error) -> Void in
if( error != nil )
{
print("Error reading height from HealthKit Store: \(error.localizedDescription)")
return
}
var heightLocalizedString = self.kUnknownString
self.height = mostRecentHeight as? HKQuantitySample
print(self.height)
// 3. Format the height to display it on the screen
if let meters = self.height?.quantity.doubleValueForUnit(HKUnit.meterUnit()) {
let heightFormatter = NSLengthFormatter()
heightFormatter.forPersonHeightUse = true
heightLocalizedString = heightFormatter.stringFromMeters(meters)
}
})
}
func updateBMI(){
if weight != nil && height != nil {
// 1. Get the weight and height values from the samples read from HealthKit
let weightInKilograms = weight!.quantity.doubleValueForUnit(HKUnit.gramUnitWithMetricPrefix(.Kilo))
let heightInMeters = height!.quantity.doubleValueForUnit(HKUnit.meterUnit())
bmi = ( weightInKilograms / ( heightInMeters * heightInMeters ) )
}
print("BMI: ",bmi)
}
I call these methods in ViewController.swift like this:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
HealthAlgorithm().updateHeight()
HealthAlgorithm().updateWeight()
HealthAlgorithm().updateBMI()
}
The problem is that BMI is returned as nil. The reason this is happening is that the updateBMI method is firing before the updateHeight and updateWeight method.
I use print(self.height) right after I define the variable in the updateHeight method, and I use print("BMI: ", bmi) right after I define the bmi variable in the updateBMI method. Since I am calling updateHeight first, print(self.height) should happen before print("BMI: ", bmi), but for some reason, the BMI: nil is getting returned first which makes no sense to me.
The methods are not being called out of order. The problem is that the function completes asynchronously. You need to call dependent code from the completion handler.
Related
I'm currently attempting to pull a series of items from my database, and append them into an array stored within a variable.
This works successfully - and once the viewDidLoad executes.
I then want to call a function which iterates through this array, and for each element in it, pull out a balance and add it to a global variable.
I can do this by calling the function inside my initial database call, and wrapping it in DispatchQueue, however - it's duplicating, and actually doubling the value, almost like the add function is being called twice.
But I can't see where this is happening, or why. My understanding is that this database call only occurs once, but it seems like the function is getting called twice.
Particularly, my problem is happening like follows:
totalBalance is equal to 0 while the database call resolves
database call finds two entries, saves them to accounts variable
calculateBalance finds first balance of 2, second balance of 3 and adds together, updating totalBalance variable to 5
calculateBalance gets called again, adding 2 and 3 to totalBalance and equalling 10
totalBalance should equal 5, but it gets 2 + 3 twice, so ends up as being 10.
Here's my code:
class DashboardViewController: UIViewController {
let db = Firestore.firestore()
var accounts: [Account] = []
var totalBalance: Int = 0
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var balanceLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
fetchAccounts()
}
func fetchAccounts() {
db.collection("accounts")
.getDocuments { (QuerySnapshot, error) in
if let err = error {
print("Error getting documents: \(err)")
} else {
if let snapshotDocuments = QuerySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let balance = data["accountBalance"] as? String {
let newAccount = Account(
accountBalance: balance,
)
self.accounts.append(newAccount)
}
DispatchQueue.main.async {
self.calculateBalance()
self.tableView.reloadData()
}
}
}
}
}
}
func calculateBalance() {
for cash in accounts {
totalBalance += Int(cash.accountBalance)!
}
DispatchQueue.main.async {
self.balanceLabel.text = "Overall balance - -£\(self.totalBalance)"
}
}
}
Any feedback here would be really appreciated - been scratching my head but think someone coming in from the outside will be able to spot what my problem is.
If I'm reading this correctly, you're calling the calculateBalance method in a loop, if you had 5 accounts in snapshotDocuments the method would get called 5 times:
for doc in snapshotDocuments {
let data = doc.data()
if let balance = data["accountBalance"] as? String {
let newAccount = Account(
accountBalance: balance,
)
self.accounts.append(newAccount)
}
//This will get called multiple times
//DispatchQueue.main.async {
// self.calculateBalance()
// self.tableView.reloadData()
//}
}
//This will get called only once
DispatchQueue.main.async {
self.calculateBalance()
self.tableView.reloadData()
}
I am using iOS Swift, and I am trying to understand how to execute a method once the value of two variables have been set up (non-null value) once the requests have finished.
After reading some documentation, I have found out some concepts which are interesting. The first one would be didSet, which works as an observer.
I could call the method using this method by simply using didSet if I would require just one variable
didSet
var myVar: String 0 {
didSet {
print("Hello World.")
}
}
Nevertheless, I also need to wait for the second one myVar2, so it would not work.
I have also found DispatchQueue, which I could use to wait a second before calling the method (the requests that I am using are pretty fast)
DispatchQueue
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
print("Hello world")
})
but I consider that this solution is not efficient.
Is there anyway to combine these two variables or requests in order to call a method once they have finishing setting the value?
Update
I have tried to replicate David s answer, which I believe is correct but I get the following error on each \.
Type of expression is ambiguous without more context
I copy here my current code
var propertiesSet: [KeyPath<SearchViewController, Car>:Bool] = [\SearchViewController.firstCar:false, \SearchViewController.secondCar:false] {
didSet {
if propertiesSet.allSatisfy({ $0.value }) {
// Conditions passed, execute your custom logic
print("All Set")
} else {
print("Not yet")
}
}
}
var firstCar: Car? {
didSet {
propertiesSet[\SearchViewController.firstCar] = true
}
}
var secondCar: Car? {
didSet {
propertiesSet[\SearchViewController.secondCar] = true
}
}
The variables are set individually, each one on its own request.
You could make your properties optional and check they both have values set before calling your function.
var varA: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}
var varB: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}
Or you can call your function on each didSet and use a guard condition at the start of your function to check that both of your properties have values, or bail out:
var varA: String? = nil {
didSet {
myFunc()
}
}
var varB: String? = nil {
didSet {
myFunc()
}
}
func myFunc() {
guard varA != nil && varB != nil else { return }
// your code
}
First, you should think very carefully about what your semantics are here. When you say "set," do you mean "assigned a value" or do you mean "assigned a non-nil value?" (I assume you mean the latter in this case.) You should ask yourself, what should happen if your method has already fired, and then another value is set? What if one of the properties has a value is set, then nil is set, then another value set? Should that fire the method 1, 2, or 3 times?
Whenever possible you should work to make these kinds of issues impossible by requiring that the values be set together, in an init rather than mutable properties, for example.
But obviously there are cases where this is necessary (UI is the most common).
If you're targeting iOS 13+, you should explore Combine for these kinds of problems. As one approach:
class Model: ObservableObject {
#Published var first: String?
#Published var second: String?
#Published var ready = false
private var observers: Set<AnyCancellable> = []
init() {
$first.combineLatest($second)
.map { $0 != nil && $1 != nil }
.assign(to: \.ready, on: self)
.store(in: &observers)
}
}
let model = Model()
var observers: Set<AnyCancellable> = []
model.$ready
.sink { if $0 { print("GO!") } }
.store(in: &observers)
model.first = "ready"
model.second = "set"
// prints "GO!"
Another approach is to separate the incidental state that includes optionals, from the actual object you're constructing, which does not.
// Possible parameters for Thing
struct Parameters {
var first: String?
var second: String?
}
// The thing you're actually constructing that requires all the parameters
struct Thing {
let first: String
let second: String
init?(parameters: Parameters) {
guard let first = parameters.first,
let second = parameters.second
else { return nil }
self.first = first
self.second = second
}
}
class TheUIElement {
// Any time the parameters change, try to make a Thing
var parameters: Parameters = Parameters() {
didSet {
thing = Thing(parameters: parameters)
}
}
// If you can make a Thing, then Go!
var thing: Thing? {
didSet {
if thing != nil { print("GO!") }
}
}
}
let element = TheUIElement()
element.parameters.first = "x"
element.parameters.second = "y"
// Prints "GO!"
You need to add a didSet to all variables that need to be set for your condition to pass. Also create a Dictionary containing KeyPaths to your variables that need to be set and a Bool representing whether they have been set already.
Then you can create a didSet on your Dictionary containing the "set-state" of your required variables and when all of their values are true meaning that all of them have been set, execute your code.
This solution scales well to any number of properties due to the use of a Dictionary rather than manually writing conditions like if aSet && bSet && cSet, which can get out of hand very easily.
class AllSet {
var propertiesSet: [KeyPath<AllSet, String>:Bool] = [\.myVar:false, \.myVar2:false] {
didSet {
if propertiesSet.allSatisfy({ $0.value }) {
// Conditions passed, execute your custom logic
print("All Set")
} else {
print("Not yet")
}
}
}
var myVar: String {
didSet {
propertiesSet[\.myVar] = true
}
}
var myVar2: String {
didSet {
propertiesSet[\.myVar2] = true
}
}
init(myVar: String, myVar2: String) {
self.myVar = myVar
self.myVar2 = myVar2
}
}
let all = AllSet(myVar: "1", myVar2: "2")
all.myVar = "2" // prints "Not yet"
all.myVar2 = "1" // prints "All set"
When I'm trying to update my datasource (which is custom in a separate class) from a closure inside the respective ViewController it won't work.
So this is the code I'm calling to update the datasource with
extension YelpSearchController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let searchTerm = searchController.searchBar.text else {
return
}
let yelpCoordinate = YLPCoordinate(latitude: 37.785834000000001, longitude: -122.406417)
let yelpClient = YLPClient.init(apiKey: appSecret)
yelpClient.search(with: yelpCoordinate, term: searchTerm , limit: 30, offset: 1, sort: YLPSortType.distance) { [weak self] result, error in
guard let results = result else { return }
let businesses = results.businesses
self?.dataSource.update(with: businesses)
}
}
}
And this is the function that is just updating my data variable inside the datasource class, as follows:
private var data = [YLPBusiness]()
func update(with data: [YLPBusiness]) {
self.data = data
}
The problem is when I call the delegate methods, as required as a datasource, they are initially called but when they are the data variable has still not been updated, so the data is nil.
If I for example try and print the data.count inside of the update func I get a result. But inside of any of the delegate methods (cellForRowAt, numberOfRowsInSection), it's all nil. So all the methods concerning and using the data variable except the update function they are not getting the data since the view is loading but the data comes after when the user inputs in the search field.
So the question is how do I get the data accessible to the delegate methods?
Forgot to update the datasource after I've received the data from inside the closure.
yelpClient.search(with: yelpCoordinate, term: searchTerm , limit: 30, offset: 1, sort: YLPSortType.distance) { [weak self] result, error in
guard let results = result else { return }
let businesses = results.businesses
self?.dataSource.update(with: businesses)
DispatchQueue.main.async {. // added this
self?.tableView.reloadData()
}
Big thanks to DonMag for the correct answer.
I'm pretty new to IOS Application Development.
I'm trying to stop viewWillAppear from finishing until after my function has finished working. How do I do that?
Here's viewWillAppear:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
checkFacts()
if reset != 0 {
print("removing all bird facts")
birdFacts.removeAll()
}
}
func checkFacts() {
let date = getDate()
var x: Bool = true
var ind: Int = 0
print("count is ", birdFacts.count)
while ind < birdFacts.count {
print("accessing each bird fact in checkFacts")
let imageAsset: CKAsset = birdFacts[ind].valueForKey("birdPicture") as! CKAsset
let image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
print(image)
if image == nil {
if (birdFacts[ind].valueForKey("sortingDate") != nil){
print("replacing fact")
print("accessing the sortingDate of current fact in checkFacts")
let sdate = birdFacts[ind].valueForKey("sortingDate") as! NSNumber
replaceFact(sdate, index: ind)
}
/*else {
birdFacts.removeAll()
print("removing all bird facts")
}*/
}
ind = ind + 1
print(ind)
}
self.saveFacts()
let y = checkRepeatingFacts()
if y {
print("removing all facts")
birdFacts.removeAll()
//allprevFacts(date, olddate: 0)
}
}
checkFacts references 2 others functions, but I'm not sure they're relevant here (but I will add them in if they are and I'm mistaken)
Instead of trying to alter or halt the application's actual lifecycle, why don't you try using a closure?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
checkFacts(){ Void in
if self.reset != 0 {
print("removing all bird facts")
birdFacts.removeAll()
}
}
}
func checkFacts(block: (()->Void)? = nil) {
let date = getDate()
var x: Bool = true
var ind: Int = 0
print("count is ", birdFacts.count)
while ind < birdFacts.count {
print("accessing each bird fact in checkFacts")
let imageAsset: CKAsset = birdFacts[ind].valueForKey("birdPicture") as! CKAsset
let image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
print(image)
if image == nil {
if (birdFacts[ind].valueForKey("sortingDate") != nil){
print("replacing fact")
print("accessing the sortingDate of current fact in checkFacts")
let sdate = birdFacts[ind].valueForKey("sortingDate") as! NSNumber
replaceFact(sdate, index: ind)
}
/*else {
birdFacts.removeAll()
print("removing all bird facts")
}*/
}
ind = ind + 1
print(ind)
}
self.saveFacts()
let y = checkRepeatingFacts()
if y {
print("removing all facts")
birdFacts.removeAll()
//allprevFacts(date, olddate: 0)
}
// CALL CODE IN CLOSURE LAST //
if let block = block {
block()
}
}
According to Apple Documentation:
Closures are self-contained blocks of functionality that can be passed around and used in your code.
Closures can capture and store references to any constants and variables from the context in which they are defined.
So by defining checkFacts() as: func checkFacts(block: (()->Void)? = nil){...} we can optionally pass in a block of code to be executed within the checkFacts() function.
The syntax block: (()->Void)? = nil means that we can take in a block of code that will return void, but if nothing is passed in, block will simply be nil. This allows us to call the function with or without the use of a closure.
By using:
if let block = block {
block()
}
we can safely call block(). If block comes back as nil, we pass over it and pretend like nothing happened. If block is not nil, we can execute the code contained within it, and go on our way.
One way we can pass our closure code into checkFacts() is by means of a trailing closure. A trailing closure looks like this:
checkFacts(){ Void in
if self.reset != 0 {
print("removing all bird facts")
birdFacts.removeAll()
}
}
Edit: Added syntax explanation.
So based on the comments, checkFacts is calling asynchronous iCloud operations that if they are not complete will result in null data that your view cannot manage.
Holding up viewWillAppear is not the way to manage this - that will just result in a user interface delay that will irritate your users.
Firstly, your view should be able to manage null data without crashing. Even when you solve this problem there may be other occasions when the data becomes bad and users hate crashes. So I recommend you fix that.
To fix the original problem: allow the view to load with unchecked data. Then trigger the checkData process and when it completes post an NSNotification. Make your view watch for that notification and redraw its contents when it occurs. Optionally, if you don't want your users to interact with unchecked data: disable appropriate controls and display an activity indicator until the notification occurs.
I have my file myAPI.swift, and two objet Round and GameStats. My Round object also have an attribute GameStats. So what I want to do is to get the GameStats property that I store inside my users defaults then assign it inside my Round object.
class myAPI: NSObject {
static let sharedInstance = myAPI()
var currentStats: GameStats?
var currentRound: Round?
private init(){
super.init()
self.loadData()
NSLog("Stats have been reload: \(self.currentRound?.gameStats)") // Return nil
// If I try to add this line the app stop running and nothing happens
NSLog("Test Singleton: \(myApp.sharedInstance.currentRound?.gameStats)")
}
func loadData(){
let backupNSData = NSUserDefaults.standardUserDefaults().objectForKey("backupNSData")
if let backupNSData = backupNSData as? NSData{
let backupData = NSKeyedUnarchiver.unarchiveObjectWithData(backupNSData)
if let backupData = backupData as? [String:AnyObject] {
guard let round = backupData["currentRound"] as? Round else {
print("error round loaddata")
return
}
self.currentRound = round
guard let stats = backupData["stats"] as? GameStats else {
print("error guard stats")
return
}
self.currentRound.gameStats = stats
NSLog("Stats reloaded: \(stats)") // This is not nil it works here
}
}
}
When my app crash I call this function to save the data
func backupData(){
var backupData:[String:AnyObject] = [String:AnyObject]()
if let round = self.currentRound {
backupData["currentRound"] = round
ColorLog.purple("Stats saved inside Round \(round.gameStats)")
}
if let stats = self.currentStat {
backupData["stats"] = stats
ColorLog.purple("Stats saved : \(stats)")
}
let backupNSData = NSKeyedArchiver.archivedDataWithRootObject(backupData)
NSUserDefaults.standardUserDefaults().setObject(backupNSData, forKey: "backupNSData")
NSUserDefaults.standardUserDefaults().synchronize()
}
So I have two question,
Is it normal that I can't call my singleton like myApp.sharedInstance.currentRound.id = 5 (for instance) inside the init() (I guess it is but I didn't find anything about that)
Why in my init() method my first NSLog self.currentRound?.gameStats is nil when in the function loadData() it wasn't ? It seems like it's losing its reference since we are leaving the function.
What am I doing right now is adding a currentStats property in my singleton, then when I retrieve data instead of doing self.currentRound.gameStats = stats I do self.currentStats = stats, then self.currentRoud.gameStats = self.currentStats and If I do that it works, I don't really know If I am doing the things right here.
Also my two objects Round and GameStats conform to NSCoding protocol as I implemented #objc func encodeWithCoder and #objc required init?(coder aDecoder: NSCoder) methods for both of them.
Thank for you help.