This question already has answers here:
What does "Fatal error: Unexpectedly found nil while unwrapping an Optional value" mean?
(16 answers)
Closed 3 years ago.
Can someone please look trough my code and tell me why my value is nil. If you need me to post other files I will. The project is mostly firebase and the app crashes whenever you try to post within the application. I believe it is crashing when the screen has to be updated because the data is hitting the database.
import UIKit
import FirebaseFirestore
class FeedVC: UITableViewController {
var db = Firestore.firestore()
var postArray = [Posts]()
override func viewDidLoad() {
super.viewDidLoad()
//db = Firestore.firestore()
//loadData()
// checkForUpdates()
}
override func viewDidAppear(_ animated: Bool){
loadData()
checkForUpdates()
}
func loadData() {
db.collection("posts").getDocuments() {
querySnapshot, error in
if let error = error {
print("\(error.localizedDescription)")
}else{
self.postArray = querySnapshot!.documents.flatMap({Posts(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
func checkForUpdates() {
db.collection("posts").whereField("timeStamp", isGreaterThan: Date())
.addSnapshotListener {
querySnapshot, error in
guard let snapshot = querySnapshot else {return}
snapshot.documentChanges.forEach {
diff in
if diff.type == .added {
self.postArray.append(Posts(dictionary: diff.document.data())!)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
#IBAction func composePost(_ sender: Any) {
let composeAlert = UIAlertController(title: "New Post", message: "Enter your name and message", preferredStyle: .alert)
composeAlert.addTextField { (textField:UITextField) in
textField.placeholder = "Your name"
}
composeAlert.addTextField { (textField:UITextField) in
textField.placeholder = "Your message"
}
composeAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
composeAlert.addAction(UIAlertAction(title: "Send", style: .default, handler: { (action:UIAlertAction) in
if let name = composeAlert.textFields?.first?.text, let content = composeAlert.textFields?.last?.text {
let newSweet = Posts(name: name, content: content, timeStamp: Date())
var ref:DocumentReference? = nil
ref = self.db.collection("posts").addDocument(data: newSweet.dictionary) {
error in
if let error = error {
print("Error adding document: \(error.localizedDescription)")
}else{
print("Document added with ID: \(ref!.documentID)")
}
}
}
}))
self.present(composeAlert, animated: true, completion: nil)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let sweet = postArray[indexPath.row]
cell.textLabel?.text = "\(sweet.name): \(sweet.content)"
cell.detailTextLabel?.text = "\(sweet.timeStamp)"
return cell
}
}
import Foundation
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
struct Posts {
var name:String
var content:String
var timeStamp:Date
var dictionary:[String:Any] {
return [
"name":name,
"content" : content,
"timeStamp" : timeStamp
]
}
}
extension Posts : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let timeStamp = dictionary ["timeStamp"] as? Date else {return nil}
self.init(name: name, content: content, timeStamp: timeStamp)
}
}
use TimeStamp instead of Date
import FirebaseFirestore
guard let stamp = data["timeStamp"] as? Timestamp else { return nil }
This is the line that's crashing according to the comments...
self.postArray.append(Posts(dictionary: diff.document.data())!)
and it's crashing because you are force-unwrapping a variable that's nil
Posts(dictionary: diff.document.data())! <- ! means 'I guarantee you will never be nil'
And it's nil because of this Posts extension...
extension Posts : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let timeStamp = dictionary ["timeStamp"] as? Date else {return nil}
and the offending code is here
guard
let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let timeStamp = dictionary ["timeStamp"] as? Date
else {return nil} //OFFENDING CODE!
If any one of those vars is not present in the dictionary, it fails, and returns nil.
So... one of your documents does not contain one of those fields.
You may also want to refer to the Firestore Guide on how to write dates. Check out the example code in Add Data
Also, Firestore doesn't have an internal Date type so use Timestamp.dateValue instead. See it here Timestamp
Related
I am new to Swift development, so sorry if this is a stupid question. I'm having issues with saving the Firestore document ID to the cell of my to do.
My goal:
Save the document ID of the to-do so it can be used in my ChangeButton protocol.
The app is a to-do list-style app. The changeButton refers to changing the button from an empty circle to a filled circle.
My cellForRowAt in my mainViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 && indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "inputCell", for: indexPath) as! InputCell
cell.delegate = self
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath) as! TaskCell
let current = sections[indexPath.section].items[indexPath.row]
cell.taskNameLabel.text = current.name
if current.checked {
cell.checkBoxOutlet.setBackgroundImage(#imageLiteral(resourceName: "checkBoxFILLED "), for: UIControl.State.normal)
} else {
cell.checkBoxOutlet.setBackgroundImage(#imageLiteral(resourceName: "checkBoxOUTLINE "), for: UIControl.State.normal)
}
cell.delegate = self
cell.items = sections[indexPath.section].items
cell.indexSection = indexPath.section
cell.indexRow = indexPath.row
cell.itemID = sections[indexPath.section].items[indexPath.row].itemID
// print("cell.itemID is \(cell.itemID)")
// print("sections.itemID is \(sections[indexPath.section].items[indexPath.row].itemID)")
return cell
}
}
My changeButton function in mainViewController
func changeButton(state: Bool, indexSection: Int?, indexRow: Int?, itemID: String?) {
print("The item ID is \(itemID)")
print("The item ID section is \(sections[indexSection!].items[indexRow!].itemID)")
sections[indexSection!].items[indexRow!].checked = state
print("Line 175 ID is \(itemID)")
if let itemID = itemID {
let itemRef = db.collection(K.FStore.lists).document(currentListID!).collection(K.FStore.sections).document("\(indexSection!)").collection(K.FStore.items).document(itemID)
if sections[indexSection!].items[indexRow!].checked {
itemRef.updateData([
K.FStore.isChecked : true,
K.FStore.checkedBy: currentUserID!
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
} else {
itemRef.updateData([
K.FStore.isChecked : false
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
} else {
print("No item ID")
}
tableView.reloadData()
}
My loadItems and loadFunctions in my mainViewController
func loadItems(listID: String, section: Int) {
let itemRef = db.collection(K.FStore.lists).document(listID).collection(K.FStore.sections).document("(section)").collection(K.FStore.items)
var itemArray = Task
itemRef.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
let name = document.data()["name"] as? String
let isChecked : Bool = (document.data()["isChecked"] != nil)
let newItem = Task(name: name ?? "FIREBASE ERROR", isChecked: isChecked)
itemArray.append(newItem)
// print(newItem.checked)
}
}
// print(itemArray)
self.sections[section].items = itemArray
self.tableView.reloadData()
}
}
//MARK: - Load sections
func loadSections(listID: String) {
let listRef = db.collection(K.FStore.lists).document(listID)
listRef.getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
let sectionNames = document.data()!["sections"] as? [String]
if let sectionNames = sectionNames {
for (index, item) in sectionNames.enumerated() {
let newSection = Section(name: item, isExpanded: true, items: [])
self.sections.append(newSection)
self.loadItems(listID: listID, section: index)
}
}
self.tableView.reloadData()
} else {
print("Document does not exist")
}
}
}
My Task class
class Task {
var name = ""
var checked = false
var date = Date()
var category: String
var number: Int
var itemID: String?
My TaskCell
protocol ChangeButton {
func changeButton(state: Bool, indexSection: Int?, indexRow: Int?, itemID: String?)
}
class TaskCell: UITableViewCell {
#IBAction func checkBoxAction(_ sender: Any) {
// print("The item ID is \(itemID)")
if items![indexRow!].checked {
delegate?.changeButton(state: false, indexSection: indexSection!, indexRow: indexRow!, itemID: itemID)
print("Line 22 \(itemID)")
} else {
delegate?.changeButton(state: true, indexSection: indexSection!, indexRow: indexRow!, itemID: itemID)
print("Line 25 \(itemID)")
}
}
#IBOutlet weak var taskNameLabel: UILabel!
#IBOutlet weak var checkBoxOutlet: UIButton!
var delegate: ChangeButton?
var indexSection: Int?
var indexRow: Int?
var tasks: [[Task]]?
var items: [Task]?
var itemID: String?
}
I am completely lost in how I can fix this. As you can see, I've tried a lot of print statements to figure out where the itemID can be loaded.
This is what I get back from those print statements:
Line 25 nil
The item ID is nil
The item ID section is nil
Line 175 ID is nil
No item ID
Line 22 nil
Please let me know if I forgot to include anything, and sorry for this extremely long post.
Thanks a ton,
Matt
Firstly,
We don't trust cells because it's reusable so we need a static class.
When cell will be reuse you can lost your data
I create some examples. Maybe it'll be helpful
// This is our model
class Task {
var name = ""
var checked = false
var date = Date()
var category: String
var number: Int
var itemID: String?
public init() {
self.category = ""
self.number = 0
}
}
// Extension for init from firebase response
extension Task {
convenience init(with firebase: [String: Any]) {
self.init()
self.name = (firebase["name"] as? String) ?? ""
}
}
// We create service for document
// We use this service like an API
final class DocumentService {
static let shared = DocumentService()
private let database: FirebaseDatabase
private var tasks: [[Task]] = []
public init(database: FirebaseDatabase = FirebaseDatabase()) {
self.database = database
}
func load(in section: Int, completion: #escaping (([Task]) -> Void)) {
database.loadData(section: section) { [unowned self] tasks in
self.tasks[section] = tasks.map(Task.init)
completion(self.tasks[section])
}
}
func check(at indexPath: IndexPath, isChecked: Bool) {
tasks[indexPath.section][indexPath.row].checked = isChecked
}
}
// We create firebase database class we can add some features in here
final class FirebaseDatabase {
func loadData(section: Int, completion: #escaping (([[String: Any]]) -> Void)) {
// TODO: firebase load data
let response: [[String: Any]] = [
["name": "Stackoverflow"]
]
completion(response)
}
}
final class TestController: UIViewController {
private let service = DocumentService.shared
override func viewDidLoad() {
super.viewDidLoad()
service.load(in: 0) { tasks in
// TODO
}
}
}
Thank you for your answer, #Vicaren. Fortunately, the solution was more simple than that. In the end, I found that I forgot to pass in the itemID argument in the loadItems() function. Thank you.
I'm using Swift and Firestore database to implement an app like Twitter.
I want to add sweet (it's like tweet) when button is clicked to the database. And then display it in the tableview.
The data is added to the database. But is not displayed in the tableview. So when I run an app I see empty tableview.
Please help!!
TableViewController file:
import UIKit
import FirebaseFirestore
import Firebase
class TableViewController: UITableViewController {
var db:Firestore!
var sweetArray = [Sweet]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
loadData()
}
func loadData() {
db.collection("sweets").getDocuments() {
querySnapshot, error in
if let error = error {
print("Error loading documents to the db: \(error.localizedDescription)")
} else {
self.sweetArray = querySnapshot!.documents.flatMap({Sweet(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
#IBAction func composeSweet(_ sender: Any) {
let composeAlert = UIAlertController(title: "New Sweet", message: "Enter your name and message", preferredStyle: .alert)
composeAlert.addTextField { (textField:UITextField) in
textField.placeholder = "Your name"
}
composeAlert.addTextField { (textField:UITextField) in
textField.placeholder = "Your message"
}
composeAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
composeAlert.addAction(UIAlertAction(title: "Send", style: .default, handler: { (action:UIAlertAction) in
if let name = composeAlert.textFields?.first?.text, let content = composeAlert.textFields?.last?.text {
let newSweet = Sweet(name: name, content: content, timeStamp: Date())
var ref:DocumentReference? = nil
ref = self.db.collection("sweets").addDocument(data: newSweet.dictionary) {
error in
if let error = error {
print("Error adding document: \(error.localizedDescription)")
} else {
print("Document added with ID: \(ref!.documentID)")
}
}
}
}))
self.present(composeAlert, animated: true, completion: nil)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sweetArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let sweet = sweetArray[indexPath.row]
cell.textLabel?.text = "\(sweet.name) : \(sweet.content)"
cell.detailTextLabel?.text = "\(sweet.timeStamp)"
return cell
}
}
Sweet file:
import Foundation
import FirebaseFirestore
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
struct Sweet {
var name: String
var content: String
var timeStamp: Date
var dictionary:[String:Any] {
return [
"name": name,
"content": content,
"timeStamp": timeStamp
]
}
}
extension Sweet:DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let timeStamp = dictionary["timeStamp"] as? Date else {return nil}
self.init(name: name, content: content, timeStamp: timeStamp)
}
}
My storyboards:
My running app:
I can't provide a specific answer but I can explain how to find what the issue is.
While adding guard statements to protect your code is awesome, it can also lead to issues not being handled appropriately.
Take this piece of code from your question for example
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let timeStamp = dictionary["timeStamp"] as? Date else {return nil}
As you can see if there's some issue with name, content or timestamp, the guard will catch it - however, returning nil means it silently fails with no indication of the problem.
Suppose for example, that a field name was accidentally called Name instead of name - well, that's going to fail but you'd never know it.
I suggest handling fields separately to catch specific problems. Like this
let name = dictionary["name"] as? String ?? "name field not found"
let name = dictionary["content"] as? String ?? "content field not found"
let name = dictionary["timesStamp"] as? Date ?? "timestamp field not found"
This is called nil coalescing and will substitute a default value in case of nil. By then examining the incoming data, you can find the document that caused the issue. You could also do this
guard let name = dictionary["name"] as? String else { //handle the error }
in either case, you then have more data about the nature of the failure.
Seems you have data in querySnapshot but empty in sweetArray which means only one this your are parsing and mapping the data received into structs incorrectly. Modify this line to fix your issue:
self.sweetArray = querySnapshot!.documents.flatMap({Sweet(dictionary: $0.data())})
private var refernceCollection: CollectionReference!
database = Firestore.firestore()
refernceCollection = Firestore.firestore().collection(kMessages)
func fetchData() {
refernceCollection.addSnapshotListener{ snapshots, error in
if error != nil {
print("error --->>")
} else {
guard let snap = snapshots else { return }
var arrUser:[MDLMessages] = []
for documet in snap.documents {
let data = documet.data()
let message = data["message"] as? String ?? "This message was deleted"
let time = data["time"] as? Date ?? Date.now
let documentId = documet.documentID
let userId = data["userId"] as? String ?? ""
let details = MDLMessages(message: message, time: time, documentId: documentId, userId: userId)
arrUser.append(details)
}
DispatchQueue.main.async {
self.arrMessages = arrUser
self.tblChatDetails.reloadData()
}
}
}
}
I have my data structure: My Firestore Database
As you'll see I have a "Michael 201A" document as well as a "Michael 201B" the idea is to retrieve the fields from these documents and display them in a tableView. Additionally, i would like the tableView to update automatically based off of any new documents that are added to the "Requests" Collection so the tableView data is always populated wit the most recent additions to the firestore database.
Function to retrieve data from FireStore
#IBOutlet weak var tableView: UITableView!
var db: Firestore!
var requestArray = [Request]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
tableView.delegate = self
tableView.dataSource = self
loadData()
}
func loadData() {
db.collection("Requests").whereField("Status", isEqualTo: true).getDocuments() {(querySnapshot, err) in
if let err = err {
print("An error occurred\(err)")
} else{
self.requestArray = querySnapshot!.documents.compactMap({Request(dictionary: $0.data())})
print(self.requestArray)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
I've added a print statement to get a reading of the value but it returns empty.
My tableView functions
extension ResidentAdvisorViewController: UITableViewDelegate {
func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You tapped me")
}
}
extension ResidentAdvisorViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return requestArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let request = requestArray[indexPath.row]
cell.textLabel?.text = "\(request.Name)"
return cell
}
}
My Request Struct
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
struct Request {
var Name: String
var Dorm: String
var Room: Int
var Status: Bool
var UID: String
var TimeStamp: Date
var dictionary:[String:Any] {
return [
"Name":Name,
"Dorm":Dorm,
"Room":Room,
"Status":Status,
"UID": UID,
"TimeStamp": TimeStamp
]
}
}
extension Request : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["Name"] as? String,
let dorm = dictionary["Dorm"] as? String,
let room = dictionary["Room"] as? Int,
let status = dictionary["Status"] as? Bool,
let uid = dictionary["UID"] as? String,
let timestamp = dictionary["Timestamp"] as? Date
else { return nil}
self.init(Name: name, Dorm: dorm, Room: room, Status: status, UID: uid, TimeStamp: timestamp)
}
}
As a side note i have checked to ensure my cell identifier matches "cell". Also, when i change the cell text to "Hello World" I am able to get it displayed in my tableView. Any assistance is greatly appreciated thank you.
There's not a whole lot wrong with the code but there are two questions within the question.
1) Why is the value empty
2) How to I populate my dataSource intially and then update it when new documents are added.
Let me address 2) first.
To initially load the data and then watch for future changes, we can uyse the .addSnapshotListener function, and then handle the specific change type within the firebase closure.
func observeAllRequests() {
let requestsCollection = self.db.collection("Requests")
let query = requestsCollection.whereField("Status", isEqualTo: true)
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let name = diff.document.get("Name") as? String ?? "No Name"
print("added: \(name)") //add to your dataSource
}
if (diff.type == .modified) {
let name = diff.document.get("Name") as? String ?? "No Name"
print("modified: \(name)") //update the request in the dataSource
}
if (diff.type == .removed) {
let name = diff.document.get("Name") as? String ?? "No Name"
print("removed: \(name)") //remove the request from the dataSource
}
}
//tableView.reloadData()
}
}
The above code will return all of the documents that match the query. Iterate over the items in the snapshot, with each being either .added, .modified or .removed. The first time the function is called, all differences will be .childAdded which allows you to initially populate the dataSource.
Any document changes after that will be just the document that was changed with the difference being by .added, .modified and .removed.
EDIT:
To address question 1)
The reason the array is empty is because of how the extension is structured - it's pretty much an all or none. Here's how it is now
extension Request : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String
let dorm = dictionary["Dorm"] as? String,
let room = dictionary["Room"] as? Int,
let status = dictionary["Status"] as? Bool,
let uid = dictionary["UID"] as? String,
let timestamp = dictionary["Timestamp"] as? String
else { return nil}
self.init(Name: name)
} }
If a field is not found then the entire thing fails and returns nil, and compactMap igores nil so you end up when an empty array. Your structure does not include Timestamp, so it fails.
I would suggest something to protect your code but allow for missing fields. The nil-coalescing operator would work well here
extension Request : DocumentSerializable {
init?(dictionary: [String : Any]) {
let name = dictionary["name"] as? String ?? "No Name"
let room = dictionary["room") as? String ?? "No Room"
etc
I am attempting to show my Firestore data into my Tableview but I can't seem to get it to show up.
protocol DocumentSerializeable {
init?(dictionary:[String:Any])
}
struct Sourse {
var name: String
var content: String
var timeStamp: Date
var dictionary: [String: Any] {
return [
"name": name,
"content": content,
"timestamp": timeStamp
]
}
}
extension Sourse : DocumentSerializeable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let timeStamp = dictionary["timeStamp"] as? Date else {return nil}
self.init(name: name, content: content, timeStamp: timeStamp)
}
}
class SourseListTableViewController: UITableViewController {
var db: Firestore!
var sourseArray = [Sourse]()
private var document: [DocumentSnapshot] = []
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
//initalize Database
db = Firestore.firestore()
loadData()
}
At first I tried this code below, there were no errors but nothing loaded in the tableview.
func loadData() {
db.collection("sourses").getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
self.sourseArray = snapshot!.documents.flatMap({Sourse(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
After some research (reading from Firestore - Append to tableView when view is loaded ) I tried this code below, but I get the error "Cannot convert value of type '(name: String, content: String, timeStamp: Date?)' to expected argument type 'Sourse'" So I tried it with the date removed from all the code and I still couldn't get it to work.
func loadData() {
db.collection("sourses").getDocuments() {
snapshot, error in
if let error = error {
print("\(error.localizedDescription)")
} else {
for document in snapshot!.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date
let newSourse = (name:name, content:content, timeStamp: timeStamp)
self.sourseArray.append(newSourse)
}
}
}
}
Here is my numberOfRows/CellForRow to make sure its not the tableview itself. I've also double checked the "cell identifier" with my storyboard.
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return sourseArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SourseTableViewCell", for: indexPath)
let sourse = sourseArray[indexPath.row]
cell.textLabel?.text = "\(sourse.name)"
cell.detailTextLabel?.text = "\(sourse.content)"
return cell
}
You need to reload your tableView after parsing your Snapshot. It is also not a good idea to force unwrap your Snapshot:
.getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date ?? Date()
let newSourse = Sourse(name:name, content:content, timeStamp: timeStamp)
self.sourseArray.append(newSourse)
}
self.tableView.reloadData()
}
}
rbaldwin talked me through it, his answer is correct, I'm just posting the full loaddata function for the record.
func loadData() {
db.collection("sourses").getDocuments() { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date ?? Date()
let newSourse = Sourse(name:name, content:content, timeStamp: timeStamp)
self.sourseArray.append(newSourse)
}
self.tableView.reloadData()
}
}
}
}
I'm trying to find where is the nil when unwrapping. Here is the piece of code I have. The lines where the fatal errors are found are at:
1st file:
date = dateFormatter().date(from: dictionary[kDATE] as! String)!
2nd file:
self.allLists.append(ShoppingList.init(dictionary: currentList))
This is from the shoppingList.swift file and the function is called in a controller
import Foundation
import Firebase
class ShoppingList{
let name: String
var totalPrice: Float
var totalItems: Int
var id: String
var date: Date
var ownerId: String
init(_name: String, _totalPrice: Float = 0, _id: String = "") {
name = _name
totalPrice = _totalPrice
totalItems = 0
id = _id
date = Date()
ownerId = "1234"
}
//creates shopping list item from this dictionary
init(dictionary: NSDictionary) {
name = dictionary[kNAME] as! String
totalPrice = dictionary[kTOTALPRICE] as! Float
totalItems = dictionary[kTOTALITEMS] as! Int
id = dictionary[kSHOPPINGLISTID] as! String
date = dateFormatter().date(from: dictionary[kDATE] as! String)!
ownerId = dictionary[kOWNERID] as! String
}
func dictionaryFromItem(item: ShoppingList) -> NSDictionary {
return NSDictionary(objects: [item.name, item.totalPrice, item.totalItems, item.id, dateFormatter().string(from: item.date), item.ownerId], forKeys: [kNAME as NSCopying, kTOTALPRICE as NSCopying, kTOTALITEMS as NSCopying, kSHOPPINGLISTID as NSCopying, kDATE as NSCopying, kOWNERID as NSCopying])
}
Here is the controller:
import UIKit
import KRProgressHUD
class AllListsViewController: UIViewController, UITableViewDataSource,UITableViewDelegate{
#IBOutlet weak var tableView: UITableView!
var allLists:[ShoppingList] = []
var nameTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
loadLists()
}
//MARK: TableView DataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allLists.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let shoppingList = allLists[indexPath.row]
cell.textLabel?.text = shoppingList.name
return cell
}
//MARK: IBActions
#IBAction func addBarButonItemPressed(_ sender: Any) {
let alertController = UIAlertController(title: "Create Shopping List", message: "Enter the shopping list name", preferredStyle: .alert)
alertController.addTextField{ (nameTextField) in
nameTextField.placeholder = "Name"
self.nameTextField = nameTextField
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel){ (action) in
}
let saveAction = UIAlertAction(title: "Save", style: .default){ (action) in
if self.nameTextField.text != ""{
self.createShoppingList()
}else{
KRProgressHUD.showWarning(message: "Name is empty!")
}
}
alertController.addAction(cancelAction)
alertController.addAction(saveAction)
self.present(alertController,animated: true, completion:nil)
}
//MARK: LoadList
func loadLists(){
//.values has all the info of the child
firebase.child(kSHOPPINGLIST).child("1234").observe(.value, with: {
snapshot in
self.allLists.removeAll()
//if we actually received smthing from firebase
if snapshot.exists(){
let sorted = ((snapshot.value as! NSDictionary).allValues as NSArray).sortedArray(using: [NSSortDescriptor(key: kDATE,ascending: false)])
for list in sorted {
let currentList = list as! NSDictionary
self.allLists.append(ShoppingList.init(dictionary: currentList))
}
} else {
print("no snapshot")
}
self.tableView.reloadData()
})
}
//MARK: Helper functions
func createShoppingList(){
let shoppingList = ShoppingList(_name: nameTextField.text!)
shoppingList.saveItemInBackground(shoppingList: shoppingList){ (error) in
if error != nil{
KRProgressHUD.showError(message: "Error creating shopping list")
return
}
}
}
}
Also the data formatter is a small function in another file.
import Foundation
import UIKit
private let dateFormat = "yyyyMMDDHHmmss"
func dateFormatter() -> DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = dateFormat
return dateFormatter
}
So you have a forced downcast and a forced optional unwrap on this line:
date = dateFormatter().date(from: dictionary[kDATE] as! String)!
Either your dictionary isn't returning a string, or the string coming out of the dictionary isn't able to be processed as a date. My guess is it's the first problem as dates are often stored as epoch.
Try this instead of the line above. Add a breakpoint at the top and step through:
print(dictionary[kDATE])
if let dictValue = dictionary[kDATE] as? String {
print(dictValue)
if let unwrappedDate = dateFormatter().date(from: dictValue) {
date = unwrappedDate
}
}
If it fails on the first if-let then the return value is not a string. If it fails on the second the problem lies with the date formatter being unable to read the format.
The first print might give you a clue as to what type to cast to, the second could help you fix the format.
Try to be careful when force unwrapping,
optionalVar!
or for downcasting.
unknownType as! Type
You should really only "use the force" when you're absolutely sure there's no way the value will be nil.