CAShapeLayer animation issue - ios

I have a CAShapeLayer (below) which is used to draw a bubble. Which all works fine, as per designs but when i start resizing it within a tableView with tableView.beginUpdates() and tableView.endUpdates(). I end up having the old layer in black momentarily. which is looks awful. I am really puzzled on how to solve this. It kinda feels that layers are black, and the fill paints over them like a mask.
I have tried several things as to add/removing the layer in different places. etc. but result is always this.
video to describe it
override func draw(_ rect: CGRect) {
self.layer.backgroundColor = UIColor.clear.cgColor
let fillColor: UIColor = self.shapeColor
let shapeLayer = CAShapeLayer()
shapeLayer.contentsScale = UIScreen.main.scale
let path = UIBezierPath()
shapeLayer.fillColor = fillColor.cgColor
let width = self.bounds.size.width
let height = self.bounds.size.height
path.move(to: CGPoint(x: 3.02, y: height * 0.5))
path.move(to: CGPoint(x: 3.02, y: 24.9))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addLine(to: CGPoint(x: 3.02, y: 14.62))
path.addCurve(to: CGPoint(x: 17.64, y: -0), controlPoint1: CGPoint(x: 3.02, y: 6.55),controlPoint2: CGPoint(x: 9.57, y: -0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addLine(to: CGPoint(x: width - 15.85, y: 0))
path.addCurve(to: CGPoint(x: width, y: 14.62), controlPoint1: CGPoint(x: width - 7.77, y: 0), controlPoint2: CGPoint(x: width, y: 6.55))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addLine(to: CGPoint(x: width, y: height - 15.1))
path.addCurve(to: CGPoint(x: width - 15.85, y: height), controlPoint1: CGPoint(x: width, y: height - 7.03), controlPoint2: CGPoint(x: width - 7.77, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addLine(to: CGPoint(x: 17.64, y: height))
path.addCurve(to: CGPoint(x: 8.69, y: height - 3.56), controlPoint1: CGPoint(x: 14.39, y: height),controlPoint2: CGPoint(x: 11.24, y: height - 1.57))
path.addLine(to: CGPoint(x: 8.79, y: height - 3.46))
path.addCurve(to: CGPoint(x: 0, y: height), controlPoint1: CGPoint(x: 7.27, y: height - 1.27), controlPoint2: CGPoint(x: 3.84, y: height + 0.51))
path.addCurve(to: CGPoint(x: 3.02, y: height - 14.1), controlPoint1: CGPoint(x: 4.06, y: height - 4.22), controlPoint2: CGPoint(x: 3.02, y: height - 9.62))
path.close()
if self.reverted {
let mirrorOverXOrigin: CGAffineTransform = CGAffineTransform(scaleX: -1.0, y: 1.0);
let translate: CGAffineTransform = CGAffineTransform(translationX: width, y: 0);
// Apply these transforms to the path
path.apply(mirrorOverXOrigin)
path.apply(translate)
}
path.fill()
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
if let bubbleLayer = self.bubbleLayer {
self.layer.replaceSublayer(bubbleLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.bubbleLayer = shapeLayer
}
shapeLayer.path = path.cgPath
shapeLayer.rasterizationScale = UIScreen.main.scale
shapeLayer.shouldRasterize = true
self.layer.insertSublayer(shapeLayer, at: 0)
self.bubbleLayer?.removeFromSuperlayer()
self.bubbleLayer = shapeLayer
}

your solution cannot work. Draw(_ :) is not called during animation block. It is draw after tableView.endUpdates(), after UITableView ends animation.
Draw(_ :) - I don't think you should use it for your needs at all.
If I can recommend you some solution:
1) In awakeFromNib:
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = self.shapeColor
self.contentView.layer.cornerRadius = 15
/** make left view and attach it to the left bottom anchors with fixed size and draw into this view the shape of your left arrow***/
self.leftArrowView.isHidden = self.isReverted
/** make right view and attach it to the right bottom anchors with fixed size and draw into this view the shape of your right arrow***/
self.rightArrowView.isHidden = !self.isReverted
}
2) In prepareForReuse:
override func prepareForReuse() {
super.prepareForReuse()
self.leftArrowView.isHidden = self.isReverted
self.rightArrowView.isHidden = !self.isReverted
}
And this should cause your cell will change size smoothly.

Related

Curved bottom bar

I want to make curved bottom bar it is not looking perfect round by below code
struct customShape: Shape {
var xAxis : CGFloat
var animatableData: CGFloat{
get { return xAxis}
set { xAxis = newValue}
}
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
let center = xAxis
path.move(to: CGPoint(x: center - 43, y: 0))
let to1 = CGPoint(x: center, y: 50)
let center1 = CGPoint(x: center - 30, y: 0)
let center2 = CGPoint(x: center - 46, y: 42)
let to2 = CGPoint(x: center + 52, y: 0)
let center3 = CGPoint(x: center + 57, y: 47)
let center4 = CGPoint(x: center + 35, y: 0)
path.addCurve(to: to1, control1: center1, control2: center2)
path.addCurve(to: to2, control1: center3, control2: center4)
}
}
}
You are trying to construct a circle with Bezier handles. And it seems you are not calculating the values but guessing and trying. This will be at least very hard if not impossible to solve. Try to add an arc instead of a curve.
Possible implementation:
struct customShape: Shape {
var xAxis : CGFloat
var radius: CGFloat = 40
var animatableData: CGFloat{
get { return xAxis}
set { xAxis = newValue}
}
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: 0, y: 0))
// add a line to the starting point of the circle
path.addLine(to: CGPoint(x: xAxis - radius, y: 0))
// add the arc with the centerpoint of (xAxis,0) and 180°
path.addArc(center: .init(x: xAxis, y: 0), radius: radius, startAngle: .init(degrees: 180), endAngle: .init(degrees: 0), clockwise: true)
// complete the rectangle
path.addLine(to: .init(x: rect.size.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.closeSubpath()
}
}
}

