Best way to set up multiple tableviewcell layouts - swift

I am in the process of creating a new tableviewcontroller however, I need this to be dynamic in a way that the layouts for each cell could change.
What I mean is that I am parsing news from JSON. Each news item could be of 3 types: Post, achievement, announcement. Each of these types contains different pieces of information. Post would have title, date, image and tags. Announcement would have the same but include an importance label. Achievement will have username, image, date, profile image, social sharing.
The news listing page will display all of the items but ordered by date but the layout for each of the types are completely different.
I have started and successfully got the below working:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row == 0)
{
let cell: NewsPostTableViewCell = tableView.dequeueReusableCellWithIdentifier("newsPostTableCell") as! NewsPostTableViewCell
return cell
}
else
{
let cell: AchievementCardTableViewCell = tableView.dequeueReusableCellWithIdentifier("achievementCardTableCell") as! AchievementCardTableViewCell
return cell
}
}
to display 2 of the different layouts but I then started thinking, how will I be able to determine in the cellForRowAtIndexPath what layout to apply? Basically in the JSON response there will be a template identifier to notify the app which template to apply but just don't know how to apply this...if possible.
Would it be best to create 3 different view controllers 1 for each of the layouts and embed them?

Depending on how you are parsing the JSON you could store the type of cell into an Array. Then as you look into the array with indexPath.row based on the type it finds there you have it execute a Switch.

Related

Data persistence with reusable cells

I have a large table designed to update a customers data protection preferences.
Some of the table is populated with reusable cells that contain a variable number of checkboxes, and depending on the json returned from the server, some of these checkboxes may be pre-checked.
When I pass the pre-checked state to the cell from tableView cellForRowAt all is well (checkboxes that are pre-checked are pre-checked). The problem I have is that these are reusable cells, and after a user has changed their selections, scrolling up or down the table triggers more calls to the setupCell function, which then resets the checkboxes to their original pre-checked state.
So, the question I have is...
What are the options for me to preserve a user’s selections after they have scrolled a table with resuable cells?
The switch statement in setupCell currently sets the pre-selections with the call to updateSelections(). Obviously this is the cause of the issue and I'm not entirely happy with placing logic directly in the cell anyway, but where is the best place to perform this logic only once? Or, is using reusable cells the wrong approach entirely to have pre-selections?
Any suggestions welcome. Here's a small code snippet to illustrate the point:
// UITableViewDataSource - passing the previous selections to setupCell in the UITableViewCell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let viewModel = viewModels[indexPath.row]
switch viewModel {
case .preferences(let preferenceId, let titleText, let isEnabled):
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Preferences") as? MarketingChannelPreferencesTableViewCell else {
return UITableViewCell()
}
cell.setupCell(id: id, text: text, isPreChecked: isPreChecked)
return cell
}
}
// UITableViewCell
func setupCell(id: String, text: String, isPreChecked: Bool) {
switch id {
case "email":
emailSelected = isPreChecked
updateSelections(id: id, isPreChecked: emailSelected)
case "post":
postSelected = isPreChecked
updateSelections(id: id, isPreChecked: postSelected)
case "text":
textSelected = isPreChecked
updateSelections(id: id, isPreChecked: textSelected)
default:
break
}
}
viewModels hold the information needed to setup each cell right? And you receive the viewModels from the service?
If so, when the user changes a specific checkbox, you should update the according viewModel. Thus, when you call setupCell inside cellForRowAt you should pass the updated info of each viewModel, resulting in the correct state of each checkbox.
You should make some action method for your checkbox buttons that you put on the MarketingChannelPreferencesTableViewCells and change your viewModels based on changing the value of these checkboxes. So when the cells data reload with user scrolling, cells show the new datas of viewModel
There're several ways.
I've created a small project maybe it will give you an solution to the problem.
https://drive.google.com/open?id=1d_RFdr6luNvRTdSC6XNE2vRWTi2IRyuT

Swift TableView Cell Images initially loading out of order

