I'm trying to set the image size for a button to match the size of the button. To get the actual size of the button, I do this in the layoutSubviews() method. But I ran into the following problem.
Calling self.setImage(imagePerson, for: .normal) causes layoutSubviews() to be called again and recursion occurs.
class CustomButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.setTitle("", for: .normal)
self.tintColor = .white
self.backgroundColor = .lightGray.withAlphaComponent(0.5)
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.size.height/2
let size = self.frame.height * 0.5
let config = UIImage.SymbolConfiguration(pointSize: size)
let imagePerson = UIImage(systemName: "person.fill",
withConfiguration: config)
let imageSafary = UIImage(systemName: "safari",
withConfiguration: config)
switch restorationIdentifier {
case "1": self.setImage(imagePerson, for: .normal)
case "2": self.setImage(imageSafary, for: .normal)
default: break
}
}
}
You can use a property to track the change in the size of the button.
Something like this:
class CustomButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
self.setTitle("", for: .normal)
self.tintColor = .white
self.backgroundColor = .lightGray.withAlphaComponent(0.5)
}
// to track the size change
var curWidth: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
// only do this if the size has changed
if curWidth != bounds.width {
curWidth = bounds.width
self.layer.cornerRadius = self.frame.size.height/2
let size = self.frame.height * 0.5
let config = UIImage.SymbolConfiguration(pointSize: size)
let imagePerson = UIImage(systemName: "person.fill",
withConfiguration: config)
let imageSafary = UIImage(systemName: "safari",
withConfiguration: config)
switch restorationIdentifier {
case "1": self.setImage(imagePerson, for: .normal)
case "2": self.setImage(imageSafary, for: .normal)
default: break
}
}
}
}
Related
inputAccessoryView's background view is falling under its own textField and profile picture imageView.
It works fine on regular screen iPhones, but on new iPhones with notches it looks like this:
Here's how it looks animated when keyboard appears: Transition animation on becomeFirstResponder()
Here's my tableView in which I'm trying to add accessoryView:
import UIKit
import SDWebImage
class CommentsTableViewController: UITableViewController {
let viewModel = CommentsViewModel()
let postID: String
let postCaption: String
let postDate: Date
let postAuthor: ZoogramUser
var keyboardAccessoryView: CommentAccessoryView = {
let commentAccessoryView = CommentAccessoryView()
return commentAccessoryView
}()
init(post: UserPost) {
self.postID = post.postID
self.postCaption = post.caption
self.postDate = post.postedDate
self.postAuthor = post.author
super.init(style: .grouped)
self.tableView.register(PostCommentsTableViewCell.self, forCellReuseIdentifier: PostCommentsTableViewCell.identifier)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Comments"
keyboardAccessoryView.delegate = self
configureKeyboardAccessoryView()
viewModel.getComments(for: self.postID) {
self.tableView.reloadData()
}
tableView.backgroundColor = .systemBackground
tableView.keyboardDismissMode = .interactive
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 100
tableView.allowsSelection = false
tableView.separatorStyle = .none
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
}
override var inputAccessoryView: UIView? {
keyboardAccessoryView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
keyboardAccessoryView.backgroundColor = .systemOrange
return keyboardAccessoryView
}
override var canBecomeFirstResponder: Bool {
return true
}
func configureKeyboardAccessoryView() {
guard let photoURL = AuthenticationManager.shared.getCurrentUserProfilePhotoURL() else {
return
}
keyboardAccessoryView.userProfilePicture.sd_setImage(with: photoURL)
}
}
And here's code for my CommentAccessoryView which I use to override inputAccessoryView:
import UIKit
protocol CommentAccessoryViewProtocol {
func postButtonTapped(commentText: String)
}
class CommentAccessoryView: UIView {
var delegate: CommentAccessoryViewProtocol?
var userProfilePicture: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.backgroundColor = .secondarySystemBackground
imageView.contentMode = .scaleAspectFill
return imageView
}()
var commentTextField: AccessoryViewTextField = {
let textField = AccessoryViewTextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.backgroundColor = .systemBackground
textField.placeholder = "Enter comment"
textField.clipsToBounds = true
textField.layer.borderWidth = 1
textField.layer.borderColor = UIColor.placeholderText.cgColor
return textField
}()
var postButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 30).isActive = true
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
button.clipsToBounds = true
button.layer.cornerRadius = 30/2
button.setImage(UIImage(systemName: "arrow.up.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 35)), for: .normal)
button.tintColor = .systemBlue
button.addTarget(self, action: #selector(didTapPostButton), for: .touchUpInside)
return button
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupConstraints()
backgroundColor = .systemBackground
commentTextField.rightView = postButton
commentTextField.rightViewMode = .always
autoresizingMask = .flexibleHeight
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
setViewCornerRadius()
}
func setViewCornerRadius() {
userProfilePicture.layer.cornerRadius = userProfilePicture.frame.height / 2
commentTextField.layer.cornerRadius = commentTextField.frame.height / 2
}
func setupConstraints() {
self.addSubviews(userProfilePicture, commentTextField)
NSLayoutConstraint.activate([
userProfilePicture.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
userProfilePicture.centerYAnchor.constraint(equalTo: self.safeAreaLayoutGuide.centerYAnchor),
userProfilePicture.widthAnchor.constraint(equalToConstant: 40),
userProfilePicture.heightAnchor.constraint(equalToConstant: 40),
commentTextField.leadingAnchor.constraint(equalTo: userProfilePicture.trailingAnchor, constant: 10),
commentTextField.centerYAnchor.constraint(equalTo: userProfilePicture.centerYAnchor),
commentTextField.heightAnchor.constraint(equalToConstant: 40),
commentTextField.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
])
}
override var intrinsicContentSize: CGSize {
return CGSize.zero
}
#objc func didTapPostButton() {
guard let text = commentTextField.text else {
return
}
commentTextField.resignFirstResponder()
delegate?.postButtonTapped(commentText: text)
}
}
I've spent days trying to google a fix for that but nothing helps.
There were posts saying they were able to fix something similar by setting customView's bottom constraint to a safe area with the following method:
override func didMoveToWindow() {
if #available(iOS 11.0, *) {
if let window = window {
let bottomAnchor = bottomAnchor.constraint(lessThanOrEqualToSystemSpacingBelow: window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1.0)
bottomAnchor.isActive = true
}
}
}
But when I use it, AutoLayout starts complaining.
UPDATE: I did what HangarRash recommended, changed CommentAccessoryView from UIView to UIInputView and centering profileImageView and textField to view itself and not to safe area. Now it's a little bit better, but seems to ignore safe area, inputAccessoryView should be above Home indicator but lies beneath it instead. Looking at last cell in TableView and Scroll indicator, it seems like TableView also isn't aware of inputAccessoryView and goes under it.
I realized a Custom Segment Control with UIView, only the first button works.
When I click on the other buttons, they don't change color, and the first button stays with the active background color.
I would like to give the buttons an on / off effect, but only the first button remains on.
I believe the problem is inside the method:
func didSelectButton (at index: Int) {
but I can't find what's missing
import UIKit
#IBDesignable class CustomSegmentedView: UIView {
#IBInspectable var selectedBackgroundColor = Colors.colorViolet1 {
didSet {
self.slideView.backgroundColor = selectedBackgroundColor
}
}
#IBInspectable var selectedTextColor: UIColor = UIColor.white
#IBInspectable var buttonText1: String = ConstantFile.buttonMonitoring {
didSet {
self.buttonTitles[0] = buttonText1
}
}
#IBInspectable var buttonText2: String = ConstantFile.buttonAlert {
didSet {
self.buttonTitles[1] = buttonText2
}
}
#IBInspectable var buttonText3: String = ConstantFile.buttonActivities {
didSet {
self.buttonTitles[2] = buttonText3
}
}
#IBInspectable var buttonText4: String = ConstantFile.buttonSettings {
didSet {
self.buttonTitles[3] = buttonText4
}
}
#IBInspectable var image1: UIImage = UIImage() {
didSet {
self.buttonImages[0] = image1
}
}
#IBInspectable var image2: UIImage = UIImage() {
didSet {
self.buttonImages[1] = image2
}
}
#IBInspectable var image3: UIImage = UIImage() {
didSet {
self.buttonImages[2] = image3
}
}
#IBInspectable var image4: UIImage = UIImage() {
didSet {
self.buttonImages[3] = image4
}
}
#IBInspectable var image1Selected: UIImage = UIImage() {
didSet {
self.buttonSelectImages[0] = image1Selected
}
}
#IBInspectable var image2Selected: UIImage = UIImage() {
didSet {
self.buttonSelectImages[1] = image2Selected
}
}
#IBInspectable var image3Selected: UIImage = UIImage() {
didSet {
self.buttonSelectImages[2] = image3Selected
}
}
#IBInspectable var image4Selected: UIImage = UIImage() {
didSet {
self.buttonSelectImages[3] = image4Selected
}
}
#IBInspectable var startingIndex: Int = 0 {
didSet {
if startingIndex > 3 {
startingIndex = 3
self.didSelectButton(at: startingIndex)
} else if startingIndex < 0 {
startingIndex = 0
self.didSelectButton(at: startingIndex)
} else {
self.didSelectButton(at: startingIndex)
}
}
}
lazy var button1: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("", for: .normal)
button.titleLabel?.font = UIFont.fontHelveticaBold14
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0)
return button
}()
lazy var button2: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("", for: .normal)
button.titleLabel?.font = UIFont.fontHelveticaBold14
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0)
return button
}()
lazy var button3: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("", for: .normal)
button.titleLabel?.font = UIFont.fontHelveticaBold14
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0)
return button
}()
lazy var button4: UIButton = {
let button = UIButton(type: .custom)
button.setTitle("", for: .normal)
button.titleLabel?.font = UIFont.fontHelveticaBold14
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -15, bottom: 0, right: 0)
return button
}()
lazy var stackView: UIStackView = UIStackView(arrangedSubviews: [])
lazy var slideView: UIView = {
var view = UIView(frame: CGRect.zero)
view.backgroundColor = self.selectedBackgroundColor
return view
}()
private var currentIndex: Int = 0
private var buttons: [UIButton] = []
private lazy var buttonTitles:[String] = [buttonText1, buttonText2, buttonText3, buttonText4]
private lazy var buttonImages:[UIImage] = [image1, image2, image3, image4]
private lazy var buttonSelectImages:[UIImage] = [image1Selected, image2Selected, image3Selected, image4Selected]
var delegate: CustomSegmentedViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
self.buttons = [button1, button2, button3, button4]
self.buttonTitles = [buttonText1, buttonText2, buttonText3, buttonText4]
self.buttonImages = [image1, image2, image3, image4]
self.buttonSelectImages = [image1Selected, image2Selected, image3Selected, image4Selected]
button1.sizeToFit()
button2.sizeToFit()
button3.sizeToFit()
button4.sizeToFit()
stackView.addArrangedSubview(button1)
stackView.addArrangedSubview(button2)
stackView.addArrangedSubview(button3)
stackView.addArrangedSubview(button4)
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
stackView.alignment = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(slideView)
self.addSubview(stackView)
stackView.pinEdges(to: self)
button1.addTarget(self, action: #selector(CustomSegmentedView.buttonTapped(sender:)), for: .touchUpInside)
button2.addTarget(self, action: #selector(CustomSegmentedView.buttonTapped(sender:)), for: .touchUpInside)
button3.addTarget(self, action: #selector(CustomSegmentedView.buttonTapped(sender:)), for: .touchUpInside)
button4.addTarget(self, action: #selector(CustomSegmentedView.buttonTapped(sender:)), for: .touchUpInside)
button1.setTitle("", for: .normal)
button2.setTitle("", for: .normal)
button3.setTitle("", for: .normal)
button4.setTitle("", for: .normal)
}
override func layoutSubviews() {
super.layoutSubviews()
button1.setImage(buttonImages[0], for: .normal)
button2.setImage(buttonImages[1], for: .normal)
button3.setImage(buttonImages[2], for: .normal)
button4.setImage(buttonImages[3], for: .normal)
self.setupFirstSelection()
}
#objc func buttonTapped(sender :UIButton!) {
switch sender {
case button1:
didSelectButton(at: 0)
break
case button2:
didSelectButton(at: 1)
break
case button3:
didSelectButton(at: 2)
break
case button4:
didSelectButton(at: 3)
break
default:
break
}
}
func didSelectButton(at index: Int) {
self.delegate?.didSelectPage(index: index)
let oldButton = self.buttons[self.currentIndex]
let newButton = self.buttons[index]
newButton.alpha = 0.0
oldButton.setImage(self.buttonImages[self.currentIndex], for: .normal)
newButton.setImage(self.buttonSelectImages[index], for: .normal)
UIView.animate(withDuration: 0.1) {
oldButton.setTitle("", for: .normal)
newButton.setTitle(self.buttonTitles[index], for: .normal)
self.stackView.layoutSubviews()
self.layoutIfNeeded()
newButton.alpha = 1.0
}
UIView.animate(withDuration: 0.2, delay: 0, options: [], animations: {
self.slideView.frame = newButton.frame
self.layoutIfNeeded()
}, completion: nil)
self.currentIndex = index
}
func setupFirstSelection() {
let index = self.startingIndex
let newButton = self.buttons[index]
newButton.setTitle(self.buttonTitles[index], for: .normal)
newButton.setImage(self.buttonSelectImages[index], for: .normal)
self.stackView.layoutSubviews()
self.slideView.frame = newButton.frame
self.slideView.layer.cornerRadius = self.slideView.frame.height/2.0
self.currentIndex = index
}
}
extension UIView {
func pinEdges(to other: UIView) {
leadingAnchor.constraint(equalTo: other.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: other.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: other.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive = true
}
}
protocol CustomSegmentedViewDelegate {
func didSelectPage(index: Int)
}
issue is in your layoutSubviews() you call every time when you call layoutSubviews() this function automatically set first self.setupFirstSelection()
replace layoutSubviews() by commenting self.setupFirstSelection()
override func layoutSubviews() {
super.layoutSubviews()
button1.setImage(buttonImages[0], for: .normal)
button2.setImage(buttonImages[1], for: .normal)
button3.setImage(buttonImages[2], for: .normal)
button4.setImage(buttonImages[3], for: .normal)
//self.setupFirstSelection()
}
This is audio player, It worked fine before I started to try stream audios from firebase. Now data is downloaded from the firebase database, the image, title and subtitle are being displayed correctly, but when user taps on the cell the audio doesn't play and it throws a fatal error, Thread 1: Fatal error: Index out of range
This is a table view controller which lists the audio tracks from firebase database.
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 cancellable: AnyCancellable?
private var viewModel = AudiosViewModel()
var paragraphs = [Audio]()
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.fetchData()
self.title = "Audio Lessons"
table.delegate = self
table.dataSource = self
cancellable = viewModel.$audios.sink { _ in
DispatchQueue.main.async{
self.table.reloadData()
}
} // Do any additional setup after loading the view.
}
// Table
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: "Cell", for: indexPath)
let audio = viewModel.audios[indexPath.row]
tableView.tableFooterView = UIView()
cell.textLabel?.text = audio.albumName
cell.detailTextLabel?.text = audio.name
cell.accessoryType = .disclosureIndicator
let imageURL = audio.audioImageName
cell.imageView?.sd_imageIndicator = SDWebImageActivityIndicator.gray
cell.imageView?.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))")
}
})
cell.textLabel?.numberOfLines = 0
cell.detailTextLabel?.numberOfLines = 0
let backgroundView = UIView()
backgroundView.backgroundColor = UIColor(named: "AudioLessonsCellHighlighted")
cell.selectedBackgroundView = backgroundView
cell.textLabel?.font = UIFont(name: "Helvetica-Bold", size: 14)
cell.detailTextLabel?.font = UIFont(name: "Helvetica", size: 12)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// present the player
let position = indexPath.row
//lessons
guard let vc = storyboard?.instantiateViewController(identifier: "AudioPlayer") as? AudioPlayerViewController else {
return
}
vc.paragraphs = paragraphs
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
}
}
}
import UIKit
import AVFoundation
import MediaPlayer
import AVKit
class AudioPlayerViewController: UIViewController {
public var position: Int = 0
public var paragraphs: [Audio] = []
#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.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 = true
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 = UIImage(named: song.audioImageName)
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)
})
}
}
private func setupView() {
setupConstraints()
}
}
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
}
}
AudioViewModel
import Foundation
import FirebaseFirestore
class AudiosViewModel: ObservableObject {
#Published var audios = [Audio]()
private var db = Firestore.firestore()
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 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 ?? ""
print(data)
return Audio(name: name, albumName: albumName, paragraphNumber: paragraphNumber, audioImageName: audioImageName, trackURL: trackURL)
}
}
}
}
Audio Structure
import Foundation
struct Audio {
let name: String
let albumName: String
let paragraphNumber: String
let audioImageName: String
let trackURL: String
}
Assign/append value to your paragraphs and also check your position if there will be no values/wrong position it'll throw index out of range
I created a DropDownMenu class, but I need to know when the value changed in the ViewController. Like a UIScrollView didScroll, but for the value change in my class.
func scrollViewDidScroll(_ scrollView: UIScrollView)
I need something like that, but for the class!
Here is the class...
class DropDownMenu: UIStackView {
var options: [String]! = [] // Labels for all of the options
var titleButton: UIButton! = UIButton() // The Main Title Button
var target: UIViewController! // The target to get the main view. Maybe remove and automatically do it later
var textColor: UIColor! = UIColor.black // The Color of the text of the options
var bgColor: UIColor! = UIColor.clear
var borderWidth: CGFloat! = 0.0
var borderColor: CGColor! = UIColor.black.cgColor
var uiBorderColor: UIColor? = nil
var cornerRadius: CGFloat! = 0.0
var font: UIFont! = UIFont.systemFont(ofSize: 18)
var images: [UIImage]? = nil
var imageInsets: [UIEdgeInsets]? = nil
var imageInset: UIEdgeInsets? = nil
var value: String! {
get {
return currentSelected
}
set {
currentSelected = newValue
}
}
private var currentSelected: String! = ""
init(options: [String]) {
self.options = options
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.axis = .vertical
}
init(titleButton: UIButton) {
self.titleButton = titleButton
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.axis = .vertical
}
func createDropDownMenu() {
currentSelected = titleButton.titleLabel?.text
let mainFrame = titleButton.frame
print("Frame: \(mainFrame)")
print("StackView frame: \(self.frame), axis: \(self.axis)")
if uiBorderColor != nil {
borderColor = uiBorderColor!.cgColor
}
self.widthAnchor.constraint(equalToConstant: mainFrame.width).isActive = true
self.leftAnchor.constraint(equalTo: titleButton.leftAnchor).isActive = true
self.topAnchor.constraint(equalTo: titleButton.bottomAnchor, constant: self.spacing).isActive = true
var y: CGFloat = 0
var place = 0
for title in self.options {
let button = UIButton(frame: CGRect(x: 0, y: y, width: mainFrame.width, height: mainFrame.height))
button.setTitle(title, for: .normal)
button.setTitleColor(textColor, for: .normal)
button.backgroundColor = bgColor
button.addTarget(self, action: #selector(dropDownOptionClicked(_:)), for: .touchUpInside)
button.layer.cornerRadius = cornerRadius
button.layer.borderWidth = borderWidth
button.layer.borderColor = borderColor
button.titleLabel?.font = font
if images != nil {
button.setBackgroundImage(images![place], for: .normal)
if imageInsets != nil {
button.imageEdgeInsets = imageInsets![place]
} else if imageInsets == nil && imageInset != nil{
button.imageEdgeInsets = imageInset!
} else {
button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}
print("Button: \(button), Title: \(String(describing: button.titleLabel?.text)), Target: \(button.allTargets)")
button.isHidden = true
button.alpha = 0
self.addArrangedSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: mainFrame.width).isActive = true
button.heightAnchor.constraint(equalToConstant: mainFrame.height).isActive = true
print("Subviews: \(self.arrangedSubviews)")
y += mainFrame.height
place += 1
print("y: \(y)")
}
}
#objc func openDropDown(_ sender: UIButton) {
print("Open DropDownMenu")
self.arrangedSubviews.forEach { (button) in
UIView.animate(withDuration: 0.7) {
button.isHidden = !button.isHidden
button.alpha = button.alpha == 0 ? 1 : 0
self.target.view.layoutIfNeeded()
}
}
}
#objc private func dropDownOptionClicked(_ sender: UIButton) {
let text = sender.titleLabel?.text
print(text as Any)
currentSelected = text
print("Value: \(String(describing: value))")
titleButton.setTitle(text, for: .normal)
openDropDown(sender)
}
init() {
super.init(frame: CGRect.zero)
self.translatesAutoresizingMaskIntoConstraints = false
self.axis = .vertical
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the creation in ViewController...
let titleButton = UIButton(frame: CGRect(x: 50, y: 290, width: 100, height: 40))
titleButton.backgroundColor = UIColor.white
titleButton.setTitle("Grade", for: .normal)
titleButton.setTitleColor(UIColor.black, for: .normal)
titleButton.layer.borderWidth = 2
titleButton.layer.cornerRadius = 10
let dp = DropDownMenu(options: ["1", "Freshman", "Sophomore", "Junior", "Senior", "College"])
dp.titleButton = titleButton
dp.target = self
dp.borderWidth = 2
dp.spacing = 5
dp.cornerRadius = 10
dp.bgColor = UIColor.white
titleButton.addTarget(dp, action: #selector(dp.openDropDown(_:)), for: .touchUpInside)
infoBox.addSubview(titleButton)
infoBox.addSubview(dp)
dp.createDropDownMenu()
The class works as expected.
I really need help on this. No answer is a bad one. This is all of my code.
You usually do that with a delegate:
class DropDownMenu: UIStackView {
weak var delegate: DropDownMenuDelegate?
var value: String! {
get {
return currentSelected
}
set {
if currentSelected != newValue {
currentSelected = newValue
self.delegate?.valueDidChange(self)
}
}
}
}
protocol DropDownMenuDelegate: class {
func valueDidChange(_ menu: DropDownMenu)
}
Then in your View Controller:
let dp = DropDownMenu(options: ["1", "Freshman", "Sophomore", "Junior", "Senior", "College"])
dp.delegate = self
(typing this in the blind as I'm away from Xcode so there may be syntax errors)
This's code,
slider = UISlider(frame: CGRect(x: 80,y: 9.5,width: UIScreen.main.bounds.width - 160,height: 15))
slider.minimumTrackTintColor = UIColor.orange
slider.isContinuous = false
slider.addTarget(self, action: #selector(pageChange), for: .valueChanged)
slider.setThumbImage(UIImage(named:"RM_3"), for: .normal)
bottomBar?.addSubview(slider)
How to clean up the left thumbs???
IN Swift 3.0
import UIKit
class DBSlider: UISlider {
#IBInspectable var thumbImage: UIImage?
#IBInspectable var minTrackColor:UIColor?
#IBInspectable var maxTrackColor:UIColor?
// MARK: Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
if let thumbImage = thumbImage
{0
self.setThumbImage(thumbImage, for: .normal)
}
if let minTrackColor = minTrackColor
{
self.minimumTrackTintColor = minTrackColor
}
if let maxTrackColor = maxTrackColor
{
self.maximumTrackTintColor = maxTrackColor
}
}
}