How to combine shapes in SwiftUI? - path

I need to create a shape from several other shapes, and ideally, I'd get a single shape struct at the end which would have two shapes stacked as ZStack would. I haven't noticed an obvious implementation of this anywhere, so maybe somebody has some ideas on how to implement it?
Here's what I want to have:
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
// Add shapes here???? For example, combine Rectangle with Circle?
}
}

You can use func addPath(_ path: Path, transform: CGAffineTransform = .identity) to create a new Shape like this :
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
var customShape = Path { p in
p.move(to: CGPoint(x: 0, y: 0))
p.addQuadCurve(to: CGPoint(x: 0, y: 100),
control: CGPoint(x: 0, y: 0))
p.addCurve(to: CGPoint(x: 100, y: 400),
control1: CGPoint(x: 0, y: 200),
control2: CGPoint(x: 100, y: 200))
p.addCurve(to: CGPoint(x: 200, y: 100),
control1: CGPoint(x: 100, y: 200),
control2: CGPoint(x: 200, y: 200))
p.addQuadCurve(to: CGPoint(x: 200, y: 0),
control: CGPoint(x: 200, y: 0))
}
let rectangle = Rectangle()
.path(in: customShape.boundingRect)
.offsetBy(dx: 100, dy: 0)
customShape.addPath(rectangle, transform: .init(scaleX: 0.5, y: 0.35))
return customShape
}
}
And use it like this :
struct SwiftUIView: View {
var body: some View {
CustomShape()
}
}

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()
}
}
}

Problems about protocol and drawing in SwiftUI

I want to define a Symbol protocol and write three different structs, Rectangle, Diamond and Oval. The first two are OK, but Oval returns UIBezierPath instead of Path. What should I do
//
// Symbol.swift
// Set
//
// Created by world on 2022/11/27.
//
import SwiftUI
protocol Symbol: Shape {
var numberOfSymbols: SetGame.Card.Number { get }
func drawing(_ path: Path, center: CGPoint, width: CGFloat, height: CGFloat) -> Path
}
extension Symbol {
func path(in rect: CGRect) -> Path {
let widthOfRect = rect.maxX - rect.minX
let heightOfRect = rect.maxY - rect.minY
let widthOfSymbol = widthOfRect
let heightOfSymbol = widthOfSymbol / 2
var path = Path()
switch numberOfSymbols {
case .one:
path = drawing(path, center: CGPoint(x: rect.midX, y: rect.midY), width: widthOfSymbol, height: heightOfSymbol)
case .two:
path = drawing(path, center: CGPoint(x: rect.midX, y: heightOfRect * 1/3), width: widthOfSymbol, height: heightOfSymbol)
path = drawing(path, center: CGPoint(x: rect.midX, y: heightOfRect * 2/3), width: widthOfSymbol, height: heightOfSymbol)
case .three:
path = drawing(path, center: CGPoint(x: rect.midX, y: heightOfRect * 1/4), width: widthOfSymbol, height: heightOfSymbol)
path = drawing(path, center: CGPoint(x: rect.midX, y: heightOfRect * 2/4), width: widthOfSymbol, height: heightOfSymbol)
path = drawing(path, center: CGPoint(x: rect.midX, y: heightOfRect * 3/4), width: widthOfSymbol, height: heightOfSymbol)
}
return path
}
}
This is the diamond part that has been implemented
result: Diamond Image
//
// Diamond.swift
// Set
//
// Created by world on 2022/11/27.
//
import SwiftUI
struct Diamond: Symbol {
var numberOfSymbols: SetGame.Card.Number
func drawing(_ path: Path, center: CGPoint, width: CGFloat, height: CGFloat) -> Path {
var path = path
let rightPoint = CGPoint(x: center.x + width/2, y: center.y)
path.move(to: rightPoint)
let bottomPoint = CGPoint(x: center.x, y: center.y + height/2)
let leftPoint = CGPoint(x: center.x - width/2 , y: center.y)
let topPoint = CGPoint(x: center.x, y: center.y - height/2)
path.addLine(to: bottomPoint)
path.addLine(to: leftPoint)
path.addLine(to: topPoint)
path.addLine(to: rightPoint)
return path
}
}
I haven't implemented the oval part yet
//
// Oval.swift
// Set
//
// Created by world on 2022/11/27.
//
import SwiftUI
struct Oval: Symbol {
var numberOfSymbols: SetGame.Card.Number
func drawing(_ path: Path, center: CGPoint, width: CGFloat, height: CGFloat) -> Path {
var path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: width, height: height), cornerRadius: 0)
// ...
}
}
Perhaps this can be solved by function overloading

SwiftUI: clipping an image to shape

