Handle scrollview size in UICollectionViewCell - ios

Im making an horizontal UICollectionView, and inside UICollectionViewCell i have scrollview and inside the scrollview i have an imageView.
The issue I'm having is that when i zoom-in the imageView,scrollView is taking all the cell size, so its not fitting to the image size height and width.thus by scrolling up and down the image disappear from scrollview, i have no idea whats going wrong in my code.
My ColectionViewCell code:
class CollectionViewCell: UICollectionViewCell {
#IBOutlet var scrollView: UIScrollView!
#IBOutlet var ImageV: UIImageView!
}
CollectionView code :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! CollectionViewCell
cell.scrollView.contentMode = UIViewContentMode.ScaleAspectFit
cell.scrollView.delegate = self
cell.ImageV.image = UIImage(named: array[indexPath.row])
cell.ImageV.contentMode = UIViewContentMode.ScaleAspectFit
cell.scrollView.minimumZoomScale = 1
cell.scrollView.maximumZoomScale = 4;
cell.scrollView.contentSize = cell.ImageV.frame.size
return cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: self.collectionView.frame.size.width , height: self.collectionView.frame.size.height - 100)
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
let indexPath = NSIndexPath(forItem: Int(currentIndex), inSection: 0)
if let cell1 = self.collectionView.cellForItemAtIndexPath(indexPath) {
let cell = cell1 as! CollectionViewCell
let boundsSize = cell.scrollView.bounds.size
var contentsFrame = cell.ImageV.frame
if contentsFrame.size.width < boundsSize.width{
contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2
}else{
contentsFrame.origin.x = 0
}
if contentsFrame.size.height < boundsSize.height {
contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2
}else{
contentsFrame.origin.y = 0
}
return cell.ImageV
}
return UIView()
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width;
oldcell = currentIndex - 1
let indexPath = NSIndexPath(forItem: Int(oldcell), inSection: 0)
if let cell1 = self.collectionView.cellForItemAtIndexPath(indexPath) {
let cell = cell1 as! CollectionViewCell
cell.scrollView.zoomScale = 0
}
}
Image preview:
https://i.imgur.com/Gr2p09A.gifv
My project found here : https://drive.google.com/file/d/0B32ROW7V8Fj4RVZfVGliXzJseGM/view?usp=sharing

