I'm new in Swift, and I have an issue with the iPhone X.
I followed this tutorial: https://www.youtube.com/watch?v=FDay6ocBlnE&index=8&list=PL0dzCUj1L5JEfHqwjBV0XFb9qx9cGXwkq in order to create a Chat App.
My problem is that the textField is fixed to the bottom, and that is not good for the iPhone X.
I really don't know how I can change this, given that I'm more familiar with storyboard and here, the collectionViewController is entirely programmatically. I searched a lot of other tutorials but I found nothing to help.
This is my code:
The bottom view (with the textfield):
class ChatInputContainerView: UIView, UITextFieldDelegate {
// ...
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
// ...
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
The CollectionViewController:
class ChatLogController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
// ...
lazy var inputContainerView: ChatInputContainerView = {
// I can't change the y value (it changes nothing)
let chatInputContainerView = ChatInputContainerView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 54))
chatInputContainerView.chatLogController = self
return chatInputContainerView
}()
override var inputAccessoryView: UIView? {
get {
return inputContainerView
}
}
override var canBecomeFirstResponder : Bool {
return true
}
}
Update
Here's the entire code:
import UIKit
import UserNotifications
class ChatLogController: UICollectionViewController, UITextFieldDelegate, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var user: Userm? {
didSet {
navigationItem.title = user?.username
loadMessages()
}
}
var messages = [Message]()
func loadMessages() {
guard let toId = user?.id else {
return
}
Api.Message.observeUserDiscussion(toId: toId) { (message) in
self.messages.append(message)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
//scroll to the last index
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
})
}
}
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
print("Notification settings: \(settings)")
guard settings.authorizationStatus == .authorized else { return }
//Not authorised
UIApplication.shared.registerForRemoteNotifications()
}
navigationItem.backBarButtonItem = UIBarButtonItem(title: " ", style: .plain, target: nil, action: nil)
collectionView?.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 20, right: 0)
// collectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
collectionView?.alwaysBounceVertical = true
collectionView?.backgroundColor = UIColor.white
collectionView?.register(ChatMessageCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.keyboardDismissMode = .interactive
arrowBackButton(greyBack)
let image = UIImage(named: "iconProfilCog")
navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(handleParamsMessage))
navigationItem.rightBarButtonItem?.tintColor = UIColor(red: 203/255, green: 203/255, blue: 203/255, alpha: 1)
setupKeyboardObservers()
emptyTextField()
}
func emptyTextField() {
self.inputContainerView.inputTextField.text = ""
self.inputContainerView.sendButton.isEnabled = false
self.inputContainerView.sendButton.alpha = 0.8
}
override func viewDidLayoutSubviews() {
inputContainerView.inputTextField.roundCorners([.topLeft,.bottomLeft], radius: 10)
inputContainerView.backgroundSendButtonView.roundCorners([.topRight,.bottomRight], radius: 22)
}
lazy var inputContainerView: ChatInputContainerView = {
let chatInputContainerView = ChatInputContainerView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 54))
chatInputContainerView.chatLogController = self
return chatInputContainerView
}()
func handleParamsMessage() {
print("params")
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let detailMessage = storyboard.instantiateViewController(withIdentifier: "MessageDetailTableViewController") as! MessageDetailTableViewController
if let user = user {
detailMessage.userId = user.id!
self.navigationController?.pushViewController(detailMessage, animated: true)
}
}
func handleUploadTap() {
let imagePickerController = UIImagePickerController()
imagePickerController.allowsEditing = true
imagePickerController.delegate = self
//imagePickerController.mediaTypes = [kUTTypeImage as String, kUTTypeMovie as String]
present(imagePickerController, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// if let videoUrl = info[UIImagePickerControllerMediaURL] as? URL {
// //we selected a video
// handleVideoSelectedForUrl(videoUrl)
// } else {
// //we selected an image
handleImageSelectedForInfo(info as [String : AnyObject])
// }
dismiss(animated: true, completion: nil)
}
fileprivate func handleImageSelectedForInfo(_ info: [String: AnyObject]) {
var selectedImageFromPicker: UIImage?
if let editedImage = info["UIImagePickerControllerEditedImage"] as? UIImage {
selectedImageFromPicker = editedImage
} else if let originalImage = info["UIImagePickerControllerOriginalImage"] as? UIImage {
selectedImageFromPicker = originalImage
}
if let selectedImage = selectedImageFromPicker {
HelperService.uploadMessagePictureToDatabase(selectedImage, completion: { (imageUrl) in
self.sendMessageWithImageUrl(imageUrl, image: selectedImage)
})
}
}
override var inputAccessoryView: UIView? {
get {
return inputContainerView
}
}
override var canBecomeFirstResponder : Bool {
return true
}
func setupKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
func handleKeyboardDidShow() {
if messages.count > 0 {
let indexPath = IndexPath(item: messages.count - 1, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .top, animated: true)
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ChatMessageCell
cell.chatLogController = self
let message = messages[indexPath.item]
cell.textView.text = message.text
setupCell(cell, message: message)
//lets modify the bubbleView's width somehow???
// cell.bubbleWidthAnchor?.constant = estimateFrameForText(message.text!).width + 25
if let text = message.text {
//a text message
cell.bubbleWidthAnchor?.constant = estimateFrameForText(text).width + 25
cell.textView.isHidden = false
} else if message.imageUrl != nil {
//fall in here if its an image message
cell.bubbleWidthAnchor?.constant = 200
cell.textView.isHidden = true
}
return cell
}
fileprivate func setupCell(_ cell: ChatMessageCell, message: Message) {
if let profileImageUrl = self.user?.profileImageUrl {
let photoUrl = URL(string: profileImageUrl)
cell.profileImageView.sd_setImage(with: photoUrl)
}
if message.fromId == Api.User.CURRENT_USER?.uid {
//outgoing blue
cell.bubbleView.backgroundColor = ChatMessageCell.blueColor
cell.textView.textColor = UIColor.white
cell.profileImageView.isHidden = true
cell.tailImageView.isHidden = true
cell.bubbleViewRightAnchor?.isActive = true
cell.bubbleViewLeftAnchor?.isActive = false
} else {
//incoming gray
cell.bubbleView.backgroundColor = UIColor(red: 243/255, green: 243/255, blue: 243/255, alpha: 1)
cell.textView.textColor = UIColor(red: 70/255, green: 70/255, blue: 70/255, alpha: 1)
cell.profileImageView.isHidden = false
cell.tailImageView.isHidden = false
cell.bubbleViewRightAnchor?.isActive = false
cell.bubbleViewLeftAnchor?.isActive = true
}
if let messageImageUrl = message.imageUrl {
let photoUrl = URL(string: messageImageUrl)
cell.messageImageView.sd_setImage(with: photoUrl)
cell.messageImageView.isHidden = false
// cell.bubbleView.backgroundColor = UIColor(red: 243/255, green: 243/255, blue: 243/255, alpha: 1)
} else {
cell.messageImageView.isHidden = true
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var height: CGFloat = 80
let message = messages[indexPath.item]
if let text = message.text {
height = estimateFrameForText(text).height + 18
} else if let imageWidth = message.imageWidth?.floatValue, let imageHeight = message.imageHeight?.floatValue {
// h1 / w1 = h2 / w2
// solve for h1
// h1 = h2 / w2 * w1
height = CGFloat(imageHeight / imageWidth * 200)
}
let width = UIScreen.main.bounds.width
return CGSize(width: width, height: height)
}
fileprivate func estimateFrameForText(_ text: String) -> CGRect {
let size = CGSize(width: 200, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15, weight: .medium)], context: nil)
}
var containerViewBottomAnchor: NSLayoutConstraint?
func handleSend() {
self.inputContainerView.sendButton.isEnabled = false
let properties = ["text": inputContainerView.inputTextField.text!]
sendMessageWithPropertiesFIR(properties as [String : AnyObject])
}
fileprivate func sendMessageWithImageUrl(_ imageUrl: String, image: UIImage) {
let properties: [String: AnyObject] = ["imageUrl": imageUrl as AnyObject, "imageWidth": image.size.width as AnyObject, "imageHeight": image.size.height as AnyObject]
sendMessageWithPropertiesFIR(properties)
}
func sendMessageWithPropertiesFIR(_ properties: [String: AnyObject]) {
print(properties["text"])
var messageText = ""
if properties["text"] != nil {
messageText = properties["text"] as! String
} else {
messageText = "A envoyé une photo"
}
Api.Message.sendMessageWithProperties(toId: user!.id!, properties: properties) {
Api.Message.isUserMuted(userId: self.user!.id!, completion: { (isMuted) in
if !isMuted {
Api.UserToken.observeUserToken(withUser: self.user!.id!, completion: { (token) in
if let token = token {
Api.User.observeCurrentUser(completion: { (user) in
Api.Notification.sendNotifPush(token: token, message: "\(user.username!): \(messageText)")
})
}
})
}
})
self.emptyTextField()
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
handleSend()
return true
}
var startingFrame: CGRect?
var blackBackgroundView: UIView?
var startingImageView: UIImageView?
//my custom zooming logic
func performZoomInForStartingImageView(_ startingImageView: UIImageView) {
self.startingImageView = startingImageView
self.startingImageView?.isHidden = true
self.inputContainerView.inputTextField.resignFirstResponder()
startingFrame = startingImageView.superview?.convert(startingImageView.frame, to: nil)
let zoomingImageView = UIImageView(frame: startingFrame!)
zoomingImageView.backgroundColor = UIColor.red
zoomingImageView.image = startingImageView.image
zoomingImageView.isUserInteractionEnabled = true
zoomingImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleZoomOut)))
if let keyWindow = UIApplication.shared.keyWindow {
blackBackgroundView = UIView(frame: keyWindow.frame)
blackBackgroundView?.backgroundColor = UIColor.black
blackBackgroundView?.alpha = 0
keyWindow.addSubview(blackBackgroundView!)
keyWindow.addSubview(zoomingImageView)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// self.inputContainerView.inputTextField.resignFirstResponder()
self.blackBackgroundView?.alpha = 1
self.inputContainerView.alpha = 0
// math?
// h2 / w1 = h1 / w1
// h2 = h1 / w1 * w1
let height = self.startingFrame!.height / self.startingFrame!.width * keyWindow.frame.width
zoomingImageView.frame = CGRect(x: 0, y: 0, width: keyWindow.frame.width, height: height)
zoomingImageView.center = keyWindow.center
}, completion: { (completed) in
// do nothing
})
}
}
func handleZoomOut(_ tapGesture: UITapGestureRecognizer) {
if let zoomOutImageView = tapGesture.view {
//need to animate back out to controller
zoomOutImageView.layer.cornerRadius = 8
zoomOutImageView.clipsToBounds = true
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.startingFrame = self.startingImageView?.superview?.convert((self.startingImageView?.frame)!, to: nil)
zoomOutImageView.frame = self.startingFrame!
self.blackBackgroundView?.alpha = 0
self.inputContainerView.alpha = 1
}, completion: { (completed) in
zoomOutImageView.removeFromSuperview()
self.startingImageView?.isHidden = false
})
}
}
}
The View:
import UIKit
class ChatInputContainerView: UIView, UITextFieldDelegate {
weak var chatLogController: ChatLogController? {
didSet {
sendButton.addTarget(chatLogController, action: #selector(ChatLogController.handleSend), for: .touchUpInside)
uploadImageView.addGestureRecognizer(UITapGestureRecognizer(target: chatLogController, action: #selector(ChatLogController.handleUploadTap)))
inputTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
}
}
let inputColor = UIColor(red: 243/255, green: 243/255, blue: 243/255, alpha: 1)
lazy var inputTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Entrer un message..."
textField.translatesAutoresizingMaskIntoConstraints = false
textField.delegate = self
textField.backgroundColor = inputColor
// textField.roundCorners([.topLeft,.bottomLeft], radius: 10)
textField.clipsToBounds = true
return textField
}()
let uploadImageView: UIImageView = {
let uploadImageView = UIImageView()
uploadImageView.isUserInteractionEnabled = true
uploadImageView.image = UIImage(named: "pinImage")
uploadImageView.translatesAutoresizingMaskIntoConstraints = false
return uploadImageView
}()
lazy var backgroundSendButtonView: UIView = {
let backgroundSendButtonView = UIView()
backgroundSendButtonView.backgroundColor = inputColor
backgroundSendButtonView.translatesAutoresizingMaskIntoConstraints = false
return backgroundSendButtonView
}()
let sendButton = UIButton(type: .system)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
addSubview(uploadImageView)
// sendButton.setTitle("Send", for: UIControlState())
sendButton.setImage(UIImage(named: "planeChat"), for: .normal)
sendButton.backgroundColor = UIColor.white
sendButton.tintColor = UIColor(red: 82/255, green: 121/255, blue: 179/255, alpha: 1)
sendButton.layer.cornerRadius = 20
sendButton.clipsToBounds = true
sendButton.translatesAutoresizingMaskIntoConstraints = false
//what is handleSend?
addSubview(sendButton)
addSubview(self.inputTextField)
//x,y,w,h
// A enlever après
self.inputTextField.leftAnchor.constraint(equalTo: uploadImageView.rightAnchor, constant: 12).isActive = true
//self.inputTextField.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
//self.inputTextField.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
self.inputTextField.topAnchor.constraint(equalTo: topAnchor, constant: 4).isActive = true
self.inputTextField.rightAnchor.constraint(equalTo: sendButton.leftAnchor, constant: -4).isActive = true
self.inputTextField.heightAnchor.constraint(equalToConstant: 48).isActive = true
//x,y,w,h
sendButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true
sendButton.centerYAnchor.constraint(equalTo: inputTextField.centerYAnchor).isActive = true
sendButton.widthAnchor.constraint(equalToConstant: 38).isActive = true
sendButton.heightAnchor.constraint(equalToConstant: 38).isActive = true
//x,y,w,h
uploadImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: 18).isActive = true
uploadImageView.centerYAnchor.constraint(equalTo: inputTextField.centerYAnchor).isActive = true
uploadImageView.widthAnchor.constraint(equalToConstant: 18).isActive = true
uploadImageView.heightAnchor.constraint(equalToConstant: 20).isActive = true
//l//et backgroundSendButtonView = UIView()
//addSubview(backgroundSendButtonView)
// backgroundSendButtonView.roundCorners([.topRight,.bottomRight], radius: 24)
insertSubview(backgroundSendButtonView, belowSubview: sendButton)
backgroundSendButtonView.rightAnchor.constraint(equalTo: rightAnchor, constant: -4).isActive = true
backgroundSendButtonView.centerYAnchor.constraint(equalTo: inputTextField.centerYAnchor).isActive = true
//backgroundSendButtonView.widthAnchor.constraint(equalToConstant: 30).isActive = true
backgroundSendButtonView.leftAnchor.constraint(equalTo: inputTextField.rightAnchor).isActive = true
backgroundSendButtonView.heightAnchor.constraint(equalTo: inputTextField.heightAnchor).isActive = true
//x,y,w,h
// let separatorLineView = UIView()
// separatorLineView.backgroundColor = UIColor(red: 220/255, green: 220/255, blue: 220/255, alpha: 1)
// separatorLineView.translatesAutoresizingMaskIntoConstraints = false
// addSubview(separatorLineView)
// //x,y,w,h
// separatorLineView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
// separatorLineView.topAnchor.constraint(equalTo: topAnchor).isActive = true
// separatorLineView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
// separatorLineView.heightAnchor.constraint(equalToConstant: 1).isActive = true
let gradientView = UIView()
let colorTop = UIColor.clear.cgColor
let colorBottom = UIColor(red: 0, green: 0, blue: 0, alpha: 0.05).cgColor
gradientView.translatesAutoresizingMaskIntoConstraints = false
addSubview(gradientView)
//x,y,w,h
gradientView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
gradientView.topAnchor.constraint(equalTo: topAnchor, constant: -25).isActive = true
gradientView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
gradientView.heightAnchor.constraint(equalToConstant: 25).isActive = true
gradientView.backgroundColor = UIColor.clear
let gradientBackground = CAGradientLayer()
gradientBackground.colors = [ colorTop, colorBottom]
gradientBackground.locations = [0.0, 1.0]
var backgroundLayer = CALayer()
backgroundLayer = gradientBackground
let width = UIScreen.main.bounds.size.width
backgroundLayer.frame = CGRect(x: 0, y: 0, width: width, height: 25)
print(backgroundLayer.frame)
print(gradientView.bounds)
gradientView.layer.insertSublayer(backgroundLayer, at: 0)
}
func setGradient(_ view: UIView, colorTop: CGColor, colorBottom: CGColor) {
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
chatLogController?.handleSend()
return true
}
#objc func textFieldDidChange(_ textField: UITextField) {
if textField == self.inputTextField {
if self.inputTextField.text!.isEmpty {
disableButton()
} else {
// sendButton.setTitleColor(typoGreyButton, for: .normal)
self.sendButton.isEnabled = true
self.sendButton.alpha = 1
}
}
}
func disableButton(){
//sendButton.setTitleColor(smoothGray, for: .normal)
sendButton.isEnabled = false
self.sendButton.alpha = 0.8
}
func emptyTextField() {
self.inputTextField.text = ""
disableButton()
}
// func textViewDidChange(_ textView: UITextView) {
// print(textView)
// if textView == self.inputContainerView.inputTextField {
// if (self.inputContainerView.inputTextField.text?.isEmpty)! {
// disableButton()
// } else {
// // sendButton.setTitleColor(typoGreyButton, for: .normal)
// self.inputContainerView.sendButton.isEnabled = true
// }
// }
//
// }
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If you just want to anchor to the bottom safe area, you can do that anywhere in your view controller:
if #available(iOS 11.0, *) {
someView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
} else {
someView.bottomAnchor.constraint(equalTo: bottomLayoutGuide.topAnchor).isActive = true
}
If, however, you want to use it as a constant (i.e. subtract the length of the bottom safe area from a value), you need to do that in a later lifecycle method like viewDidLayoutSubviews():
if #available(iOS 11.0, *) {
someView.bottomAnchor.constraint(equalTo: anotherView.bottomAnchor, constant: -view.safeAreaInsets.bottom).isActive = true
} else {
someView.bottomAnchor.constraint(equalTo: anotherView.bottomAnchor, constant: -bottomLayoutGuide.length).isActive = true
}
iOS 11 revamped their safe area API so make sure that you support pre-iOS-11 devices as I did in these examples.
I also just noticed that you've set the view's frame explicitly in its intializer. You typically don't want to set a view's frame like that if you're using auto layout (constraints). Therefore, I would suggest not setting the view's frame like you did and instead using constraints to do it.
Related
This is the tableViewController with data from firebase, please pay attention that I put number to the images so that to be able to understand which image is displayed in which position.
This is the AudioPlayerViewController, and user comes here when he taps on the first cell in the tableview. The image displays correctly in all cases at first launch.
But when user changes the track right inside of the AudioPlayerViewController pressing next or previous button, the labels are changing accordingly and correctly but the image remains the one which passed on the first launch.
This is the Audio Structure
import Foundation
import UIKit
struct Audio {
let image: UIImage?
let name: String
let albumName: String
let audioImageName: String
let trackURL: String
let time: String
}
This is the AudiosViewModel which fetches data from firebase-firestorm and populates the cells in ViewController.
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 trackURL = data["trackURL"] as? String ?? ""
let time = data["time"] as? String ?? ""
return Audio(image: image, name: name, albumName: albumName, audioImageName: audioImageName, trackURL: trackURL, time: time)
}
}
}
}
This is the ViewController with the list of audios fetched from firebase-firestorm
import UIKit
import AVKit
import AVFoundation
import FirebaseFirestore
import Combine
import SDWebImage
class ViewController: 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?.chevronImage?.image = UIImage(systemName: "chevron.compact.right")
cell?.chevronImage?.tintColor = .systemMint
cell?.commonInit(song.time ,song.albumName, song.name, viewModel.image, UIImage(systemName: "chevron.compact.right"))
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
let song = viewModel.audios[indexPath.row]
let imageURL = song.audioImageName
SDWebImageManager.shared.loadImage(
with: URL(string: imageURL),
options: .continueInBackground, // or .highPriority
progress: nil,
completed: { [weak self] (image, data, error, cacheType, finished, url) in
guard let self = self else { return }
if error != nil {
// Do something with the error
print("error")
return
}
guard image != nil else {
// No image handle this error
print("error")
return
}
// Do something with image
self.viewModel.image = image
})
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) as? AudioCustomTableViewCell {
cell.cellView.backgroundColor = UIColor(named: "objectHighlightedColor")
cell.mainTitle?.textColor = UIColor(named: "labelHighlighted")
cell.detailTitle?.textColor = UIColor(named: "labelHighlighted")
cell.chevronImage?.tintColor = UIColor(named: "labelHighlighted")
}
}
func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? AudioCustomTableViewCell {
cell.cellView.backgroundColor = UIColor(named: "objectUnHighlightedColor")
cell.mainTitle?.textColor = UIColor(named: "labelUnHighlighted")
cell.detailTitle?.textColor = UIColor(named: "labelUnHighlighted")
cell.chevronImage?.tintColor = .systemMint
}
}
}
This is the AudioPlayerViewController
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class MyVolumeView: MPVolumeView {
override func volumeSliderRect(forBounds bounds: CGRect) -> CGRect {
// this will avoid the thumb x-offset issue
// while keeping the route button vertically aligned
return bounds.insetBy(dx: 12.0, dy: 0.0).offsetBy(dx: -12.0, dy: -5.0)
}
}
class AudioPlayerViewController: UIViewController {
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
private lazy var audioImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .scaleAspectFill
v.clipsToBounds = true
v.layer.cornerRadius = 8
return v
}()
private lazy var subjectNameLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.font = .systemFont(ofSize: 14, weight: .bold)
return v
}()
private lazy var lessonNameLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.textAlignment = .center
v.font = .systemFont(ofSize: 12, weight: .light)
return v
}()
private lazy var progressBar: UISlider = {
let v = UISlider()
v.translatesAutoresizingMaskIntoConstraints = false
v.minimumTrackTintColor = UIColor(named: "subtitleColor")
v.isContinuous = false
return v
}()
private lazy var elapsedTimeLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = .systemFont(ofSize: 14, weight: .light)
v.text = "00:00"
return v
}()
private lazy var remainingTimeLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.font = .systemFont(ofSize: 14, weight: .light)
v.text = "00:00"
return v
}()
let previousButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 20)
v.setImage(UIImage(systemName: "backward.fill", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let seekBackwardButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 30)
v.setImage(UIImage(systemName: "gobackward.15", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let playPauseButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 50)
v.setImage(UIImage(systemName: "pause.fill", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let seekForwardButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 30)
v.setImage(UIImage(systemName: "goforward.15", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
let nextButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
let config = UIImage.SymbolConfiguration(pointSize: 20)
v.setImage(UIImage(systemName: "forward.fill", withConfiguration: config), for: .normal)
v.tintColor = .white
return v
}()
private lazy var controlStack: UIStackView = {
let v = UIStackView(arrangedSubviews: [previousButton, seekBackwardButton, playPauseButton, seekForwardButton, nextButton])
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.distribution = .equalSpacing
v.spacing = 20
return v
}()
let volumeViewContainer: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let volumeView = MyVolumeView()
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGesture(gesture:)))
self.progressBar.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)
progressBar.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.progressBar.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
}
}
//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")
}
holder.applyBlurEffect()
audioImageView.image = mainImage
holder.addSubview(audioImageView)
holder.addSubview(subjectNameLabel)
holder.addSubview(lessonNameLabel)
holder.addSubview(elapsedTimeLabel)
holder.addSubview(remainingTimeLabel)
holder.addSubview(controlStack)
holder.addSubview(progressBar)
holder.addSubview(volumeViewContainer)
volumeViewContainer.addSubview(volumeView)
NSLayoutConstraint.activate([
audioImageView.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
audioImageView.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
audioImageView.topAnchor.constraint(equalTo: holder.topAnchor, constant: 20),
audioImageView.heightAnchor.constraint(equalToConstant: holder.bounds.width - 40)
])
NSLayoutConstraint.activate([
subjectNameLabel.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
subjectNameLabel.bottomAnchor.constraint(equalTo: lessonNameLabel.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
lessonNameLabel.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
lessonNameLabel.bottomAnchor.constraint(equalTo: progressBar.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
progressBar.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
progressBar.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
progressBar.bottomAnchor.constraint(equalTo: remainingTimeLabel.topAnchor, constant: -3),
])
NSLayoutConstraint.activate([
elapsedTimeLabel.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
elapsedTimeLabel.bottomAnchor.constraint(equalTo: controlStack.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
remainingTimeLabel.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
remainingTimeLabel.bottomAnchor.constraint(equalTo: controlStack.topAnchor, constant: -8),
])
NSLayoutConstraint.activate([
controlStack.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 32),
controlStack.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -32),
controlStack.bottomAnchor.constraint(equalTo: volumeViewContainer.topAnchor, constant: -16),
controlStack.heightAnchor.constraint(equalToConstant: 60)
])
NSLayoutConstraint.activate([
volumeViewContainer.leadingAnchor.constraint(equalTo: holder.leadingAnchor, constant: 20),
volumeViewContainer.trailingAnchor.constraint(equalTo: holder.trailingAnchor, constant: -20),
volumeViewContainer.bottomAnchor.constraint(equalTo: holder.bottomAnchor, constant: -75),
volumeViewContainer.heightAnchor.constraint(equalToConstant: 30.0),
])
lessonNameLabel.text = song.name
subjectNameLabel.text = song.albumName
progressBar.addTarget(self, action: #selector(progressScrubbed(_:)), for: .valueChanged)
volumeViewContainer.addObserver(self, forKeyPath: "bounds", context: nil)
playPauseButton.addTarget(self, action: #selector(didTapPlayPauseButton), for: .touchUpInside)
previousButton.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)
}
#objc func panGesture(gesture:UIPanGestureRecognizer) {
let currentPoint = gesture.location(in: progressBar)
let percentage = currentPoint.x/progressBar.bounds.size.width;
let delta = Float(percentage) * (progressBar.maximumValue - progressBar.minimumValue)
let value = progressBar.minimumValue + delta
progressBar.setValue(value, animated: true)
}
#objc func progressScrubbed(_ :UISlider) {
let seconds : Int64 = Int64(progressBar.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 = mainImage {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { size in
return image
}
}
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playerItem?.duration
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player?.rate
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func updateNowPlaying(isPause: Bool) {
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo!
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.currentTime
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPause ? 0 : 1
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")
}
}
}
}
func stringFromTimeInterval(interval: TimeInterval) -> String {
let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60) % 60
let timeFormatter = NumberFormatter()
timeFormatter.minimumIntegerDigits = 2
timeFormatter.minimumFractionDigits = 0
timeFormatter.roundingMode = .down
guard let minsString = timeFormatter.string(from: NSNumber(value: minutes)),
let secStr = timeFormatter.string(from: NSNumber(value: seconds)) else {
return "00:00"
}
return "\(minsString):\(secStr)"
}
#objc func didTapPlayPauseButton() {
if player?.timeControlStatus == .playing {
//pause
player?.pause()
//show play button
let config = UIImage.SymbolConfiguration(pointSize: 50)
playPauseButton.setImage(UIImage(systemName: "play.fill", withConfiguration: config), for: .normal)
//shrink image
// UIView.animate(withDuration: 0.2, animations: {
// self.audioImageView.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
let config = UIImage.SymbolConfiguration(pointSize: 50)
playPauseButton.setImage(UIImage(systemName: "pause.fill", withConfiguration: config), for: .normal)
//increase image size
// UIView.animate(withDuration: 0.4, animations: {
// self.audioImageView.frame = CGRect(x: 20,
// y: 20,
// width: self.holder.frame.size.width - 40,
// height: self.holder.frame.size.width - 40)
// })
}
}
#objc func didTapBackButton() {
if position > 0 {
position = position - 1
player?.pause()
for subview in holder.subviews {
subview.removeFromSuperview()
}
configure()
}
}
#objc func didTapNextButton() {
if position < (paragraphs.count - 1) {
position = position + 1
player?.pause()
for subview in holder.subviews {
subview.removeFromSuperview()
}
configure()
}
}
#objc func seekBackwardButtonTapped(){
if player == nil { return }
let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
var newTime = playerCurrentTime - seekDuration
if newTime < 0 { newTime = 0 }
player?.pause()
let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
player?.seek(to: selectedTime)
player?.play()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
player?.play()
UIApplication.shared.isIdleTimerDisabled = true
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
player?.pause()
UIApplication.shared.isIdleTimerDisabled = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "bounds" {
// make sure we're getting notified of the MyVolumeView container view
if let cv = object as? UIView,
let mpv = cv.subviews.first as? MyVolumeView {
// set MyVolumeView frame to container view's bounds
// and offset its y-position by 4-points (because of its buggy layout)
mpv.frame = cv.bounds.offsetBy(dx: 0.0, dy: 4.0)
}
}
}
}
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.
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
}
}
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]
This line causes the bug, viewModel.image will always be the last download one.
self.viewModel.image = cell?.audioImage.image
you can simply pass the image URL to the next VC
vc.mainImage = viewModel.audios[indexPath.row].audioImageName
then use SDWebImage to load this image from the url
I tried the other method of SDWebImage and in didselectRowAt according to suggestion of Rahul Nimje. Thanks to him it worked. Dear Rahul, how will it affect on weight of the app, is it the correct way?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let position = indexPath.row
let song = viewModel.audios[indexPath.row]
let imageURL = song.audioImageName
SDWebImageManager.shared.loadImage(
with: URL(string: imageURL),
options: .continueInBackground, // or .highPriority
progress: nil,
completed: { [weak self] (image, data, error, cacheType, finished, url) in
guard let self = self else { return }
if let err = error {
// Do something with the error
return
}
guard let img = image else {
// No image handle this error
return
}
// Do something with image
self.viewModel.image = image
})
I have a tableView showing multiple tasks already created by the user. When the user click on a task (tableView cell) i want to present a pop-up showing more info about the user's task. I already created the popUp view and it's showing fine but the data (category, date, hour) is not showing inside the popUp. I don't know how to access my data and put it into the pop-up View when the user click on the row. Here's what i tried so far :
MyTasksCollectionCell
enum DisplayedTasks {
case current
case past
}
class MyTasksCollectionCell: UICollectionViewCell, UITableViewDelegate, UITableViewDataSource {
var displayedTasks = DisplayedTasks.current
var tasks = [Add]()
var pastTasks = [Add]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! MyTasksTableCell
cell.accessoryType = .disclosureIndicator
let task = {() -> Add in
switch (displayedTask) {
case .current:
// First segment tapped
return self.tasks[indexPath.row]
case past:
// Second segment tapped
return self.pastTasks[indexPath.row]
}
}()
cell.categoryLabel.text =
"\(task.category)"
cell.dateLabel.text =
"\(task.date)"
cell.hourLabel.text =
"\(task.hour)"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let task = {() -> Add in
switch (displayedTask) {
case .current:
// First segment tapped
return self.tasks[indexPath.row]
case past:
// Second segment tapped
return self.pastTasks[indexPath.row]
}
}()
let selectedCell = tableView.cellForRow(at: indexPath) as? MyTasksDetailView//Warning message here !!! "Cast from 'UITableViewCell?' to unrelated type 'MyTasksDetailView' always fails"
selectedCell?.category.text = "\(task.category)"
selectedCell?.hour.text = "\(task.hour)"
selectedCell?.date.text = "\(task.date)"
print (selectedCell?.category.text)
let popupView = MyTasksDetailView()
UIApplication.shared.keyWindow?.addSubview(popupView)
}
extension UIApplication {
var keyWindow: UIWindow? {
// Get connected scenes
return UIApplication.shared.connectedScenes
// Keep only active scenes, onscreen and visible to the user
.filter { $0.activationState == .foregroundActive }
// Keep only the first `UIWindowScene`
.first(where: { $0 is UIWindowScene })
// Get its associated windows
.flatMap({ $0 as? UIWindowScene })?.windows
// Finally, keep only the key window
.first(where: \.isKeyWindow)
}
}
MyTasksDetailView
class MyTasksDetailView: UIView {
var setCategory: String? {
didSet {
categoryLabel.text = setCategory ?? ""
}
}
var setDate: String? {
didSet {
dateLabel.text = setDate ?? ""
}
}
var setHour: String? {
didSet {
hourLabel.text = setHour ?? ""
}
}
let categoryLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 28, weight: .bold)
// label.text = "category"
label.textAlignment = .center
label.textColor = .label
return label
}()
let dateLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
// label.text = "date"
label.textAlignment = .center
label.numberOfLines = 3
label.textColor = .label
return label
}()
let hourLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
// label.text = "hour"
label.textAlignment = .center
label.numberOfLines = 3
label.textColor = .label
return label
}()
let container: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.clipsToBounds = true
v.backgroundColor = .white
v.layer.cornerRadius = 24
v.backgroundColor =
// 1
UIColor { traitCollection in
// 2
switch traitCollection.userInterfaceStyle {
case .dark:
// 3
v.layer.borderColor = UIColor.label.cgColor
return UIColor.systemBackground
default:
// 4
v.layer.borderColor = UIColor.black.cgColor
return UIColor.systemBackground
}
}
return v
}()
lazy var stack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [categoryLabel, dateLabel, hourLabel])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
return stack
}()
#objc func animateOut() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.container.transform = CGAffineTransform(translationX: 0, y: -self.frame.height)
self.alpha = 0
}) { (complete) in
if complete {
self.removeFromSuperview()
}
}
}
#objc func animateIn() {
self.container.transform = CGAffineTransform(translationX: 0, y: -self.frame.height)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.container.transform = .identity
self.alpha = 1
})
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(animateOut)))
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = bounds
blurEffectView.alpha = 1
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addSubview(blurEffectView)
self.frame = UIScreen.main.bounds
self.addSubview(container)
container.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
container.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
container.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7).isActive = true
container.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.45).isActive = true
container.addSubview(stack)
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true
stack.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.5).isActive = true
animateIn()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Add (DataStruct)
struct Add {
static var details: Add = Add()
var category: String = ""
var date: String = ""
var hour: String = ""
var id: String?
func getDict() -> [String: Any] {
let dict = ["category": self.category,
"date": self.date,
"hour": self.hour,
] as [String : Any]
return dict
}
}
I can't see where you set needed data to your MyTasksDetailView.
Add property "task" to MyTasksDetailView (cant find type of your task instance so in my example I used Task)
internal var task: Task? {
didSet {
if let task = task {
setCategory = task.category
setDate = task.date
setHour = task.hour
}
}
}
And set it after init
let popupView = MyTasksDetailView()
popupView.task = task
UIApplication.shared.keyWindow?.addSubview(popupView)
Implementing a protocol/delegate pattern as suggested by expert #DonMag, i am unable to get the code working...
I show a ratings window like below and when the user changes the ratings , i want the ratings emoji to update and the controller to pop, i also use animation, now for view i have a separate ratings class and for controller a separate class,
The problem is this function gets tapped , i can detect it but it does not reach the protocol method
#objc func ratingButtonTapped(_ sender: UIButton) {
// print(sender.titleLabel?.text)
guard let t = sender.titleLabel?.text else {
return
}
ratingDelegate?.defineTheratings(t)
}
Controller class
import UIKit
import CoreData
protocol RatingsPresentation: class {
func defineTheratings(_ ratings: String)
}
class RatingsViewController: UIViewController, RatingsPresentation {
func defineTheratings(_ ratings: String) {
print("ratings seleced\(ratings)")
self.restaurant.rating = ratings
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
appDelegate.saveContext()
}
if self.presentedViewController != nil {
self.dismiss(animated: true, completion: nil)
}
else {
self.navigationController?.popViewController(animated: true)
}
}
var restaurant: Restaurant!
var rates = RatingsView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(rates.view)
//Delegates
let vc = RatingsView()
// set the rating delegate
vc.ratingDelegate = self
//
if let restaurantImage = restaurant.image {
rates.bkgImageView.image = UIImage(data: restaurantImage as Data)
}
rates.crossBtn.addTarget(self, action: #selector(closeRatings), for: .touchUpInside)
let animateCross = CGAffineTransform(translationX: 1000, y: 0)
rates.crossBtn.transform = animateCross
}
#objc func closeRatings() {
navigationController?.popViewController(animated: true)
}
}
View Class
import UIKit
class RatingsView: UIViewController {
var bkgImageView = UIImageView()
var crossBtn = UIButton()
var btnCollection: [UIButton] = []
weak var ratingDelegate: RatingsPresentation?
override func viewDidLoad() {
super.viewDidLoad()
let ratings: [String] = ["cool", "happy","love", "sad", "angry"]
let animationBlur = UIBlurEffect(style: .dark)
let visualBlurView = UIVisualEffectView(effect: animationBlur)
// add elements to self
view.addSubview(bkgImageView)
view.addSubview(visualBlurView)
bkgImageView.translatesAutoresizingMaskIntoConstraints = false
visualBlurView.translatesAutoresizingMaskIntoConstraints = false
bkgImageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
bkgImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
bkgImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
bkgImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// constrain visualBlurView to all 4 sides
visualBlurView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
visualBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
visualBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
visualBlurView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
bkgImageView.contentMode = .scaleAspectFill
// Do any additional setup after loading the view.
//Add stackView
let stackEmoji = UIStackView()
stackEmoji.translatesAutoresizingMaskIntoConstraints = false
stackEmoji.axis = .vertical
stackEmoji.spacing = 5
stackEmoji.distribution = .fill
stackEmoji.alignment = .top
let font = UIFont(name: "Rubik-Medium", size: 28)
let fontM = UIFontMetrics(forTextStyle: .body)
ratings.forEach { (btn) in
let b = UIButton()
b.setTitle(btn, for: .normal)
b.titleLabel?.font = fontM.scaledFont(for: font!)
b.setImage(UIImage(named: btn), for: .normal)
stackEmoji.addArrangedSubview(b)
btnCollection.append(b)
//btn animation
let sizeAnimation = CGAffineTransform(scaleX: 5, y: 5)
let positionAnimation = CGAffineTransform(translationX: 1000, y: 0)
let combinedAninaton = sizeAnimation.concatenating(positionAnimation)
b.transform = combinedAninaton
b.addTarget(self, action: #selector(ratingButtonTapped(_:)), for: .touchUpInside)
}
view.addSubview(stackEmoji)
stackEmoji.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackEmoji.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// if let img = UIImage(named: "cross") {
// crossBtn.setImage(img, for: [])
// }
crossBtn.setTitle("X", for: [])
crossBtn.setTitleColor(.white, for: .normal)
crossBtn.setTitleColor(.gray, for: .highlighted)
crossBtn.titleLabel?.font = UIFont.systemFont(ofSize: 44, weight: .bold)
view.addSubview(crossBtn)
crossBtn.translatesAutoresizingMaskIntoConstraints = false
crossBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -30).isActive = true
crossBtn.topAnchor.constraint(equalTo: view.topAnchor, constant: 150).isActive = true
}
#objc func ratingButtonTapped(_ sender: UIButton) {
// print(sender.titleLabel?.text)
guard let t = sender.titleLabel?.text else {
return
}
ratingDelegate?.defineTheratings(t)
}
}
Problem is here
var rates = RatingsView() //// shown 1
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(rates.view) /////// you add this as a subview
//Delegates
let vc = RatingsView() //////// this is useless - not shown 1
// set the rating delegate
vc.ratingDelegate = self ////// and set the delegate for this non-shown
So you need to remove let vc = RatingsView() and set the delegate for
rates.ratingDelegate = self
I know this question has been asked lots of times but none of solutions worked for me.
I have a custom UIView class which I use for displaying alert message. I added UIButton to close the view. However, nothing happens when I tab it.
import UIKit
public class Alert: UIView {
public var image: UIImage?
public var title: String?
public var message: String?
public var closeButtonText: String?
public var dialogBackgroundColor: UIColor = .white
public var dialogTitleTextColor: UIColor = .black
public var dialogMessageTextColor: UIColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
public var dialogImageColor: UIColor = UIColor(red:0.47, green:0.72, blue:0.35, alpha:1.0)
public var overlayColor: UIColor = .black
public var overlayOpacity: CGFloat = 0.66
public var paddingSingleTextOnly: CGFloat = 8
public var paddingTopAndBottom: CGFloat = 24
public var paddingFromSides: CGFloat = 8
public var seperatorHeight: CGFloat = 6
private var height: CGFloat = 0
private var width: CGFloat = 0
private var maxSize: CGSize = CGSize()
private let marginFromSides: CGFloat = 80
public lazy var imageSize: CGSize = CGSize(width: 75, height: 75)
public var overlay = false
public var blurOverlay = true
//animation duration
public var duration = 0.33
private var onComplete: (() -> Void)?
#objc public var titleFont: UIFont = UIFont.systemFont(ofSize: 18)
#objc public var messageFont: UIFont = UIFont.systemFont(ofSize: 15)
private lazy var backgroundView: UIView = {
let view = UIView()
view.alpha = 0
return view
}()
public let dialogView: UIView = {
let view = UIView()
view.layer.cornerRadius = 6
view.layer.masksToBounds = true
view.alpha = 0
view.clipsToBounds = true
return view
}()
private lazy var imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFit
return view
}()
public lazy var closeButton: UIButton = {
let button = UIButton()
return button
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
private lazy var messageLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
#objc func closeButtonTapped(sender: UIButton){
dismiss()
}
private func calculations() {
height += paddingTopAndBottom
maxSize = CGSize(width: frame.width - marginFromSides * 2, height: frame.height - marginFromSides)
}
public convenience init(title:String, message: String, image:UIImage) {
self.init(frame: UIScreen.main.bounds)
self.title = title
self.message = message
self.image = image
}
public convenience init(title:String, image:UIImage) {
self.init(frame: UIScreen.main.bounds)
self.title = title
self.image = image
}
public convenience init(title: String, message: String) {
self.init(frame: UIScreen.main.bounds)
self.title = title
self.message = message
}
public convenience init(message: String) {
self.init(frame: UIScreen.main.bounds)
paddingTopAndBottom = paddingSingleTextOnly
paddingFromSides = paddingSingleTextOnly * 2
self.message = message
}
override init(frame: CGRect) {
super.init(frame: frame)
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func createOverlay() {
backgroundView.frame = frame
backgroundView.backgroundColor = overlayColor
backgroundView.isUserInteractionEnabled = true
addSubview(backgroundView)
if let window = UIApplication.shared.keyWindow {
window.addSubview(backgroundView)
} else if let window = UIApplication.shared.delegate?.window??.rootViewController {
window.view.addSubview(self)
}
}
private func createBlurOverlay() {
backgroundView.frame = frame
//Blur Effect
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = frame
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
backgroundView.addSubview(blurEffectView)
addSubview(backgroundView)
if let window = UIApplication.shared.keyWindow {
window.addSubview(backgroundView)
} else if let window = UIApplication.shared.delegate?.window??.rootViewController {
window.view.addSubview(self)
}
}
private func createTitle(title: String) {
titleLabel.font = titleFont
titleLabel.text = title
titleLabel.frame.origin.y = height + 2
let titleLabelSize = titleLabel.sizeThatFits(maxSize)
handleSize(size: titleLabelSize)
titleLabel.frame.size = titleLabelSize
titleLabel.textColor = self.dialogTitleTextColor
dialogView.addSubview(titleLabel)
}
private func createMessage(message: String) {
messageLabel.font = messageFont
messageLabel.text = message
messageLabel.frame.origin.y = height
let messageLabelSize = messageLabel.sizeThatFits(maxSize)
messageLabel.frame.size = messageLabelSize
messageLabel.textColor = self.dialogMessageTextColor
handleSize(size: messageLabelSize)
dialogView.addSubview(messageLabel)
}
private func createImage(image: UIImage) {
imageView.image = image.withRenderingMode(.alwaysTemplate)
imageView.frame.origin.y = height
imageView.frame.size = imageSize
imageView.tintColor = self.dialogImageColor
handleSize(size: imageSize)
dialogView.addSubview(imageView)
}
private func createButton(){
closeButton.setTitle("Close", for: .normal)
closeButton.tintColor = UIColor.white
closeButton.frame.origin.y = height + 20
let closeButtonSize = CGSize(width: width - 60, height: 60)
closeButton.frame.size = closeButtonSize
closeButton.layer.cornerRadius = 6
closeButton.backgroundColor = Color.NavigationBar.tintColor
closeButton.isUserInteractionEnabled = true
handleSize(size: closeButtonSize)
dialogView.addSubview(closeButton)
}
private func createDialog() {
centerAll()
height += paddingTopAndBottom
dialogView.frame.size = CGSize(width: width, height: height)
dialogView.backgroundColor = self.dialogBackgroundColor
dialogView.isUserInteractionEnabled = true
addSubview(dialogView)
self.dialogView.center = self.center
self.dialogView.transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
if let window = UIApplication.shared.keyWindow {
window.addSubview(dialogView)
closeButton.addTarget(self, action: #selector(closeButtonTapped(sender:)), for: .touchUpInside)
} else if let window = UIApplication.shared.delegate?.window??.rootViewController {
UIApplication.topViewController()?.view.addSubview(self)
window.view.addSubview(self)
closeButton.addTarget(self, action: #selector(closeButtonTapped(sender:)), for: .touchUpInside)
}
}
private func centerAll() {
if ((messageLabel.text) != nil) {
messageLabel.frame.origin.x = (width - messageLabel.frame.width) / 2
}
if ((titleLabel.text) != nil) {
titleLabel.frame.origin.x = (width - titleLabel.frame.width) / 2
}
if ((imageView.image) != nil) {
imageView.frame.origin.x = (width - imageView.frame.width) / 2
}
closeButton.frame.origin.x = (width - closeButton.frame.width) / 2
}
private func handleSize(size: CGSize) {
if width < size.width + paddingFromSides * 2 {
width = size.width + paddingFromSides * 2
}
if paddingTopAndBottom != paddingSingleTextOnly {
height += seperatorHeight
}
height += size.height
}
private func showAnimation() {
UIView.animate(withDuration: duration, animations: {
if self.overlay {
self.backgroundView.alpha = self.overlayOpacity
self.dialogView.transform = CGAffineTransform(scaleX: 1, y: 1)
}
self.dialogView.alpha = 1
})
}
public func show() {
if let complete = onComplete {
self.onComplete = complete
}
calculations()
if self.overlay {
if blurOverlay {
createBlurOverlay()
} else {
createOverlay()
}
}
if let img = image {
createImage(image: img)
}
if let title = title {
createTitle(title: title)
}
if let message = message {
createMessage(message: message)
}
createButton()
createDialog()
showAnimation()
}
public func dismiss(){
UIView.animate(withDuration: duration, animations: {
if self.overlay {
self.backgroundView.alpha = 0
}
self.dialogView.transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
self.dialogView.alpha = 0
}, completion: { (completed) in
self.dialogView.removeFromSuperview()
if (self.overlay)
{
self.backgroundView.removeFromSuperview()
}
self.removeFromSuperview()
if let completionHandler = self.onComplete {
completionHandler()
}
})
}
}
How I create the alert;
let alert = Alert(title: "hata",message: "hata mesajı ekrana basıldı", image: #imageLiteral(resourceName: "error"))
alert.show()
If I declare target inside UIViewController (Where I create this UIView) as
Alert.closeButton.addTarget(self, action: #selector(closeButtonTapped(sender:), for: .touchUPInside)
and create function inside UIViewController It is working. I can't figure out why It doesn't work when in custom class.
So my question is that how can close the alert view when tabbed the button?
I tried below solution but didn't work for me;
UIButton target action inside custom class
Assuming these lines are inside a function - such as due to a button tap:
#IBAction func didTap(_ sender: Any) {
let alert = Alert(title: "hata",message: "hata mesajı ekrana basıldı", image: #imageLiteral(resourceName: "error"))
alert.show()
}
You are creating an instance of your Alert class, calling the .show() function inside it, and then it goes out of scope.
So, as soon as that function exists, alert no longer exists, and no code inside it can run.
You need to have a class-level variable to hold onto it while it is displayed:
class MyViewController: UIViewController {
var alert: Alert?
#IBAction func didTap(_ sender: Any) {
alert = Alert(title: "hata",message: "hata mesajı ekrana basıldı", image: #imageLiteral(resourceName: "error"))
alert?.show()
}
}
Here is a demonstration of the "Wrong Way" and the "Right Way" to handle your Alert view: https://github.com/DonMag/EmreTest
Look this code
1- Call IBAction inside UIView Class
import UIKit
public class Alert: UIView {
public lazy var closeButton: UIButton = {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
button.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
return button
}()
func createDialog() {
closeButton.addTarget(self, action: #selector(self.closeButtonTapped(sender:)), for: .touchUpInside)
self.addSubview(closeButton)
}
#objc func closeButtonTapped(sender: UIButton){
print("Call 1")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let alert = Alert(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
alert.createDialog()
self.view.addSubview(alert)
}
}
2- Call IBAction inside UIViewController Class
import UIKit
public class Alert: UIView {
public lazy var closeButton: UIButton = {
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
button.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
return button
}()
func createDialog() {
// closeButton.addTarget(self, action: #selector(self.closeButtonTapped(sender:)), for: .touchUpInside)
self.addSubview(closeButton)
}
#objc func closeButtonTapped(sender: UIButton){
print("Call 1")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let alert = Alert(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
alert.createDialog()
alert.closeButton.addTarget(self, action: #selector(self.closeButtonTappedController(_:)), for: .touchUpInside)
self.view.addSubview(alert)
}
#IBAction func closeButtonTappedController(_ sender:UIButton){
print("Call 2")
}
}