can you spot the nil while unwrapping? - ios

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.

Related

how to customizing swift firabase didselectedrowat

hello i am begginer to swift i get an error "Cannot assign value of type 'Character' to type '[String]'" how can i fix that my brain is now lost in this code blog enter code here
import UIKit
import FirebaseFirestore
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
class PhoneViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var phoneModelText = [String]()
var imeiAdressText = [String]()
var userNameText = [String]()
var idText = [String]()
var phoneNumberText = [String]()
var detailsText = [String]()
var dateText = [String]()
var priceText = [String]()
var adressText = [String]()
var selectedPhoneModelText = ""
var selectedimeiAdressText = ""
var selecteduserNameText = ""
var selectedidText = ""
var selectedphoneNumberText = ""
var selecteddetailsText = ""
var selecteddateText = ""
var selectedpriceText = ""
var selectedadressText = ""
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
tableView.dataSource = self
tableView.delegate = self
getdata()
}
func makeAlert(titleInput: String, messageInput : String) {
let alert = UIAlertController(title: titleInput, message: messageInput, preferredStyle: UIAlertController.Style.alert)
let okButton = UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: nil)
alert.addAction(okButton)
present(alert, animated: true, completion: nil)
}
func fetchBook(documentId: String) {
let db = Firestore.firestore()
let docRef = db.collection("Databases").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.makeAlert(titleInput: "alert", messageInput: "\(error.localizedDescription)")
}
else {
if let document = document {
let id = document.documentID
let data = document.data()
let phonemodel = data?["phoneName"] as? String ?? ""
let imeiadress = data?["imeiNumberText"] as? Int ?? 0
let username = data?["userNameText"] as? String ?? ""
let idcard = data?["idCardtext"] as? Int ?? 0
let phonenumber = data?["phoneNumberText"] as? Int ?? 0
let adress = data?["adressNameText"] as? String ?? ""
let details = data?["detailSectionText"] as? String ?? ""
let date = data?["currentDateText"] as? String ?? ""
let price = data?["priceValueText"] as? Int ?? 0
let image = data?["imageurl"] as? String ?? ""
DispatchQueue.main.async {
self.selectedphoneNumberText = phonemodel
self.phoneModelText.text = phonemodel
self.imeiAdressText.text = String(imeiadress)
self.userNameText.text = username
self.idText.text = String(idcard)
self.phoneNumberText.text = String(phonenumber)
self.adressText.text = adress
self.detailsText.text = details
self.dateText.text = date
self.priceText.text = String(price)
}
}
}
}
}
}
extension PhoneViewController : UITableViewDataSource,UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return phoneModelText.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = phoneModelText[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// how i customizing there
performSegue(withIdentifier: "toPhoneListView", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toPhoneListView" {
let destinationVC = segue.destination as! PhoneListViewController
destinationVC.selectedPhoneModelText
destinationVC.selectedimeiAdressText
destinationVC.selecteduserNameText
destinationVC.selectedidText
destinationVC.selectedphoneNumberText
destinationVC.selecteddetailsText
destinationVC.selecteddateText
destinationVC.selectedpriceText
destinationVC.selectedadressText
}
}
}
This is how your phoneModelText is defined
var phoneModelText = [String]()
that indicates that phoneModelText is an array of strings, so it would look something like this
phoneModelText[0] = "Some String"
phoneModelText[1] = "Another string"
but then later you're attempting to assign string to that array
self.phoneModelText.text = phonemodel
And that's not how arrays work. If you want to add phoneModel to the array it would be this
self.phoneModelText.append(phoneModel) //assume phoneModel = "yet another string"
so then the array would look like this
phoneModelText[0] = "Some String"
phoneModelText[1] = "Another string"
phoneModelText[2] = "yet another string"
In general I would suggest naming your vars so they more represent what they contain - instead of phoneModelText, call it phoneModelTextArray. That wil reduce confusion and make the code more readable.
As far as a solution, it's not clear why there are a bunch of arrays
var phoneModelText = [String]()
var imeiAdressText = [String]()
var userNameText = [String]()
but I suggest changing all of that around. One option is to define a class with properties and then have an array of classes
class ContactClass {
var id = ""
var phoneText = ""
var imeiAdressText = ""
var userNameText = ""
}
and then an array of classes within your controller
var contactArray = [ContactClass]()
and then lastly, when reading data from Firebase, instantiate the class, populate the class properties and add the class to the array
else {
if let document = document {
let contact = ContactClass()
contact.id = document.documentID
contact.phoneText = data?["phoneName"] as? String ?? ""
contact.imeiAdressText = data?["imeiNumberText"] as? Int ?? 0
contact.userNameText = data?["userName"] as? String ?? ""
self.contactArray.append(contact)

The data from Firestore database is not displayed in the tableview

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()
}
}
}
}

