Building Netflix App in Swift 5 and UIKit - Episode 13 - Hooking things together
import UIKit
import Combine
import YouTubeiOSPlayerHelper
class ContentDetailViewController: UIViewController {
private let viewModel: ContentDetailViewModel
private let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .clear
scrollView.showsVerticalScrollIndicator = false
return scrollView
}()
private let contentView: UIView = {
let view = UIView()
return view
}()
private let playerView: YTPlayerView = {
let playerView = YTPlayerView(frame: .zero)
playerView.isHidden = true
return playerView
}()
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "video")
return imageView
}()
private let nameLabel: UILabel = {
let label = UILabel()
label.textColor = .label
label.font = .preferredFont(forTextStyle: .headline)
label.textAlignment = .left
label.numberOfLines = 0
return label
}()
private let overviewLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = .preferredFont(forTextStyle: .body)
label.textColor = .label
label.numberOfLines = 0
return label
}()
private let playButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
let title = NSAttributedString(string: "재생", attributes: [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)])
config.attributedTitle = AttributedString(title)
config.image = UIImage(systemName: "play.fill")?.withTintColor(.black, renderingMode: .alwaysOriginal)
config.imagePlacement = .leading
config.imagePadding = 5
config.baseBackgroundColor = .white
config.baseForegroundColor = .black
button.configuration = config
return button
}()
private let downloadButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
let title = NSAttributedString(string: "저장", attributes: [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)])
config.attributedTitle = AttributedString(title)
config.image = UIImage(systemName: "square.and.arrow.down")?.withTintColor(.white, renderingMode: .alwaysOriginal)
config.imagePlacement = .leading
config.imagePadding = 5
config.baseBackgroundColor = .systemGray
config.baseForegroundColor = .white
button.configuration = config
return button
}()
private let metadataLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.textColor = .systemGray
label.textAlignment = .left
label.font = .preferredFont(forTextStyle: .footnote)
return label
}()
private var cancellables = Set<AnyCancellable>()
init(viewModel: ContentDetailViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setUI()
bind()
}
private func bind() {
playButton
.tapPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
if let id = self?.viewModel.videoId {
let playerVars = [
"playsinline" : 1,
"showinfo" : 0,
"rel" : 0,
"modestbranding" : 1,
"controls" : 1
]
self?.playVideo()
self?.playerView.load(withVideoId: id, playerVars: playerVars)
} else {
print("Cannot Fetch Id")
}
}
.store(in: &cancellables)
}
private func playVideo() {
imageView.isHidden = true
playerView.isHidden = false
}
func stopVideo() {
if !playerView.isHidden {
playerView.stopVideo()
imageView.isHidden = false
playerView.isHidden = true
}
}
private func setUI() {
view.backgroundColor = .black
view.addSubview(scrollView)
playerView.delegate = self
scrollView.delegate = self
scrollView.addSubview(contentView)
contentView.addSubview(imageView)
contentView.addSubview(playerView)
contentView.addSubview(nameLabel)
contentView.addSubview(playButton)
contentView.addSubview(downloadButton)
contentView.addSubview(overviewLabel)
contentView.addSubview(metadataLabel)
var cancellables: AnyCancellable?
cancellables = viewModel.getImage()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in
cancellables?.cancel()
}, receiveValue: { [weak self] image in
self?.imageView.image = image
})
applyConstraints()
if let title = viewModel.contentModel.title {
nameLabel.text = title
} else if let anotherTitle = viewModel.contentModel.original_title {
nameLabel.text = anotherTitle
} else {
nameLabel.text = "Default Title"
}
overviewLabel.text = viewModel.contentModel.overview
metadataLabel.text = "\(viewModel.contentModel.original_title ?? "")" + " " + "\(viewModel.contentModel.release_date ?? "")"
}
private func applyConstraints() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
imageView.translatesAutoresizingMaskIntoConstraints = false
playButton.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
overviewLabel.translatesAutoresizingMaskIntoConstraints = false
playerView.translatesAutoresizingMaskIntoConstraints = false
downloadButton.translatesAutoresizingMaskIntoConstraints = false
metadataLabel.translatesAutoresizingMaskIntoConstraints = false
let scrollViewConstraints = [
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
]
let contentViewConstraints = [
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
]
NSLayoutConstraint.activate(scrollViewConstraints)
NSLayoutConstraint.activate(contentViewConstraints)
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
let contentViewHeight = contentView.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor)
contentViewHeight.priority = .defaultLow
contentViewHeight.isActive = true
let imageViewConstraints = [
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalToConstant: 500)
]
let playerViewConstraints = [
playerView.topAnchor.constraint(equalTo: contentView.topAnchor),
playerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
playerView.heightAnchor.constraint(equalToConstant: 500)
]
let nameLabelConstraints = [
nameLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 10),
nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
]
let playButtonConstraints = [
playButton.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 20),
playButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
playButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
playButton.heightAnchor.constraint(equalToConstant: 20)
]
let downloadButtonConstraints = [
downloadButton.topAnchor.constraint(equalTo: playButton.bottomAnchor, constant: 10),
downloadButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
downloadButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
downloadButton.heightAnchor.constraint(equalToConstant: 20)
]
let overviewLabelConstraints = [
overviewLabel.topAnchor.constraint(equalTo: downloadButton.bottomAnchor, constant: 10),
overviewLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
overviewLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
]
let metadataLabelConstraints = [
metadataLabel.topAnchor.constraint(equalTo: overviewLabel.bottomAnchor, constant: 10),
metadataLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
metadataLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
]
NSLayoutConstraint.activate(imageViewConstraints)
NSLayoutConstraint.activate(playerViewConstraints)
NSLayoutConstraint.activate(nameLabelConstraints)
NSLayoutConstraint.activate(playButtonConstraints)
NSLayoutConstraint.activate(downloadButtonConstraints)
NSLayoutConstraint.activate(overviewLabelConstraints)
NSLayoutConstraint.activate(metadataLabelConstraints)
}
}
extension ContentDetailViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let defaultY = imageView.frame.size.height
let contentOffset = scrollView.contentOffset.y
if defaultY <= contentOffset {
stopVideo()
}
}
}
extension ContentDetailViewController: YTPlayerViewDelegate {
func playerViewDidBecomeReady(_ playerView: YTPlayerView) {
playerView.playVideo()
}
}
protocol SearchResultViewControllerDelegate: AnyObject {
func didTapResult(_ result: ContentModel)
}
extension SearchResultViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = viewModel.searchResultsModel.value[indexPath.row]
delegate?.didTapResult(model)
}
}
extension SearchViewController: SearchResultViewControllerDelegate {
func didTapResult(_ result: ContentModel) {
let detailVC = ContentDetailViewController(viewModel: ContentDetailViewModel(contentModel: result))
navigationController?.pushViewController(detailVC, animated: true)
}
}