I am writing an iOS app in Swift that puts data from an API into a TableView. The goal is that the user can scroll down a continuous list of blog posts. What is happening is that data is filling the first screen's amount of cells fine, but when the user scrolls down, the app crashes as one of the cell properties, cellImageView becomes nil and is unwrapped (as force unwrap is specified in the cell class's code):
class BlogPostEntryCell: UITableViewCell {
//MARK: Properites
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var bodyTextView: UITextView!
#IBOutlet weak var initialsImageView: UIImageView!
}
Here is the code that causes the crash:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NewCell", for: indexPath) as! NewCell
apiCaller.getAPIDataThroughAsyncCall(id: nextCell, entry: { cellContent in
DispatchQueue.main.async(execute: {
cell.cellLabel.text = cellContent.labelText
cell.cellTextView.text = cellContent.textViewText
// App crashes at the below line
cell.initialsImageView = self.createPicture(initials: cellContent.pictureText)
})
})
nextCell += 1
return cell
}
The interesting thing is that if do this:
cell.cellLabel?.text = cellContent.labelText
cell.cellTextView?.text = cellContent.textViewText
// App does NOT crash at the below line
cell.initialsImageView? = self.createPicture(initials: cellContent.pictureText)
, the app doesn't crash but the data just repeats over and over again as the user scrolls.
I've tried the solution to a similar problem here: Why does data returned from coredata become nil after tableview scroll in swift?, but that did not work. However, I do believe this has to do with the asynchronous API calls.
I am thinking that possible solutions may include population an array of API data before generating the cells, but I would like some insight on the source of this problem before a rethink the architecture of the code.
You need to make an API call in your viewDidLoad to know how many element are there and record their ID in an array. Then in your cellForRow function, you can get the id in this way
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NewCell", for: indexPath) as! NewCell
apiCaller.getAPIDataThroughAsyncCall(id: array[indexPath.row], entry: { cellContent in
DispatchQueue.main.async(execute: {
...
})
})
return cell
}
The reason your nextCell wont work is that, this cellForRow function is called every time when your table view is trying to display a cell that is not currently on your screen. Say you know you have 10 element and your screen can only display 5 of them. When you scroll to bottom, your nextCell will become 10 and everything works fine as now. But now when you scroll up, the table view have to prepare the previous 5 again. So now your nextCell becomes 15, which you don't have corresponding element in your API call.
It is bad practice to call any API in cellForRowAt indexPath: IndexPath, it produces crashes as you see, have a look at MVVM. It is like wining a war without having to go to battle and I refer to this instance where you call API in cellForRowAt indexPath: IndexPath and the outlets are not initialised.
Related
again me with a short question since Swift is confusing me ATM - but I hope I will get used to it soon ;)
Ok so what I was wondering is: When I call a TableView and generate different Cells is there a way to Interrupt after a few and wait for User Input and react to it?
For Example: 2nd Cell is something like "Go to North or West" after that I want a User Input - with Buttons - in whatever direction he likes to go and react to it with following Cells (different Story Parts -> out of other Arrays?).
What is confusing me is that I just load the whole Application in viewDidLoad and i don't know how I can control the "Flow" within this.
I would really appreciate if someone could show me how I can achieve this maybe even with a small description about how I can control the Program Flow within the Method. I really think this knowledge and understanding would lift my understanding for Swift a few Levels higher.
Thanks in advance!
Here is my current Code which is not including any functionality for the named Question since I don't know how to manage this :)
import UIKit
class ViewController: UIViewController {
var storyLines = ["Test Cell 1 and so on first cell of many","second cell Go to North or West","third Cell - Buttons?"]
var actualTables = 1
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.dataSource = self
}
#IBOutlet weak var tableView: UITableView!
}
extension ViewController : UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return storyLines.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TxtLine", for: indexPath)
cell.textLabel?.text = storyLines[indexPath.row]
cell.textLabel?.numberOfLines = 0;
cell.textLabel?.lineBreakMode = .byWordWrapping
return cell
}
}
Cocoa is event driven. You are always just waiting for user input. It's a matter of implementing the methods that Cocoa has configured to tell you about it.
So here, for example, if you want to hear about when the user presses a button, you configure the button with an action-and-target to call a method in your view controller when the user presses it. That way, your method can see which button the user pressed and remake the table view's data model (your storyLines array) and reload the table with the new data.
first of all i'm saying straight forward I know this is "duplicate" and and this is my 2nd time asking the same question - the problem is that my first one has been closed without me understanding the problem so please if someone wants to close this question again first let me understand what am I doing wrong. The solution I got last time was not relevant so if I could get addressed specifically that would be great!
I am trying to create a custom cell on tableview from an array. when I append any filed on my custom cell I get unexpected nil on all of the fileds and I have no idea why
this is my custom cell:
class CustomMovieCell: UITableViewCell {
#IBOutlet weak var title: UILabel!
#IBOutlet weak var rating: UILabel!
#IBOutlet weak var releaseYear: UILabel!
#IBOutlet weak var genre: UILabel!
var imageBackground: String!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
and this is my UITableView cellForRowAtIndexPath method:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as! CustomMovieCell
let movieFetched = Movie(title: moviesArray[indexPath.row].title, image: moviesArray[indexPath.row].image, rating: moviesArray[indexPath.row].rating, releaseYear: moviesArray[indexPath.row].releaseYear, genre: moviesArray[indexPath.row].genre)
print(movieFetched)
cell.title.text? = movieFetched.title
cell.rating.text? = String(movieFetched.rating)
cell.releaseYear.text? = String(movieFetched.releaseYear)
cell.genre.text? = String(movieFetched.genre[0])
return cell
}
what am I missing? when appending ANY of the files I get unexpectedly found nil while unwrapping optional value - I did not know UIlabel as IBOutlet are optional? even-though they are not optional in my custom cell class.
when debugging I can see that all values of the cell - title, image, rating, releaseYear and genre are nil when trying to assign them a value - so I really have no idea what to do at this point. I have deleted and re-created the cell from scratch and it did not make any differents.
As I already stated - I know this is "duplicate". please though - do not close it before you help me because I did not get any answer the last time, I got directed to a wall-of-text page that did not help me understand my issue. The other "duplicate" pages are like a general "what are optional values" kind of question and do not help me with this specific issue.
edit:
I have uploaded this project to github if it helps anyone help me to figure out this issue
https://github.com/alonsd/MoviesApi
You've connected the custom cell class with 2 cells. One is in xib and another one is in this UIViewController. This UIViewController's prototype cell doesn't have these labels. So it will be nil and it will crash
Delete the prototype cell from MoviesViewController in storyboard. And add this in MoviesViewController viewDidLoad
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "MovieCell")
Change tableView cellForRowAt method as follows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell") as! TableViewCell
cell.title.text = moviesArray[indexPath.row].title
cell.rating.text = String(moviesArray[indexPath.row].rating)
cell.releaseYear.text = String(moviesArray[indexPath.row].releaseYear)
cell.genre.text = String(moviesArray[indexPath.row].genre[0])
return cell
}
Problem I want to allow users to hit 'swap' in a table cell and then find a different Realm object to populate the 2 text labels (for exercise name and number of reps) in the cell with the values from the new object.
Research There's quite a bit (admittedly old) on 'moving rows' (e.g. here How to swap two custom cells with one another in tableview?) and also here (UITableView swap cells) and then there's obviously a lot on reloading data in itself but I can't find anything on this use case.
What have I tried my code below works fine for retrieving a new object. i.e. there's some data in the cell, then when you hit the 'swapButton' it goes grabs another one ready to put in the tableView. I know how to reload data generally but not within one particular cell in situ (the cell that the particular swap button belongs to... each cell has a 'swap button').
I'm guessing I need to somehow find the indexRow of the 'swapButton' and then access the cell properties of that particular cell but not sure where to start (I've played around with quite a few different variants but I'm just guessing so it's not working!)
class WorkoutCell : UITableViewCell {
#IBOutlet weak var exerciseName: UILabel!
#IBOutlet weak var repsNumber: UILabel!
#IBAction func swapButtonPressed(_ sender: Any) {
swapExercise()
}
func swapExercise() {
let realmExercisePool = realm.objects(ExerciseGeneratorObject.self)
func generateExercise() -> WorkoutExercise {
let index = Int(arc4random_uniform(UInt32(realmExercisePool.count)))
return realmExercisePool[index].generateExercise()
}
}
//do something here like cell.workoutName
//= swapExercise[indexRow].generateExercise().name???
}
Hold your objects somewhere in a VC that shows UITableView. Then add the VC as the target to swap button. Implement swapping objects on button press and reload data of table view after.
The whole idea is to move logic to view controller, not in separate cell.
There are 2 ways.
1. Adding VS as button action target.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ... // get cell and configure it
cell.swapBtn.addTarget(self, action: #selector(swapTapped(_:)), for: .touchUpInside)
return cell
}
func swapTapped(_ button: UIButton) {
let buttonPosition = button.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!
// find object at that index path
// swap it with another
self.tableView.reloadData()
}
Make VC to be delegate of cell. More code. Here you create protocol in cell and add delegate variable. Then when you create cell you assign to VC as delegate for cell:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ... // get cell and configure it
cell.delegate = self
return cell
}
func swapTappedForCell(_ cell: SwapCell) {
// the same logic for swapping
}
Solution from OP I adapted the code here How to access the content of a custom cell in swift using button tag?
Using delegates and protocols is the most sustainable way to achieve this I think.
I hope this helps others with the same problem!
I have a button in a cell as a toggle to check in members in a club. When I check in a member, I need the button's state to stay ON after scrolling, but it turns back off. Here is the cellForRow method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.membersTableVw.dequeueReusableCell(withIdentifier: "CellMembersForCoach", for: indexPath) as! CellMembersForCoach
let member = members[indexPath.row]
cell.setMember(member)
cell.cellController = self
return cell
}
Here is the portion in the custom cell class where I toggle the button
#IBOutlet weak var checkBtn: UIButton!
#IBAction func setAttendance(_ sender: Any){
// toggle state
checkBtn.isSelected = !checkBtn.isSelected
}
The toggling works but after scrolling the table, the button state changes back to original. Any suggestion is appreciated.
This happens because you are reusing the cells.
You need to keep track of which cells have been selected. Perhaps in your member's class. Then when you are in your cellForRowAt you should check if this cell has been selected before and set the correct state for your button.
This is because of tableview is reusing your cell. so you have to maintain button as per tableView data source.
Shamas highlighted a correct way to do it, so I'll share my whole solution.
I created a singleton class to store an array of checked cells:
class Utility {
// Singleton
private static let _instance = Utility()
static var Instance: Utility{
return _instance
}
var checkedCells = [Int]()
In the custom cell class I have action method wired to the check button to add and remove checked cells:
#IBOutlet weak var checkBtn: UIButton!
#IBAction func setAttendance(_ sender: Any){
// Get cell index
let indexPath :NSIndexPath = (self.superview! as! UITableView).indexPath(for: self)! as NSIndexPath
if !checkBtn.isSelected{
Utility.Instance.checkedCells.append(indexPath.row)
}else{
// remove unchecked cell from list
if let index = Utility.Instance.checkedCells.index(of: indexPath.row){
Utility.Instance.checkedCells.remove(at: index)
}
}
// toggle state
checkBtn.isSelected = !checkBtn.isSelected
}
In the cellForRowAt method in the view controller I check if the cell row is in the array and decide if the toggle button should be checked:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.membersTableVw.dequeueReusableCell(withIdentifier: "CellMembersForCoach", for: indexPath) as! CellMembersForCoach
if Utility.Instance.checkedCells.contains(indexPath.row){
cell.checkBtn.isSelected = true
}
return cell
}
The problem is here:
checkBtn.isSelected = !checkBtn.isSelected
This code will reflect the button selection state every time the cell is configured when the delegate cellForRowAt invokes. So if you selected it before, now it turns to not-selected.
Since the tableView is reusing cells you code is not going to work.
You have to keep track of each button when selected and set it again when tableview is reusing cells when you scroll.
Solution : You can take an array(contains bool) which is size of your tableview data.
So you have to set state of button using array and update array when selected or deselected.
I'm new to Swift and iOS and have a situation where I would like to take an array of URL strings and populate a ImageView within a UITableViewCell, which I have made an appropriate class for, called MyTableViewCell.
It's crashing and I am getting the error:
"fatal error: unexpectedly found nil while unwrapping an Optional value"
Which I can see is happening in the viewDidLoad() when the line imageTableView.datasource = self runs.
I am setting the data and delegate in the view as so:
override func viewDidLoad() {
super.viewDidLoad()
imagesTableView.dataSource = self
imagesTableView.delegate = self
}
Below, imageUrls is an array of Strings.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: MyTableViewCell = tableView.dequeueReusableCell(withIdentifier: "myTableViewCell", for: indexPath) as! MyTableViewCell
cell.imageView.sd_setImage(with: URL(string: imageUrls[indexPath.row]))
return cell
}
I have an outlet to the cell like so in the class:
#IBOutlet var imagesTableView: UITableView!
My question is, what is the proper way to do this with swift 3 and SDWebImage, and why/where is the nil value from the imagesTableView occurring?
It may be worth noting that I have also tried hard-coding some string URL values into the imageUrls to ensure that that was not the problem (I get the same error with hardcoded values).
It turns out that although I had an outlet connected, it somehow lost the reference - I deleted the whole tableview and subviews and recreated and connected them, and the error was gone. Something funky must have happened when I drag and dropped for the outlet previously.