Set UILabel Text at Completion of CloudKit Asyncronous Operation in Swift - ios

I'm new to Swift, and asynchronous code in general, so tell me if this is way off. Basically I want to:
Open the App
That triggers a read of CloudKit records
Once the read is complete a UILabel will display the number of records retrieved
This clearly isn't useful in itself, but as a principle it will help me to understand the asynchronous code operation, and how to trigger actions on their completion.
// In ViewController Swift file:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
readDatabase()
}
#IBOutlet weak var myLabel: UILabel!
}
let VC=ViewController()
//In Another Swift file:
func readDatabase() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "myRecord", predicate: predicate)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
privateDB.perform(query, inZoneWith:nil) { (allRecs, err) in
VC.myLabel.text = ("\(allRecs?.count) records retreived")
/*
ERROR OCCURS IN LINE ABOVE:
CONSOLE: fatal error: unexpectedly found nil while unwrapping an Optional value
BY CODE LINE: Thread 8:EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
*/
}
}
I'm able to set the text field from within the viewDidLoad function, so why not from a function called within that function?
A few other things I've tried:
Use async dispatch to put it on thread 1
Implement a var with within the ViewController class, with a didSet that sets the text, set the var to the desired value in the privateDB.perform code to trigger the change
These both create the same problem as above.
Yes, I know there isn't any error handling in .perform, and yes there are records. If I trigger the setting of the UILabel text to the record count manually a few seconds after the view has loaded, it works fine.
So the question is...
How do I use the completion of the database read as a trigger to load attributes of the records to the view?
Thanks
Edit
What actually happened here was that VC was created globally, but never presented - since loadView was never called for it, myLabel didn't exist and, being a force unwrapped property, caused a crash when it was referenced

The problem is with this line: let VC=ViewController(). Here you instantiate a new instance of your ViewController class and try to set the label on that newly created instance. However, you would want to set the label on your viewController instance that is currently displayed.
Just change this line VC.myLabel.text = ("\(allRecs?.count) records retreived") to self.myLabel.text = ("\(allRecs?.count) records retreived") and it should work fine.

Got it, the solution looks like this:
// In ViewController Swift file:
typealias CompletionHandler = (_ recCount:Int,_ err:Error?) -> Void
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
readDatabase(completionHandler: { (recCount,success) -> Void in
if err == nil {
self.myLabel.text = "\(recCount) records loaded"
} else {
self.myLabel.text = "load failed: \(err)"
}
})
}
#IBOutlet weak var myLabel: UILabel!
}
//In Another Swift file:
func readDatabase() {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "myRecord", predicate: predicate)
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
privateDB.perform(query, inZoneWith:nil) { (allRecs, err) in
if let recCount = allRecs?.count {
completionHandler(recCount,err)
} else {
completionHandler(0,err)
}
}
}
The difference between this and the original is that this uses the CompletionHandler typealias in the function call for loading the database records, which returns the count of records and an optional error.
The completion operation can now live in the ViewController class and access the UILabel using self.myLabel, which solves the error that was occurring earlier, while keeping the database loading code separate to the ViewController class.
This version of the code also has basic error handling.

Related

Today Widget unable to load with a specific line of code

