Delegate Property Mysteriously Reseting - ios

I'll keep this short
PROBLEM:
I dismiss the view controller and call a function on the incoming controller that fires fine. The value even prints to console fine. I assigned the value to local variable and it prints and works fine until I begin updating the other properties on the struct object which is holding the value. In fact, the value is erased entirely from all the object it was stored. The code only touches the value once so I am not sure what is going on.
This is how I am dismissing:
var controller = AddVendersTableViewController()
override func viewDidLoad() {
...
//MARK: Temp
controller.delegate = self
}
#objc func returnToOriginatingController(){
dismiss(animated: true) { [weak self] in
self?.controller.enabledStatusChecker()
print("This is the initiated view controller — \(String(describing: self?.controller))")
}
submitButton.isHidden = true
}
This is the receiving end on the new controller:
var delegate : CompanyAddressDelegate? = nil
func enabledStatusChecker(){
if delegate != nil {
guard let string = delegate?.getCompanyAddress() else {return}
let localString = string
localVenderObject?.address = localString
print(localVenderObject ?? "this is not working")
print("\(self) — This is the current VC ")
}
print(localVenderObject ?? "No Value in enabledStatus")
if localVenderObject?.name != nil && localVenderObject?.phone != nil && localVenderObject?.email != nil && localVenderObject?.website != nil && localVenderObject?.address != nil {
submitButton.isEnabled = true
submitButton.layer.backgroundColor = UIColor.systemBlue.cgColor // temp color
}
}

Related

'Invalid document reference. Document references must have an even number of segments' when trying to edit/delete new entry

