Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 60 additions & 48 deletions Examples/Sources/ControlsExample/ControlsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,71 +16,83 @@ struct ControlsApp: App {
@State var text = ""
@State var flavor: String? = nil
@State var enabled = true
@State var progressViewSize: Int = 10
@State var isProgressViewResizable = true

var body: some Scene {
WindowGroup("ControlsApp") {
#hotReloadable {
VStack(spacing: 30) {
VStack {
Text("Button")
Button("Click me!") {
count += 1
ScrollView {
VStack(spacing: 30) {
VStack {
Text("Button")
Button("Click me!") {
count += 1
}
Text("Count: \(count)")
}
Text("Count: \(count)")
}
.padding(.bottom, 20)
.padding(.bottom, 20)

#if !canImport(UIKitBackend)
VStack {
Text("Toggle button")
Toggle("Toggle me!", active: $exampleButtonState)
.toggleStyle(.button)
Text("Currently enabled: \(exampleButtonState)")
}
.padding(.bottom, 20)
#endif

#if !canImport(UIKitBackend)
VStack {
Text("Toggle button")
Toggle("Toggle me!", active: $exampleButtonState)
.toggleStyle(.button)
Text("Currently enabled: \(exampleButtonState)")
Text("Toggle switch")
Toggle("Toggle me:", active: $exampleSwitchState)
.toggleStyle(.switch)
Text("Currently enabled: \(exampleSwitchState)")
}
.padding(.bottom, 20)
#endif

VStack {
Text("Toggle switch")
Toggle("Toggle me:", active: $exampleSwitchState)
.toggleStyle(.switch)
Text("Currently enabled: \(exampleSwitchState)")
}
#if !canImport(UIKitBackend)
VStack {
Text("Checkbox")
Toggle("Toggle me:", active: $exampleCheckboxState)
.toggleStyle(.checkbox)
Text("Currently enabled: \(exampleCheckboxState)")
}
#endif

#if !canImport(UIKitBackend)
VStack {
Text("Checkbox")
Toggle("Toggle me:", active: $exampleCheckboxState)
.toggleStyle(.checkbox)
Text("Currently enabled: \(exampleCheckboxState)")
Text("Slider")
Slider($sliderValue, minimum: 0, maximum: 10)
.frame(maxWidth: 200)
Text("Value: \(String(format: "%.02f", sliderValue))")
}
#endif

VStack {
Text("Slider")
Slider($sliderValue, minimum: 0, maximum: 10)
.frame(maxWidth: 200)
Text("Value: \(String(format: "%.02f", sliderValue))")
}
VStack {
Text("Text field")
TextField("Text field", text: $text)
Text("Value: \(text)")
}

VStack {
Text("Text field")
TextField("Text field", text: $text)
Text("Value: \(text)")
}
Toggle("Enable ProgressView resizability", active: $isProgressViewResizable)
Slider($progressViewSize, minimum: 10, maximum: 100)
ProgressView()
.resizable(isProgressViewResizable)
.frame(width: progressViewSize, height: progressViewSize)

VStack {
Text("Drop down")
HStack {
Text("Flavor: ")
Picker(of: ["Vanilla", "Chocolate", "Strawberry"], selection: $flavor)
VStack {
Text("Drop down")
HStack {
Text("Flavor: ")
Picker(
of: ["Vanilla", "Chocolate", "Strawberry"], selection: $flavor)
}
Text("You chose: \(flavor ?? "Nothing yet!")")
}
Text("You chose: \(flavor ?? "Nothing yet!")")
}
}.padding().disabled(!enabled)
}.padding().disabled(!enabled)

Toggle(enabled ? "Disable all" : "Enable all", active: $enabled)
.padding()
Toggle(enabled ? "Disable all" : "Enable all", active: $enabled)
.padding()
}
.frame(minHeight: 600)
}
}.defaultSize(width: 400, height: 600)
}
Expand Down
32 changes: 31 additions & 1 deletion Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,15 @@ public final class AppKitBackend: AppBackend {
}

public func naturalSize(of widget: Widget) -> SIMD2<Int> {
if let spinner = widget.subviews.first as? NSProgressIndicator,
spinner.style == .spinning
{
let size = spinner.intrinsicContentSize
return SIMD2(
Int(size.width),
Int(size.height)
)
}
let size = widget.intrinsicContentSize
return SIMD2(
Int(size.width),
Expand Down Expand Up @@ -1181,11 +1190,32 @@ public final class AppKitBackend: AppBackend {
}

public func createProgressSpinner() -> Widget {
let container = NSView()
let spinner = NSProgressIndicator()
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.isIndeterminate = true
spinner.style = .spinning
spinner.startAnimation(nil)
return spinner
container.addSubview(spinner)
return container
}

public func setSize(
ofProgressSpinner widget: Widget,
to size: SIMD2<Int>
) {
guard Int(widget.frame.size.height) != size.y else { return }
setSize(of: widget, to: size)
let spinner = NSProgressIndicator()
spinner.translatesAutoresizingMaskIntoConstraints = false
spinner.isIndeterminate = true
spinner.style = .spinning
spinner.startAnimation(nil)
spinner.widthAnchor.constraint(equalToConstant: CGFloat(size.x)).isActive = true
spinner.heightAnchor.constraint(equalToConstant: CGFloat(size.y)).isActive = true

widget.subviews = []
widget.addSubview(spinner)
}

public func createProgressBar() -> Widget {
Expand Down
17 changes: 17 additions & 0 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,16 @@ public protocol AppBackend: Sendable {
/// Creates an indeterminate progress spinner.
func createProgressSpinner() -> Widget

/// Sets the size of a progress spinner.
///
/// This method exists because AppKitBackend requires special handling to resize progress spinners.
///
/// The default implementation forwards to ``AppBackend/setSize(of:to:)``.
func setSize(
ofProgressSpinner widget: Widget,
to size: SIMD2<Int>
)

/// Creates a progress bar.
func createProgressBar() -> Widget
/// Updates a progress bar to reflect the given progress (between 0 and 1), and the
Expand Down Expand Up @@ -1028,6 +1038,13 @@ extension AppBackend {
todo()
}

public func setSize(
ofProgressSpinner widget: Widget,
to size: SIMD2<Int>
) {
setSize(of: widget, to: size)
}

public func createProgressBar() -> Widget {
todo()
}
Expand Down
46 changes: 42 additions & 4 deletions Sources/SwiftCrossUI/Views/ProgressView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
private var label: Label
private var progress: Double?
private var kind: Kind
private var isSpinnerResizable: Bool = false

private enum Kind {
case spinner
Expand All @@ -23,7 +24,7 @@
private var progressIndicator: some View {
switch kind {
case .spinner:
ProgressSpinnerView()
ProgressSpinnerView(isResizable: isSpinnerResizable)
case .bar:
ProgressBarView(value: progress)
}
Expand All @@ -50,6 +51,14 @@
self.kind = .bar
self.progress = value.map(Double.init)
}

/// Makes the ProgressView resize to fit the available space.
/// Only affects ``Kind/spinner``.

Check warning on line 56 in Sources/SwiftCrossUI/Views/ProgressView.swift

View workflow job for this annotation

GitHub Actions / uikit (iPhone)

'Kind' doesn't exist at '/SwiftCrossUI/ProgressView/resizable(_:)'
public func resizable(_ isResizable: Bool = true) -> Self {
var progressView = self
progressView.isSpinnerResizable = isResizable
return progressView
}
}

extension ProgressView where Label == EmptyView {
Expand Down Expand Up @@ -101,7 +110,11 @@
}

struct ProgressSpinnerView: ElementaryView {
init() {}
let isResizable: Bool

init(isResizable: Bool = false) {
self.isResizable = isResizable
}

func asWidget<Backend: AppBackend>(backend: Backend) -> Backend.Widget {
backend.createProgressSpinner()
Expand All @@ -114,8 +127,33 @@
backend: Backend,
dryRun: Bool
) -> ViewUpdateResult {
ViewUpdateResult.leafView(
size: ViewSize(fixedSize: backend.naturalSize(of: widget))
let naturalSize = backend.naturalSize(of: widget)
guard isResizable else {
// Required to reset its size when resizability
// gets changed at runtime
backend.setSize(ofProgressSpinner: widget, to: naturalSize)
return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize))
}
let minimumDimension = max(min(proposedSize.x, proposedSize.y), 0)
let size = SIMD2(
minimumDimension,
minimumDimension
)
if !dryRun {
// Doesn't change the rendered size of ProgressSpinner
// on UIKitBackend, but still sets container size to
// (width: n, height: n) n = min(proposedSize.x, proposedSize.y)
backend.setSize(ofProgressSpinner: widget, to: size)
}
return ViewUpdateResult.leafView(
size: ViewSize(
size: size,
idealSize: naturalSize,
minimumWidth: 0,
minimumHeight: 0,
maximumWidth: nil,
maximumHeight: nil
)
)
}
}
Expand Down
Loading