Swift IOS Issues updating Label in DetailView from MasterView - ios

My first posting on stackoverflow, so sorry if its not right
I am using the Master/Detail View template in XCode. This is a BLT Central app and it gets notified of events happening on the BLE device.
I have a Master view, this is updated using
public func UpdateView() {
tableView.beginUpdates()
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
tableView.endUpdates()
}
This works fine and the TableView shows the updates live whenever the BLE notifies
However I also want to update the detail view live, incase this is being shown.
I have the label linked in the DetailView via:
#IBOutlet weak var detailDescription: UILabel!
And it updates just fine when the MasterView seques to the DetailView
However if I try to update the Label when the BLE notify arrives the #IBOutlet detailDescription has turned to nil, so the update fails (Label not linked)
func UpdateDetail() {
guard let label = detailDescription else {
return; //Label not linked
}
label.text = "New Data"
}
The UpdateDetail() function is also used in viewDidLoad() and in that case its working fine
Whats really weird is that if I add a timer in the DetailView to just do the update
var timer : Timer? = nil
#objc func fireTimer() {
DispatchQueue.main.async {
self.UpdateDetail()
}
}
It works fine, so its possibly something to do with calling the UpdateDetail() function from outside the Detail class.
I have checked if the detailDescription reference gets reset by adding a didSet to the property, and its only called once when the view is loaded
Guess I could use the timer work around, but I am totally baffled why the detailItem appears as nil sometimes, so would be grateful for a sanity check.
UPDATE Gone back to basics_______
So I have now gone back to the standard Apple Master->Detail view template and added a simple timer which updates the list. The ListView updates fine, however it still does not update the detail view dynamically. outside its own class.
I am using on a 1 second timer after MainView is loaded, the list view updates fine, however the detail does not.
When debugging it steps into configureView() fine, but detailDescriptionLabel is nil after the first viewDidLoad()
Have tried all the suggestions below, however in each case the weak reference to the label is nil after the initial Load
Totally baffled
#objc func doTimer() {
for index in 0..<objects.count {
objects[index]=(objects[index] as! NSDate).addingTimeInterval(1)
}
tableView.beginUpdates()
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
tableView.endUpdates()
DispatchQueue.main.async {
self.detailViewController?.configureView()
}
}
Full code for MasterViewController.swift here, rest is the same as standard template.
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var objects = [Any]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
navigationItem.leftBarButtonItem = editButtonItem
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
}
//Added timer here
_ = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(doTimer), userInfo: nil, repeats: true)
}
//Update data in list
#objc func doTimer() {
for index in 0..<objects.count {
objects[index]=(objects[index] as! NSDate).addingTimeInterval(1)
}
tableView.beginUpdates()
tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
tableView.endUpdates()
DispatchQueue.main.async {
self.detailViewController?.configureView()
}
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
#objc
func insertNewObject(_ sender: Any) {
objects.insert(NSDate(), at: 0)
let indexPath = IndexPath(row: 0, section: 0)
tableView.insertRows(at: [indexPath], with: .automatic)
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let object = objects[indexPath.row] as! NSDate
cell.textLabel!.text = object.description
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(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.
}
}
}

My answer is based on the provided Git repository.
In MasterViewController - you are calling self.detailViewController?.configureView() but you never assign the detail controller and because it's nil it fails to call the configureView function. You can do that in prepare(for segue: UIStoryboardSegue, sender: Any?) by setting self.detailViewController = controller
This won't still help you with the update of the label value.
The reason fo that is because in #objc func doTimer() { you are always setting (overriding) a new Date object into your array (what you probably aimed for is to update value for same reference). Because of this, the reference you assigned to detailViewController is different and you never update the detailItem in your timer. Hence calling the configureView won't make any change as the value remained the same.

It's happening because your detailDescription label is weak "weak var detailDescription". and each time you write this code
guard let label = detailDescription else {
return; //Label not linked
}
label.text = "New Data"
you create a new object and assign value to that object. So the original value for detailDescription label object doesn't change. Try using the same detailDescription object everytime when you change its value.

Related

Data in the TableView are removed (Swift)

