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)
}
}
}
}
I am trying to create a feature programmatically so that when a user selects a cell in the collection view the app keeps a count of the image selected and adds it as an overlay. I am also wanting to add the video duration to the bottom of the image if the selection is a video. I know my problem is in my constraints. You can see in the image example below that I am trying to add the count to the top left of the collection view cell, but also when the user deselects a cell the count adjusts so for example if the number 2 in the image below was deselected the number 3 would become 2. For the most part I think I have the code working but I cannot get the constraints to work. With the current configuration I am getting an error (see below) but I do not even know where to begin with this problem.
"Unable to activate constraint with anchors because they have
no common ancestor. Does the constraint or its anchors reference
items in different view hierarchies? That's illegal."
CollectionView:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
cell.commonInit()
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) as? TestCVCell {
//Not sure what to put here
}
}
Overlay
class CustomAssetCellOverlay: UIView {
let countSize = CGSize(width: 40, height: 40)
lazy var circleView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.layer.cornerRadius = self.countSize.width / 2
view.alpha = 0.4
return view
}()
let countLabel: UILabel = {
let label = UILabel()
let font = UIFont.preferredFont(forTextStyle: .headline)
label.font = UIFont.systemFont(ofSize: font.pointSize, weight: UIFont.Weight.bold)
label.textAlignment = .center
label.textColor = .white
label.adjustsFontSizeToFitWidth = true
return label
}()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
private func commonInit() {
addSubview(circleView)
addSubview(countLabel)
//***** START - UPDATED BASED ON SUGGESTION IN COMMENTS******
countLabel.translatesAutoresizingMaskIntoConstraints = false
//***** END - UPDATED BASED ON SUGGESTION IN COMMENTS******
countLabel.centerXAnchor.constraint(equalTo: circleView.centerXAnchor).isActive = true
countLabel.centerYAnchor.constraint(equalTo: circleView.centerYAnchor).isActive = true
}
}
Collection View Cell
var img = UIImageView()
var overlayView = UIView()
var asset: PHAsset? {
didSet {}
}
var isVideo: Bool = false {
didSet {
durationLabel.isHidden = !isVideo
}
}
override var isSelected: Bool {
didSet { overlay.isHidden = !isSelected }
}
var imageView: UIImageView = {
let view = UIImageView()
view.clipsToBounds = true
view.contentMode = .scaleAspectFill
view.backgroundColor = UIColor.gray
return view
}()
var count: Int = 0 {
didSet { overlay.countLabel.text = "\(count)" }
}
var duration: TimeInterval = 0 {
didSet {
let hour = Int(duration / 3600)
let min = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let sec = Int(duration.truncatingRemainder(dividingBy: 60))
var durationString = hour > 0 ? "\(hour)" : ""
durationString.append(min > 0 ? "\(min):" : ":")
durationString.append(String(format: "%02d", sec))
durationLabel.text = durationString
}
}
let overlay: CustomAssetCellOverlay = {
let view = CustomAssetCellOverlay()
view.isHidden = true
return view
}()
let durationLabel: UILabel = {
let label = UILabel()
label.preferredMaxLayoutWidth = 80
label.backgroundColor = .gray
label.textColor = .white
label.textAlignment = .right
label.font = UIFont.boldSystemFont(ofSize: 20)
return label
}()
func commonInit() {
addSubview(imageView)
imageView.addSubview(overlay)
imageView.addSubview(durationLabel)
imageView.translatesAutoresizingMaskIntoConstraints = false
//***** START - UPDATED BASED ON SUGGESTION IN COMMENTS******
overlay.translatesAutoresizingMaskIntoConstraints = false
overlayView.translatesAutoresizingMaskIntoConstraints = false
//***** END - UPDATED BASED ON SUGGESTION IN COMMENTS******
NSLayoutConstraint.activate([
overlay.topAnchor.constraint(equalTo: imageView.topAnchor),
overlay.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
overlay.leftAnchor.constraint(equalTo: imageView.leftAnchor),
overlay.rightAnchor.constraint(equalTo: imageView.rightAnchor),
overlayView.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
overlayView.centerYAnchor.constraint(equalTo: overlay.centerYAnchor),
overlayView.widthAnchor.constraint(equalToConstant: 80.0),
overlayView.heightAnchor.constraint(equalToConstant: 80.0),
]
)
}
//Some other stuff
I have SegmentedControl with 2 lines using:
// AppDelegate
UILabel.appearanceWhenContainedInInstancesOfClasses([UISegmentedControl.self]).numberOfLines = 0
The problem is the line fonts are the same exact size. I need to change the titleTextAttributes for each line so that the second line is smaller then the first line.
I know I can use this for both lines:
segmentedControl.setTitleTextAttributes([NSAttributedStringKey.font : UIFont.systemFont(ofSize: 17))
How can I do this?
// The SegmentedControl
let segmentedControl: UISegmentedControl = {
let segmentedControl = UISegmentedControl(items: ["Pizza\n123.1K", "Turkey Burgers\n456.2M", "Gingerale\n789.3B"])
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.tintColor = UIColor.orange
segmentedControl.backgroundColor = .white
segmentedControl.isHighlighted = true
segmentedControl.addTarget(self, action: #selector(selectedIndex(_:)), for: .valueChanged)
return segmentedControl
}()
You'll want to create a custom control by subclassing UIControl. Here's a quick example:
CustomSegmentedControl.swift
import UIKit
import CoreImage
public class CustomSegmentedControl: UIControl {
public var borderWidth: CGFloat = 1.0
public var selectedSegementIndex = 0 {
didSet {
self.styleButtons()
}
}
public var numberOfSegments: Int {
return self.segments.count
}
private var buttons: [UIButton] = []
private var stackView = UIStackView(frame: CGRect.zero)
private var stackBackground = UIView(frame: CGRect.zero)
private var segments: [NSAttributedString] = [] {
didSet {
for subview in self.stackView.arrangedSubviews {
subview.removeFromSuperview()
}
self.buttons = []
for i in 0..<segments.count {
let segment = segments[i]
self.createAndAddSegmentButton(title: segment)
}
self.styleButtons()
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
private func setup() {
self.addSubview(stackBackground)
self.stackBackground.constrainToBounds(of: self)
self.addSubview(stackView)
self.stackView.constrainToBounds(of: self)
self.stackView.axis = .horizontal
self.stackView.distribution = .fillEqually
self.stackView.spacing = borderWidth
self.layer.cornerRadius = 5.0
self.layer.borderWidth = borderWidth
self.clipsToBounds = true
self.stackBackground.backgroundColor = tintColor
}
private func createAndAddSegmentButton(title: NSAttributedString) {
let button = createSegmentButton(title: title)
self.buttons.append(button)
self.stackView.addArrangedSubview(button)
}
private func createSegmentButton(title: NSAttributedString) -> UIButton {
let button = UIButton(frame: CGRect.zero)
button.titleLabel?.numberOfLines = 0
button.titleLabel?.textAlignment = .center
button.setAttributedTitle(title, for: .normal)
button.addTarget(self, action: #selector(self.actSelected(button:)), for: .touchUpInside)
return button
}
override public var tintColor: UIColor! {
willSet {
self.layer.borderColor = newValue.cgColor
self.stackBackground.backgroundColor = newValue
}
}
public func setSegments(_ segments: [NSAttributedString]) {
self.segments = segments
}
#objc private func actSelected(button: UIButton) {
guard let index = self.buttons.index(of: button) else {
print("invalid selection should never happen, would want to handle better than this")
return
}
self.selectedSegementIndex = index
self.sendActions(for: .valueChanged)
}
private func styleButtons() {
for i in 0..<self.buttons.count {
let button = self.buttons[i]
if i == selectedSegementIndex {
button.backgroundColor = self.tintColor
button.titleLabel?.textColor = self.backgroundColor ?? .white
} else {
button.backgroundColor = self.backgroundColor
button.titleLabel?.textColor = self.tintColor
}
}
}
}
extension UIView {
func constrainToBounds(of view: UIView) {
self.translatesAutoresizingMaskIntoConstraints = false
let attrs: [NSLayoutAttribute] = [.leading, .top, .trailing, .bottom]
let constraints = attrs.map { (attr) -> NSLayoutConstraint in
return NSLayoutConstraint(item: self,
attribute: attr,
relatedBy: .equal,
toItem: view,
attribute: attr,
multiplier: 1.0,
constant: 0)
}
NSLayoutConstraint.activate(constraints)
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var customSegment: CustomSegmentedControl!
private var segments: [NSAttributedString] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.customSegment.backgroundColor = .white
self.customSegment.tintColor = .orange
let pizza = createText(title: "Pizza", subTitle: "123K")
let turkey = createText(title: "Turkey Burgers", subTitle: "456.2M")
let gingerAle = createText(title: "Gingerale", subTitle: "789.3B")
self.segments = [pizza, turkey, gingerAle]
self.customSegment.setSegments(self.segments)
self.customSegment.addTarget(self, action: #selector(self.segmentSelectionChanged(control:)), for: .valueChanged)
}
#objc private func segmentSelectionChanged(control: CustomSegmentedControl) {
let segment = self.segments[control.selectedSegementIndex]
print("selected segment = \(segment.string)")
}
func createText(title: String, subTitle: String) -> NSAttributedString {
let titleStr = NSMutableAttributedString(string: "\(title)\n", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16)])
let subStr = NSAttributedString(string: subTitle, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 10)])
titleStr.append(subStr)
return titleStr
}
}
My problem is that UITableView lags quite a lot while scrolling.
This is what I am trying to achieve
Starting from the top I have a simple section header with only one checkbox and one UILabel. Under this header, you can see a custom cell with only one UILabel aligned to the center. This custom cell works like another header for the data that would be shown below (Basically a 3D array). Under these "headers" are custom cells that contain one multiline UILabel and under this label is a container for a variable amount of lines containing a checkbox and an UILabel. On the right side of the cell is also a button (blue/white arrow).
So this means the content is shown like this:
Section header (containing day and date)
Custom UITableViewCell = header (containing some header information)
Custom UITableViewCell (containing data to be shown)
Here is my code:
cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let (isHeader, headerNumber, semiResult) = checkIfIsHeader(section: indexPath.section, row: indexPath.row)
let row = indexPath.row
if isHeader {
let chod = objednavkaDny[indexPath.section].chody[headerNumber+1]
let cell = tableView.dequeueReusableCell(withIdentifier: cellHeaderReuseIdentifier, for: indexPath) as! ObjednavkyHeaderTableViewCell
cell.titleLabel.text = chod.popisPoradiJidla
cell.selectionStyle = .none
return cell
}else{
let chod = objednavkaDny[indexPath.section].chody[headerNumber]
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ObjednavkyTableViewCell
cell.updateData(objednavka: chod.objednavky[row-semiResult], canSetAmount: self.typDialogu == 3)
return cell
}
}
checkIfIsHeader:
func checkIfIsHeader(section: Int, row: Int) -> (Bool, Int, Int){
if let cachedResult = checkIfHeaderCache[section]?[row] {
return (cachedResult[0] == 1, cachedResult[1], cachedResult[2])
}
var isHeader = false
var semiResult = 0
var headerNumber = -1
for (index, chod) in objednavkaDny[section].chody.enumerated() {
let sum = chod.objednavky.count
if row == semiResult {
isHeader = true
break
}else if row < semiResult {
semiResult -= objednavkaDny[section].chody[index-1].objednavky.count
break
}else {
headerNumber += 1
semiResult += 1
if index != objednavkaDny[section].chody.count - 1 {
semiResult += sum
}
}
}
checkIfHeaderCache[section] = [Int:[Int]]()
checkIfHeaderCache[section]![row] = [isHeader ? 1 : 0, headerNumber, semiResult]
return (isHeader, headerNumber, semiResult)
}
and the main cell that shows the data:
class ObjednavkyTableViewCell: UITableViewCell {
lazy var numberTextField: ObjednavkyTextField = {
let textField = ObjednavkyTextField()
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let mealLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .black
label.textAlignment = .left
label.font = UIFont(name: ".SFUIText", size: 15)
label.numberOfLines = 0
label.backgroundColor = .white
label.isOpaque = true
return label
}()
lazy var detailsButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(named: "arrow-right")?.withRenderingMode(.alwaysTemplate), for: .normal)
button.imageView?.tintColor = UIColor.custom.blue.classicBlue
button.imageView?.contentMode = .scaleAspectFit
button.contentHorizontalAlignment = .right
button.imageEdgeInsets = UIEdgeInsetsMake(10, 0, 10, 0)
button.addTarget(self, action: #selector(detailsButtonPressed), for: .touchUpInside)
button.backgroundColor = .white
button.isOpaque = true
return button
}()
let pricesContainerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.isOpaque = true
return view
}()
var canSetAmount = false {
didSet {
canSetAmount ? showNumberTextField() : hideNumberTextField()
}
}
var shouldShowPrices = false {
didSet {
shouldShowPrices ? showPricesContainerView() : hidePricesContainerView()
}
}
var pricesContainerHeight: CGFloat = 0
private let priceViewHeight: CGFloat = 30
var mealLabelLeadingConstraint: NSLayoutConstraint?
var mealLabelBottomConstraint: NSLayoutConstraint?
var pricesContainerViewHeightConstraint: NSLayoutConstraint?
var pricesContainerViewBottomConstraint: NSLayoutConstraint?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func detailsButtonPressed() {
}
func updateData(objednavka: Objednavka, canSetAmount: Bool) {
self.canSetAmount = canSetAmount
if let popisJidla = objednavka.popisJidla, popisJidla != "", popisJidla != " " {
self.mealLabel.text = popisJidla
}else{
self.mealLabel.text = objednavka.nazevJidelnicku
}
if objednavka.objects.count > 1 {
shouldShowPrices = true
setPricesStackView(with: objednavka.objects)
checkIfSelected(objects: objednavka.objects)
}else{
shouldShowPrices = false
self.numberTextField.text = String(objednavka.objects[0].pocet)
//setSelected(objednavka.objects[0].pocet > 0, animated: false)
objednavka.objects[0].pocet > 0 ? setSelectedStyle() : setDeselectedStyle()
}
}
//---------------
func checkIfSelected(objects: [ObjednavkaObject]) {
var didChangeSelection = false
for object in objects { // Checks wether cell should be selected or not
if object.pocet > 0 {
setSelected(true, animated: false)
setSelectedStyle()
didChangeSelection = true
break
}
}
if !didChangeSelection {
setSelected(false, animated: false)
setDeselectedStyle()
}
}
//--------------
func showNumberTextField() {
numberTextField.isHidden = false
mealLabelLeadingConstraint?.isActive = false
mealLabelLeadingConstraint = mealLabel.leadingAnchor.constraint(equalTo: numberTextField.trailingAnchor, constant: 10)
mealLabelLeadingConstraint?.isActive = true
}
func hideNumberTextField() {
numberTextField.isHidden = true
mealLabelLeadingConstraint?.isActive = false
mealLabelLeadingConstraint = mealLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor, constant: 0)
mealLabelLeadingConstraint?.isActive = true
}
func showPricesContainerView() {
hideNumberTextField()
pricesContainerView.isHidden = false
mealLabelBottomConstraint?.isActive = false
pricesContainerViewBottomConstraint?.isActive = true
}
func hidePricesContainerView() {
pricesContainerView.isHidden = true
pricesContainerViewBottomConstraint?.isActive = false
mealLabelBottomConstraint?.isActive = true
}
//--------------
func setSelectedStyle() {
self.backgroundColor = UIColor.custom.blue.classicBlue
mealLabel.textColor = .white
mealLabel.backgroundColor = UIColor.custom.blue.classicBlue
for subview in pricesContainerView.subviews where subview is ObjednavkyPriceView {
let priceView = (subview as! ObjednavkyPriceView)
priceView.titleLabel.textColor = .white
priceView.checkBox.backgroundColor = UIColor.custom.blue.classicBlue
priceView.titleLabel.backgroundColor = UIColor.custom.blue.classicBlue
priceView.backgroundColor = UIColor.custom.blue.classicBlue
}
pricesContainerView.backgroundColor = UIColor.custom.blue.classicBlue
detailsButton.imageView?.tintColor = .white
detailsButton.backgroundColor = UIColor.custom.blue.classicBlue
}
func setDeselectedStyle() {
self.backgroundColor = .white
mealLabel.textColor = .black
mealLabel.backgroundColor = .white
for subview in pricesContainerView.subviews where subview is ObjednavkyPriceView {
let priceView = (subview as! ObjednavkyPriceView)
priceView.titleLabel.textColor = .black
priceView.checkBox.backgroundColor = .white
priceView.titleLabel.backgroundColor = .white
priceView.backgroundColor = .white
}
pricesContainerView.backgroundColor = .white
detailsButton.imageView?.tintColor = UIColor.custom.blue.classicBlue
detailsButton.backgroundColor = .white
}
//-----------------
func setPricesStackView(with objects: [ObjednavkaObject]) {
let subviews = pricesContainerView.subviews
var subviewsToDelete = subviews.count
for (index, object) in objects.enumerated() {
subviewsToDelete -= 1
if subviews.count - 1 >= index {
let priceView = subviews[index] as! ObjednavkyPriceView
priceView.titleLabel.text = object.popisProduktu // + " " + NSNumber(value: object.cena).getFormattedString(currencySymbol: "Kč") // TODO: currencySymbol
priceView.canSetAmount = canSetAmount
priceView.count = object.pocet
priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == "")
}else {
let priceView = ObjednavkyPriceView(frame: CGRect(x: 0, y: CGFloat(index) * priceViewHeight + CGFloat(index * 5), width: pricesContainerView.frame.width, height: priceViewHeight))
pricesContainerView.addSubview(priceView)
priceView.titleLabel.text = object.popisProduktu // + " " + NSNumber(value: object.cena).getFormattedString(currencySymbol: "Kč") // TODO: currencySymbol
priceView.numberTextField.delegate = self
priceView.canSetAmount = canSetAmount
priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == "")
priceView.count = object.pocet
pricesContainerHeight += ((index == 0) ? 30 : 35)
}
}
if subviewsToDelete > 0 { // Deletes unwanted subviews
for _ in 0..<subviewsToDelete {
pricesContainerView.subviews.last?.removeFromSuperview()
pricesContainerHeight -= pricesContainerHeight + 5
}
}
if pricesContainerHeight < 0 {
pricesContainerHeight = 0
}
pricesContainerViewHeightConstraint?.constant = pricesContainerHeight
}
func setupView() {
self.layer.shouldRasterize = true
self.layer.rasterizationScale = UIScreen.main.scale
self.backgroundColor = .white
contentView.addSubview(numberTextField)
contentView.addSubview(mealLabel)
contentView.addSubview(detailsButton)
contentView.addSubview(pricesContainerView)
setupConstraints()
}
func setupConstraints() {
numberTextField.anchor(leading: readableContentGuide.leadingAnchor, size: CGSize(width: 30, height: 30))
numberTextField.centerYAnchor.constraint(equalTo: mealLabel.centerYAnchor).isActive = true
detailsButton.anchor(trailing: readableContentGuide.trailingAnchor, size: CGSize(width: 30, height: 30))
detailsButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
mealLabel.anchor(top: contentView.topAnchor, trailing: detailsButton.leadingAnchor, padding: .init(top: 10, left: 0, bottom: 0, right: -10))
mealLabelBottomConstraint = mealLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
mealLabelBottomConstraint?.priority = UILayoutPriority(rawValue: 999)
pricesContainerView.anchor(top: mealLabel.bottomAnchor, leading: readableContentGuide.leadingAnchor, trailing: detailsButton.leadingAnchor, padding: .init(top: 10, left: 0, bottom: 0, right: -10))
pricesContainerViewBottomConstraint = pricesContainerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
pricesContainerViewBottomConstraint?.priority = UILayoutPriority(rawValue: 999)
pricesContainerViewHeightConstraint = pricesContainerView.heightAnchor.constraint(equalToConstant: 0)
pricesContainerViewHeightConstraint?.priority = UILayoutPriority(rawValue: 999)
pricesContainerViewHeightConstraint?.isActive = true
}
}
To conclude how it is done:
tableView.rowHeight is set to UITableViewAutomaticDymension
inside cellForRowAt I get the data from an array and give it to the
cell
all the cells are set up in code using constraints
all the views have set isOpaque = true
heights of the cells are cached
cells are set to rasterize
I also noticed that it lags at certain scroll levels and that sometimes it works just fine and sometimes it lags a lot.
Despite all of the optimization I have done, the tableView still lags while scrolling.
Here is a screenshot from Instruments
Any tip how to improve the scrolling performance is highly appreciated!
(I might have forgotten to include some code/information so feel free to ask me in the comments.)
I can't tell you where the lag happens exactly but when we are talking about lagging during scrolling, it's related to your cellForRowAt delegate method. What happends is that too many things are going on within this method and it's called for every cells that are displaying & going to display. I see that your are trying to cache the result by checkIfHeaderCache but still, there is a for loop at the very beginning to determine header cell.
Suggestions:
I don't know where you get data (objednavkaDny) from but right after you get the data, do a full loop through and determin cell type one by one, and save the result some where base on your design. During this loading time, you can show some loading message on the screen. Then, within the cellForRow method, you should be just simply using things like
if (isHeader) {
render header cell
} else {
render other cell
}
Bottom line:
cellForRow method is not designed to handle heavy calculations, and it will slow down the scrolling if you do so. This method is for assigning values to the cached table view cell only and that's the only thing it is good at.
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.