I've added a Today extension to my app and it all works fine until a specific line of code is compiled. NB: compiled, not executed!
My TodayViewController is:
class StoredDoses {
func getDoses(doses: inout [Dose]) {
if let userD = UserDefaults(suiteName: "com.btv.mySuite") {
if let dosesData = userD.object(forKey: "doses_key") {
do {
// -----------------------------------------------
// Comment the line below out and the widget works
doses = try PropertyListDecoder().decode([Dose].self, from: dosesData as! Data)
// -----------------------------------------------
} catch {
print ("ERROR")
}
}
}
}
}
class TodayViewController: UIViewController, NCWidgetProviding {
#IBOutlet weak var aText: UILabel!
#IBOutlet weak var bText: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view from its nib.
}
func widgetPerformUpdate(completionHandler: (#escaping (NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
// If there's no update required, use NCUpdateResult.NoData
// If there's an update, use NCUpdateResult.NewData
//Just for development stage - not real, final code
let form = DateFormatter()
form.timeStyle = .short
aText.text = form.string(from: Date())
completionHandler(NCUpdateResult.newData)
}
}
So, the above code isn't well written, but it's what I've used to finally narrow down the cause of the unloading widget. The array of Doses is a custom, codable class, but if I try to get an array of String then it's the same. The StoredDoses code is included in the main app and doesn't cause any problems.
Just to re-iterate: I'm not trying to execute any method in the StoredDoses class. I don't even have an instance of it in the widget. When the doses = ... line is merely commented out then the widget loads and the aText label in the widget appears with the current time in it.
Ok, so thanks to #Chris' apparently unconnected advise I got it sorted!
It appears to have been an Interface Builder issue: somehow it had retained the original name of the UILabel that was auto-created when I added the Today extension in Xcode. At some point, after connecting an IBOutlet to the label with "Hello World" in it, I'd renamed it to something slightly more relevant but hadn't unconnected it before over-typing the new name in the TodayViewController.
The console didn't throw up any problems and at times seemed to work, but when the line with
try PropertyListDecoder().decode([Dose].self, from: dosesData as! Data)
was present then it stopped working without any console messages.
I only found that out after I explored #Chris comment about the as! Data. I re-wrote to first get the Data:
if let userD = UserDefaults(suiteName: "com.btv.mySuite") {
if let dosesData = userD.object(forKey: "doses_key") {
if let unwrappedData = dosesData as? Data {
do {
doses = try PropertyListDecoder().decode([SplitDose].self, from: unwrappedData)
} catch {
doses.removeAll()
}
}
}
}
Once this was compiled (remember, it's still not being executed - this is just sitting there waiting to be used) the console threw up a message and the app crashed out showing the old UILabel name as not key-compliant. Reconnecting the UILabel in IB fixed everything and I could compile the original code....
This probably deserves a Radar entry but right now I don't want to waste another day re-creating (if at all possible) this problem!

viewDidLoad, is it called only once?

I am a beginner in iOS development, and I am following one tutorial using firebase database to make a simple chat app. Actually I am confused with the use of viewDidLoad method.
Here is the screenshot of the app: https://ibb.co/gqD4Tw
I don't understand why retrieveMessage() method is put on viewDidLoad when I want to send data (chat message) to firebase database, I used sendButtonPressed() method (which is an IBAction) and when I want to retrieve data from the database, I use retrieveMessage().
The retrieveMessage() method is called on viewDidLoad, as far as I know the viewDidLoad method is called only once after the view is loaded into memory. We usually use it for initial setup.
So, if viewDidLoad is called only once in initial setup, why the retrieveMessage() method can retrieve all the message that I have sent to my own database over and over again, after I send message data to the database ?
I don't understand why retrieveMessage() method is put on viewDidLoad below is the simplified code:
class ChatViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var messageArray = [Message]()
#IBOutlet var messageTextfield: UITextField!
#IBOutlet var messageTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//Set as the delegate and datasource :
messageTableView.delegate = self
messageTableView.dataSource = self
//the delegate of the text field:
messageTextfield.delegate = self
retrieveMessage()
///////////////////////////////////////////
//MARK: - Send & Recieve from Firebase
#IBAction func sendPressed(_ sender: AnyObject) {
// Send the message to Firebase and save it in our database
let messageDB = FIRDatabase.database().reference().child("message")
let messageDictionary = ["MessageBody":messageTextfield.text!, "Sender": FIRAuth.auth()?.currentUser?.email]
messageDB.childByAutoId().setValue(messageDictionary) {
(error,ref) in
if error != nil {
print(error!)
} else {
self.messageTextfield.isEnabled = true
self.sendButton.isEnabled = true
self.messageTextfield.text = ""
}
}
}
//Create the retrieveMessages method :
func retrieveMessage () {
let messageDB = FIRDatabase.database().reference().child("message")
messageDB.observe(.childAdded, with: { (snapshot) in
let snapshotValue = snapshot.value as! [String:String]
let text = snapshotValue["MessageBody"]!
let sender = snapshotValue["Sender"]!
let message = Message()
message.messsageBody = text
message.sender = sender
self.messageArray.append(message)
self.messageTableView.reloadData()
})
}
}
viewDidLoad method is called only once in ViewController lifecycle.
The reason retrieveMessage() is called in viewDidLoad because it's adding observer to start listening for received and sent message. Once you receive or send message then this block(observer) is called and
then adding that text in array self.messageArray.append(message) and updating tableview.
viewDidLoad gets called only once but the firebase functions starts a listener, working in background and syncronizeing data.
Its called in viewDidLoad because it tells -> When this view loads, start listening for messages.
ViewDidLoad() is only called upon initializing the ViewController.
If you want to have a function called every time the user looks at the VC again (e.g. after a segue back from another VC) you can just use ViewDidAppear().
It is also called when ViewDidLoad() is called.

