Swift 2.1 - Using UITableView for a single cell - ios

I want to achieve something similar to the following example image
where I want to present a selected or default category on the first screen
and when it's clicked it moves to the next screen with other category options to choose.
In this example, where it says Entertainment and Project 01 represent different data entities (data store) and it looks like it's using UITableView with each UITableViewCell connected to different data store.
I first want to know if my analysis so far is correct.
In my case, I just need to do that Entertainment part and in the next screen, show all category options like the second screen in the example. And after selection is made, the first screen should reflect the selection from the second screen.
Is UITableView the right choice to show this single field (cell) and segue to another ViewController?
All the demo app examples I see, don't demonstrate the usage of UITableView for this purpose so I'm not sure what is the best option in my case.

Yes UITableView is a good option. Just create a delegate method in "choose category" class. Call the delegate on didSelectRowAtIndexPath method of the UITableView in Category class. Then assign Expense class as the delegate for the category class.
Basically you'll be performing a segue once you click on entertainment. User will be presented with "Choose Category controller" Once the user selects the table row. Delegate will be called. Implement that delegate in the expense class. And in the implementation just reload the table with the new value.
UPDATE:
Your choose category class will be something similar to :
import UIKit
protocol ChooseCategoryControllerDelegate: class {
func categoryController(controller: ChooseCategoryTableViewController, didSelectCategory category: String)
}
class ChooseCategoryTableViewController: UITableViewController {
let categories = ["category1","category2","category3","category4"]
var selectedcategory: String!
weak var delegate: ChooseCategoryControllerDelegate!
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("TypeCell", forIndexPath: indexPath)
cell.textLabel?.text = categories[indexPath.row]
return cell
}
// MARK: - Table view delegate
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedcategory = categories[indexPath.row] as? String
delegate.categoryController(self, didSelectCategory: selectedcategory)
}
}
In your expense controller add the following code outside your class declaration:
extension ExpenseController: ChooseCategoryControllerDelegate {
func typesController(controller: ChooseCategoryTableViewController, didSelectCategory category: String) {
let selectedCategory = category
dismissViewControllerAnimated(true, completion: nil)
//update your table category with the new category!!
//reload your table here
}
}
Re-Update:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "CategorySegue" {
let controller = segue.destinationViewController as! ChooseCategoryTableViewController
controller.delegate = self
}
}
Also name your segue as "CategorySegue"

It depends, if the only option is the Entertainment then maybe UITableView is not the best option. Regardless, you can use a UITableView with static cells and a segue from the entertainment cell to the category.

Related

Handle events in subviews in MVVM in Swift

