├── .gitignore
├── Demo
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Demo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── DemoApp.swift
│ ├── Demo.entitlements
│ ├── InsetPreview.swift
│ ├── ContentView.swift
│ ├── CircularCurveComparisonView.swift
│ └── LayoutDirectedPreview.swift
├── Demo.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcuserdata
│ │ │ └── vyom.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ ├── xcuserdata
│ │ └── vyom.xcuserdatad
│ │ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ ├── xcshareddata
│ │ └── xcschemes
│ │ │ └── Demo.xcscheme
│ └── project.pbxproj
└── Package.swift
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcuserdata
│ └── vyom.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Package.swift
├── LICENSE
├── README.md
└── src
├── SmoothRoundedRectangle+Attributes.swift
├── SmoothRoundedRectangle+Helper.swift
└── SmoothRoundedRectangle.swift
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/Demo/Demo/DemoApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct DemoApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.xcworkspace/xcuserdata/vyom.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sachinkmr57/SmoothRoundedRectangle/HEAD/Demo/Demo.xcodeproj/project.xcworkspace/xcuserdata/vyom.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Demo/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 |
3 | // Leave blank. This is only here so that Xcode doesn't display it.
4 |
5 | import PackageDescription
6 |
7 | let package = Package(
8 | name: "client",
9 | products: [],
10 | targets: []
11 | )
12 |
--------------------------------------------------------------------------------
/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Demo/Demo/Demo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/xcuserdata/vyom.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Demo.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/vyom.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SmoothRoundedRectangle.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 1
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "SmoothRoundedRectangle",
6 | platforms: [
7 | .iOS(.v13),
8 | .macOS(.v10_15),
9 | .tvOS(.v13),
10 | .watchOS(.v6),
11 | ],
12 | products: [
13 | .library(name: "SmoothRoundedRectangle", targets: ["SmoothRoundedRectangle"]),
14 | ],
15 | dependencies: [],
16 | targets: [
17 | .target(name: "SmoothRoundedRectangle", path: "src"),
18 | ]
19 | )
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sachin Kumar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Demo/Demo/InsetPreview.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SmoothRoundedRectangle
3 |
4 | @available(iOS 17, *) // @Previewable
5 | #Preview {
6 | @Previewable @State var radius: Double = 64
7 | @Previewable @State var smoothness: Double = -10
8 |
9 | let smoothUnevenShape = SmoothRoundedRectangle(
10 | radius: radius,
11 | style: .smooth(smoothness)
12 | )
13 |
14 | VStack {
15 | smoothUnevenShape
16 | .strokeBorder(lineWidth: 4)
17 | .foregroundStyle(.indigo)
18 | .aspectRatio(contentMode: .fit)
19 | .frame(maxHeight: .infinity)
20 |
21 | VStack(alignment: .leading, spacing: 0) {
22 | HStack {
23 | Text("Radius")
24 | Spacer()
25 | Text(radius, format: .number.precision(.fractionLength(0)))
26 | .monospacedDigit()
27 | }
28 | Slider(value: $radius, in: 0...128)
29 |
30 | Divider().frame(height: 64)
31 |
32 | HStack {
33 | Text("Smoothness")
34 | Spacer()
35 | Text(smoothness, format: .number.precision(.fractionLength(2)))
36 | .monospacedDigit()
37 | }
38 | Slider(value: $smoothness, in: -10...10)
39 | }
40 | }
41 | .padding()
42 | .preferredColorScheme(.dark)
43 | }
44 |
--------------------------------------------------------------------------------
/Demo/Demo/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SmoothRoundedRectangle
3 |
4 | struct ContentView: View {
5 | @State var radius: Double = 128
6 | @State var smoothness: Double = 1
7 |
8 | var body: some View {
9 | VStack(spacing: 64) {
10 | ZStack {
11 | RoundedRectangle(cornerRadius: radius, style: .continuous)
12 | .foregroundStyle(.red)
13 |
14 | SmoothRoundedRectangle(
15 | radius: radius,
16 | style: .smooth(smoothness)
17 | )
18 | .foregroundStyle(.indigo)
19 | }
20 | .frame(width: 512, height: 512)
21 | .frame(width: 254, height: 254, alignment: .bottomTrailing)
22 |
23 | VStack(alignment: .leading, spacing: 0) {
24 | HStack {
25 | Text("Radius")
26 | Spacer()
27 | Text(radius, format: .number.precision(.fractionLength(0)))
28 | .monospacedDigit()
29 | }
30 | Slider(value: $radius, in: 0...256)
31 |
32 | Divider().frame(height: 64)
33 |
34 | HStack {
35 | Text("Smoothness")
36 | Spacer()
37 | Text(smoothness, format: .number.precision(.fractionLength(2)))
38 | .monospacedDigit()
39 | }
40 | Slider(value: $smoothness, in: 0...1)
41 | }
42 | }
43 | .padding()
44 | .preferredColorScheme(.dark)
45 | }
46 | }
47 |
48 | #Preview {
49 | ContentView()
50 | }
51 |
--------------------------------------------------------------------------------
/Demo/Demo/CircularCurveComparisonView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SmoothRoundedRectangle
3 |
4 | struct CircularCurveComparisonView: View {
5 | @State var radius: Double = 8
6 | @State var smoothness: Double = 1
7 |
8 | let sizes: [CGFloat] = [16, 32, 48, 64, 128, 256]
9 |
10 | var body: some View {
11 | VStack(spacing: 8) {
12 | ForEach(sizes, id: \.self) { size in
13 |
14 | ZStack {
15 | RoundedRectangle(cornerRadius: radius, style: .continuous)
16 | .foregroundStyle(.red)
17 |
18 | SmoothRoundedRectangle(
19 | radius: radius,
20 | style: .smooth(smoothness)
21 | )
22 | .foregroundStyle(.indigo)
23 | }
24 | .frame(width: size, height: size)
25 | }
26 |
27 | VStack(alignment: .leading, spacing: 0) {
28 | HStack {
29 | Text("Radius")
30 | Spacer()
31 | Text(radius, format: .number.precision(.fractionLength(0)))
32 | .monospacedDigit()
33 | }
34 | Slider(value: $radius, in: 0...sizes.last!)
35 |
36 | Divider().frame(height: 64)
37 |
38 | HStack {
39 | Text("Smoothness")
40 | Spacer()
41 | Text(smoothness, format: .number.precision(.fractionLength(2)))
42 | .monospacedDigit()
43 | }
44 | Slider(value: $smoothness, in: 0...1)
45 | }
46 | }
47 | .padding()
48 | .preferredColorScheme(.dark)
49 | }
50 | }
51 |
52 | #Preview {
53 | CircularCurveComparisonView()
54 | }
55 |
--------------------------------------------------------------------------------
/Demo/Demo/LayoutDirectedPreview.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SmoothRoundedRectangle
3 |
4 | @available(iOS 16, *) // UnevenRoundedRectangle
5 | @available(iOS 17, *) // @Previewable
6 | #Preview {
7 | @Previewable @State var radius: Double = 64
8 | @Previewable @State var smoothness: Double = 1
9 |
10 | let unevenShape = UnevenRoundedRectangle(
11 | topLeadingRadius: 0,
12 | bottomLeadingRadius: 0,
13 | bottomTrailingRadius: radius,
14 | topTrailingRadius: radius,
15 | style: .continuous
16 | )
17 |
18 | let smoothUnevenShape = SmoothRoundedRectangle(
19 | topLeadingRadius: 0,
20 | bottomLeadingRadius: 0,
21 | bottomTrailingRadius: radius,
22 | topTrailingRadius: radius,
23 | style: .smooth(smoothness)
24 | )
25 |
26 | VStack {
27 | ForEach(LayoutDirection.allCases, id: \.self) { direction in
28 | unevenShape
29 | .fill(.red)
30 | .overlay {
31 | smoothUnevenShape
32 | .foregroundStyle(.indigo)
33 | }
34 | .environment(\.layoutDirection, direction)
35 | .overlay {
36 | Text(String(describing: direction))
37 | }
38 | }
39 |
40 | VStack(alignment: .leading, spacing: 0) {
41 | HStack {
42 | Text("Radius")
43 | Spacer()
44 | Text(radius, format: .number.precision(.fractionLength(0)))
45 | .monospacedDigit()
46 | }
47 | Slider(value: $radius, in: 0...128)
48 |
49 | Divider().frame(height: 64)
50 |
51 | HStack {
52 | Text("Smoothness")
53 | Spacer()
54 | Text(smoothness, format: .number.precision(.fractionLength(2)))
55 | .monospacedDigit()
56 | }
57 | Slider(value: $smoothness, in: 0...1)
58 | }
59 | }
60 | .padding()
61 | .preferredColorScheme(.dark)
62 | }
63 |
--------------------------------------------------------------------------------
/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "appearances" : [
10 | {
11 | "appearance" : "luminosity",
12 | "value" : "dark"
13 | }
14 | ],
15 | "idiom" : "universal",
16 | "platform" : "ios",
17 | "size" : "1024x1024"
18 | },
19 | {
20 | "appearances" : [
21 | {
22 | "appearance" : "luminosity",
23 | "value" : "tinted"
24 | }
25 | ],
26 | "idiom" : "universal",
27 | "platform" : "ios",
28 | "size" : "1024x1024"
29 | },
30 | {
31 | "idiom" : "mac",
32 | "scale" : "1x",
33 | "size" : "16x16"
34 | },
35 | {
36 | "idiom" : "mac",
37 | "scale" : "2x",
38 | "size" : "16x16"
39 | },
40 | {
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "32x32"
44 | },
45 | {
46 | "idiom" : "mac",
47 | "scale" : "2x",
48 | "size" : "32x32"
49 | },
50 | {
51 | "idiom" : "mac",
52 | "scale" : "1x",
53 | "size" : "128x128"
54 | },
55 | {
56 | "idiom" : "mac",
57 | "scale" : "2x",
58 | "size" : "128x128"
59 | },
60 | {
61 | "idiom" : "mac",
62 | "scale" : "1x",
63 | "size" : "256x256"
64 | },
65 | {
66 | "idiom" : "mac",
67 | "scale" : "2x",
68 | "size" : "256x256"
69 | },
70 | {
71 | "idiom" : "mac",
72 | "scale" : "1x",
73 | "size" : "512x512"
74 | },
75 | {
76 | "idiom" : "mac",
77 | "scale" : "2x",
78 | "size" : "512x512"
79 | }
80 | ],
81 | "info" : {
82 | "author" : "xcode",
83 | "version" : 1
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SmoothRoundedRectangle
[](./LICENSE) [](./LANGUAGE)
2 |
3 | ## Overview
4 | A custom SwiftUI shape which mimics Figma's smooth corner rounding for rectangles. The smoothness value can be anything between 0 to 1, 0 with completely circular corner radius and 1 with full smoothness. Corners to which rounding has to be applied can be specified as well. The amount of rounding can be even more than half of the smaller dimension of rectangle just like in Figma.
5 |
6 | ### Detail explanation
7 | Article: [Parametric corner smoothing in SwiftUI](https://medium.com/@zvyom/parametric-corner-smoothing-in-swiftui-108acea52874)
8 |
9 | ## Usage
10 | Below are few examples:
11 | #### Uniform radii on all corners with custom smoothing
12 | ``` swift
13 | SmoothRoundedRectangle(radius: 80, style: .smooth(1))
14 | .fill(.cyan)
15 | .frame(width: 240, height: 240)
16 | ```
17 |
18 | #### Uniform radii on selected corners
19 | ``` swift
20 | SmoothRoundedRectangle(radius: 80, corners: [.topLeading, .bottomTrailing])
21 | .fill(.green)
22 | .frame(width: 240, height: 80)
23 | ```
24 |
25 | #### Different radii on different corners
26 | ``` swift
27 | SmoothRoundedRectangle(
28 | topLeadingRadius: 80,
29 | bottomLeadingRadius: 80,
30 | bottomTrailingRadius: 20,
31 | topTrailingRadius: 20,
32 | style: .continuous
33 | )
34 | .frame(width: 240, height: 120)
35 | ```
36 |
37 | ### Using inside clipShape
38 | ``` swift
39 | ContentView()
40 | .clipShape(SmoothRoundedRectangle(radius: 12, style: .continues))
41 | ```
42 | ## Results and comparison
43 | #### A comparison of zero smoothness and 70%.
44 | The indigo corner has 70% smoothing and the green one has no smoothing.
45 | 
46 |
47 | #### A comparison of different smoothnesses and comparison with Figma's smoothness.
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
36 |
42 |
43 |
44 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
69 |
75 |
76 |
77 |
78 |
84 |
86 |
92 |
93 |
94 |
95 |
97 |
98 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/SmoothRoundedRectangle+Attributes.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SmoothRoundedRectangle+Attributes.swift
3 | // SmoothRoundedRectangle
4 | //
5 | // Created by Kumar on 06/07/24.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension SmoothRoundedRectangle {
11 |
12 |
13 | /// A smoothing style that controls how the rounded corners of a `SmoothRoundedRectangle` are computed.
14 | ///
15 | /// Use these styles to choose between perfectly circular corners, Apple’s continuous (squircle-like)
16 | /// corners, or a custom smoothness factor. The style influences the curvature blend between edges and
17 | /// corners for a more refined appearance than a simple circular radius.
18 | ///
19 | /// - circular: Produces classic circular arcs at the corners (equivalent to a smoothing factor of 0).
20 | /// This matches the traditional rounded rectangle look where each corner is a quarter-circle.
21 | ///
22 | /// - continuous: Matches the system’s continuous corner style (approximately a smoothing factor of 0.7 on iOS),
23 | /// yielding a squircle-like shape that visually blends edges and corners more fluidly than pure circular arcs.
24 | ///
25 | /// - smooth(_:): Lets you specify a custom smoothing factor in the range 0...1. Values closer to 0 resemble
26 | /// circular corners; values closer to 1 increase the smooth blending between edges and corners. Use this
27 | /// to fine-tune the aesthetic of the shape.
28 | ///
29 | /// - smooth: A convenience alias equivalent to `.smooth(1)`, providing the maximum smoothing effect.
30 | enum Style {
31 | /// No smoothing applied.
32 | /// - Same as calling `smooth(0)`
33 | case circular
34 | /// iOS default smoothness.
35 | /// - Same as calling `smooth(0.7)`
36 | case continuous
37 | /// Custom factor between `0` and `1`, represented as a `Double`.
38 | /// - Note: Values outside the valid range will be clamped to the nearest bound.
39 | case smooth(_: Double)
40 |
41 | /// Maximum valid smoothness.
42 | /// - Same as calling `smooth(1)`
43 | public static var smooth: Self { .smooth(1) }
44 | }
45 |
46 | /// An option set representing which corners of a SmoothRoundedRectangle should be affected.
47 | ///
48 | /// Use `Corners` to selectively apply rounding and smoothing to specific corners of the shape.
49 | /// You can combine multiple corners using set algebra (e.g., `[.topLeading, .bottomTrailing]`).
50 | ///
51 | /// Common presets are provided for convenience:
52 | /// - `all`: Applies to all four corners.
53 | /// - `top`: Applies to the top-left and top-right corners.
54 | /// - `bottom`: Applies to the bottom-left and bottom-right corners.
55 | /// - `leading`: Applies to the leading-side corners (adapts to layout direction).
56 | /// - `trailing`: Applies to the trailing-side corners (adapts to layout direction).
57 | ///
58 | /// Individual corners:
59 | /// - `topLeading`: The top-leading corner.
60 | /// - `topTrailing`: The top-trailing corner.
61 | /// - `bottomLeading`: The bottom-leading corner.
62 | /// - `bottomTrailing`: The bottom-trailing corner.
63 | ///
64 | /// Conforms to:
65 | /// - `OptionSet`: Enables combining multiple corners using bitwise operations.
66 | /// - `Sendable`: Safe to use across concurrency domains.
67 | struct Corners: OptionSet, Sendable {
68 |
69 | public let rawValue: Int
70 | public init(rawValue: Int) {
71 | self.rawValue = rawValue
72 | }
73 |
74 | public static let topLeading = Corners(rawValue: 1 << 0)
75 | public static let topTrailing = Corners(rawValue: 1 << 1)
76 | public static let bottomLeading = Corners(rawValue: 1 << 2)
77 | public static let bottomTrailing = Corners(rawValue: 1 << 3)
78 |
79 | public static let all: Corners = [.topLeading, .topTrailing, .bottomLeading, .bottomTrailing]
80 | public static let top: Corners = [.topLeading, .topTrailing]
81 | public static let bottom: Corners = [.bottomLeading, .bottomTrailing]
82 | public static let leading: Corners = [.topLeading, .bottomLeading]
83 | public static let trailing: Corners = [.topTrailing, .bottomTrailing]
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/SmoothRoundedRectangle+Helper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SmoothRoundedRectangle+Helper.swift
3 | // SmoothRoundedRectangle
4 | //
5 | // Created by Kumar on 01/07/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | typealias CornerAttributes = SmoothRoundedRectangle.SmoothCornerAttributes
11 |
12 | extension SmoothRoundedRectangle {
13 |
14 | /// Attributes required for rounding a corner
15 | struct SmoothCornerAttributes {
16 | var radius: CGFloat
17 | var smoothness: CGFloat
18 |
19 | // Segment length consumed by rounding and smoothing
20 | var segmentLength: CGFloat
21 |
22 | init(radius: CGFloat, smoothness: CGFloat = 0) {
23 | self.radius = radius
24 | self.smoothness = smoothness
25 | self.segmentLength = radius * (1 + smoothness)
26 | }
27 | }
28 |
29 | enum Corner {
30 | case topRight, bottomRight, bottomLeft, topLeft
31 | }
32 |
33 | struct SmoothRectangleAttributes {
34 | var topRight: CornerAttributes
35 | var bottomRight: CornerAttributes
36 | var bottomLeft: CornerAttributes
37 | var topLeft: CornerAttributes
38 | }
39 |
40 | /// Parameters required for calculation of a smooth corner
41 | struct SmoothCornerParameters {
42 |
43 | // Refernce: Fig 11.1 in https://www.figma.com/blog/desperately-seeking-squircles/
44 | let a, b, c, d, p, r: CGFloat
45 |
46 | // Deviation angle for the line from centre to start of circular arc due to smoothing
47 | let theta: CGFloat
48 |
49 | func unpack() -> (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat) {
50 | (a, b, c, d, p, r, theta)
51 | }
52 | }
53 |
54 | /// scale down smoothness and corner radius if segment length is more than available
55 | func normalizeCorners(_ rect: CGRect, rectAttr: SmoothRectangleAttributes) -> SmoothRectangleAttributes {
56 | let normalizedTopRight = getNormalizedCorner(rectAttr.topRight, in: rect, verticalNeighbour: rectAttr.bottomRight, horizontalNeighbour: rectAttr.topLeft)
57 | let normalizedBottomRight = getNormalizedCorner(rectAttr.bottomRight, in: rect, verticalNeighbour: rectAttr.topRight, horizontalNeighbour: rectAttr.bottomLeft)
58 | let normalizedBottomLeft = getNormalizedCorner(rectAttr.bottomLeft, in: rect, verticalNeighbour: rectAttr.topLeft, horizontalNeighbour: rectAttr.bottomRight)
59 | let normalizedTopLeft = getNormalizedCorner(rectAttr.topLeft, in: rect, verticalNeighbour: rectAttr.bottomLeft, horizontalNeighbour: rectAttr.topRight)
60 |
61 | return SmoothRectangleAttributes(
62 | topRight: normalizedTopRight,
63 | bottomRight: normalizedBottomRight,
64 | bottomLeft: normalizedBottomLeft,
65 | topLeft: normalizedTopLeft
66 | )
67 | }
68 |
69 | /// Compare a corner with both adjacent corners and compute normalized radius and smoothness
70 | private func getNormalizedCorner(
71 | _ base: CornerAttributes,
72 | in rect: CGRect,
73 | verticalNeighbour: CornerAttributes,
74 | horizontalNeighbour: CornerAttributes
75 | ) -> CornerAttributes {
76 | let (trR1, trS1) = calculateNormalization(base, horizontalNeighbour, edge: rect.size.width)
77 | let (trR2, trS2) = calculateNormalization(base, verticalNeighbour, edge: rect.size.height)
78 | return CornerAttributes(radius: min(trR1, trR2), smoothness: min(trS1, trS2))
79 | }
80 |
81 | /// Compare a corner with adjacent corner and compute normalized radius and smoothness
82 | private func calculateNormalization(
83 | _ base: CornerAttributes,
84 | _ adjacent: CornerAttributes,
85 | edge: CGFloat
86 | ) -> (CGFloat, CGFloat) {
87 | if (base.radius + adjacent.radius) >= edge {
88 | let scaleFactor = edge / (base.radius + adjacent.radius)
89 | return (base.radius * scaleFactor, 0)
90 | } else if (base.segmentLength + adjacent.segmentLength) > edge {
91 | let scaleFactor = edge / (base.segmentLength + adjacent.segmentLength)
92 | return (base.radius, (1 + base.smoothness) * scaleFactor - 1)
93 | } else {
94 | return (base.radius, base.smoothness)
95 | }
96 | }
97 |
98 | /// Compute start point, both control points for ramp up and down bezeri curves for all corners
99 | func computeCurvePoints(cornerAttributes: SmoothCornerParameters, rect: CGRect, corner: Corner) -> [CGPoint] {
100 | let (a, b, c, d, p, _, _) = cornerAttributes.unpack()
101 |
102 | switch corner {
103 | case .topRight:
104 | return [
105 | CGPoint(x: rect.size.width - (p - a - b - c), y: d),
106 | CGPoint(x: rect.size.width - (p - a), y: 0),
107 | CGPoint(x: rect.size.width - (p - a - b), y: 0),
108 | CGPoint(x: rect.size.width, y: p),
109 | CGPoint(x: rect.size.width, y: (p - a - b)),
110 | CGPoint(x: rect.size.width, y: (p - a))
111 | ]
112 | case .bottomRight:
113 | return [
114 | CGPoint(x: rect.size.width - d, y: rect.size.height - (p - a - b - c)),
115 | CGPoint(x: rect.size.width, y: rect.size.height - (p - a)),
116 | CGPoint(x: rect.size.width, y: rect.size.height - (p - a - b)),
117 | CGPoint(x: rect.size.width - p, y: rect.size.height),
118 | CGPoint(x: rect.size.width - (p - a - b), y: rect.size.height),
119 | CGPoint(x: rect.size.width - (p - a), y: rect.size.height)
120 | ]
121 | case .bottomLeft:
122 | return [
123 | CGPoint(x: (p - a - b - c), y: rect.size.height - d),
124 | CGPoint(x: (p - a), y: rect.size.height),
125 | CGPoint(x: (p - a - b), y: rect.size.height),
126 | CGPoint(x: 0, y: rect.size.height - p),
127 | CGPoint(x: 0, y: rect.size.height - (p - a - b)),
128 | CGPoint(x: 0, y: rect.size.height - (p - a))
129 | ]
130 | case .topLeft:
131 | return [
132 | CGPoint(x: d, y: (p - a - b - c)),
133 | CGPoint(x: 0, y: (p - a)),
134 | CGPoint(x: 0, y: (p - a - b)),
135 | CGPoint(x: p, y: 0),
136 | CGPoint(x: (p - a - b), y: 0),
137 | CGPoint(x: (p - a), y: 0)
138 | ]
139 | }
140 | }
141 |
142 | /// Compute all parameters required to draw the curves
143 | func computeParameters(rect: CGRect, cornerAttributes: CornerAttributes) -> SmoothCornerParameters {
144 | let smoothnessFactor = cornerAttributes.smoothness
145 | let p = (1 + smoothnessFactor) * cornerAttributes.radius
146 |
147 | let angleBeta = 90 * (1 - smoothnessFactor)
148 | let angleTheta = 45 * smoothnessFactor // theta = (90 - beta)/2
149 |
150 | let c = cornerAttributes.radius * tan(angleTheta / 2 * .pi / 180) * cos(angleTheta * .pi / 180)
151 | let d = cornerAttributes.radius * tan(angleTheta / 2 * .pi / 180) * sin(angleTheta * .pi / 180)
152 | // let arcSeg be the segment consumed by corner rounding excluding smoothing
153 | let arcSeg = sin(angleBeta / 2 * .pi / 180) * cornerAttributes.radius * sqrt(2)
154 | let b = (p - arcSeg - c - d) / 3
155 | let a = 2 * b
156 |
157 | return SmoothCornerParameters(
158 | a: a, b: b, c: c, d: d, p: p, r: cornerAttributes.radius, theta: angleTheta
159 | )
160 | }
161 |
162 | /// Draw the corner path
163 | func drawCornerPath(
164 | _ path: inout Path,
165 | in rect: CGRect,
166 | cornerAttributes: SmoothCornerAttributes,
167 | corner: Corner
168 | ) {
169 | if cornerAttributes.radius != 0 {
170 | let attributes = computeParameters(rect: rect, cornerAttributes: cornerAttributes)
171 | let (_, _, _, _, p, radius, theta) = attributes.unpack()
172 | let points = computeCurvePoints(cornerAttributes: attributes, rect: rect, corner: corner)
173 | let startAngle = startAngle(corner)
174 | path.addLine(to: curveStart(rect, corner: corner, p: p))
175 | path.addCurve(to: points[0], control1: points[1], control2: points[2])
176 | path.addArc(
177 | center: centerPoint(rect, corner: corner, radius: radius),
178 | radius: radius,
179 | startAngle: Angle(degrees: Double(startAngle + theta)),
180 | endAngle: Angle(degrees: Double(startAngle + 90 - theta)),
181 | clockwise: false
182 | )
183 | path.addCurve(to: points[3], control1: points[4], control2: points[5])
184 | } else {
185 | path.addLine(to: curveStart(rect, corner: corner, p: 0))
186 | }
187 | }
188 |
189 | /// Starting point of the ramp up bezier curve
190 | func curveStart(_ rect: CGRect, corner: Corner, p: CGFloat) -> CGPoint {
191 | switch corner {
192 | case .topRight:
193 | return CGPoint(x: rect.width - p, y: 0)
194 | case .bottomRight:
195 | return CGPoint(x: rect.width, y: rect.height - p)
196 | case .bottomLeft:
197 | return CGPoint(x: p, y: rect.height)
198 | case .topLeft:
199 | return CGPoint(x: 0, y: p)
200 | }
201 | }
202 |
203 | /// Start angle for the circular arc
204 | func startAngle(_ corner: Corner) -> CGFloat {
205 | switch corner {
206 | case .topRight:
207 | 270
208 | case .bottomRight:
209 | 0
210 | case .bottomLeft:
211 | 90
212 | case .topLeft:
213 | 180
214 | }
215 | }
216 |
217 | /// Center for the circular arc
218 | func centerPoint(_ rect: CGRect, corner: Corner, radius: CGFloat) -> CGPoint {
219 | switch corner {
220 | case .topRight:
221 | return CGPoint(x: rect.width - radius, y: radius)
222 | case .bottomRight:
223 | return CGPoint(x: rect.width - radius, y: rect.height - radius)
224 | case .bottomLeft:
225 | return CGPoint(x: radius, y: rect.height - radius)
226 | case .topLeft:
227 | return CGPoint(x: radius, y: radius)
228 | }
229 | }
230 | }
231 |
232 | extension SmoothRoundedRectangle.Style {
233 | var value: CGFloat {
234 | switch self {
235 | case .circular:
236 | return 0
237 | case .continuous:
238 | return 0.7
239 | case .smooth(let value):
240 | let validRange = 0...1.0
241 | return max(min(value, validRange.upperBound), validRange.lowerBound)
242 | }
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/SmoothRoundedRectangle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SmoothRoundedRectangle.swift
3 | // SmoothRoundedRectangle
4 | //
5 | // Created by Kumar on 01/07/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A customizable SwiftUI shape that draws a rounded rectangle with optional "smooth" corner transitions.
11 | ///
12 | /// SmoothRoundedRectangle allows you to specify individual corner radii while also applying a smoothing style
13 | /// to produce visually pleasing, continuous corner curves. It conforms to `InsettableShape`, enabling
14 | /// stroke insetting and precise layout control in SwiftUI.
15 | ///
16 | /// Usage examples:
17 | /// - Uniform radius on all corners:
18 | /// ```swift
19 | /// SmoothRoundedRectangle(radius: 16)
20 | /// .fill(.blue)
21 | /// ```
22 | /// - Apply to specific corners:
23 | /// ```swift
24 | /// SmoothRoundedRectangle(radius: 20, corners: [.topLeading, .bottomTrailing])
25 | /// .stroke(.red, lineWidth: 2)
26 | /// ```
27 | /// - Different radii per corner with a chosen style:
28 | /// ```swift
29 | /// SmoothRoundedRectangle(
30 | /// topLeadingRadius: 24,
31 | /// bottomLeadingRadius: 12,
32 | /// bottomTrailingRadius: 32,
33 | /// topTrailingRadius: 8,
34 | /// style: .smooth
35 | /// )
36 | /// .fill(.green)
37 | /// ```
38 | ///
39 | /// Features:
40 | /// - Supports left-to-right and right-to-left layout directions; leading/trailing-aware initializers will
41 | /// automatically map to the correct physical corners.
42 | /// - Conforms to `InsettableShape` so you can use `.inset(by:)` implicitly via `strokeBorder` and related APIs.
43 | /// - Provides multiple convenience initializers for uniform radii, selected corners, or fully customized
44 | /// per-corner radii with a smoothing `Style`.
45 | ///
46 | /// Initializers:
47 | /// - `init(radius:style:)`
48 | /// Creates a rectangle with the same radius applied to all corners, using the given smoothing style.
49 | /// - `init(radius:corners:style:)`
50 | /// Applies the same radius to a subset of corners specified by `Corners`, using the given smoothing style.
51 | /// - `init(topLeadingRadius:bottomLeadingRadius:bottomTrailingRadius:topTrailingRadius:style:)`
52 | /// Allows fine-grained control of each corner radius with a chosen smoothing `Style`.
53 | ///
54 | /// Notes:
55 | /// - The effective geometry is adjusted by any inset amount to support accurate stroked borders.
56 | /// - The actual corner drawing and normalization rely on associated helper types and functions
57 | /// (e.g., `CornerAttributes`, `Style`, `Corners`, `SmoothRectangleAttributes`, `normalizeCorners`, `drawCornerPath`)
58 | /// which define how smoothing is computed and applied to each corner.
59 | ///
60 | /// Platform:
61 | /// - Designed for SwiftUI on Apple platforms (iOS, iPadOS, macOS, watchOS, visionOS).
62 | public struct SmoothRoundedRectangle: InsettableShape {
63 | let topLeftCorner: CornerAttributes
64 | let topRightCorner: CornerAttributes
65 | let bottomLeftCorner: CornerAttributes
66 | let bottomRightCorner: CornerAttributes
67 |
68 | var insetAmount = 0.0
69 |
70 | // MARK: - Initializers
71 |
72 | /// Creates a SmoothRoundedRectangle with the same corner radius applied to all four corners,
73 | /// using the provided smoothing style.
74 | ///
75 | /// - Parameters:
76 | /// - radius: The corner radius to apply uniformly to all corners, in points. Values less than or equal to zero result in square corners.
77 | /// - style: The smoothing style that controls how the corner transitions are drawn. Use `.smooth` for continuous, visually pleasing curves or other styles as available.
78 | /// - Discussion:
79 | /// This initializer is the most convenient way to produce a rounded rectangle with uniform corner radii.
80 | /// The smoothing style affects the curvature near the corners to create more natural transitions than
81 | /// standard circular arcs. The shape conforms to `InsettableShape`, so borders drawn with `strokeBorder`
82 | /// will respect the correct inset geometry.
83 | /// - SeeAlso: `init(radius:corners:style:)`, `init(topLeadingRadius:bottomLeadingRadius:bottomTrailingRadius:topTrailingRadius:style:)`
84 | public init(radius: CGFloat, style: Style = .smooth) {
85 | self.init(topLeadingRadius: radius, bottomLeadingRadius: radius, bottomTrailingRadius: radius, topTrailingRadius: radius, style: style)
86 | }
87 |
88 |
89 | /// Creates a SmoothRoundedRectangle that applies the same corner radius to a specified subset of corners,
90 | /// using the provided smoothing style.
91 | ///
92 | /// - Parameters:
93 | /// - radius: The corner radius, in points, to apply to the selected corners. Values less than or equal to zero
94 | /// result in square corners for those selections.
95 | /// - corners: A set of corners to which the radius should be applied. Use values such as
96 | /// `[.topLeading, .bottomTrailing]` or `.all`. Leading/trailing are automatically mapped
97 | /// based on the current layout direction (left-to-right or right-to-left).
98 | /// - style: The smoothing style that controls the curvature transition at the rounded corners. Defaults to `.smooth`.
99 | ///
100 | /// - Discussion:
101 | /// This initializer is useful when you want to round only certain corners while leaving others square.
102 | /// The `style` parameter governs how the transition into each rounded corner is drawn, producing more
103 | /// continuous and visually pleasing shapes than basic circular arcs. Because this shape conforms to
104 | /// `InsettableShape`, borders drawn with `strokeBorder` will respect the correct inset geometry.
105 | ///
106 | /// - SeeAlso: `init(radius:style:)`, `init(topLeadingRadius:bottomLeadingRadius:bottomTrailingRadius:topTrailingRadius:style:)`
107 | public init(radius: CGFloat, corners: Corners, style: Style = .smooth) {
108 | self.init(
109 | topLeadingRadius: corners.contains(.topLeading) ? radius : 0,
110 | bottomLeadingRadius: corners.contains(.bottomLeading) ? radius : 0,
111 | bottomTrailingRadius: corners.contains(.bottomTrailing) ? radius : 0,
112 | topTrailingRadius: corners.contains(.topTrailing) ? radius : 0,
113 | style: style
114 | )
115 | }
116 |
117 |
118 | /// Creates a SmoothRoundedRectangle with individually configurable corner radii and a chosen smoothing style.
119 | ///
120 | /// - Parameters:
121 | /// - topLeadingRadius: The radius, in points, for the top-leading corner. Leading/trailing are automatically
122 | /// resolved based on the current layout direction (left-to-right or right-to-left). Values ≤ 0 produce a square corner.
123 | /// - bottomLeadingRadius: The radius, in points, for the bottom-leading corner. Leading/trailing are automatically
124 | /// resolved based on the current layout direction. Values ≤ 0 produce a square corner.
125 | /// - bottomTrailingRadius: The radius, in points, for the bottom-trailing corner. Leading/trailing are automatically
126 | /// resolved based on the current layout direction. Values ≤ 0 produce a square corner.
127 | /// - topTrailingRadius: The radius, in points, for the top-trailing corner. Leading/trailing are automatically
128 | /// resolved based on the current layout direction. Values ≤ 0 produce a square corner.
129 | /// - style: The smoothing style that determines how the corner transitions are drawn. Use `.smooth` for
130 | /// continuous, visually pleasing curves or another available style.
131 | ///
132 | /// - Discussion:
133 | /// Use this initializer when you need fine-grained control of each corner’s radius. The provided `style`
134 | /// governs how the transitions into and out of each corner are shaped, yielding more natural, continuous
135 | /// curves than simple circular arcs. The shape conforms to `InsettableShape`, so borders drawn with
136 | /// `strokeBorder` will respect the correct inset geometry. This initializer also respects the environment’s
137 | /// layout direction, mapping leading/trailing radii to their corresponding physical corners.
138 | ///
139 | /// - SeeAlso:
140 | /// - `init(radius:style:)` for a uniform radius on all corners
141 | /// - `init(radius:corners:style:)` for applying the same radius to a selected subset of corners
142 | public init(
143 | topLeadingRadius: CGFloat = 0,
144 | bottomLeadingRadius: CGFloat = 0,
145 | bottomTrailingRadius: CGFloat = 0,
146 | topTrailingRadius: CGFloat = 0,
147 | style: Style
148 | ) {
149 | let smoothnessValue = style.value
150 | self.topLeftCorner = CornerAttributes(radius: topLeadingRadius, smoothness: smoothnessValue)
151 | self.topRightCorner = CornerAttributes(radius: topTrailingRadius, smoothness: smoothnessValue)
152 | self.bottomLeftCorner = CornerAttributes(radius: bottomLeadingRadius, smoothness: smoothnessValue)
153 | self.bottomRightCorner = CornerAttributes(radius: bottomTrailingRadius, smoothness: smoothnessValue)
154 | }
155 |
156 | // MARK: - Path
157 |
158 | public func path(in rect: CGRect) -> Path {
159 | let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount)
160 |
161 | let normRect = normalizeCorners(insetRect, rectAttr: SmoothRectangleAttributes(
162 | topRight: topRightCorner,
163 | bottomRight: bottomRightCorner,
164 | bottomLeft: bottomLeftCorner,
165 | topLeft: topLeftCorner)
166 | )
167 |
168 | var path = Path()
169 | path.move(to: CGPoint(x: normRect.topLeft.segmentLength, y: 0))
170 |
171 | drawCornerPath(&path, in: insetRect, cornerAttributes: normRect.topRight, corner: .topRight)
172 | drawCornerPath(&path, in: insetRect, cornerAttributes: normRect.bottomRight, corner: .bottomRight)
173 | drawCornerPath(&path, in: insetRect, cornerAttributes: normRect.bottomLeft, corner: .bottomLeft)
174 | drawCornerPath(&path, in: insetRect, cornerAttributes: normRect.topLeft, corner: .topLeft)
175 | path.closeSubpath()
176 |
177 | return path.offsetBy(dx: insetAmount, dy: insetAmount)
178 | }
179 |
180 | // MARK: - Insettable
181 |
182 | /// Insets the shape’s path by the specified amount and returns a new shape representing that inset geometry.
183 | ///
184 | /// - Parameter amount: The number of points by which to inset the shape on all sides. Positive values
185 | /// shrink the shape inward; negative values expand it outward.
186 | /// - Returns: A new shape conforming to `InsettableShape` whose drawing and layout calculations are offset
187 | /// by the given inset amount. This enables accurate stroked borders when using APIs like `strokeBorder`,
188 | /// ensuring the stroke is drawn within the shape’s bounds.
189 | /// - Discussion:
190 | /// The inset amount is accumulated each time this method is called, allowing multiple inset operations
191 | /// to compose. The inset is applied to the rectangle used to compute the rounded and smoothed corners,
192 | /// so both fill and stroke paths reflect the adjusted geometry.
193 | public func inset(by amount: CGFloat) -> some InsettableShape {
194 | var shape = self
195 | shape.insetAmount += amount
196 | return shape
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/Demo/Demo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 70;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2ADED9152CE0E5A6008A4CFC /* SmoothRoundedRectangle in Frameworks */ = {isa = PBXBuildFile; productRef = 2ADED9142CE0E5A6008A4CFC /* SmoothRoundedRectangle */; };
11 | /* End PBXBuildFile section */
12 |
13 | /* Begin PBXFileReference section */
14 | 2ADED8D12CE0E35E008A4CFC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
15 | 2ADED9112CE0E56F008A4CFC /* SmoothRoundedRectangle */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SmoothRoundedRectangle; path = ..; sourceTree = ""; };
16 | 2ADED9162CE0E5A7008A4CFC /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
17 | /* End PBXFileReference section */
18 |
19 | /* Begin PBXFileSystemSynchronizedRootGroup section */
20 | 2ADED8E62CE0E526008A4CFC /* Demo */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Demo; sourceTree = ""; };
21 | /* End PBXFileSystemSynchronizedRootGroup section */
22 |
23 | /* Begin PBXFrameworksBuildPhase section */
24 | 2ADED8E22CE0E526008A4CFC /* Frameworks */ = {
25 | isa = PBXFrameworksBuildPhase;
26 | buildActionMask = 2147483647;
27 | files = (
28 | 2ADED9152CE0E5A6008A4CFC /* SmoothRoundedRectangle in Frameworks */,
29 | );
30 | runOnlyForDeploymentPostprocessing = 0;
31 | };
32 | /* End PBXFrameworksBuildPhase section */
33 |
34 | /* Begin PBXGroup section */
35 | 2ADED9102CE0E56F008A4CFC /* Frameworks */ = {
36 | isa = PBXGroup;
37 | children = (
38 | 2ADED9112CE0E56F008A4CFC /* SmoothRoundedRectangle */,
39 | );
40 | name = Frameworks;
41 | sourceTree = "";
42 | };
43 | BD288DBACF346D39BE8BEBFD = {
44 | isa = PBXGroup;
45 | children = (
46 | 2ADED8D12CE0E35E008A4CFC /* Assets.xcassets */,
47 | 2ADED8E62CE0E526008A4CFC /* Demo */,
48 | 2ADED9102CE0E56F008A4CFC /* Frameworks */,
49 | 2ADED9162CE0E5A7008A4CFC /* Demo.app */,
50 | );
51 | indentWidth = 4;
52 | sourceTree = "";
53 | tabWidth = 4;
54 | usesTabs = 1;
55 | wrapsLines = 1;
56 | };
57 | /* End PBXGroup section */
58 |
59 | /* Begin PBXNativeTarget section */
60 | 2ADED8E42CE0E526008A4CFC /* Demo */ = {
61 | isa = PBXNativeTarget;
62 | buildConfigurationList = 2ADED9072CE0E527008A4CFC /* Build configuration list for PBXNativeTarget "Demo" */;
63 | buildPhases = (
64 | 2ADED8E12CE0E526008A4CFC /* Sources */,
65 | 2ADED8E22CE0E526008A4CFC /* Frameworks */,
66 | 2ADED8E32CE0E526008A4CFC /* Resources */,
67 | );
68 | buildRules = (
69 | );
70 | dependencies = (
71 | );
72 | fileSystemSynchronizedGroups = (
73 | 2ADED8E62CE0E526008A4CFC /* Demo */,
74 | );
75 | name = Demo;
76 | packageProductDependencies = (
77 | 2ADED9142CE0E5A6008A4CFC /* SmoothRoundedRectangle */,
78 | );
79 | productName = Demo;
80 | productReference = 2ADED9162CE0E5A7008A4CFC /* Demo.app */;
81 | productType = "com.apple.product-type.application";
82 | };
83 | /* End PBXNativeTarget section */
84 |
85 | /* Begin PBXProject section */
86 | 8C837CC0C17751C310B0A29D /* Project object */ = {
87 | isa = PBXProject;
88 | attributes = {
89 | BuildIndependentTargetsInParallel = YES;
90 | LastSwiftUpdateCheck = 1610;
91 | ORGANIZATIONNAME = chenzook;
92 | TargetAttributes = {
93 | 2ADED8E42CE0E526008A4CFC = {
94 | CreatedOnToolsVersion = 16.1;
95 | };
96 | };
97 | };
98 | buildConfigurationList = 0649CA3E896BAFA19C6914A0 /* Build configuration list for PBXProject "Demo" */;
99 | compatibilityVersion = "Xcode 14.0";
100 | developmentRegion = en;
101 | hasScannedForEncodings = 0;
102 | knownRegions = (
103 | Base,
104 | en,
105 | );
106 | mainGroup = BD288DBACF346D39BE8BEBFD;
107 | productRefGroup = BD288DBACF346D39BE8BEBFD;
108 | projectDirPath = "";
109 | projectRoot = "";
110 | targets = (
111 | 2ADED8E42CE0E526008A4CFC /* Demo */,
112 | );
113 | };
114 | /* End PBXProject section */
115 |
116 | /* Begin PBXResourcesBuildPhase section */
117 | 2ADED8E32CE0E526008A4CFC /* Resources */ = {
118 | isa = PBXResourcesBuildPhase;
119 | buildActionMask = 2147483647;
120 | files = (
121 | );
122 | runOnlyForDeploymentPostprocessing = 0;
123 | };
124 | /* End PBXResourcesBuildPhase section */
125 |
126 | /* Begin PBXSourcesBuildPhase section */
127 | 2ADED8E12CE0E526008A4CFC /* Sources */ = {
128 | isa = PBXSourcesBuildPhase;
129 | buildActionMask = 2147483647;
130 | files = (
131 | );
132 | runOnlyForDeploymentPostprocessing = 0;
133 | };
134 | /* End PBXSourcesBuildPhase section */
135 |
136 | /* Begin XCBuildConfiguration section */
137 | 2ADED9082CE0E527008A4CFC /* Debug */ = {
138 | isa = XCBuildConfiguration;
139 | buildSettings = {
140 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
141 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
142 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
143 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements;
144 | CODE_SIGN_STYLE = Automatic;
145 | CURRENT_PROJECT_VERSION = 1;
146 | DEVELOPMENT_ASSET_PATHS = "";
147 | ENABLE_PREVIEWS = YES;
148 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
149 | GCC_C_LANGUAGE_STANDARD = gnu17;
150 | GENERATE_INFOPLIST_FILE = YES;
151 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
152 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
153 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
154 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
155 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
156 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
157 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
158 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
159 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
160 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
161 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
162 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
163 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
164 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
165 | MACOSX_DEPLOYMENT_TARGET = 15.0;
166 | MARKETING_VERSION = 1.0;
167 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
168 | MTL_FAST_MATH = YES;
169 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Demo;
170 | PRODUCT_NAME = "$(TARGET_NAME)";
171 | SDKROOT = auto;
172 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
173 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
174 | SWIFT_EMIT_LOC_STRINGS = YES;
175 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
176 | SWIFT_VERSION = 5.0;
177 | TARGETED_DEVICE_FAMILY = "1,2,7";
178 | XROS_DEPLOYMENT_TARGET = 2.1;
179 | };
180 | name = Debug;
181 | };
182 | 2ADED9092CE0E527008A4CFC /* Release */ = {
183 | isa = XCBuildConfiguration;
184 | buildSettings = {
185 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
186 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
188 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements;
189 | CODE_SIGN_STYLE = Automatic;
190 | CURRENT_PROJECT_VERSION = 1;
191 | DEVELOPMENT_ASSET_PATHS = "";
192 | ENABLE_PREVIEWS = YES;
193 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
194 | GCC_C_LANGUAGE_STANDARD = gnu17;
195 | GENERATE_INFOPLIST_FILE = YES;
196 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
197 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
198 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
199 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
200 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
201 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
202 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
203 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
204 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
205 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
206 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
207 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
208 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
209 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
210 | MACOSX_DEPLOYMENT_TARGET = 15.0;
211 | MARKETING_VERSION = 1.0;
212 | MTL_FAST_MATH = YES;
213 | PRODUCT_BUNDLE_IDENTIFIER = com.example.Demo;
214 | PRODUCT_NAME = "$(TARGET_NAME)";
215 | SDKROOT = auto;
216 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
217 | SWIFT_COMPILATION_MODE = wholemodule;
218 | SWIFT_EMIT_LOC_STRINGS = YES;
219 | SWIFT_VERSION = 5.0;
220 | TARGETED_DEVICE_FAMILY = "1,2,7";
221 | XROS_DEPLOYMENT_TARGET = 2.1;
222 | };
223 | name = Release;
224 | };
225 | 6391BA3C1ABAEEA9B49FEC4D /* Release */ = {
226 | isa = XCBuildConfiguration;
227 | buildSettings = {
228 | ALWAYS_SEARCH_USER_PATHS = NO;
229 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
230 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
231 | CLANG_ANALYZER_NONNULL = YES;
232 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
233 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
234 | CLANG_CXX_LIBRARY = "libc++";
235 | CLANG_ENABLE_MODULES = YES;
236 | CLANG_ENABLE_OBJC_ARC = YES;
237 | CLANG_ENABLE_OBJC_WEAK = YES;
238 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
239 | CLANG_WARN_BOOL_CONVERSION = YES;
240 | CLANG_WARN_COMMA = YES;
241 | CLANG_WARN_CONSTANT_CONVERSION = YES;
242 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
243 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
244 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
245 | CLANG_WARN_EMPTY_BODY = YES;
246 | CLANG_WARN_ENUM_CONVERSION = YES;
247 | CLANG_WARN_INFINITE_RECURSION = YES;
248 | CLANG_WARN_INT_CONVERSION = YES;
249 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
250 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
251 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
253 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
254 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
255 | CLANG_WARN_STRICT_PROTOTYPES = YES;
256 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
257 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
258 | CLANG_WARN_UNREACHABLE_CODE = YES;
259 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
260 | COPY_PHASE_STRIP = NO;
261 | DEAD_CODE_STRIPPING = YES;
262 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
263 | ENABLE_NS_ASSERTIONS = NO;
264 | ENABLE_STRICT_OBJC_MSGSEND = YES;
265 | GCC_C_LANGUAGE_STANDARD = gnu11;
266 | GCC_NO_COMMON_BLOCKS = YES;
267 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
268 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
269 | GCC_WARN_UNDECLARED_SELECTOR = YES;
270 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
271 | GCC_WARN_UNUSED_FUNCTION = YES;
272 | GCC_WARN_UNUSED_VARIABLE = YES;
273 | MTL_ENABLE_DEBUG_INFO = NO;
274 | PRODUCT_NAME = "$(TARGET_NAME)";
275 | VALIDATE_PRODUCT = YES;
276 | };
277 | name = Release;
278 | };
279 | 7CB849B0CEC51A876D32E730 /* Debug */ = {
280 | isa = XCBuildConfiguration;
281 | buildSettings = {
282 | ALWAYS_SEARCH_USER_PATHS = NO;
283 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
284 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
285 | CLANG_ANALYZER_NONNULL = YES;
286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
288 | CLANG_CXX_LIBRARY = "libc++";
289 | CLANG_ENABLE_MODULES = YES;
290 | CLANG_ENABLE_OBJC_ARC = YES;
291 | CLANG_ENABLE_OBJC_WEAK = YES;
292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
293 | CLANG_WARN_BOOL_CONVERSION = YES;
294 | CLANG_WARN_COMMA = YES;
295 | CLANG_WARN_CONSTANT_CONVERSION = YES;
296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
299 | CLANG_WARN_EMPTY_BODY = YES;
300 | CLANG_WARN_ENUM_CONVERSION = YES;
301 | CLANG_WARN_INFINITE_RECURSION = YES;
302 | CLANG_WARN_INT_CONVERSION = YES;
303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
307 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
309 | CLANG_WARN_STRICT_PROTOTYPES = YES;
310 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
312 | CLANG_WARN_UNREACHABLE_CODE = YES;
313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
314 | COPY_PHASE_STRIP = NO;
315 | DEAD_CODE_STRIPPING = YES;
316 | DEBUG_INFORMATION_FORMAT = dwarf;
317 | ENABLE_STRICT_OBJC_MSGSEND = YES;
318 | ENABLE_TESTABILITY = YES;
319 | GCC_C_LANGUAGE_STANDARD = gnu11;
320 | GCC_DYNAMIC_NO_PIC = NO;
321 | GCC_NO_COMMON_BLOCKS = YES;
322 | GCC_OPTIMIZATION_LEVEL = 0;
323 | GCC_PREPROCESSOR_DEFINITIONS = (
324 | "DEBUG=1",
325 | "$(inherited)",
326 | );
327 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
328 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
329 | GCC_WARN_UNDECLARED_SELECTOR = YES;
330 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
331 | GCC_WARN_UNUSED_FUNCTION = YES;
332 | GCC_WARN_UNUSED_VARIABLE = YES;
333 | MTL_ENABLE_DEBUG_INFO = YES;
334 | ONLY_ACTIVE_ARCH = YES;
335 | PRODUCT_NAME = "$(TARGET_NAME)";
336 | };
337 | name = Debug;
338 | };
339 | /* End XCBuildConfiguration section */
340 |
341 | /* Begin XCConfigurationList section */
342 | 0649CA3E896BAFA19C6914A0 /* Build configuration list for PBXProject "Demo" */ = {
343 | isa = XCConfigurationList;
344 | buildConfigurations = (
345 | 7CB849B0CEC51A876D32E730 /* Debug */,
346 | 6391BA3C1ABAEEA9B49FEC4D /* Release */,
347 | );
348 | defaultConfigurationIsVisible = 0;
349 | defaultConfigurationName = Release;
350 | };
351 | 2ADED9072CE0E527008A4CFC /* Build configuration list for PBXNativeTarget "Demo" */ = {
352 | isa = XCConfigurationList;
353 | buildConfigurations = (
354 | 2ADED9082CE0E527008A4CFC /* Debug */,
355 | 2ADED9092CE0E527008A4CFC /* Release */,
356 | );
357 | defaultConfigurationIsVisible = 0;
358 | defaultConfigurationName = Release;
359 | };
360 | /* End XCConfigurationList section */
361 |
362 | /* Begin XCSwiftPackageProductDependency section */
363 | 2ADED9142CE0E5A6008A4CFC /* SmoothRoundedRectangle */ = {
364 | isa = XCSwiftPackageProductDependency;
365 | productName = SmoothRoundedRectangle;
366 | };
367 | /* End XCSwiftPackageProductDependency section */
368 | };
369 | rootObject = 8C837CC0C17751C310B0A29D /* Project object */;
370 | }
371 |
--------------------------------------------------------------------------------