How to animate/fill an UIBezierPath in a given order? - swift

From a file I have a bunch of SVG paths that I convert to UIBezierPath. In order to make my example simple I create the path manually:
struct myShape: Shape {
func path(in rect: CGRect) -> Path {
let p = UIBezierPath()
p.move(to: CGPoint(x: 147, y: 32))
p.addQuadCurve(to: CGPoint(x: 203, y: 102), controlPoint: CGPoint(x: 181, y: 74))
p.addQuadCurve(to: CGPoint(x: 271, y: 189), controlPoint: CGPoint(x: 242, y: 166))
p.addQuadCurve(to: CGPoint(x: 274, y: 217), controlPoint: CGPoint(x: 287, y: 204))
p.addQuadCurve(to: CGPoint(x: 229, y: 235), controlPoint: CGPoint(x: 258, y: 229))
p.addQuadCurve(to: CGPoint(x: 193, y: 235), controlPoint: CGPoint(x: 204, y: 241))
p.addQuadCurve(to: CGPoint(x: 190, y: 219), controlPoint: CGPoint(x: 183, y: 231))
p.addQuadCurve(to: CGPoint(x: 143, y: 71), controlPoint: CGPoint(x: 199, y: 195))
p.addQuadCurve(to: CGPoint(x: 125, y: 33), controlPoint: CGPoint(x: 134, y: 55))
p.addCurve(to: CGPoint(x: 147, y: 32), controlPoint1: CGPoint(x: 113, y: 5), controlPoint2: CGPoint(x: 128, y: 9))
p.close()
return Path(p.cgPath)
}
}
The result is the following figure:
For this figure I have a separated path/shape that represents the "median" of the figure and the order in which this figure should be filled. The path is just a concatenation of lines.
struct myMedian: Shape {
func path(in rect: CGRect) -> Path {
let p = UIBezierPath()
p.move(to: CGPoint(x: 196, y: 226))
p.addLine(to: CGPoint(x: 209, y: 220))
p.addLine(to: CGPoint(x: 226, y: 195))
p.addLine(to: CGPoint(x: 170, y: 86))
p.addLine(to: CGPoint(x: 142, y: 43))
p.addLine(to: CGPoint(x: 131, y: 39))
return Path(p.cgPath)
}
}
To visualize the order of the lines I've added the red arrows:
Now I need to fill the "big figure" in the same order as the "median stroke". I know how to fill the whole figure in one step, but not splitwise and especially I don't know how to manage the direction of the animation.
The final result should look like this:
Since I'm using SwiftUI it should be compatible with it.
The main view is:
struct DrawCharacter: View {
var body: some View {
ZStack(alignment: .topLeading){
myShape()
myMedian()
}
}
}