Fetching a contacts mailing address using the address label swift 5

How do I pull a contacts mailing address using the mailing address label?
The function buildContactsAddress_Array below builds an array containing the address label(name) and the address ID. The array is used to populate a tableView where the user can select the address by its name. I have included pretty much all of the related code to try and make things as clear as I can. Thanks in advance.
This is the part I want to change or replace to use address label. Right now it just uses the first/home address.
if let firstPostalAddress = (theName.postalAddresses.first),
let labelValuePair = firstPostalAddress.value(forKey: "labelValuePair") as? AnyObject,
let finalPostalAddress = labelValuePair.value(forKey: "value") as? CNPostalAddress
{
mailAddress = CNPostalAddressFormatter.string(from: finalPostalAddress, style: .mailingAddress)
}
struct contactAddresses
{
var theLabel: String
var theID: String
}
private var addressesArray = [contactAddresses]()
private var addressID: String = ""
private var theContactID: String = ""
This func pulls the contacts info using the contacts ID.
func getContactFromID_Ouote(contactID: String)
{
let store = CNContactStore()
var theName = CNContact()
let theKeys = [CNContactNamePrefixKey,
CNContactGivenNameKey,
CNContactFamilyNameKey,
CNContactOrganizationNameKey,
CNContactPostalAddressesKey,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)] as! [CNKeyDescriptor]
do {
theName = try store.unifiedContact(withIdentifier: contactID, keysToFetch: theKeys)
contactName = CNContactFormatter.string(from: theName, style: .fullName)!
contactPrefix = theName.namePrefix
contactFirst = theName.givenName
contactLast = theName.familyName
companyName = theName.organizationName == "" ? "" : theName.organizationName
} catch {
print("Fetching contact data failed: \(error)")
}
if let firstPostalAddress = (theName.postalAddresses.first),
let labelValuePair = firstPostalAddress.value(forKey: "labelValuePair") as? NSObject,
let finalPostalAddress = labelValuePair.value(forKey: "value") as? CNPostalAddress
{
mailAddress = CNPostalAddressFormatter.string(from: finalPostalAddress, style: .mailingAddress)
}
}
This func puts the contacts addresses into an array. The array is then used to populate a tableView.
func buildContactsAddress_Array(contactID: String)
{
let store = CNContactStore()
var theName = CNContact()
let theKeys = [CNContactPostalAddressesKey] as [CNKeyDescriptor]
do {
theName = try store.unifiedContact(withIdentifier: contactID, keysToFetch: theKeys)
let postalAddress = theName.postalAddresses
postalAddress.forEach { (mailAddress) in
// Strip forst 4 and last 4 from _$!<Home>!$_
let aaa = mailAddress.label
let bbb = aaa!.dropLast(4)
let ccc = bbb.dropFirst(4)
addressesArray.append(contactAddresses(theLabel: String(ccc), theID: mailAddress.identifier))
}
addressesArray.sort { $0.theLabel < $1.theLabel }
} catch {
print("Fetching contact addresses failed: \(error)")
}
}
This is the tableView extension. When a cell is tapped, addressID is populated with the ID of the appropriate mailing address.
extension QuotePreview_VC: UITableViewDelegate, UITableViewDataSource
{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return addressesArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let theCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
theCell.textLabel?.text = addressesArray[indexPath.row].theLabel
return theCell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
addressID = addressesArray[indexPath.row].theID
populateThePrintFld()
closeThePicker()
}
}
Here is what I ended up doing.
if addressesArray.count > 1
{
// Grab the address picked by the user in the TV
if let addressID = addressID
{
if let finalPostalAddress = theName.postalAddresses.first(where: { labelValuePair in labelValuePair.identifier == addressID })?.value
{
mailAddress = CNPostalAddressFormatter.string(from: finalPostalAddress, style: .mailingAddress)
}
}
} else {
// Grab the first available address
if let firstPostalAddress = (theName.postalAddresses.first),
let labelValuePair = firstPostalAddress.value(forKey: "labelValuePair") as? NSObject,
let finalPostalAddress = labelValuePair.value(forKey: "value") as? CNPostalAddress
{
mailAddress = CNPostalAddressFormatter.string(from: finalPostalAddress, style: .mailingAddress)
}
}