Why is selector being sent to previous view controller?

I have an app with a data model class that declares a protocol, and two view controllers embedded in a navigation controller. The data model class is a shared instance. Both view controllers are delegates of the data model. The second view controller has a UITableView.
On start, calls to data model functions from the first view controller work as expected. When I segue from the first view controller to the second, calls to data model functions work as expected as well.
However, when I navigate back to the first view controller and a data model function is called, the app crashes with this error:
2017-04-03 09:48:12.623027 CoreDataTest[3207:1368182]
-[CoreDataTest.PracticesViewController noDupeWithResult:]: unrecognized selector sent to instance 0x15fe136e0
That PracticesViewController is the second view controller. I don't understand why a selector is being sent to what I am thinking of as the previous view controller. My expectation is that the selector should be sent to the first view controller that has just been navigated back to,
I am self-taught, so I presume there is something basic that I am missing, but I don't know what I don't know. Can someone explain why the crash is happening?
Code for the data model
import Foundation
import CoreData
import UIKit
#objc protocol PracticesDataDelegate {
#objc optional func practicesLoadError(headline:String,message:String)
#objc optional func practicesLoaded(practices:[NSManagedObject])
#objc optional func practiceStored()
#objc optional func practiceDeleted()
#objc optional func noDupe(result:String)
}
class PracticesDataModel {
static let sharedInstance = PracticesDataModel()
private init () {}
var delegate: PracticesDataDelegate?
var practices: [NSManagedObject] = []
// some code omitted . . .
/// Check for a duplicate exercise
func checkForDupe(title:String,ngroup:String,bowing:String,key:String){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Practice")
let predicate = NSPredicate(format: "exTitle == %# AND notegroup == %# AND bowing == %# AND key == %#", title, ngroup, bowing, key)
fetchRequest.predicate = predicate
do {
practices = try managedContext.fetch(fetchRequest)
if practices.count == 0 {
self.delegate?.noDupe!(result:"none") // exception happens here
} else {
self.delegate?.noDupe!(result:"dupe")
}
} catch let error as NSError {
// to come
}
}
The top of the first view controller
import UIKit
class galamianSelection: UIViewController, ExcerciseDataModelDelegate, PracticesDataDelegate {
let exerciseModel = ExerciseDataModel.sharedInstance
let pModel = PracticesDataModel.sharedInstance
// some code omitted . . .
//// THE VIEW ////
override func viewDidLoad() {
super.viewDidLoad()
exerciseModel.delegate = self
pModel.delegate = self
exerciseModel.loadExercises()
}
//// RESPOND TO PRACTICE DATA MODEL ////
func practiceStored() {
print("exercise stored")
}
func noDupe(result: String) {
if result == "none" {
let d = Date()
pModel.storePractice(date: d, exTitle: theExerciseTitle.text!, notegroup: theNoteGroup.text!, bowing: theBowings.text!, rythem: theRhythms.text!, key: theKey.text!, notes: "")
} else {
print("dupe")
}
}
The top of the second view controller
import UIKit
class PracticesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, PracticesDataDelegate {
#IBOutlet weak var tableView: UITableView!
let pModel = PracticesDataModel.sharedInstance
// some code omitted . . .
//// THE VIEW ////
override func viewDidLoad() {
super.viewDidLoad()
pModel.delegate = self
pModel.getPractices()
}
delegate is properly set to self in both view controllers.
I am happy to provide more code is needed, but I suspect someone who knows can diagnose from what I've provided.
There are a few issues here.
You are trying to reuse a delegate but only setting it in viewDidLoad. viewDidLoad is only called when, as the name implies, the view initially loads. That means that if you have VC1 in a navigation controller, then you push to VC2, then pop back to VC1, viewDidLoad will not be called a second time. The view has already loaded. If you want a piece of code to be called every time a view controller comes back into focus, you should put it into viewWillAppear: or viewDidAppear:
You are force unwrapping an optional protocol method which you haven't actually implemented (a bug which you're seeing as a result of the first issue, but it's still an issue on its own). Change the force unwrap to an optional self.delegate?.noDupe?(result:"none")
You also declared your delegate like this var delegate: PracticesDataDelegate?. This makes your class retain its delegate and is generally not the right behavior. In your case it actually causes a retain cycle (until you change the delegate). You should change this declaration to weak var delegate: PracticesDataDelegate?
What appears to be happening is:
In PracticesViewController you set the model's delegate to self, as in pModel.delegate = self.
You navigate back to your first view controller. This means that PracticesViewController gets deallocated. But the model's delegate has not been changed, so it's still pointing to the memory location where the view controller used to be.
Later (but soon), your model tries to call a method on its delegate, but it can't because it was deallocated. This crashes your app.
You could reassign the value of the delegate, for example in viewDidAppear. Or you could have your model check whether its delegate implements a method before attempting to call it. That's a standard practice for optional protocol methods-- since they don't have to be implemented, you check first before calling them.
In general, don't use ! in Swift unless you want your code to crash there if something goes wrong.

Why doesn't my If-Else block work the way it should?

I'm new to swift and using firebase, I'm making a simple application that let's user input a cellular prefix number and displays what cellular network is that number.
I'M having a hard problems with the if-else part. Whenever I add the else part, the label always displays "Missing" but whenever I print the snapshot.key and snapshot.value, I get the correct result in the console. When I remove the else part, it works. I'm really having a hard time figuring out the answer. Thanks in advance! Here is the code:
import UIKit
import Firebase
class ViewController: UIViewController, FBSDKLoginButtonDelegate {
let loginButton: FBSDKLoginButton = {
let button = FBSDKLoginButton()
button.readPermissions = ["email"]
return button
}()
#IBOutlet weak var numField: UITextField!
#IBOutlet weak var answerField: UILabel!
#IBAction func enterButton(sender: AnyObject) {
matchNumber()
}
func matchNumber(){
let number: String = numField.text!
let numRef = Firebase(url: "https://npi2.firebaseio.com/num_details")
numRef.queryOrderedByKey().observeEventType(.ChildAdded, withBlock: { snapshot in
let network = snapshot.value as! String!
if snapshot.key == self.numField.text! {
self.answerField.text = network
}
else {
self.answerField.text = "Missing"
}
})
}
I think I know what is happening.
EXPLANATION:
1) You make your request to Firebase.
2) The request is asynch, so the code keeps running.
3) The request is not done yet, therefore the else statement is executed because the value is not yet returned (checks for initial state).
4) When you remove the else, the code executes the if because there is nothing else to execute until the request is done (initial state is checked but there is no code to execute for it, then Firebase checks again because there is a data change and the if statement is executed).
REFERENCE:
From the FIREBASE DOCUMENTATION (that you should have read before coming here :P)
"Firebase data is retrieved by attaching an asynchronous listener to a database reference. The listener will be triggered once for the initial state of the data and again anytime the data changes. This document will cover the basics of retrieving data, how data is ordered, and how to perform simple queries on data in your Firebase database."
https://www.firebase.com/docs/ios/guide/retrieving-data.html
SOLUTION:
Use the completion handler. It's made for that. Don't use else statements inside your request like that. That's not how you use Firebase.