On the first View Controller I have a tableview that lists various sites like google, stack overflow... For each service added an image will load based on the first letter of that site. So an image of a T will load for Twitter.
If the user wants he/she can tap that cell and go to a 2nd VC and add the URL. When the user comes back to the first VC that site will try to pull the favicon instead of the letter image... this works, but not gracefully.
What I initially wanted was for each image to show up as soon as it loaded, obviously not all at once and not so it would disrupt the user interaction with the app.
What is happening now is that they show up a few at a time (which is ok) but not in the right place, initially. So say I have amazon, google, Microsoft, Facebook, and apple...the favicon would actually be out of order so Microsoft might have googles logo and after several seconds it might shift to Facebooks and then depending on how many there are it might shift again until it is all in the right matching place…This also happens if i scroll 'below the fold' and after several moments will right itself (the cell title remains in order however)
So I obviously have something in my code wrong, and would love, at the minimum, get it so it puts Facebook right the first time and then google, etc etc
OR another option could be all of the images START out as the letter image and then the code tries to replace it with the favicon...and get it right on the first try..
Any help would be great
Here is my Table View cellForRowAt code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "serviceCell", for: indexPath as IndexPath) as! ServiceTableViewCell
let row = indexPath.row
cell.serviceNameLabel.text = serviceArray[row].serviceName
DispatchQueue.global().async {
let myURLString: String = "http://www.google.com/s2/favicons?domain=\(self.serviceArray[row].serviceUrl)"
if let myURL = URL(string: myURLString), let myData = try? Data(contentsOf: myURL), let image = UIImage(data: myData) {
cell.serviceLogoImage.image = image
} else {
cell.serviceLogoImage.image = UIImage.init(named: "\(self.getLetterOrNumberAndChooseImage(text: self.serviceArray[row].serviceName))")
}
}
return cell
}
}
cell.serviceLogoImage.image =
Must be called from the Main Thread, since you are modifying User Interface.
Apple Documentation
Note For the most part, use UIKit classes only from your app’s main
thread. This is particularly true for classes derived from UIResponder
or that involve manipulating your app’s user interface in any way.
Check this thread how to run the different threads from background and to the main.

parse: check column then use data in that row

Is it possible to check the Parse column "ID" for 0 (any number) then if 0 (the one in the column) equals indexPath.row 0 (the first cell) it displays the data from the row of 0 in Parse? The picture above is the class for the viewcontroller, the tableview cell data is in another class.
ID: number that will be checked with the indexPath.row
navTitle: navigation bar title
articleTitle: UILabel
written: UILabel
date: UILabel
article: UITextView
Edit for possible duplicate: It is not a duplicate, the other question answers how to move data from a cell to a view controller using prepareForSegue. This question is asking how, if possible, to check a column on Parse and if a number in that column matches an indexPath.row it will use the data from that row to which the number corresponds.
I now understand you would like to show the data that is appropriate for a given index path in a detail view controller, after they tap on the cell at said index path. This is assuming you are correctly showing the appropriate data in the table view, and now the question is how to obtain and show the relevant data for the row the user tapped on in a new view controller.
To do this, you could add an internal property to your detail view controller that is an Int. Set this property to the table view's selected index path's row before the detail view controller is shown, perhaps in prepareForSegue for example.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let detailVC = segue.destinationViewController as? DetailViewController {
if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
detailVC.integerIdentifier = selectedIndexPath.row
}
}
}
Now in that detail view controller, you can access the property's value and query for the information you need from Parse given that value. (And you'll likely want to display some feedback to the user to inform them you're downloading the appropriate data to display.) For example, you could have the following code in a function you call from viewDidLoad:
let query = PFQuery(className: "eventsDetail")
query.whereKey("ID", equalTo: self.integerIdentifier)
query.getFirstObjectInBackgroundWithBlock { (detail: PFObject?, error: NSError?) -> Void in
//update your labels etc using the detail (Optional) PFObject
}
I want to note, importantly, that this is not a great way to store the data in Parse. One should not tie the data to how it will be displayed, which is the case here tying an identifier to the index path, forcing item with ID 0 to magically be the article details appropriate for the first cell in the table. You may wish to revisit the database design. For example, perhaps your other class that is used to generate a list of events could have a pointer to the event detail class, which would allow you to obtain the event details before you select an event and not have to query again for details.
Original Answer:
I understand you would like to show the data in this Parse class in your app in a table view, where the first table view cell displays the data for ID 0, the second cell displays the data for ID 1, etc.
To accomplish this, you'll want to query this class and apply an ascending order on the "ID" column. You will get back an array of PFObjects that are sorted as desired - lowest to highest ID, which you can use for your table view's data source to map them one-to-one.
Your query may look something like this:
let query = PFQuery(className: "Article")
query.orderByAscending("ID")
query.findObjectsInBackgroundWithBlock { (articles: [PFObject]?, error: NSError?) -> Void in
//use articles array as the data source, update the interface
self.articles = articles
self.tableView.reloadData()
}
Now in your table view data source methods, use this array to populate the table:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.articles.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let article = self.articles[indexPath.row]
//... configure cell, show article title, etc, return cell
}