I'm currently working on some loading screen in SwiftUI and want to implement some kind of image animation with a simple path. I've done a simple Shape-animation with the path, but Ive stuck with clipping an image to it. Is it possible?
I just want my image to simply fly over the screen (with a configurable path). Imagine that the stroke has a color of background, so we won't see it. Thanks!
struct loadingPath: Shape {
func path(in rect: CGRect) -> Path {
let path =
Path { path in
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addQuadCurve(to: CGPoint(x: 230, y: 200), control: CGPoint(x: -100, y: 300))
path.addQuadCurve(to: CGPoint(x: 90, y: 400), control: CGPoint(x: 400, y: 130))
path.addLine(to: CGPoint(x: 90, y: 600))
}
return path
}
}
.........
var body: some View {
ZStack {
loadingPath()
.trim(from: 0, to: x)
.stroke(Color.purple)
.frame(width: 200, height: 200)
.onAppear() {
withAnimation(Animation.easeInOut(duration: 3).delay(0.5)) {
self.x = 1
}
}
}
If you want to give shape and then want to clipped it then use following :
.clipShape(RoundedRectangle(cornerRadius: 55,
style: .continuous))
Or if you you want to clipped image according to it's frame then you can simply clipped it after give it frame like following :
.
.
loadingPath()
.trim(from: 0, to: x)
.stroke(Color.purple)
.frame(width: 200, height: 200)
.clipped()
.onAppear() { ... }

BezierPath clipShape issue

I added curves on the right and left sides. When you look at the stroke function, everything is normal, but when the stroke function is removed, the curve on the right works normally, but the curve on the left does not work. is this a bug?
ContentView
struct ContentView: View {
#State var viewState: CGSize = .zero
var body: some View {
ZStack {
Color.orange
.clipShape(FooBezierPath(rightOffset: viewState).stroke())//remove stroke
.edgesIgnoringSafeArea(.all)
.overlay(
HStack {
Image(systemName: "chevron.right")
.font(.largeTitle)
.offset(y: 115)
.edgesIgnoringSafeArea(.all)
Spacer()
Image(systemName: "chevron.left")
.font(.largeTitle)
.contentShape(Rectangle())
.gesture(DragGesture().onChanged({ (value) in
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.viewState = value.translation
}
}).onEnded({ (value) in
withAnimation(.spring(response: 0.5, dampingFraction: 0.6, blendDuration: 0)) {
self.viewState = .zero
}
}))
.edgesIgnoringSafeArea(.all)
.offset(y: 70)
}
,alignment: .topTrailing
)
.padding(.horizontal)
}
}
}
BezierPath
struct FooBezierPath: Shape {
var rightOffset: CGSize
func path(in rect: CGRect) -> Path {
return Path { path in
let width = rect.width + rightOffset.width
let height = rect.height
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))
path.addLine(to: CGPoint(x: 0, y: 0))
//MARK: - Left Curve
path.move(to: CGPoint(x: 0, y: 80))
path.addCurve(to: CGPoint(x: 0, y: 180), control1: CGPoint(x: 50, y: 130), control2: CGPoint(x: 50, y: 130))
//MARK: - Right Curve
path.move(to: CGPoint(x: width, y: 80))
path.addCurve(to: CGPoint(x: width, y: 180), control1: CGPoint(x: width - 50, y: 130), control2: CGPoint(x: width - 50, y: 130))
}
}
}
It Helps to Draw the entire shape without moving points, otherwise the clip malfunctions for some reason. Try this:
struct FooBezierPath: Shape {
var rightOffset: CGSize
func path(in rect: CGRect) -> Path {
return Path { path in
let width = rect.width + rightOffset.width
let height = rect.height
// Draw left Edge of Shape
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 80))
path.addCurve(to: CGPoint(x: 0, y: 180), control1: CGPoint(x: 50, y: 130), control2: CGPoint(x: 50, y: 130))
path.addLine(to: CGPoint(x: 0, y: height))
// Draw Bottom Edge of Shape
path.addLine(to: CGPoint(x: width, y: height))
// Draw Right Edge of Shape
path.addLine(to: CGPoint(x: width, y: 180))
path.addCurve(to: CGPoint(x: width, y: 80), control1: CGPoint(x: width - 50, y: 130), control2: CGPoint(x: width - 50, y: 130))
path.addLine(to: CGPoint(x: width, y: 0))
// Draw Top Edge of Shape
path.addLine(to: CGPoint(x:0, y: 0))
}
}
}
Tried and tested the code, the results are:
Without Stroke
With Stroke

Creating triangle view fails when not added to x: 0 and y: 0

This code works when the frame's x and y are 0, but fails when using different x and y's:
class Triangle: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
let startX = self.center.x
let startY: CGFloat = 0
path.move(to: CGPoint(x: startX, y: startY))
path.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
path.addLine(to: CGPoint(x: 0, y: self.bounds.height))
path.close()
UIColor.green.setStroke()
path.stroke()
}
}
import UIKit
class ViewController: UIViewController {
#IBAction func animate(_ sender: UIButton) {
let triangleView = Triangle(frame: CGRect(x: 50, y: 50, width: 30, height: 30))
triangleView.backgroundColor = .clear
self.view.addSubview(triangleView)
}
}
This works:
let triangleView = Triangle(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
When failing it looks like this:
Well that is one ugly triangle. Why does it works with x: 0 and y:0 and fails when using different floats there? How can I fix this?
replace let startX = self.center.x with let startX = self. bounds.width / 2

Resources