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
Related
Here is my code :
class SchedulerViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,
scheduleCellDelegate
{
var scheduleArray : Array<Array<String>>?
var scheduler : [String : Array<Array<String>>]?
var deviceID : String = ""
let retrievedString = KeychainWrapper.standard.string(forKey: "token")
var day = ""
var dayNum = 0
#IBOutlet weak var spinner: UIActivityIndicatorView!
#IBOutlet var buttons: [UIButton]!
#IBOutlet weak var scheduleView: UITableView!
var header : HTTPHeaders? = nil
var ScheduleURL : Dictionary<String, String>?
override func viewDidLoad() {
super.viewDidLoad()
scheduleView.delegate = self
scheduleView.dataSource = self
spinner.isHidden = true
//scheduleView.allowsSelection = false
scheduleView.register(UINib(nibName: "schedulerCell", bundle: nil), forCellReuseIdentifier: "schedulerCell")
self.getData()
}
func getData(){
AFFunctions.getAFRequest(ofType: ScheduleResponse.self, url: ScheduleURL!["GET"]!) { responseData, statusCode in
print(responseData?.data?.scheduler, statusCode)
self.scheduler = responseData?.data?.scheduler
DispatchQueue.main.async {
self.scheduleView.reloadData()
}
}
}
var buttonNum : Int?
#IBAction func daySelected(_ sender: UIButton) {
self.buttons.forEach { $0.tintColor = ($0 == sender) ? UIColor.orange : UIColor.systemTeal }
self.dayNum = sender.tag
switch dayNum {
case 0 : self.day = "Sunday"
case 1 : self.day = "Monday"
case 2 : self.day = "Tuesday"
case 3 : self.day = "Wednesday"
case 4 : self.day = "Thursday"
case 5 : self.day = "Friday"
case 6 : self.day = "Saturday"
default : self.day = "Sunday"
}
showDetail(day : day,dayNum : dayNum)
}
func showDetail(day : String, dayNum : Int) {
if let dayArray = scheduler?[day]
{
scheduleArray = dayArray
self.scheduleView.reloadData()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return scheduleArray?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("scheduleCell", owner: self, options: nil)?.first as! scheduleCell
cell.cellDelegate = self
cell.editBtn.tag = indexPath.row
cell.deleteSchedule.tag = indexPath.row
scheduleArray = scheduler![self.day]
/////////////THE BELOW STATEMENT THROWS THE ERROR ///////////////////
if let firstLabel = self.scheduleArray?[indexPath.row][0], let secondLabel = self.scheduleArray?[indexPath.row][1] {
DispatchQueue.main.async {
cell.timeLabel1.text = firstLabel
cell.timeLabel2.text = secondLabel
}
}
return cell
}
func didPressButton(_ tag: Int, btnType: String) {
let deleteURL = K.delURL
if(btnType == "delete") {
AFFunctions.deleteAFRequest(ofType: scheduleResponse.self, url: "\(deleteURL)?day=\(self.day)&place=\(tag)") { [self]
responseData, statusCode in
if(statusCode == 200){
let deleteAlert = UIAlertController(title: "Deleted", message: "Device Schedule Successfully Deleted", preferredStyle: UIAlertController.Style.alert)
deleteAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction!) in
self.scheduler![self.day]?.remove(at: tag)
self.scheduleView.reloadData()
}))
self.present(deleteAlert, animated: true, completion: nil)
}
}
}
}
}
I am doing the changes in the array locally as data is fetched only once in ViewDidLoad() with getData function. It shows a schedule for each day of the week (7 buttons, one for each day, are linked to an Outlet Collection), with 2 buttons embedded in the custom cell nib - an edit button and a delete button. I have implemented the logic for deleting, button tags are equal to IndexPath.row which works perfectly and I am able to delete the values I want but when I can't seem to get the table reload working. Even after deleting the row data, the table doesn't update itself. I am calling reloadData after successful deletion. What am I doing wrong?
there are 2 issues
when you update something on UI after a request call, you have to push the UI update process back to the main thread (tableview.reloadData() should get triggered on the main thread)
self.scheduleArray?[indexPath.row][0] and self.scheduleArray?[indexPath.row][1] is the issue index out of range. Because you assume it always contains 2 items without safe check.
I've refactored the code a bit as below
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "scheduleCell") else {
return Bundle.main.loadNibNamed("scheduleCell", owner: self, options: nil)?.first as! scheduleCell
}
cell.cellDelegate = self
cell.editBtn.tag = indexPath.row
cell.deleteSchedule.tag = indexPath.row
scheduleArray = scheduler?[self.day]
guard let item = scheduleArray?[indexPath.row], item.count == 2 else {
return cell
}
if let firstLabel = item.first, let secondLabel = item.last {
cell.timeLabel1.text = firstLabel
cell.timeLabel2.text = secondLabel
}
return cell
}
func didPressButton(_ tag: Int, btnType: String) {
let deleteURL = K.delURL
if(btnType == "delete") {
AFFunctions.deleteAFRequest(ofType: scheduleResponse.self, url: "\(deleteURL)?day=\(self.day)&place=\(tag)") { [self]
responseData, statusCode in
if(statusCode == 200){
// This has to be executed on the main thread to get tableView updated
DispatchQueue.main.async {
let deleteAlert = UIAlertController(title: "Deleted", message: "Device Schedule Successfully Deleted", preferredStyle: UIAlertController.Style.alert)
deleteAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction!) in
self.scheduler![self.day]?.remove(at: tag)
self.scheduleView.reloadData()
}))
self.present(deleteAlert, animated: true, completion: nil)
}
}
}
}
}
Solved it! Updated Code :
class SchedulerViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,
scheduleCellDelegate
{
var scheduleArray : Array<Array<String>>?
var scheduler : [String : Array<Array<String>>]?
var deviceID : String = ""
let retrievedString = KeychainWrapper.standard.string(forKey: "token")
var day = ""
var dayNum = 0
#IBOutlet weak var spinner: UIActivityIndicatorView!
#IBOutlet var buttons: [UIButton]!
#IBOutlet weak var scheduleView: UITableView!
var header : HTTPHeaders? = nil
var ScheduleURL : Dictionary<String, String>?
override func viewDidLoad() {
super.viewDidLoad()
scheduleView.delegate = self
scheduleView.dataSource = self
spinner.isHidden = true
//scheduleView.allowsSelection = false
scheduleView.register(UINib(nibName: "schedulerCell", bundle: nil), forCellReuseIdentifier: "schedulerCell")
self.getData()
}
func getData(){
self.header =
[
"Content-Type" : "application/json",
"Authorization": retrievedString!
]
//scheduleView.register(scheduleCell.self, forCellReuseIdentifier: "scheduleCell")
print(ScheduleURL!["GET"])
AFFunctions.getAFRequest(ofType: ScheduleResponse.self, url: ScheduleURL!["GET"]!) { responseData, statusCode in
print(responseData?.data?.scheduler, statusCode)
self.scheduler = responseData?.data?.scheduler
self.scheduleView.reloadData()
}
}
var buttonNum : Int?
#IBAction func daySelected(_ sender: UIButton) {
self.buttons.forEach { $0.tintColor = ($0 == sender) ? UIColor.orange : UIColor.systemTeal }
self.dayNum = sender.tag
print(sender.tag)
switch dayNum {
case 0 : self.day = "Sunday"
case 1 : self.day = "Monday"
case 2 : self.day = "Tuesday"
case 3 : self.day = "Wednesday"
case 4 : self.day = "Thursday"
case 5 : self.day = "Friday"
case 6 : self.day = "Saturday"
default : self.day = "Sunday"
}
print(day, dayNum)
showDetail(day : day)
}
func showDetail(day : String) {
print(day)
print(scheduler?[day])
if let dayArray = scheduler?[day]
{ print(dayArray)
scheduleArray = dayArray
self.scheduleView.reloadData()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("111 CHECK")
return scheduleArray?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("scheduleCell", owner: self, options: nil)?.first as! scheduleCell
cell.cellDelegate = self
cell.editBtn.tag = indexPath.row
cell.deleteSchedule.tag = indexPath.row
if let item = scheduleArray?[indexPath.row],
item.count > 1 {
DispatchQueue.main.async {
cell.timeLabel1.text = item[0]
cell.timeLabel2.text = item[1]
}
}
return cell
}
func didPressButton(_ tag: Int, btnType: String) {
let deleteURL = K.delURL
print("134", self.day)
print("135", self.scheduleArray?[tag])
print("136", scheduler?[self.day])
print("TAG : ", tag)
print("BTN TYPE: ", btnType)
if(btnType == "delete") {
AFFunctions.deleteAFRequest(ofType: scheduleResponse.self, url: "\(deleteURL)?day=\(self.day)&place=\(tag)") { [self]
responseData, statusCode in
print("\(deleteURL)?day=\(self.day)&place=\(tag)")
print(responseData, statusCode)
if(statusCode == 200){
self.scheduler![self.day]!.remove(at: tag)
DispatchQueue.main.async {
let deleteAlert = UIAlertController(title: "Deleted", message: "Device Schedule Successfully Deleted", preferredStyle: UIAlertController.Style.alert)
deleteAlert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (action: UIAlertAction!) in
self.showDetail(day: self.day)
// self.scheduleView.reloadData()
}))
self.present(deleteAlert, animated: true, completion: nil)
}
}
}
}
}
}
The issue here is you are holding multiple sources of truth and fail at synchronysing them. You have:
var scheduleArray : Array<Array<String>>?
var scheduler : [String : Array<Array<String>>]?
Where both seem to hold the same information in different form. I can´t see why you are doing this from the example code you posted.
You get an error at:
self.scheduleArray?[indexPath.row][0]
because when you delete your item you are removing it from scheduler and reload your tableview. The tableview on the other hand get´s the information how many rows it should render from scheduleArray:
return scheduleArray?.count ?? 0
and these differ at that time because you didn´t assign scheduler to scheduleArray.
So 2 possible solutions here:
assign scheduleArray before you reload your tableview
self.scheduler![self.day]?.remove(at: tag)
scheduleArray = scheduler![self.day]
and remove the assignment in the cellForItemAt function
stop using scheduleArray and scheduler. Use only a single collection to hold the information.
I have a core data Swift master/detail view application (code is located here) where the core data object, called sweetnote, is basically this:
class sweetnote: NSObject {
private(set) var noteId : UUID
private(set) var noteTitle : String
private(set) var noteText : NSAttributedString
private(set) var noteCreated : Int64
private(set) var noteModified : Int64
private(set) var noteCategory : String
}
I have a Master view controller which renders these notes in a UITableView with the help of a function in Helpers/sweetnoteCoreDataHelper.swift called readNotesFromCoreData:
static func readNotesFromCoreData(fromManagedObjectContext: NSManagedObjectContext) -> [sweetnote] {
var returnedNotes = [sweetnote]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Note")
fetchRequest.predicate = nil
do {
let fetchedNotesFromCoreData = try fromManagedObjectContext.fetch(fetchRequest)
fetchedNotesFromCoreData.forEach { (fetchRequestResult) in
let noteManagedObjectRead = fetchRequestResult as! NSManagedObject
returnedNotes.append(sweetnote.init(
noteId: noteManagedObjectRead.value(forKey: "noteId") as! UUID,
noteTitle: noteManagedObjectRead.value(forKey: "noteTitle") as! String,
noteText: noteManagedObjectRead.value(forKey: "noteText") as! NSAttributedString,
noteCreated: noteManagedObjectRead.value(forKey: "noteCreated") as! Int64,
noteModified: noteManagedObjectRead.value(forKey: "noteModified") as! Int64,
noteCategory: noteManagedObjectRead.value(forKey: "noteCategory")
as! String))
}
} catch let error as NSError {
// TODO error handling
print("Could not read notes from core data. \(error), \(error.userInfo)")
}
// Set note count
self.count = returnedNotes.count
// Sort by modified date
return returnedNotes.sorted() {
$0.noteModified > $1.noteModified
}
}
And then in my Master view controller (at UI/MasterViewController.swift) I have a UISearchController configured at the top of the UITableView which is currently not applying any filtering logic based on the text and I'm having trouble determining where I need to update:
import UIKit
import Foundation
import CoreData
class MasterViewController: UITableViewController, NSFetchedResultsControllerDelegate, UISearchBarDelegate, UISearchResultsUpdating {
var detailViewController: DetailViewController? = nil
var searchtemplate: String? {didSet {print (searchtemplate as Any)}}
// Search results controller
let resultSearchController = UISearchController(searchResultsController: nil)
var sweetnotes: [sweetnote] = []
var searchResults: [sweetnote] = []
override func viewDidLoad() {
super.viewDidLoad()
// Core data initialization
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
// Create alert controller
let alert = UIAlertController(
title: "Could note get app delegate",
message: "Could note get app delegate, unexpected error occurred. Try again later.",
preferredStyle: .alert)
// Add OK action
alert.addAction(UIAlertAction(title: "OK",
style: .default))
// Show alert
self.present(alert, animated: true)
return
}
// Pass the context forward from the app delegate
let managedContext = appDelegate.persistentContainer.viewContext
// Set context in the storage
sweetnoteStorage.storage.setManagedContext(managedObjectContext: managedContext)
// Set the search controller programmatically
resultSearchController.searchResultsUpdater = self
resultSearchController.hidesNavigationBarDuringPresentation = false
resultSearchController.obscuresBackgroundDuringPresentation = false
self.definesPresentationContext = true
// Scope bar
//resultSearchController.searchBar.scopeButtonTitles = ["All", "Ideas", "Information", "Lifestyle", "Lists", "Recipes", "Other"]
//searchView.addSubview(resultSearchController.searchBar)
tableView.tableHeaderView = resultSearchController.searchBar
resultSearchController.searchBar.tintColor = UIColor.darkGray
resultSearchController.searchBar.barTintColor = UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 0.8)
resultSearchController.searchBar.placeholder = "Search sweetnotes"
// Prefer large titles
self.navigationController?.navigationBar.prefersLargeTitles = true
// Edit-note button
navigationItem.leftBarButtonItem = editButtonItem
// Add-note button
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
navigationItem.rightBarButtonItem = addButton
if let split = splitViewController {
let controllers = split.viewControllers
detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
self.tableView.delegate = self
}
...
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
// Dismiss search bar on scroll-down
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
resultSearchController.dismiss(animated: false, completion: nil)
}
#objc
func insertNewObject(_ sender: Any) {
performSegue(withIdentifier: "showCreateNoteSegue", sender: self)
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
//let object = objects[indexPath.row]
let object = sweetnoteStorage.storage.readNote(at: indexPath.row)
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
//controller.detailItem = resultSearchController.isActive ? searchResults[indexPath.row] : sweetnotes[indexPath.row]
}
}
}
// MARK: - Table View
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
filterContentForSearchText(searchBar.text!, scope: searchBar.scopeButtonTitles![selectedScope])
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
print(text)
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
searchtemplate = searchText
tableView.reloadData()
}
func updateSearchResultsForSearchController(searchController: UISearchController, fromManagedObjectContext: NSManagedObjectContext) {
let searchText = searchController.searchBar.text!
let predicate = NSPredicate(format: "%K CONTAINS[c] %#", argumentArray: ["noteText", searchText])
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Note")
fetchRequest.predicate = predicate
do {
let fetchedNotesFromCoreData = try fromManagedObjectContext.fetch(fetchRequest)
fetchedNotesFromCoreData.forEach { (fetchRequestResult) in
let noteManagedObjectRead = fetchRequestResult as! NSManagedObject
searchResults.append(sweetnote.init(
noteId: noteManagedObjectRead.value(forKey: "noteId") as! UUID,
noteTitle: noteManagedObjectRead.value(forKey: "noteTitle") as! String,
noteText: noteManagedObjectRead.value(forKey: "noteText") as! NSAttributedString,
noteCreated: noteManagedObjectRead.value(forKey: "noteCreated") as! Int64,
noteModified: noteManagedObjectRead.value(forKey: "noteModified") as! Int64,
noteCategory: noteManagedObjectRead.value(forKey: "noteCategory")
as! String))
}
} catch let error as NSError {
// TODO error handling
print("Could not read notes from core data. \(error), \(error.userInfo)")
}
tableView.reloadData()
}
...
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if resultSearchController.isActive && resultSearchController.searchBar.text != "" {
return searchResults.count
} else {
return sweetnoteStorage.storage.count()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! sweetnoteUITableViewCell
//let note = fetchedResultsController.object(at: indexPath)
//configureCell(cell, withEvent: note)
if let object = sweetnoteStorage.storage.readNote(at: indexPath.row) {
cell.noteTitleLabel!.text = object.noteTitle
cell.noteTextLabel!.attributedText = object.noteText
cell.noteCategoryLabel!.text = object.noteCategory
cell.noteDateLabel!.text = sweetnoteDateHelper.convertDate(date: Date.init(minutes: object.noteModified))
}
//cell.contentView.layer.cornerRadius = 6.0
//cell.contentView.layer.borderColor = UIColor.gray.withAlphaComponent(0.5).cgColor
//cell.contentView.layer.borderWidth = 1.0
//cell.contentView.layer.masksToBounds = true
//cell.contentView.clipsToBounds = true
cell.backgroundColor = UIColor.white
cell.selectionStyle = UITableViewCell.SelectionStyle.none
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable
if resultSearchController.isActive {
return false
} else {
return true
}
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
presentDeletionFailsafe(indexPath: indexPath)
//objects.remove(at: indexPath.row)
//sweetnoteStorage.storage.removeNote(at: indexPath.row)
//tableView.deleteRows(at: [indexPath], with: .fade)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
// Deletion failsafe function
func presentDeletionFailsafe(indexPath: IndexPath) {
let alert = UIAlertController(title: nil, message: "Are you sure you would like to delete this note?", preferredStyle: .alert)
// Delete the note
let yesAction = UIAlertAction(title: "Yes", style: .default) { _ in
// replace data variable with your own data array
sweetnoteStorage.storage.removeNote(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .fade)
}
alert.addAction(yesAction)
// Cancel and don't delete note
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
func configureCell(_ cell: sweetnoteUITableViewCell, withEvent note: Note) {
cell.noteTitleLabel!.text = note.noteTitle
cell.noteTextLabel!.attributedText = note.noteText
cell.noteCategoryLabel!.text = note.noteCategory
cell.noteDateLabel!.text = sweetnoteDateHelper.convertDate(date: Date.init(minutes: note.noteModified))
}
}
The function updateSearchResultsForSearchController is where I tried to establish a predicate and execute the search result fetch request, based on some similar questions out there where people implemented search controllers on top of core data projects, but I am definitely still missing something or am in the wrong place because when text is entered to the search bar, all notes remain in the table view.
Any answers/suggestions are massively appreciated as I've spent a good deal of time on this. I am happy to provide more information and context if that helps you out. And again the code is located in my 'searchbar' branch of my project at here. Thank you very much in advance.
I'm trying to read from firebase realtime database a series of instructions.
I have an unknown number of information on my Vehicles node, so I just use nextObject method to get the number of times that the function need to iterate.
The problem is that at the opening, my table is empty. When I click on the searchbar then my cells contents did appear. How can I solve these problem?
Here my UITableView file:
import UIKit
import FirebaseDatabase
import Alamofire
class Vehicles: UITableViewController,
UISearchResultsUpdating, UISearchBarDelegate {
//variables
var model: NSMutableArray = []
var numberOfVehicles: NSMutableArray = []
var price: NSMutableArray = []
var imagePathString: NSMutableArray = []
var detailpage: NSMutableArray = []
var populator: NSMutableArray = []
var searching = false
var matches = [Int]()
let searchController = UISearchController(searchResultsController: nil)
#IBOutlet weak var InfoTableView: UITableView!
var InfoList: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
loadData()
//this should reload but, it didn't.
self.InfoTableView.reloadData()
//then the searchbar that is good and don't have any problem.
searchingField()
}
func loadData() {
//read data from database
let rootRef = Database.database().reference()
let conditionalRef = rootRef.child("Vehicles")
conditionalRef.observe(.value) {(snap: DataSnapshot) in
// Get all the children from snapshot you got back from Firebase
let snapshotChildren = snap.children
// Loop over all children in Firebase
while let child = snapshotChildren.nextObject() as? DataSnapshot {
// Get code node key and save it to they array
self.populator.add(child.key)
if self.populator.contains("\(child.key)") {
let userRef = rootRef.child("Vehicles").child("\(child.key)")
userRef.observeSingleEvent(of: .value, with: { snapshot in
let userDict = snapshot.value as! [String: Any]
let model1 = userDict["Model"] as! String
self.model.add(model1)
let detail1 = userDict["Detail"] as! String
self.detailpage.add(detail1)
let numberOfVehicles1 = userDict["numberOfVehicles"] as! String
self.numberOfVehicles.add(numberOfVehicles1)
let Price1 = userDict["Price"] as! String
self.price.add(Price1)
let imageURL1 = userDict["imageURL"] as! String
self.imagePathString.add(imageURL1)
}) //end second observeSingleEvent
}
else {
let alert = UIAlertController(title: "Error", message: "No one vehicle found", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "ok", style: UIAlertAction.Style.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
} //end searching object in Vehicles node
} //end first observeSingleEvent
}//end func
func searchingField() {
//setup searchbar
tableView.estimatedRowHeight = 50
navigationController?.navigationBar.prefersLargeTitles = true
searchController.searchBar.delegate = self
searchController.searchResultsUpdater = self
searchController.searchBar.backgroundColor = UIColor.white
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search"
navigationItem.searchController = searchController
definesPresentationContext = true
let attributes = [
NSAttributedString.Key.foregroundColor : UIColor.black,
NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 17)
]
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).setTitleTextAttributes(attributes, for: .normal)
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).title = "Dismiss"
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self])
}
// MARK: Search Controller
func updateSearchResults(for searchController: UISearchController) {
var regArray = self.model as NSArray as! [String]
if let searchText = searchController.searchBar.text,
!searchText.isEmpty {
matches.removeAll()
for index in 0..<model.count {
if regArray[index].lowercased().contains(
searchText.lowercased()) {
matches.append(index)
}
}
searching = true
} else {
searching = false
}
tableView.reloadData()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searching = false
tableView.reloadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return searching ? matches.count : model.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searching ? matches.count : model.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "TableCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as! Vehicles_cell
let row = indexPath.row
var regArray = self.model as NSArray as! [String]
cell.Label.text = searching ? regArray[matches[row]] : model[row] as! String
cell.Subtitle?.text = "N. Vehicles: \(self.numberOfVehicles[indexPath.row]) - Price: \(self.price[indexPath.row])$"
Alamofire.request("\(self.imagePathString[indexPath.row])").response { response in
guard let image = UIImage(data:response.data!) else {
// Handle error
return
}
let imageData = image.jpegData(compressionQuality: 1.0)
cell.Image.contentMode = .scaleAspectFit
cell.Image.image = UIImage(data : imageData!)
}
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowrentDetails" {
let myIndexPath = self.tableView.indexPathForSelectedRow!
//save detail1 in UserDefault
let SVDetail = self.detailpage[myIndexPath.row]
let SVDetaildefaults = UserDefaults.standard
SVDetaildefaults.set(SVDetail, forKey: "sv_detail")
SVDetaildefaults.synchronize()
_ = segue.destination
as! Vehicles_Detail
}
}
//IMPOSTA LE DIMENSIONI DELLE CELLE
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch indexPath.row {
default:
return 100
}
}
}
I expect the table show on the opening all the data from database, while actually and not repeat unless I click on the searchbar. And the table shouldn't be repeated twice.
Edit (solution to duplicates)
This is so embarrassing. The answer to this problem is simple
In numberOfSections function, I used address.count instead to use 1 Section. So, what I saw were not duplicate cells, but new block sections of model.count
Your tableview isn't reloading data once it is fetched during the Firebase observation, but is in updateSearchResults(). Does adding self.InfoTableView.reloadData() inside your loadData() in between the //end searching object in Vehicles node and //end first observeSingleEvent closing brackets fix the issue?
Edit: The reason your reload of tableview data doesn't fix the issue within viewDidLoad() is because it gets called before the loadData() function starts to iterate through your Firebase data objects. By doing it at the end of the Firebase observation, you're ensuring that you've loaded all of your data from Firebase prior to calling the reload.
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.
I've googled and read a lot of questions and answers about this topic, however I still can't solve my problem. I have a project connected to firebase, with a tableView connected to that information. The following error pops up:
unexpectedly found nil while unwrapping an Optional value.
I've checked all my connections between UIButtons, UILabels and such, and most of all i have tried using breakpoints to locate the error. However I can't seem to get my head around a solution. Here comes code and messages from the project and the error:
#IBOutlet weak var addButton: UIBarButtonItem!
let listToUsers = "ListToUsers"
var backgroundNr = 0
var items: [GroceryItem] = []
let ref = FIRDatabase.database().reference(withPath: "grocery-items")
let usersRef = FIRDatabase.database().reference(withPath: "online")
var user: User!
var userCountBarButtonItem: UIBarButtonItem!
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
if user?.email == "tor#gmail.com" {
addButton.isEnabled = false
}
tableView.allowsMultipleSelectionDuringEditing = false
userCountBarButtonItem = UIBarButtonItem(title: "1",
style: .plain,
target: self,
action: #selector(userCountButtonDidTouch))
userCountBarButtonItem.tintColor = UIColor.white
navigationItem.leftBarButtonItem = userCountBarButtonItem
usersRef.observe(.value, with: { snapshot in
if snapshot.exists() {
self.userCountBarButtonItem?.title = snapshot.childrenCount.description
} else {
self.userCountBarButtonItem?.title = "0"
}
})
ref.queryOrdered(byChild: "completed").observe(.value, with: { snapshot in
var newItems: [GroceryItem] = []
for item in snapshot.children {
let groceryItem = GroceryItem(snapshot: item as! FIRDataSnapshot)
newItems.append(groceryItem)
}
self.items = newItems
self.tableView.reloadData()
})
FIRAuth.auth()!.addStateDidChangeListener { auth, user in
guard let user = user else { return }
self.user = User(authData: user)
let currentUserRef = self.usersRef.child(self.user!.uid)
currentUserRef.setValue(self.user!.email)
currentUserRef.onDisconnectRemoveValue()
}
}
// MARK: UITableView Delegate methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! SubjectTableViewCell
let groceryItem = items[indexPath.row]
if backgroundNr == 0 {
cell.backgroundColor = UIColor(red: 222/255, green: 164/255, blue: 50/255, alpha: 0.6)
backgroundNr += 1
} else {
cell.backgroundColor = UIColor.white
backgroundNr -= 1
}
cell.textLabel?.text = groceryItem.name
cell.detailTextLabel?.text = groceryItem.addedByUser
toggleCellCheckbox(cell, isCompleted: groceryItem.completed)
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let groceryItem = items[indexPath.row]
groceryItem.ref?.removeValue()
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) else { return }
let groceryItem = items[indexPath.row]
let toggledCompletion = !groceryItem.completed
toggleCellCheckbox(cell, isCompleted: toggledCompletion)
groceryItem.ref?.updateChildValues([
"completed": toggledCompletion
])
}
func toggleCellCheckbox(_ cell: UITableViewCell, isCompleted: Bool) {
if !isCompleted {
cell.accessoryType = .none
cell.textLabel?.textColor = UIColor.black
cell.detailTextLabel?.textColor = UIColor.black
} else {
cell.accessoryType = .checkmark
cell.textLabel?.textColor = UIColor.gray
cell.detailTextLabel?.textColor = UIColor.gray
}
}
// MARK: Add Item
#IBAction func addButtonTapped(_ sender: Any) {
let alert = UIAlertController(title: "Kunskaps Område",
message: "Lägg till objekt",
preferredStyle: .alert)
let saveAction = UIAlertAction(title: "Spara",
style: .default) { _ in
// 1
guard let textField = alert.textFields?.first,
let text = textField.text else { return }
// 2
let groceryItem = GroceryItem(name: text,
addedByUser: self.user!.email,
completed: false)
// 3
let groceryItemRef = self.ref.child(text.lowercased())
// 4
groceryItemRef.setValue(groceryItem.toAnyObject())
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .default)
alert.addTextField()
alert.addAction(saveAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}
func userCountButtonDidTouch() {
performSegue(withIdentifier: listToUsers, sender: nil)
}
#IBAction func startClockTapp(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(GroceryListTableViewController.updateCounter), userInfo: nil, repeats: true)
}
func updateCounter() {
counter += 1
let clock = String(counter)
print(clock)
let clockCurrent = Clock(name: clock, addedByUser: self.user!.email)
let clockCurrentRef = self.ref.child(clock.lowercased())
clockCurrentRef.setValue(clockCurrent.toAnyObject())
}
When using breakpoints to locate where the error occurs, it seems that it is located in the following function: (the first line of code in "UITable view delegate methods)
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
I don't now if I'm using the breakpoints wrong, however that's the result I get, if it helps.
The "grocery" objects struct file´s code is the following:
import Foundation
import Firebase
struct GroceryItem {
let key: String
let name: String
let addedByUser: String
let ref: FIRDatabaseReference?
var completed: Bool
init(name: String, addedByUser: String, completed: Bool, key: String = "") {
self.key = key
self.name = name
self.addedByUser = addedByUser
self.completed = completed
self.ref = nil
}
init(snapshot: FIRDataSnapshot) {
key = snapshot.key
let snapshotValue = snapshot.value as! [String: AnyObject]
name = snapshotValue["name"] as! String
addedByUser = snapshotValue["addedByUser"] as! String
completed = snapshotValue["completed"] as! Bool
ref = snapshot.ref
}
func toAnyObject() -> Any {
return [
"name": name,
"addedByUser": addedByUser,
"completed": completed
]
}
}
The database content on Firebase gets structured as following:
educationlevel-e230e
grocery-items
Fysik:, addedByUser:, completed:
The full string of the basic error message:
fatal error: unexpectedly found nil while unwrapping an Optional value
2016-12-24 01:22:54.799117 EducationLevel[1077:263810]
fatal error:
unexpectedly found nil while unwrapping an Optional value
I really can't find the answer to this problem by my self or anywhere on the web, REALLY appreciates it if I could get some help (sorry for the probably bad english, I'm from Sweden;) )
(If you need more strings from the error message or anything like that I'll immediately update this question)
Thanks!! Tor from Sweden.