Selecting Multiple Table View Cells At Once in Swift

I am trying to make an add friends list where the user selects multiple table view cells and a custom check appears for each selection. I originally used didSelectRowAtIndexPath, but this did not give me the results I am looking for since you can highlight multiple cells, but unless you unhighlight the original selected row you cannot select anymore. I then tried using didHighlighRowAtIndexPath, but this doesn't seem to work because now I am getting a nil value for my indexPath. Here is my code:
override func tableView(tableView: UITableView, didHighlightRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! AddedYouCell
let currentUser = PFUser.currentUser()?.username
let username = currentCell.Username.text
print(currentCell.Username.text)
let Friends = PFObject(className: "Friends");
Friends.setObject(username!, forKey: "To");
Friends.setObject(currentUser!, forKey: "From");
Friends.saveInBackgroundWithBlock { (success: Bool,error: NSError?) -> Void in
print("Friend has been added.");
currentCell.Added.image = UIImage(named: "checked.png")
}
}
How can I solve this? Thanks
I'm not going to write the code for you, but this should help you on your way:
To achieve your goal, you should separate the data from your views (cells).
Use an Array (i.e. friendList) to store your friend list and selected state of each of them, and use that Array to populate your tableView.
numberOfCellsForRow equals friendList.count
In didSelectRowAtIndexPath, use indexPath.row to change the state of your view (cell) and set the state for the same index in your Array
In cellForRowAtIndexpath, use indexPath.row to retrieve from the Array what the initial state of the cell should be.

Segue in UITableView with multiple sections, each containing objects filtered from a single array