Data in the TableView is removed (Swift)
I have a code for my application "Notes".
But sometimes the data from the table is merely deleted .
How can I fix this?
If you scroll the page back, the data will be erased
** I use to save and load data:**
func save() {
UserDefaults.standard.set(myData, forKey: "notes")
UserDefaults.standard.synchronize()
}
func load(){
if let loadData = UserDefaults.standard.value(forKey: "notes") as? [String] {
myData = loadData
table.reloadData()
}
}
ViewController 1
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var table: UITableView!
var myData: [String] = []
var selectedRow: Int = -1
var newRowText:String = ""
var detailView: DetailViewController!
override func viewDidLoad() {
super.viewDidLoad()
load()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if selectedRow == -1 {
return
}
myData[selectedRow] = newRowText
if newRowText == "" {
myData.remove(at: selectedRow)
}
table.reloadData()
save()
}
#objc func AddNewNotes(){
if table.isEditing{
return
}
let name:String = ""
myData.insert(name, at: 0)
let indexPath: IndexPath = IndexPath(row: 0, section: 0)
table.insertRows(at: [indexPath], with: .automatic)
table.selectRow(at: indexPath, animated: true, scrollPosition: .none)
self.performSegue(withIdentifier: "detail", sender: nil )
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = myData[indexPath.row]
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let detailView:DetailViewController = segue.destination as!DetailViewController
selectedRow = table.indexPathForSelectedRow!.row
detailView.masterView = self
detailView.setText(t: myData[selectedRow])
}
}
Since the data source array that you are filling the table view from is myData, it seems that you would need to call your save() method before reloading the table view (table.reloadData()) in the viewWillAppear(_:) life cycle method:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// ...
save()
myTableView.reloadData()
// ...
}
Also, (obviously) make sure that myData data source contains the saved desired data.
However, I would recommend to save the data before even leaving the second view controller, probably viewWillDisappear(_:) would be a good place to do it; By applying this, there is no need to call save() in the first view controller anymore, all you have to do is to reload the table view. That would be more logical because the first view controller should displays the saved data, but not saving it, it shall be in the second view controller where you are adding data.

How to reload a view-controller after data has been fetched from a network request?

I have a problem and can't seem to fix it after looking at tutorials online and other SO questions with a similar problem, which leaves me to think I've done something wrong/bad practice related in my code.
I have 2 table view controllers.
The first TableViewController is populated from a database, all this works fine. When I click one of the cells it segues to a second TableViewController which also should be populated from a database (depending on what you select in the first VC).
Currently if I click a cell in TVC1 it goes to TVC2 and it's empty, then it I click back within my navigation controller and select something else, it goes back to TVC2 and shows me my first selection. This indicates that TVC2 is being loaded before the network has returned its data from the database.... so, I tried using tableView.reloadData() in various places like viewDidLoad and viewDidAppear, but i just can't seem to get it to work.
Below is both TVC's. I've stuck with MVC design pattern and haven't included the model and severConnection code for each TVC because I don't want to over complicate the post, however if you'd like to see either I will update.
Thanks in advance for any help.
TableViewController1
class MenuTypeTableViewController: UITableViewController, MenuTypeServerProtocol {
//Properties
var cellItems: NSArray = NSArray()
var selectedItem = String()
override func viewDidLoad() {
super.viewDidLoad()
let menuTypeServer = MenuTypeServer()
menuTypeServer.delegate = self
menuTypeServer.downloadItems()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier: String = "cellType"
let myCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
let item: MenuTypeModel = cellItems[indexPath.row] as! MenuTypeModel
myCell.textLabel?.text = item.type
return myCell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedCell = tableView.cellForRow(at: indexPath)
selectedItem = (selectedCell?.textLabel?.text)!
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "typeItems" {
let destinationVC = segue.destination as? TypeItemsTableViewController
destinationVC?.selectedItem = self.selectedItem
}
}
}
TableViewController2:
class TypeItemsTableViewController: UITableViewController, TypeItemsServerProtocol {
//Properties
var cellItems: NSArray = NSArray()
var selectedItem: String = String()
let typeItemsServer = TypeItemsServer()
override func viewDidLoad() {
super.viewDidLoad()
typeItemsServer.delegate = self
self.typeItemsServer.foodType = self.selectedItem
self.typeItemsServer.downloadItems()
self.tableView.reloadData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier: String = "cellTypeItem"
let myCell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)!
let item: TypeItemsModel = cellItems[indexPath.row] as! TypeItemsModel
myCell.textLabel?.text = item.name!
return myCell
}
}
Try adding this to TypeItemsTableViewController
override func viewDidLoad() {
super.viewDidLoad()
cellItems = NSArray()//make sure you have the empty array at the start
typeItemsServer.delegate = self
self.typeItemsServer.foodType = self.selectedItem
self.typeItemsServer.downloadItems()
self.tableView.reloadData()
}
and
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
typeItemsServer.delegate = nil
}
Add this at the top
var cellItems: NSArray = NSArray() {
didSet {
tableview.reloadData()
}
}
Now you can remove other tableview.reloadData() calls since it will automatically be called once cellItems are set...
I think you have a timing problem. You're reloading right after your async data call. You reload but your data isn't in place at that time. Try using functions with escaping or use "didSet" on your data like:
var dataArray: [type] {
didSet {
tableview.reloadData()
}
}

