After a few hours of trying to figure out why my customs cells aren't showing, I have hit a bit of a bump. Using the MVC pattern:
Model.swift
struct studentProperties {
var _title : String!
var _forename : String!
var _surname : String!
}
class MainModel {
// Singleton instances
static let modelInstance = MainModel()
static var structInstance = studentProperties()
// Array of structs
var studentArray: [studentProperties] = []
// MARK: - Initialization
private init() {}
func studentInput(title: String, forename: String, surname: String) {
MainModel.structInstance._title = title
MainModel.structInstance._forename = forename
MainModel.structInstance._surname = surname
}
}
CreateStudentController.swift
I won't show the whole file, but this code is inside of a save button - user inputs data, and that data gets append to the array in model
#IBAction private func saveNewStudentButton(_ sender: UIBarButtonItem) {
// Put student data into the struct properties in model
MainModel.modelInstance.studentInput(title: studentTitle.text!, forename: studentForename.text!, surname: studentSurname.text!)
// Append the user input
MainModel.modelInstance.studentArray.append(MainModel.structInstance)
dismiss(animated: true, completion: nil)
performSegue(withIdentifier: "returnToStudentsList", sender: sender)
}
MainStudentController.swift
class MainStudentController: UIViewController, UITableViewDelegate, UITableViewDataSource {
//MARK: - #IBOutlets
#IBOutlet weak var myTableView: UITableView!
//MARK: - #IBActions
#IBAction func addStudentButton(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
performSegue(withIdentifier: "createStudentSegue", sender: sender)
}
override func viewDidLoad() {
super.viewDidLoad()
myTableView.delegate = self
myTableView.dataSource = self
myTableView.reloadData()
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(MainModel.modelInstance.studentArray.count)
return MainModel.modelInstance.studentArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: MainCell = self.myTableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MainCell
cell.studentTitle?.text = MainModel.modelInstance.studentArray[0]._title
cell.studentForename?.text = MainModel.modelInstance.studentArray[0]._forename
cell.studentSurname?.text = MainModel.modelInstance.studentArray[0]._surname
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
myTableView.deselectRow(at: indexPath, animated: true)
}
}
There is another file - which is of UITableViewCell, but it literally only has the outlets for my custom cell.
Issue
The issue I am currently experiencing is (I think) around this method?:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(MainModel.modelInstance.studentArray.count)
return MainModel.modelInstance.studentArray.count
}
User inputs data -> it gets append to that array -> thus now I am using those elements from the array to represent the amount of rows there will be.
Sadly, it currently isn't showing any rows? I could add multiple elements and it still wouldn't show any rows. Yet, when I print that same line out, it shows that there are elements inside(dependant on how many times I add new data). I'm a little confused as to why this isn't adding rows?
I'm not sure if it's something I am missing or isn't correct in code. I would appreciate if anyone could see where I have gone wrong?
Hopefully that explains my situation.
Thanks
According to this code, your problem is just that the table view itself is not getting reloaded. Your only reload call is in viewDidLoad, so it only gets loaded when your view loads the first time (that method only gets called once). You can fix this easily by moving myTableView.reloadData() from viewDidLoad into viewWillAppear or viewDidAppear. That should be all you need to do.
Something like this:
override func viewWillAppear() {
super.viewWillAppear()
myTableView.reloadData()
}
Related
My application fetches data from a mock API.
Using a custom cell, I display the names of authors on my landing page viewController.
When I click on a cell, it takes that author's book information to display on a 2nd TableViewController.
But even though the implementation is the same as for the landing page. My app freezes until I get a EXC_BAD_ACCESS error
It seems like it's stuck in an infinite loop, but without a proper error, it's hard to know why.
Infinite Loop?
I can get this to work without using a custom cell, but then I cannot display all the information I want (only book title or release date), so the data is there.
import UIKit
class BooksTableViewCell: UITableViewCell {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var pages: UILabel!
#IBOutlet weak var release: UILabel!
// #IBOutlet var coverImage: UIImageView!
static let cellIdentifier = "BooksTableViewCell"
//
override func awakeFromNib() {
super.awakeFromNib()
}
static func nib() -> UINib {
return UINib(nibName: "BooksTableViewCell", bundle: nil)
}
//MARK: configure
public func configure(with viewModel: BooksCellViewModel) {
name.text = viewModel.name
pages.text = String(viewModel.pages)
release.text = viewModel.release
// coverImage.image = viewModel.image
}
}
import UIKit
class BooksTableViewController: UITableViewController {
var books: [Book] = []
var authorName: String = ""
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(BooksTableViewCell.nib(), forCellReuseIdentifier: BooksTableViewCell.cellIdentifier)
tableView.delegate = self
tableView.dataSource = self
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return authorName
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return books.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("Hello1")
let cell = tableView.dequeueReusableCell(withIdentifier: BooksTableViewCell.cellIdentifier, for: indexPath) as! BooksTableViewCell
print("Hello2")
let model = books[indexPath.row]
cell.configure(with: BooksCellViewModel(name: model.title, pages: model.pages, release: model.releaseDate))
return cell
}
}
The landing page controller and cell is similar but works with no problems
import UIKit
class LandingTableViewController: UITableViewController {
let parser = DataAPI()
var authors = [Author]()
var books = [Book]()
var authorName = ""
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(AuthorTableViewCell.nib(), forCellReuseIdentifier: AuthorTableViewCell.cellIdentifier)
tableView.delegate = self
tableView.dataSource = self
parser.getData {
data in
self.authors = data
//Reload UI on Main thread:
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "List of Authors"
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return authors.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: AuthorTableViewCell.cellIdentifier, for: indexPath) as! AuthorTableViewCell
let model = authors[indexPath.row]
cell.configure(with: AuthorCellViewModel(name: model.authorName))
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
books = authors[indexPath.row].books
authorName = authors[indexPath.row].authorName
performSegue(withIdentifier: "Show Books", sender: nil)
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
if (segue.identifier == "Show Books") {
let showBooksViewController: BooksTableViewController = segue.destination as! BooksTableViewController
showBooksViewController.books = books
showBooksViewController.authorName = authorName
}
}
}
I was able to fix the issue by correctly naming my variables. I needed to be using releaseDate not release as per my model object.
I know how to preserve the action we have done on UITableView, after scrolling back and forth.
Now Iam doing a simple UITableView on MVVM
which has a Follow button . like this.
Follow button changes to Unfollow after click and resets after scrolling.
Where and How to add the code to prevent this?
Here is the tableview Code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Vm.personFollowingTableViewViewModel.count
}
var selectedIndexArray:[Int] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowList_MVVM.PersonFollowingTableViewCell.identifier , for: indexPath) as? PersonFollowingTableViewCell else{
return UITableViewCell()
}
cell.configure(with: Vm.personFollowingTableViewViewModel[indexPath.row])
cell.delegate = self
return cell
}
and configure(with: ) function
#objc public func didTapButton(){
let defaultPerson = Person(name: "default", username: "default", currentFollowing: true, image: nil)
let currentFollowing = !(person?.currentFollowing ?? false)
person?.currentFollowing = currentFollowing
delegate?.PersonFollowingTableViewCell(self, didTapWith: person ?? defaultPerson )
configure(with: person ?? defaultPerson)
}
func configure(with person1 : Person){
self.person = person1
nameLabel.text = person1.name
usernameLabel.text = person1.username
userImageview.image = person1.image
if person1.currentFollowing{
//Code to change button UI
}
custom delegate of type Person is used
I guess your main issue is with Button title getting changed on scroll, so i am posting a solution for that.
Note-: Below code doesn’t follow MVVM.
Controller-:
import UIKit
class TestController: UIViewController {
#IBOutlet weak var testTableView: UITableView!
var model:[Model] = []
override func viewDidLoad() {
for i in 0..<70{
let modelObject = Model(name: "A\(i)", "Follow")
model.append(modelObject)
}
}
}
extension TestController:UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TestTableCell
cell.dataModel = model[indexPath.row]
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
extension TestController:Actions{
func followButton(cell: UITableViewCell) {
let indexPath = testTableView.indexPath(for: cell)
model[indexPath!.row].buttonTitle = "Unfollow"
testTableView.reloadRows(at: [indexPath!], with: .automatic)
}
}
class Model{
var name: String?
var buttonTitle: String
init(name: String?,_ buttonTitle:String) {
self.name = name
self.buttonTitle = buttonTitle
}
}
Cell-:
import UIKit
protocol Actions:AnyObject{
func followButton(cell:UITableViewCell)
}
class TestTableCell: UITableViewCell {
#IBOutlet weak var followButtonLabel: UIButton!
#IBOutlet weak var eventLabel: UILabel!
var dataModel:Model?{
didSet{
guard let model = dataModel else{
return
}
followButtonLabel.setTitle(model.buttonTitle, for: .normal)
eventLabel.text = model.name
}
}
weak var delegate:Actions?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func followAction(_ sender: Any) {
delegate?.followButton(cell:self)
}
}
To convert this into MVVM approach, there are few things you need to change and move out.
The loop I have in viewDidLoad shouldn’t be there. That will be some API call, and should be handled by viewModel, and viewModel can delegate that to other repository to handle or handle itself. Upon receiving response viewModel update its state and communicate with View (in our case tableView) to re-render itself.
Code in extension where I am updating model object shouldn’t be in controller (model[indexPath!.row].buttonTitle = "Unfollow"), that has to be done by viewModel, and once the viewModel state changes it should communicate with view to re-render.
The interaction responder (Button action) in Cell class, should delegate action to viewModel and not controller.
Model class should be in its own separate file.
In short viewModel handles the State of your View and it should be the one watching your model for updates, and upon change it should ask View to re-render.
There are more things you could do to follow strict MVVM approach and make your code more loosely coupled and testable. Above points might not be 100% correct I have just shared some basic ideas i have. You can check article online for further follow up.
The above answer works . But I have gone through what suggested by #Joakim Danielson to find what exactly happens when you are updating the View and Why it is not updating on ViewModel
So I made an update to delegate function
ViewController delegate function
func PersonFollowingTableViewCell1( _ cell: PersonFollowingTableViewCell, array : Person, tag : Int)
Here, I called the array in the Viewmodel and assigned the values of array in func argument to it.
like ViewModel().Vmarray[tag].currentFollow = array[tag].currentFollow
How can I access the value of the cell of the table view that the user selects? I know how to access the value if the user hasn't searched because I can just access the IndexPath's value of my array, but I can't when the user has searched something because the townArray won't line up with the cells that are shown.
To give you a better understanding- just say I have an array of fruits which has [apples, bananas, oranges]. If the user searches for bananas, then the only cell with text showing (results) will say bananas. If I then try to access the IndexPath element of fruits, I will get apples since it is the first element of fruits and bananas is the first element showing. What I want is to get the access the value bananas when the user selects bananas and searches "bananas" instead of apples. I know this may be confusing but please let me know if you have any thoughts on how I can solve this issue.
var searchedResult: [Fruit] = yourSearchingFunction()
tableview.reloadData()
IndexPaths would refresh correctly after TableView reloaded with a new collection of fruits
Explanation is showed with the following code:
class ViewController: UIViewController {
#IBOutlet private weak var tableView: UITableView!
private var originalFruits: [Fruit] = [] // This data is used to cache
private var fruits: [Fruit] = [] // This data is used to display on TableView
// MARK: - View Cycle
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupData()
}
private func setupData() {
// Loading all fruits at the first time [Apple, Banana, Orange ]
originalFruits = yourSetupDataFunctionToFetchAllFruitsAtTheFirstTime()
fruits = originalFruits
tableView.reloadData()
}
#IBAction private func actionTapToSearchButton(_ sender: Any) {
let searchingKey = searchTextField.text
if searchingKey.isEmpty {
fruits = originalFruits
} else {
fruits = yourSeacrchingFruitFunction(searchingKey)
}
tableView.reloadData()
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: FruitCell.identifier) as? FruitCell {
return cell
}
return UITableViewCell()
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
}
I have a text field that when I type a word in, and then press a button is supposed to add the word the the tableview. I know the array is being updated because after the button is pressed, the array, with its new value print fine in the console. I've tried reloadData() in several places but it's not doing anything. Here is my code:
import UIKit
class Arraytest: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var textField: UITextField?
var names = ["Jack", "Andy", "Guy", "Bruce", "Nick", "James", "Dominick"]
#IBAction func addButton(_ sender: Any) {
let name : String = textField!.text!
names.append(name)
textField?.text! = ""
for guy in names{
**print(guy)**
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.textLabel?.text = names[indexPath.row]
***tableView.reloadData()***
return cell
}
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Let's say I type John in the text field, here is what the console prints:
Jack
Andy
Guy
Bruce
Nick
James
Dominick
John
I know the array works fine, but not why the tableView won't reload when everyone claims reloadData() works(I'm sure it does, and I'm just making an easy mistake!)..Any ideas?
EDIT: Ok it turns out that you do have to drag the IBOutlet from the tableview. The reason I didn't do this earlier was because I watched a video and his worked without making the connection.
You should learn more about table view & how it works. Please see Apple Documentation Creating and Configuring a Table View
Here the method cellForRowAt is a data source of table view and it's get fired from tableview automatically when it's need to populate cell data. You couldn't manually call this method. Implementing this method inside #IBAction func addButton() does nothing. Function always needs to call from another method. So you should remove cellForRowAt inside #IBAction func addButton().
Solution: Get an table view outlet from storyboard. If need help see here Connect the UI to Code
#IBOutlet weak var tableView: UITableView?
Then set tableview datasource and delegate in viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
tableView?.dataSource = self;
tableView?.delegate = self;
}
And finally update #IBAction func addButton() as below
#IBAction func addButton(_ sender: Any) {
let name : String = textField!.text!
names.append(name)
textField?.text! = ""
for guy in names{
print(guy)
}
tableView?.reloadData()
}
Full source may look like this:
import UIKit
class Arraytest: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var textField: UITextField?
#IBOutlet weak var tableView: UITableView?
var names = ["Jack", "Andy", "Guy", "Bruce", "Nick", "James", "Dominick"]
override func viewDidLoad() {
super.viewDidLoad()
tableView?.dataSource = self;
tableView?.delegate = self;
}
#IBAction func addButton(_ sender: Any) {
let name : String = textField!.text!
names.append(name)
textField?.text! = ""
for guy in names{
**print(guy)**
}
tableView?.reloadData()
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Your code has two very big mistakes (assuming that your tableview delegates are connected to your viewcontroller correctly).
First your a missing an IBOulet reference to your tableview. Create one and you can call the reloadData() method in that outlet.
Second, you should not call the public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell delegate method inside your IBAction. It is a delegate method, it is already listening for an event or command that will fire its logic, in this case reloadData().
So your IBaction should only take the value from the textfield and add it to the array, to finally call reloadData(), which will call public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
this code works... hope it helps:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableview: UITableView!
#IBOutlet weak var textField: UITextField?
var names = ["Jack", "Andy", "Guy", "Bruce", "Nick", "James", "Dominick"]
// MARK: tableview delegate methods
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")
cell.textLabel?.text = names[indexPath.row]
return cell
}
// MARK: ibactions
#IBAction func addButton(_ sender: Any) {
let name : String = textField!.text!
names.append(name)
textField?.text! = ""
tableview.reloadData()
}
}
You have to remove TableView delegate method from
#IBAction func addButton(_ sender: Any) {
}
method & put it outside of it. Create a property of this tableView form storyboard like that
#IBOutlet weak var tableView: UITableView!
In viewDidload method set datasource & delegate of it like that
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.datasource = self
self.tableView.delegate = self
}
Hope it helps.
You can achieve this by adding a property observer to the array of names. The didset method will run if there is any changes to the value.
class Arraytest: UIViewController, UITableViewDelegate UITableViewDatasource {
var names = ["Jack", "Andy", "Guy", "Bruce", "Nick", "James", "Dominick"] {
didSet {
self.tableView.reloadData();
}
}
}
The tableview func inside the addButton func doesnt run.. remove it and keep it like this:
#IBAction func addButton(_ sender: Any) {
let name : String = textField!.text!
names.append(name)
textField?.text! = ""
for guy in names{
**print(guy)**
}
}
You have to take your tableview func outside the #IBAction func addButton, as it's part of the uitableviewdatasource and is required to display your cell. After that, all you need is to put tableView.reloadData() after the for guy in names{ ... } loop.
Drag a connection from a split view controller to your view controller and use reloadData().
I have a method which grabs JSON and appends individual values from the JSON into the array like so:
internal let methods = Methods()
var countryData = [""]
#IBOutlet weak var countryTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
displayCountries()
}
func displayCountries() {
// Wait for task to complete before grabbing JSON
methods.getCountriesData {() -> () in
for (_, value) in self.methods.getJSON() {
self.countryData.append(String(describing: value))
}
DispatchQueue.main.async {
self.countryTableView.reloadData()
print(self.countryData)
}
}
}
I have UITableView delegates declared like so also:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return countryData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")! as UITableViewCell
let row = indexPath.row
cell.textLabel?.text = self.countryData[row]
return cell
}
The print(self.countryData) prints all of the countries within the log (100's of countries) however for some reason the countries aren't being displayed in the UITableView, does anyone understand why?
You need to specify the table view's data source. This can be done in code or in the storyboard.
In code:
countryTableView.dataSource = self
You will also need to have your data source object conform to the UITableViewDataSource protocol:
MyViewController: UIViewController, UITableViewDataSource {
Since the data is an array of strings and a property on the view controller, this should work fine.
In the storyboard:
Simply drag the dataSource outlet to the object which is providing the data (in this example it is the view controller):