Using a custom UITabbar inside UITabbarController programmatically

I'm trying to use my own UITabBar instance inside a UITabBarController. Using Storyboard I know that you can add your Custom Class to your TabbarController using the following:
How can I achieve the same thing programmatically?
Here's my custom class:
I've tried modifying and overriding the tabBar variable inside UITabBarController but it seems that it's a get only variable and can not be modified. Is there an easy solution to fix this issue?
Much appreciated!
class CustomTabBar: UITabBar {
private var shapeLayer: CALayer?
private func addShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.strokeColor = UIColor.lightGray.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1.0
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
override func draw(_ rect: CGRect) {
self.addShape()
}
func createPath() -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let buttonRadius: CGFloat = 35
return abs(self.center.x - point.x) > buttonRadius || abs(point.y) > buttonRadius
}
func createPathCircle() -> CGPath {
let radius: CGFloat = 37.0
let path = UIBezierPath()
let centerWidth = self.frame.width / 2
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: (centerWidth - radius * 2), y: 0))
path.addArc(withCenter: CGPoint(x: centerWidth, y: 0), radius: radius, startAngle: CGFloat(180).degreesToRadians, endAngle: CGFloat(0).degreesToRadians, clockwise: false)
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
}
And here's my view controller:
final class TabbarViewController: UITabBarController {
// MARK: - Properties
private lazy var tabBarItemControllers: [UIViewController] = {
let editorController = ...
let settingsController = ...
return [editorController, settingsController]
}()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureTabbar()
setViewControllers(tabBarItemControllers, animated: true)
}
}

CAShapeLayer draws different line width for one path, Swift

i have a problem while drawing curve:
strokePath = UIBezierPath()
strokePath.move(to: CGPoint(x:curveWidth / 2.0 - headerWidth, y: headerHeight))
strokePath.addLine(to: CGPoint(x: 0, y: headerHeight))
strokePath.addCurve(to: CGPoint(x: curveRadius, y: headerHeight - curveRadius),
controlPoint1: CGPoint(x: degree, y: headerHeight),
controlPoint2: CGPoint(x: curveRadius, y: headerHeight - curveRadius))
strokePath.addLine(to: CGPoint(x: curveWidth - curveRadius, y: curveRadius))
strokePath.addCurve(to: CGPoint(x: curveWidth, y: 0),
controlPoint1: CGPoint(x: curveWidth - curveRadius + curveRadius * 0.25, y: 0),
controlPoint2: CGPoint(x: curveWidth, y: 0))
strokePath.addLine(to: CGPoint(x: self.frame.size.width, y: 0.0))
strokeLayer.path = strokePath.cgPath
strokeLayer.strokeColor = uiConfig.color.misc.tabGrayBorder.cgColor
strokeLayer.fillColor = UIColor.clear.cgColor
strokeLayer.lineWidth = 0.5
self.layer.addSublayer(strokeLayer)
and i have unexpectable result:
curve is bold than the line, have somebody idea why it happened and how it fixing?
sorry, its my mistake after review many lines of code i find active "Clip to bounds" property of one of my parent view

All four corner of a view having different radius

How do i design a UIView whose all 4 corners have different corner radius. So, UIView with its top right corner radius of 20, top left corner radius of 30, bottom left corner radius of 10 and bottom right corner radius of 20
I want to add different cornerRadius to Viw Container shown in image.
using UIBEZIER method cuts off the share comment and like portion.
viw Container is pinned to cell
You can use this UIView extension. It will create and apply a mask layer according to your radius values.
extension UIView {
func applyRadiusMaskFor(topLeft: CGFloat = 0, bottomLeft: CGFloat = 0, bottomRight: CGFloat = 0, topRight: CGFloat = 0) {
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.width - topRight, y: 0))
path.addLine(to: CGPoint(x: topLeft, y: 0))
path.addQuadCurve(to: CGPoint(x: 0, y: topLeft), controlPoint: .zero)
path.addLine(to: CGPoint(x: 0, y: bounds.height - bottomLeft))
path.addQuadCurve(to: CGPoint(x: bottomLeft, y: bounds.height), controlPoint: CGPoint(x: 0, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width - bottomRight, y: bounds.height))
path.addQuadCurve(to: CGPoint(x: bounds.width, y: bounds.height - bottomRight), controlPoint: CGPoint(x: bounds.width, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: topRight))
path.addQuadCurve(to: CGPoint(x: bounds.width - topRight, y: 0), controlPoint: CGPoint(x: bounds.width, y: 0))
let shape = CAShapeLayer()
shape.path = path.cgPath
layer.mask = shape
}
}
Example usage:
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .red
view.applyRadiusMaskFor(topLeft: 80, bottomLeft: 40, bottomRight: 30, topRight: 60)
Result: Radius applied image

