Skip to content

Instantly share code, notes, and snippets.

@twocentstudios
Created February 1, 2023 11:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save twocentstudios/6deb870942ce0b69816c7550c73a3a14 to your computer and use it in GitHub Desktop.
Save twocentstudios/6deb870942ce0b69816c7550c73a3a14 to your computer and use it in GitHub Desktop.
Example code for "Path Drawing in SwiftUI"
import SwiftUI
/// Example code for "Path Drawing in SwiftUI"
///
/// Support: Xcode 14.2, iOS 16.0+
///
/// Instructions:
/// * Create a new SwiftUI app in Xcode.
/// * Paste these contents into a new file.
/// * Open SwiftUI Previews (opt+cmd+return)
struct RoundedRectShape: Shape {
var cornerRadius: CGFloat = 20
func path(in rect: CGRect) -> Path {
Path(roundedRect: rect, cornerRadius: cornerRadius)
}
}
struct RoundedRectView_Previews: PreviewProvider {
static var previews: some View {
RoundedRectShape()
.fill(.gray)
.previewingShape()
}
}
struct RoundedRectStrokeView_Previews: PreviewProvider {
static var previews: some View {
RoundedRectShape()
.stroke(.gray)
.previewingShape()
}
}
struct RoundedRectStrokeFillView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
RoundedRectShape()
.fill(.gray)
RoundedRectShape()
.stroke(Color.black, lineWidth: 4)
}
.previewingShape()
}
}
struct BannerShape: Shape {
func path(in rect: CGRect) -> Path {
return Path { p in
p.move(to: .init(x: rect.minX, y: rect.maxY))
p.addLine(to: .init(x: rect.minX, y: rect.minY))
p.addLine(to: .init(x: rect.maxX, y: rect.midY))
p.closeSubpath() // same as: `p.addLine(to: .init(x: rect.minX, y: rect.maxY))`
}
}
}
struct BannerView_Previews: PreviewProvider {
static var previews: some View {
BannerShape()
.fill(.gray)
.previewingShape()
}
}
struct BannerAbsoluteShape: Shape {
func path(in rect: CGRect) -> Path {
return Path { p in
p.move(to: .init(x: 10, y: 50))
p.addLine(to: .init(x: 10, y: 10))
p.addLine(to: .init(x: 100, y: 30))
p.closeSubpath() // same as: `p.addLine(to: .init(x: rect.minX, y: rect.maxY))`
}
}
}
struct BannerAbsoluteView_Previews: PreviewProvider {
static var previews: some View {
BannerAbsoluteShape()
.fill(Color(.darkGray))
.background(Color(.lightGray))
.previewingShape()
}
}
struct RoundedRectArcUnsafeShape: Shape {
let cornerRadius: CGFloat
func path(in rect: CGRect) -> Path {
Path { p in
p.move(to: .init(x: rect.minX + cornerRadius, y: rect.minY))
p.addLine(to: .init(x: rect.maxX - cornerRadius, y: rect.minY))
p.addArc(
tangent1End: .init(x: rect.maxX, y: rect.minY),
tangent2End: .init(x: rect.maxX, y: rect.minY + cornerRadius),
radius: cornerRadius
)
p.addLine(to: .init(x: rect.maxX, y: rect.maxY - cornerRadius))
p.addArc(
tangent1End: .init(x: rect.maxX, y: rect.maxY),
tangent2End: .init(x: rect.maxX - cornerRadius, y: rect.maxY),
radius: cornerRadius
)
p.addLine(to: .init(x: rect.minX + cornerRadius, y: rect.maxY))
p.addArc(
tangent1End: .init(x: rect.minX, y: rect.maxY),
tangent2End: .init(x: rect.minX, y: rect.maxY - cornerRadius),
radius: cornerRadius
)
p.addLine(to: .init(x: rect.minX, y: rect.minY + cornerRadius))
p.addArc(
tangent1End: .init(x: rect.minX, y: rect.minY),
tangent2End: .init(x: rect.minX + cornerRadius, y: rect.minY),
radius: cornerRadius
)
p.closeSubpath()
}
}
}
struct RoundedRectArcUnsafeView_Previews: PreviewProvider {
static var previews: some View {
let cornerRadius: CGFloat = 100
ZStack {
RoundedRectArcUnsafeShape(cornerRadius: cornerRadius)
.stroke(.gray, lineWidth: 9)
RoundedRectangle(cornerRadius: cornerRadius, style: .circular)
.stroke(.red, lineWidth: 1)
}
.previewingShape()
}
}
struct RoundedRectArcShape: Shape {
let cornerRadius: CGFloat
func path(in rect: CGRect) -> Path {
let maxBoundedCornerRadius = min(min(cornerRadius, rect.width / 2.0), rect.height / 2.0)
let minBoundedCornerRadius = max(maxBoundedCornerRadius, 0.0)
let boundedCornerRadius = minBoundedCornerRadius
return Path { p in
p.move(to: .init(x: rect.minX + boundedCornerRadius, y: rect.minY))
p.addLine(to: .init(x: rect.maxX - boundedCornerRadius, y: rect.minY))
p.addArc(
tangent1End: .init(x: rect.maxX, y: rect.minY),
tangent2End: .init(x: rect.maxX, y: rect.minY + boundedCornerRadius),
radius: boundedCornerRadius
)
p.addLine(to: .init(x: rect.maxX, y: rect.maxY - boundedCornerRadius))
p.addArc(
tangent1End: .init(x: rect.maxX, y: rect.maxY),
tangent2End: .init(x: rect.maxX - boundedCornerRadius, y: rect.maxY),
radius: boundedCornerRadius
)
p.addLine(to: .init(x: rect.minX + boundedCornerRadius, y: rect.maxY))
p.addArc(
tangent1End: .init(x: rect.minX, y: rect.maxY),
tangent2End: .init(x: rect.minX, y: rect.maxY - boundedCornerRadius),
radius: boundedCornerRadius
)
p.addLine(to: .init(x: rect.minX, y: rect.minY + boundedCornerRadius))
p.addArc(
tangent1End: .init(x: rect.minX, y: rect.minY),
tangent2End: .init(x: rect.minX + boundedCornerRadius, y: rect.minY),
radius: boundedCornerRadius
)
p.closeSubpath()
}
}
}
struct RoundedRectArcView_Previews: PreviewProvider {
static var previews: some View {
let cornerRadius: CGFloat = 100
ZStack {
RoundedRectArcShape(cornerRadius: cornerRadius)
.stroke(.gray, lineWidth: 9)
RoundedRectangle(cornerRadius: cornerRadius, style: .circular)
.stroke(.red, lineWidth: 1)
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.stroke(.blue, lineWidth: 1)
}
.previewingShape()
}
}
struct QuadCurveScoop: Shape {
/// 0...1
var pointOffsetFraction: CGFloat = 0.0
func path(in rect: CGRect) -> Path {
Path { p in
p.move(to: .init(x: rect.minX, y: rect.minY))
p.addQuadCurve(
to: .init(x: rect.maxX, y: rect.minY),
control: .init(x: rect.maxX * pointOffsetFraction, y: rect.maxY)
)
}
}
}
struct QuadCurveScoopView_Previews: PreviewProvider {
static var previews: some View {
HStack(alignment: .top) {
QuadCurveScoop(pointOffsetFraction: 0.0)
.fill(Color(.darkGray))
.frame(height: 100)
.border(Color(.red))
.overlay(alignment: .bottomLeading) {
Circle()
.fill(Color(.darkGray))
.frame(width: 4, height: 4)
}
QuadCurveScoop(pointOffsetFraction: 0.5)
.fill(Color(.darkGray))
.frame(height: 100)
.border(Color(.red))
.overlay(alignment: .bottom) {
Circle()
.fill(Color(.darkGray))
.frame(width: 4, height: 4)
}
QuadCurveScoop(pointOffsetFraction: 1.0)
.fill(Color(.darkGray))
.border(Color(.red))
.overlay(alignment: .bottomTrailing) {
Circle()
.fill(Color(.darkGray))
.frame(width: 4, height: 4)
}
}
.background(Color(.lightGray))
.frame(width: 200, height: 200)
.padding(50)
.previewLayout(.sizeThatFits)
}
}
struct Cloud1Shape: Shape {
func path(in rect: CGRect) -> Path {
let inset = rect.width / 2.0
return Path { p in
p.addEllipse(in: rect.inset(by: .init(top: 0, left: 0, bottom: 0, right: inset)))
p.addEllipse(in: rect.inset(by: .init(top: 0, left: inset / 2.0, bottom: 0, right: inset / 2.0)))
p.addEllipse(in: rect.inset(by: .init(top: 0, left: inset, bottom: 0, right: 0)))
}
}
}
struct Cloud1View_Previews: PreviewProvider {
static var previews: some View {
Cloud1Shape()
.fill(.gray)
.previewingShape()
}
}
struct Cloud1StrokeView_Previews: PreviewProvider {
static var previews: some View {
Cloud1Shape()
.stroke(.gray, lineWidth: 4)
.previewingShape()
}
}
struct Cloud2Shape: Shape {
func path(in rect: CGRect) -> Path {
let inset = rect.width / 2.0
let leftEllipse = Path(ellipseIn: rect.inset(by: .init(top: 0, left: 0, bottom: 0, right: inset)))
let centerEllipse = Path(ellipseIn: rect.inset(by: .init(top: 0, left: inset / 2.0, bottom: 0, right: inset / 2.0)))
let rightEllipse = Path(ellipseIn: rect.inset(by: .init(top: 0, left: inset, bottom: 0, right: 0)))
let combinedCGPath = leftEllipse.cgPath
.union(centerEllipse.cgPath)
.union(rightEllipse.cgPath)
return Path(combinedCGPath)
}
}
struct Cloud2StrokeView_Previews: PreviewProvider {
static var previews: some View {
Cloud2Shape()
.stroke(.gray, lineWidth: 4)
.previewingShape()
}
}
struct MapOnboardingBubbleShape: Shape {
var cornerRadius: CGFloat = 12
var arrowRectSize: CGFloat = 20
var arcLength: CGFloat = 12
/// 0.0 = left, 0.5 = center, 1.0 = right
var arrowOffsetFraction: CGFloat = 0.5
func baseXPos(for rect: CGRect) -> CGFloat {
(rect.maxX - cornerRadius - cornerRadius - arrowRectSize) * arrowOffsetFraction + cornerRadius
}
func path(in rect: CGRect) -> Path {
let roundedRect = Path(roundedRect: rect, cornerRadius: cornerRadius)
let arrowPath = Path { p in
p.move(to: .init(x: baseXPos(for: rect), y: rect.maxY))
p.addLine(to: .init(
x: baseXPos(for: rect) + arrowRectSize - arcLength,
y: rect.maxY + arrowRectSize - arcLength
))
p.addQuadCurve(
to: .init(
x: baseXPos(for: rect) + arrowRectSize,
y: rect.maxY + arrowRectSize - arcLength
),
control: .init(
x: baseXPos(for: rect) + arrowRectSize,
y: rect.maxY + arrowRectSize
)
)
p.addLine(to: .init(x: baseXPos(for: rect) + arrowRectSize, y: rect.maxY))
p.closeSubpath()
}
let combinedCGPath = roundedRect.cgPath.union(arrowPath.cgPath)
let combinedPath = Path(combinedCGPath)
return combinedPath
}
}
struct MapOnboardingBubbleShape_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 30) {
MapOnboardingBubbleShape(arrowOffsetFraction: 0.0).fill(Color(.darkGray))
.frame(width: 150, height: 100, alignment: .center)
.overlay { Text("0.0").foregroundColor(.white) }
MapOnboardingBubbleShape(arrowOffsetFraction: 0.5).fill(Color(.darkGray))
.frame(width: 150, height: 100, alignment: .center)
.overlay { Text("0.5").foregroundColor(.white) }
MapOnboardingBubbleShape(arrowOffsetFraction: 1.0).fill(Color(.darkGray))
.frame(width: 150, height: 100, alignment: .center)
.overlay { Text("1.0").foregroundColor(.white) }
}
.padding(50)
.previewLayout(.sizeThatFits)
}
}
struct DrawBubbleView: View {
@State var drawFraction: CGFloat = 0
var body: some View {
VStack {
MapOnboardingBubbleShape()
.trim(from: 0, to: drawFraction)
.stroke(.gray, lineWidth: 3)
.animation(.spring(), value: drawFraction)
.frame(width: 150, height: 100)
.padding(.bottom, 50)
Button(drawFraction > 0.0 ? "Hide" : "Show") {
drawFraction = drawFraction > 0.0 ? 0.0 : 1.0
}
.tint(Color.gray)
}
}
}
struct DrawBubbleView_Previews: PreviewProvider {
static var previews: some View {
DrawBubbleView()
}
}
struct BubbleTransitionView: View {
@State var isVisible: Bool = false
var body: some View {
VStack {
ZStack {
if isVisible {
Text("Hello!")
.padding(30)
.background {
MapOnboardingBubbleShape().fill(Color(.systemGray5))
}
.transition(.opacity.combined(with: .scale).animation(.spring(response: 0.25, dampingFraction: 0.7)))
}
}
.frame(width: 200, height: 100)
.padding(.bottom, 50)
Button(isVisible ? "Hide" : "Show") {
isVisible.toggle()
}
.tint(Color.gray)
}
}
}
struct BubbleTransitionView_Previews: PreviewProvider {
static var previews: some View {
BubbleTransitionView()
}
}
extension View {
@ViewBuilder func previewingShape() -> some View {
frame(width: 200, height: 100)
.padding(50)
.previewLayout(.sizeThatFits)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment