源码
接着就是源码了
1. AppDelegate.swift
import AVKit
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback, mode: .moviePlayback)
} catch {
print("Failed to set audioSession category to playback")
}
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role)
}
}
2. SceneDelegate.swift
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else {
return
}
window = UIWindow(windowScene: windowScene)
window?.rootViewController = UINavigationController(rootViewController: CategoryListViewController())
window?.makeKeyAndVisible()
#if targetEnvironment(macCatalyst)
if let titlebar = window?.windowScene?.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
3. CategoryProvider.swift
import Foundation
class CategoryProvider: ObservableObject {
@Published var categories: [Category] = []
var currentContext: ProviderContext = .general
var dataProvider: DataProvider
init(dataProvider: DataProvider) {
self.dataProvider = dataProvider
refresh()
}
enum ProviderContext: Equatable {
case general, favorites, lotsOfVideos
var formattedName: String {
switch self {
case .general: return "All Videos"
case .favorites: return "Favorites"
case .lotsOfVideos: return "Lots of Videos"
}
}
var missingCategoriesTitle: String {
if self == .general || self == .lotsOfVideos {
return "Couldn't find any videos..."
} else {
return "No Favorite Videos"
}
}
var missingCategoriesDescription: String {
if self == .general || self == .lotsOfVideos {
return "No videos or categories were loaded. Has something gone wrong?"
} else {
return "You haven't favorited any videos."
}
}
}
public func refresh() {
switch currentContext {
case .general:
categories = dataProvider.categories
case .lotsOfVideos:
categories = dataProvider.massiveCategoryList
case .favorites:
categories = dataProvider.categoriesWithFavoriteVideos
}
}
}
4. DataProvider.swift
import Foundation
class DataProvider: ObservableObject {
@Published var categories: [Category] = []
/// Compute new categories that only contain favorited videos. Based on the `categories` object.
var categoriesWithFavoriteVideos: [Category] {
return categories.filter { category in
return !category.favoriteVideos.isEmpty
}
}
var massiveCategoryList: [Category] {
var categories: [Category] = []
for _ in 0..<100 {
categories.append(contentsOf: self.categories)
}
return categories
}
init() {
categories = [
Category(title: "SwiftUI", videos: [
Video(
title: "SwiftUI",
description: "",
thumbnailName: "swiftui")
]),
Category(title: "UIKit", videos: [
Video(
title: "Demystifying Views in iOS",
description: "",
thumbnailName: "views"),
Video(
title: "Reproducing Popular iOS Controls",
description: "",
thumbnailName: "controls")
]),
Category(title: "Frameworks", videos: [
Video(
title: "Fastlane for iOS",
description: "",
thumbnailName: "fastlane"),
Video(
title: "Beginning RxSwift",
description: "",
thumbnailName: "rxswift")
]),
Category(title: "Miscellaneous", videos: [
Video(
title: "Data Structures & Algorithms in Swift",
description: "",
thumbnailName: "datastructures"),
Video(
title: "Beginning ARKit",
description: "Learn about ARKit in this amazing tutorial!",
thumbnailName: "arkit"),
Video(
title: "Machine Learning in iOS",
description: "",
thumbnailName: "machinelearning"),
Video(
title: "Push Notifications",
description: "",
thumbnailName: "notifications")
])
]
}
}
5. Video.swift
import Foundation
class Video: Identifiable, Equatable, ObservableObject, Hashable {
var id = UUID()
var title: String
var description: String
var thumbnailName: String
@Published var favorite = false
init(
title: String,
description: String,
thumbnailName: String
) {
self.title = title
self.description = description
self.thumbnailName = thumbnailName
}
static func == (lhs: Video, rhs: Video) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
6. Category.swift
import Foundation
class Category: Identifiable, Hashable, Equatable {
var id = UUID()
var title: String
var videos: [Video]
var favoriteVideos: [Video] {
return videos.filter { video in
return video.favorite
}
}
init(title: String, videos: [Video]) {
self.title = title
self.videos = videos
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Category, rhs: Category) -> Bool {
return lhs.id == rhs.id
}
}
7. CategoryListViewController.swift
import UIKit
import AVKit
class CategoryListViewController: UICollectionViewController {
// MARK: - Properties
private var dataProvider = DataProvider()
private lazy var dataSource = makeDataSource()
// MARK: - Value Types
typealias DataSource = UICollectionViewDiffableDataSource<Category, Video>
typealias Snapshot = NSDiffableDataSourceSnapshot<Category, Video>
init() {
super.init(collectionViewLayout: UICollectionViewLayout())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
#if os(tvOS)
collectionView.backgroundColor = .clear
#else
collectionView.backgroundColor = .systemBackground
navigationItem.largeTitleDisplayMode = .automatic
navigationController?.navigationBar.prefersLargeTitles = true
#endif
title = "Categories"
configureLayout()
applySnapshot(animatingDifferences: false)
}
// MARK: - Functions
func makeDataSource() -> DataSource {
collectionView.register(
VideoCollectionViewCell.self,
forCellWithReuseIdentifier: VideoCollectionViewCell.reuseIdentifier)
let dataSource = DataSource(
collectionView: collectionView
) { collectionView, indexPath, video -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "VideoCollectionViewCell",
for: indexPath) as? VideoCollectionViewCell
cell?.video = video
cell?.layoutSubviews()
return cell
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
guard kind == UICollectionView.elementKindSectionHeader else {
return nil
}
let section = self.dataSource.snapshot()
.sectionIdentifiers[indexPath.section]
let view = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier,
for: indexPath) as? SectionHeaderReusableView
view?.titleLabel.text = section.title
return view
}
return dataSource
}
func applySnapshot(animatingDifferences: Bool = true) {
var snapshot = Snapshot()
snapshot.appendSections(dataProvider.categories)
dataProvider.categories.forEach { category in
snapshot.appendItems(category.videos, toSection: category)
}
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
}
// MARK: - UICollectionViewDataSource Implementation
extension CategoryListViewController {
override func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
guard let videoURL = Bundle.main.url(forResource: "rick", withExtension: "mp4") else {
return
}
let item = AVPlayerItem(url: videoURL)
let player = AVQueuePlayer(
playerItem: item)
player.actionAtItemEnd = .pause
presentPlayerController(with: player, customPlayer: true)
}
func presentPlayerController(with player: AVPlayer, customPlayer: Bool = false) {
let controller: UIViewController
if customPlayer {
let customController = CustomPlayerViewController()
customController.delegate = self
customController.player = player
controller = customController
} else {
let avController = AVPlayerViewController()
avController.delegate = self
avController.player = player
controller = avController
}
present(controller, animated: true) {
player.play()
}
}
}
// MARK: - Layout Handling
extension CategoryListViewController {
private func configureLayout() {
collectionView.register(
SectionHeaderReusableView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier)
let layout = UICollectionViewCompositionalLayout { _, layoutEnvironment -> NSCollectionLayoutSection? in
let isPhone = layoutEnvironment.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiom.phone
let size = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
heightDimension: NSCollectionLayoutDimension.absolute(isPhone ? 280 : 250))
var itemCount = 1
#if targetEnvironment(macCatalyst) || os(tvOS)
let width = self.view.frame.width
if width >= 1800 {
itemCount = 6
} else if width >= 1200 {
itemCount = 4
} else if width >= 1000 {
itemCount = 3
} else {
itemCount = 2
}
#if os(tvOS)
itemCount -= 1
#endif
#else
let orientation = UIDevice.current.orientation
if isPhone {
itemCount = orientation == .landscapeRight || orientation == .landscapeLeft ? 2 : 1
} else {
itemCount = orientation == .portrait || orientation == .portraitUpsideDown ? 3 : 4
}
#endif
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: itemCount)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
#if os(tvOS)
section.contentInsets.trailing = 20
#endif
// Supplementary header view setup
let headerFooterSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(20))
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top)
section.boundarySupplementaryItems = [sectionHeader]
return section
}
collectionView.collectionViewLayout = layout
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
}
extension CategoryListViewController: AVPlayerViewControllerDelegate {
@objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool {
if let presentedViewController = presentedViewController as? AVPlayerViewController,
presentedViewController == playerViewController {
return true
}
return false
}
@objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
// Dismiss the controller when PiP starts so that the user is returned to the item selection screen.
return true
}
@objc func playerViewController(
_ playerViewController: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
restore(playerViewController: playerViewController, completionHandler: completionHandler)
}
}
extension CategoryListViewController: CustomPlayerViewControllerDelegate {
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: CustomPlayerViewController) -> Bool {
// Dismiss the controller when PiP starts so that the user is returned to the item selection screen.
return true
}
func playerViewController(
_ playerViewController: CustomPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
restore(playerViewController: playerViewController, completionHandler: completionHandler)
}
}
extension CategoryListViewController {
func restore(playerViewController: UIViewController, completionHandler: @escaping (Bool) -> Void) {
if let presentedViewController = presentedViewController {
presentedViewController.dismiss(animated: false) { [weak self] in
self?.present(playerViewController, animated: false) {
completionHandler(true)
}
}
} else {
present(playerViewController, animated: false) {
completionHandler(true)
}
}
}
}
8. VideoCollectionViewCell.swift
import UIKit
class VideoCollectionViewCell: UICollectionViewCell {
private lazy var thumbnailView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.layer.cornerRadius = 8
imageView.layer.cornerCurve = .continuous
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 1
#if os(tvOS)
label.font = UIFont.systemFont(ofSize: 28, weight: .semibold)
#else
label.font = UIFont.preferredFont(forTextStyle: .headline)
#endif
return label
}()
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
#if os(tvOS)
label.font = UIFont.systemFont(ofSize: 28, weight: .regular)
#else
label.font = UIFont.preferredFont(forTextStyle: .body)
#endif
return label
}()
var video: Video? {
didSet {
thumbnailView.layer.shadowOffset = CGSize(width: 2, height: 2)
thumbnailView.layer.shadowRadius = 10
thumbnailView.layer.shadowOpacity = 0.3
thumbnailView.image = UIImage(named: video?.thumbnailName ?? "")
titleLabel.text = video?.title
subtitleLabel.text = video?.description
}
}
public static var reuseIdentifier: String {
return String(describing: self)
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(thumbnailView)
contentView.addSubview(titleLabel)
contentView.addSubview(subtitleLabel)
layoutSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
var spacing: CGFloat = 8
#if os(tvOS)
spacing = 12
#endif
NSLayoutConstraint.activate([
thumbnailView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: spacing),
thumbnailView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: spacing),
thumbnailView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -spacing),
thumbnailView.heightAnchor.constraint(lessThanOrEqualTo: contentView.heightAnchor, multiplier: 3 / 4),
titleLabel.topAnchor.constraint(equalTo: thumbnailView.bottomAnchor, constant: 5),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: spacing),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -spacing),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor)
])
}
#if os(tvOS)
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
let propertyAnimator: UIViewPropertyAnimator
if isFocused {
propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
self.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
} else {
propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) {
self.transform = .identity
}
}
propertyAnimator.startAnimation()
}
#endif
}
9. SectionHeaderReusableView.swift
import UIKit
class SectionHeaderReusableView: UICollectionReusableView {
static var reuseIdentifier: String {
return String(describing: SectionHeaderReusableView.self)
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
var textStyle = UIFont.TextStyle.title1
#if os(tvOS)
textStyle = .title3
#endif
label.font = UIFont.systemFont(
ofSize: UIFont.preferredFont(forTextStyle: textStyle).pointSize,
weight: .bold)
label.adjustsFontForContentSizeCategory = true
label.textColor = .label
label.textAlignment = .left
label.numberOfLines = 1
label.setContentCompressionResistancePriority(
.defaultHigh, for: .horizontal)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
var usingtvOS = false
#if os(tvOS)
usingtvOS = true
#endif
#if os(tvOS) || targetEnvironment(macCatalyst)
backgroundColor = .clear
#else
backgroundColor = .systemBackground
#endif
addSubview(titleLabel)
if UIDevice.current.userInterfaceIdiom == .pad || usingtvOS {
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(
equalTo: leadingAnchor,
constant: 5),
titleLabel.trailingAnchor.constraint(
lessThanOrEqualTo: trailingAnchor,
constant: -5)
])
} else {
NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(
equalTo: readableContentGuide.leadingAnchor),
titleLabel.trailingAnchor.constraint(
lessThanOrEqualTo: readableContentGuide.trailingAnchor)
])
}
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(
equalTo: topAnchor,
constant: 10),
titleLabel.bottomAnchor.constraint(
equalTo: bottomAnchor,
constant: -10)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
10. CustomPlayerViewController.swift
import UIKit
import AVKit
/// The Set of custom player controllers currently using or transitioning out of PiP
private var activeCustomPlayerViewControllers = Set<CustomPlayerViewController>()
public class CustomPlayerViewController: UIViewController {
weak var delegate: CustomPlayerViewControllerDelegate?
public var player: AVPlayer? {
didSet {
playerLayer = AVPlayerLayer(player: player)
}
}
private var playerLayer: AVPlayerLayer?
private var pictureInPictureController: AVPictureInPictureController?
private var controlsView: CustomPlayerControlsView?
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .fullScreen
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
guard let playerLayer = playerLayer else {
fatalError("Missing AVPlayerLayer")
}
view.backgroundColor = .black
view.layer.addSublayer(playerLayer)
pictureInPictureController = AVPictureInPictureController(
playerLayer: playerLayer)
pictureInPictureController?.delegate = self
let tapGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(tapGestureHandler))
#if os(tvOS)
tapGestureRecognizer.allowedTouchTypes = [UITouch.TouchType.indirect].map { $0.rawValue as NSNumber
}
tapGestureRecognizer.allowedPressTypes = []
#endif
view.addGestureRecognizer(tapGestureRecognizer)
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerLayer?.frame = view.bounds
}
@objc private func tapGestureHandler(recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
if controlsView == nil {
showControls()
} else {
hideControls()
}
default:
break
}
}
private func showControls() {
let controlsView = CustomPlayerControlsView(player: player, pipController: pictureInPictureController)
controlsView.delegate = self
controlsView.translatesAutoresizingMaskIntoConstraints = false
controlsView.alpha = 0.0
let controlsViewHeight: CGFloat = 180.0
view.addSubview(controlsView)
NSLayoutConstraint.activate([
controlsView.heightAnchor.constraint(equalToConstant: controlsViewHeight),
controlsView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 90),
controlsView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -90),
controlsView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60)
])
UIView.animate(withDuration: 0.25) {
controlsView.alpha = 1.0
}
self.controlsView = controlsView
// Set the additional bottom safe area inset to the height of the custom UI so existing PiP windows avoid it.
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: controlsViewHeight, right: 0)
}
private func hideControls() {
guard let controlsView = controlsView else { return }
UIView.animate(withDuration: 0.25) {
controlsView.alpha = 0.0
} completion: { _ in
controlsView.removeFromSuperview()
self.controlsView = nil
}
// Reset the safe area inset to its default value.
additionalSafeAreaInsets = .zero
}
}
extension CustomPlayerViewController: CustomPlayerControlsViewDelegate {
func controlsViewDidRequestStartPictureInPicture(
_ controlsView: CustomPlayerControlsView
) {
pictureInPictureController?.startPictureInPicture()
hideControls()
}
func controlsViewDidRequestStopPictureInPicture(
_ controlsView: CustomPlayerControlsView
) {
pictureInPictureController?.stopPictureInPicture()
hideControls()
}
func controlsViewDidRequestControlsDismissal(
_ controlsView: CustomPlayerControlsView
) {
hideControls()
}
func controlsViewDidRequestPlayerDismissal(
_ controlsView: CustomPlayerControlsView
) {
player?.rate = 0
dismiss(animated: true)
}
}
// MARK: - AVPictureInPictureDelegate
extension CustomPlayerViewController: AVPictureInPictureControllerDelegate {
public func pictureInPictureControllerWillStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
activeCustomPlayerViewControllers.insert(self)
}
public func pictureInPictureControllerDidStartPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
dismiss(animated: true, completion: nil)
}
public func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
failedToStartPictureInPictureWithError error: Error
) {
activeCustomPlayerViewControllers.remove(self)
}
public func pictureInPictureControllerDidStopPictureInPicture(
_ pictureInPictureController: AVPictureInPictureController
) {
activeCustomPlayerViewControllers.remove(self)
}
public func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
delegate?.playerViewController(
self,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
}
}
protocol CustomPlayerViewControllerDelegate: AnyObject {
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(
_ playerViewController: CustomPlayerViewController
) -> Bool
func playerViewController(
_ playerViewController: CustomPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
)
}
11. CustomPlayerControlsView.swift
import AVKit
import Combine
import UIKit
protocol CustomPlayerControlsViewDelegate: AnyObject {
func controlsViewDidRequestStartPictureInPicture(_ controlsView: CustomPlayerControlsView)
func controlsViewDidRequestStopPictureInPicture(_ controlsView: CustomPlayerControlsView)
func controlsViewDidRequestControlsDismissal(_ controlsView: CustomPlayerControlsView)
func controlsViewDidRequestPlayerDismissal(_ controlsView: CustomPlayerControlsView)
}
class CustomPlayerControlsView: UIView {
weak var delegate: CustomPlayerControlsViewDelegate?
private weak var player: AVPlayer?
private weak var pipController: AVPictureInPictureController?
private lazy var spacerView = UIView()
private lazy var buttonStackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .center
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 8
return stackView
}()
private lazy var progressView: UIProgressView = {
let progressView = UIProgressView()
progressView.translatesAutoresizingMaskIntoConstraints = false
return progressView
}()
private var progressTimer: Timer?
private var canStopPictureInPictureCancellable: Cancellable?
init(player: AVPlayer?, pipController: AVPictureInPictureController?) {
self.player = player
self.pipController = pipController
super.init(frame: .zero)
setupViewLayout()
let progressTimer = Timer(
timeInterval: 0.5,
repeats: true
) { [weak self] _ in
guard
let player = player,
let item = player.currentItem
else {
return
}
let progress = CMTimeGetSeconds(player.currentTime()) / CMTimeGetSeconds(item.duration)
self?.progressView.progress = Float(progress)
}
self.progressTimer = progressTimer
RunLoop.main.add(progressTimer, forMode: .default)
let menuGestureRecognizer = UITapGestureRecognizer(
target: self,
action: #selector(menuGestureHandler))
menuGestureRecognizer.allowedPressTypes = [UIPress.PressType.menu].map {
$0.rawValue as NSNumber
}
addGestureRecognizer(menuGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
progressTimer?.invalidate()
progressTimer = nil
canStopPictureInPictureCancellable = nil
}
private func setupViewLayout() {
#if os(tvOS)
let canStopPiP = pipController?.canStopPictureInPicture ?? false
#else
let canStopPiP = false
#endif
spacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
updatePiPButtons(canStopPiP: canStopPiP)
#if os(tvOS)
canStopPictureInPictureCancellable = pipController?.publisher(
for: \.canStopPictureInPicture)
.sink { [weak self] in
self?.updatePiPButtons(canStopPiP: $0)
}
#endif
addSubview(progressView)
NSLayoutConstraint.activate([
progressView.heightAnchor.constraint(equalToConstant: 10),
progressView.leadingAnchor.constraint(equalTo: leadingAnchor),
progressView.trailingAnchor.constraint(equalTo: trailingAnchor),
progressView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
addSubview(buttonStackView)
NSLayoutConstraint.activate([
buttonStackView.heightAnchor.constraint(equalToConstant: 90),
buttonStackView.bottomAnchor.constraint(equalTo: progressView.topAnchor, constant: -40),
buttonStackView.widthAnchor.constraint(equalTo: progressView.widthAnchor, multiplier: 3 / 4),
buttonStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10)
])
}
private func updatePiPButtons(canStopPiP: Bool) {
let buttonSideLength: CGFloat = 50
var buttons: [UIButton] = []
let startButton = CustomPlayerCircularButtonView(symbolName: "pip.enter", height: buttonSideLength)
startButton.addTarget(self, action: #selector(startButtonPressed), for: [.primaryActionTriggered, .touchUpInside])
buttons.append(startButton)
if canStopPiP {
let stopButton = CustomPlayerCircularButtonView(symbolName: "pip.exit", height: buttonSideLength)
stopButton.addTarget(self, action: #selector(stopButtonPressed), for: [.primaryActionTriggered, .touchUpInside])
buttons.append(stopButton)
}
#if os(iOS)
let closePlayerButton = CustomPlayerCircularButtonView(symbolName: "xmark", height: buttonSideLength)
closePlayerButton.addTarget(self, action: #selector(closePlayerPressed), for: .touchUpInside)
buttons.append(closePlayerButton)
#endif
let existingButtons = buttonStackView.arrangedSubviews
for view in existingButtons {
view.removeFromSuperview()
}
buttonStackView.addArrangedSubview(spacerView)
for button in buttons {
buttonStackView.addArrangedSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: buttonSideLength),
button.heightAnchor.constraint(equalToConstant: buttonSideLength)
])
}
}
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let nextButton = context.nextFocusedView as? CustomPlayerCircularButtonView {
nextButton.backgroundColor = .white
nextButton.tintColor = .darkGray
}
if let previousButton = context.previouslyFocusedView as? CustomPlayerCircularButtonView {
previousButton.backgroundColor = .darkGray
previousButton.tintColor = .white
}
}
@objc private func startButtonPressed() {
delegate?.controlsViewDidRequestStartPictureInPicture(self)
}
@objc private func stopButtonPressed() {
delegate?.controlsViewDidRequestStopPictureInPicture(self)
}
@objc private func closePlayerPressed() {
delegate?.controlsViewDidRequestPlayerDismissal(self)
}
@objc private func menuGestureHandler(recognizer: UITapGestureRecognizer) {
switch recognizer.state {
case .ended:
delegate?.controlsViewDidRequestControlsDismissal(self)
default:
break
}
}
}
12. CustomPlayerCircularButtonView.swift
import UIKit
class CustomPlayerCircularButtonView: UIButton {
init(symbolName: String, height: CGFloat) {
super.init(frame: .zero)
backgroundColor = .white.withAlphaComponent(0.5)
layer.cornerRadius = height / 2
tintColor = .black
setImage(
UIImage(systemName: symbolName)?
.withRenderingMode(.alwaysTemplate),
for: .normal)
imageView?.contentMode = .scaleAspectFit
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
13. AVPlayer+Extension.swift
import AVKit
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
14. ConditionalModifiers.swift
import SwiftUI
extension View {
@ViewBuilder
func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
if conditional {
content(self)
} else {
self
}
}
}
15. CurrentPlatform.swift
import Foundation
enum Platform {
case macOS
case iOS
case tvOS
case watchOS
#if os(macOS)
static let current = macOS
#elseif os(iOS)
static let current = iOS
#elseif os(tvOS)
static let current = tvOS
#elseif os(watchOS)
static let current = watchOS
#else
#error("Unsupported platform")
#endif
}
本篇主要讲述了基于视频播放器的画中画实现,感兴趣的给个赞或者关注~~~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。