I'm a beginner, clearly out of my league and I haven't been able to find an answer online.
I have a UITableViewController with a UITableViewshowing custom objects stored in one array. I don't show all the object of the array in one single section of said TableView: the TableView has multiple sections, each containing a filtered portion of my objects array (I filter the custom objects array checking that the object category property is equal to a category that I specified in a categories array).
This filtering and showing the single array in different sections is working fine (I understand that maybe it's not elegant, as I said I'm a beginner in coding and I absolutely needed to work with one single array, without creating other arrays corresponding to the filtered results), but to better understand my issue I think it's better that I show what I did, so here's the TableView part of my code:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return myCategoriesArray.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
for (var i = 0; i <= section; i++){
if section == i {
for eachCategory in myCategoriesArray {
return myObjectsArray!.filter() { $0.objectCategoryProperty == myCategoriesArray[i] }.count
}
}
}
// ...
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("objectCell", forIndexPath: indexPath) as UITableViewCell
for (var i = 0; i <= indexPath.section; i++){
if indexPath.section == i {
for eachCategory in myCategoriesArray {
cell.textLabel?.text = myObjectsArray!.filter() { $0.objectCategoryProperty == myCategoriesArray[i] }[indexPath.row].nameProperty
return cell
}
}
}
// ...
}
This works in the sense that I have the UITableViewController showing all my objects, but filtered in separated sections by category.
My issue is with the segue when I select a cell and show a detail view.
Here's my prepareForSegue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var nextVC = segue.destinationViewController as MyNextViewController
if let indexPath = tableView.indexPathForSelectedRow() {
let selected = myObjectsArray![indexPath.row]
nextVC.passedObject = selected
}
}
}
I'm sure that many of you already see my issue: the object that I pass to the next ViewController is selected in the custom objects array using as index [indexPath.row], but indexPath.row starts at 0 for each section, so when I select an object its index in the TableView is not equal to the index in the custom objects array, meaning that I pass the wrong object.
Now, I'm stuck because I don't see a way to pass the right (meaning, selected) object to the next View Controller while preserving the fact that I'm working with only a single array.
I was toying with the idea of adding an objectIDString property to every object and a single var currentlySelectedObjectIDString that is set every time a cell is selected and try to pass to the next View Controller the object with the objectIDString property matching the currentlySelectedObjectIDString, but it looks like a bad idea to my inexperienced eyes and I'm actually not sure how I could accomplish that even if I wanted to (maybe implementing didSelectRowAtIndexPath:, but I have not been able to make it work).
Any help would be really appreciated, I've been stuck on this for so long I begin to question a) my sanity b)every decision I made so far in the project (meaning, the single array for all objects that is filtered in sections), but I'm already so invested in it that I really would like not to have to start over.
Thank you,
Cesare
P.S. I hope my question is clear, english isn't my main language... sorry for any mistake!
I suggest you, to use a NSFetchedResultsController. This class have a property sectionNameKeyPath. In this property you could set your category and you won't need more iterate with a repetition in each numberOfSection and numberOfRows.
like this:
let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: "event.startDate", cacheName: nil)
In my case i need filter data by event.startDate.
I don't know if you are using core data, but if you are using, this is the better way to do this.
I'll expose them for you!
In the first moment NSFetchedResultsController like complicated, but its very very useful. Don't be afraid.
I don't know exactly your model and data. In this case i'll show you my owner sample.
Please see my question in the following link:
Sectioning TableView and rows with Core Data Swift
In this link, see my question, and in the bottom i'll explain the complete solution with the others answer.
If this is not clear for you, please, talk with me :.)
I spent all day trying to figure out a way to solve my own question above and I think I've finally found a working-workaround.
My premise and disclaimer is that this is a pile of hacks, I post this only in case this might help someone in my situation in the future, but clearly the way to deal with this kind of situation is Core Data, as suggested by Weles' answer, not what I did.
Here's briefly what I've done to get my multi-component UITableView, in which all the data come from a single array of custom objects that is filtered by a different value in every component, to pass the selected object to the detail view when a cell is selected.
1) I added to all my customObjects an objectID : String computed property (current date + random number).
2) I added a var currentlySelectedObjectID : String? in my TableViewController.
3) I subclassed UITableViewCell, creating a CustomTableViewCell class that only adds to the normal class a var selectedCellID : String?, then I changed my cellForRowAtIndexPath to return a CustomTableViewCell instead of a UITableViewCell. Inside this method, before returning the cell, I also set the property selectedCellID of the cell equal to objectID of the current object. I also had to change the class of the cell in the Storyboard from UITableViewCell to CustomTableViewCell.
4) In the Storyboard I removed the segue from the cell to the detailViewController that was automatically created by Xcode and I set a custom StoryboardID to the detailViewController ("detailVC"),
5) Inside didSelectRowAtIndexPath of TableViewController I did all the work that before I was trying to do in prepareForSegue, but in a different way (not a segue, a self.navigationController?.pushViewController). Here's the code:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexPath = tableView.indexPathForSelectedRow();
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as CustomTableViewCell!;
self.currentlySelectedObjectID = currentCell.selectedCellID
// detailViewController instance
var detailVC = self.storyboard?.instantiateViewControllerWithIdentifier("detailVC") as MyDetailViewController
// I filter my objects array to "extract" the object with the objectID property equal to the currentlySelectedObjectID property (which is equal to the currentCell.selectedCellID, as set above). This array must have only 1 value. If so, I set the property passedCustomObject that I have in my detailViewController to the same object selected.
if (myObjectsArray!.filter() { $0.objectID == self.currentlySelectedObjectID }).count == 1 {
detailVC.passedCustomObject = (myObjectsArray!.filter() { $0.objectID == self.currentlySelectedObjectID })[0]
} else {
println("Error passing the object selected in the TableView to the DetailView")
}
// I push the detailViewController on top of the stack
self.navigationController?.pushViewController(detailVC, animated: true)
}
I think there are very good chance that a decent programmer (I am not one, but I hope to become one some day), seeing what I did, could faint.
Again, I don't think anyone should do this, if you're in my same situation go straight to Core Data: I spent a day on this, there's good chance that in three or four I could have had Core Data working.
But still, as hacked and inefficient as this is, it works... I tested multiple times. So, having spent so much time and having found no useful similar previous answers online, I thought to post mine.
Don't do this, I'm really afraid this is easily breakable! :)
I still look forward to other answers, to learn from my numerous mistakes!