Show images in cells from array - ios

I have TableViewController and AudioPlayerViewController. In TableViewController I want to know how many tracks have been listened to. And show the image in the listened cells TableViewController. To detect that track have been listened I use this code in AudioPlayerViewController:
func audioPlayerDidFinishPlaying(_ audioPlayer: AVAudioPlayer, successfully flag: Bool) {
UserDefaults.standard.set( arrayOfListened(trackIndex), forKey: "key")
}
And to show image in listened cells in TableViewController next:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(format: "cell", indexPath.row), for: indexPath) as! MasterViewCell
if indexPath.row == arrayOfListened[indexPath.row] {
cell.cellStatusImage.image = UIImage(named: "statusDone.png")
}
}
But my app crashed. How to fix it?

The problem is that you are trying to get an element from the array by indexPath.row which probably doesn't exist. Try to use something like this:
...
guard
arrayOfListened.indices.contains(indexPath.row),
indexPath.row == arrayOfListened[indexPath.row]
else {
return cell
}
... your cell configuration
Or, if your arrayOfListened just contains song IDs, you can simply use:
if arrayOfListened.contains(indexPath.row) {
... your cell configuration
}

Related

Present ViewController by Clicking CollectionViewCell inside TableView(with several sections)

I'm making a music app recently, and I'd like to know how to pass data from CollectionView to TableView which has several sections. Here is the home page of my project, and what I want to do is when user tap the image, it will precent another ViewController with the information about the picture. I already know how to present a ViewController by clicking CollectionViewCell inside TableView by delegate, but only if there's only one section.
The thing is that I have 5 sections in this page, and I also have 5 different models for encoding the JSON from API. So how I show the pictures is to send the image urls(from 5 models) to TableViewCell in each section like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.identifier, for: indexPath) as? HomeTableViewCell else { return UITableViewCell() }
cell.delegate = self
switch homeSections[indexPath.section] {
case .newReleases:
if let newReleases = self.newReleases?.albums.items.map({$0.images[0].url}) {
cell.getPictures = newReleases
}
return cell
case .followSingers:
if let currentlyFollowing = self.currentlyFollowing?.artists.items.map({$0.images[0].url}) {
cell.getPictures = currentlyFollowing
cell.isCircle = true
}
return cell
case .catrgories:
if let categories = self.musicCategory?.playlists.items.map({$0.images[0].url}) {
cell.getPictures = categories
}
return cell
case .artists:
if let playlist = self.relatedArtists?.artists.map({$0.images[0].url}){
cell.getPictures = playlist
}
return cell
case .recentlyPlayed:
if let recentlyPlayed = self.recentlyPlayed?.items?.map({$0.track.album.images[0].url}) {
cell.getPictures = recentlyPlayed
}
return cell
}
}
However, when I want to pass the information which the user tap, there's nothing I can pass but indexPath. I've tried to declare the 5 different model types in TableViewCell, but I still don't know which section did the user tap. Does anyone can help? Thanks a lot!
Update:
To make the question more clearly, please refer to the information below.
In this page, I have a TableView and embed a CollectionView in the TableViewCell, and there's only one ImageView in the CollectionViewCell.
The "New Releases", "Currently Following" and "Categories" are the header of TableView. The "New Releases" is the first section, and the "Currently Following" is the second section, and so on.
Here is how I set cellForItem in CollectionView Delegate. It basically converts String to URL, and display the picture on the screen.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeCollectionViewCell.identifier, for: indexPath) as? HomeCollectionViewCell else { return UICollectionViewCell() }
guard let url = URL(string: getPictures[indexPath.row]) else { return UICollectionViewCell() }
cell.myImageView.getImages(url: url)
if isCircle == true {
cell.myImageView.layer.cornerRadius = cell.myImageView.frame.width/2
}
return cell
}
When the user taps the image, it will only trigger the didSelectItemAt in CollectionView. And I can only pass indexPath.row so far.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.sendIndexPath(index: indexPath.row)
}

UITableViewCell delegate indexPath