You can define an animatableData property in your Shape to enable SwiftUI to interpolate between states (e.g., filled and unfilled). Where you start drawing each stroke will matter for direction. You could also use .trim on the path to truncate it if you prefer and tie that value into animatableData.
For a whole character/kanji, you may need to compose a meta Shape or View of multiple sub-Shapes with defined positions, but that's actually less work for you in the long run because you can create a library of strokes that are easy to recombine, no?
In the example below, I move a moon from full to crescent by changing the percentFullMoon property from other views. That property is used by the path drawing function to setup some arcs. SwiftUI reads the animatableData property to figure out how to draw however many frames it chooses to interpolate at the time of animation.
It's rather simple, even if this API is obscurely named.
Does that make sense? Note: the code below uses some helper functions like clamp, north/south, and center/radius, but are irrelevant to the concept.
import SwiftUI
/// Percent full moon is from 0 to 1
struct WaningMoon: Shape {
/// From 0 to 1
var percentFullMoon: Double
var animatableData: Double {
get { percentFullMoon }
set { self.percentFullMoon = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
addExteriorArc(&path, rect)
let cycle = percentFullMoon * 180
switch cycle {
case 90:
return path
case ..<90:
let crescent = Angle(degrees: 90 + .clamp(0.nextUp, 90, 90 - cycle))
addInteriorArc(&path, rect, angle: crescent)
return path
case 90.nextUp...:
let gibbous = Angle(degrees: .clamp(0, 90.nextDown, 180 - cycle))
addInteriorArc(&path, rect, angle: gibbous)
return path
default: return path
}
}
private func addInteriorArc(_ path: inout Path, _ rect: CGRect, angle: Angle) {
let xOffset = rect.radius * angle.tan()
let offsetCenter = CGPoint(x: rect.midX - xOffset, y: rect.midY)
return path.addArc(
center: offsetCenter,
radius: rect.radius / angle.cos(),
startAngle: .south() - angle,
endAngle: .north() + angle,
clockwise: angle.degrees < 90) // False == Crescent, True == Gibbous
}
private func addExteriorArc(_ path: inout Path, _ rect: CGRect) {
path.addArc(center: rect.center,
radius: rect.radius,
startAngle: .north(),
endAngle: .south(),
clockwise: true)
}
}
Helpers
extension Comparable {
static func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N {
Swift.max(min, Swift.min(variable, max))
}
func clamp<N:Comparable>(_ min: N, _ max: N, _ variable: N) -> N {
Swift.max(min, Swift.min(variable, max))
}
}
extension Angle {
func sin() -> CGFloat {
CoreGraphics.sin(CGFloat(self.radians))
}
func cos() -> CGFloat {
CoreGraphics.cos(CGFloat(self.radians))
}
func tan() -> CGFloat {
CoreGraphics.tan(CGFloat(self.radians))
}
static func north() -> Angle {
Angle(degrees: -90)
}
static func south() -> Angle {
Angle(degrees: 90)
}
}
extension CGRect {
var center: CGPoint {
CGPoint(x: midX, y: midY)
}
var radius: CGFloat {
min(width, height) / 2
}
var diameter: CGFloat {
min(width, height)
}
var N: CGPoint { CGPoint(x: midX, y: minY) }
var E: CGPoint { CGPoint(x: minX, y: midY) }
var W: CGPoint { CGPoint(x: maxX, y: midY) }
var S: CGPoint { CGPoint(x: midX, y: maxY) }
var NE: CGPoint { CGPoint(x: maxX, y: minY) }
var NW: CGPoint { CGPoint(x: minX, y: minY) }
var SE: CGPoint { CGPoint(x: maxX, y: maxY) }
var SW: CGPoint { CGPoint(x: minX, y: maxY) }
func insetN(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: midX, y: minY + height / denominator)
}
func insetE(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: minX - width / denominator, y: midY)
}
func insetW(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: maxX + width / denominator, y: midY)
}
func insetS(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: midX, y: maxY - height / denominator)
}
func insetNE(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: maxX + width / denominator, y: minY + height / denominator)
}
func insetNW(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: minX - width / denominator, y: minY + height / denominator)
}
func insetSE(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: maxX + width / denominator, y: maxY - height / denominator)
}
func insetSW(_ denominator: CGFloat) -> CGPoint {
CGPoint(x: minX - width / denominator, y: maxY - height / denominator)
}
}

Related

Swift UIView How to add more drawing after calling draw function ? Using UIBezierPath