Swift 3 UITableView not updating

I was assigned with converting an older Objective C app to Swift.
I had another project come up but when I came back to it and to my somewhat working Swift 2 version, upon updating to Swift 3 the UITableView does not seem to update.
It is built in Interface Builder (IB). The data source and delegate functions are linked in IB.
I made a sample project where I want to reload a different array on a button press. On a button press the main array is set equal to a different array. then self.tableView.reloadData() is called. The array in debugging has a value of 4 and is not empty so numberOfRowsInSection returns a number greater than 0. The table height and width are what you would expect and are visible. The table just does not refresh. The cells populate the first time the array loads.
I have also tried downloading tutorials where they add a new cell to a table but it does not appear to work either. I have also tried manually assigning the app delegate and datasource in MasterViewController.swift. I also tried wrapping the reloadData() call in DispatchQueue.main.async but that did not seem to help either.
Hopefully I'm just missing something very basic here. Below is my MasterViewController file. Thanks for any help.
Current version of Xcode: 8.2.1
Version of swift: 3.0.2
OSX: Sierra 10.12.2
import UIKit
class MasterViewController: UITableViewController {
var detailViewController: DetailViewController? = nil
var objects = [Any]()
var list1 = ["Eggs", "Milk", "Bread", "Bacon"];
var list2 = ["France", "Italy", "England", "Spain"];
var currentArray = [String]();
var setVar = false;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem
if(currentArray.count <= 0){
currentArray = list2;
}else{
currentArray = list1;
}
//self.tableView.dataSource = self;
// self.tableView.delegate = self;
//self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell");
//self.tableView.reloadData();
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject(_:)))
self.navigationItem.rightBarButtonItem = addButton
if let split = self.splitViewController {
let controllers = split.viewControllers
self.detailViewController = (controllers[controllers.count-1] as! UINavigationController).topViewController as? DetailViewController
}
}
override func viewWillAppear(_ animated: Bool) {
self.clearsSelectionOnViewWillAppear = self.splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func insertNewObject(_ sender: Any) {
objects.insert(NSDate(), at: 0)
let indexPath = IndexPath(row: 0, section: 0)
self.tableView.insertRows(at: [indexPath], with: .automatic)
}
// MARK: - Segues
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return currentArray.count;
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel!.text = currentArray[indexPath.row];
return cell
}
func refreshUI(){
self.tableView.reloadData();
}
func changeVar(){
if(setVar){
currentArray.removeAll();
self.currentArray = self.list1;
setVar = false;
}else{
currentArray.removeAll();
self.tableView.reloadData();
list2.append("Italy");
self.currentArray = self.list2;
setVar = true;
}
self.refreshUI();
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
objects.remove(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.
}
}
}
class DetailViewController: UIViewController {
#IBOutlet weak var detailDescriptionLabel: UILabel!
#IBOutlet weak var test2: UIButton!
#IBOutlet weak var test1: UIButton!
var mc = MasterViewController();
func configureView() {
// Update the user interface for the detail item.
if let detail = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.configureView()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var detailItem: NSDate? {
didSet {
// Update the view.
self.configureView()
}
}
#IBAction func button1(){
NSLog("here is the button1");
mc.changeVar();
}
#IBAction func button2(){
NSLog("here is the button2");
mc.changeVar();
}
}
Your tableView is using the data of currentArray in cellForRowAt function.
The only place that you assign to currentArray is in viewDidLoad.
Currently, in the code the add button will cause a new string of the current timestamp to the objects array, As the table is pulling data from currentArray and not objects, it will never change.
I do not see changeVar() function being called anywhere in the code.
Change the target of the add button to call changeVar function and see if that updates the data in the table. If not, you'll have to provide the exact code that your saying is not working, as the current code I wouldn't expect it to change anything.
EDIT:
In your detail view controller you are trying to update a value from the master view controller... But.. It is not the same instance.
var mc = MasterViewController();
^^ This code creates a NEW instance of MasterViewController so calling that code wont change anything on your tableView.
change this line to
weak var mc: MasterViewController?
Then when you create the detailviewcontroller you can do:
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.mc = self
Then your DetailViewController has a reference to the master controller and can call the function as expected.

