iosswiftgoogle-cloud-firestoreavplayersdwebimage

How to display the image from tableViewCell downloaded from URL using SDWebImage in the detailViewController?


My app downloads images from firebase and displays them in the tableViewController cells, also I need the AudioPlayerViewController to display exactly same image of the selected row/cell. For now in anyway it displays only the last downloaded image. I tried to save downloaded images into array but it always threw errors like cannot assign UIImage to [UIImage] or something like this. Hope to your help friends.

TableViewCell displays images correctly and accordingly But when user taps on whichever row, it starts the AudioPlayerViewController and displays the last downloaded image anyway. Please see next image. This is the screenShot of the print of the downloaded images

import UIKit
import AVKit
import AVFoundation
import FirebaseFirestore
import Combine
import SDWebImage

class ListOfAudioLessonsTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    @IBOutlet var table: UITableView!
    
    let placeHolderImage = UIImage(named: "placeHolderImage")
    private var viewModel = AudiosViewModel()
    private var cancellable: AnyCancellable?



        override func viewDidLoad() {
            super.viewDidLoad()
            
            self.viewModel.fetchData()
            self.title = "Audio Lessons"
            
            let nib = UINib(nibName: "AudioCustomTableViewCell", bundle: nil)
            table.register(nib, forCellReuseIdentifier: "audioCustomCell")
                       
            table.delegate = self
            table.dataSource = self
            
            cancellable = viewModel.$audios.sink { _ in
                DispatchQueue.main.async{
                    
                    self.table.reloadData()
        }
    }            
}
    
    

        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            print("audios count = ", viewModel.audios.count)
            return viewModel.audios.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "audioCustomCell", for: indexPath) as? AudioCustomTableViewCell
            let song = viewModel.audios[indexPath.row]
            let imageURL = song.audioImageName

            cell?.audioImage.sd_imageIndicator = SDWebImageActivityIndicator.gray
            cell?.audioImage.sd_setImage(with: URL(string: imageURL),
                                        placeholderImage: placeHolderImage,
                                        options: SDWebImageOptions.highPriority,
                                        context: nil,
                                        progress: nil,
                                        completed: { downloadedImage, downloadException, cacheType, downloadURL in
                if let downloadException = downloadException {
                    print("error downloading the image: \(downloadException.localizedDescription)")
                } else {

                    print("successfuly downloaded the image: \(String(describing: downloadURL?.absoluteString))")
                    
                }

                self.viewModel.image = cell?.audioImage.image
                
            })
            
            tableView.tableFooterView = UIView()
            cell?.textLabel?.font = UIFont(name: "Helvetica-Bold", size: 14)
            cell?.detailTextLabel?.font = UIFont(name: "Helvetica", size: 12)
            
            cell?.commonInit(song.albumName, song.name, viewModel.image)
                    
            return cell!
            
        }
    
        
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            
            return 120
            
        }
        
        
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let position = indexPath.row
        guard let vc = storyboard?.instantiateViewController(identifier: "AudioPlayer") as? AudioPlayerViewController else {
            
            return
            
        }
        vc.mainImage = viewModel.image
        vc.paragraphs = viewModel.audios
        vc.position = position
        
        
        present(vc, animated: true)
        
    }
    
     func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
        if let cell = tableView.cellForRow(at: indexPath) {
            cell.contentView.backgroundColor = UIColor(named: "AudioLessonsHighlighted")
            cell.textLabel?.highlightedTextColor = UIColor(named: "textHighlighted")
            cell.detailTextLabel?.highlightedTextColor = UIColor(named: "textHighlighted")
            
        }
    }
    
     func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
        if let cell = tableView.cellForRow(at: indexPath) {
            cell.contentView.backgroundColor = nil
            
        }
    }
}

This is the viewModel

import Foundation
import FirebaseFirestore
import SDWebImage