Well First lets start with UIViewController that is holding your UICollectionView:
Define a variable to hold the collection view layout:
var flowLayout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
You will have to override viewWillLayoutSubviews and this is going to handle the collectionView size.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
flowLayout.itemSize = CGSize(width: view.frame.width, height:view.frame.height)
}
Also you will need to override viewDidLayoutSubviews to handle the size of each new cell to set it to default size:
override func viewDidLayoutSubviews() {
if let currentCell = imageCollectionView.cellForItemAtIndexPath(desiredIndexPath) as? GalleryCell {
currentCell.configureForNewImage()
}
}
in ViewDidLoad setup the collectionView to be horizontal with flow layout:
// Set up flow layout
flowLayout.scrollDirection = UICollectionViewScrollDirection.Horizontal
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
// Set up collection view
imageCollectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height), collectionViewLayout: flowLayout)
imageCollectionView.translatesAutoresizingMaskIntoConstraints = false
imageCollectionView.registerClass(GalleryCell.self, forCellWithReuseIdentifier: "GalleryCell")
imageCollectionView.dataSource = self
imageCollectionView.delegate = self
imageCollectionView.pagingEnabled = true
view.addSubview(imageCollectionView)
imageCollectionView.contentSize = CGSize(width: 1000.0, height: 1.0)
In your UICollectionView method cellForItemAtIndexPath load the image only without setting anything:
cell.image = UIImage(named: array[indexPath.row])
Now lets move to GalleryCell class and to handle scrollView when zooming:
class GalleryCell: UICollectionViewCell, UIScrollViewDelegate {
var image:UIImage? {
didSet {
configureForNewImage()
}
}
var scrollView: UIScrollView
let imageView: UIImageView
override init(frame: CGRect) {
imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
scrollView = UIScrollView(frame: frame)
scrollView.translatesAutoresizingMaskIntoConstraints = false
super.init(frame: frame)
contentView.addSubview(scrollView)
contentView.addConstraints(scrollViewConstraints)
scrollView.addSubview(imageView)
scrollView.delegate = self
scrollView.maximumZoomScale = 4
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal func configureForNewImage() {
imageView.image = image
imageView.sizeToFit()
setZoomScale()
scrollViewDidZoom(scrollView)
}
public func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return imageView
}
public func scrollViewDidZoom(scrollView: UIScrollView) {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
if verticalPadding >= 0 {
// Center the image on screen
scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
} else {
// Limit the image panning to the screen bounds
scrollView.contentSize = imageViewSize
}
}
I've tested it with example and it should solve your issue with scrollview and #Spencevail explained mostly why it's being caused !

You need to set the contentInset on your scrollview. What's happening is The contentSize of the UIScrollView is originally set to match the screen size with the image inside of that. As you zoom in on the scrollview, the contentSize expands proportionately, so those black areas above and below the photos when you're zoomed all the way out expand as you zoom in. In other words You're expanding the area above and below where your image can go. If you adjust the contentInset it will bring in that dead area and not allow the scrollview to move the image out of the window.
public func scrollViewDidZoom(scrollView: UIScrollView) {
let imageViewSize = imageView.frame.size
let scrollViewSize = scrollView.bounds.size
let verticalPadding = imageViewSize.height < scrollViewSize.height ? (scrollViewSize.height - imageViewSize.height) / 2 : 0
let horizontalPadding = imageViewSize.width < scrollViewSize.width ? (scrollViewSize.width - imageViewSize.width) / 2 : 0
if verticalPadding >= 0 {
// Center the image on screen
scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
} else {
// Limit the image panning to the screen bounds
scrollView.contentSize = imageViewSize
}
}
This looks almost the same as an open source Cocoapod I help manage, SwiftPhotoGallery. Take a look at the code for the SwiftPhotoGalleryCell and you should get a pretty good idea of how to do this. (Feel free to just use the cocoapod too if you want!)

Related

Collection view items equal spacing layout

I have no experience in collection view layout and I decided to ask your advices. I faced with the next problem. I have a simple collection view with custom cell. Cell has only label, which has left and right constraints equal to 16. So the width of cell depends on content of label. And I have no clue why items have so strange spacings. I set spacings in the storyboard - 12 points left and right spacings.
Actual result:
I wanna get something like this:
So in which direction should I move? Thanks for your answers.
UPDATE: Strange behaviour occurs on iPhones running iOS 12..<13.0
What you are seeing is the default behavior for collectionView layout.
You need to subclass UICollectionViewFlowLayout to get this behavior.
import UIKit
class LeftAlignCellCollectionFlowLayout: UICollectionViewFlowLayout {
private(set) var cellHeight: CGFloat = 36
init(cellHeight: CGFloat) {
super.init()
self.cellHeight = cellHeight
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
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 }
self.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
var newAttributes = attributes
var leftMargin = self.sectionInset.left
var maxY: CGFloat = -1.0
let availableWidth: CGFloat = collectionView.frame.width
let layout = collectionView.collectionViewLayout
for attribute in attributes {
if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
if cellAttribute.frame.width > availableWidth {
cellAttribute.frame.origin.x = 0
cellAttribute.frame.size = CGSize(width: availableWidth, height: cellHeight)
}
else {
if cellAttribute.frame.origin.y >= maxY {
leftMargin = self.sectionInset.left
}
var frame = cellAttribute.frame
frame.origin.x = leftMargin
frame.size.height = cellHeight
cellAttribute.frame = frame
leftMargin += cellAttribute.frame.width + self.minimumInteritemSpacing
maxY = max(cellAttribute.frame.maxY , maxY)
}
newAttributes.append(cellAttribute)
}
}
return newAttributes
}
}
Now you can use above layout like this.
let flowLayout = LeftAlignCellCollectionFlowLayout(cellHeight: 40)
flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
flowLayout.minimumInteritemSpacing = 10
flowLayout.minimumLineSpacing = 10
collectionView.collectionViewLayout = flowLayout

How to center each view in the collection with UICollectionViewFlowLayout?