Mask UIView (UICollectionView) correctly

Ok, I need to mask a horizontal UICollectionViewso it looks like a ribbon. My web dev counterpart got it done with an SVG that he used as a masking layer.
Here is my current situation:
And here is what I need (Photo from our web app):
Small detail in image spacing aside, I have an SVG like this:
Which I can successfully convert into UIBezierPath code (Paintcode):
let pathPath = UIBezierPath()
pathPath.move(to: CGPoint(x: 0.05, y: 0))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
pathPath.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
pathPath.addLine(to: CGPoint(x: 324, y: 150.2))
pathPath.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
pathPath.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
pathPath.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
pathPath.usesEvenOddFillRule = true
UIColor.lightGray.setFill()
pathPath.fill()
Now what do I do?
You should use the mask property of your UICollectionView by setting it to a view whose alpha channel indicates what part of the UICollectionView you want to mask out. In outline it will probably be something like:
// If you don't have a custom subclass of UICollectionView... you can handle the resize in the
// UICollectionViewController or whatever view contoller is handling your view.
//
// If you do have a custom subclass of UICollectionView... you could do something similar
// in layoutSubviews.
class MyViewController : UICollectionViewController {
// recreate the mask after the view lays out it's subviews
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let maskImage = createMaskingImage(size: self.view.bounds.size) {
let maskView = UIView(frame: self.view.bounds)
maskView.layer.contents = maskImage
self.view.mask = maskView
}
}
}
// create a masking image for a view of the given size
func createMaskingImage(size: CGSize) -> UIImage? {
let drawingBounds = CGRect(origin: CGPoint.zero, size: size)
UIGraphicsBeginImageContext(size)
// We're going to jump down to the level of cgContext for scaling
// You might be able to do this from the level of UIGraphics, but I don't know how so...
// Implicitly unwrapped optional, but if we can't get a CGContext we're in trouble
let cgContext = UIGraphicsGetCurrentContext()!
let maskingPath = createMaskingPath()
let pathBounds = maskingPath.bounds;
cgContext.saveGState()
// Clearing the image may not strictly be necessary
cgContext.clear(drawingBounds)
// Scale the context so that when we draw the path it fits in the drawing bounds
// Could just use "size" here instead of drawingBounds.size, but I think this makes it a
// little more explicit that we're matching up two rects
cgContext.scaleBy(x: drawingBounds.size.width / pathBounds.size.width,
y: drawingBounds.size.height / pathBounds.size.height)
cgContext.setFillColor(UIColor.lightGray.cgColor)
cgContext.addPath(maskingPath.cgPath)
cgContext.fillPath(using: .evenOdd)
cgContext.restoreGState()
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
func createMaskingPath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0.05, y: 0))
path.addCurve(to: CGPoint(x: 162.02, y: 3.8), controlPoint1: CGPoint(x: 0.05, y: 2.1), controlPoint2: CGPoint(x: 72.57, y: 3.8))
path.addCurve(to: CGPoint(x: 324, y: 0), controlPoint1: CGPoint(x: 251.48, y: 3.8), controlPoint2: CGPoint(x: 324, y: 2.1))
path.addLine(to: CGPoint(x: 324, y: 150.2))
path.addCurve(to: CGPoint(x: 162.02, y: 154), controlPoint1: CGPoint(x: 324, y: 152.3), controlPoint2: CGPoint(x: 251.48, y: 154))
path.addCurve(to: CGPoint(x: 0.05, y: 150.2), controlPoint1: CGPoint(x: 72.57, y: 154), controlPoint2: CGPoint(x: 0.05, y: 152.3))
path.addCurve(to: CGPoint(x: 0, y: 0.17), controlPoint1: CGPoint(x: 0.05, y: 148.1), controlPoint2: CGPoint(x: 0, y: 0.17))
return path
}
/*
This is a UICollectionView, which clips normally on the left and right,
but allows some extra space horizontally.
A typical example is you need to clip the scrolling items but you
still need to allow shadows.
*/
import Foundation
import UIKit
class CustomClipCollectionView: UICollectionView {
private lazy var extraSpaceOnBaseButStillClipSidesNormally: CALayer = {
let l = CALayer()
l.backgroundColor = UIColor.black.cgColor
return l
}()
override func layoutSubviews() {
extraSpaceOnBaseButStillClipSidesNormally.frame = bounds.insetBy(
dx: 0, dy: -10)
layer.mask = extraSpaceOnBaseButStillClipSidesNormally
super.layoutSubviews()
}
}

Resources