This example I am drawing a line and I want to add an arrow on the tip of it. The arrow function works fine when I use it in draw function but it is not being added to the view when I call it later on. Is there some way for me to add it later on ?
**My BezierView Class
**
final class BezierView: UIView {
var currentPoint: CGPoint = CGPoint()
override func draw(_ rect: CGRect) {
let line1 = UIBezierPath()
line1.move(to: CGPoint(x: 20, y: 65))
line1.addLine(to: CGPoint(x: 20, y: 15))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 5))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 65))
line1.lineWidth = 2
UIColor.darkGray.setStroke()
line1.stroke()
currentPoint = line1.currentPoint
}
func drawArrow() {
let point = currentPoint
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
**My View Controller
**
override func viewDidLoad() {
super.viewDidLoad()
let bezierView = BezierView(frame: CGRect(x: 0, y: 40, width: self.view.frame.width, height: 200))
bezierView.drawArrow()
}
I have tried to use: self.setNeedsDisplay(), self.setNeedsLayout(), self.layoutSubviews() functions in the drawArrow function but they didn't change the outcomes.
The reason why filling and stroking a bezier path draws things on the screen is because the draw(_:) method is called (by iOS) in a graphics context created by the system that allows you to draw on the screen. After draw(_:) returns, the graphics context ends, and you can no longer draw things on the screen.
Therefore, calling drawArrow in viewDidLoad does not work. You are not in that specific graphics context. Unfortunately, you cannot get this graphics context from outside of draw(_:) and draw in it.
In other words, all the drawing has to be done in draw(_:). If you just want to draw one arrow, you can just keep a boolean to store whether the arrow has been drawn or not:
final class BezierView: UIView {
var isArrowDrawn = false {
didSet {
// this causes draw(_:) to be called again
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let line1 = UIBezierPath()
line1.move(to: CGPoint(x: 20, y: 65))
line1.addLine(to: CGPoint(x: 20, y: 15))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 5))
line1.addLine(to: CGPoint(x: self.frame.width - 20, y: 65))
line1.lineWidth = 2
UIColor.darkGray.setStroke()
line1.stroke()
if isArrowDrawn {
let point = line1.currentPoint
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
func drawArrow() {
isArrowDrawn = true
}
}
If you want multiple arrows, to be drawn whenever drawArrow is called, then you would need to keep track of the list of points at which the arrows all are located. Something like
final class BezierView: UIView {
var currentPoint: CGPoint = CGPoint()
var arrowPoints = [CGPoint]() {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
// ... omitted for brevity
currentPoint = line1.currentPoint
for point in arrowPoints {
let arrowTip = UIBezierPath()
arrowTip.move(to: CGPoint(x: point.x, y: point.y))
arrowTip.addLine(to: CGPoint(x: point.x - 6, y: point.y - 6))
arrowTip.addLine(to: CGPoint(x: point.x + 6, y: point.y - 6))
arrowTip.close()
UIColor.darkGray.setFill()
arrowTip.fill()
}
}
func drawArrow() {
arrowPoints.append(currentPoint)
}
}
Basically, you need to keep track of all the "states" that your view can be in, and your draw(_:) method is responsible for drawing the current state.
If you find this quite annoying, consider switching to using layers instead. You can, at any time, create a CAShapeLayer with a path, and add it as a sublayer as self.layer. Here is a tutorial to get started with.

iOS: Chat bubble change position of tail

I'm trying to achive this layout
[LAYOUT TO ACHIEVE]
and right now I have gradient part done, blur part done, and also bezier path mostly done. My question is that I can't replace TAIL from bottom right part to top left part (to be similar like in the picture).
[LAYOUT ACHIEVED]
My Code I wrote for acheiving:
public final class BubbleView: UIView {
public var configuration: Configuration = Configuration.defaultConfiguration {
didSet {
setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
super.backgroundColor = .clear
}
public override func draw(_ rect: CGRect) {
let hasBlurSubview = subviews.compactMap({$0}).contains(where: {$0 is UIVisualEffectView })
guard !hasBlurSubview else {
return
}
let lineWidth = configuration.lineWidth
let bezierPath = UIBezierPath()
bezierPath.lineWidth = lineWidth
let bottom = rect.height - lineWidth
let right = rect.width - lineWidth
let top = 0.0
let left = 0.0
bezierPath.move(to: CGPoint(x: right - 22, y: bottom))
bezierPath.addLine(to: CGPoint(x: 17 + lineWidth, y: bottom))
bezierPath.addCurve(to: CGPoint(x: left, y: bottom - 18), controlPoint1: CGPoint(x: 7.61 + lineWidth, y: bottom), controlPoint2: CGPoint(x: left, y: bottom - 7.61))
bezierPath.addLine(to: CGPoint(x: left, y: 17 + lineWidth))
bezierPath.addCurve(to: CGPoint(x: 17 + borderWidth, y: top), controlPoint1: CGPoint(x: left, y: 7.61 + lineWidth), controlPoint2: CGPoint(x: 7.61 + lineWidth, y: top))
bezierPath.addLine(to: CGPoint(x: right - 21, y: top))
bezierPath.addCurve(to: CGPoint(x: right - 4, y: 17 + lineWidth), controlPoint1: CGPoint(x: right - 11.61, y: top), controlPoint2: CGPoint(x: right - 4, y: 7.61 + lineWidth))
bezierPath.addLine(to: CGPoint(x: right - 4, y: bottom - 11))
bezierPath.addCurve(to: CGPoint(x: right, y: bottom), controlPoint1: CGPoint(x: right - 4, y: bottom - 1), controlPoint2: CGPoint(x: right, y: bottom))
bezierPath.addLine(to: CGPoint(x: right + 0.05, y: bottom - 0.01))
bezierPath.addCurve(to: CGPoint(x: right - 11.04, y: bottom - 4.04), controlPoint1: CGPoint(x: right - 4.07, y: bottom + 0.43), controlPoint2: CGPoint(x: right - 8.16, y: bottom - 1.06))
bezierPath.addCurve(to: CGPoint(x: right - 22, y: bottom), controlPoint1: CGPoint(x: right - 16, y: bottom), controlPoint2: CGPoint(x: right - 19, y: bottom))
bezierPath.close()
let blurEffect = UIVisualEffectView(effect: UIBlurEffect(style: .light))
blurEffect.alpha = configuration.blurAlpha
blurEffect.frame = bezierPath.bounds
let mask = CAShapeLayer()
mask.path = bezierPath.cgPath
blurEffect.layer.mask = mask
insertSubview(blurEffect, at: 0)
let gradient = CAGradientLayer()
gradient.frame = CGRect(origin: CGPoint.zero, size: size)
gradient.colors = configuration.borderGradientColors.map({$0.cgColor})
gradient.startPoint = configuration.borderGradientStartPoint
gradient.endPoint = configuration.borderGradientEndPoint
let shape = CAShapeLayer()
shape.lineWidth = lineWidth
shape.path = bezierPath.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradient.mask = shape
blurEffect.layer.addSublayer(gradient)
}
}
public class Configuration {
static var defaultConfiguration: Configuration {
return .init(
lineWidth: 2.0,
blurAlpha: 0.95,
borderGradientColors: [Assets.Colors.white05.color, Assets.Colors.white01.color],
borderGradientStartPoint: CGPoint(x: 0.0, y: 0.5),
borderGradientEndPoint: CGPoint(x: 1.0, y: 0.5)
)
}
var lineWidth: CGFloat
var blurAlpha: CGFloat
var borderGradientColors: [UIColor]
var borderGradientStartPoint: CGPoint
var borderGradientEndPoint: CGPoint
init(
lineWidth: CGFloat,
blurAlpha: CGFloat,
borderGradientColors: [UIColor],
borderGradientStartPoint: CGPoint,
borderGradientEndPoint: CGPoint
) {
self.lineWidth = lineWidth
self.blurAlpha = blurAlpha
self.borderGradientColors = borderGradientColors
self.borderGradientStartPoint = borderGradientStartPoint
self.borderGradientEndPoint = borderGradientEndPoint
}
}

Add filled circles (markers) at SwiftUI path points

I'm trying to draw a line with markers at each point by using a Shape view in SwiftUI. I want the line to have a filled circle at each CGPoint on the line. The closest to this that I've gotten is by adding an arc at each point. Instead of using the arc, how can I add a Circle() shape at each point? I'm open to other approaches to accomplish this. My only requirements are to use SwiftUI and have the ability to interact with the markers.
import SwiftUI
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
path.addArc(center: pt, radius: 8, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.padding()
.frame(width: 400, height: 300)
}
}
You can make two different shapes, one for the line and one for the markers, and overlay them. Then you can also control their coloring individually:
struct LineShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
path.move(to: CGPoint(x: 0.0, y: (1 - values[0]) * Double(rect.height)))
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addLine(to: pt)
}
return path
}
}
struct MarkersShape: Shape {
let values: [Double]
func path(in rect: CGRect) -> Path {
let xStep = rect.width / CGFloat(values.count - 1)
var path = Path()
for i in 1..<values.count {
let pt = CGPoint(x: Double(i) * Double(xStep), y: (1 - values[i]) * Double(rect.height))
path.addEllipse(in: CGRect(x: pt.x - 8, y: pt.y - 8, width: 16, height: 16))
}
return path
}
}
struct ContentView: View {
var body: some View {
LineShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.stroke(.red, lineWidth: 2.0)
.overlay(
MarkersShape(values: [0.2, 0.4, 0.3, 0.8, 0.5])
.fill(.blue)
)
.frame(width: 350, height: 300)
}
}
According to previous posts and comments here is my solution:
First there is need to use extension for filling and setting shape stroke:
extension Shape {
public func fill<Shape: ShapeStyle>(_ fillContent: Shape, strokeColor: Color, lineWidth: CGFloat) -> some View {
ZStack {
self.fill(fillContent)
self.stroke(strokeColor, lineWidth: lineWidth)
}
}
}
Another thing is Scaler struct used to make scaling calculations:
struct Scaler {
let bounds: CGPoint
let maxVal: Double
let minVal: Double
let valuesCount: Int
var xFactor: CGFloat {
valuesCount < 2 ? bounds.x : bounds.x / CGFloat(valuesCount - 1)
}
var yFactor: CGFloat {
bounds.y / maxVal
}
}
Now it's time for real chart shape drawing:
struct ChartLineDotsShape: Shape {
let values: [Double]
let dotRadius: CGFloat
func path(in rect: CGRect) -> Path {
guard let maxVal = values.max(), let minVal = values.min() else { return Path() }
let scaler = Scaler(bounds: CGPoint(x: rect.width, y: rect.height), maxVal: maxVal, minVal: minVal, valuesCount: values.count)
return Path { path in
var valuesIterator = values.makeIterator()
let dx = scaler.xFactor
var x = 0.0
guard let y = valuesIterator.next() else { return }
path.move(to: CGPoint(x: x, y: calculate(y, scaler: scaler)))
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
while let y = valuesIterator.next() {
draw(point: CGPoint(x: x, y: calculate(y, scaler: scaler)), on: &path)
x += dx
}
}
}
private func calculate(_ value: CGFloat, scaler: Scaler) -> CGFloat {
scaler.bounds.y - value * scaler.yFactor
}
private func draw(point: CGPoint, on path: inout Path) {
path.addLine(to: CGPoint(x: point.x, y: point.y))
path.addEllipse(in: CGRect(x: point.x - dotRadius * 0.5, y: point.y - dotRadius * 0.5, width: dotRadius, height: dotRadius))
path.move(to: CGPoint(x: point.x, y: point.y))
}
}
If there is single iteration loop lines dots drawing shape we can try to use it in some view:
public struct ChartView: View {
public var body: some View {
ChartLineDotsShape(values: [0.3, 0.5, 1.0, 2.0, 2.5, 2.0, 3.0, 6.0, 2.0, 1.5, 2.0], dotRadius: 4.0)
.fill(Color.blue, strokeColor: Color.blue, lineWidth: 1.0)
.shadow(color: Color.blue, radius: 2.0, x: 0.0, y: 0.0)
.frame(width: 320.0, height: 200.0)
.background(Color.blue.opacity(0.1))
.clipped()
}
}
Finally preview setup:
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView()
.preferredColorScheme(.dark)
}
}
and viola:

