I came across this demo and really want to learn how to do this:
So far, I've created a subclass to UICollectionViewFlowLayout that fades out the start and end of the cells. Taken from https://gist.github.com/vinhnx/bb1354b247ebfe3790563173ac72baa9
This is the part that fades out the start and end cells
for attrs in attributes {
if attrs.frame.intersects(rect) {
let distance = visibleRect.midY - attrs.center.y
let normalizedDistance = abs(distance) / (visibleRect.height * fadeFactor)
let fade = 1 - normalizedDistance
attrs.alpha = fade
}
}
How do I make the cells that is at the top stand still and not continue to slide up?
Full code of the function handling the fade:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesSuper: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElements(in: rect) as [UICollectionViewLayoutAttributes]!
if let attributes = NSArray(array: attributesSuper, copyItems: true) as? [UICollectionViewLayoutAttributes]{
var visibleRect = CGRect()
visibleRect.origin = collectionView!.contentOffset
visibleRect.size = collectionView!.bounds.size
for attrs in attributes {
if attrs.frame.intersects(rect) {
let distance = visibleRect.midY - attrs.center.y
let normalizedDistance = abs(distance) / (visibleRect.height * fadeFactor)
let fade = 1 - normalizedDistance
attrs.alpha = fade
print(fade)
}
}
return attributes
}else{
return nil
}
}
The look of my app right now:
Related
I've a UICollectionView, with multiple sections and rows.
Header and Footer views wherever necessary, of fixed size.
Cells that are autoresizable
The cell view are designed like :
Green colour - ImageView
Orange colour - Labels with numberOfLines = 0.
The cell should expand it's size according to label numberOfLines.
I've achieved this using this code in MyCustomCell :
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
super.apply(layoutAttributes)
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let targetSize = CGSize(width: Constants.screenWidth/3.5, height: 0)
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
The cells are autoresizing but the contentView (in cyan colour) are Centre aligned both vertically and horizontally.
I need to make it vertically align to Top.
I had the alignment problem with headers and footers too. For that i've subclassed UICollectionViewFlowLayout
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {
let context: UICollectionViewLayoutInvalidationContext = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
let indexPath = preferredAttributes.indexPath
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter, at: [indexPath])
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader, at: [indexPath])
return context
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var topMargin = sectionInset.top
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
topMargin = sectionInset.top
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
Here is an image to illustrate current situation. The cyan are contentView of cells. I've to make it align to Top.
EDIT:
So i realised that UICollectionViewFlowLayout code was creating more bugs than fixing a problem. I added layoutAttributesForElements to left align my cell in case there is only one cell. What it actually did was align all of my cells to the left.
Modified the code as
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
if attributes?.count == 1 {
if let currentAttribute = attributes?.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
}
return attributes
}
}
Now my UICollectionViewCell are properly aligned to horizontal centre with exception to if only one cell which will be left aligned.
Still no solution for vertical alignment.
So I worked around this for quite a bit. No answers from stack overflow help in any way. So i played with custom UICollectionViewFlowLayout method layoutAttributesForElements.
This method will call for every section, and will have layoutAttributesForElements for rect. This will give an array of UICollectionViewLayoutAttributes which one can modify as per the need.
I was having problem figuring out the y coordinate which I would like to change for my cells.
So first with a loop in attributes from layoutAttributesForElements I got the header and saved it's height in a variable. Then changed the rest of cell's y coordinate with the headerHeight value.
Not sure if this is the right way or not, nor did any performance testing. For now everything seems fine without any lag or jerk.
Here is full code or custom UICollectionViewFlowLayout.
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attr = super.layoutAttributesForElements(in: rect)
var attributes = [UICollectionViewLayoutAttributes]()
for itemAttributes in attr! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
// manipulate itemAttributesCopy
attributes.append(itemAttributesCopy)
}
if attributes.count == 1 {
if let currentAttribute = attributes.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
} else {
var sectionHeight: CGFloat = 0
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .supplementaryView else {
return
}
if layoutAttribute.representedElementKind == UICollectionView.elementKindSectionHeader {
sectionHeight = layoutAttribute.frame.size.height
}
}
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.x == 0 {
sectionHeight = max(layoutAttribute.frame.minY, sectionHeight)
}
layoutAttribute.frame = CGRect(origin: CGPoint(x: layoutAttribute.frame.origin.x, y: sectionHeight), size: layoutAttribute.frame.size)
}
}
return attributes
}
}
NOTE: You need to copy the attributes in an array first to work with. Else xcode will yell Logging only once for UICollectionViewFlowLayout cache mismatched frame in console.
If any new/perfect solution is there please let me know.
CHEERS!!!
I have a "off-screen" UIView hierarchy which I want render in different locations of my screen. In addition it should be possible to show only parts of this view hierarchy and should reflect all changes made to this hierarchy.
The difficulties:
The UIView method drawHierarchy(in:afterScreenUpdates:) always calls draw(_ rect:) and is therefore very inefficient for large hierarchies if you want to incorporate all changes to the view hierarchy. You would have to redraw it every screen update or observe all changing properties of all views. Draw view hierarchy documentation
The UIView method snapshotView(afterScreenUpdates:) also does not help much since I have not found a way to get a correct view hierarchy drawing if this hierarchy is "off-screen". Snapshot view documentation
"Off-Screen": The root view of this view hierarchy is not part of the UI of the app. It has no superview.
Below you can see a visual representation of my idea:
Here's how I would go about doing it. First, I would duplicate the view you are trying to duplicate. I wrote a little extension for this:
extension UIView {
func duplicate<T: UIView>() -> T {
return NSKeyedUnarchiver.unarchiveObject(with: NSKeyedArchiver.archivedData(withRootObject: self)) as! T
}
func copyProperties(fromView: UIView, recursive: Bool = true) {
contentMode = fromView.contentMode
tag = fromView.tag
backgroundColor = fromView.backgroundColor
tintColor = fromView.tintColor
layer.cornerRadius = fromView.layer.cornerRadius
layer.maskedCorners = fromView.layer.maskedCorners
layer.borderColor = fromView.layer.borderColor
layer.borderWidth = fromView.layer.borderWidth
layer.shadowOpacity = fromView.layer.shadowOpacity
layer.shadowRadius = fromView.layer.shadowRadius
layer.shadowPath = fromView.layer.shadowPath
layer.shadowColor = fromView.layer.shadowColor
layer.shadowOffset = fromView.layer.shadowOffset
clipsToBounds = fromView.clipsToBounds
layer.masksToBounds = fromView.layer.masksToBounds
mask = fromView.mask
layer.mask = fromView.layer.mask
alpha = fromView.alpha
isHidden = fromView.isHidden
if let gradientLayer = layer as? CAGradientLayer, let fromGradientLayer = fromView.layer as? CAGradientLayer {
gradientLayer.colors = fromGradientLayer.colors
gradientLayer.startPoint = fromGradientLayer.startPoint
gradientLayer.endPoint = fromGradientLayer.endPoint
gradientLayer.locations = fromGradientLayer.locations
gradientLayer.type = fromGradientLayer.type
}
if let imgView = self as? UIImageView, let fromImgView = fromView as? UIImageView {
imgView.tintColor = .clear
imgView.image = fromImgView.image?.withRenderingMode(fromImgView.image?.renderingMode ?? .automatic)
imgView.tintColor = fromImgView.tintColor
}
if let btn = self as? UIButton, let fromBtn = fromView as? UIButton {
btn.setImage(fromBtn.image(for: fromBtn.state), for: fromBtn.state)
}
if let textField = self as? UITextField, let fromTextField = fromView as? UITextField {
if let leftView = fromTextField.leftView {
textField.leftView = leftView.duplicate()
textField.leftView?.copyProperties(fromView: leftView)
}
if let rightView = fromTextField.rightView {
textField.rightView = rightView.duplicate()
textField.rightView?.copyProperties(fromView: rightView)
}
textField.attributedText = fromTextField.attributedText
textField.attributedPlaceholder = fromTextField.attributedPlaceholder
}
if let lbl = self as? UILabel, let fromLbl = fromView as? UILabel {
lbl.attributedText = fromLbl.attributedText
lbl.textAlignment = fromLbl.textAlignment
lbl.font = fromLbl.font
lbl.bounds = fromLbl.bounds
}
if recursive {
for (i, view) in subviews.enumerated() {
if i >= fromView.subviews.count {
break
}
view.copyProperties(fromView: fromView.subviews[i])
}
}
}
}
to use this extension, simply do
let duplicateView = originalView.duplicate()
duplicateView.copyProperties(fromView: originalView)
parentView.addSubview(duplicateView)
Then I would mask the duplicate view to only get the particular section that you want
let mask = UIView(frame: CGRect(x: 0, y: 0, width: yourNewWidth, height: yourNewHeight))
mask.backgroundColor = .black
duplicateView.mask = mask
finally, I would scale it to whatever size you want using CGAffineTransform
duplicateView.transform = CGAffineTransform(scaleX: xScale, y: yScale)
the copyProperties function should work well but you can change it if necessary to copy even more things from one view to another.
Good luck, let me know how it goes :)
I'd duplicate the content I wish to display and crop it as I want.
Let's say I have a ContentViewController which carries the view hierarchy I wish to replicate. I would encapsule all the changes that can be made to the hierarchy inside a ContentViewModel. Something like:
struct ContentViewModel {
let actionTitle: String?
let contentMessage: String?
// ...
}
class ContentViewController: UIViewController {
func display(_ viewModel: ContentViewModel) { /* ... */ }
}
With a ClippingView (or a simple UIScrollView) :
class ClippingView: UIView {
var contentOffset: CGPoint = .zero // a way to specify the part of the view you wish to display
var contentFrame: CGRect = .zero // the actual size of the clipped view
var clippedView: UIView?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
clippedView?.frame = contentFrame
clippedView?.frame.origin = contentOffset
}
}
And a view controller container, I would crop each instance of my content and update all of them each time something happens :
class ContainerViewController: UIViewController {
let contentViewControllers: [ContentViewController] = // 3 in your case
override func viewDidLoad() {
super.viewDidLoad()
contentViewControllers.forEach { viewController in
addChil(viewController)
let clippingView = ClippingView()
clippingView.clippedView = viewController.view
clippingView.contentOffset = // ...
viewController.didMove(to: self)
}
}
func somethingChange() {
let newViewModel = ContentViewModel(...)
contentViewControllers.forEach { $0.display(newViewModel) }
}
}
Could this scenario work in your case ?
Right now I have a collectionView for which each cell contains a horizontal stackView. The stackView gets populated with a series of UIViews (rectangles), one for each day of a month - each cell corresponds to a month. I fill the stack views like so:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == self.collectionView {
...
return cell
} else if collectionView == self.timeline {
let index = indexPath.row
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM"
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: timelineMonthCellReuseIdentifier, for: indexPath) as! SNTimelineMonthViewCell
let firstPost = posts.first?.timeStamp
let month = Calendar.current.date(byAdding: .month, value: index, to: firstPost!)
print(dateFormatter.string(from: month!),dateFormatter.string(from: firstPost!),"month diff")
for post in posts {
print(post.timeStamp, "month diff")
}
cell.monthLabel.text = dateFormatter.string(from: month!)
cell.monthLabel.textAlignment = .center
if let start = month?.startOfMonth(), let end = month?.endOfMonth(), let stackView = cell.dayTicks {
var date = start
while date <= end {
let line = UIView()
if posts.contains(where: { Calendar.current.isDate(date, inSameDayAs: $0.timeStamp) }) {
line.backgroundColor = UIColor(red:0.15, green:0.67, blue:0.93, alpha:1.0)
let tapGuesture = UITapGestureRecognizer(target: self, action: #selector (self.tapBar (_:)))
line.isUserInteractionEnabled = true
line.addGestureRecognizer(tapGuesture)
self.dayTicks[date] = line
} else {
line.backgroundColor = UIColor.clear
}
stackView.addArrangedSubview(line)
date = Calendar.current.date(byAdding: .day, value: 1, to: date)!
}
}
return cell
} else {
preconditionFailure("Unknown collection view!")
}
}
Then, when the user stops scrolling a different collection view, I want to add a subview called arrowView ontop of the dayTick (see how self.dayTicks gets populated with the subviews of the stackView above).
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width
let post = posts[Int(currentIndex)]
for (_,tick) in self.dayTicks {
tick.subviews.forEach({ $0.removeFromSuperview() })
}
let day = Calendar.current.startOfDay(for: post.timeStamp)
let tick = self.dayTicks[day]
let arrow = UIImage(named:"Tracer Pin")
let arrowView = UIImageView(image: arrow)
// arrowView.clipsToBounds = false
print((tick?.frame.origin)!,"tick origin")
// arrowView.frame.origin = (tick?.frame.origin)!
// arrowView.frame.size.width = 100
// arrowView.frame.size.height = 100
tick?.addSubview(arrowView)
}
This kind of works and it looks like this:
The red rectangle is added but it appears to the right of the dayTick, and it appears as a long thin rectangle. In actuality, the Tracer Pin image referenced looks like this:
Thats at least where the red color comes from but as you can see its stretching it weird and clipping everything thats not in a rectangular UIView space.
Now note that I commented out the 4 lines that set the size and origin of the arrowView as well as setting clipToBounds to false. When I uncomment these lines - the arrowView simply doesn't show up at all so I must be doing this wrong. What I want is to show something like this:
How can I put it directly on top like that?
Another perspective might be to do this with CALayer. Here are some clues (cut from another project) to help you discover a solution:
#IBInspectable open var slideIndicatorThickness: CGFloat = 5.0 {
didSet {
if slideIndicator != nil { slideIndicator.removeFromSuperlayer() }
let slideLayer = CALayer()
let theOrigin = CGPoint(x: bounds.origin.x, y: bounds.origin.y)
let theSize = CGSize(width: CGFloat(3.0), height: CGFloat(10.0)
slideLayer.frame = CGRect(origin: theOrigin, size: theSize)
slideLayer.backgroundColor = UIColor.orange.cgColor
slideIndicator = slideLayer
layer.addSublayer(slideIndicator)
}
}
fileprivate var slideIndicator: CALayer!
fileprivate func updateIndicator() {
// ..
// Somehow figure out new frame, based on stack view's frame.
slideIndicator.frame.origin.x = newOrigin
}
You may have to implement this on a subclass of UIStackView, or your own custom view that is a wrapper around UIStackView.
It looks like you have a fixed height on the arrowView. Could it be that the red triangle portion is under another view?
Debug View Hierarchy
Click the debug view hierarchy which is the second from right icon - it looks like 3 rectangles. Check to see if the whole image is there.
I ended up using CALayer - thanks to #ouni's suggestion For some reason the CALayer seemed to draw directly on top of the view whereas a subview didn't. One key was unchecking the "Clip to bounds box on the collectionView cell itself (as opposed to the subview) - so that I could draw the flared base of the arrow outside of the collection view cell:
My code looks like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == self.collectionView {
print("is collection view")
print(scrollView,"collection view")
let currentIndex = self.collectionView.contentOffset.x / self.collectionView.frame.size.width
let post = posts[Int(currentIndex)]
for (_,tick) in self.dayTicks {
tick.layer.sublayers = nil
}
let day = Calendar.current.startOfDay(for: post.timeStamp)
let tick = self.dayTicks[day]
let arrowLayer = CAShapeLayer()
let path = UIBezierPath()
let start_x = (tick?.bounds.origin.x)!
let start_y = (tick?.bounds.minY)!
let top_width = (tick?.bounds.width)!
let tick_height = (tick?.bounds.height)!
let tip_height = CGFloat(10)
let tip_flare = CGFloat(10)
path.move(to: CGPoint(x: start_x, y: start_y))
path.addLine(to: CGPoint(x: start_x + top_width,y: start_y))
path.addLine(to: CGPoint(x: start_x + top_width,y: start_y + tick_height))
path.addLine(to: CGPoint(x: start_x + top_width + tip_flare,y: start_y+tick_height+tip_height))
path.addLine(to: CGPoint(x: start_x - tip_flare,y: start_y + tick_height + tip_height))
path.addLine(to: CGPoint(x: start_x,y: start_y+tick_height))
path.close()
arrowLayer.path = path.cgPath
arrowLayer.fillColor = UIColor(red:0.99, green:0.13, blue:0.25, alpha:1.0).cgColor
tick?.layer.addSublayer(arrowLayer)
} else {
print(scrollView, "timeline collection view")
}
}
This draws the arrow on top of the subview beautifully.
I want to create a custom UIView subclass representing a bunch of stars on a dark-blue sky.
Therefore, I created this view:
import UIKit
class ConstellationView: UIView {
// MARK: - Properties
#IBInspectable var numberOfStars: Int = 80
#IBInspectable var animated: Bool = false
// Private properties
private var starsToDraw = [CAShapeLayer]()
// Layers
private let starsLayer = CAShapeLayer()
// MARK: - Drawing
// override func drawRect(rect: CGRect) {
override func layoutSubviews() {
// Generate stars
drawStars(rect: self.bounds)
}
/// Generate stars
func drawStars(rect: CGRect) {
let width = rect.size.width
let height = rect.size.height
let screenBounds = UIScreen.main.bounds
// Create the stars and store them in starsToDraw array
for _ in 0 ..< numberOfStars {
let x = randomFloat() * width
let y = randomFloat() * height
// Calculate the thinness of the stars as a percentage of the screen resolution
let thin: CGFloat = max(screenBounds.width, screenBounds.height) * 0.003 * randomFloat()
let starLayer = CAShapeLayer()
starLayer.path = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: thin, height: thin)).cgPath
starLayer.fillColor = UIColor.white.cgColor
starsToDraw.append(starLayer)
}
// Define a fade animation
let appearAnimation = CABasicAnimation(keyPath: "opacity")
appearAnimation.fromValue = 0.2
appearAnimation.toValue = 1
appearAnimation.duration = 1
appearAnimation.fillMode = kCAFillModeForwards
// Add the animation to each star (if animated)
for (index, star) in starsToDraw.enumerated() {
if animated {
// Add 1 s between each animation
appearAnimation.beginTime = CACurrentMediaTime() + TimeInterval(index)
star.add(appearAnimation, forKey: nil)
}
starsLayer.insertSublayer(star, at: 0)
}
// Add the stars layer to the view layer
layer.insertSublayer(starsLayer, at: 0)
}
private func randomFloat() -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UINT32_MAX)
}
}
It works quite well, here is the result:
However, I'd like to have it animated, that is, each one of the 80 stars should appear one after the other, with a 1 second delay.
I tried to increase the beginTimeof my animation, but it does not seem to do the trick.
I checked with drawRect or layoutSubviews, but there is no difference.
Could you help me ?
Thanks
PS: to reproduce my app, just create a new single view app in XCode, create a new file with this code, and set the ViewController's view as a ConstellationView, with a dark background color. Also set the animated property to true, either in Interface Builder, or in the code.
PPS: this is in Swift 3, but I think it's still comprehensible :-)
You're really close, only two things to do!
First, you need to specify the key when you add the animation to the layer.
star.add(appearAnimation, forKey: "opacity")
Second, the fill mode for the animation needs to be kCAFillModeBackwards instead of kCAFillModeForwards.
For a more detailed reference see - https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html
And here's a fun tutorial (for practice with CAAnimations!) - https://www.raywenderlich.com/102590/how-to-create-a-complex-loading-animation-in-swift
Hope this helps 😀
Full Code:
class ConstellationView: UIView {
// MARK: - Properties
#IBInspectable var numberOfStars: Int = 80
#IBInspectable var animated: Bool = true
// Private properties
private var starsToDraw = [CAShapeLayer]()
// Layers
private let starsLayer = CAShapeLayer()
override func awakeFromNib() {
super.awakeFromNib()
}
// MARK: - Drawing
override func layoutSubviews() {
// Generate stars
drawStars(rect: self.bounds)
}
/// Generate stars
func drawStars(rect: CGRect) {
let width = rect.size.width
let height = rect.size.height
let screenBounds = UIScreen.main.bounds
// Create the stars and store them in starsToDraw array
for _ in 0 ..< numberOfStars {
let x = randomFloat() * width
let y = randomFloat() * height
// Calculate the thinness of the stars as a percentage of the screen resolution
let thin: CGFloat = max(screenBounds.width, screenBounds.height) * 0.003 * randomFloat()
let starLayer = CAShapeLayer()
starLayer.path = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: thin, height: thin)).cgPath
starLayer.fillColor = UIColor.white.cgColor
starsToDraw.append(starLayer)
}
// Define a fade animation
let appearAnimation = CABasicAnimation(keyPath: "opacity")
appearAnimation.fromValue = 0.2
appearAnimation.toValue = 1
appearAnimation.duration = 1
appearAnimation.fillMode = kCAFillModeBackwards
// Add the animation to each star (if animated)
for (index, star) in starsToDraw.enumerated() {
if animated {
// Add 1 s between each animation
appearAnimation.beginTime = CACurrentMediaTime() + TimeInterval(index)
star.add(appearAnimation, forKey: "opacity")
}
starsLayer.insertSublayer(star, above: nil)
}
// Add the stars layer to the view layer
layer.insertSublayer(starsLayer, above: nil)
}
private func randomFloat() -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UINT32_MAX)
}
}
How can I implement this? A table view with oblique cells. I was thinking first to make the cells overlap and cut out a piece, but I can't make it work.
Now the single solution I can think of is to download the second image in first cell and cut out the top triangle and add to the first cell. But that wouldn't be too optimal memory wise and processing wise if the user is scrolling through the list.
I appreciate any advice, thank you!
I've done something similar like this but I didn't use UITableView or UICollectionView.
What I use is having one UIImageView(AView) on top of the other UIImageView(BView) in view hieararchy. So I add UISwipeGestureRecognizer to BView .And at the top of BView I've added a seperator view(when VC load user could not see it). And I basically animating BView however I want.
But since they are not cells in UITableView you can't give "pulling" behavior to user which is a UX disadvantage for your app.
I solved it the following way. I used UICollectionView in the end because I couldn't find a way to overlap nicely the cells from UITableView. So first I made a custom layout which looks like this:
CustomCollectionViewFlowLayout.swift
import UIKit
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func collectionViewContentSize() -> CGSize {
let xSize = self.itemSize.width
let ySize = CGFloat(self.collectionView!.numberOfItemsInSection(0)) * self.itemSize.height
var size = CGSizeMake(xSize, ySize)
if self.collectionView!.bounds.size.width > size.width {
size.width = self.collectionView!.bounds.size.width
}
if self.collectionView!.bounds.size.height > size.height {
size.height = self.collectionView!.bounds.size.height
}
return size
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let attributesArray = super.layoutAttributesForElementsInRect(rect) as! [UICollectionViewLayoutAttributes]
let numberOfItems = self.collectionView?.numberOfItemsInSection(0)
for attribute in attributesArray {
let xPosition = attribute.center.x
var yPosition = attribute.center.y
if attribute.indexPath.row == 0 {
attribute.zIndex = Int(INT_MAX)
} else {
yPosition -= CGFloat(60 * attribute.indexPath.row)
attribute.zIndex = numberOfItems! - attribute.indexPath.row
}
attribute.center = CGPointMake(xPosition, yPosition)
}
return attributesArray
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
return UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
}
}
Then I used paths and shapes to cut out that part and also draw a white rectangle. So it looks like this:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ChallengeCollectionViewCell", forIndexPath: indexPath) as! ChallengeCollectionViewCell
cell.challengeImage.sd_setImageWithURL(NSURL(string: STATIC_IMAGE_URL), completed: nil)
let trianglePath = CGPathCreateMutable()
CGPathMoveToPoint(trianglePath, nil, 0, 0)
CGPathAddLineToPoint(trianglePath, nil, 0, cell.challengeImage!.frame.size.height)
CGPathAddLineToPoint(trianglePath, nil, cell.challengeImage!.frame.size.width, cell.challengeImage!.frame.size.height - 50)
CGPathAddLineToPoint(trianglePath, nil, cell.challengeImage!.frame.width, 0)
CGPathAddLineToPoint(trianglePath, nil, 0, 0)
CGPathCloseSubpath(trianglePath)
let shapeLayer = CAShapeLayer()
shapeLayer.frame = cell.challengeImage!.frame
shapeLayer.path = trianglePath
cell.challengeImage.layer.mask = shapeLayer
cell.challengeImage.layer.masksToBounds = true
let linePath = CGPathCreateMutable()
CGPathMoveToPoint(linePath, nil, 0, cell.challengeImage!.frame.size.height)
CGPathAddLineToPoint(linePath, nil, cell.challengeImage!.frame.size.width, cell.challengeImage!.frame.size.height - 50)
CGPathCloseSubpath(linePath)
let lineShape = CAShapeLayer()
lineShape.path = linePath
lineShape.lineWidth = 10
lineShape.strokeColor = UIColor.whiteColor().CGColor
cell.challengeImage.layer.addSublayer(lineShape)
return cell
}
I hope it will be useful for someone who runs into the same problem. Thanks and best wishes!