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
}
}
Related
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"
I've been playing around with the focus api in UICollectionView and I've been unable to get the delegate methods to trigger. Unfortunately the documentation is rather sparse.
I have a custom UIViewController subclass in which I implement the follow:
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return storedCellReference != nil ? [storedCellReference] : super.preferredFocusEnvironments
}
func collectionView(_ collectionView: UICollectionView,
canFocusItemAt indexPath: IndexPath) -> Bool { return true }
func collectionView(UICollectionView, shouldUpdateFocusIn: UICollectionViewFocusUpdateContext) -> Bool { return true }
func collectionView(UICollectionView, didUpdateFocusIn: UICollectionViewFocusUpdateContext, with: UIFocusAnimationCoordinator) {
// custom animation code here
}
In the collection view delegate didSelect handler, I'm storing a reference to the cell and calling view.setNeedsFocusUpdate() yet none of the focus api methods are hitting breakpoints. Anyone have any pointers?
I am trying to achieve the following image by using the UICollectionViewCell.
But i have got 3 different cases for this:
A) A particular type of journey only onward journey time is visible.
B) A particular type of journey both onward journey and return journey is visible(as shown in the below image).
C) A particular type of journey has 2 onward journey and 2 return journey time.
So i am trying to use the collection view for this. Am i correct in doing this approach and how to achieve this logic using collection view?
I would probably subclass my UICollectionViewCell, define all necessary layout type enums and pass it in my cellForItemAtIndexPath as a parameter in a custom method, so the custom collection view cell knows how to layout itself.
Something like this:
enum MyLayoutType
{
case layoutA
case layoutB
case layoutC
}
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource
{
override func viewDidLoad()
{
super.viewDidLoad()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
// MARK: Collection View DataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCustomCVCell", for: indexPath) as! MyCustomCVCell
// Decide here what layout type you would like to set
cell.setUpWithType(layoutType: .layoutA)
return cell
}
}
And then, in your custom collection view cell subclass (don't forget to assign the Custom class in your interface builder file):
class MyCustomCVCell: UICollectionViewCell
{
func setUpWithType(layoutType: MyLayoutType)
{
switch layoutType
{
case .layoutA:
self.backgroundColor = .red
case .layoutB:
self.backgroundColor = .yellow
case .layoutC:
self.backgroundColor = .blue
}
}
}
I have a main view controller executed first which looks something like below,
MainViewController
class MainViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: DataSource!
SomeAction().call() {
self.dataSource.insert(message: result!, index: 0)
}
}
DataSource of the collectionview
class DataSource: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var conversation: [messageWrapper] = []
override init() {
super.init()
}
public func insert(message: messageWrapper, index: Int) {
self.conversation.insert(message, at: index)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return conversation.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let textViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "textViewCell", for: indexPath) as! TextCollectionViewCell
let description = conversation[indexPath.row].description
textViewCell.textView.text = description
return textViewCell
}
}
So, when the MainViewController is executed there is one added to the datasource of the collectionview which works perfectly fine.
Problem
Now, I have another class which looks something like
SomeController
open class SomeController {
let dataSource: DataSource = DataSource()
public func createEvent() {
self.dataSource.insert(message: result!, index: 1)
}
}
When I add some data from the above controller, the conversation is empty which doesn't have the existing one record and throw Error: Array index out of range. I can understand that it is because I have again instantiated the DataSource.
How to add/remove data from other class?
Is it the best practice to do it?
Can I make the conversation as global variable?
The Datasource class had been re initialised with it's default nil value, you have to pass the updated class to the next controller to access its updated state.
How to add/remove data from other class?
You should use class Datasource: NSObject {
And your collection view delegates on your viewcontroller class.
pass your dataSource inside prepareForSegue
Is it the best practice to do it?
Yes
Can I make the conversation as global variable?
No, best to use models / mvc style. Data on your models, ui on your viewcontrollers.
It seems your initial count is 1 but you insert at index 1(out of index)
Use self.dataSource.insert(message: result!, index: 0) insteaded
Or use append.
I wrote didSelectItemAtIndexPath func in UICollectionViewCell to select a UICollectionViewController. I wrote the code in two ways but it doesnot work at all. Also, I don't getting an error in there.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item == 0 {
let layout = UICollectionViewFlowLayout()
let controller1 = DigitalSLRCon(collectionViewLayout: layout)
let nav = UINavigationController()
nav.pushViewController(controller1, animated: true)
// OR
let layout = UICollectionViewFlowLayout()
let controller1 = DigitalSLRCon(collectionViewLayout: layout)
navigationController?.pushViewController(controller1, animated: true)
}
didSelectItemAtIndexpathis a function of UICollectionViewDelegate - you don't implement it in the cell, but in your CollectionView's delegate, so probably the view controller which contains the collection view.
Make the UIViewController that holds the collection view conform to UICollectionViewDelegate protocol
Assign that view controller to collection view's delegate property
Implement the didSelectItemAtIndexpath function in the view controller
Please remove the delegates from the UICollectionViewCell The delegates are not for the cell but the UICollectionView handler
If you want to change some element in the cell
you can override the default variables in the cell like this
override var isSelected: Bool {
didSet {
if isSelected {
//do something
} else {
//not selected
}
}
}
override var isHighlighted: Bool {
didSet {
if isHighlighted {
//do something
} else {
//not selected
}
}
}