Initialize object in parse

I am trying to implement a like feature. I have a label and a button. If the button is pressed it should add one vote to Parse and the label would retrieve the number of votes. However it does not work with my code. It looks like I need to initialize parseObject but I don't know how to. my code for the cell is below. Thank you. I have been stuck for this problem for days.
import UIKit
import ParseUI
import Parse
var parseObject:PFObject?
var votes = [PFObject]()
class NewCollectionViewCell: UICollectionViewCell {
var parseObject = PFObject(className: "Posts")
#IBOutlet weak var postsImageView: PFImageView!
#IBOutlet weak var postsLabel: UILabel!
#IBOutlet weak var votesLabel:UILabel?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
postsLabel.textAlignment = NSTextAlignment.Center
}
#IBAction func vote(sender: AnyObject) {
if (parseObject != nil) {
if let votes = parseObject!.objectForKey("votes") as? Int {
parseObject!.setObject(votes + 1, forKey: "votes")
parseObject!.saveInBackgroundWithTarget(nil, selector: nil)
votesLabel?.text = "\(votes + 1) votes"
}
else {
parseObject!.setObject(1, forKey: "votes")
parseObject!.saveInBackgroundWithTarget(nil, selector: nil)
votesLabel?.text = "1 votes"
}
}
}
}
First of all, you have implement you target method. I am not sure whether it will save the object or not when you pass in nil. For making it consistent, you better include target and selector because it will also tell you whether the object is saved successfully. You can also go to the Parse dashboard to check whether it's saved successfully or not.
And yes, you have not instantiate your object. Otherwise, your if-else statement just got executed and did nothing. If you try to debug it, the parseObject is actually nil. In Parse, there are several ways to do and docs provides you everything: https://parse.com/docs/ios/api/Classes/PFObject.html#//api/name/objectWithClassName:

Resources