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
Related
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
I am an android application developer and new to iOS programming and my very first challenge is to build a 2-way scrolling table in iOS. I am getting many solutions with UICollectionView inside UITableView. But in my case rows will scroll together, not independent of each other. There are more than 15 columns and 100+ rows with text data in the table.
I have achieved the same in Android by using a ListView inside a HorizontalScrollView. But yet to find any solution in iOS. Any help is greatly appreciated.
EDIT: I have added a couple of screens of the android app where the table is scrolled horizontally.
So you want this:
You should use a UICollectionView. You can't use UICollectionViewFlowLayout (the only layout that's provided in the public SDK) because it is designed to only scroll in one direction, so you need to implement a custom UICollectionViewLayout subclass that arranges the elements to scroll in both directions if needed.
For full details on building a custom UICollectionViewLayout subclass, you should watch these: videos from WWDC 2012:
Session 205: Introducing Collection Views
Session 219: Advanced Collection Views and Building Custom Layouts
Anyway, I'll just dump an example implementation of GridLayout here for you to start with. For each IndexPath, I use the section as the row number and the item as the column number.
class GridLayout: UICollectionViewLayout {
var cellHeight: CGFloat = 22
var cellWidths: [CGFloat] = [] {
didSet {
precondition(cellWidths.filter({ $0 <= 0 }).isEmpty)
invalidateCache()
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: totalWidth, height: totalHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// When bouncing, rect's origin can have a negative x or y, which is bad.
let newRect = rect.intersection(CGRect(x: 0, y: 0, width: totalWidth, height: totalHeight))
var poses = [UICollectionViewLayoutAttributes]()
let rows = rowsOverlapping(newRect)
let columns = columnsOverlapping(newRect)
for row in rows {
for column in columns {
let indexPath = IndexPath(item: column, section: row)
poses.append(pose(forCellAt: indexPath))
}
}
return poses
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return pose(forCellAt: indexPath)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return false
}
private struct CellSpan {
var minX: CGFloat
var maxX: CGFloat
}
private struct Cache {
var cellSpans: [CellSpan]
var totalWidth: CGFloat
}
private var _cache: Cache? = nil
private var cache: Cache {
if let cache = _cache { return cache }
var spans = [CellSpan]()
var x: CGFloat = 0
for width in cellWidths {
spans.append(CellSpan(minX: x, maxX: x + width))
x += width
}
let cache = Cache(cellSpans: spans, totalWidth: x)
_cache = cache
return cache
}
private var totalWidth: CGFloat { return cache.totalWidth }
private var cellSpans: [CellSpan] { return cache.cellSpans }
private var totalHeight: CGFloat {
return cellHeight * CGFloat(collectionView?.numberOfSections ?? 0)
}
private func invalidateCache() {
_cache = nil
invalidateLayout()
}
private func rowsOverlapping(_ rect: CGRect) -> Range<Int> {
let startRow = Int(floor(rect.minY / cellHeight))
let endRow = Int(ceil(rect.maxY / cellHeight))
return startRow ..< endRow
}
private func columnsOverlapping(_ rect: CGRect) -> Range<Int> {
let minX = rect.minX
let maxX = rect.maxX
if let start = cellSpans.firstIndex(where: { $0.maxX >= minX }), let end = cellSpans.lastIndex(where: { $0.minX <= maxX }) {
return start ..< end + 1
} else {
return 0 ..< 0
}
}
private func pose(forCellAt indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let pose = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let row = indexPath.section
let column = indexPath.item
pose.frame = CGRect(x: cellSpans[column].minX, y: CGFloat(row) * cellHeight, width: cellWidths[column], height: cellHeight)
return pose
}
}
To draw the separating lines, I added hairline views to each cell's background:
class GridCell: UICollectionViewCell {
static var reuseIdentifier: String { return "cell" }
override init(frame: CGRect) {
super.init(frame: frame)
label.frame = bounds.insetBy(dx: 2, dy: 2)
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.addSubview(label)
let backgroundView = UIView(frame: CGRect(origin: .zero, size: frame.size))
backgroundView.backgroundColor = .white
self.backgroundView = backgroundView
rightSeparator.backgroundColor = .gray
backgroundView.addSubview(rightSeparator)
bottomSeparator.backgroundColor = .gray
backgroundView.addSubview(bottomSeparator)
}
func setRecord(_ record: String) {
label.text = record
}
override func layoutSubviews() {
super.layoutSubviews()
let thickness = 1 / (window?.screen.scale ?? 1)
let size = bounds.size
rightSeparator.frame = CGRect(x: size.width - thickness, y: 0, width: thickness, height: size.height)
bottomSeparator.frame = CGRect(x: 0, y: size.height - thickness, width: size.width, height: thickness)
}
private let label = UILabel()
private let rightSeparator = UIView()
private let bottomSeparator = UIView()
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here's my demo view controller:
class ViewController: UIViewController {
var records: [[String]] = (0 ..< 20).map { row in
(0 ..< 6).map {
column in
"Row \(row) column \(column)"
}
}
var cellWidths: [CGFloat] = [ 180, 200, 180, 160, 200, 200 ]
override func viewDidLoad() {
super.viewDidLoad()
let layout = GridLayout()
layout.cellHeight = 44
layout.cellWidths = cellWidths
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.isDirectionalLockEnabled = true
collectionView.backgroundColor = UIColor(white: 0.95, alpha: 1)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.register(GridCell.self, forCellWithReuseIdentifier: GridCell.reuseIdentifier)
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return records.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return records[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GridCell.reuseIdentifier, for: indexPath) as! GridCell
cell.setRecord(records[indexPath.section][indexPath.item])
return cell
}
}
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.
I've been having this problem for weeks and I think I am not able to solve it.
I have a tableView which has custom tableViewCells and they are filled with data. The tableViewCell has two UILabels and one UIView.
The problem appears when I scroll several times the drawings on the UIViews are overlapped one with another, I think they are redrawn. I know that this behavior is because of the reuse of the cells but I can't even locate the origin of the problem.
UIViews show perfectly when opening the app
UIViews get overlapped after scrolling
My UITableView's cellForRowAtIndexPath is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let CellIdentifier = "Cell"
let cell: CustomTableViewcell = self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! CustomTableViewcell
//self.tableView.dequeueReusableCellWithIdentifier(CellIdentifier) as! CustomTableViewcell
cell.prepareForReuse()
self.createUIForCell(cell)
self.configureCell(cell, indexPath: indexPath)
print("\t\(parsedData[indexPath.row].stationName): \(parsedData[indexPath.row].freeBikes)")
return cell
}
func createUIForCell(cell: CustomTableViewcell) {
cell.distanceLabel.textColor = UIColor.whiteColor()
cell.distanceLabel.backgroundColor = UIColor.clearColor()
cell.bikeStationLabel.textColor = UIColor.whiteColor()
cell.bikeStationLabel.backgroundColor = UIColor.clearColor()
}
func configureCell(cell: CustomTableViewcell, indexPath: NSIndexPath) {
let stations = parsedData[indexPath.row]
if indexPath.row % 2 == 0 {
cell.stackView.arrangedSubviews.first!.backgroundColor = cellBackgroundColor1
cell.progressView.backgroundColor = cellBackgroundColor2
} else {
cell.stackView.arrangedSubviews.first!.backgroundColor = cellBackgroundColor2
cell.progressView.backgroundColor = cellBackgroundColor1
}
cell.progressView.getWidth(stations.freeBikes, freeDocks: stations.freeDocks)
cell.getLocation(stations.latitude, longitude: stations.longitude)
cell.getParameters(stations.freeBikes, freeDocks: stations.freeDocks)
cell.bikeStationLabel.text = stations.stationName
if stations.distanceToLocation != nil {
if stations.distanceToLocation! < 1000 {
cell.distanceLabel.text = String(format: "%.f m", stations.distanceToLocation!)
} else {
cell.distanceLabel.text = String(format: "%.1f km", stations.distanceToLocation! / 1000)
}
} else {
cell.distanceLabel.text = ""
}
}
For the before mentioned UIVIew inside my custom cell I have created a separated class for it to handle the drawing and it looks like this:
import UIKit
class ProgressViewBar: UIView {
var freeBikes = 0
var freeDocks = 0
var text: UILabel? = nil
var text2: UILabel? = nil
func getWidth(freeBikes: Int, freeDocks: Int) {
self.freeBikes = freeBikes
self.freeDocks = freeDocks
}
override func drawRect(rect: CGRect) {
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: rect.width * (CGFloat(freeBikes) / (CGFloat(freeBikes) + CGFloat(freeDocks))), height: frame.height), cornerRadius: 0)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.whiteColor().CGColor
shapeLayer.fillColor = UIColor.whiteColor().CGColor
self.layer.addSublayer(shapeLayer)
drawText(rect)
}
func drawText(rect: CGRect) {
if freeBikes != 0 {
text?.removeFromSuperview()
text = UILabel(frame: CGRect(x: 2, y: 0, width: 20, height: 20))
text!.text = String("\(freeBikes)")
text!.textAlignment = NSTextAlignment.Left
text!.font = UIFont(name: "HelveticaNeue-Thin", size: 17)
text!.backgroundColor = UIColor.clearColor()
text!.textColor = UIColor(red: 1/255, green: 87/255, blue: 155/255, alpha: 1)
self.addSubview(text!)
}
text2?.removeFromSuperview()
text2 = UILabel(frame: CGRect(x: rect.width * (CGFloat(freeBikes) / (CGFloat(freeBikes) + CGFloat(freeDocks))) + 2, y: 0, width: 20, height: 20))
text2!.text = String("\(freeDocks)")
text2!.textAlignment = NSTextAlignment.Left
text2!.font = UIFont(name: "HelveticaNeue-Thin", size: 17)
text2!.backgroundColor = UIColor.clearColor()
text2!.textColor = UIColor.whiteColor()
self.addSubview(text2!)
}
}
The view needs two parameters to be drawn and I pass them using the func getWidth(freeBikes: Int, freeDocks: Int) method calling it from the `ViewController. If any other piece of code is needed you can look at the repo.
The problem is that when reusing the cell you call drawRect once again and don't clear the already drawn rect. Clear it before drawing another one.
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!)