Draw Shape over another Shape

Problem :
I cannot draw a shape on another shape.
What I am trying to achieve :
Draw circles on the line.
Anyway, the circle is shifting the line. I didn't find a way to make it as swift UI seems relatively new. I am currently learning swift and I prefer swift UI rater than storyboard.
If circle and line are different struct, this is because I want to reuse the shape later on.
There is the code :
import SwiftUI
public var PointArray = [CGPoint]()
public var PointArrayInit:Bool = false
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
PointArray.append(Point)
path.addLine(to: Point)
xStepLoopIncrement += xStep
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
Arc(startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true, centerCustom: CGPoint(x: 50, y: 400))
.stroke(Color.blue, lineWidth: 4)
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
} else {
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
There is what I get :
Here is an other way for you, you can limit the size of drawing with giving a frame or you can use the available size of view without limiting it or even you can use the current limit coming from parent and updated it, like i did on drawing the Line. The method that I used was overlay modifier.
struct ContentView: View {
var body: some View {
ArcView(radius: 30.0)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.frame(width: 60, height: 60)
.overlay(LineView().stroke(lineWidth: 10).foregroundColor(.red).frame(width: 400))
}
}
struct ArcView: Shape {
let radius: CGFloat
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
}
}
}
struct LineView: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLines([CGPoint(x: rect.minX, y: rect.midY), CGPoint(x: rect.maxX, y: rect.midY)])
}
}
}
result:

