How do you determine spacing between cells in UICollectionView flowLayout - iphone
I have a UICollectionView with a flow layout and each cell is a square. How do I determine the spacing between each cells on each row? I can't seem to find the appropriate settings for this. I see there's a min spacing attributes on the nib file for a collection view, but I set this to 0 and the cells doesn't even stick.
Any other idea?
Update: Swift version of this answer: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
Taking #matt's lead I modified his code to insure that items are ALWAYS left aligned. I found that if an item ended up on a line by itself, it would be centered by the flow layout. I made the following changes to address this issue.
This situation would only ever occur if you have cells that vary in width, which could result in a layout like the following. The last line always left aligns due to the behavior of UICollectionViewFlowLayout, the issue lies in items that are by themselves in any line but the last one.
With #matt's code I was seeing.
In that example we see that cells get centered if they end up on the line by themselves. The code below insures your collection view would look like this.
#import "CWDLeftAlignedCollectionViewFlowLayout.h"
const NSInteger kMaxCellSpacing = 9;
#implementation CWDLeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
if (nil == attributes.representedElementKind) {
NSIndexPath* indexPath = attributes.indexPath;
attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
}
}
return attributesToReturn;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* currentItemAttributes =
[super layoutAttributesForItemAtIndexPath:indexPath];
UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];
CGRect currentFrame = currentItemAttributes.frame;
if (indexPath.item == 0) { // first item of section
currentFrame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = currentFrame;
return currentItemAttributes;
}
NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
CGFloat previousFrameRightPoint = CGRectGetMaxX(previousFrame) + kMaxCellSpacing;
CGRect strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
self.collectionView.frame.size.width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it's own new line
currentFrame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = currentFrame;
return currentItemAttributes;
}
currentFrame.origin.x = previousFrameRightPoint;
currentItemAttributes.frame = currentFrame;
return currentItemAttributes;
}
#end
To get a maximum interitem spacing, subclass UICollectionViewFlowLayout and override layoutAttributesForElementsInRect: and layoutAttributesForItemAtIndexPath:.
For example, a common problem is this: the rows of a collection view are right-and-left justified, except for the last line which is left-justified. Let's say we want all the lines to be left-justified, so that the space between them is, let's say, 10 points. Here's an easy way (in your UICollectionViewFlowLayout subclass):
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray* arr = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes* atts in arr) {
if (nil == atts.representedElementKind) {
NSIndexPath* ip = atts.indexPath;
atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
}
}
return arr;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewLayoutAttributes* atts =
[super layoutAttributesForItemAtIndexPath:indexPath];
if (indexPath.item == 0) // degenerate case 1, first item of section
return atts;
NSIndexPath* ipPrev =
[NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;
if (atts.frame.origin.x <= rightPrev) // degenerate case 2, first item of line
return atts;
CGRect f = atts.frame;
f.origin.x = rightPrev;
atts.frame = f;
return atts;
}
The reason this is so easy is that we aren't really performing the heavy lifting of the layout; we are leveraging the layout work that UICollectionViewFlowLayout has already done for us. It has already decided how many items go in each line; we're just reading those lines and shoving the items together, if you see what I mean.
There are a few things to consider:
Try changing the minimum spacing in IB, but leave the cursor in that field. Notice that Xcode doesn't immediately mark the document as changed. When you click in a different field, though, Xcode does notice that the document is changed and marks it so in the file navigator. So, be sure to tab or click over to a different field after making a change.
Save your storyboard/xib file after making a change, and be sure to rebuild the app. It's not hard to miss that step, and then you're left scratching your head wondering why your changes didn't seem to have any effect.
UICollectionViewFlowLayout has a minimumInteritemSpacing property, which is what you're setting in IB. But the collection's delegate can also have a method to determine the inter-item spacing. That method trump's the layout's property, so if you implement it in your delegate your layout's property won't be used.
Remember that the spacing there is a minimum spacing. The layout will use that number (whether it comes from the property or from the delegate method) as the smallest allowable space, but it may use a larger space if it has space leftover on the line. So if, for example, you set the minimum spacing to 0, you may still see a few pixels between items. If you want more control over exactly how the items are spaced you should probably use a different layout (possibly one of your own creation).
A little bit of maths does the trick more easily. The code wrote by Chris Wagner is horrible because it calls the layout attributes of each previous items. So the more you scroll, the more it's slow...
Just use modulo like this (I'm using my minimumInteritemSpacing value as a max value too):
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);
if (indexPath.item % numberOfItemsPerLine != 0)
{
NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);
CGRect itemFrame = [currentItemAttributes frame];
itemFrame.origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
currentItemAttributes.frame = itemFrame;
}
return currentItemAttributes;
}
An easy way to left-justify is to modify layoutAttributesForElementsInRect: in your subclass of UICollectionViewFlowLayout:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
{
//fix blur
CGRect theFrame = CGRectIntegral(layoutAttributes.frame);
//left justify
if(prevFrame.origin.x > -FLT_MAX &&
prevFrame.origin.y >= theFrame.origin.y &&
prevFrame.origin.y <= theFrame.origin.y) //workaround for float == warning
{
theFrame.origin.x = prevFrame.origin.x +
prevFrame.size.width +
EXACT_SPACE_BETWEEN_ITEMS;
}
prevFrame = theFrame;
layoutAttributes.frame = theFrame;
}
return allLayoutAttributes;
}
Clean Swift solution, from an history of evolution:
there was matt answer
there was Chris Wagner lone items fix
there was mokagio sectionInset and minimumInteritemSpacing improvement
there was fanpyi Swift version
now here is a simplified and clean version of mine:
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
}
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
collectionView != nil else {
// should never happen
return nil
}
// if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
if indexPath.item != 0,
let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) {
// the next item on a line
currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
} else {
// the first item on a line
currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
}
return currentItemAttributes
}
func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
}
func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
}
}
Usage: the spacing between items is determined by delegate's collectionView (_:layout:minimumInteritemSpacingForSectionAt:).
I put it on github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout, where I actually added a feature of supporting both scroll directions (horizontal and vertical).
The swift version of Chris solution.
class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
var maxCellSpacing = 14.0
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexPath = attributes.indexPath
attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
}
}
return attributesToReturn;
}
return super.layoutAttributesForElementsInRect(rect)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
let sectionInset = collectionViewFlowLayout.sectionInset
if (indexPath.item == 0) { // first item of section
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
let previousFrameRightPoint = Double(previousFrame.origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing
let currentFrame = currentItemAttributes.frame
var width : CGFloat = 0.0
if let collectionViewWidth = self.collectionView?.frame.size.width {
width = collectionViewWidth
}
let strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it's own new line
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
var frame = currentItemAttributes.frame;
frame.origin.x = CGFloat(previousFrameRightPoint)
currentItemAttributes.frame = frame;
}
return currentItemAttributes;
}
}
To use it do the following:
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
var layout = PazLeftAlignedCollectionViewFlowLayout()
layout.itemSize = CGSizeMake(220.0, 230.0)
layout.minimumLineSpacing = 12.0
return layout
}
The "problem" with UICollectionViewFlowLayout is that it applies a justified align to the cells: The first cell in a row is left-aligned, the last cell in a row is right-aligned and all other cells in between are evenly distributed with an equal spacing that's greater than the minimumInteritemSpacing.
There are already many great answers to this post that solve this problem by subclassing UICollectionViewFlowLayout. As a result you get a layout that aligns the cells left. Another valid solution to distribute the cells with a constant spacing is to align the cells right.
AlignedCollectionViewFlowLayout
I've created a UICollectionViewFlowLayout subclass as well that follows a similar idea as suggested by matt and Chris Wagner that can either align the cells
⬅︎ left:
or ➡︎ right:
You can simply download it from here, add the layout file to your project and set AlignedCollectionViewFlowLayout as your collection view's layout class:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout
How it works (for left-aligned cells):
+---------+----------------------------------------------------------------+---------+
| | | |
| | +------------+ | |
| | | | | |
| section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
| inset | |intersection| | | line rect | inset |
| |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
| (left) | | | current item | (right) |
| | +------------+ | |
| | previous item | |
+---------+----------------------------------------------------------------+---------+
The concept here is to check if the current cell with index i and the previous cell with the index i−1 occupy the same line.
If they don't the cell with index i is the left most cell in the line.
→ Move the cell to the left edge of the collection view (without changing its vertical position).
If they do, the cell with index i is not the left most cell in the line.
→ Get the previous cell's frame (with the index i−1) and move the current cell next to it.
For right-aligned cells...
... you do the same vice-versa, i.e. you check the next cell with the index i+1 instead.
You can do it in two ways.
Firstly, do some modification in layoutAttributesForItem,
get the current attributes's layout via the previous layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame.
layoutAttributesForItem(at:): This method provides on demand layout information to the collection view. You need to override it and return the layout attributes for the item at the requested indexPath.
Secondly, new some attributes via UICollectionViewLayoutAttributes(forCellWith: indexPath),layout them wherever you want.
And the math is a little larger, because it performs the heavy lifting of the layout.
layoutAttributesForElements(in:): In this method you need to return the layout attributes for all the items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.
You can check My repo
you can put items left aligned
you can put items right aligned
you can put items right aligned and items reversed
A cleaner swift version for people interested, based on Chris Wagner's answer:
class AlignLeftFlowLayout: UICollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(9.0)
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes.frame
curAttributes.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
Here it is for NSCollectionViewFlowLayout
class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(2.0)
override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes!.frame
curAttributes!.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes!.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
a swift version base on mokagio:https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
if let attributesToReturn = attributesToReturn {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
if let attr = layoutAttributesForItemAtIndexPath(indexpath) {
attributes.frame = attr.frame
}
}
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
if let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){
let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section)
let isFirstItemInSection = indexPath.item == 0;
let layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right;
if (isFirstItemInSection) {
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero
let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
let currentFrame = currentItemAttributes.frame;
let strecthedCurrentFrame = CGRectMake(sectionInset.left,
currentFrame.origin.y,
layoutWidth,
currentFrame.size.height)
// if the current frame, once left aligned to the left and stretched to the full collection view
// widht intersects the previous frame then they are on the same line
let isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)
if (isFirstItemInRow) {
// make sure the first item on a line is left aligned
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
var frame = currentItemAttributes.frame;
frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section)
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
return nil
}
func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex)
}
}
return self.minimumInteritemSpacing
}
func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index)
}
}
return self.sectionInset
}
}
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.
How to create grid of squares with no spaces between them?
I want a grid of squares with no spaces between them. Currently, I use custom UICollectionViewLayout: class BoardViewLayout: UICollectionViewLayout { override func collectionViewContentSize() -> CGSize { return self.collectionView!.frame.size } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let boardSize = Int(sqrt(Double(self.collectionView!.numberOfItemsInSection(0)))) let index = indexPath.row let size = self.collectionView!.frame.width / CGFloat(boardSize) let xIndex = CGFloat(index%boardSize) let yIndex = CGFloat(index/boardSize) let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath) attributes.frame = CGRect(origin: CGPointMake(size*xIndex, size*yIndex), size: CGSizeMake(size, size)) return attributes } override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var attributesArray = [UICollectionViewLayoutAttributes]() for i in 0..<self.collectionView!.numberOfItemsInSection(0) { attributesArray.append(self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: i, inSection: 0))!) } return attributesArray } } It looks like this: screenshot of the result I need to get rid of the spaces between third and fourth row and third and fourth column (see the screenshot). Later, there will be square images instead of only colours (borders won't change) and there will be background that has to perfectly fit the images (so it looks like one image if all squares are filled and borders disappear) I know it strongly depends on how system scales images etc., but there must be way to do this. Thank you for help
You just need to set your minimum spacing to be 0: - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 0.0; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section{ return 0.0; }
Hide Background View from Table View Controller
I have just added a placeholder label to display text when my table view is empty however I can't figure out how to dismiss the background view once a new cell has been added. This is how the table view looks once a cell has been added: This is the code I am using to display the label: override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete method implementation. // Return the number of rows in the section. println(arrayObject.paymentsArray().count) if arrayObject.paymentsArray().count == 0 { backgroundLabel.text = "You haven't added any transactions yet. Tap the add button to add a new transaction." backgroundLabel.frame = CGRectMake(0, 0, self.view.bounds.width, self.view.bounds.height) backgroundLabel.numberOfLines = 0 backgroundLabel.textAlignment = NSTextAlignment.Center backgroundLabel.sizeToFit() self.tableView.backgroundView = backgroundLabel self.tableView.separatorStyle = UITableViewCellSeparatorStyle.None return 0 } else { return arrayObject.paymentsArray().count } } How can I dismiss the background label once a the arrayObject.paymentsArray().count does not equal 0? EDIT: I changed the code to this: override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int { // #warning Incomplete method implementation. // Return the number of rows in the section. println(arrayObject.paymentsArray().count) if arrayObject.paymentsArray().count == 0 { backgroundLabel.text = "You haven't added any transactions yet. Tap the add button to add a new transaction." backgroundLabel.frame = CGRectMake(0, 0, self.view.bounds.width, self.view.bounds.height) backgroundLabel.numberOfLines = 0 backgroundLabel.textAlignment = NSTextAlignment.Center backgroundLabel.sizeToFit() backgroundLabel.hidden = false self.tableView.backgroundView = backgroundLabel self.tableView.separatorStyle = UITableViewCellSeparatorStyle.None return 0 } else { backgroundLabel.hidden = true return arrayObject.paymentsArray().count } } This now hides the message as intended however the table view has lost the dividing lines in between the cells.
You need to keep a reference to your background label (say as a property of your viewController) and then set hidden equal to true e.g. self.backgroundLabel.hidden = true on the label in your else clause.else { self.backgroundLabel.hidden = true return arrayObject.paymentsArray().count } To get your lines back you need to set the separator style, you set it to none - you need single line. { self.backgroundLabel.hidden = true return arrayObject.paymentsArray().count self.tableView.separatorStyle = UITableViewCellSeparatorStyle.SingleLine }
iOS7 Status Bar like the native weather app
Does anyone know how can I reproduce a similar effect from the native iOS7 weather app? Basically, the status bar inherits the view's background underneath, but the content doesn't show up. Also, a 1 pixel line is drawn after the 20 pixels height of the status bar, only if some content is underlayed.
The best thing is to make it through the clipSubview of the view. You put your content into the view and make constraints to left/right/bottom and height. Height on scroll view you check is the cell has minus position and at that time you start to change the height of content (clip) view to get desired effect. This is a real app you can download and take a look from www.fancyinteractive.com. This functionality will be available soon as next update. - (void)scrollViewDidScroll:(UIScrollView *)scrollView { NSArray *visibleCells = [convertorsTableView visibleCells]; if (visibleCells.count) { for (CVConverterTableViewCell *cell in visibleCells) { CGFloat positionYInView = [convertorsTableView convertRect:cell.frame toView:self.view].origin.y; [self clipLayoutConstraint:cell.clipHeightLayoutConstraint withPosition:positionYInView defaultHeight:cell.frameHeight]; [cell.converterLabel layoutIfNeeded]; [cell.iconImageView layoutIfNeeded]; } } [self checkStatusBarSeperator:scrollView.contentOffset.y]; } - (void)clipLayoutConstraint:(NSLayoutConstraint *)constraint withPosition:(CGFloat)position defaultHeight:(CGFloat)defaultHeight { if (position < 0) { constraint.constant = (defaultHeight - -position - 20 > 10) ? defaultHeight - -position - 20 : 10; } else constraint.constant = defaultHeight; }
You can accomplish this by setting a mask to the table view's layer. You will not be able however to render the animations inside the cells, but you can do those yourself behind the table view, and track their movement with the table view's scrollview delegate methods. Here is some informations on CALayer masks: http://evandavis.me/blog/2013/2/13/getting-creative-with-calayer-masks
Swift 5: override func scrollViewDidScroll(_ scrollView: UIScrollView) { guard let visibleCells = tableView.visibleCells as? [TableViewCell] else { return } let defaultClipHeight: CGFloat = 24 let statusBarHeight: CGFloat = UIApplication.statusBarHeight if !visibleCells.isEmpty { for cell in visibleCells { let topSpace = cell.frame.size.height - defaultClipHeight - cell.clipBottomConstraint.constant let cellOffsetY = tableView.contentOffset.y - cell.frame.origin.y + statusBarHeight if cellOffsetY > topSpace { let clipOffsetY = cellOffsetY - topSpace let clipHeight = defaultClipHeight - clipOffsetY cell.clipHeightConstraint.constant = max(clipHeight, 0) } else { cell.clipHeightConstraint.constant = defaultClipHeight } } } } Starting Page: Scrolling First Item: Scrolling Second Item:
How to make a UIScrollView auto scroll when a UITextField becomes a first responder
I've seen posts around here that suggest that UIScrollViews should automatically scroll if a subview UITextField becomes the first responder; however, I can't figure out how to get this to work. What I have is a UIViewController that has a UIScrollView and within the UIScrollView there are multiple textfields. I know how to do this manually if necessary; however, from what I've been reading, it seems possible to have it autoscroll. Help please.
I hope this example will help you You can scroll to any point by this code. scrollView.contentOffset = CGPointMake(0,0); So if you have textfield, it must have some x,y position on view, so you can use CGPoint point = textfield.frame.origin ; scrollView.contentOffset = point This should do the trick, But if you don't know when to call this code, so you should learn UITextFieldDelegate methods Implement this method in your code - (void)textFieldDidBeginEditing:(UITextField *)textField { // Place Scroll Code here } I hope you know how to use delegate methods.
I know this question has already been answered, but I thought I would share the code combination that I used from #Adeel and #Basil answer, as it seems to work perfectly for me on iOS 9. -(void)textFieldDidBeginEditing:(UITextField *)textField { // Scroll to the text field so that it is // not hidden by the keyboard during editing. [scroll setContentOffset:CGPointMake(0, (textField.superview.frame.origin.y + (textField.frame.origin.y))) animated:YES]; } -(void)textFieldDidEndEditing:(UITextField *)textField { // Remove any content offset from the scroll // view otherwise the scroll view will look odd. [scroll setContentOffset:CGPointMake(0, 0) animated:YES]; } I also used the animated method, it makes for a much smoother transition.
Here is the Swift 4 update to #Supertecnoboff's answer. It worked great for me. func textFieldDidBeginEditing(_ textField: UITextField) { scroll.setContentOffset(CGPoint(x: 0, y: (textField.superview?.frame.origin.y)!), animated: true) } func textFieldDidEndEditing(_ textField: UITextField) { scroll.setContentOffset(CGPoint(x: 0, y: 0), animated: true) } Make sure to extend UITextFieldDelegate and set the textfields' delegate to self.
There is nothing you have to do manually. It is the default behavior. There are two possibilities as to why you are not seeing the behavior The most likely reason is that the keyboard is covering your UITextField. See below for solution The other possibility is that you have another UIScrollView somewhere in the view hierarchy between the UITextField and the UIScrollView that you want to auto scroll. This is less likely but can still cause problems. For #1, you want to implement something similar to Apple's recommendations for Moving Content That Is Located Under the Keyboard. Note that the code provided by Apple does not account for rotation. For improvements on their code, check out this blog post's implementation of the keyboardDidShow method that properly translates the keyboard's frame using the window.
- (void)textFieldDidBeginEditing:(UITextField *)textField { CGRect rect = [textField bounds]; rect = [textField convertRect:rect toView:self.scrollView]; rect.origin.x = 0 ; rect.origin.y -= 60 ; rect.size.height = 400; [self.scrollView scrollRectToVisible:rect animated:YES]; }
You can use this function for autoScroll of UITextField on UITextFieldDelegate - (void)textFieldDidBeginEditing:(UITextField *)textField { [self autoScrolTextField:textField onScrollView:self.scrollView]; } - (void) autoScrolTextField: (UITextField *) textField onScrollView: (UIScrollView *) scrollView { float slidePoint = 0.0f; float keyBoard_Y_Origin = self.view.bounds.size.height - 216.0f; float textFieldButtomPoint = textField.superview.frame.origin.y + (textField.frame.origin.y + textField.frame.size.height); if (keyBoard_Y_Origin < textFieldButtomPoint - scrollView.contentOffset.y) { slidePoint = textFieldButtomPoint - keyBoard_Y_Origin + 10.0f; CGPoint point = CGPointMake(0.0f, slidePoint); scrollView.contentOffset = point; } EDIT: Im now using IQKeyboardManager Kudos to the developer of this, you need to try this.
Solution extension UIScrollView { func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) { guard let firstResponderSubview = findFirstResponderSubview() else { return } scrollVertically(toFirstResponder: firstResponderSubview, keyboardFrameHight: keyboardFrameHight, animated: true) } private func scrollVertically(toFirstResponder view: UIView, keyboardFrameHight: CGFloat, animated: Bool) { let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight let maxY = contentSize.height - scrollViewVisibleRectHeight if contentOffset.y >= maxY { return } var point = view.convert(view.bounds.origin, to: self) point.x = 0 point.y -= scrollViewVisibleRectHeight/2 if point.y > maxY { point.y = maxY } else if point.y < 0 { point.y = 0 } setContentOffset(point, animated: true) } } extension UIView { func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } } func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] } class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] { parenView.subviews.flatMap { subView -> [T] in var result = getAllSubviews(from: subView) as [T] if let view = subView as? T { result.append(view) } return result } } } Full Sample Do not forget to paste the Solution code here import UIKit class ViewController: UIViewController { private weak var scrollView: UIScrollView! private lazy var keyboard = KeyboardNotifications(notifications: [.willHide, .willShow], delegate: self) override func viewDidLoad() { super.viewDidLoad() let scrollView = UIScrollView() view.addSubview(scrollView) scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true scrollView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true scrollView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true scrollView.contentSize = CGSize(width: view.frame.width, height: 1000) scrollView.isScrollEnabled = true scrollView.indicatorStyle = .default scrollView.backgroundColor = .yellow scrollView.keyboardDismissMode = .interactive self.scrollView = scrollView addTextField(y: 20) addTextField(y: 300) addTextField(y: 600) addTextField(y: 950) } private func addTextField(y: CGFloat) { let textField = UITextField() textField.borderStyle = .line scrollView.addSubview(textField) textField.translatesAutoresizingMaskIntoConstraints = false textField.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: y).isActive = true textField.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 44).isActive = true textField.widthAnchor.constraint(equalToConstant: 120).isActive = true textField.heightAnchor.constraint(equalToConstant: 44).isActive = true } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) keyboard.isEnabled = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) keyboard.isEnabled = false } } extension ViewController: KeyboardNotificationsDelegate { func keyboardWillShow(notification: NSNotification) { guard let userInfo = notification.userInfo as? [String: Any], let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } scrollView.contentInset.bottom = keyboardFrame.height scrollView.scrollVerticallyToFirstResponderSubview(keyboardFrameHight: keyboardFrame.height) } func keyboardWillHide(notification: NSNotification) { scrollView.contentInset.bottom = 0 } } /// Solution extension UIScrollView { func scrollVerticallyToFirstResponderSubview(keyboardFrameHight: CGFloat) { guard let firstResponderSubview = findFirstResponderSubview() else { return } scrollVertically(toFirstResponder: firstResponderSubview, keyboardFrameHight: keyboardFrameHight, animated: true) } private func scrollVertically(toFirstResponder view: UIView, keyboardFrameHight: CGFloat, animated: Bool) { let scrollViewVisibleRectHeight = frame.height - keyboardFrameHight let maxY = contentSize.height - scrollViewVisibleRectHeight if contentOffset.y >= maxY { return } var point = view.convert(view.bounds.origin, to: self) point.x = 0 point.y -= scrollViewVisibleRectHeight/2 if point.y > maxY { point.y = maxY } else if point.y < 0 { point.y = 0 } setContentOffset(point, animated: true) } } extension UIView { func findFirstResponderSubview() -> UIView? { getAllSubviews().first { $0.isFirstResponder } } func getAllSubviews<T: UIView>() -> [T] { UIView.getAllSubviews(from: self) as [T] } class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] { parenView.subviews.flatMap { subView -> [T] in var result = getAllSubviews(from: subView) as [T] if let view = subView as? T { result.append(view) } return result } } } // https://stackoverflow.com/a/42600092/4488252 import Foundation protocol KeyboardNotificationsDelegate: class { func keyboardWillShow(notification: NSNotification) func keyboardWillHide(notification: NSNotification) func keyboardDidShow(notification: NSNotification) func keyboardDidHide(notification: NSNotification) } extension KeyboardNotificationsDelegate { func keyboardWillShow(notification: NSNotification) {} func keyboardWillHide(notification: NSNotification) {} func keyboardDidShow(notification: NSNotification) {} func keyboardDidHide(notification: NSNotification) {} } class KeyboardNotifications { fileprivate var _isEnabled: Bool fileprivate var notifications: [NotificationType] fileprivate weak var delegate: KeyboardNotificationsDelegate? fileprivate(set) lazy var isKeyboardShown: Bool = false init(notifications: [NotificationType], delegate: KeyboardNotificationsDelegate) { _isEnabled = false self.notifications = notifications self.delegate = delegate } deinit { if isEnabled { isEnabled = false } } } // MARK: - enums extension KeyboardNotifications { enum NotificationType { case willShow, willHide, didShow, didHide var selector: Selector { switch self { case .willShow: return #selector(keyboardWillShow(notification:)) case .willHide: return #selector(keyboardWillHide(notification:)) case .didShow: return #selector(keyboardDidShow(notification:)) case .didHide: return #selector(keyboardDidHide(notification:)) } } var notificationName: NSNotification.Name { switch self { case .willShow: return UIResponder.keyboardWillShowNotification case .willHide: return UIResponder.keyboardWillHideNotification case .didShow: return UIResponder.keyboardDidShowNotification case .didHide: return UIResponder.keyboardDidHideNotification } } } } // MARK: - isEnabled extension KeyboardNotifications { private func addObserver(type: NotificationType) { NotificationCenter.default.addObserver(self, selector: type.selector, name: type.notificationName, object: nil) } var isEnabled: Bool { set { if newValue { for notificaton in notifications { addObserver(type: notificaton) } } else { NotificationCenter.default.removeObserver(self) } _isEnabled = newValue } get { _isEnabled } } } // MARK: - Notification functions extension KeyboardNotifications { #objc func keyboardWillShow(notification: NSNotification) { delegate?.keyboardWillShow(notification: notification) isKeyboardShown = true } #objc func keyboardWillHide(notification: NSNotification) { delegate?.keyboardWillHide(notification: notification) isKeyboardShown = false } #objc func keyboardDidShow(notification: NSNotification) { isKeyboardShown = true delegate?.keyboardDidShow(notification: notification) } #objc func keyboardDidHide(notification: NSNotification) { isKeyboardShown = false delegate?.keyboardDidHide(notification: notification) } }
If you have multiple textfields say Textfield1, Textfield2, Textfield3 and you want to scroll the scrollview along the y-axis when textfield2 becomes first responder: if([Textfield2 isFirstResponder]) { scrollView.contentOffset = CGPointMake(0,yourY); }
As Michael McGuire mentioned in his point #2 above, the system's default behavior misbehaves when the scroll view contains another scroll view between the text field and the scroll view. I've found that the misbehavior also occurs when there's a scroll view merely next to the text field (both embedded in the scroll view that needs to be adjusted to bring the text field into view when the text field wants to start editing. This is on iOS 12.1. But my solution is different from the above. In my top-level scroll view, which is sub-classed so I can add properties and override methods, I override scrollRectToVisible:animated:. It simply calls its [super scrollRectToVisible:animated:] unless there's a property set that tells it to adjust the rect passed in, which is the frame of the text field. When the property is non-nil, it is a reference to the UITextField in question, and the rect is adjusted so that the scroll view goes further than the system thought it would. So I put this in the UIScrollView's sub-classed header file: #property (nullable) UITextField *textFieldToBringIntoView; (with appropriate #synthesize textFieldToBringIntoView; in the implementation. Then I added this override method to the implementation: - (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)how { if (textFieldToBringIntoView) { // Do whatever mucking with `rect`'s origin needed to make it visible // based on context or its spatial relationship with the other // view that the system is getting confused by. textFieldToBringIntoView = nil; // Go back to normal } [super scrollRectToVisible:rect animated:how]; } In the delegate method for the UITextField for when it's about to begin editing, just set textFieldToBringIntoView to the textField in question: - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { // Ensure it scrolls into view so that keyboard doesn't obscure it // The system is about to call |scrollRectIntoView:| for the scrolling // superview, but the system doesn't get things right in certain cases. UIScrollView *parent = (UIScrollView *)textField.superview; // (or figure out the parent UIScrollView some other way) // Tell the override to do something special just once // based on this text field's position in its parent's scroll view. parent.textFieldToBringIntoView = textField; // The override function will set this back to nil return(YES); } It seems to work. And if Apple fixes their bug, it seems like it might still work (fingers crossed).
Building off of Vasily Bodnarchuk's answer I created a gist with a simple protocol that you can implement and it'll do it all for you. All you need to do is call registerAsTextDisplacer() I created a BaseViewController in my project and made that implement it https://gist.github.com/CameronPorter95/cb68767f5f8052fdc70293c167e9430e
Other solutions I saw, let you set the offset to the origin of the textField but this makes the scroller view go beyond it bounds. I did this adjustment to the offset instead to not go beyond the bottom nor the top offsets. Set the keyboardHeightConstraint to the bottom of the page. When the keyboard shows, update its constraint's constant to negative the keyboard height. Then scroll to the responderField as we will show below. #IBOutlet var keyboardHeightConstraint: NSLayoutConstraint? var responderField: String? #objc func keyboardNotification(notification: NSNotification) { guard let keyboardValue = notification.userInfo [UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } let keyboardHeight = keyboardValue.cgRectValue.height keyboardHeightConstraint?.constant = -keyboardHeight scroll(field: responderField!) } func textFieldDidBeginEditing(_ textField: UITextField) { responderField = textField } Now we want to make sure we do not scroll greater than the bottom offset nor less than the top offset. At the same time, we want to calculate the offset of the field's maxY value. To do that, we subtract the scrollView.bounds.size.height from the maxY value. let targetOffset = field.frame.maxY - scrollView.bounds.size.height I found it nicer to scroll an extra distance of the keyboard height, but you could neglect that if you want to scroll right below the field. let targetOffset = keyboardHeight + field.frame.maxY - scrollView.bounds.size.height Remember to add the scrollView.contentInset.bottom if you have the tab bar visible. func scroll(field: UITextField) { guard let keyboardConstraintsConstant = keyboardHeightConstraint?.constant else { return } let keyboardHeight = -keyboardConstraintsConstant view.layoutIfNeeded() let bottomOffset = scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.bottom let topOffset = -scrollView.safeAreaInsets.top let targetOffset = keyboardHeight + field.frame.maxY + scrollView.contentInset.bottom - scrollView.bounds.size.height let adjustedOffset = targetOffset > bottomOffset ? bottomOffset : (targetOffset < topOffset ? topOffset : targetOffset) scrollView.setContentOffset(CGPoint(x: 0, y: adjustedOffset), animated: true) }
If you have scrollView and tableView with invalidating intrinsicContentSize as the subview, you can disable tableView scrolling in storyboard or set tableView.isScrollEnabled to false in code.