class AudiosViewModel: ObservableObject {

@Published var audios = [Audio]()
private var db = Firestore.firestore()
var image: UIImage?

func fetchData() {
    db.collection("audios").addSnapshotListener { [self] (querySnapshot, error) in
    guard let documents = querySnapshot?.documents else {
    print("No Documents")
    return
}

self.audios = documents.map { (queryDocumentSnapshot) -> Audio in
    let data = queryDocumentSnapshot.data()
     
    let image = image
    let name = data["name"] as? String ?? ""
    let albumName = data["albumName"] as? String ?? ""
    let audioImageName = data["audioImageName"] as? String ?? ""
    let paragraphNumber = data["paragraphNumber"] as? String ?? ""
    let trackURL = data["trackURL"] as? String ?? ""
   
    return Audio(image: image, name: name, albumName: albumName, paragraphNumber: paragraphNumber, audioImageName: audioImageName, trackURL: trackURL)
              
            }
        }
    }
}

this is the AudioPlayerViewController

import UIKit
import AVFoundation
import MediaPlayer
import AVKit
import Combine

class AudioPlayerViewController: UIViewController {
                
//    private var viewModel = AudiosViewModel()
    public var position: Int = 0
    public var paragraphs: [Audio] = []
    public var mainImage = UIImage(named: "placeHolderImage")
   
@IBOutlet var holder: UIView!
var player: AVPlayer?
    var playerItem: AVPlayerItem?
    var isSeekInProgress = false
    var chaseTime = CMTime.zero
    fileprivate let seekDuration: Float64 = 15
    var playerCurrentItemStatus: AVPlayerItem.Status = .unknown

                // User Interface elements
private let albumImageView: UIImageView = {
let imageView = UIImageView()
    imageView.layer.shadowOpacity = 0.3
    imageView.layer.shadowRadius = 3
    imageView.layer.shadowOffset = CGSize(width: 6, height: 6)
    imageView.layer.shadowRadius = 8
imageView.contentMode = .scaleAspectFill
return imageView
}()
    
private let paragraphNumberLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 16, weight: .light)
label.numberOfLines = 0 // allow line wrap
label.textColor = UIColor(named: "PlayerColors")
return label
}()
                
private let albumNameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 18, weight: .bold)
label.numberOfLines = 0 // allow line wrap
label.textColor = UIColor(named: "PlayerColors")
return label
}()
                
private let songNameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
    label.font = .systemFont(ofSize: 16, weight: .ultraLight)
label.numberOfLines = 0 // allow line wrap
label.textColor = UIColor(named: "PlayerColors")
return label
}()
    
private let elapsedTimeLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 12, weight: .light)
label.textColor = UIColor(named: "PlayerColors")
label.text = "00:00"
label.numberOfLines = 0
return label
}()
        
private let remainingTimeLabel: UILabel = {
let label = UILabel()
label.textAlignment = .left
label.font = .systemFont(ofSize: 12, weight: .light)
label.textColor = UIColor(named: "PlayerColors")
label.text = "00:00"
label.numberOfLines = 0
return label
}()
        
private let playbackSlider: UISlider = {
let v = UISlider()
    v.addTarget(AudioPlayerViewController.self, action: #selector(progressScrubbed(_:)), for: .valueChanged)
    v.minimumTrackTintColor = UIColor.lightGray
v.maximumTrackTintColor = UIColor.darkGray
v.thumbTintColor = UIColor(named: "PlayerColors")
v.minimumValue = 0
v.isContinuous = false
return v
}()
   
    
    

let playPauseButton = UIButton()
    
    
                

override func viewDidLoad() {
    super.viewDidLoad()
                    
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGesture(gesture:)))
    self.playbackSlider.addGestureRecognizer(panGesture)
                    