Object isn't updating in tableview with new Realm data

Whenever I click on a row in my tableview and change the text in the fields to something else, it always crashes and doesn't actually update the data. I'm saving all my data in a "Realm" database, which im fairly new at using.
I'm doing the updating in the "unwindToNoteList" function. Can someone help me know why it's adding a new row and crashing instead of updating the current row?
import UIKit
import Realm
class NoteTableViewController: UITableViewController {
// MARK: Properties
var notes = Note.allObjects()
override func viewDidLoad() {
super.viewDidLoad()
// Get Realm Database location
println(RLMRealm.defaultRealm().path)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return Int(notes.count)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "NoteTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NoteTableViewCell
let object = notes[UInt(indexPath.row)] as! Note
// Configure the cell...
cell.titleLabel.text = object.title
cell.bodyLabel.text = object.body
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return NO if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete from database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.deleteObject(notes[UInt(indexPath.row)] as! RLMObject)
realm.commitWriteTransaction()
// Delete row from table view
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .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
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" { // Clicked on a cell
let noteDetailViewController = segue.destinationViewController as! NoteViewController
// Get the cell that generated this segue.
if let selectedNoteCell = sender as? NoteTableViewCell {
let indexPath = tableView.indexPathForCell(selectedNoteCell)!
let selectedNote = notes[UInt(indexPath.row)] as! Note
noteDetailViewController.note = selectedNote
}
}
else if segue.identifier == "AddItem" { // Clicked add button
println("Adding new note")
}
}
#IBAction func unwindToNoteList(sender: UIStoryboardSegue) {
if let selectedIndexPath = tableView.indexPathForSelectedRow() { // User clicked on a row
// Update an existing note.
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
Note.createOrUpdateInRealm(realm, withValue: ["title": note.title, "body": note.body, "id": note.id])
realm.commitWriteTransaction()
println("Yes")
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
else {
// Add a new note.
let newIndexPath = NSIndexPath(forRow: Int(notes.count), inSection: 0)
// Persist in database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
Note.createInRealm(realm, withValue: ["title": note.title, "body": note.body, "id": uuid])
realm.commitWriteTransaction()
println("no")
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
}
Finally solved this, by not updating the object while in the tableview but while in the view controller that holds the text fields
Here's my NoteViewController
import UIKit
import Realm
class NoteViewController: UIViewController, UITextFieldDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
// MARK: Properties
#IBOutlet var saveButton: UIBarButtonItem!
#IBOutlet var titleTextField: UITextField!
#IBOutlet var bodyTextField: UITextField!
/*
This value is either passed by `NoteTableViewController` in `prepareForSegue(_:sender:)`
or constructed as part of adding a new note.
*/
var note = Note?()
// MARK: Navigation
#IBAction func cancel(sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
let isPresentingInAddNoteMode = presentingViewController is UINavigationController
if isPresentingInAddNoteMode {
dismissViewControllerAnimated(true, completion: nil)
} else {
navigationController!.popViewControllerAnimated(true)
}
}
// This method lets you configure a view controller before it's presented.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if saveButton === sender {
let title = titleTextField.text ?? ""
let body = bodyTextField.text ?? ""
/* The operator ?? unwraps the optional string in titleTextField.text since it may or may not have text in the field, and returns the value if it's a valid string. If nil though, it returns and empty string ("") instead
*/
// Update the database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
note?.title = titleTextField.text
note?.body = bodyTextField.text
realm.commitWriteTransaction()
// Set the note to be passed to NoteTableViewController after the unwind segue.
// AKA pass "note" to NoteTableViewController with whatever the text fields have in them
note = Note(title: title, body: body)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// Handle the text field’s user input through delegate callbacks.
titleTextField.delegate = self
// Set up views if editing an existing Note.
if let note = note {
navigationItem.title = note.title
titleTextField.text = note.title
bodyTextField.text = note.body
}
// Enable the Save button only if the text field has a valid Note name.
checkValidNoteName()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: UITextFieldDelegate
func textFieldDidBeginEditing(textField: UITextField) {
// Disable the Save button while editing.
saveButton.enabled = false
}
func checkValidNoteName() {
// Disable the Save button if the text field is empty.
let text = titleTextField.text ?? ""
saveButton.enabled = !text.isEmpty
}
func textFieldDidEndEditing(textField: UITextField) {
checkValidNoteName()
navigationItem.title = textField.text
}
}
And here's the original, NoteTableViewController. Updated a bit as well.
import UIKit
import Realm
class NoteTableViewController: UITableViewController {
// MARK: Properties
var notes = Note.allObjects()
override func viewDidLoad() {
super.viewDidLoad()
// Get Realm Database location
println(RLMRealm.defaultRealm().path)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return Int(notes.count)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "NoteTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NoteTableViewCell
let object = notes[UInt(indexPath.row)] as! Note
// Configure the cell...
cell.titleLabel.text = object.title
cell.bodyLabel.text = object.body
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return NO if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete from database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.deleteObject(notes[UInt(indexPath.row)] as! RLMObject)
realm.commitWriteTransaction()
// Delete row from table view
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .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
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" { // Clicked on a cell
let noteDetailViewController = segue.destinationViewController as! NoteViewController
// Get the cell that generated this segue.
if let selectedNoteCell = sender as? NoteTableViewCell {
let indexPath = tableView.indexPathForCell(selectedNoteCell)!
let selectedNote = notes[UInt(indexPath.row)] as! Note
noteDetailViewController.note = selectedNote
}
}
else if segue.identifier == "AddItem" { // Clicked add button
println("Adding new note")
}
}
#IBAction func unwindToNoteList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? NoteViewController, note = sourceViewController.note {
let uuid = NSUUID().UUIDString // Needed for primary key. see below
var unwindedNote = Note()
if let selectedIndexPath = tableView.indexPathForSelectedRow() { // User clicked on a row
// Updating of the note is done in NoteViewController
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
else {
// Add a new note.
let newIndexPath = NSIndexPath(forRow: Int(notes.count), inSection: 0)
// Persist in database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
unwindedNote.title = note.title
unwindedNote.body = note.body
unwindedNote.id = uuid // This is done for the primary key that Realm needs, unique for each object created.
realm.addObjects([unwindedNote])
realm.commitWriteTransaction()
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
}
Looking at the code you provided there, I was going to suggest that putting your Realm writing logic in the view controller segue-controlled methods might be a little too late.
I would normally recommend that you should place any logic involving Realm writes in a method that is explicitly called from a user interacting with it. For example, in the tableView(_ tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath) method when they tap on a table cell, and then once the write has completed, perform the segue (or segue unwinding) then.
In any case, good to hear you solved it!

Data won't load into my Table View

So I have a UITableViewController class for my notes app where everything is working fine, adding, deleting, and editing of the table view. But now I'm trying to load the data using "Realm" Database. I'm fairly new to iOS (Swift) and especially Realm, so I'm kind of confused. I got it to save into the database perfectly, and it shows up when I click the "Save" button in my navigation bar. But when I restart the app, the table view is completely empty. I've been stuck on this for a while now, tried everything I know so far, but just can not get it to show up whatsoever. Can someone help me out, and maybe tell me what I'm doing wrong, or not doing at all? Thank You very much.
Here is my class as well
import UIKit
import Realm
class NoteTableViewController: UITableViewController {
// MARK: Properties
var notes = [Note]() // Initialized with a default value (an empty array of Note objects).
override func viewDidLoad() {
super.viewDidLoad()
// Get Realm Database location
println(RLMRealm.defaultRealm().path)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return notes.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "NoteTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NoteTableViewCell
// Fetches the appropriate note for the data source layout in the notes array
let note = notes[indexPath.row]
// Configure the cell...
cell.titleLabel.text = note.title
cell.bodyLabel.text = note.body
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return NO if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
notes.removeAtIndex(indexPath.row)
// Get the default Realm (Will do this part later)
/*
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.deleteObject(notes[Int(indexPath.row)])
realm.commitWriteTransaction()
*/
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .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
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" { // Clicked on a cell
let noteDetailViewController = segue.destinationViewController as! NoteViewController
// Get the cell that generated this segue.
if let selectedNoteCell = sender as? NoteTableViewCell {
let indexPath = tableView.indexPathForCell(selectedNoteCell)!
let selectedNote = notes[indexPath.row]
noteDetailViewController.note = selectedNote
}
}
else if segue.identifier == "AddItem" { // Clicked add button
println("Adding new note")
}
}
#IBAction func unwindToNoteList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? NoteViewController, note = sourceViewController.note {
if let selectedIndexPath = tableView.indexPathForSelectedRow() { // User clicked on a row
// Update an existing note.
notes[selectedIndexPath.row] = note
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
else {
// Add a new note.
let newIndexPath = NSIndexPath(forRow: notes.count, inSection: 0)
notes.append(note)
// Persist in database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
Note.createInRealm(realm, withValue: notes[newIndexPath.row])
realm.commitWriteTransaction()
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
}
And this one is the Note Object class
import UIKit // Automatically imports Foundation
import Realm
class Note: RLMObject {
// MARK: Properties
dynamic var title: String = ""
dynamic var body: String = ""
// MARK: Initialization
init?(title: String, body: String) { // Failable initializer
// Initialize stored properties.
self.title = title
self.body = body
super.init()
// Initialization should fail if there is no title
if title.isEmpty {
return nil
}
}
// Must have for Realm to work
override init() {
super.init()
}
}
Solved it after a good 2 more days...Here is my updated class
Did as Swinny89 said, and started with a new notes object, of all objects, instead of initializing an "empty" notes array.
import UIKit
import Realm
class NoteTableViewController: UITableViewController {
// MARK: Properties
var notes = Note.allObjects()
override func viewDidLoad() {
super.viewDidLoad()
// Get Realm Database location
println(RLMRealm.defaultRealm().path)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return Int(notes.count)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "NoteTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NoteTableViewCell
let object = notes[UInt(indexPath.row)] as! Note
// Configure the cell...
cell.titleLabel.text = object.title
cell.bodyLabel.text = object.body
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return NO if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete from database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.deleteObject(notes[UInt(indexPath.row)] as! RLMObject)
realm.commitWriteTransaction()
// Delete row from table view
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .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
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" { // Clicked on a cell
let noteDetailViewController = segue.destinationViewController as! NoteViewController
// Get the cell that generated this segue.
if let selectedNoteCell = sender as? NoteTableViewCell {
let indexPath = tableView.indexPathForCell(selectedNoteCell)!
let selectedNote = notes[UInt(indexPath.row)] as! Note
noteDetailViewController.note = selectedNote
}
}
else if segue.identifier == "AddItem" { // Clicked add button
println("Adding new note")
}
}
#IBAction func unwindToNoteList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? NoteViewController, note = sourceViewController.note {
let uuid = NSUUID().UUIDString // Needed for primary key. see below
var unwindedNote = Note()
if let selectedIndexPath = tableView.indexPathForSelectedRow() { // User clicked on a row
// Updating of the note is done in NoteViewController
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
else {
// Add a new note.
let newIndexPath = NSIndexPath(forRow: Int(notes.count), inSection: 0)
// Persist in database
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
unwindedNote.title = note.title
unwindedNote.body = note.body
unwindedNote.id = uuid // This is done for the primary key that Realm needs, unique for each object created.
realm.addObjects([unwindedNote])
realm.commitWriteTransaction()
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
}
This is because your Note array is initialised as an empty array and that's what you are using to tell the tableView how many rows there are, which is 0.
Once your Note array has been set and has some data you can then call tableView.reloadData() to reload the array with the data.
Also, looking at the code above, your class is inheriting from UITableViewController rather than implementing UITableViewControllerDelegate and UITableViewControllerDataSource. Once your class implements these you need to make sure that you set the viewController as the datasource and delegate for the tableViewController either in the storyboard or through code.

Resources