Similar questions are posted here, but the problem relies on my code and I don't know to solve it.
I don't know why, but as I'm scrolling my collectionview, the cells are moving more to the left. See image below:
Here is the code for my UICollectionViewFlowLayout
import UIKit
class PrayerFlowLayout: UICollectionViewFlowLayout {
//let standardItemAlpha: CGFloat = 0.3
let standardItemScale: CGFloat = 0.85
var isSetup = false
override func prepare() {
super.prepare()
if isSetup == false {
setupCollectionView()
isSetup = true
}
}
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var attributesCopy = [UICollectionViewLayoutAttributes]()
for itemAttributes in attributes! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
changeLayoutAttributes(itemAttributesCopy)
attributesCopy.append(itemAttributesCopy)
}
return attributesCopy
}
// indicates the point on where to stop scrolling each prayer
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// get layout attribute to use to make some calculations
let layoutAttributes = self.layoutAttributesForElements(in: collectionView!.bounds)
// get the horizontal center on the collection
let center = collectionView!.frame.size.width / 2
// add the center to the proposed content offset
let proporsedContentOffsetCenterOrigin = proposedContentOffset.x + center
let closest = layoutAttributes!.sorted {
abs($0.center.x - proporsedContentOffsetCenterOrigin) < abs($1.center.x - proporsedContentOffsetCenterOrigin)
}.first ?? UICollectionViewLayoutAttributes()
let targetContentOffset = CGPoint(x: floor(closest.center.x - center), y: proposedContentOffset.y - center)
return targetContentOffset
}
func changeLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) {
let collectionCenter = collectionView!.bounds.width / 2
let offset = collectionView!.contentOffset.x
let normalizedCenter = attributes.center.x - offset
let maxDistance = collectionView!.bounds.size.width + self.minimumLineSpacing
//collectionView!.frame.width + self.minimumLineSpacing // self.itemSize.width + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
//let alpha = ratio * (1 - self.standardItemAlpha) + self.standardItemAlpha
let scale = ratio * (1 - self.standardItemScale) + self.standardItemScale
//attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
}
func setupCollectionView() {
self.collectionView!.decelerationRate = UIScrollView.DecelerationRate.fast
let collectionSize = collectionView!.bounds.size
let yInset = (collectionSize.height - self.itemSize.height) / 3
let xInset = (collectionSize.width - self.itemSize.width) / 2
let topPos = (collectionView!.bounds.height - (collectionView!.bounds.height - 75) )
self.sectionInset = UIEdgeInsets.init(top: topPos, left: xInset, bottom: yInset, right: xInset)
}
}
Any ideas on how I can have all the cells always centered?
You need to use UICollectionViewDelegateFlowLayout method like this
class CardListViewController:UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout{
// UICollectionViewDelegateFlowLayout Delegate method
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let leftAndRightPaddings: CGFloat = 20.0
let numberOfItemsPerRow: CGFloat = 1.0
let width = (collectionView.frame.width - leftAndRightPaddings)/numberOfItemsPerRow
return CGSize(width: width, height: collectionView.frame.width)
}
}
From the Storyboard Set Section Insets Like this.
Output :

Is it possible to create a scroll view with an animated page control in Swift?