let audioSession = AVAudioSession.sharedInstance()
    do {
        try audioSession.setCategory(AVAudioSession.Category.playback)
            }
                catch{
                        print(error)
                    }
                }
                
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        

        
        
                
if holder.subviews.count == 0 {
    configure()
                    }
                }
                func configure() {
                    // set up player
                    let song = paragraphs[position]
                    
                    let url = URL(string: song.trackURL)
                    let playerItem: AVPlayerItem = AVPlayerItem(url: url!)
                    do {
                        try AVAudioSession.sharedInstance().setMode(.default)
                        try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
                        guard url != nil else {
                            print("urls string is nil")
                            return
                        }
   
                        player = AVPlayer(playerItem: playerItem)
                        
                        
                        let duration : CMTime = playerItem.asset.duration
                        
                        let seconds : Float64 = CMTimeGetSeconds(duration)
                        
                        remainingTimeLabel.text = self.stringFromTimeInterval(interval: seconds)
                        
                        let currentDuration : CMTime = playerItem.currentTime()
                        
                        let currentSeconds : Float64 = CMTimeGetSeconds(currentDuration)
                        
                        elapsedTimeLabel.text = self.stringFromTimeInterval(interval: currentSeconds)

                        playbackSlider.maximumValue = Float(seconds)
                       
                        
                        player!.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { (CMTime) -> Void in
                            
                            if self.player!.currentItem?.status == .readyToPlay {
                                let time : Float64 = CMTimeGetSeconds(self.player!.currentTime());
                                self.playbackSlider.value = Float(time)
                                
                                self.elapsedTimeLabel.text = self.stringFromTimeInterval(interval: time)
                            }
                            
                            let playbackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp
                            if playbackLikelyToKeepUp == false{
                                print("IsBuffering")
                                self.playPauseButton.isHidden = true
                               
                            } else {
                                // stop the activity indicator
                                print("Buffering completed")
                                
                                self.playPauseButton.isHidden = false
                                
                            }
                        }
                        
                        
                        playbackSlider.addTarget(self, action: #selector(AudioPlayerViewController.progressScrubbed(_:)), for: .valueChanged)
                        self.view.addSubview(playbackSlider)
                        //subroutine used to keep track of current location of time in audio file
                        guard let player = player else {
                            print("player is nil")
                            return
                        }
                        player.play()
                        
                    }
                    catch {
                        print("error accured")
                    }
                    // set up user interface elements
                    
                    //album cover
albumImageView.frame = CGRect(x: 20,
                              y: 20,
                              width: holder.frame.size.width - 40,
                              height: holder.frame.size.width - 40)
                    
                    albumImageView.image = mainImage
                    

                    
                    
                    holder.addSubview(albumImageView)
                    
                    //Labels Song name, album, artist
                    albumNameLabel.frame = CGRect(x: 20,
                                                 y: holder.frame.size.height - 300,
                                                  width: holder.frame.size.width - 40,
                                                  height: 20)
                    paragraphNumberLabel.frame = CGRect(x: 20,
                                                  y: holder.frame.size.height - 280,
                                                  width: holder.frame.size.width-40,
                                                  height: 20)
                    songNameLabel.frame = CGRect(x: 20,
                                                  y: holder.frame.size.height - 260,
                                                  width: holder.frame.size.width-40,
                                                  height: 20)
                    playbackSlider.frame = CGRect(x: 20,
                                                  y: holder.frame.size.height - 235,
                                                  width: holder.frame.size.width-40,
                                                  height: 40)
                    elapsedTimeLabel.frame = CGRect(x: 25,
                                                    y: holder.frame.size.height - 200,
                                                    width: holder.frame.size.width-40,
                                                    height: 15)
                    remainingTimeLabel.frame = CGRect(x: holder.frame.size.width-60,
                                                      y: holder.frame.size.height - 200,
                                                      width: holder.frame.size.width-20,
                                                      height: 15)
                    songNameLabel.text = song.name
                    albumNameLabel.text = song.albumName
                    paragraphNumberLabel.text = song.paragraphNumber
                    holder.addSubview(songNameLabel)
                    holder.addSubview(albumNameLabel)
                    holder.addSubview(paragraphNumberLabel)
                    holder.addSubview(elapsedTimeLabel)
                    holder.addSubview(remainingTimeLabel)
                    //Player controls
                    let nextButton = UIButton()
                    let backButton = UIButton()
                    let seekForwardButton = UIButton()
                    let seekBackwardButton = UIButton()
                    //frames of buttons
                    playPauseButton.frame = CGRect(x: (holder.frame.size.width - 40) / 2.0,
                                                   y: holder.frame.size.height - 172.5,
                                                   width: 40,
                                                   height: 40)
                    
                    nextButton.frame = CGRect(x: holder.frame.size.width - 70,
                                                   y: holder.frame.size.height - 162.5,
                                                   width: 30,
                                                   height: 20)
                    
                    backButton.frame = CGRect(x: 70 - 30,
                                            y: holder.frame.size.height - 162.5,
                                            width: 30,
                                            height: 20)
                    seekForwardButton.frame = CGRect(x: holder.frame.size.width - 140,
                                                     y: holder.frame.size.height - 167.5,
                                            width: 30,
                                            height: 30)
                    seekBackwardButton.frame = CGRect(x: 110,
                                            y: holder.frame.size.height - 167.5,
                                            width: 30,
                                            height: 30)
                    let volumeView = MPVolumeView(frame: CGRect(x: 20,
                                                                y: holder.frame.size.height - 80,
                                                                width: holder.frame.size.width-40,
                                                                height: 30))
                    
                    
                    
                    
                    
                    holder.addSubview(volumeView)
                    //actions of buttons
                    playPauseButton.addTarget(self, action: #selector(didTapPlayPauseButton), for: .touchUpInside)
                    backButton.addTarget(self, action: #selector(didTapBackButton), for: .touchUpInside)
                    nextButton.addTarget(self, action: #selector(didTapNextButton), for: .touchUpInside)
                    seekForwardButton.addTarget(self, action: #selector(seekForwardButtonTapped), for: .touchUpInside)
                    seekBackwardButton.addTarget(self, action: #selector(seekBackwardButtonTapped), for: .touchUpInside)
                    //styling of buttons
                    playPauseButton.setBackgroundImage(UIImage(systemName: "pause.fill"), for: .normal)
                    nextButton.setBackgroundImage(UIImage(systemName: "forward.fill"), for: .normal)
                    backButton.setBackgroundImage(UIImage(systemName: "backward.fill"), for: .normal)
                    seekForwardButton.setBackgroundImage(UIImage(systemName: "goforward.15"), for: .normal)
                    seekBackwardButton.setBackgroundImage(UIImage(systemName: "gobackward.15"), for: .normal)
   
                    playPauseButton.tintColor = UIColor(named: "PlayerColors")
                    nextButton.tintColor = UIColor(named: "PlayerColors")
                    backButton.tintColor = UIColor(named: "PlayerColors")
                    seekForwardButton.tintColor = UIColor(named: "PlayerColors")
                    seekBackwardButton.tintColor = UIColor(named: "PlayerColors")
                    
                    holder.addSubview(playPauseButton)
                    holder.addSubview(nextButton)
                    holder.addSubview(backButton)
                    holder.addSubview(seekForwardButton)
                    holder.addSubview(seekBackwardButton)
}
    
    @objc func panGesture(gesture: UIPanGestureRecognizer) {
        let currentPoint = gesture.location(in: playbackSlider)
        let percentage = currentPoint.x/playbackSlider.bounds.size.width;
        let delta = Float(percentage) * (playbackSlider.maximumValue - playbackSlider.minimumValue)
        let value = playbackSlider.minimumValue + delta
        playbackSlider.setValue(value, animated: true)
    }
    
    @objc func progressScrubbed(_ playbackSlider: UISlider!) {
        let seconds : Int64 = Int64(playbackSlider.value)
        let targetTime:CMTime = CMTimeMake(value: seconds, timescale: 1)
        player!.seek(to: targetTime)
        if player!.rate == 0
        {
            player?.play()
        }
    }
    
    func setupNowPlaying() {
      // Define Now Playing Info
      var nowPlayingInfo = [String : Any]()
      nowPlayingInfo[MPMediaItemPropertyTitle] = "Unstoppable"
      
      if let image = UIImage(named: "artist") {
        nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { size in
          return image
        }
      }
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem?.duration
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player?.rate
      
      // Set the metadata
      MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
    
    func updateNowPlaying(isPause: Bool) {
      // Define Now Playing Info
      var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo!
      
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime
      nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPause ? 0 : 1
      
      // Set the metadata
      MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
    
    func setupNotifications() {
      let notificationCenter = NotificationCenter.default
      notificationCenter.addObserver(self,
                                     selector: #selector(handleInterruption),
                                     name: AVAudioSession.interruptionNotification,
                                     object: nil)
      notificationCenter.addObserver(self,
                                     selector: #selector(handleRouteChange),
                                     name: AVAudioSession.routeChangeNotification,
                                     object: nil)
    }

    @objc func handleRouteChange(notification: Notification) {
      guard let userInfo = notification.userInfo,
        let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSession.RouteChangeReason(rawValue:reasonValue) else {
          return
      }
      switch reason {
      case .newDeviceAvailable:
        let session = AVAudioSession.sharedInstance()
        for output in session.currentRoute.outputs where output.portType == AVAudioSession.Port.headphones {
          print("headphones connected")
          DispatchQueue.main.sync {
            player?.play()
          }
          break
        }
      case .oldDeviceUnavailable:
        if let previousRoute =
          userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
          for output in previousRoute.outputs where output.portType == AVAudioSession.Port.headphones {
            print("headphones disconnected")
            DispatchQueue.main.sync {
                player?.pause()
            }
            break
          }
        }
      default: ()
      }
    }
    
    @objc func handleInterruption(notification: Notification) {
      guard let userInfo = notification.userInfo,
        let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
        let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
          return
      }
      
      if type == .began {
        print("Interruption began")
        // Interruption began, take appropriate actions
      }
      else if type == .ended {
        if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
          let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
          if options.contains(.shouldResume) {
            // Interruption Ended - playback should resume
            print("Interruption Ended - playback should resume")
            player?.play()
          } else {
            // Interruption Ended - playback should NOT resume
            print("Interruption Ended - playback should NOT resume")
          }
        }
      }
    }
    
    @objc func didTapPlayPauseButton() {
        if player?.timeControlStatus == .playing {
                           //pause
                           player?.pause()
                           
                           
                           //show play button
                           playPauseButton.setBackgroundImage(UIImage(systemName: "play.fill"), for: .normal)
                           //shrink image
                           UIView.animate(withDuration: 0.2, animations: {
                               self.albumImageView.frame = CGRect(x: 50,
                                                             y: 50,
                                                             width: self.holder.frame.size.width - 100,
                                                             height: self.holder.frame.size.width - 100)
                               
                           })
                           
                       } else {
                           //play
                           player?.play()
                           //show pause button
                           playPauseButton.setBackgroundImage(UIImage(systemName: "pause.fill"), for: .normal)
                           
                           //increase image size
                           UIView.animate(withDuration: 0.4, animations: {
                               self.albumImageView.frame = CGRect(x: 20,
                                                             y: 20,
                                                             width: self.holder.frame.size.width - 40,
                                                             height: self.holder.frame.size.width - 40)
                               
        })
    }
}

This is Audio Struct

import Foundation
import UIKit

struct Audio {
    let image: UIImage?
    let name: String
    let albumName: String
    let paragraphNumber: String
    let audioImageName: String
    let trackURL: String
    
}

This is customTableViewCell

import UIKit

class AudioCustomTableViewCell: UITableViewCell {

    @IBOutlet weak var cellView: UIView!
    
    @IBOutlet weak var audioImage: UIImageView!
    
    @IBOutlet weak var mainTitle: UILabel!
    
    @IBOutlet weak var detailTitle: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        
        setCardView(toView: audioImage)
        setCardView(toView: cellView)
        
        self.audioImage.layer.cornerRadius = 6
        audioImage.clipsToBounds = true
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    func commonInit(_ title: String, _ subtitle: String, _ image: UIImage?){
        mainTitle.text = title
        detailTitle.text = subtitle
        audioImage.image = image
        
        
    }
    func setCardView(toView: UIView) {
        toView.layer.shadowColor = UIColor.black.cgColor
        toView.layer.shadowOpacity = 0.3
        toView.layer.shadowRadius = 3
        toView.layer.shadowOffset = CGSize(width: 6, height: 6)
        toView.layer.shadowRadius = 8
        toView.layer.cornerRadius = 6
    }
    
}

Solution

  • when you are navigating from didSelectRowAt Indexpath please pass all the values to your detailViewController when you navigate to that view all value will be there to use.

    use let song = viewModel.audios[indexPath.row] let imageURL = song.audioImageName

    Get values and pass it to your detailViewController

    do the same thing in didSelectRowAt Indexpath what you did in cellForRowAt indexPath because there you are just passing position and taking values from model and passing it that's why this might be happening.

    if you are getting only this error cannot assign UIImage to [UIImage]

    then you can just convert your UIImage to [UIImage] and assign it to your [UIImage]