I am trying to get into MVVM in Swift and I am wondering how to handle events in subviews in MVVM, and how these events can travel up the chain of views/viewmodels. I'm talking about pure Swift for now (no SwiftRx etc.).
Example
Say I have a TableViewController with a TableViewModel. The view model holds an array of objects and creates a TableCellViewModel for each one, since each cell represents one of these objects. The TableViewController gets the number of rows to display from its model and also the view model for each cell, so it can pass it along to the cell.
We then have a TableCell and each cell has a TableCellViewModel. The TableCell queries its model for things like user-facing strings etc.
Now let's say TableCell also has a delete button that delete's that row. I'm wondering how to handle that: Usually, the cell would forward the button press to its view model, but this is not where we need it - we eventually need to know about the button press in either TableViewController or TableViewModel, so we can remove the row from the table view.
So the question is:
How does the button event get from a TableCell upwards in the view chain in MVVM?
Code
As requested in the comments, code that goes with the example:
class TableViewController: UIViewController, UITableViewDataSource {
var viewModel: TableViewModel = TableViewModel()
// setup and such
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.viewModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableCell
cell.viewModel = self.viewModel.cellViewModel(at: indexPath.item)
return cell
}
}
class TableViewModel {
// setup, get data from somewhere, ...
var count: Int {
return self.modelObjects.count
}
func cellViewModel(at index: Int) -> TableCellViewModel {
let modelObject = self.modelObjects[index]
let cellViewModel = TableCellViewModel(modelObject: modelObject)
return cellViewModel
}
}
class TableCell {
var viewModel: TableCellViewModel!
// setup UI, do what a cell does
func viewModelChanged() {
self.titleLabel.text = self.viewModel.title()
}
func deleteButtonPressed(_ sender: UIButton) {
// Oh, what to do, what to do?
}
}
class TableCellViewModel {
private var modelObject: ModelObject
init(modelObject: ModelObject) {
self.modelObject = modelObject
}
func title() -> String {
return self.modelObject.title
}
}
TableViewModel is the source of truth, so all global operations should be performed in there. Pressing a button is completely UI operation and viewModel shouldn't handle this in direct way.
So, for now we know two facts:
TableViewModel should delete the cell from array and then viewController should handle the deletion animation process;
Button press shouldn't be handled in child viewModel.
According to this you can achieve it by:
Pass button pressed event up to viewController (use callback or delegate pattern);
Call TableViewModel method to delete specific cell:
viewModel.deleteCell(at: indexPath)
Properly handle deletion animation in viewController.
may be you can use nextResponder util nextResponder is VC, and VC responder to delegate (eg:CellEventDelegate) that handle delete data and cell
UIResponder *nextResponder = pressedCell.nextResponder;
while (nextResponder) {
if ([nextResponder conformsToProtocol:#protocol(CellEventDelegate)]) {
if ([nextResponder respondsToSelector:#selector(onCatchEvent:)]) {
[((id<CellEventDelegate>)nextResponder) onCatchEvent:event];
}
break;
}
nextResponder = nextResponder.nextResponder;
}

Master Detail View With Segue

I'm Trying to learn how to do a detail view for my project .
I have a simple tableView with a simple Array data to fill it.
The Table View :
TableView Example
I designed a detail View as well, with static tableViewCells
Detail View example :
Example
I'v Connected both with a segue :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("Profile", sender: indexPath);
}
I also connected all the labels and images with Outlets i want to change between each cell but i don't how to advance from here.
right now every cell shows the same thing but i want to change the data between rows . So i would like to change the data through the segue and create a master detail application like in my tableview. Can anybody help me ?
Am using Swift 2.3 and Xcode 8.1
If I understand your question correctly, you just want to pass dataSource element to the next viewController. So you can just pick it using indexPath.row and use sender parameter to set it in prepareForSegue method.
The code below assumes your dataSource is self.users array.
Swift 3
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let user = self.users[indexPath.row]
self.performSegueWithIdentifier("Profile", sender: user)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let segueId = segue.identifier? else { return }
if segueId == "Profile" {
guard let profileVC = segue.destination as? ProfileViewController else { return }
profileVC.user = sender as? User
}
}
Swift 2
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let user = self.users[indexPath.row]
self.performSegueWithIdentifier("Profile", sender: nil)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let segueId = segue.identifier else { return }
if segueId == "Profile" {
guard let profileVC = segue.destinationViewController as? ProfileViewController else { return }
profileVC.user = sender as? User
}
}
Edit
im trying to change data like al the labels you saw between rows like
for example shalvata will have a different data from light house and
so , change the labels and images and so on
It is still unclear for me what data you want to change exactly. Also I don't understand the language on your screenshots, but since you name the relationship as master-detail, I suppose the second screen is meant to show more info about the entity selected on the first screen.
If so, you should start from designing you model so that it contains all those fields you need on the second screen. Judging by the icons it would be something like
struct Person {
var name: String?
var image: UIImage?
var age: Int?
var address: String?
var phone: String?
var schedule: String?
var music: String?
var smoking: Bool?
var car: String?
var info: String?
var hobby: String?
}
Note: Remove ? for those fields which aren't optionals, i.e. always must be set for every entity (perhaps name field)
Usage
I don't known how and when you create your Person array, but basically there are two approaches:
Use a list of entities with all fields filled on MasterVC and just pass the selected person to the DetailVC in didSelectRowAtIndexPath
Use a list of entities with some basic data (name, address, image) required for MasterVC and fill the rest of the fields only when required (didSelectRowAtIndexPath method)
In any case you'll get selected person in DetailVC and now everything you need is to use that data in cellForRow method, just as you did on MasterVC. Perhaps it would be a better option to use static TableViewController for Details screen.
Sounds like what you're trying to do does not involve segues at all. You can change data of cells using the cellForRow method in your tableViewController.
https://developer.apple.com/reference/uikit/uitableview/1614983-cellforrow
For example
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = "foo"
return cell
}
If that sounds confusing to you then you should take a step back and do some tutorials then post specific questions on SO.

UITableViewCells not appearing in second Tab