Good time of day, I'm new in iOS development. I'm developing project where i have tableViewCell and button,progressBar on it. When i tap button indexPath of this cell is passed through delegate to viewController and then with another method i'm
downloading some data and show it's progress in progressBar. So when i tap to one cell and then to another, progress in first cell stops and continues in second, could anyone help? Thanks in advance )
Here is delegate methods in viewController:
func didTouchButtonAt(_ indexPath: IndexPath) {
songs[indexPath.row].isTapped = true
let selectedSong = self.songs[indexPath.row] as Song
DownloadManager.shared.delegate = self
self.indexQueue.append(indexPath)
self.selectedIndexPath = indexPath
DownloadManager.shared.download(url: selectedSong.url , title: selectedSong.title)
}
func downloadProgress(_ progress: Progress) {
if (progress.completedUnitCount) < progress.totalUnitCount {
selectedIndexPath = indexQueue.first
}
else if(!indexQueue.isEmpty){
indexQueue.removeFirst()
}
print(progress.fractionCompleted)
print(progress.completedUnitCount, progress.totalUnitCount)
print(indexQueue.count)
var cell: ViewControllerTableViewCell?
cell = self.tableView.cellForRow(at: self.selectedIndexPath!) as? ViewControllerTableViewCell
if cell != nil {
cell?.progress = Float(progress.fractionCompleted)
}
}
this is cell:
#IBAction func downloadButtonTouched(sender: Any){
self.delegate?.didTouchButtonAt(self.indexPath!)
self.progressBar.isHidden = false
}
As #RakshithNandish mentioned, I'v used list of indexPathes and when i tap to button, list adds indexPath. So, before passing progress to cell i check if progress is completed: if not, pass progress to first element of queue, otherwise just delete first element from queue, works fine.
You can create a model that may be an array which will hold the indexpath of tapped button's cell i.e. append indexpath to the array whenever button is tapped and remove it whenever you want. Later on while returning cells in cellForRowAtIndexPath you check if the array contains indexPath for which you are returning cell.
class DemoCell: UITableViewCell {
#IBOutlet button: UIButton!
}
class DemoTableViewController: UITableViewController {
var buttonTappedIndexPaths: [IndexPath] = []
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: DemoCell.className, for: indexPath) as! DemoCell
if buttonTappedIndexPaths.contains(indexPath) {
//show progress view spinning or whatever you want
} else {
//don't show progress view
}
}
}

Tableview button.tag throw lldb

i don't know what happen, i set the button.tag with the table row and when it reach row > 1, it will throw lldb. it works if the button.tag <= 1
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cells")! as UITableViewCell
let alertBtn = cell.viewWithTag(1) as! UIButton;
alertBtn.tag = indexPath.row
alertBtn.addTarget(self, action: Selector(("showAlert:")), for: UIControlEvents.touchUpInside)
return cell
}
Application crash on this line, because it fails to find a view with tag 1, the tag is updating in every cell with row value.
let alertBtn = cell.viewWithTag(1) as! UIButton
remove this line and Take #IBOutlet for alertBtn From UITableViewCell instead of refreshing with tag.
Swift 3X...
You are replacing your tag so first tag items are getting nil so replace this code ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cells")! as UITableViewCell
let alertBtn = cell.viewWithTag(1) as! UIButton
alertBtn.addTarget(self, action: #selcetor(showAlert(sender:))), for: .touchUpInside)
return cell
}
func showAlert(sender:UIButton) {
let point = sender.convert(CGPoint.zero, to: self.tableview)
let indexpath = self.tableview.indexPathForRow(at: point)
}
Try to do custom UITableViewCell.
Declare protocol and delegate for Your new class class. Wire up a action and call delegate
protocol MyCellDelegate: class {
func buttonPressed(for cell: MyCell)
}
class MyCell:UITableViewCell {
weak var delegate: MyCellDelegate?
#IBAction func buttonPressed(sender: Any){
self.delegate?.buttonPressed(for: self)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
.......
cell.delegate = self
........
}
Remember to add new protocol implementation to Your VC. You can add prepareForReuse method and reset delegate to nil when cell is reused.
If you want to get indexPath of cell containing tapped button you can use function similar to this matching your requirement.
func showAlert(sender: AnyObject) {
if let cell = sender.superview?.superview as? UITableViewCell{ // do check your viewchierarchy in your case
let indexPath = itemTable.indexPath(for: cell)
}
print(indexPath)// you can use this indexpath to get index of tapped button
}
Remove this line from cellForRowAtIndexPath alertBtn.tag = indexPath.row
If you can use Custom Cell for this purpose you can get indexpath of selected button as you were getting previously.
Create CustomCell and create IBOutlet for your button and labels etc. You can access subviews of your cell in cellForRowAtIndexPath and assign tag to your button. If you have any queries regarding CustomCell do let me know.

Iterating through TableViewCells skips some

