Dynamic CollectionViewCell size using AutoLayout not rendering properly - ios

I currently have my UICollectionView set up in the IB like so:
The bottom constraint has it's constant changed to the height of the keyboard when the keyboard appears:
func keyboardWillShow(notification: NSNotification) {
let keyboardHeight = getKeyboardHeight(notification)
keyboardHeightConstraint.constant = keyboardHeight + 20
print(cardCollectionView.frame)
self.cardCollectionView.configureCollectionView()
self.cardCollectionView.reloadInputViews()
}
Where configureCollectionView() is:
func configureCollectionView() {
self.backgroundColor = UIColor.clearColor()
// Create the layout
let space = 10.0 as CGFloat
let flowLayout = UICollectionViewFlowLayout()
let width = (self.frame.size.width) - 4 * space
let height = (self.frame.size.height)
let edgeInsets = UIEdgeInsetsMake(0, 2 * space, 0, 2 * space)
// Set top and bottom margins if scrolling horizontally, left and right margins if scrolling vertically
flowLayout.minimumLineSpacing = space
flowLayout.minimumInteritemSpacing = 0
// Set horizontal scrolling
flowLayout.scrollDirection = .Horizontal
// Set edge insets
flowLayout.sectionInset = edgeInsets
flowLayout.itemSize = CGSizeMake(width, height)
self.setCollectionViewLayout(flowLayout, animated: true)
// Always allow it to scroll
self.alwaysBounceHorizontal = true
}
Now the problem arises when the keyboard appears:
When the keyboard first appears the CollectionView gets resized and then configureCollectionView() is called. As a result the CollectionViewCell should also get resized but instead it looks like it's being clipped by the bounds of the resized CollectionView.
This can be seen as in the 2nd image in the GIF (after the keyboard appears) there are no rounded corners on the bottom and the 4 buttons are there.
It seems like when I click another TextView the CollectionViewCell goes back to it's correct size, although it still has an alpha = 1 even though nowhere in my code did I set the alpha to one.
To get my final desired result, I had to dismiss the keyboard.
What is wrong with my implementation, what can I do different to achieve my desired result when the keyboard is shown instead of having to go through all of the above?

Related

Animate the height of a UIScrollView based on it's content

My situation:
I have a horizontal ScrollView containing a StackView.
Inside this StackView there are some Views, that can be expanded/collapsed.
When I want to expand one of these Views, I first unhide some subViews in the View. After that I need to change the height of the ScrollView based on the new height of this View.
But this is not working...
I try this code:
UIView.animate(withDuration: 0.3) { [self] in
// Toggle hight of all subViews
stackView.arrangedSubviews.forEach { itemView in
guard let itemView = itemView as? MyView else { return }
itemView.toggleView()
}
// Now update the hight of the StackView
// But here the hight is always from the previous toggle
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print(height)
heightConstraint.constant = height
}
This code nicely animates, but always to the wrong height.
So the ScrollView animates to collapsed when it should be expanded and expanded when it should be collapsed.
Anyone with on idea how to solve this?
The problem is that, whatever you are doing here:
itemView.toggleView()
may have done something to change the height a view, but then you immediately call:
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
before UIKit has updated the frames.
So, you can either track your own height property, or...
get the frame heights after the update - such as with:
DispatchQueue.main.async {
let height = self.stackView.arrangedSubviews.map {$0.frame.size.height}.max() ?? 0.0
print("h", height)
self.scrollHeightConstraint.constant = height
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}

Get the default shrunk and expanded height of large title navigation bar

I have enabled large titles for the navigation bar with:
navigationController?.navigationBar.prefersLargeTitles = true
This makes the navigation bar start with an expanded height, and shrink as the user scrolls down.
Now, I want to add a subview inside the navigation bar that resizes, based on how tall the navigation bar is. To do this, I will need to get both the maximum and minimum height of the navigation bar, so I can calculate the fraction of how much it's expanded.
I can get the current height of the navigation bar like this:
guard let height = navigationController?.navigationBar.frame.height else { return }
print("Navigation height: \(height)")
I'm calling this inside scrollViewDidScroll, and as I'm scrolling, it seems that the expanded height is around 96 and the shrunk height is around 44. However, I don't want to hardcode values.
iPhone 12
Expanded (96.33)
Shrunk (44)
iPhone 8
Expanded (96.5)
Shrunk (44)
I am also only able to get these values when the user physically scrolls up and down, which won't work in production. And even if I forced the user to scroll, it's still too late, because I need to know both heights in advance so I can insert my resizing subview.
I want to get these values, but without hardcoding or scrolling
Is there any way I can get the height of both the shrunk and expanded navigation bar?
Came across my own question a year later. The other answer didn't work, so I used the view hierarchy.
It seems that the shrunk appearance is embedded in a class called _UINavigationBarContentView. Since this is a private class, I can't directly access it. But, its y origin is 0 and it has a UILabel inside it. That's all I need to know!
extension UINavigationBar {
func getCompactHeight() -> CGFloat {
/// Loop through the navigation bar's subviews.
for subview in subviews {
/// Check if the subview is pinned to the top (compact bar) and contains a title label
if subview.frame.origin.y == 0 && subview.subviews.contains(where: { $0 is UILabel }) {
return subview.bounds.height
}
}
return 0
}
}
Usage:
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Navigation"
if
let navigationBar = navigationController?.navigationBar,
let window = UIApplication.shared.keyWindow
{
navigationBar.prefersLargeTitles = true /// Enable large titles.
let compactHeight = navigationBar.getCompactHeight() // 44 on iPhone 11
let statusBarHeight = window.safeAreaInsets.top // 44 on iPhone 11
let navigationBarHeight = compactHeight + statusBarHeight
print(navigationBarHeight) // Result: 88.0
}
}
The drawback of this answer is if Apple changes UINavigationBar's internals, it might not work. Good enough for me though.
Using following extension u can get extra height
extension UINavigationBar
{
var largeTitleHeight: CGFloat {
let maxSize = self.subviews
.filter { $0.frame.origin.y > 0 }
.max { $0.frame.origin.y < $1.frame.origin.y }
.map { $0.frame.size }
return maxSize?.height ?? 0
}
}
And I said earlier u can get extended height by following
guard let height = navigationController?.navigationBar.frame.maxY else { return }
print("Navigation height: \(height)")
let window = UIApplication.shared.keyWindow
let topPadding = window?.safeAreaInsets.top
let extendedHeight = height - topPadding
You can get shrunk height by subtracting difference from extended height
guard let difference = navigationController?.navigationBar.lagreTitleHeight else {return}
let shrunkHeight = extendedHeight - difference