I have the following problem:
I am making a Pokédex-like application that displays a list of all 721 Pokémon on the first tab, and another list on the second tab containing My Favorite Pokémon. Essentially, there are two identical ViewControllers connected to my TabBar.
My storyboard is as follows:
So here is the problem:
The TableView on the first (and initial) tab works fine. However, when I load the TableView on the second tab the Pokémon are loaded, but not displayed. I am able to click the TableViewCell and go to the detail page, but the label in the TableViewCell is not showing anything.
This is the code I use for loading Favorites TableView
class FavoritesViewController: BaseViewController,
UITableViewDataSource, UITableViewDelegate {
#IBOutlet var FavoritesListView: UITableView!
var pokemonList: [String] = ["Nothing Here!"]
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FavoriteCell", forIndexPath: indexPath) as! FavoriteCell
var name = pokemonList[indexPath.row]
capitalizeFirstLetter(&name)
cell.nameLabel.text = name
return cell;
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pokemonList.count
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print(pokemonList[indexPath.row])
self.performSegueWithIdentifier("ToPokemonDetail", sender: pokemonList[indexPath.row])
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "ToPokemonDetail"){
let destination = segue.destinationViewController as! PokemonDetailViewController
let thisPokemon = sender as! String
destination.currentPokemon = thisPokemon
}
}
override func viewWillAppear(animated: Bool) {
FavoritesListView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Fetch the cached list, getNames returns an array of strings
let list = utility.getNames("Favorites")
pokemonList = list
}
The delegate and the dataSource are set via the storyboard.
The above code works, and shows the Favorites list just fine. The class for the complete Pokédex has a similar construction.
I have tried switching Favorites and Pokédex around, so that it shows the complete Pokémon list on startup. All 721 Pokémon are shown correctly, but then the Favorites are not visible.
What else I have tried:
Checking the Reuse Identifiers, over and over
Referencing outlets should be bound correctly
Calling TableView.reloadData() in the viewDidAppear method
Switching around the tab items
Does anyone have any clue what on earth is going on here?
Feel free to ask any more questions
Edit: this is what happens when I swap the two TabBar Buttons around, no code changes
Pokédex Screen
Favorites Screen
GitHub Project Here
Problem is in storyboard cell label frame. Set constraints of view controller for (Any,Any) Size Class. I can commit the code on github if you can give me write rights on your git. Thanks
Perhaps your table's delegate and dataSource are not set.
table.delegate = self
table.dataSource = self
Of course this is after you add the properties to your view controller
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
Your number of rows is always 0 for that controller,
I looked into your code pokemonList count is always 0 its not updating data in it
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pokemonList.count
}
The big issue is your PokemonDetailViewController is not a UITableViewController. It needs to inherent from UITableViewDataSource, UITableViewDelegate and then be connected to the storyboard view to provide data and formatting for a table.

How to add a segue programmatically?

