Using TVPosterView inside UICollectionViewCell creates odd sizing issue - swift

I'm trying to create a tvOS project using UICollectionView displays camera streams.
In the UICollectionViewCell I'm using TVPosterView to display the stream/image and a title.
The problem that I'm facing currently that when the app starts all thumbnails/UICollectionViewCell seems to have proper size; First item focused with larger size than others. When I roll-out to other item/s, the earlier focused item became much smaller than the size it has when started.
Let me try to explain the situation if I'm not clear with my words.
When the app starts - Camera A focused and larger size than others:
When roll-out Camera A, it become much smaller than Camera D or Camera E; Also, the currently focused Camera B do not has a larger size anymore (if I compared with Camera A size from previous screen):
In the UICollectionView controller I have following delegate method:
extension ListingViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let thumbWidth = CGFloat(320)
let thumbHeight = CGFloat(thumbWidth * 0.7)
return CGSize(width: thumbWidth, height: thumbHeight)
}
}
In the UICollectionViewCell I basically has following codes:
internal func setupUI()
{
posterView = TVPosterView()
posterView.tag = 1100
posterView.clipsToBounds = false
addSubview(posterView)
posterView.alignToSuperView()
}
alignToSuperView() basically an extension to UIView with following codes:
public func alignToSuperView()
{
self.translatesAutoresizingMaskIntoConstraints = false
guard let margins = self.superview?.layoutMarginsGuide else {
return
}
self.leadingAnchor.constraint(equalTo: margins.leadingAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: margins.trailingAnchor).isActive = true
self.topAnchor.constraint(equalTo: margins.topAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: margins.bottomAnchor).isActive = true
self.superview?.layoutIfNeeded()
}

Related

Calendar-like UICollectionView - how to add left inset before first item only?

I have the following UICollectionView:
It has vertical scrolling, 1 section and 31 items.
It has the basic setup and I am calculating itemSize to fit exactly 7 per row.
Currently it looks like this:
However, I would like to make an inset before first item, so that the layout is even and there are the same number of items in first and last row. This is static and will always contain 31 items, so I am basically trying to add left space/inset before first item, so that it looks like this:
I have tried using a custom UICollectionViewDelegateFlowLayout method:
collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int)
But since there is only 1 section with 31 rows, it insets all of the rows, not just the first.
I know I could probably add two more "blank" items, but I feel like there is a better solution I may not be aware of. Any ideas?
EDIT: I've tried Tarun's answer, but this doesn't work. Origin of first item changes, but the rest stays as is, therefore first overlaps the second and the rest remain as they were. Shifting them all doesn't work either. I ended up with:
You need to subclass UICollectionViewFlowLayout and that will provide you a chance to customize the frame for all items within the collectionView.
import UIKit
class LeftAlignCellCollectionFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
guard let collectionView = self.collectionView else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
let leftMargin = self.sectionInset.left
let layout = collectionView.collectionViewLayout
for attribute in attributes {
if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
// Check for `indexPath.item == 0` & do what you want
// cellAttribute.frame.origin.x = 0 // 80
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
}
Now you can use this custom layout class as your flow layout like following.
let flowLayout = LeftAlignCellCollectionFlowLayout()
collectionView.collectionViewLayout = flowLayout
Following Taran's suggestion, I've decided to use a custom UICollectionViewFlowLayout. Here is a generic answer that works for any number of items in the collectionView, as well as any inset value:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = self.collectionView else { return nil }
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var newAttributes: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
if let cellAttribute = collectionView.collectionViewLayout.layoutAttributesForItem(at: attribute.indexPath) {
let itemSize = cellAttribute.frame.size.width + self.minimumInteritemSpacing
let targetOriginX = cellAttribute.frame.origin.x + CGFloat(self.itemInset) * itemSize
if targetOriginX <= collectionView.bounds.size.width {
cellAttribute.frame.origin.x = targetOriginX
} else {
let shiftedPosition = lround(Double((targetOriginX / itemSize).truncatingRemainder(dividingBy: CGFloat(self.numberOfColumns))))
cellAttribute.frame.origin.x = itemSize * CGFloat(shiftedPosition)
cellAttribute.frame.origin.y += itemSize
}
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
where:
self.itemInset is the value we want to inset from the left (2 for my initial question, but it can be any number from 0 to the number of columns-1)
self.numberOfColumns is - as the name suggests - number of columns in the collectionView. This pertains to the number of days in my example and would always be equal to 7, but one might want this to be a generic value for some other use case.
Just for the sake of the completeness, I provide a method that calculates a size for my callendar collection view, based on the number of columns (days):
private func collectionViewItemSize() -> CGSize {
let dimension = self.collectionView.frame.size.width / CGFloat(Constants.numberOfDaysInWeek) - Constants.minimumInteritemSpacing
return CGSize(width: dimension, height: dimension)
}
For me, Constants.numberOfDaysInWeek is naturally 7, and Constants.minimumInteritemSpacing is equal to 2, but those can be any numbers you desire.

Collectionview inside tableview causing issue with 2 columns

I have vertical collection-view inside tableview cell. collection view contain feature of load more too. for self sizing of collection view, i make tableview cell automaticDimension.
Also i have give height constant to collection-view. first time its loaded correctly but once i go to last cell and its load-more after reloading it create lot of space after collection view. can any one let me know what i am doing wrong here. or is there any other way around to make collection-view inside tableview self sizing so it increase tableview cell height too
**
TableviewCell Class
**
justForYouCollectionView.dataSource = self
justForYouCollectionView.delegate = self
justForYouCollectionView.isScrollEnabled = false
self.collectionHeight.constant = self.justForYouCollectionView.collectionViewLayout.collectionViewContentSize.height
justForYouCollectionView.reloadData()
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
self.layoutIfNeeded()
let contentSize = self.justForYouCollectionView.collectionViewLayout.collectionViewContentSize
return CGSize(width: contentSize.width, height: contentSize.height + 20)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == self.justForYouArray.count - 1 && self.isLoadMore {
updateNextSet()
}
}
**
CollectionViewCell Class
**
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
// Specify you want _full width_
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
// Calculate the size (height) using Auto Layout
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
// Assign the new size to the layout attributes
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
extra space can be seen in image
I have worked on that earlier all I did is, set tableview height constraint set in storyboard and drop its outlet in viewController then after populate data get array count and divide by 2 and after dividing I multiply it by CollectionViewCell height and set that height to the UITableView height constraint like this.
let count = (array.count / 2) * cellHeight
tableviewHeightConstraint.constant = CGFloat(count)
This will solve your problem.
try
let a = (yourArray.count /2 ) * heightCell
tblViewHeightConstraint.constant = CGFloat(a)