Autolayout working wrong

I'm working on profile screen, there is a UITableView inside the ViewController and I placed all user info inside the UITableView Header. To make screen more accurate due to different sizes of "About" label I use Autolayout with this code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
sizeHeaderToFit()
}
func sizeHeaderToFit() {
let headerView = tableView.tableHeaderView!
headerView.setNeedsLayout()
headerView.layoutIfNeeded()
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
var frame = headerView.frame
frame.size.height = height
headerView.frame = frame
tableView.tableHeaderView = headerView
}
Everything works fine, but the last string of About label is not showing, text interrupts (I set word wrap):
Add a height constraint on tableview header and try to change height constraint where you are changing frame. As you are using autolayout so you should play with the constraints rather than playing directly with the frames
you can follow this tutorial
https://useyourloaf.com/blog/variable-height-table-view-header/
Hope it will help you.

CollectionView Cell gets shifted to right after horizontal Scrolling

I have a collectionView Cell that is of the same size of my CollectionView i.e. one Cell at a time is displayed on the screen and I want minimum separation of 10 between cells, the problem is when I scroll the cell the cells aren't properly fitting the whole screen, and the shifting of cell is increased after every scroll. (Check screenshots for better understanding)
I assume you have set pagingEnabled for the collection view. It inherits this property from UIScrollView (because UICollectionView is a subclass of UIScrollView).
The problem is that the collection view uses its own width (320 points in your post) as the width of a page. Each of your cells is the same width as the collection view, but then you have a 10 point “gutter” between the cells. This means that the distance from the left edge of cell 0 to the left edge of cell 1 is 320 + 10 = 330 points. So when you scroll to show cell 1, the collection view stops scrolling at offset 320 (its own width), but cell 1 actually starts at offset 330.
The easiest fix is probably to turn off pagingEnabled and implement paging yourself by overriding scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) in your collection view delegate, like this:
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { return }
let pageWidth = scrollView.bounds.size.width + flowLayout.minimumInteritemSpacing
let currentPageNumber = round(scrollView.contentOffset.x / pageWidth)
let maxPageNumber = CGFloat(collectionView?.numberOfItems(inSection: 0) ?? 0)
// Don't turn more than one more page when decelerating, and don't go beyond the first or last page.
var pageNumber = round(targetContentOffset.pointee.x / pageWidth)
pageNumber = max(0, currentPageNumber - 1, pageNumber)
pageNumber = min(maxPageNumber, currentPageNumber + 1, pageNumber)
targetContentOffset.pointee.x = pageNumber * pageWidth
}
You'll also want to set the item size to match the device screen size, and set the deceleration rate to fast:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout, let collectionView = collectionView else { return }
flowLayout.itemSize = collectionView.bounds.size
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
Result:
The reason is that you have not taken the minimum separation of 10 while setting width of the cell(320). Hence this 10 is getting accumulated each time to scroll.
So you have to subtract 10 out of 320 while setting the width, so the width should be 310 IMO.

UITextView with other elements (similar to Evernote) iOS

I'm trying to implement a view very similar to Evernote's screen in which you add a New Note.
It seems like a UITableView embedded in a NavigationController. This tableview contains static cells (2 or 3) with the bottom one being a UITextView in which you add the content of the note, but when you scroll on the textView, the other cells that contain a textField and another control.
How can this be achieved? I know that Apple doesn't recommend a TextView inside a ScrollView, and doing it with table view it gets a bit weird with all the scrolling from the table and text view.
Here are some examples:
Any suggestions?
Thank you!
Firstly, They disabled text view scrolling and set its size to about screen size. Secondly, once text view's text is out of frame, expand it(calculate its size again).
So I found my problem, when I was setting the constraints for the content view (view inside scrollview) I set an Equal value for its height. To fix it I just made that relationship to Greater or Equal than... it now expands.
The other problem now is that when showing the keyboard it is not scrolling to the text I tap to. (The insets are properly setup though)
// MARK: Notififations from the Keyboard
func didShowKeyboard (notification: NSNotification) {
if momentTextView.isFirstResponder() {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.size.height, right: 0)
scrollView.scrollIndicatorInsets = scrollView.contentInset
let caretPosition = momentTextView.caretRectForPosition(momentTextView.selectedTextRange!.start)
let newHeight = caretPosition.height * 1.5
let newCaretPosition = CGRect(x: caretPosition.origin.x, y: caretPosition.origin.y, width: caretPosition.width, height: newHeight)
scrollView.scrollRectToVisible(newCaretPosition, animated: true)
}
}
}
func willHideKeyboard (notification: NSNotification) {
if momentTextView.isFirstResponder() {
scrollView.contentInset = UIEdgeInsetsZero
scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
}
}

Resources