A UICollectionView will consist of a feed of videos. When the user is inline with a video, I would like it to play. With my current setup, several videos play at once (I suppose depending on the pagination) once they are loaded in to the collection view.
How do I play videos inline in a UICollectionView?
A cell in the UICollectionView feed will contain a UIView, which will hold the video player. This is the UIView's class PlayerViewClass:
import Foundation
import UIKit
import AVKit
import AVFoundation
class PlayerViewClass: UIView {
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
}
The Feed's collectionView cellForItemAt indexPath delegate method in the FeedViewController is as follows:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
if let cell = cell as? MyCollectionViewCell {
...
//Configuring the cell
...
//Video player
let avPlayer = AVPlayer(url: post.fullURL)
//Setting cell's player
cell.playerView.playerLayer.player = avPlayer
//TODO: Change so this is only executed when the user is inline.
cell.playerView.player?.play()
}
return cell
}
The cell MyCollectionViewCell has an IBOutlet linked to the playerView UIView:
class MyCollectionViewCell {
#IBOutlet weak var playerView: PlayerViewClass!
override func awakeFromNib() {
super.awakeFromNib()
//Setup, not relevant
}
}
I found the following GitHub repo, which shows the functionality that I would like to implement; however, I'm a little unsure of how to do this with my setup below.
Thanks so much!
You'll need to know which cells are visible.You get that "for free" via the UICollectionView API's visibleCells property. Here's the documentation.
Additionally, you'll need to do some bookkeeping when the UICollectionView scrolls. In reviewing the documentation for UICollectionView, you'll see it is a subclass of UIScrollView. UIScrollView comes with delegate methods that enable you to track this. Here's a "physics for poets" approach to what you could do to accomplish this task:
Let's say this is your view controller:
class YourViewController: UIViewController {
let collectionView = UICollectionView()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
Then, you'd implement your UICollectionViewDelegate and UICollectionViewDataSource methods here:
extension YourViewController: UICollectionViewDelegate, UICollectionViewDataSource {
// Bare bones implementation
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// TODO: need to implement
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// TODO: need to implem,ent
return UICollectionViewCell()
}
}
Finally, you'd implement UIScrollViewDelegate methods to detect beginning of scrolling and end of scrolling. Here's where you'd implement your logic for starting/stopping video:
extension YourViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
collectionView.visibleCells.forEach { cell in
// TODO: write logic to stop the video before it begins scrolling
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
collectionView.visibleCells.forEach { cell in
// TODO: write logic to start the video after it ends scrolling
}
}
}
You'll want to muck around with the timing of stopping/starting animations to see what looks good, so feel free to poke around the various UIScrollViewDelegate methods.
For a play a video in Inline you can refer to the MMPlayerView in gitHub it will be a helpful framework "https://cocoapods.org/pods/MMPlayerView"
Related
I am trying to recreate this thing. I've created in Storyboard skeleton. Here's the idea of my code:
Fetch images from URL's array with help of the function getThumbnailFromImage
Add UIImage's with my thumbnails in array webImages
Add in ViewController reusable cell MyCollectionView
...
But here I am with this))) (Don't mind absence of Auto Layout). What am I doing wrong? I think that the problem is with reloadData() but I don't know where to put it.
ViewController:
//
// ViewController.swift
// youtube-clone
//
// Created by мас on 16.08.2022.
//
import Foundation
import UIKit
import YouTubePlayer
import AVFoundation
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var url: [URL?] = [
URL(string: "https://www.youtube.com/watch?v=KhebpuFBD14"),
URL(string: "https://www.youtube.com/watch?v=UfNdNrRHpUw"),
URL(string: "https://www.youtube.com/watch?v=CX-BdDHW0Ho"),
URL(string: "https://www.youtube.com/watch?v=NIOMtSzfpck")
]
var webImages: [UIImage] = []
var currentPage: Int = 0
#IBOutlet var myPage: UIPageControl!
#IBOutlet weak var buttonInfo: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
myPage.currentPage = 0
myPage.numberOfPages = webImages.count
}
// MARK: - Collection View Setup
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return webImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
getThumbnailFromImage(url: url[indexPath.row]!, completion: { image in
self.webImages.append(image!)
})
cell.myWebImage.image = webImages[indexPath.row]
cell.myWebImage.layer.cornerRadius = 20
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
myPage.currentPage = indexPath.row
}
// MARK: - Layout Setup // IGNORE IT
func setupLayout() {
buttonInfo.layer.cornerRadius = 25
buttonInfo.imageView!.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
}
// MARK: - Videos Thumbnail Fetcher
func getThumbnailFromImage(url: URL, completion: #escaping ((_ image: UIImage?) -> Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumbnailTime = CMTimeMake(value: 7, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil)
let thumbImage = UIImage(cgImage: cgThumbImage)
DispatchQueue.main.async {
completion(thumbImage)
}
}
catch {
print(error.localizedDescription)
}
}
}
}
Reusable Cell AKA MyCollectionCell:
import UIKit
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
}
P.s.: YouTubePlayer is custom pod from GitHub, it's not currently used.
You do NOT have to use AVAssetImageGenerator, Simply you can use Youtube API to fetch the thumbnail images as .jpg image by video id,
and each YouTube video has four generated images.
https://img.youtube.com/vi/{id}/0.jpg
https://img.youtube.com/vi/{id}/1.jpg
https://img.youtube.com/vi/{id}/2.jpg
https://img.youtube.com/vi/{id}/3.jpg
Example
https://img.youtube.com/vi/KhebpuFBD14/0.jpg
And then it is preferred to use a third party to load this image as its displayed in a list, like https://github.com/SDWebImage/SDWebImage or https://github.com/onevcat/Kingfisher and you will NOT be worry about Concurrency or caching.
A couple of thoughts:
#matt is right in the comment - getThumbnailFromImage will likely not have called the completion block by the time cellForItemAt returns.
From what is visible in the code you posted, webImages.count will still be 0 when your collection view checks numberOfItemsInSection. If the number of items is 0, cellForItemAt may never get called so the call to getThumbnailFromImage wouldn't even be reached. (I'm not sure if the white box in your screenshot is part of a cell or another view element. If a cell is being displayed, I'm assuming you're populating webImages somewhere else before the collection view gets laid out).
One way you could work around these issues is by giving each cell a URL rather than a thumbnail. That way the cell can be displayed while the image is still loading. The cell could look something like this:
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
func configure(urlString: String) {
guard let self = self, let url = URL(string: urlString) else {
return
}
getThumbnailFromImage(url: url, completion: { [weak self] image in
self?.myWebImage.image = image
})
}
// Move `getThumbnailForImage` function to here, or give the cell a delegate to call back to the VC with if you don't want any networking in the view itself
}
The cellForItemAt function in the VC would need to be changed to something like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
cell.configure(urlString: url[indexPath.row])
cell.myWebImage.layer.cornerRadius = 20 // This should probably live in the cell since the parent doesn't actually need to know about it!
return cell
}
An added benefit of this approach is that you're not referencing a separate array of images that could theoretically end up being in the wrong order if there's a mistake somewhere in the code. You could get rid of the webImages array entirely and use urls.count in numberOfItemsInSection instead - or eventually the number of elements returned from an API somewhere.
Side note - make sure you add [weak self] at the beginning of any closure that references self to avoid trying to access it after it's been deallocated! Currently the call to getThumbnailFromImage doesn't have that :)
Also, note that I changed to a guard statement for checking that the URL exists. This is much safer than force unwrapping a URL(string:) value, especially if you ever end up getting the strings from a dynamic source.
Summary
Can a UICollectionViewCell subclass prevent didSelectItemAt: indexPath being sent to the UICollectionViewDelegate for taps on some of its sub views, but to proceed as normal for others?
Use case
I have a UICollectionViewCell that represents a summary of an article. For most articles, when they are tapped, we navigate through to show the article.
However, some article summaries show an inline video preview. When the video preview is tapped, we should not navigate through, but when the other areas of the article summary are tapped (the headline), we should navigate through.
I'd like the article summary cell to be able to decide whether a tap on it should be considered as a selection.
You have to add tapGestureRecogniser on those subviews of cell on which you don't want delegate to get called.
tapGestureRecogniser selector method will get called when you will tap on those subview and gesture will not get passed to delegate.
What you need to do is to attach UITapGestureRecognizer to your view and monitor taps from it:
class MyViewController: UIViewController, UICollectionViewDataSource, MyCellDelegate {
var dataSource: [Article] = []
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ArticleCell", for: indexPath) as! MyCell
cell.article = dataSource[indexPath.row]
cell.delegate = self
return cell
}
func articleDidTap(_ article: Article) {
// do what you need
}
}
// your data model
struct Article {}
protocol MyCellDelegate: class {
func articleDidTap(_ article: Article)
}
class MyCell: UICollectionViewCell {
var article: Article! {
didSet {
// update your views here
}
}
weak var delegate: MyCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(MyCell.tap)))
}
#objc func tap() {
delegate?.articleDidTap(article)
}
}
This should work since your video view should overlap root view and prevent receiving taps from gesture recognizer.
I can't seem to figure out how to start an animation after my "Play" button has been clicked from my UICollectionViewCell. Here is some of my code below, any help please?
I have other code that pertains to my collectionView setup but not sure if you need to see it or not.
It seems, I'm only able to run the animation once the viewAppears but how do you initiate an animation well after the viewAppears?
Here is my UICollectionViewCell code:
import UIKit
class CreateCollectionViewCell: UICollectionViewCell {
var animateDelegate: AnimateScenesDelegate!
#IBOutlet weak var scenes: UIImageView!
#IBAction func scenePlay(sender: UIButton) {
animateDelegate.animateScenes()
let playButtonFromCreateCollection = scenes.image!
print("It was this button \(playButtonFromCreateCollection)")
}
}
HERE is some of my UIViewController Code:
class CreateViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, AnimateScenesDelegate {
#IBOutlet weak var StoryViewFinal: UIImageView!
var scene01_68: [UIImage] = []
func animateScenes () {
print("Play button was pressed")
StoryViewFinal.animationImages = scene01_68
StoryViewFinal.animationDuration = 15.0
StoryViewFinal.animationRepeatCount = 1
StoryViewFinal.startAnimating()
}
func loadScenes () {
for i in 1...158 {
scene01_68.append(UIImage(named: "Scene01_\(i)")!)
print(scene01_68.count)
}
}
override func viewDidAppear(animated: Bool) {
animateScenes()
super.viewDidLoad()
loadScenes ()
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
\\ OTHER CODE...
cell.animateDelegate = self
return cellA
}
It seems to be the case that you want: When a button is tapped in the cell, the view controller should perform some animation? This issue comes from needing better coordination between the cell and the view controller. Cells are just views and don't have the knowledge to do anything outside themselves.
When the view controller formats the cell in cellForItemAtIndexPath, you need to give it a "perform animation delegate" performAnimationDelegate. This is a reference back to the view controller.
protocol AnimateScenesDelegate {
func animateScenes()
}
class CreateCollectionViewCell: UICollectionViewCell {
weak var animateDelegate : AnimateScenesDelegate
#IBAction func scenePlay(sender: UIButton) {
animateDelegate?.animateScenes()
}
}
class CreateViewController: UIViewController, ... AnimateScenesDelegate {
func animateScenes() {
//Animate here ...
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//...
cell.animateDelegate = self
}
}
Note the weak var on the cell delegate, because you don't want the cell to keep the view controller alive.
This is not the only way to do this but it's established and simple. Remember that the delegate (view controller) doesn't have any information about what is calling it, so you would have to add a parameter or check if you wanted to know for example which cell is being tapped. Hope this helps.
Objective
I wanna place my (BusinessViewTableHeader: UIView) as tableView header:
tableView.tableHeaderView = BusinessViewTableHeader.instanceFromNib() as! BusinessViewTableHeader
Inside BusinessViewTableHeader there is a UICollectionView which are supposed to display images when swiped, much like the Tinder app.
This is my UIView subclass:
class BusinessViewTableHeader: UIView {
#IBOutlet var collectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
self.collectionView.delegate = self
self.collectionView.registerNib(UINib(nibName: "BusinessImageCollectionCell", bundle: nil), forCellWithReuseIdentifier: "BusinessImageCollectionCell")
}
class func instanceFromNib() -> UIView {
return UINib(nibName: "BusinessViewTableHeader", bundle: nil).instantiateWithOwner(nil, options: nil)[0] as! UIView
}
....
}
extension BusinessViewTableHeader: UICollectionViewDelegate, UICollectionViewDataSource {
....
}
Problem
I have a custom UIView xib containing a UICollectionView. The problem is that I can´t add any cells (items) to the UICollectionView. I can add items to my other UICollectionView which are placed inside a UIViewController. The first image is showing the properties for the UICollectionView inside a UIViewController, the second image is showing the UICollectionView inside a UIView xib.
[![UICollectionView in UIViewController][1]][1]
[1]: http://i.stack.imgur.com/zFCeG.png
[![UICollectionView in UIView xib][2][2]
[2]: http://i.stack.imgur.com/jKU6z.png
Question
Why am I not able to add items to the UICollectionView inside the UIView xib? How?
You can't have UICollectionViewCell when the UICollectionView is on a Nib. What you need to do is to create the UICollectionViewCell as another nib and get it registered in the class that you are using for your CollectionView.
Create a new nib, drag a UICollectionViewCell inside it, and do something like this in the class that works with your UICollectionView.
override func awakeFromNib() {
let nibName = UINib(nibName: "ClassCollectionCell", bundle:nil)
collectionView.registerNib(nibName, forCellWithReuseIdentifier: "collectionCell")
}
Remember you can add a custom class to the UICollectionViewCell so you can pass dynamic data to it.
Adding cells in a xib is not supported. If you must use a xib file, then you will need a separate xib which contains the UICollectionView cell. Storyboards may be a better solution.
It is not clear what you are trying to achieve. UICollectionView has specific means for creating headers which uses the datasource and delegate. Collection views are good for displaying items in a grid layout or other complex arrangements.
If all you need is to display a list of rows, then a UITableViewController might be an easier alternative.
Whatever the case, it is probably better to use a storyboard instead of a xib, and to subclass the UICollectionViewController or UITableViewController, rather than a subview.
Your custom class name can be entered in the identity inspector for the UIViewController or UIView:
In Swift 3.0 register nib with following method-
let nibName = UINib(nibName: "FruitCell", bundle:nil)
collectionView.register(nibName, forCellWithReuseIdentifier: "CellIdentifier")
In Swift 3.0
first Create Xibfile of UIView
import UIKit
class SubCatagoryListView:UIView , UICollectionViewDelegate , UICollectionViewDataSource
{
#IBOutlet weak var mainView: UIView!
#IBOutlet weak var btnClose: UIButton!
#IBOutlet weak var subCategoryListCollectionView: UICollectionView!
#IBOutlet weak var lblTitle: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
subCategoryListCollectionView.register(UINib(nibName: "SubcatagoryListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "SubcatagoryListCollectionViewCell")
mainView.layer.cornerRadius = 10.0
mainView.clipsToBounds = true
}
static func subCatagoryListView() -> SubCatagoryListView? {
let arr = Bundle.main.loadNibNamed("SubCatagoryListView", owner: self, options: nil)
if arr != nil {
if arr!.count > 0 {
if let view = arr![0] as? SubCatagoryListView {
return view;
}
}
}
return nil;
}
#IBAction func btnBackgroundTapped(_ sender: UIButton)
{
self.removeFromSuperview()
}
#IBAction func btnCloseTapped(_ sender: UIButton)
{
self.removeFromSuperview()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SubcatagoryListCollectionViewCell", for: indexPath) as! SubcatagoryListCollectionViewCell
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
let cellsize = CGSize(width: (subCategoryListCollectionView.bounds.size.width/3) - 10, height: 50)
return cellsize
}
}
After Create new CollectionViewCell Xib file
import UIKit
class SubcatagoryListCollectionViewCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
after create both file i am load xib on my storybord
var subcatagoryXib:SubCatagoryListView?
override func awakeFromNib() {
super.awakeFromNib()
if let subcategoryView = SubCatagoryListView.subCatagoryListView()
{
subcatagoryXib = subcategoryView
}
}
#IBAction func btnAddInterestTapped(_ sender: UIButton) {
if subcatagoryXib != nil
{
self.subcatagoryXib!.frame = CGRect(x:0, y: 0, width: self.view.frame.width , height: self.view.frame.height)
self.view.addSubview(self.subcatagoryXib!)
}
}
Same as #Pato's answer, but here is a more thorough tutorial for how to add a customized UICollectionViewCell inside a Xib file from #aestusLabs on Medium. It's a 3-5 min reading, I personally find it very helpful. It basically tells you to create another customized UICollectionViewCell with .xib, and register it in your "level 1" cell's awakeFromNib().
https://medium.com/#aestusLabs/adding-a-uicollectionviews-to-a-custom-uitableviewcell-xib-tutorial-swift-4-xcode-9-2-1ec9ce4095d3
I have a UICollectionView and want to be able to perform custom behaviour when the user scrolls through implementing the scrollView delegate methods. Is it possible to have two separate objects that act as the collectionView delegate and scrollView delegate when working with a collectionView?
You cannot have separate delegates. UICollectionView is a subclass of UIScrollView, and overrides its delegate property to change its type to UICollectionViewDelegate (which is a subtype of UIScrollViewDelegate). So you can only assign one delegate to a collection view, and it may implement any combination of UICollectionViewDelegate methods and UIScrollViewDelegate methods.
However, you can forward the UIScrollViewDelegate methods to another object without much difficulty. Here's how you'd do it in Swift; it would be very similar in Objective-C (since this is all done using the Objective-C runtime):
import UIKit
import ObjectiveC
class ViewController: UICollectionViewController {
let scrollViewDelegate = MyScrollViewDelegate()
override func respondsToSelector(aSelector: Selector) -> Bool {
if protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, false, true).types != nil || protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, true, true).types != nil {
return scrollViewDelegate.respondsToSelector(aSelector)
} else {
return super.respondsToSelector(aSelector)
}
}
override func forwardingTargetForSelector(aSelector: Selector) -> AnyObject? {
if protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, false, true).types != nil || protocol_getMethodDescription(UIScrollViewDelegate.self, aSelector, true, true).types != nil {
return scrollViewDelegate
} else {
return nil
}
}
Note that MyScrollViewDelegate probably has to be a subclass of NSObject for this to work.
If I understand you correctly, then you just need your view controller to subclass UICollectionViewController or UICollectionViewDelegate. Then you can access the scrollView delegate methods since they are inherited by the collectionView
Create subclass of UICollectionViewController and write scroll view delegates into it.
class CustomCollectionViewController: UICollectionViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
}
}
In your target class
class MyCollectionViewController: CustomCollectionViewController, UICollectionViewDelegateFlowLayout {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
}