Collection view cell vertical top alignment

I have a problem in collection view(Grid Format) cell height. The grid format contains two columns overall, so basically 2 cells in each row. My collection view cell height increase according to the content inside it but the content is center aligned and not top aligned. I want to achieve the height of the cells in a row which has the greater height. How can I do that? Please advice.
https://drive.google.com/file/d/0B56cE4V6JI-RYmdQT2pIZ0hYQ2phM3Z2YmJNYU1SeXNnYTNN/view?usp=sharing
https://drive.google.com/file/d/0B56cE4V6JI-RQUdqVmV4b244cFI5SGd2TnJfbG1tckdQU21Z/view?usp=sharing
https://drive.google.com/file/d/0B56cE4V6JI-RYTYxVzJyVUp1clpJTkVqYjN6QXBPeERvVHZR/view?usp=sharing
I have added a link above which is the problem right now.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let cellsAcross: CGFloat = 2
let spaceBetweenCells: CGFloat = 10
let dim = (collectionView.bounds.width - (cellsAcross - 1) * spaceBetweenCells) / cellsAcross
print(dim) // calculating width
//calculating my content height
let toppingHeight = sizeOfString(string: PizzaMenuItems[indexPath.row].pizzaToppings, constrainedToWidth: Double(dim))
let pizzaNameHeight = sizeOfString(string: PizzaMenuItems[indexPath.row].menuName, constrainedToWidth: Double(dim))
let newHeight = toppingHeight.height + pizzaNameHeight.height + 57
// to increase my collection view height
let heightt = (view.frame.width)/2
let count = self.PizzaMenuItems.count
if self.PizzaMenuItems.count == 1 {
}
else{
if count % 2 == 0
{ //even Number
collectionViewC_Height.constant = heightt * CGFloat(self.PizzaMenuItems.count/2) + 57
} else
{ // odd Number
collectionViewC_Height.constant = heightt * CGFloat(self.PizzaMenuItems.count/2+1)}
}
return CGSize(width: dim, height: newHeight)}
Verbal work around:
In your collection cell xib don't specify UILabel on the container view.
place a UIView on the bottom layer of container view with (top:0,lead:0,trail:0,bottom:0) constraints.
Now place your UILabel with constraints (top:0,lead:0,trail:0,height:<=UIView)
This helps you to adjust your label height according to their content which help to anchor label alignment on top.

Detecting of flat landscape / portrait orientation

