Skip to content

Commit ef2e87c

Browse files
committed
Add sample project and convenience initializers
1 parent 1bd9619 commit ef2e87c

7 files changed

Lines changed: 242 additions & 24 deletions

File tree

Example/CircularControlExample/ContentView.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,25 @@ import SwiftUI
99

1010
struct ContentView: View {
1111
var body: some View {
12-
VStack {
13-
Image(systemName: "globe")
14-
.imageScale(.large)
15-
.foregroundStyle(.tint)
16-
Text("Hello, world!")
12+
NavigationSplitView {
13+
List {
14+
NavigationLink("Simple Progress") {
15+
SimpleDemoView()
16+
.navigationTitle("Simple Progress")
17+
.toolbarTitleDisplayMode(.inline)
18+
}
19+
20+
NavigationLink("Editable Control") {
21+
EditableDemoView()
22+
.navigationTitle("Editable Control")
23+
.toolbarTitleDisplayMode(.inline)
24+
}
25+
}
26+
#if os(macOS)
27+
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
28+
#endif
29+
} detail: {
30+
Text("Select an item")
1731
}
18-
.padding()
1932
}
2033
}
21-
22-
#Preview {
23-
ContentView()
24-
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// DynamicView.swift
3+
// CircularControlExample
4+
//
5+
// Created by Phil Zakharchenko on 12/24/24.
6+
//
7+
8+
import SwiftUI
9+
10+
/// A view that switches between horizontal and vertical layouts depending on the horizontal size class and fitting size.
11+
struct DynamicView<Content: View>: View {
12+
@ViewBuilder let contentView: Content
13+
14+
#if !os(macOS)
15+
@Environment(\.horizontalSizeClass) var horizontalSizeClass
16+
#else
17+
enum SizeClass {
18+
case compact
19+
case regular
20+
}
21+
22+
private let horizontalSizeClass: SizeClass = .regular
23+
#endif
24+
25+
var body: some View {
26+
ViewThatFits {
27+
switch horizontalSizeClass {
28+
case .regular:
29+
HStack {
30+
contentView
31+
}
32+
.padding()
33+
34+
VStack {
35+
contentView
36+
}
37+
.padding()
38+
default:
39+
VStack {
40+
contentView
41+
}
42+
.padding()
43+
44+
HStack {
45+
contentView
46+
}
47+
.padding()
48+
}
49+
}
50+
}
51+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// EditableDemoView.swift
3+
// CircularControlExample
4+
//
5+
// Created by Phil Zakharchenko on 12/24/24.
6+
//
7+
8+
import SwiftUI
9+
import PZCircularControl
10+
11+
struct EditableDemoView: View {
12+
@State private var isDisabled: Bool = false
13+
@State private var allowsWrapping: Bool = false
14+
15+
@State private var firstProgress: Double = 0.4
16+
@State private var secondProgress: Double = 0.75
17+
18+
var body: some View {
19+
VStack {
20+
GroupBox {
21+
Toggle("Disabled", isOn: $isDisabled)
22+
Toggle("Wraps Around", isOn: $allowsWrapping)
23+
.disabled(isDisabled)
24+
}
25+
26+
DynamicView {
27+
contentView
28+
.circularControlAllowsWrapping(allowsWrapping)
29+
}
30+
.frame(maxHeight: .infinity, alignment: .center)
31+
}
32+
.padding()
33+
}
34+
35+
@ViewBuilder
36+
private var contentView: some View {
37+
CircularControl(progress: $firstProgress)
38+
.padding()
39+
.disabled(isDisabled)
40+
41+
CircularControl(progress: $secondProgress, strokeWidth: 30, style: .init(
42+
track: Color.indigo.opacity(0.2),
43+
progress: LinearGradient(
44+
colors: [.mint, .blue],
45+
startPoint: .topLeading,
46+
endPoint: .bottomTrailing
47+
),
48+
shadow: .init(color: .mint.opacity(0.6), radius: 12)
49+
))
50+
.fontDesign(.rounded)
51+
.circularControlKnobScale(1)
52+
.disabled(isDisabled)
53+
.padding()
54+
55+
CircularControl(
56+
progress: $secondProgress,
57+
strokeWidth: 25,
58+
style: .init(
59+
track: Color.cyan.opacity(0.2),
60+
progress: Color.cyan
61+
),
62+
format: .custom({ value in "\(Int(value * 10)) / 10" })
63+
)
64+
.disabled(isDisabled)
65+
.padding()
66+
}
67+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// SimpleDemoView.swift
3+
// CircularControlExample
4+
//
5+
// Created by Phil Zakharchenko on 12/24/24.
6+
//
7+
8+
import SwiftUI
9+
import PZCircularControl
10+
11+
struct SimpleDemoView: View {
12+
var body: some View {
13+
DynamicView {
14+
contentView
15+
}
16+
}
17+
18+
@ViewBuilder
19+
private var contentView: some View {
20+
CircularControl(progress: 0.4)
21+
.padding()
22+
23+
CircularControl(progress: 0.63, strokeWidth: 30, style: .init(
24+
track: Color.indigo.opacity(0.2),
25+
progress: LinearGradient(
26+
colors: [.teal, .blue],
27+
startPoint: .topLeading,
28+
endPoint: .bottomTrailing
29+
),
30+
shadow: .init(color: .teal.opacity(0.6), radius: 12)
31+
), format: .fraction)
32+
.fontDesign(.serif)
33+
.padding()
34+
35+
CircularControl(
36+
progress: 0.75,
37+
strokeWidth: 25,
38+
style: .init(
39+
track: Color.orange.opacity(0.2),
40+
progress: Color.orange
41+
)
42+
) {
43+
Image(systemName: "star.fill")
44+
.font(.largeTitle)
45+
.foregroundStyle(.orange)
46+
}
47+
.padding()
48+
}
49+
}

Sources/PZCircularControl/CircularControl+Track.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ extension CircularControl {
6363
.padding(strokeWidth / 2)
6464
.onChange(of: progress) { oldValue, newValue in
6565
if !isDragging {
66-
withAnimation(.snappy) {
67-
currentProgress = newValue
68-
}
66+
currentProgress = newValue
6967
}
7068
}
7169
.onAppear {

Sources/PZCircularControl/CircularControl.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,39 @@ public struct CircularControl<Label: View, TrackStyle: ShapeStyle, ProgressStyle
2424
public init(
2525
progress: Double,
2626
isEditable: Bool = false,
27-
strokeWidth: CGFloat = 20,
27+
strokeWidth: CGFloat = .defaultStrokeWidth,
2828
style: CircularControlStyle<TrackStyle, ProgressStyle, KnobStyle> = .init(),
2929
onProgressChange: ((Double) -> Void)? = nil,
3030
@ViewBuilder label: () -> Label
3131
) {
32-
self.progress = progress.clamped(to: 0...1)
33-
self._currentProgress = State(initialValue: progress.clamped(to: 0...1))
32+
let initialProgress = progress.clamped(to: 0...1)
33+
self.progress = initialProgress
34+
self._currentProgress = State(initialValue: initialProgress)
3435
self.isEditable = isEditable
3536
self.strokeWidth = strokeWidth
3637
self.strokeStyle = style
3738
self.onProgressChange = onProgressChange
3839
self.label = label()
3940
}
4041

42+
public init(
43+
progress: Binding<Double>,
44+
strokeWidth: CGFloat = .defaultStrokeWidth,
45+
style: CircularControlStyle<TrackStyle, ProgressStyle, KnobStyle> = .init(),
46+
@ViewBuilder label: () -> Label
47+
) {
48+
let initialProgress = progress.wrappedValue.clamped(to: 0...1)
49+
self.progress = initialProgress
50+
self._currentProgress = State(initialValue: initialProgress)
51+
self.isEditable = true
52+
self.strokeWidth = strokeWidth
53+
self.strokeStyle = style
54+
self.onProgressChange = { newValue in
55+
progress.wrappedValue = newValue.clamped(to: 0...1)
56+
}
57+
self.label = label()
58+
}
59+
4160
public var body: some View {
4261
Track(
4362
progress: currentProgress,
@@ -61,11 +80,11 @@ public struct CircularControl<Label: View, TrackStyle: ShapeStyle, ProgressStyle
6180

6281
// MARK: - Default Label Convenience Initializer
6382

64-
public extension CircularControl where Label == DefaultLabel {
65-
init(
83+
extension CircularControl where Label == DefaultLabel {
84+
public init(
6685
progress: Double,
6786
isEditable: Bool = false,
68-
strokeWidth: CGFloat = 20,
87+
strokeWidth: CGFloat = .defaultStrokeWidth,
6988
style: CircularControlStyle<TrackStyle, ProgressStyle, KnobStyle> = .init(),
7089
format: DefaultLabelFormat = .percentage,
7190
onProgressChange: ((Double) -> Void)? = nil
@@ -80,6 +99,26 @@ public extension CircularControl where Label == DefaultLabel {
8099
DefaultLabel(format: format)
81100
}
82101
}
102+
103+
public init(
104+
progress: Binding<Double>,
105+
strokeWidth: CGFloat = .defaultStrokeWidth,
106+
style: CircularControlStyle<TrackStyle, ProgressStyle, KnobStyle> = .init(),
107+
format: DefaultLabelFormat = .percentage
108+
) {
109+
self.init(
110+
progress: progress,
111+
strokeWidth: strokeWidth,
112+
style: style
113+
) {
114+
DefaultLabel(format: format)
115+
}
116+
}
117+
}
118+
119+
extension CGFloat {
120+
@usableFromInline
121+
static let defaultStrokeWidth: CGFloat = 20
83122
}
84123

85124
// MARK: - Xcode Previews

Sources/PZCircularControl/DefaultLabel.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ public struct DefaultLabel: View {
1313
@Environment(\.circularControlProgress) private var progress
1414

1515
public var body: some View {
16-
Text(format.string(from: progress))
17-
.font(.system(.title, weight: .semibold).monospacedDigit())
18-
.foregroundStyle(.primary)
19-
.animation(.snappy, value: progress)
20-
.contentTransition(.numericText())
16+
ViewThatFits {
17+
Text(format.string(from: progress))
18+
.font(.system(.title, weight: .semibold).monospacedDigit())
19+
.fixedSize()
20+
.foregroundStyle(.primary)
21+
.animation(.snappy, value: progress)
22+
.contentTransition(.numericText())
23+
24+
Text("")
25+
}
2126
}
2227
}
2328

0 commit comments

Comments
 (0)