Keep active cell in collectionview in one place - swift

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()
})
}
}

Related

Using TVPosterView inside UICollectionViewCell creates odd sizing issue

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()
}

Blinking table view cell

After adding a new set of data to a table view, i'd like to have the cell that displays the new data blink green for 5 seconds. I used an extension to UIView. I call the extension within "cellForRowAtIndexPath" but it turns the cell green permanently and it never turns back to default background color again. Heres the extension i am using:
extension UIView {
func blink(duration: TimeInterval = 0.5, delay: TimeInterval = 0.0, alpha: CGFloat = 0.0) {
UIView.animate(withDuration: duration, delay: delay, options: [.curveEaseInOut, .repeat, .autoreverse], animations: {
self.backgroundColor = .systemGreen
})
}
}
self.blink() is called within "cellForRowAtIndexPath"
I have no idea why the animation isnt working, does anyone have a solution?
Heres a working answer i now managed to find for my problem.
First the blinking extension:
Using .repeat .autoreverse i wasnt able to stop the animation at the exact time that it turned back to default color which didnt look nice. So I decided to write the animation myself and let the cell blink 3 times this way:
extension UIView {
func blink(duration: TimeInterval = 1.4, repetitions: Int = 3) {
var remainingReps = repetitions
UIView.animate(withDuration: duration, animations: {
self.backgroundColor = Colors.primaryAlpha
}) { (error) in
UIView.animate(withDuration: duration, animations: {
self.backgroundColor = .systemBackground
}) { (error) in
remainingReps -= 1
if remainingReps > 0 {
self.blink(duration: duration, repetitions: remainingReps)
}
}
}
}
}
You can choose yourself how many times you want the cell to blink by its "repetitions" parameter.
Now in your tableview add following code for tableView willDisplay cell:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let dataSet = myData[indexPath.row]
let myCell = cell as! YourCellType
myCell.checkIfShouldBlink(data: dataSet)
}
}
Finally within your TableViewCell Class:
func checkIfShouldBlink(data: MyDataObject){
//Here you can implement any kind of logic to have the cell blink depending
//on your dataSet. In my case for exmaple, if the timestamp of the data is
//less than 5 secs old (new Data) i have the cell blink.
if data.whatEverLogicYouWantToPutHere{
self.blink()
}
}

Animating UIView always goes to top left, even when the center is somewhere else

I am trying to animate a UIView shrinking and moving to a different center point. It starts when the user taps a certain UICollectionViewCell, a new UIView is made, centred on the cell, and starts expanding until it fills the entire screen. This works fine. I store the original center point of the cell in the new UIView (custom class with property originCenter).
let expandingCellView = SlideOverView(frame: cell.bounds)
expandingCellView.center = collectionView.convert(cell.center, to: self)
expandingCellView.textLabel.text = cell.textLabel.text
expandingCellView.originWidth = cell.bounds.width
expandingCellView.originHeight = cell.bounds.height
expandingCellView.originCenter = self.convert(expandingCellView.center, to: self)
expandingCellView.originView = self
self.addSubview(expandingCellView)
expandingCellView.widthConstraint.constant = self.frame.width
expandingCellView.heightConstraint.constant = self.frame.height
UIView.animate(withDuration: 0.3, animations: {
expandingCellView.center = self.center
expandingCellView.layoutIfNeeded()
})
This code works perfectly fine. Now I have added a button to that expanded view, which executes the following code:
widthConstraint.constant = originWidth
heightConstraint.constant = originHeight
print(self.convert(self.originCenter, to: nil))
UIView.animate(withDuration: 0.5, animations: {
self.center = self.convert(self.originCenter, to: nil)
self.layoutIfNeeded()
}, completion: { (done) in
// self.removeFromSuperview()
})
The shrinking of the view to its original size works fine. The only thing that doesn't work is the centering. I take the original cell centerpoint, and convert it to this views coordinate system. This produces coordinates which I think are correct. However all the views just move to the top left of the screen.
Below is a link to a screen recording. The first UIView prints its new centerpoint as (208.75, 411.75) and the second UIView I open prints its center as (567.25, 411.75). These values seem correct to me, however they don't move to this point, as you can see in the video. Any way I can fix this?
Even when setting the new center to for instance CGPoint(x: 500, y: 500), the view still moves to x = 149.5 and y = 149.5
Video: https://streamable.com/pxemc
Create a var that has a weak reference to the cell
weak var selectedCell: UICollectionViewCell!
assign the selected cell in CollectionView delegate didSelectItemAt call
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItemAtIndexPath(indexPath)
}
after add expandingCellView as subview do this to make it go from cell size to full screen
// make expanding view the same size as cell
expandingCellView.frame = selectedCell.frame
// animate
UIView.animate(withDuration: 0.5, animations: {
self.expandingCellView.layoutIfNeeded()
self.expandingCellView.frame = self.view.frame
}, completion: { (_) in
self.expandingCellView.layoutIfNeeded()
})
just reverse it to make it small again like the cell size
// animate
UIView.animate(withDuration: 0.5, animations: {
self.expandingCellView.layoutIfNeeded()
self.expandingCellView.frame = self.selectedCell.frame
}, completion: { (_) in
self.expandingCellView.layoutIfNeeded()
})
frame use global position.
frame vs bounds

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)

Calculate & move UIView on keyboard(show/hide)

I am trying to calculate the position to move a UITextField along with its parent UIView if the keyboard is overlapping the field and move back to its original position after keyboard is closed.
I have already tried https://github.com/hackiftekhar/IQKeyboardManager and it does not work in my particular case.
To explain the problem, please refer two attached screenshot, one when keyboard is opened and another when it is closed.
As you can see, on keyboard open, the text field is overlapping the keyboard, I want to move the text field along with popup view to readjust and sit above the keyboard.
Here is what I tried.
func textFieldDidBeginEditing(_ textField: UITextField) {
self.startOriginY = self.frame.origin.y
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
}
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let screenHeight = self.backgroundView.frame.height
let viewHeight = self.frame.height
let diffHeight = screenHeight - viewHeight - keyboardHeight
if diffHeight < 0 {
self.frame.origin.y = -self.textField.frame.height
}
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.frame.origin.y = self.startOriginY
}
This code moves the view to incorrect position. I am trying to figure out how to calculate the correct position to move the view and remove keyboard overlap.
What is the best way to go about solving this problem?
Thank you.
Basically you need to embedded your view inside a scroll view and use Apple's example to handle the adjustment by altering the bottom content inset of the scroll view:
Embedded inside a scroll view
Register for keyboard notifications
Implement logic to handle the notifications by altering the bottom content inset
I have converted Apple's code snipe to Swift.
Keyboard will show:
if let info = notification.userInfo,
let size = (info[UIKeyboardFrameEndUserInfoKey] as AnyObject?) {
let newSize = size.cgRectValue.size
let contentInset = UIEdgeInsetsMake(0.0, 0.0, newSize.height, 0.0)
scrollView.contentInset = contentInset
}
Keyboard will hide:
let contentInset = UIEdgeInsets.zero
scrollView.contentInset = contentInset
scrollView.scrollIndicatorInsets = contentInset
You can also add hard-coded offset to the newSize.height i.e: newSize.height + 20