I have a problem while animating the CAGradientLayer angle.
Angle in CAGradientLayer is represented through start and end point properties.
I want to animate the gradient in a circular fashion.
When I set it inside an animationGroup it doesn't work. No animation is happening.
When I am changing the properties in
DispatchQueue.main.asyncAfter(deadline: now() + 1.0) {
// change properties here
}
it works. But a very fast animation is happening. Which is not good enough.
On the internet the only thing there is is locations and color changes, but no angle change.
Below you can find a Playground project to play with
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
// Gradient layer specification
lazy var gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.withAlphaComponent(0.5).cgColor, UIColor.orange.withAlphaComponent(0.5).cgColor, UIColor.orange.cgColor]
gradientLayer.locations = [0, 0.27, 1]
gradientLayer.frame = view.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
return gradientLayer
}()
func animationMapFunction(points: [(CGPoint, CGPoint)], keyPath: String) -> [CABasicAnimation] {
return points.enumerated().map { (arg) -> CABasicAnimation in
let (offset, element) = arg
let gradientStartPointAnimation = CABasicAnimation(keyPath: keyPath)
gradientStartPointAnimation.fromValue = element.0
gradientStartPointAnimation.toValue = element.1
gradientStartPointAnimation.beginTime = CACurrentMediaTime() + Double(offset)
return gradientStartPointAnimation
}
}
lazy var gradientAnimation: CAAnimation = {
let startPointAnimationPoints = [(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0)),
(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0))]
let endPointAnimatiomPoints = [(CGPoint(x: 1.0, y:1.0), CGPoint(x: 0.0, y:1.0)),
(CGPoint(x: 0.0, y:1.0), CGPoint(x: 0.0, y:0.0)),
(CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y:0.0)),
(CGPoint(x: 1.0, y:0.0), CGPoint(x: 1.0, y:1.0))]
let startPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "startPoint")
let endPointAnimations = animationMapFunction(points: startPointAnimationPoints, keyPath: "endPoint")
let animationGroup = CAAnimationGroup()
animationGroup.duration = 5.0
animationGroup.repeatCount = Float.infinity
animationGroup.animations = startPointAnimations + endPointAnimations
return animationGroup
}()
override func loadView() {
let view = UIView(frame: UIScreen.main.bounds)
self.view = view
view.layer.addSublayer(gradientLayer)
}
func animate() {
view.layer.removeAllAnimations()
gradientLayer.add(gradientAnimation, forKey: nil)
}
}
// Present the view controller in the Live View window
let vc = MyViewController()
PlaygroundPage.current.liveView = vc
vc.animate()
So what I did was a timer, that triggers start and end points change.
I rotate from 0 to 360 degrees, while incrementing the angle with a given constant (in my case 6)
I created a function mapping: angle → (startPoint, endPoint)
func animate() {
stopAnimation()
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { timer in
self.angle += 6.0
self.angle.formTruncatingRemainder(dividingBy: 360)
CATransaction.begin()
// disable implicit animation
CATransaction.setDisableActions(true)
let pos = points(from: self.angle)
self.gradientLayer.startPoint = pos.0
self.gradientLayer.endPoint = pos.1
CATransaction.commit()
})
}
func stopAnimation() {
timer?.invalidate()
}
And here are the utility functions
extension CGPoint {
var inverse: CGPoint {
return CGPoint(x: 1 - x, y: 1 - y)
}
}
fileprivate func points(from angle: Double) -> (CGPoint, CGPoint) {
let start: CGPoint
switch angle {
case let x where 0 <= x && x < 90:
start = CGPoint(x: x / 180, y: 0.5 - x / 180)
case let x where 90 <= x && x < 180:
start = CGPoint(x: x / 180, y: x / 180 - 0.5)
case let x where 180 <= x && x < 270:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (x - 180) / 180)
case let x where 270 <= x && x < 360:
start = CGPoint(x: 2.0 - x / 180, y: 0.5 + (360 - x) / 180)
default:
start = CGPoint.zero
}
return (start, start.inverse)
}
Related
I'm trying to draw an arrow on a MKPolyline, which is working well by overriding the draw method, however the result is a blurred line. Any clues on why this might be?
It also does not show the arrows when zoomed out (zoomScale < 0.05), and also increases the line width as you zoom out,
Code below:
class ArrowPolylineRenderer: MKPolylineRenderer {
override func applyStrokeProperties(to context: CGContext, atZoomScale zoomScale: MKZoomScale) {
super.applyStrokeProperties(to: context, atZoomScale: zoomScale)
UIGraphicsPushContext(context)
if let ctx = UIGraphicsGetCurrentContext() {
ctx.setLineWidth(self.lineWidth)
}
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
if self.polyline.pointCount < 2 { return }
let mapPoints = self.polyline.points()
let startPoint = mapPoints[0]
let endPoint = mapPoints[1]
let p = CGMutablePath()
if zoomScale >= 0.05 {
let midPoint = self.point(for: MKMapPoint(x: (startPoint.x + endPoint.x)/2, y: (startPoint.y + endPoint.y)/2))
let originX = endPoint.x - startPoint.x
let originY = endPoint.y - startPoint.y
let bearingR = atan2f(Float(originY), Float(originX))
let p1 = CGPoint(x: midPoint.x - 60 / (zoomScale + 0.5), y: midPoint.y + 80 / (zoomScale + 0.5))
let p2 = CGPoint(x: midPoint.x + 60 / (zoomScale + 0.5), y: midPoint.y)
let p3 = CGPoint(x: midPoint.x - 60 / (zoomScale + 0.5), y: midPoint.y - 80 / (zoomScale + 0.5))
let points = [p1, p2, p3]
let t1 = CGAffineTransform(translationX: midPoint.x, y: midPoint.y)
let t2 = t1.rotated(by: CGFloat(bearingR))
let t3 = t2.translatedBy(x: -midPoint.x, y: -midPoint.y)
p.addLines(between: points, transform: t3)
}
p.addLines(between: [self.point(for: startPoint), self.point(for: endPoint)])
p.closeSubpath()
context.setStrokeColor(UIColor.yellow.cgColor)
context.setLineWidth(5/zoomScale)
context.addPath(p)
context.strokePath()
}
}
I'm working on adding 4 CAEmitterLayers to a subview. Each CAEmitterLayer is a line, and will be positioned on each side. Rotation will be controlled via CGAffineTransform
My problem:
I can't get the .Left and .Right emitterPosition to properly line up with the left and right side of the view
Here's my progress:
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = GlobalConstants.smallCornerRadius
createParticles(direction: .Up)
createParticles(direction: .Down)
createParticles(direction: .Left)
createParticles(direction: .Right)
}
enum EmitTo {
case Up
case Down
case Left
case Right
}
func createParticles(direction: EmitTo) {
self.layoutSubviews()
let particleEmitter = CAEmitterLayer()
particleEmitter.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
particleEmitter.bounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
if direction == .Up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: 0))
} else if (direction == .Down) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi))
} else if (direction == .Left) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: .pi / 2))
} else if (direction == .Right) {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi / 2))
}
if direction == .Up || direction == .Down {
particleEmitter.emitterPosition = CGPoint(x: self.frame.width / 2, y: 0)
}
if direction == .Left {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: self.frame.height)
}
if direction == .Right {
particleEmitter.emitterPosition = CGPoint(x: self.frame.height / 2, y: -50)
}
particleEmitter.emitterSize = self.bounds.size
particleEmitter.emitterShape = CAEmitterLayerEmitterShape.line
particleEmitter.zPosition = 1
let triangle = makeEmitterCell(direction: direction)
particleEmitter.emitterCells = [triangle]
self.layer.addSublayer(particleEmitter)
}
func makeEmitterCell(direction: EmitTo) -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
What's happening:
My question
How can I get the .Left and .Right CAEmitterLayer to properly align with the left side and right side of the gray view?
Green Triangle Image:
Your approach to try to use CGAffineTransform is promising because a line emitter is a horizontal line that has only a width dimension. By rotating it, you can have it emit from the left or right.
However you need to use emitter bounds with the view height as width and the view width as height.
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
Accordingly, one sets the width of the emitterSize to the height of the view:
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
The emitter position must be adjusted in the same way.
Why is this so? Take a look at the following illustration with an emission from the top:
If you rotate the layer by 90 degrees you would only cover a small area in the middle. Also the emissions would not be visible, because they would be to much outside to the left.
The difference between having the emitter on the left or the right is just a call to setAffineTransform with either -.pi / 2 or .pi / 2.
The emitters from top and bottom of course should get the normal bounds, they only need to be rotated as you do in your question.
Hints
In your createParticles method you call self.layoutSubviews(). That should be avoided.
Apple docs explicitly state:
You should not call this method directly.
see https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews.
Instead you could override layoutSubviews and call your createParticles methods from there. Make sure to remove any previously created emitter layers by removing them.
One small thing: Swift switch cases start with a lowercase letter by convention.
Code
A complete example to help you get started might then look something like this:
import UIKit
class EmitterView: UIView {
private var emitters = [CAEmitterLayer]()
enum EmitTo {
case up
case down
case left
case right
}
override func layoutSubviews() {
super.layoutSubviews()
emitters.forEach { $0.removeFromSuperlayer() }
emitters.removeAll()
createParticles(direction: .up)
createParticles(direction: .down)
createParticles(direction: .left)
createParticles(direction: .right)
}
func createParticles(direction: EmitTo) {
let particleEmitter = CAEmitterLayer()
emitters.append(particleEmitter)
particleEmitter.position = CGPoint(x: bounds.midX, y: bounds.midY)
particleEmitter.emitterShape = .line
switch direction {
case .up, .down:
particleEmitter.bounds = self.bounds
particleEmitter.emitterPosition = CGPoint(x: bounds.size.width / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.width, height: 0)
if direction == .up {
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: -.pi))
}
case .left, .right:
particleEmitter.bounds = CGRect(x: 0, y: 0, width: bounds.size.height, height: bounds.size.width)
particleEmitter.emitterPosition = CGPoint(x: bounds.height / 2, y: 0)
particleEmitter.emitterSize = CGSize(width: bounds.size.height, height: 0)
let rotationAngle: CGFloat = direction == EmitTo.left ? -.pi / 2 : .pi / 2
particleEmitter.setAffineTransform(CGAffineTransform(rotationAngle: rotationAngle))
}
particleEmitter.emitterCells = [makeEmitterCell()]
layer.addSublayer( particleEmitter)
}
func makeEmitterCell() -> CAEmitterCell {
let cell = CAEmitterCell()
cell.birthRate = 25
cell.lifetime = 3
cell.lifetimeRange = 0
cell.velocity = 10
cell.velocityRange = 5
cell.emissionLongitude = .pi
cell.spin = 2
cell.spinRange = 3
cell.scale = 0.4
cell.scaleRange = 0.6
cell.scaleSpeed = -0.10
cell.contents = UIImage(named: "greenTriangle")?.cgImage
return cell
}
}
Test
I'm making a loading spinner animation that pushes a view out from the middle, and then rotates all the way around the center view back to it's original location. This is what I am trying to achieve:
The inner arrow moves the view away from the center. I've already achieved this, the part I am stuck on is then rotating the view around the center view. I've read various other StackOverflow posts but have not been close to achieving this.
Code so far:
UIView.animate(withDuration: 0.5) {
self.topView.transform = CGAffineTransform(translationX: 20, y: -20)
} completion: { _ in
self.topView.setAnchorPoint(self.centerView.center)
// Rotate
}
}
Here is how I am setting the anchor point of the view. I'm using this as the view disappears when setting its anchor point otherwise.
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
Once the full 360 rotation is complete I would then need to move the view back in towards the center, completing the animation.
For the part when the loading view rotates around the circle view, you can use a UIBezierPath and create a CAKeyframeAnimation based on its path.
Take a look at this implementation. Hope it helps.
class LoadingViewController: UIViewController {
var circlePath: UIBezierPath!
lazy var loader = makeLoader()
lazy var centerView = makeCenterView()
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func makeLoader() -> UIButton {
let padding: CGFloat = 100
let width = self.view.frame.width - (2 * padding)
let b = UIButton(frame: CGRect(x: padding, y: 200, width: width, height: 50))
b.addTarget(self, action: #selector(didTap), for: .touchUpInside)
b.backgroundColor = .blue
return b
}
func makeCenterView() -> UIView {
let width: CGFloat = 20
let height: CGFloat = 20
let x = self.view.center.x - width/2
let y = self.view.center.y - height/2
let view = UIView(frame: CGRect(x: x, y: y, width: width, height: height))
view.backgroundColor = .green
return view
}
func setup() {
//create a UIBezierPath with a center that is at the center of the green view and a radius that has a length as the distance between the green view and the blue view.
let arcCenterX = centerView.center.x
let arcCenterY = centerView.center.y
let arcCenter = CGPoint(x: arcCenterX, y: arcCenterY)
let radius = arcCenterY - loader.center.y
let startAngle = -CGFloat.pi/2
let endAngle = CGFloat.pi*(1.5)
let arcPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
self.circlePath = arcPath
self.view.addSubview(loader)
self.view.addSubview(centerView)
}
#objc func didTap() {
//create a CAKeyframeAnimation with a path that matches the UIBezierPath above.
let loadAnimation = CAKeyframeAnimation(keyPath: "position")
loadAnimation.path = self.circlePath.cgPath
loadAnimation.calculationMode = .paced
loadAnimation.duration = 2.0
loadAnimation.rotationMode = .rotateAuto
loadAnimation.repeatCount = Float(CGFloat.greatestFiniteMagnitude)
loader.layer.add(loadAnimation, forKey: "circleAnimation")
}
}
Hello all, I tried to add arc for UIBezierPath I could not able to get the exact curve,
here is my code here I have added the bezier path for the added curve from the center position.
#IBDesignable
class MyTabBar: 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
//The below 4 lines are for shadow above the bar. you can skip them if you do not want a shadow
shapeLayer.shadowOffset = CGSize(width:0, height:0)
shapeLayer.shadowRadius = 10
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.3
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
path.addCurve(to: CGPoint(x: centerWidth, y: height),
controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
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 hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
for member in subviews.reversed() {
let subPoint = member.convert(point, from: self)
guard let result = member.hitTest(subPoint, with: event) else { continue }
return result
}
return nil
}
}
this is tab bar controller added plus button in center view center, and the when tap the plus button to add the curve based popup should show, I don't know how to add curve based popup.
class TabbarViewController: UITabBarController,UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.navigationController?.navigationBar.isHidden = true
setupMiddleButton()
}
// TabBarButton – Setup Middle Button
func setupMiddleButton() {
let middleBtn = UIButton(frame: CGRect(x: (self.view.bounds.width / 2)-25, y: -20, width: 50, height: 50))
middleBtn.setImage(UIImage(named: "PlusBtn"), for: .normal)
self.tabBar.addSubview(middleBtn)
middleBtn.addTarget(self, action: #selector(self.menuButtonAction), for: .touchUpInside)
self.view.layoutIfNeeded()
}
// Menu Button Touch Action
#objc func menuButtonAction(sender: UIButton) {
//show the popUp
}
}
Please share me the findings & share me your refreance
Thanks
New edit:
I created a general-purpose method that will generate polygons with a mixture of sharp and rounded corners of different radii. I used it to create a project with a look rather like what you are after. You can download it from Github:
https://github.com/DuncanMC/CustomTabBarController.git
Here's what it looks like:
Note that the area of the tab's view controller that extends into the tab bar controller does not get taps. If you try to tap there, it triggers the tab bar controller. You will have to do some more tinkering to get that part to work.
Ultimately you may have to create a custom UITabBar (or UITabBar-like component) and possibly a custom parent view controller that acts like a UITabBar, in order to get what you want.
The method that creates polygon paths is called buildPolygonPathFrom(points:defaultCornerRadius:)
It takes an array of PolygonPoint structs. Those are defined like this:
/// A struct describing a single vertex in a polygon. Used in building polygon paths with a mixture of rounded an sharp-edged vertexes.
public struct PolygonPoint {
let point: CGPoint
let isRounded: Bool
let customCornerRadius: CGFloat?
init(point: CGPoint, isRounded: Bool, customCornerRadius: CGFloat? = nil) {
self.point = point
self.isRounded = isRounded
self.customCornerRadius = customCornerRadius
}
init(previousPoint: PolygonPoint, isRounded: Bool) {
self.init(point: previousPoint.point, isRounded: isRounded, customCornerRadius: previousPoint.customCornerRadius)
}
}
The code to build the path for the custom tab bar looks like this:
func tabBarMaskPath() -> CGPath? {
let width = bounds.width
let height = bounds.height
guard width > 0 && height > 0 else { return nil }
let dentRadius: CGFloat = 35
let cornerRadius: CGFloat = 20
let topFlatPartWidth = (width - dentRadius * 2.0) / 2
let polygonPoints = [
PolygonPoint(point: CGPoint(x: 0, y: 0), // Point 0
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: 0, y: height), // Point 1
isRounded: false),
PolygonPoint(point: CGPoint(x: width, y: height), // Point 2
isRounded: false),
PolygonPoint(point: CGPoint(x: width, y: 0), // Point 3
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth + dentRadius * 2, y: 0), // Point 4
isRounded: true,
customCornerRadius: cornerRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth + dentRadius * 2, y: dentRadius + cornerRadius), // Point 5
isRounded: true,
customCornerRadius: dentRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth , y: dentRadius + cornerRadius), // Point 6
isRounded: true,
customCornerRadius: dentRadius),
PolygonPoint(point: CGPoint(x: topFlatPartWidth , y: 0), // Point 7
isRounded: true,
customCornerRadius: cornerRadius),
]
return buildPolygonPathFrom(points: polygonPoints, defaultCornerRadius: 0)
}
Previous answer:
I just tried it, and it is possible to subclass UITabBar. I created a subclass of UITabBar where I use a mask layer to cut a circular "notch" out of the top of the tab bar. The code is below. It looks like the screenshot below. It isn't quite what you're after, but it's a start:
(The background color for the "Page 1" view controller is set to light gray, and you can see that color showing through in the "notch" I cut out of the tab bar.)
//
// CustomTabBar.swift
// TabBarController
//
// Created by Duncan Champney on 3/31/21.
//
import UIKit
class CustomTabBar: UITabBar {
var maskLayer = CAShapeLayer()
override var frame: CGRect {
didSet {
configureMaskLayer()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configureMaskLayer()
self.layer.mask = maskLayer
self.layer.borderWidth = 0
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configureMaskLayer()
self.layer.mask = maskLayer
}
func configureMaskLayer() {
let rect = layer.bounds
maskLayer.frame = rect
let circleBoxSize = rect.size.height * 1.25
maskLayer.fillRule = .evenOdd
let path = UIBezierPath(rect: rect)
let circleRect = CGRect(x: rect.size.width/2 - circleBoxSize / 2,
y: -circleBoxSize/2,
width: circleBoxSize,
height: circleBoxSize)
let circle = UIBezierPath.init(ovalIn: circleRect)
path.append(circle)
maskLayer.path = path.cgPath
maskLayer.fillColor = UIColor.white.cgColor // Any opaque color works and has no effect
}
}
Edit:
To draw your popover view you'll need to create a filled path that shape. You'll have to construct a custom shape like that with a combination of lines and arcs. I suggest using a CGMutablePath and the method addArc(tangent1End:tangent2End:radius:transform:) since that enables you to provide endpoints rather than angles.
Edit #2:
Another part of the puzzle:
Here is a custom UIView subclass that masks itself in the shape you're after
//
// ShapeWithTabView.swift
// ShapeWithTab
//
// Created by Duncan Champney on 4/1/21.
//
import UIKit
class ShapeWithTabView: UIView {
var cornerRadius: CGFloat = 20
var tabRadius: CGFloat = 60
var tabExtent: CGFloat = 0
var shapeLayer = CAShapeLayer()
var maskLayer = CAShapeLayer()
func buildShapeLayerPath() -> CGPath {
let boxWidth = min(bounds.size.width - 40, 686)
let boxHeight = min(bounds.size.height - 40 - tabRadius * 2 - tabExtent, 832)
// These are the corners of the view's primary rectangle
let point1 = CGPoint(x: 0, y: boxHeight)
let point2 = CGPoint(x: 0, y: 0)
let point3 = CGPoint(x: boxWidth, y: 0)
let point4 = CGPoint(x: boxWidth, y: boxHeight)
// These are the corners of the "tab" that extends outside the view's normal bounds.
let tabPoint1 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight)
let tabPoint2 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight + tabExtent + tabRadius * 2 )
let tabPoint3 = CGPoint(x: boxWidth / 2 - tabRadius, y: boxHeight + tabExtent + tabRadius * 2)
let tabPoint4 = CGPoint(x: boxWidth / 2 - tabRadius , y: boxHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: boxHeight - cornerRadius))
path.addArc(tangent1End: point2,
tangent2End: point3,
radius: cornerRadius)
path.addArc(tangent1End: point3,
tangent2End: point4,
radius: cornerRadius)
path.addArc(tangent1End: point4,
tangent2End: point1,
radius: cornerRadius)
//
path.addArc(tangent1End: tabPoint1,
tangent2End: tabPoint2,
radius: tabRadius)
path.addArc(tangent1End: tabPoint2,
tangent2End: tabPoint3,
radius: tabRadius)
path.addArc(tangent1End: tabPoint3,
tangent2End: tabPoint4,
radius: tabRadius)
path.addArc(tangent1End: tabPoint4,
tangent2End: point1,
radius: tabRadius)
path.addArc(tangent1End: point1,
tangent2End: point2,
radius: cornerRadius)
return path
}
func doInitSetup() {
self.layer.addSublayer(shapeLayer)
self.layer.mask = maskLayer
backgroundColor = .lightGray
//Configure a shape layer to draw an outline
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 2
//Configure a mask layer to mask the view to our custom shape
maskLayer.fillColor = UIColor.white.cgColor
maskLayer.strokeColor = UIColor.white.cgColor
maskLayer.lineWidth = 2
}
override init(frame: CGRect) {
super.init(frame: frame)
self.doInitSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.doInitSetup()
}
public func updateShapeLayerPath() {
let path = buildShapeLayerPath()
shapeLayer.path = path
maskLayer.path = path
}
override var frame: CGRect {
didSet {
print("New frame = \(frame)")
shapeLayer.frame = layer.bounds
}
}
}
Combined with the modified tab bar from above, it looks like the image below. The final task is to get the custom view sized and positioned correctly, and have it land on top of the tab bar.
I'm trying to make an app based on image editing and I'm founding some problems coding a resizing by corners dragging crop area.
I have croppingView.swift where the crop rectangle is drawn when launched:
override func viewDidAppear(_ animated: Bool) {
let imagePoint = CGPoint(x: contentView.center.x, y: contentView.center.y)
let imageSize = CGSize(width: cropImageView.image!.size.width, height: cropImageView.image!.size.height)
let widthScale = cropImageView.frame.width / imageSize.width
let heightScale = cropImageView.frame.height / imageSize.height
let scale = min(widthScale, heightScale)
let width = imageSize.width * scale
let height = imageSize.height * scale
let size = CGSize(width: width, height: height)
let originViewFrame = CGPoint(x: imagePoint.x - width/2.0, y: imagePoint.y - height/2.0)
let cropSize = CGSize(width: width * 0.7, height: height * 0.7)
let cropViewFrame = CGRect(origin: originViewFrame, size: cropSize)
cropView = ShapeView(frame: cropViewFrame)
cropView.backgroundColor = UIColor(red: 224.0/255.0, green: 224.0/255.0, blue: 224.0/255.0, alpha: 0.3)
cropView.layer.borderColor = UIColor(red: 97.0/255.0, green: 97.0/255.0, blue: 97.0/255.0, alpha: 1.0).cgColor
cropView.layer.borderWidth = 1.0
cropView.center = imagePoint
self.view.addSubview(cropView)
}
The ShapeView.swift to draw the cropping area is as follows:
class ShapeView: UIView {
var previousLocation = CGPoint.zero
var topLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var topRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomLeft = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
var bottomRight = DragHandle(fillColor:UIColor(red: 0.0/255.0, green: 150.0/255.0, blue: 136.0/255.0, alpha: 1.0),
strokeColor: UIColor.white)
override func didMoveToSuperview() {
superview?.addSubview(topLeft)
superview?.addSubview(topRight)
superview?.addSubview(bottomLeft)
superview?.addSubview(bottomRight)
var pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
topRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomLeft.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
bottomRight.addGestureRecognizer(pan)
pan = UIPanGestureRecognizer(target: self, action: #selector(handleMove))
self.addGestureRecognizer(pan)
self.updateDragHandles()
}
func updateDragHandles() {
topLeft.center = self.transformedTopLeft()
topRight.center = self.transformedTopRight()
bottomLeft.center = self.transformedBottomLeft()
bottomRight.center = self.transformedBottomRight()
}
//Gesture Methods
#IBAction func handleMove(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self.superview!)
var center = self.center
center.x += translation.x
center.y += translation.y
self.center = center
gesture.setTranslation(CGPoint.zero, in: self.superview!)
updateDragHandles()
}
#IBAction func handlePan(gesture:UIPanGestureRecognizer) {
let translation = gesture.translation(in: self)
switch gesture.view! {
case topLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 1))
}
self.bounds.size.width -= translation.x
self.bounds.size.height -= translation.y
case topRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0, y: 1))
}
self.bounds.size.width += translation.x
self.bounds.size.height -= translation.y
case bottomLeft:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 0))
}
self.bounds.size.width -= translation.x
self.bounds.size.height += translation.y
case bottomRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint.zero)
}
self.bounds.size.width += translation.x
self.bounds.size.height += translation.y
default:()
}
gesture.setTranslation(CGPoint.zero, in: self)
updateDragHandles()
if gesture.state == .ended {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
}
}
}
As you can see I'm adding one circle in each corner of the rectangle. These circles are used to resize the cropping area by dragging. Here the key is the updateDragHandles()function which updates the positions of all other circle keeping them on the corners.
So when launching the cropping area appears like:
And when user moves for example the bottom-right corner by dragging everything is working fine:
Now the problem is when I click on the portrait button to change cropping area orientation. With the next code into croppingView.swift the grey rectangle correctly changes the size but the circles remain on the their position:
#IBAction func portButton(_ sender: Any) {
portButton.tintColor = UIColor.systemBlue
landButton.tintColor = UIColor.systemGray
isPortait = 1
let inWidth = cropView.frame.size.width
let inHeight = cropView.frame.size.height
if inWidth > inHeight {
let scaleW = cropView.frame.size.height / cropView.frame.size.width
let scaleH = cropView.frame.size.width / cropView.frame.size.height
let fixPoint = CGPoint(x: 0.5, y: 0.5)
cropView.setAnchorPoint(anchorPoint: fixPoint)
cropView.transform = cropView.transform.scaledBy(x: scaleW, y: scaleH)
let vc = ShapeView()
vc.updateDragHandles()
}
}
It's like updateDragHandles is not working from this ViewController...
Probably it's a basic mistake but I can't find out the solution.
Any suggestion?
Thank you in advance!
DragHandle.swift
let diameter:CGFloat = 30
import UIKit
class DragHandle: UIView {
var fillColor = UIColor.darkGray
var strokeColor = UIColor.lightGray
var strokeWidth: CGFloat = 1.0
required init(coder aDecoder: NSCoder) {
fatalError("Use init(fillColor:, strokeColor:)")
}
init(fillColor: UIColor, strokeColor: UIColor, strokeWidth width: CGFloat = 1.0) {
super.init(frame: CGRect(x: 0, y: 0, width: diameter, height: diameter))
self.fillColor = fillColor
self.strokeColor = strokeColor
self.strokeWidth = width
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
super.draw(rect)
let handlePath = UIBezierPath(ovalIn: rect.insetBy(dx: 10 + strokeWidth, dy: 10 + strokeWidth))
fillColor.setFill()
handlePath.fill()
strokeColor.setStroke()
handlePath.lineWidth = strokeWidth
handlePath.stroke()
}
}
UIViewExtensions.swift
import Foundation
import UIKit
extension UIView {
func offsetPointToParentCoordinates(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x + self.center.x, y: point.y + self.center.y)
}
func pointInViewCenterTerms(point:CGPoint) -> CGPoint {
return CGPoint(x: point.x - self.center.x, y: point.y - self.center.y)
}
func pointInTransformedView(point: CGPoint) -> CGPoint {
let offsetItem = self.pointInViewCenterTerms(point: point)
let updatedItem = offsetItem.applying(self.transform)
let finalItem = self.offsetPointToParentCoordinates(point: updatedItem)
return finalItem
}
func originalFrame() -> CGRect {
let currentTransform = self.transform
self.transform = .identity
let originalFrame = self.frame
self.transform = currentTransform
return originalFrame
}
func transformedTopLeft() -> CGPoint {
let frame = self.originalFrame()
let point = frame.origin
return self.pointInTransformedView(point: point)
}
func transformedTopRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
return self.pointInTransformedView(point: point)
}
func transformedBottomRight() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.x += frame.size.width
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func transformedBottomLeft() -> CGPoint {
let frame = self.originalFrame()
var point = frame.origin
point.y += frame.size.height
return self.pointInTransformedView(point: point)
}
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
self.layer.position = position
self.layer.anchorPoint = anchorPoint
}
}