Xcode: Why is my optional value nill while unwrapping? [duplicate]

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

NSUser Defaults - Saving 3 variables together and then fetching

I'm using NSUserDefaults for persistent storage in my app. It is a game where after the game ends the score, together with the date and name (entered by user) must be stored and then shown in an table view. My code so far is:
import UIKit
class LeaderboardVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
var finishedGame = 0
var gameScore:Int! = 0
var name:String!
var date:String!
var score:Int!
var scoreData = [NSArray]()
var defaults = UserDefaults.standard
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
if finishedGame == 1{
saveNew()
}
self.tableView.delegate = self
self.tableView.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.isNavigationBarHidden = false
self.navigationItem.hidesBackButton = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func saveNew() {
let enterNameAlert = UIAlertController(title: "Please Enter Your Name", message: "This will be used to place you in the leaderboards", preferredStyle: .alert)
enterNameAlert.addTextField { (textField:UITextField) -> Void in
textField.placeholder = "Name"
textField.autocapitalizationType = UITextAutocapitalizationType.words
textField.autocorrectionType = UITextAutocorrectionType.no
textField.clearsOnBeginEditing = true
textField.clearsOnInsertion = true
textField.clearButtonMode = UITextFieldViewMode.always
textField.keyboardType = UIKeyboardType.default
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let confirmAction = UIAlertAction(title: "Confirm", style: .default) { (action:UIAlertAction) in
let currentTime = Date()
let timeFormatter = DateFormatter()
timeFormatter.locale = Locale.current
timeFormatter.dateFormat = "HH:mm dd/MM/yy"
let convertedTime = timeFormatter.string(from: currentTime) //date
let enteredName = enterNameAlert.textFields?.first?.text //name
// set object of date,name and score here
}
enterNameAlert.addAction(cancelAction)
enterNameAlert.addAction(confirmAction)
self.present(enterNameAlert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! LeaderBoardCell
cell.dateLabel?.text =
cell.dateLabel.adjustsFontSizeToFitWidth = true
cell.scoreLabel?.text =
cell.scoreLabel.adjustsFontSizeToFitWidth = true
cell.nameLabel?.text =
cell.nameLabel.adjustsFontSizeToFitWidth = true
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return scoreData.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
There are 3 labels in the cell one for each of the 3 values which must be displayed. How do I set these values using NSUserDefaults (They must be together FOR example score:500 goes with name:John and so on)?
EDIT - NEW CODE
import UIKit
class LeaderboardVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
var finishedGame = 0
var gameScore:Int! = 0
var defaults = UserDefaults.standard
var newUserArray = NSMutableArray()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
if finishedGame == 1{
saveNew()
}
self.tableView.delegate = self
self.tableView.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.isNavigationBarHidden = false
self.navigationItem.hidesBackButton = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func saveNew() {
let enterNameAlert = UIAlertController(title: "Please Enter Your Name", message: "This will be used to place you in the leaderboards", preferredStyle: .alert)
enterNameAlert.addTextField { (textField:UITextField) -> Void in
textField.placeholder = "Name"
textField.autocapitalizationType = UITextAutocapitalizationType.words
textField.autocorrectionType = UITextAutocorrectionType.no
textField.clearsOnBeginEditing = true
textField.clearsOnInsertion = true
textField.clearButtonMode = UITextFieldViewMode.always
textField.keyboardType = UIKeyboardType.default
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let confirmAction = UIAlertAction(title: "Confirm", style: .default) { (action:UIAlertAction) in
let currentTime = Date()
let timeFormatter = DateFormatter()
timeFormatter.locale = Locale.current
timeFormatter.dateFormat = "HH:mm dd/MM/yy"
let convertedTime = timeFormatter.string(from: currentTime)
let enteredName = enterNameAlert.textFields?.first?.text
let newUserData = self.defaults.object(forKey: "UserData") as! NSArray
let newUserArray = NSMutableArray(array: newUserData)
let newUserRecord = [
"Name" : enteredName!,
"Score" : String(self.gameScore),
"Date" : convertedTime
] as [String : String]
newUserArray.add(newUserRecord)
self.defaults.set(newUserArray, forKey: "UserData")
self.defaults.synchronize()
print(newUserArray)
print("hello")
print(self.defaults)
}
enterNameAlert.addAction(cancelAction)
enterNameAlert.addAction(confirmAction)
self.present(enterNameAlert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! LeaderBoardCell
let userData = defaults.object(forKey: "UserData") as! NSArray
cell.dateLabel?.text = "Date: \(((userData.object(at: indexPath.row) as! [String:Any])["Date"] as! String))"
cell.dateLabel.adjustsFontSizeToFitWidth = true
cell.scoreLabel?.text = "Score: \(((userData.object(at: indexPath.row) as! [String:Any])["Score"] as! String))"
cell.scoreLabel.adjustsFontSizeToFitWidth = true
cell.nameLabel?.text = "Name: \(((userData.object(at: indexPath.row) as! [String:Any])["Name"] as! String))"
cell.nameLabel.adjustsFontSizeToFitWidth = true
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return newUserArray.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
You can set dictionary to UserDefaults (that's for 3 values at one time). To do a leaderboard you'll need to hold this dictionaries in array.
let scoreDict: [String : Any] = ["name": "John", "score": 500, "date": NSDate()]
let defaults = UserDefaults.standard
let scoresKey = "scores"
var currentScores: [Any] = defaults.array(forKey: scoresKey) ?? []
currentScores.append(scoreDict)
defaults.set(currentScores, forKey: scoresKey)
defaults.synchronize()
I couldn't get as much but I just found that you need to add some elements to your scoreData for the tableview datasource
Here you can add the complete object in UserDefaults
//Make your scoreData as MutableArray
var scoreData = NSMutableArray()
// Record of a user
let user = [
"name" : "John",
"score" : 10,
"id" : 20
] as [String : Any]
scoreData.add(user)
defaults.set(scoreData, forKey: "UserData")
How to fetch them
let userData = defaults.object(forKey: "UserData") as! NSArray
print((userData.object(at: 0) as! [String:Any])["name"] as! String)
print((userData.object(at: 0) as! [String:Any])["score"] as! Int)
print((userData.object(at: 0) as! [String:Any])["id"] as! Int)
I have printed them on console and only the 0th index. You can print them on your Label with indexPath.row elements.
EDIT
In order to add a new record, first Fetch the old data from userDefaults
let newUserData = defaults.object(forKey: "UserData") as! NSArray
Then convert it into mutable array because we want to add a new user record. I could have done it while fetching but on fetching from UserDefaults it gives a immutable object no matter we type cast to NSMutableArray. So convert the newUserData to mutable object
let newUserMutableArray = NSMutableArray(array: newUserData)
Add a new record
// New record
let newUserRecord = [
"name" : "Rajan",
"score" : 20,
"id" : 30
] as [String : Any]
newUserMutableArray.add(newUserRecord)
Save it again
defaults.set(newUserMutableArray, forKey: "UserData")
And again fetch the way it is mentioned above. Now the fetched NSArray will contain two elements.
I still prefer to use core data in this case as the handling will become lot easy.
EDIT
After fetching
scoreDataArray = defaults.object(forKey: "UserData") as! NSArray
Reload your table
self.yourTableView.reloadData()
Your table view datasource methods will be like
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return scoreDataArray.count
}
In your cellForRowAt datasource method
cell.scoreLabel?.text = scoreDataArray.object(at: indexPath.row) as! [String:Any])["score"] as! String

Resources