I have a tableview and in each cell there is a checkbox. I also have a "select all" button.
My problem is that when I click select all I want to update all the checkboxes to checked state. So from a list of 100 cells, all get checked but every 13th cell does not. To make it clearer, on my simulators screen are 12 cells visible that all get checked. When I start scrolling, the first cell that comes up is unchecked, and is then followed by 12 checked ones :S
When I scroll a little and click "select all" again, the skipped ones become also checked..
Anyone have a clue what am I missing?
This is the cell code:
class ListTableViewCell: UITableViewCell {
#IBOutlet weak var checkbox: UIButton!
var buttonState = false{
didSet{
if buttonState{
checkbox.setImage(#imageLiteral(resourceName: "checked"), for: .normal)
}else{
checkbox.setImage(#imageLiteral(resourceName: "unchecked"), for: .normal)
}
}
}
#IBAction func checkboxAction(_ sender: UIButton) {
if buttonState {
buttonState = false
}else{
buttonState = true
}
}
func simulateCheck(){
buttonState = true
}
And here are some snipets from my controller:
private var articleValues: [ArticleValue] = []{
didSet{
tableView.reloadData()
}
}
func selectAll(){
for i in 0..<articleValues.count{
let cell = tableView.cellForRow(at: IndexPath(item: i, section: 0)) as? ListTableViewCell
cell?.simulateCheck()
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
}
return cell
}
Your UITableView is backed by a data source. This means that you shouldn't change cells directly like you do here:
cell?.simulateCheck()
tableView.reloadData()
Instead you should keep a list of all the checked positions, maybe another array that has bools for each corresponding articleValue (this is not the best design).
var checkedValues = Bool
In your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method you would then set the state of the cell:
articleValueCell.buttonState = checkedValues[indexPath.row]
In your selectAll method fill this array with true values and then call tableView.reloadData()
private var checkedValues = [Bool]()
private var articleValues: [ArticleValue] = []{
didSet{
checkedValues = Array(repeating: false, count: articleValues.count)
tableView.reloadData()
}
}
func selectAll(){
checkedValues = Array(repeating: true, count: articleValues.count)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
articleValueCell.buttonState = checkedValues[indexPath.row]
}
return cell
}
Another mistake is that you should never iterate on all the cells in the table because they are reused, no point in going through your data source and getting a cell for each. It only makes sense to iterate through tableView.visibleCells. But like in your case, most of the time you don't need that either, you should just update your data source accordingly and reload the table or just the modified cell.
It's not recommended that you refer to cells directly within a table view. The reason is that UITableViews have an efficient method of only loading the cells as they are needed (and deallocating them when they are no longer needed, e.g. the cell scrolls off screen). Because of this the cell you are try to refer to may not be loaded.
Instead you should interact with it via the cellForRowAt method. If you want to "select all" cells, you should create a property that stores the value of checked or not checked via a Bool and then set all of the ArticleValue elements to true for that property and reload the data inside selectAll().
It could work something like this:
func selectAll() {
articleValues.forEach {
$0.checked = true
}
tableView.reloadData()
}
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
if articleValue.checked {
articleValueCell.simulateCheck()
}
}
return cell
}

how can I get the image from dynamically generated UITableViewCell and pass it with segue to the other View?

I have a UITableViewController with UITableViewCell dynamically generated there. Each cell contains an imageView that I'm filling with images fetched from my server. I'm using alamofire_images to do so. My code looks as follows:
func tableView(testDetailsPanel: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = testDetailsPanel.dequeueReusableCellWithIdentifier("cell") as! TestDetailsCell
let test:SingleTest = self.items[indexPath.row] as! SingleTest
if(test.photo != "") {
cell.myPhoto.af_setImageWithURL(NSURL(string: test.photo)!)
} else {
cell.myPhoto.image = UIImage(named: "clusterLarge")
}
return cell
}
I thought that since I'm downloading images while displaying the table, there is no need to download it again on the other screen (which is accessible through clicking each cell).
So my idea was to pass the image from specific cell to the other screen through segue. But the problem is that from the method prepareForSegue I don't have access to the specific cell that user clicks. So my other choice was to use protocols. I created a very simple one:
protocol HandlePhoto: class {
func setUpBackgroundPhoto(miniature: UIImage)
}
and then in my native class I wanted to use it in didSelectRowAtIndexPath method:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let test:SingleTest = self.items[indexPath.row] as! SingleTest
let cell = testDetailsPanel.dequeueReusableCellWithIdentifier("cell") as! TestDetailsCell
if(test.photo != "") {
handlePhoto.setUpBackgroundPhoto(cell.testPhoto.image!)
self.performSegueWithIdentifier("testPhotoDetailsSegue", sender: test)
}
} else {
self.performSegueWithIdentifier("testTextDetailsSegue", sender: test)
}
}
But this line:
handlePhoto.setUpBackgroundPhoto(cell.testPhoto.image!)
throws error:
fatal error: unexpectedly found nil while unwrapping an Optional value
So my final question is: how can I access photo from the specific cell that user chooses and pass it to other view (without downloading it there for the 2nd time)?
Your didSelectRowAtIndexPath implementation is wrong, with dequeueReusableCellWithIdentifier you are obtaining a new cell, not the selected cell.
Try this:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
{
let selectedCell = tableView.cellForRow(at: indexPath) as! TestDetailsCell
//this will return downloaded image or "clusterLarge"
let image = selectedCell.myPhoto.image
//
//Make your check on image and extra setup
//
self.performSegueWithIdentifier("testPhotoDetailsSegue", sender: test)
}
Why are you using dequeueReusableCellWithIdentifier in your didSelectRowAtIndexPath?; instead, you should get the cell directly using:
let cell = yourTableView.cellForRowAtIndexPath(indexPath) as! TestDetailsCell
if let image = cell.testPhoto.image {
print(image)//this is what you want.
}

Resources