How to group tableView data using sectionNameKeyPath (Swift) - ios

I've not been able to figure out how to do the following in Swift and looking for pointers.
I'm pulling string values from Core Data and displaying them in a UITableView...that's working and displaying all entries. I now want to 'group' these values by date created (also stored in Core Data) but am having a hard time implementing this group header...here's what I have:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var sound = self.sounds[indexPath.row]
var cell = UITableViewCell()
// Convert Date to String
var formatter: NSDateFormatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd-HH-mm-ss"
let dateTimePrefix: String = formatter.stringFromDate(sound.dateSaved)
// Display the following in each rows
cell.textLabel!.text = sound.name + " Saved: " + dateTimePrefix
return cell
}
Could someone point me in the right direction as to how to implement 'headers'? I already have the tableView in my Main.storyboard setup to 'Grouped' as a style.
--- Update
Thanks for the response below but, unfortunately, the examples are in Obj-C and I'm shooting for swift.
To expand, I have the following retrieving data from Core Data and displaying all in UITableView:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = self
self.tableView.delegate = self
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
var context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var request = NSFetchRequest(entityName: "Sound")
self.sounds = context.executeFetchRequest(request, error: nil)! as [Sound]
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sounds.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestCell")
var sound = self.sounds[indexPath.row]
// Convert Date to String
var formatter: NSDateFormatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd # HH:mm:ss"
let dateTimePrefix: String = formatter.stringFromDate(sound.dateSaved)
// Display the following in each rows
cell.textLabel!.text = sound.name // Sound Name
// Display the following as each subtitles
cell.detailTextLabel!.text = dateTimePrefix // Sound Duration (Currently 'date')
return cell
}
Could someone point me in the right direction as to how I go about 'grouping' each by month with a 'month' header using sectionNameKeyPath, I believe? Specifically, which piece of code would need to be modified to break list into 'sections'.

Here are some pointers:
You need to use an NSFetchedResultsController ("FRC") - check out the documentation here. You will see that you specify the sectionNameKeyPath when initialising the FRC.
There is boilerplate code for updating a tableView using an FRC. The easiest way to get this, and observe how the FRC works, is to create a project using Apple's Master/Detail application template, ensuring you select the "Use Core Data" option. Play with it in that project to see how it works, and then cut/paste into your own project.
In your case, you want to the month (and presumably year?) as the section name. There are a few ways to do that, but the easiest is to create a function (eg. sectionIdentifier) in your Sound class which returns a string with the section name, in the format you desire, derived from the dateSaved. Then specify sectionNameKeyPath:sectionIdentifier when initialising the FRC.

Related

Embedded UITableView not reloading with reloadData() on UIButtonClick in Swift 4

I am trying to get my table view to update when I add a new cell to my SQLite Table. However, as of now, when I add a new cell to the SQLite Table and call reloadData, nothing updates or happens. My UITableView is embedded into my Main View Controller so that might be part of the issue. I also feel as if this may be an issue with the tableView.dataSource or something. I am not sure. I am very new to swift.
I have tried calling the reloadData from the UITableViewController file and from the UIViewController which contains the button that is supposed to add data and then update the Table. Neither have worked. I also tried to substitute it with tableView?.beginUpdates() and tableView?.endUpdates()
My button code (part of button script):
#IBAction func addTask(_ sender: Any) {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mmZZZZZ"
let taskDateFormatted = dateFormatter.string(from: taskDate.date)
let newTask = Task(title: taskTitle.text!, description: descriptionTextView.text,
date: taskDateFormatted)
let instSQLiteData = SQLiteData()
let instTaskTableVC = TaskTableVC()
instSQLiteData.uploadData(newTask.title, newTask.description, newTask.date)
instTaskTableVC.taskTable?.reloadData()
dismiss(animated: true, completion: nil)
}
My UITableViewController script/class (part of tableview script):
class TaskTableVC : UITableViewController {
#IBOutlet var taskTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.taskTable.dataSource = self
self.taskTable.delegate = self
}
How my UITableViewController loads data (part of tableview script):
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let instSQLiteData = SQLiteData()
return instSQLiteData.getRowCount()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let instSQLiteData = SQLiteData()
let cell = tableView.dequeueReusableCell(withIdentifier: "taskCell", for: indexPath)
cell.textLabel?.text = instSQLiteData.getTasksArray()[indexPath.row]
return cell
}
Once the button is pressed I just want the table to update but I cannot get it to work even though I have tried to follow a ton of other questions with the same topic. Much thanks!

