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!
Related
So, basically, I've been launching the app with existing .xib VC and Swift file, but i've met a problem: despite all the connections were properly distributed, when launched, app didn't show my VC, it basically showed transparent view(bottom sheet). After checking the fact that I did everything as I should've done, I've tried to re-create things, deleted my old Swift file and storyboard VC and created a new one.
My storyboard VC and Swift VC
As you may see on this image, I've connected everything properly, name of custom class is correct, I have even filled in ID's, despite I don't use it. And even though, dragging my views form .xib to my Swift file does nothing. Then I've tried to manually write the code of #IBOutlet and connected views - it seemed right, but issue remained: app was crushing when I tried to launch this VC. Can't get what my problem is and rescuing some help on this case.
Here is my swift file:
import Foundation
import UIKit
import Alamofire
import AlamofireImage
class MarkerPopUpView: UIViewController {
var id = "254"
func getID(title: String) {
id = title
}
func getImage(title: String) {
var url = String()
for marker in arrOfMarkers {
if (marker.id) == Int(title) {
url = marker.getImage
streetNameView.text = marker.street
}
}
AF.request(url).responseImage { response in
debugPrint(response)
if case .success(let image) = response.result {
self.markerImage.image = image.af.imageRounded(withCornerRadius: 16)
self.markerImage.layer.cornerRadius = 12
print ("Success!")
}
else{
self.markerImage.image = UIImage(named: "Illustration")
self.markerImage.layer.cornerRadius = 12
let error = response.error
print ("ERROR: \(String(describing: error))")
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
getImage(title: id)
}
}
UPDATE at the bottom.
I have followed the UIKit section of this Apple iOS Dev Tutorial, up to and including the Saving New Reminders section. The tutorials provide full code for download at the beginning of each section.
But, I want to get FirebaseFirestore involved. I have some other Firestore projects that work, but I always thought that I was doing something not quite right, so I'm always looking for better examples to learn from.
This is how I found Peter Friese's 3-part YT series, "Build a To-Do list with Swift UI and Firebase". While I'm not using SwiftUI, I figured that the Firestore code should probably work with just a few changes, as he creates a Repository whose sole function is to interface between app and Firestore. No UI involved. So, following his example, I added a ReminderRepository.
It doesn't work, but I'm so close. The UITableView looks empty but I know that the records are being loaded.
Stepping through in the debugger, I see that the first time the numberOfRowsInSection is called, the data hasn't been loaded from the Firestore, so it returns 0. But, eventually the code does load the data. I can see each Reminder as it's being mapped and at the end, all documents are loaded into the reminderRepository.reminders property.
But I can't figure out how to get the loadData() to make the table reload later.
ReminderRepository.swift
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
init() {
loadData()
}
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
}
The only difference from the Apple code is that in the ListDataSource.swift file, I added:
var remindersRepository: ReminderRepository
override init() {
remindersRepository = ReminderRepository()
}
and all reminders references in that file have been changed to
remindersRepository.reminders.
Do I need to provide a callback for the init()? How? I'm still a little iffy on the matter.
UPDATE: Not a full credit solution, but getting closer.
I added two lines to ReminderListViewController.viewDidLoad() as well as the referenced function:
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshTournaments(_:)), for: .valueChanged)
#objc
private func refreshTournaments(_ sender: Any) {
tableView.reloadData()
refreshControl?.endRefreshing()
}
Now, when staring at the initial blank table, I pull down from the top and it refreshes. Now, how can I make it do that automatically?
Firstly create some ReminderRepositoryDelegate protocol, that will handle communication between you Controller part (in your case ReminderListDataSource ) and your model part (in your case ReminderRepository ). Then load data by delegating controller after reminder is set. here are some steps:
creating delegate protocol.
protocol ReminderRepositoryDelegate: AnyObject {
func reloadYourData()
}
Conform ReminderListDataSource to delegate protocol:
class ReminderListDataSource: UITableViewDataSource, ReminderRepositoryDelegate {
func reloadYourData() {
self.tableView.reloadData()
}
}
Add delegate weak variable to ReminderRepository that will weakly hold your controller.
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
weak var delegate: ReminderRepositoryDelegate?
init() {
loadData()
}
}
set ReminderListDataSource as a delegate when creating ReminderRepository
override init() {
remindersRepository = ReminderRepository()
remindersRepository.delegate = self
}
load data after reminder is set
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
delegate?.reloadYourData()
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
Please try changing var reminders = [Reminder]() to
var reminders : [Reminder] = []{
didSet {
self.tableview.reloadData()
}
}
Hi I am in desperate need for some help
All this is happening in a UIViewController child class
I am currently attaching the listener and populating an array and then feeding it to a UICollectionView in the following function (excuse some of the cut off code):
fileprivate func fetchNotes() { // This function is called in vidDidLoad()
let db = Firestore.firestore()
// Attaching listener (ie. listener is an attribute of the class)
listener = db.collection("Courses").document(titleForNavBar).collection("Notes")
.addSnapshotListener { snapshot, error in
// checking for any error
if error != nil {
self.arrayOfNotes.removeAll()
self.allNotesView.arrayOfNotes = self.arrayOfNotes
DispatchQueue.main.async {
self.allNotesView.allNotesCollectionView.reloadData()
}
return
} else {
self.arrayOfNotes.removeAll()
// if there is no error, the array holding all the objects is populated, in a for..loop
for document in (snapshot?.documents)! {
if let noteName = document.data()["noteName"] as? String,
let lectureInformation = document.data()["lectureInformation"] as? String,
let noteDescription = document.data()["noteDescription"] as? String,
let forCourse = document.data()["forCourse"] as? String,
let storageReference = document.data()["storageReference"] as? String,
let noteSize = document.data()["noteSize"] as? Int,
let rating = document.data()["rating"] as? Int
{
self.arrayOfNotes.append(Note(forCourse: forCourse, lectureInformation: lectureInformation, noteDescription: noteDescription, noteName: noteName, noteSize: noteSize, rating: rating, storageReference: storageReference))
self.allNotesView.arrayOfNotes = self.arrayOfNotes
// reloading the UICollectionView (on the main thread) so that it displays new data
DispatchQueue.main.async {
self.allNotesView.allNotesCollectionView.reloadData()
}
}
}
}
}
}
When the view disappears, I am also removing the listener
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(true)
if listener != nil {
listener.remove()
}
print("listener removed")
}
This works fine, when I install the application for the first time on any device or simulator. When I try to launch the controller, the second time, I get a very nasty error that I have no idea how to debug.
To be accurate the console throws this error:
NoteShare[97230:10528984] *** Assertion failure in -[FSTLevelDBRemoteDocumentCache decodedMaybeDocument:withKey:], third_party/firebase/ios/Source/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm:152
I know this question is quite long (sorry about that), but have you guys come across this error. Please give some hint on how to solve this problem. Thanks! If you need to see any other piece of my code, please let me know.
It seems to be failing here. I don't see what you could be doing wrong in your code to cause that, so you may have hit a bug. It seems very similar to this issue, which has been fixed in the repo but not been released.
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.
I'm noticing an interesting behaviour. I've been testing the performance of constructing a linked list with many elements. For some reason, past a certain amount of deallocations, the test with crash.
Here's my LinkedList implementation:
class LinkedList<T> {
let data: T
var next: LinkedList?
init(data: T, next: LinkedList? = nil) {
self.data = data
self.next = next
}
func cons(_ data: T) -> LinkedList {
return LinkedList(data: data, next: self)
}
}
I am testing this using the XCTest library. I made this test function:
let number = 104633
func testPerformanceExample() {
self.measure {
var list = LinkedList<Int>(data: 5)
for i in 0..<number {
list = list.cons(i)
}
}
}
I spent a fair amount of time trying to home into this number. It seems that if I try to construct a LinkedList with 104634 nodes, I get a Thread 1: EXC_BAD_ACCESS (code=2, address=0x7fff5a059ff8) crash, and the debug navigator shows a tower of LinkedList.deinit calls:
Another interesting thing is that if you move the list outside of the test function, it no longer crashes:
var list = LinkedList<Int>(data: 5)
func testPerformanceExample() {
self.measure {
for i in 0..<self.number {
self.list = self.list.cons(i)
}
}
}
I curious as to why a long series of deallocations can cause a crash. Thanks in advance!
EDIT:
This crash also occurs when you run the code outside of a XCTestCase. I've got this code in a UIViewController:
class ViewController: UIViewController {
let number = 1046340
override func viewDidLoad() {
super.viewDidLoad()
let date = Date()
var list = LinkedList<Int>(data: 0, next: nil)
for i in 0..<number {
list = list.cons(i)
}
let timeInterval = Date().timeIntervalSince(date)
print(timeInterval)
}
}
I could not compile your test.
This code worked with 100x as many nodes. I don't think the self reference is your problem but it does indicate that you are using an older Swift version. Upgrade your tools and try again.