I am able to delete the logged in users document when my app is first loaded. However, if I create a new entry, long press the new entry from the table view and select delete, I get a crash. I thought it had something to do with the document ID not being saved but I couldn't figure out why. If the same newly created entry is deleted after the app is closed and reopened then it will delete with no problem, but if I leave the app open and delete immediately after creating a new document, it will crash.
class BudgetViewController: UIViewController: {
var budgetData = [Transaction]()
func showAdd() {
let modalViewController = AddCategory()
modalViewController.addCategoryCompletion = { newCategories in
self.budgetData.append(newCategories)
self.tableView.reloadData()
}
modalViewController.modalPresentationStyle = .overFullScreen
modalViewController.modalTransitionStyle = .crossDissolve
modalViewController.selectionDelegate = self
present(modalViewController, animated: true, completion: nil)
}
#objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer){
if gestureRecognizer.state == .began {
let touchPoint = gestureRecognizer.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
let cell = CategoryCell()
var data = budgetData[indexPath.row]
let modalViewController = EditCategory()
modalViewController.deleteCategory = { row in
self.deletedRow = row
self.deleteRow()
}
modalViewController.documentID = data.trailingSubText ?? ""
modalViewController.modalPresentationStyle = .overFullScreen
modalViewController.modalTransitionStyle = .crossDissolve
present(modalViewController, animated: true, completion: nil)
modalViewController.row = indexPath.row
print("longpressed\(indexPath.row)\(data.trailingSubText)")
}
}
}
override func viewDidLoad() {
loadNewData()
}
func loadNewData() {
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Category").getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
for document in snapshot!.documents {
let data = document.data()
let title = data["title"] as? String ?? ""
let uid = data["uid"] as? String ?? ""
let documentID = document.documentID
// let timeStamp = data["timeStamp"] as? Date
let newSourse = Transaction(title: title, dateInfo: "0% out of spent", image: UIImage.gymIcon, amount: 12, annualPercentageRate: 12, trailingSubText: documentID, uid: uid)
self.budgetData.append(newSourse)
}
self.tableView.reloadData()
}
}
}
class AddCategory: UIViewController {
#objc func saveAction(){
guard let uid = Auth.auth().currentUser?.uid else { return }
let newCategory = Transaction(title: textField.text ?? "", dateInfo: "0% out of spent", image: UIImage.gymIcon, amount: 12, annualPercentageRate: 23, trailingSubText: "", uid: uid)
db.collection("users").document(uid).collection("Category").addDocument(data: newCategory.dictionary)
self.dismiss(animated: false, completion: {
self.addCategoryCompletion?(newCategory)
})
self.dismiss(animated: false, completion: nil)
print("selected")
}
}
}
class EditCategory: UIViewController {
func deleteAction(){
guard let user = Auth.auth().currentUser?.uid else { return }
print("document::\(self.documentID)")
// let budget = textField.text
db.collection("users").document(user).collection("Category").document(documentID).delete { (err) in
if let err = err {
print(err.localizedDescription)
}else{
self.dismiss(animated: false, completion: {
self.deleteCategory?(self.row)
})
print("deleted successfully")
}
}
}
}
The error is strongly suggesting that user is nil or empty at the time you run this code:
guard let user = Auth.auth().currentUser?.uid else { return }
db.collection("users").document(user).collection("Category").getDocuments()
This almost certainly means that a user was not signed in at the time. Your code needs to check currentUser for nil before trying to access its uid property. nil means that no user is currently signed in.
The user will not be signed in immediately at app launch. You should use an auth state listener to get a callback when the user object becomes available.
Maybe not the best approach, but it is working now. I called
self.budgetData.removeAll()
self.loadNewData()
self.tableView.reloadData()
inside of my callback
modalViewController.addCategoryCompletion = { newCategories in
self.budgetData.append(newCategories)
self.tableView.reloadData()
}`
Based on the presented code, when a new category is added to Firebase
let newCategory = Transaction(title: textField.text ?? ...)
db.collection("users").document(uid).collection("Category").addDocument(data: newCategory
you're not getting a valid documentId from Firebase first. So therefore that object exists in your dataSource with no documentId so when you try to remove it, there's a crash.
A couple of options
Option 1: Create a firebase reference first, which will provide a Firebase documentId that you can add to the object when writing. See the docs. Like this
let newCategoryRef = db.collection("Category").document()
let docId = newCategoryRef.documentId
...add docId to the category object, then add to dataSource
or
Option 2: Add an observer to the node (see Realtime Updates) so when a new document is written, the observers event will fire and present the newly added document, which will contain a valid documentId, and then craft an category object based on that data and add that object to your dataSource array. In this case, you don't need to add it to the dataSource array when writing as it will auto-add after it's written based on the observers .added event.

Issues with Swift's Combine framework CombineLatest

I went through the WWDC video of "Introducing Combine" where it was said that whenever a publisher value gets updated the CombineLatest gets called and updated. But the snippet I created works oddly.
class Mango {
var enableButton = false
#Published var userName = "admin"
#Published var password = "poweruser"
#Published var passwordAgain = "poweruser"
var validatePassword: AnyCancellable {
Publishers.CombineLatest($password, $passwordAgain).map { (password, reenterpass) -> String? in
print("Is Password Same to \(password)? :", password == reenterpass)
guard password == reenterpass else { return nil }
return password
}.eraseToAnyPublisher()
.map { (str) -> Bool in
print("In Map", str != nil)
guard str != nil else { return false }
return true
}.assign(to: \.enableButton, on: self)
}
init() {
validatePassword
}
func checkSub() {
print("1. Is password same? ->",enableButton)
password = "nopoweruser"
print("2. Is password same? ->",enableButton)
}
}
When I initialize and call the function checkSub() where the publisher 'password' is updated the CombineLatest does not get called. Why is it behaving oddly?
Input:
let mango = Mango()<br>
mango.checkSub()
Output:
Is Password Same to poweruser? : true
In Map true
1. Is password same? -> true
2. Is password same? -> true
It seems like the issue is with memory management. The validatePassword cancellable is autoreleased, meaning that the subscription is completed as soon as you create it, since you do not retain it. Make it a property instead of computed property, using lazy var and it should work fine.
lazy var validatePassword: AnyCancellable = {
Publishers.CombineLatest($password, $passwordAgain).map { (password, reenterpass) -> String? in
print("Is Password Same to \(password)? :", password == reenterpass)
guard password == reenterpass else { return nil }
return password
}.eraseToAnyPublisher()
.map { (str) -> Bool in
print("In Map", str != nil)
guard str != nil else { return false }
return true
}.assign(to: \.enableButton, on: self)
}()
With lazy you are retaining the cancellable which gets released only after the object is released. So, this should work properly.

How to reload the page while moving from tab bar in swift 3?

Here after running the application i had clicked on cart in tab bar at that time there was no items added in cart so it shown your shopping cart is empty but after adding items in cart then I clicked on cart icon on tab bar also it is showing like as in previous and the cart page was not reloading can anyone help me how to resolve this ?
I had used this code in my home view controller
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 1 {
let navController = self.tabBarController?.viewControllers![1] as! UINavigationController
let secondViewController = navController.viewControllers[0] as! AddToCartViewController
secondViewController.firstTimeTableSkip = true
self.tabBarController?.selectedIndex = 1
}
}
in my cart view controller I used this code
override func viewWillAppear(_ animated: Bool) {
if firstLoginCheck == true {
checkOutButton.isHidden = false
checkOutButton.layer.cornerRadius = 5
if conditonCheck == true || conditonCheck == nil{
checkOutButton.isHidden = true
checkOutButton.layer.cornerRadius = 5
tableDetails.isHidden = true
emptyView.isHidden = true
tableDetails.separatorInset = UIEdgeInsets.zero
loginCheck = UserDefaults.standard.integer(forKey: "CustomerLogin")
activityIndicator.startAnimating()
emptyView.isHidden = true
if loginCheck == 1 {
let token = UserDefaults.standard.value(forKey: "CustomerLoginToken")
self.customerKeyToken = token!
print(self.customerKeyToken!)
if itemCode != nil {
self.customerAddToCartItemsDownloadJsonWithURl(cartApi: customerAddtoCartApi)
customerCartItemsDownloadJsonWithURl(cartApi: customerCartApi)
}
else {
customerCartItemsDownloadJsonWithURl(cartApi: customerCartApi)
}
}else if (loginCheck == 0) || (loginCheck == nil) {
if let token = UserDefaults.standard.value(forKey: "user_token") {
self.key = token as? String
let defaultValue = UserDefaults.standard.value(forKey: "MenuButton") as? String
if (defaultValue == "Tab" && itemCode == nil) {
check = true
cartCountApiDownloadJsonWithURL(cartCountApi: getCartApi)
}
} else {
gettingKeyFromJsonWithURL(keyApi: keygettingApi)
}
if itemCode != nil && key != nil {
self.PostingKeyFromJsonWithURL(PostingApi: postingKeyApi)
}
if key != nil {
cartCountApiDownloadJsonWithURL(cartCountApi: getCartApi)
emptyView.isHidden = true
}
}
}
Rather than using notification or delegate or relevant, viewWillApear() method will be more suitable for reloading views. Just put your code which you want to be reloaded into viewWillApear()
you can set notification whenever your cart view will appear and set method for reloading when notification fire.
Check the part of code where you are setting the badge icon count for "Cart". You need to persist the cart data in the same code flow.
Since you are having a boolean check in viewWillAppear, retrieve the cart data before performing the check.
It is a good idea to have CoreData for local persistence in case you are not syncing it with any server.

How to stop viewWillAppear from completing until a function has finished

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.

Reference variable whose value is assigned inside block

Hi had a confusion on what is the scope of the value that is assigned to outside ref variable inside a completion block. For example in the below code will the values of operationError and savedRecords persist outside of completion block.
func applyLocalChangesToServer(insertedOrUpdatedCKRecords:Array<CKRecord>,deletedCKRecordIDs:Array<CKRecordID>) throws
{
var savedRecords:[CKRecord]?
var conflictedRecords:[CKRecord] = [CKRecord]()
var removeRecords:[CKRecord] = [CKRecord]()
var operationError : NSError?
let ckModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave:insertedOrUpdatedCKRecords, recordIDsToDelete: deletedCKRecordIDs);
ckModifyRecordsOperation.atomic = true
ckModifyRecordsOperation.modifyRecordsCompletionBlock = ({(savedRecords1,deletedRecordIDs1,error)->Void in
operationError = error
if error == nil
{
wasSuccessful = true
savedRecords = savedRecords1
}
else
{
wasSuccessful = false
savedRecords = nil
errorCKS = self.handleError(error!)
}
})
ckModifyRecordsOperation.perRecordCompletionBlock = ({(ckRecord,error)->Void in
if error != nil
{
if error!.code == CKErrorCode.ServerRecordChanged.rawValue
{
conflictedRecords.append(ckRecord!)
}
}
})
self.operationQueue?.addOperation(ckModifyRecordsOperation)
self.operationQueue?.waitUntilAllOperationsAreFinished()
if conflictedRecords.count > 0
{
//Do work here
}
else if operationError != nil //Other then the partial error
{
throw operationError
}
}
Note: Had assign operationError since the func applyLocalChangesToServer throws an error and is inside a while loop.
Your assumption is correct, these variables defied in the enclosure scope will be modified after the completion handler is performed. So you code should work as expected.
Also you can use following:
ckModifyRecordsOperation.main()
instead of:
self.operationQueue?.addOperation(ckModifyRecordsOperation)
self.operationQueue?.waitUntilAllOperationsAreFinished()
Hope it helps.

Resources