How to present data, saved in Realm, in another viewController's tableViewCells?

I'm trying to make an app, in which one of the functions is, that in one viewController, the user can enter some data about a given person (a photo, a name and an answer to a certain question). When pressing the "save new person-button", the user is to be redirected to the previous viewController, which amongst other things holds a tableView.
In this tableView, I want there to be created a new cell for each time the user presses the "save new person-button" in the other viewController. The cell should, of course, hold the person's name (and ideally also a miniature of the photo).
Now, it is important that the data is stored 'internally' - also after exiting a certain viewController or closing the app. Therefore I'm using Realm as a database to store all the data.
I'm very new when it comes to using Realm, so maybe my questions seem stupid. Basically, I think I have been able to make the "Save new person-button" save all the data in the realm database. But when trying to make the button create new cells (which must stay, once created!!) in the previous viewController... All kinds of weird things happen!
What the code underneath shows is my current attempt on passing at least the name through a segue, but that creates several problems: The cells don't stay. If you try to add a new cell, it just overrides the old one. And finally, the PeopleData.count apparently counts the number of letters in the name - and creates fx. 5 identical cells for a 5 letter long name! :D
I'm sure this is not the right way to pass the data ... So what is the best way to present this data? So that the cells stay once created - and so that the user can add several new people/several new cell without overriding the old ones!
Relevant code:
viewController where the user can enter the data:
#IBAction func SaveNewPerson() {
let NewPerson = person()
NewPerson.name = PersonName.text!
NewPerson.answer = PersonAnswer.text!
NewPerson.extraIdentifier = PersonExtraIdentifier.text!
let realm = try! Realm()
try! realm.write {
realm.add(NewPerson)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// let allPeople = realm.objects(person.self)
let SomePersonName = PersonName.text!
if segue.identifier == "PassingSomeInfo" {
if let vc = segue.destination as? AllPersonsInYourDiary {
vc.PeopleData = SomePersonName
}
}
Viewcontroller with the tableView:
var PeopleData: ""
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return PeopleData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Person1", for: indexPath)
cell.textLabel?.text = self.PeopleData
cell.textLabel?.numberOfLines = 0
cell.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
return cell
}
Third viewController (used to store all the person-data):
class person: Object {
#objc dynamic var name = ""
#objc dynamic var answer = ""
#objc dynamic var extraIdentifier = "" }
Thanks!
This question is honestly a little to broad. What you are asking for are several things at once. And if I were to write the implementation for you I wouldn't answer your actual question on what you need to do. What I think you are asking is this:
How do I write and retrieve data from realm.
How do I structure my code for passing data between views.
So my recommendation is this.
Don't pass the data with a segue. Create a class that will access Realm and retrieve the data you need for that view. Name it something like Database, Realm or LocalStorage.
In that class add methods for writing to Realm and retrieving from Realm. How to do that you can find in the Realm documentation: https://realm.io/docs/swift/latest/
In your view that needs to display the entries instantiate your Realm class. Some psuedo code to help you on the way:
class ViewController {
fileprivate let database = LocalStorage()
fileprivate var peopleData: [People]()
viewDidLoad() {
super.viewDidLoad()
database.retrieveData(primaryKey: "extraIdentifier", completionHandler: { entries, err in
if err == nil {
self.peopleData = entries
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return peopleData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let person = self.peopleData[indexPath.item]
let cell = tableView.dequeueReusableCell(withIdentifier: "Person1", for: indexPath)
cell.textLabel?.text = person.name
cell.textLabel?.numberOfLines = 0
cell.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
return cell
}
}
After you call
try! realm.write {
realm.add(NewPerson)
}
you already saved on realm so you don't have to pass this on the segue you can retrieve from realm on the next view controller by calling
realm.objects(ofType: NewPerson.self)
This will return an array of NewPerson or you can also search just one with your primary key
realm.object(ofType: NewPerson.self, forPrimaryKey: yourKey)

CollectionView in TableView displays false Data swift

I'm trying to combine a CollectionViewwith a TableView, so fare everything works except one problem, which I cant fix myself.
I have to load some data in the CollectionViews which are sorted with the header of the TableViewCell where the CollectionView is inside. For some reason, every time I start the app, the first three TableViewCells are identical. If I scroll a little bit vertically, they change to the right Data.
But it can also happen that while using it sometimes displays the same Data as in on TableViewCell another TableViewCell, here again the problem is solved if I scroll a little.
I think the problem are the reusableCells but I cant find the mistake myself. I tried to insert a colletionView.reloadData() and to set the cells to nil before reusing, sadly this didn`t work.
My TableViewController
import UIKit
import RealmSwift
import Alamofire
import SwiftyJSON
let myGroupLive = DispatchGroup()
let myGroupCommunity = DispatchGroup()
var channelTitle=""
class HomeVTwoTableViewController: UITableViewController {
var headers = ["LIVE","Channel1", "Channel2", "Channel3", "Channel4", "Channel5", "Channel6"]
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.navigationBar.isTranslucent = false
DataController().fetchDataLive(mode: "get")
DataController().fetchDataCommunity(mode: "get")
}
//MARK: Custom Tableview Headers
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return headers[section]
}
//MARK: DataSource Methods
override func numberOfSections(in tableView: UITableView) -> Int {
return headers.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
//Choosing the responsible PrototypCell for the Sections
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellBig", for: indexPath) as! HomeVTwoTableViewCell
print("TableViewreloadMain")
cell.collectionView.reloadData()
return cell
}
else if indexPath.section >= 1 {
// getting header Titel for reuse in cell
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
else {
channelTitle = self.tableView(tableView, titleForHeaderInSection: indexPath.section)!
let cell = tableView.dequeueReusableCell(withIdentifier: "cellSmall", for: indexPath) as! HomeVTwoTableViewCellSmall
// anti Duplicate protection
cell.collectionView.reloadData()
return cell
}
}
}
}
My TableViewCell with `CollectionView
import UIKit
import RealmSwift
var communities: Results<Community>?
class HomeVTwoTableViewCellSmall: UITableViewCell{
//serves as a translator from ChannelName to the ChannelId
var channelOverview: [String:String] = ["Channel1": "399", "Channel2": "401", "Channel3": "360", "Channel4": "322", "Channel5": "385", "Channel6": "4"]
//Initiaize the CellChannel Container
var cellChannel: Results<Community>!
//Initialize the translated ChannelId
var channelId: String = ""
#IBOutlet weak var collectionView: UICollectionView!
}
extension HomeVTwoTableViewCellSmall: UICollectionViewDataSource,UICollectionViewDelegate {
//MARK: Datasource Methods
func numberOfSections(in collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return (cellChannel.count)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCellSmall", for: indexPath) as? HomeVTwoCollectionViewCellSmall else
{
fatalError("Cell has wrong type")
}
//removes the old image and Titel
cell.imageView.image = nil
cell.titleLbl.text = nil
//inserting the channel specific data
let url : String = (cellChannel[indexPath.row].pictureId)
let name :String = (cellChannel[indexPath.row].communityName)
cell.titleLbl.text = name
cell.imageView.downloadedFrom(link :"link")
return cell
}
//MARK: Delegate Methods
override func layoutSubviews() {
myGroupCommunity.notify(queue: DispatchQueue.main, execute: {
let realm = try! Realm()
//Getting the ChannelId from Dictionary
self.channelId = self.channelOverview[channelTitle]!
//load data from Realm into variables
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.dataSource = self
self.collectionView.delegate = self
print("collectionView layout Subviews")
self.collectionView.reloadData()
})
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCommunity = (cellChannel[indexPath.row].communityId)
let home = HomeViewController()
home.showCommunityDetail()
}
}
Thanks in advance.
tl;dr make channelTitle a variable on your cell and not a global variable. Also, clear it, and your other cell variables, on prepareForReuse
I may be mistaken here, but are you setting the channelTitle on the cells once you create them? As I see it, in your viewController you create cells based on your headers, and for each cell you set TableViewController's channelTitle to be the title at the given section.
If this is the case, then the TableViewCell actually isn't receiving any information about what it should be loading before you call reloadData().
In general, I would also recommend implementing prepareForReuse in your HomeVTwoTableViewCellSmall, since it will give you a chance to clean up any stale data. Likely you would want to do something like set cellChannel and channelId to empty strings or nil in that method, so when the cell is reused that old data is sticking around.
ALSO, I just reread the cell code you have, and it looks like you're doing some critical initial cell setup in layoutSubviews. That method is going to be potentially called a lot, but you really only need it to be called once (for the majority of what it does). Try this out:
override the init with reuse identifier on the cell
in that init, add self.collectionView.dataSource = self and self.collectionView.delegate = self
add a didSet on channelTitle
set channelTitle in the viewController
So the code would look like:
var channelTitle: String = "" {
didSet {
self.channelId = self.channelOverview[channelTitle]!
self.cellChannel = realm.objects(Community.self).filter("channelId = \(String(describing: self.channelId)) ")
self.collectionView.reloadData()
}
}
This way you're only reloading your data when the cell is updated with a new channel, rather than every layout of the cell's views.
Sorry... one more addition. I wasn't aware of how your channelTitle was actually being passed. As I see it, you're using channelTitle as a global variable rather than a local one. Don't do that! remove channelTitle from where it is currently before implementing the code above. You'll see some errors, because you're setting it in the ViewController and accessing it in the cell. What you want is to set the channelTitle on the cell from the ViewController (as I outlined above). That also explains why you were seeing the same data across all three cells. Basically you had set only ONE channelTitle and all three cells were looking to that global value to fetch their data.
Hope that helps a little!
(also, you should be able to remove your else if block in the cellForRowAtIndexPath method, since the else block that follows it covers the same code. You can also delete your viewDidLoad, since it isn't doing anything, and you should, as a rule, see if you can get rid of any !'s because they're unsafe. Use ? or guard or if let instead)

How to fix the Disappearing/Invisible/Empty UITableViewCells issue in Swift?

I have two ViewControllers, one is GoalsViewController and the other one is AddNewGoalViewController.
The GoalsViewController is useful to delete goals (cells) and to add new goals (cells). There is a UITableView and a button, Add new Goal. When the user presses the Add new Goal button, it will pass to AddNewGoalViewController. In AddNewGoalViewController users will select workout, notifications (how many times they want to be notified), and how much they want to run, walk or do any other work.
I checked a tutorial (click on word "tutorial" to check it), and it was helpful. The problem is that it is implementing empty cells. Download my project to check it better.
EDIT: After spending A LOT of time looking into your project, I found the issue.
Click on your Main.Storyboard file. Click on the file inspector. Uncheck Size Classes. Done. Your project works !
This seems to be an XCode bug. If you check again Size Classes, your project should still work.
The fix is therefore to uncheck and then check Size Classes in the File Inspector of your Main.storyboard file.
NONETHELESS: My syntax advice is still valid, it makes for cleaner code:
Well, did you check the solution of the exercise?
There is a link at the end of the page ;)
1st Difference:
var workouts = [Workout]()
var numbers = [Workout]()
func loadSampleMeals() {
let workouts1 = Workout(name: "Run", number: "1000")!
let workouts2 = Workout(name: "Walk", number: "2000")!
let workouts3 = Workout(name: "Push-Ups", number: "20")!
workouts += [workouts1, workouts2, workouts3]
numbers += [workouts1, workouts2, workouts3]
}
should be:
var workouts = [Workout]()
func loadSampleMeals() {
let workouts1 = Workout(name: "Run", number: "1000")!
let workouts2 = Workout(name: "Walk", number: "2000")!
let workouts3 = Workout(name: "Push-Ups", number: "20")!
workouts += [workouts1, workouts2, workouts3]
}
2nd Difference:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "DhikrTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! GoalsTableViewCell
// Fetches the appropriate meal for the data source layout.
let dhikr = workouts[indexPath.row]
let number = numbers[indexPath.row]
cell.nameLabel.text = dhikr.name
cell.numberLabel.text = number.number
//cell.photoImageView.image = dhikr.photo
//cell.ratingControl.rating = dhikr.rating
return cell
}
should be:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "DhikrTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! GoalsTableViewCell
// Fetches the appropriate meal for the data source layout.
let dhikr = workouts[indexPath.row]
cell.nameLabel.text = dhikr.name
cell.numberLabel.text = dhikr.number
//cell.photoImageView.image = dhikr.photo
//cell.ratingControl.rating = dhikr.rating
return cell
}
P.S.:
class Workout {
// MARK: Properties
var name: String
//var notifications: Int
var number: Int
// MARK: Initialization
init?(name: String, number: Int) {
// Initialize stored properties.
self.name = name
//self.notifications = notifications
self.number = number
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty || number < 0{
return nil
}
}
}
number will never be < 0, perhaps you meant == 0?.

NSManagedObject data is <fault> and entity is null, after UITableViewCell comes back into view

I am seeing some very odd behavior with Core Data using Swift and Xcode 7.2. Below is an animation illustrating the issue.
I created a simple example with an entity named 'Person' that has only a 'name' attribute. I fetch all of the 'Person' entities and display them in a table view. If I force a cell to be reloaded then the textLabel that holds the name and NSManagedObject objectID displays without the name. The objectID, however, remains unchanged. This happens both on the device and in the simulator.
I'm printing the value of the Person entity to the console in "cellForRowAtIndexPath" so that I can show the value when the cell is initially loaded vs. when it's reloaded.
As you can see, it appears that the NSManagedObject is still present despite the fact that it seems to lose all references to its 'Person' subclass.
Here is the code for the affected ViewController:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let cellIdentifier = "PersonCell"
#IBOutlet weak var tableView: UITableView!
var people: NSArray = []
override func viewDidLoad() {
super.viewDidLoad()
people = fetchPeople()
}
func fetchPeople() -> NSArray {
let context = AppDelegate().managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Person")
let sort = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sort]
do {
let results = try context.executeFetchRequest(fetchRequest)
return results
} catch {
let error = error as NSError
print("\(error.domain) \(error.code): \(error.localizedDescription)")
return []
}
}
// MARK: - UITableView methods
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return people.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
let person = people[indexPath.row]
print("in cellForRowAtIndexPath:")
print(people[0])
cell.textLabel?.text = "\(person.name) - \(person.objectID)"
return cell
}
}
Any help is appreciated. Thank you.
The problem is here:
let context = AppDelegate().managedObjectContext
You create a new instance of the application delegate, and with it
a managed object context. As soon as program control returns from fetchPeople(), there is no reference to the context anymore and it is
deallocated. The effect is that accessing properties of managed objects
which were created in this context returns nil.
What you want is to get a reference to the (one and only) application delegate
and its managed object context, something like
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
Alternatively, pass the context to the view controller in
the didFinishLaunchingWithOptions method or in
prepareForSegue or whatever is suitable in your app.

Resources