I know, I know this has been asked a lot of times. I also found this question but the solution it suggested did not work for me.
I am just trying to build an app to demonstrate how to use those things in UIKit (in case I want to use them later on. I can just copy the code).
I have created a View Controller with a table view in it. I wrote a class called PrototypeTableController to act as the view controller class for the view controller I created in the storyboard.
When the user taps on one of the cells, I want another view controller to show, called Prototype Table Content. And different text will be shown if you tap on different cells.
In the storyboard, it's like this:
The text of the label in Prototype Table Content will be different when the user taps on a different cell. This means I need to send data from one view controller to another.
The post mentioned above suggested that I should give the segue an identifier, so I did:
Here is my code:
View controller class for the table view:
class PrototypeTableController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let data = ["Cell1", "Cell2", "Cell3", "Cell4", "Cell5"]
let contents = ["Hello", "Nice", "OMG", "Jesus", "Peace"]
var content: String?
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = data[indexPath.row]
return cell
}
func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return "This is a prototype table view created by Sweeper"
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "my table"
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
content = contents[indexPath.row]
tableView.deselectRowAtIndexPath(indexPath, animated: true)
performSegueWithIdentifier("showContent", sender: tableView)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showContent" {
let destination = segue.destinationViewController as! PrototypeTableContentViewController
destination.contentString = content
}
}
}
View controller class for Prototype Table Content view:
class PrototypeTableContentViewController: UIViewController {
#IBOutlet var tableContent: UILabel!
var contentString: String?
override func viewDidLoad() {
super.viewDidLoad()
tableContent.text = contentString
}
}
I think I did all the things suggested in the post mentioned above. I added an identifier, I called performSegueWithIdentifier
, I also deselected the cell after the tapping.
However, it just doesn't go to the other view controller! It stays on the same controller! Like this:
When the user taps on one of the cells, I want another view controller to show, called Prototype Table Content. And different text will be shown if you tap on different cells.
While you can programmatically call performSegueWithIdentifier, it's a lot of effort that the storyboard can automatically handle for you. Just use a show storyboard segue from your prototype cell to PrototypeTableContentViewController.
prepareForSegue knows which cell you selected because the cell is the sender. All you have to do is set the destination view controller's contentString.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let controller = (segue.destinationViewController as? PrototypeTableContentViewController where segue.identifier == "showContent", let cell = sender as? UITableViewCell, textLabel = cell.textLabel else {
return
}
controller.contentString = textLabel.text
}
This is very similar to how a template like Master-Detail segues from a cell to show details about a cell (although Apple uses indexPathForSelectedRow to pass the cell's details):
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
In either case, the SDK performs the storyboard segue for you; a segue didn't need to be programmatically added or performed.
Make sure your tableview delegate is set. If you are using storyboard, make sure delegate outlet in your storyboard is connected properly. If you are creating tableview by code, then you should do tableView.delegate=self; to set the delegate.
Your code is fine.
And one more thing:
You might need to change this line:
performSegueWithIdentifier("showContent", sender: tableView)
you need to make the sender as the row but not the tableview,so that the prepare for segue will get the sender as row instead of whole tableview.
As you are calling the prepareForSegue overtime you select a row, it makes sense to make the row as sender in performSegueWithIdentifier.
So it would be:
let row=indexPAth.row
performSegueWithIdentifier("showContent", sender: row)

how to make my searchBar TableView perform a segue

I have created a TableView application following the "Beginning iPhone Development with Swift " book.The search Bar tableView is created with code and not within the storyboard.The book explains how to get search results and display the corresponding cells but I would like my app to perform a segue to a ViewController I have created in the storyBoard.How can I trigger a Segue with code ?
for more info , this is my file :
import UIKit
class SearchResultsController: UITableViewController , UISearchResultsUpdating{
let sectionsTableIdentifier = "section identifier"
var products = [product]()
var filteredProducts = [product]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerClass(UITableViewCell.self,
forCellReuseIdentifier: sectionsTableIdentifier)
}
// MARK: - Table view data source
override func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return filteredProducts.count
}
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(
sectionsTableIdentifier) as UITableViewCell
cell.textLabel!.text = filteredProducts[indexPath.row].name
return cell }
// 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 == "detailView"{
let index = self.tableView?.indexPathForSelectedRow()
var destinationViewController : infoViewController = segue.destinationViewController as infoViewController
destinationViewController.Title = filteredProducts[index!.row].title
destinationViewController.eam = filteredProducts[index!.row].energy
destinationViewController.fam = filteredProducts[index!.row].fat
destinationViewController.pam = filteredProducts[index!.row].protein
destinationViewController.cam = filteredProducts[index!.row].carbohydrates
destinationViewController.imgName = filteredProducts[index!.row].imgName
}
}
func updateSearchResultsForSearchController(
searchController: UISearchController) {
let searchString = searchController.searchBar.text
filteredProducts.removeAll()
for prod in products{
var name = prod.name.lowercaseString
if name.rangeOfString(searchString) != nil {
filteredProducts.append(prod)
}
}
tableView.reloadData()
}}
Because the controller is built in code, you need to use the SearchResultsController's tableView delegate method didSelectRowAtIndexPath to trigger the presentation of the next view controller.
Assuming that there is a table view controller underpinning the SearchResultsController, you could potentially use that as the delegate of the SearchResultsController. The main table view controller might already have the necessary code to segue when a cell is selected, in which case you need to check which tableView has been selected in order to correctly determine which product the cell represents.
To set the delegate, add the following line to the code (in your comment above) where you create the SearchResultsController:
resultsController.tableView?.delegate = self
Then amend the didSelectRowAtIndexPath method to test which tableView is triggering the method:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if (tableView == self.tableView) {
// use the existing code to present the detail VC, based on the data in the main table view
...
} else {
// use new code to present the detail VC, based on data from the SearchResultsController
...
}
}
If the main table view controller is in a storyboard, you can use a segue to present the detail VC. In this case you would use self.performSegueWithIdentifier() in the above code. If not, you would either use self.navigationController?.pushViewController() (if you are embedded in a navigation controller) or self.presentViewController() (to present the detail VC modally).
Another option would be to set the SearchResultsController's delegate to be self (in viewDidLoad), and then to implement didSelectRowAtIndexPath in the SearchResultsController class. In this case, you don't need to test which tableView has triggered the method, but you will not be able to use a segue.

Resources