Using Drawn Shapes and Changing Their Features

After spending a whole day trying to work out how to do it I have finally worked out a way of having non-rectangular buttons.
All three buttons at the bottom are on one button. The hitTest then works out which UIBezierPath the click was within then calls a function.
import UIKit
class ViewController: UIViewController {
let path = UIBezierPath()
let path2 = UIBezierPath()
let path3 = UIBezierPath()
let path4 = UIBezierPath()
#IBAction func clicked(_ sender: Any) {
print("Clicked")
}
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
drawShape()
let gesture = UITapGestureRecognizer(target: self, action: #selector (self.checkAction(sender:)))
self.button.addGestureRecognizer(gesture)
}
func drawShape(){
let buttonWidth = button.frame.width
let buttonHeight = buttonWidth * 1.23
path.move(to: CGPoint(x: 0, y: buttonHeight*0.85))
path.addLine(to: CGPoint(x: 0, y: buttonHeight))
path.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight))
path.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.68))
path.close()
path2.move(to: CGPoint(x: 0, y: buttonHeight*0.68))
path2.addLine(to: CGPoint(x: 0, y: buttonHeight*0.85))
path2.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.68))
path2.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.35))
path2.close()
path3.move(to: CGPoint(x: 0, y: buttonHeight*0.50))
path3.addLine(to: CGPoint(x: 0, y: buttonHeight*0.68))
path3.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.35))
path3.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.06))
path3.close()
path4.move(to: CGPoint(x: 0, y: buttonHeight*0.45))
path4.addLine(to: CGPoint(x: 0, y: buttonHeight*0.50))
path4.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.06))
path4.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.01))
path4.close()
}
func checkAction(sender : UITapGestureRecognizer) {
let location = sender.location(in: button)
print(location)
hitTest(tapLocation: location)
}
public func hitTest(tapLocation:CGPoint){
if path.contains(tapLocation){
print("Button3")
}
if path2.contains(tapLocation){
print("Button2")
}
if path3.contains(tapLocation){
print("Button1")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Then in the file linked to the button:
import UIKit
#IBDesignable
class PushButtonView: UIButton {
override func draw(_ rect: CGRect) {
let buttonWidth = self.frame.width
let buttonHeight = buttonWidth * 1.23
let color1 = hexStringToUIColor(hex: "#e0dfd5")
let color2 = hexStringToUIColor(hex: "#ef6461")
let color3 = hexStringToUIColor(hex: "#e4b363")
let color4 = hexStringToUIColor(hex: "#313638")
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: buttonHeight*0.85))
path.addLine(to: CGPoint(x: 0, y: buttonHeight))
path.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight))
path.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.68))
path.close()
color4.setFill()
path.fill()
let path2 = UIBezierPath()
path2.move(to: CGPoint(x: 0, y: buttonHeight*0.68))
path2.addLine(to: CGPoint(x: 0, y: buttonHeight*0.85))
path2.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.68))
path2.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.35))
path2.close()
color3.setFill()
path2.fill()
let path3 = UIBezierPath()
path3.move(to: CGPoint(x: 0, y: buttonHeight*0.50))
path3.addLine(to: CGPoint(x: 0, y: buttonHeight*0.68))
path3.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.35))
path3.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.06))
path3.close()
color2.setFill()
path3.fill()
let path4 = UIBezierPath()
path4.move(to: CGPoint(x: 0, y: buttonHeight*0.45))
path4.addLine(to: CGPoint(x: 0, y: buttonHeight*0.50))
path4.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.06))
path4.addLine(to: CGPoint(x: buttonWidth, y: buttonHeight*0.01))
path4.close()
color1.setFill()
path4.fill()
}
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.characters.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt32 = 0
Scanner(string: cString).scanHexInt32(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}
}
Everything works! It may not be the best way of doing it but after 5 hours of trying different things this is whats worked best. However I am now stuck with the problem of having no idea how to manipulate those shapes I have drawn in the external file. I am sure that this has been asked and answered before but I literally have no idea was to search for.
For example if it was javascript I would assume a 'path' drawn within the object 'button' would be accessed by self.button.path (or something similar), however, my understanding is Swift3 doesn't work like that. Am I correct? So if I wanted to change the backgroundColor of the NOTES button/shape with an animate how would I get this effect:
UIView.animate(withDuration: 1, animations: {
self.button.path.color = UIColor.red
}, completion: { finished in
if(finished){
//callFunction()
}
})
Is this possible, or once a path is drawn is it not a changeable object and has to be deleted and redrawn?