The designer wants the following animation from a swipe gesture.
As it can be seen the user can swipe cards and see what each card has. At the same time, the user can see in the right side of the screen the following card and the last one in the left. Also, cards are changing their size while the user is moving the scroll.
I have already worked with page control views but I have no idea if this is possible with a page Control (which actually is the question of this post).
Also, I have already tried with a collectionView but when I swipe (actually is an horizontal scroll) the scroll has an uncomfortable inertia and also, I have no idea how to make the animation.
In this question a scrolled page control is implemented but now I just wondering if and animation like the gif provided is possible.
If the answer is yes, I would really appreciate if you can give tips of how I can make this possible.
Thanks in advance.
Based on the Denislava Shentova comment I found a good library that solves this issue.
For all people in the future and their work hours, I just took code from UPCarouselFlowLayout library and deleted some I didn't need.
Here is the code of a simple viewController that shows the following result:
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
// CollectionView variable:
var collectionView : UICollectionView?
// Variables asociated to collection view:
fileprivate var currentPage: Int = 0
fileprivate var pageSize: CGSize {
let layout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
var pageSize = layout.itemSize
pageSize.width += layout.minimumLineSpacing
return pageSize
}
fileprivate var colors: [UIColor] = [UIColor.black, UIColor.red, UIColor.green, UIColor.yellow]
override func viewDidLoad() {
super.viewDidLoad()
self.addCollectionView()
self.setupLayout()
}
func setupLayout(){
// This is just an utility custom class to calculate screen points
// to the screen based in a reference view. You can ignore this and write the points manually where is required.
let pointEstimator = RelativeLayoutUtilityClass(referenceFrameSize: self.view.frame.size)
self.collectionView?.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
self.collectionView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: pointEstimator.relativeHeight(multiplier: 0.1754)).isActive = true
self.collectionView?.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
self.collectionView?.heightAnchor.constraint(equalToConstant: pointEstimator.relativeHeight(multiplier: 0.6887)).isActive = true
self.currentPage = 0
}
func addCollectionView(){
// This is just an utility custom class to calculate screen points
// to the screen based in a reference view. You can ignore this and write the points manually where is required.
let pointEstimator = RelativeLayoutUtilityClass(referenceFrameSize: self.view.frame.size)
// This is where the magic is done. With the flow layout the views are set to make costum movements. See https://github.com/ink-spot/UPCarouselFlowLayout for more info
let layout = UPCarouselFlowLayout()
// This is used for setting the cell size (size of each view in this case)
// Here I'm writting 400 points of height and the 73.33% of the height view frame in points.
layout.itemSize = CGSize(width: pointEstimator.relativeWidth(multiplier: 0.73333), height: 400)
// Setting the scroll direction
layout.scrollDirection = .horizontal
// Collection view initialization, the collectionView must be
// initialized with a layout object.
self.collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
// This line if for able programmatic constrains.
self.collectionView?.translatesAutoresizingMaskIntoConstraints = false
// CollectionView delegates and dataSource:
self.collectionView?.delegate = self
self.collectionView?.dataSource = self
// Registering the class for the collection view cells
self.collectionView?.register(CardCell.self, forCellWithReuseIdentifier: "cellId")
// Spacing between cells:
let spacingLayout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
spacingLayout.spacingMode = UPCarouselFlowLayoutSpacingMode.overlap(visibleOffset: 20)
self.collectionView?.backgroundColor = UIColor.gray
self.view.addSubview(self.collectionView!)
}
// MARK: - Card Collection Delegate & DataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CardCell
cell.customView.backgroundColor = colors[indexPath.row]
return cell
}
// MARK: - UIScrollViewDelegate
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let layout = self.collectionView?.collectionViewLayout as! UPCarouselFlowLayout
let pageSide = (layout.scrollDirection == .horizontal) ? self.pageSize.width : self.pageSize.height
let offset = (layout.scrollDirection == .horizontal) ? scrollView.contentOffset.x : scrollView.contentOffset.y
currentPage = Int(floor((offset - pageSide / 2) / pageSide) + 1)
}
}
class CardCell: UICollectionViewCell {
let customView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 12
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.customView)
self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true
self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
} // End of CardCell
class RelativeLayoutUtilityClass {
var heightFrame: CGFloat?
var widthFrame: CGFloat?
init(referenceFrameSize: CGSize){
heightFrame = referenceFrameSize.height
widthFrame = referenceFrameSize.width
}
func relativeHeight(multiplier: CGFloat) -> CGFloat{
return multiplier * self.heightFrame!
}
func relativeWidth(multiplier: CGFloat) -> CGFloat{
return multiplier * self.widthFrame!
}
}
Note that there are some other clases in this code but temporarily you can run the whole code in the ViewController.swift file. After you test, please split them into different files.
In order tu run this code, you need the following module. Make a file called UPCarouselFlowLayout.swift and paste all this code:
import UIKit
public enum UPCarouselFlowLayoutSpacingMode {
case fixed(spacing: CGFloat)
case overlap(visibleOffset: CGFloat)
}
open class UPCarouselFlowLayout: UICollectionViewFlowLayout {
fileprivate struct LayoutState {
var size: CGSize
var direction: UICollectionViewScrollDirection
func isEqual(_ otherState: LayoutState) -> Bool {
return self.size.equalTo(otherState.size) && self.direction == otherState.direction
}
}
#IBInspectable open var sideItemScale: CGFloat = 0.6
#IBInspectable open var sideItemAlpha: CGFloat = 0.6
open var spacingMode = UPCarouselFlowLayoutSpacingMode.fixed(spacing: 40)
fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)
override open func prepare() {
super.prepare()
let currentState = LayoutState(size: self.collectionView!.bounds.size, direction: self.scrollDirection)
if !self.state.isEqual(currentState) {
self.setupCollectionView()
self.updateLayout()
self.state = currentState
}
}
fileprivate func setupCollectionView() {
guard let collectionView = self.collectionView else { return }
if collectionView.decelerationRate != UIScrollViewDecelerationRateFast {
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
}
}
fileprivate func updateLayout() {
guard let collectionView = self.collectionView else { return }
let collectionSize = collectionView.bounds.size
let isHorizontal = (self.scrollDirection == .horizontal)
let yInset = (collectionSize.height - self.itemSize.height) / 2
let xInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset)
let side = isHorizontal ? self.itemSize.width : self.itemSize.height
let scaledItemOffset = (side - side*self.sideItemScale) / 2
switch self.spacingMode {
case .fixed(let spacing):
self.minimumLineSpacing = spacing - scaledItemOffset
case .overlap(let visibleOffset):
let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
let inset = isHorizontal ? xInset : yInset
self.minimumLineSpacing = inset - fullSizeSideItemOverlap
}
}
override open func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map({ self.transformLayoutAttributes($0) })
}
fileprivate func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let isHorizontal = (self.scrollDirection == .horizontal)
let collectionCenter = isHorizontal ? collectionView.frame.size.width/2 : collectionView.frame.size.height/2
let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y) - offset
let maxDistance = (isHorizontal ? self.itemSize.width : self.itemSize.height) + self.minimumLineSpacing
let distance = min(abs(collectionCenter - normalizedCenter), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
attributes.alpha = alpha
attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
attributes.zIndex = Int(alpha * 10)
return attributes
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView , !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let isHorizontal = (self.scrollDirection == .horizontal)
let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
let proposedContentOffsetCenterOrigin = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide
var targetContentOffset: CGPoint
if isHorizontal {
let closest = layoutAttributes.sorted { abs($0.center.x - proposedContentOffsetCenterOrigin) < abs($1.center.x - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
}
else {
let closest = layoutAttributes.sorted { abs($0.center.y - proposedContentOffsetCenterOrigin) < abs($1.center.y - proposedContentOffsetCenterOrigin) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
}
return targetContentOffset
}
}
Again, this module was made by Paul Ulric, you can installed with cocoa.

How to set different height and width of cells in horizontal collectionview

I have a horizontal collection view and it shows 5 cells at a time and i have adjusted that through sizeForItemAt :
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.numberCollectionView?.bounds.size.width)!/5 - 3, height: (self.numberCollectionView?.bounds.size.width)!/5 - 3 )
}
But these 5 cells would have different width and height and there is a ratio to it. So the center item would take 40% of the whole size, the 2nd and 3rd item would take 20% of the size and the 1st and 5th would take 10% of the size. According to this ratio items size would get change on scroll. These items are all Label.
Edit:
unlike the suggested question and other questions in stackoverflow in my case there is only one row and i have custom UICollectionViewFlowLayout to handle the scrolling by cell
UICollectionViewFlowLayout to handle the scrolling by cell:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
Then to find the center cell of the CollectionView:
private func findCenterIndex() -> Int {
let center = self.view.convert(numberCollectionView.center, to: self.numberCollectionView)
let row = numberCollectionView!.indexPathForItem(at: center)?.row
guard let index = row else {
return 0
}
self.rating = index
return index
}
under scrollViewDidScrolltransformed cell:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
transformCell()
}
private func transformCell() {
let row = findCenterIndex()
var offset : Float = 0.0
for cell in numberCollectionView.visibleCells as! [VotingCell] {
if row == numberCollectionView.indexPath(for: cell)?.row {
offset = 1.1
cell.label.font = UIFont.boldSystemFont(ofSize: 28.0)
cell.label.textColor = UIColor.black
cell.alpha = 1.0
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 1 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 1
{
offset = 0.8
cell.label.font = UIFont(name: cell.label.font.fontName, size: 26.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 2 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 2 {
offset = 0.7
cell.label.font = UIFont(name: cell.label.font.fontName, size: 20.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else
{
offset = 0.00
}
cell.transform = CGAffineTransform.identity
cell.transform = CGAffineTransform(scaleX: CGFloat(offset), y: CGFloat(offset))
}
}
also called transformCell() from viewDidLayoutSubviews for initial load.

Adding horizontal line below cell in uicollectionview

I am implementing like a calendar layout with some modification which is shown in the screenshot. To achieve this I have used UICollectionView. The problem is, I have to draw a screen width continuous line(green line in screenshot). The green line should cover the whole width, I know its not showing over the circular cell due to half of the cornerRadius and a vertical line only after the first cell(10 am). Where i have to add the shapelayer, so that it ll seems like a continuous line. Here is the code which I have tried so far.
KKViewController.m
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! KKBookCollectionViewCell
self.bookingCollectionView.backgroundColor = UIColor.whiteColor()
let rectangularRowIndex:NSInteger = indexPath.row % 5
if(rectangularRowIndex == 0 )
{
cell.userInteractionEnabled = false
cell.layer.cornerRadius = 0
cell.timeSlotLabel.text = "10am"
cell.backgroundColor = UIColor.whiteColor()
cell.layer.borderWidth = 0
cell.layer.borderColor = UIColor.clearColor().CGColor
}
else
{
cell.userInteractionEnabled = true
cell.layer.cornerRadius = cell.frame.size.width/2
cell.timeSlotLabel.text = ""
//cell.backgroundColor = UIColor.lightGrayColor()
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor.grayColor().CGColor
if cell.selected == true
{
cell.backgroundColor = UIColor.greenColor()
}
else
{
cell.backgroundColor = UIColor.lightGrayColor()
}
}
return cell
}
KKCollectionCell.m
var borderWidth:CGFloat!
var borderPath:UIBezierPath!
override func awakeFromNib() {
super.awakeFromNib()
drawHorizontalLine()
dottedLine(with: borderPath, and: borderWidth)
drawVerticalLine()
dottedLine(with: borderPath, and: borderWidth)
func drawVerticalLine()
{
borderPath = UIBezierPath()
borderPath.moveToPoint(CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y))
//borderPath.addLineToPoint(CGPointMake(self.frame.size.width - 5, self.frame.origin.y + self.frame.size.height - 50))
borderPath.addLineToPoint(CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height))
borderWidth = 2.0
print("border path is %f, %f:\(borderPath)")
}
func drawHorizontalLine()
{
borderPath = UIBezierPath()
borderPath.moveToPoint(CGPointMake(0, self.frame.origin.y))
borderPath.addLineToPoint(CGPointMake(self.frame.size.width + 10, self.frame.origin.y))
borderWidth = 2.0
print("border path is %f, %f:\(borderPath)")
}
func dottedLine (with path:UIBezierPath, and borderWidth:CGFloat)
{
let shapeLayer = CAShapeLayer()
shapeLayer.strokeStart = 0.0
shapeLayer.strokeColor = UIColor.greenColor().CGColor
shapeLayer.lineWidth = borderWidth
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [1,2]
shapeLayer.path = path.CGPath
self.layer.addSublayer(shapeLayer)
}
You can add a new view inside the collection view cell and set the corner radius for that new view. Also you have to reduce the spacing between the cells. Then the line will look like what you expected.
I know it's an old question but it was never properly answered, i was looking for such behaivior and achieved it with the help of another answer.
To achieve that you need to subclass the UICollectionViewFlowLayout (maybe a simple layout also would do but i didn't try).
class HorizontalLineFlowLayout: UICollectionViewFlowLayout {
var insets: CGFloat = 10.0
static let lineWidth: CGFloat = 10.0
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var attributesCopy: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
attributesCopy += [attribute]
let indexPath = attribute.indexPath
if collectionView!.numberOfItems(inSection: indexPath.section) == 0 { continue }
let contains = attributes.contains { layoutAttribute in
layoutAttribute.indexPath == indexPath && layoutAttribute.representedElementKind == HorizontalLineDecorationView.kind
}
if !contains {
let horizontalAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: HorizontalLineDecorationView.kind, with: indexPath)
let width = indexPath.item == collectionView!.numberOfItems(inSection: indexPath.section) - 1 ?
attribute.frame.width + insets :
attribute.frame.width + insets * 1.5
let frame = CGRect(x: attribute.frame.minX, y: attribute.frame.minY, width: width, height: 0.5)
horizontalAttribute.frame = frame
attributesCopy += [horizontalAttribute]
}
}
return attributesCopy
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
the func layoutAttributesForElements is called each time, and it makes the lines for the cells you specify and for the frames you specify.
you would also need the decoration view which looks like that:
class HorizontalLineDecorationView: UICollectionReusableView {
static let kind = "HorizontalLineDecorationView"
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .gray
alpha = 0.2
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
in general its just a view that goes behind the cell, respectively to its indexPath, so the lines are not all along the screen but those are some lines gathered togather which look like a full line, you can adjust its width and height, play with that.
notice that frame that is set for an attribute in the layout, that is the frame of the decoration view, and is defined with respect to the cell (attribute).
dont forget to register that decoration view and also to make the layout and pass it to the collecitonview like this:
let layout = HorizontalLineFlowLayout()
layout.register(HorizontalLineDecorationView.self, forDecorationViewOfKind: HorizontalLineDecorationView.kind)
layout.minimumInteritemSpacing = 10.0
layout.minimumLineSpacing = 10.0
layout.sectionInset = .zero
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(DateCell.self, forCellWithReuseIdentifier: "month")
collectionView.delegate = monthDelegate
collectionView.dataSource = monthDelegate
collectionView.backgroundColor = .clear
the end result is

Resources