Is there way to detect if it is flat and landscape or flat and portrait orientation of device ?
I am asking about that becouse I have UICollectionView in my UIViewController and in Portrait mode cells are placed in one row, in line. In Landscape they are in only column - they swaped from horizontal to vertical. I have update of collection data for every 60 seconds. After this time have to call reloadData() to actualize the data and If I will leave device *inFlat orientation, layout will get destroyed, cuz of sizeForItemAt. None of conditions would be perfect.
let isPortrait = UIDevice.current.orientation.isPortrait
if !isPortrait {
return CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height / 4)
} else {
return CGSize(width: collectionView.bounds.width / 4, height: collectionView.bounds.height)
}
There is no way to place it like this:
let isFlat = UIDevice.currentorientation.isFlat
if isFlat && isPortrait {
//
}
It can't be in same time isPortrait and isFlat.
Is there any work around this ?
In my opinion, to resolve it, you should save last size of item. When our device stays in flat, sizeForItemAt will return last size of item.
var currentSize : CGSize?
override func viewDidLoad() {
super.viewDidLoad()
// In viewDidLoad, need to set default value for currentSize. Make sure in case
// your app start at flat orientation, you collectionView still has a size
currentSize = CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height / 4);
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if UIDevice.current.orientation.isPortrait {
currentSize = CGSize(width: collectionView.bounds.width, height: collectionView.bounds.height / 4)
} else if UIDevice.current.orientation.isPortrait {
currentSize = CGSize(width: collectionView.bounds.width / 4, height: collectionView.bounds.height)
}
return currentSize;
}
Only update value of currentSize when your device stays in portrait or landscape mode. If not, use current size of item.
From iOS 12 you can get directly from Apple SDK the new orientation:
.isFlat
"A Boolean value indicating whether the specified orientation is face
up or face down."
UIDeviceOrientation.isFlat
https://developer.apple.com/documentation/uikit/uideviceorientation/2981866-isflat

Keep active cell in collectionview in one place

I'm working on a tvOS application where I want the active cell of a collectionView always in the same position. In other words, I don't want the active cell to go through my collectionView but I want the collectionView to go through my active cell.
Example in the following image. The active label is always in the center of the screen while you can scroll the collectionView to get a different active label over there.
Basically a pickerview with a custom UI. But the pickerview of iOS is unfortunately not provided on tvOS...
I've already looked into UICollectionViewScrollPosition.verticallyCentered. That partially gets me there but it isn't enough. If I scroll really fast, the active item jumps further down the list and when I pause it scrolls up all the way to the center. I really want the active item to be in center of the screen at all times.
Any ideas on how to do this?
Update based on #HMHero answer
Okay, I tried to do what you told me, but can't get the scrolling to work properly. This is perhaps due to my calculation of the offset, or because (like you said) setContentOffset(, animated: ) doesn't work.
Right now I did the following and not sure where to go from here;
Disable scrolling and center last and first label
override func viewDidLoad() {
super.viewDidLoad()
//Disable scrolling
self.collectionView.isScrollEnabled = false
//Place the first and last label in the middle of the screen
self.countryCollectionView.contentInset.top = collectionView.frame.height * 0.5 - 45
self.countryCollectionView.contentInset.bottom = collectionView.frame.height * 0.5 - 45
}
Getting the position of a label to retrieve the distance from the center of the screen (offset)
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let indexPath = context.nextFocusedIndexPath,
let cell = countryCollectionView.cellForItem(at: indexPath) {
//Get the center of the next focused cell, and convert that to position in the visible part of the (tv) screen itself
let cellCenter = countryCollectionView.convert(cell.center, to: countryCollectionView.superview)
//Get the center of the collectionView
let centerView = countryCollectionView.frame.midY
//Calculate how far the next focused cell y position is from the centerview. (Offset)
let offset = cellCenter.y - centerView
}
}
The offset returns incrementals of 100 when printing. The labels' height is 90, and there is a spacing of 10. So I thought that would be correct although it runs through all the way up to 2400 (last label).
Any ideas on where to go from here?
It's been more than a year since I worked on tvOS but as I remember it should be fairly simple with a simple trick. There might be a better way to do this with updated api but this is what I did.
1) Disable to scroll on collection view isScrollEnabled = false
2) In delegate, optional public func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator), get nextFocusedIndexPath and calculate the offset of the cell to be at the center of the collection view.
3) With the offset, animate the scroll manually in the delegate callback.
I found the old post where I got an idea.
How to center UICollectionViewCell that was focused?
I ended up doing somewhat different from the answer in the thread but it was a good hint.
Okay, along with some pointers of #HMHero I've achieved the desired effect. It involved animating the contentOffset with a custom animation. This is my code;
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let previousIndexPath = context.previouslyFocusedIndexPath,
let cell = countryCollectionView.cellForItem(at: previousIndexPath) {
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
cell.contentView.alpha = 0.3
cell.contentView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
})
}
if let indexPath = context.nextFocusedIndexPath,
let cell = countryCollectionView.cellForItem(at: indexPath) {
let cellCenter = CGPoint(x: cell.bounds.origin.x + cell.bounds.size.width / 2, y: cell.bounds.origin.y + cell.bounds.size.height / 2)
let cellLocation = cell.convert(cellCenter, to: self.countryCollectionView)
let centerView = countryCollectionView.frame.midY
let contentYOffset = cellLocation.y - centerView
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
collectionView.contentOffset.y = contentYOffset
cell.contentView.alpha = 1.0
cell.contentView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
self.countryCollectionView.layoutIfNeeded()
})
}
}