Skip to content

Commit 116e909

Browse files
committed
Merge branch 'fix/ProgessSpinnerResizing' into usemylatestchanges
# Conflicts: # Examples/Sources/ControlsExample/ControlsApp.swift
2 parents df627ba + ea80229 commit 116e909

File tree

8 files changed

+148
-20
lines changed

8 files changed

+148
-20
lines changed

Examples/Sources/ControlsExample/ControlsApp.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,24 +68,32 @@ struct ControlsApp: App {
6868
}
6969
#endif
7070

71-
VStack {
72-
Text("Text field")
73-
TextField("Text field", text: $text)
74-
Text("Value: \(text)")
75-
}
71+
VStack {
72+
Text("Slider")
73+
Slider($sliderValue, minimum: 0, maximum: 10)
74+
.frame(maxWidth: 200)
75+
Text("Value: \(String(format: "%.02f", sliderValue))")
76+
}
7677

77-
Toggle("Enable ProgressView resizability", active: $isProgressViewResizable)
78-
Slider($progressViewSize, minimum: 10, maximum: 100)
79-
ProgressView()
80-
.resizable(isProgressViewResizable)
81-
.frame(width: progressViewSize, height: progressViewSize)
78+
VStack {
79+
Text("Text field")
80+
TextField("Text field", text: $text)
81+
Text("Value: \(text)")
82+
}
8283

8384
VStack {
8485
Text("Drop down")
8586
HStack {
8687
Text("Flavor: ")
8788
Picker(of: ["Vanilla", "Chocolate", "Strawberry"], selection: $flavor)
89+
Toggle("Enable ProgressView resizability", active: $isProgressViewResizable)
90+
Slider($progressViewSize, minimum: 10, maximum: 100)
91+
Button("Randomize progress view size") {
92+
progressViewSize = Int.random(in: 10...100)
8893
}
94+
ProgressView()
95+
.resizable(isProgressViewResizable)
96+
.frame(width: progressViewSize, height: progressViewSize)
8997

9098
VStack {
9199
Text("Drop down")
@@ -115,7 +123,6 @@ struct ControlsApp: App {
115123
}
116124
.frame(minHeight: 600)
117125
}
118-
119126
}.defaultSize(width: 400, height: 600)
120127
}
121128
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,15 @@ public final class AppKitBackend: AppBackend {
484484
}
485485

486486
public func naturalSize(of widget: Widget) -> SIMD2<Int> {
487+
if let spinner = widget.subviews.first as? NSProgressIndicator,
488+
spinner.style == .spinning
489+
{
490+
let size = spinner.intrinsicContentSize
491+
return SIMD2(
492+
Int(size.width),
493+
Int(size.height)
494+
)
495+
}
487496
let size = widget.intrinsicContentSize
488497
return SIMD2(
489498
Int(size.width),
@@ -1189,11 +1198,32 @@ public final class AppKitBackend: AppBackend {
11891198
}
11901199

11911200
public func createProgressSpinner() -> Widget {
1201+
let container = NSView()
1202+
let spinner = NSProgressIndicator()
1203+
spinner.translatesAutoresizingMaskIntoConstraints = false
1204+
spinner.isIndeterminate = true
1205+
spinner.style = .spinning
1206+
spinner.startAnimation(nil)
1207+
container.addSubview(spinner)
1208+
return container
1209+
}
1210+
1211+
public func setProgressSpinnerSize(
1212+
_ widget: Widget,
1213+
_ size: SIMD2<Int>
1214+
) {
1215+
guard Int(widget.frame.size.height) != size.y else { return }
1216+
setSize(of: widget, to: size)
11921217
let spinner = NSProgressIndicator()
1218+
spinner.translatesAutoresizingMaskIntoConstraints = false
11931219
spinner.isIndeterminate = true
11941220
spinner.style = .spinning
11951221
spinner.startAnimation(nil)
1196-
return spinner
1222+
spinner.widthAnchor.constraint(equalToConstant: CGFloat(size.x)).isActive = true
1223+
spinner.heightAnchor.constraint(equalToConstant: CGFloat(size.y)).isActive = true
1224+
1225+
widget.subviews = []
1226+
widget.addSubview(spinner)
11971227
}
11981228

11991229
public func createProgressBar() -> Widget {

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,15 @@ public protocol AppBackend: Sendable {
541541
/// Creates an indeterminate progress spinner.
542542
func createProgressSpinner() -> Widget
543543

544+
/// Changes the Spinner's Size.
545+
/// Required due to AppKitBackend needing special treatment.
546+
/// Forward to ``AppBackend/setSize(of widget: Widget, to size: SIMD2<Int>)``
547+
/// on other Backends.
548+
func setProgressSpinnerSize(
549+
_ widget: Widget,
550+
_ size: SIMD2<Int>
551+
)
552+
544553
/// Creates a progress bar.
545554
func createProgressBar() -> Widget
546555
/// Updates a progress bar to reflect the given progress (between 0 and 1), and the
@@ -1028,6 +1037,13 @@ extension AppBackend {
10281037
todo()
10291038
}
10301039

1040+
public func setProgressSpinnerSize(
1041+
_ widget: Widget,
1042+
_ size: SIMD2<Int>
1043+
) {
1044+
setSize(of: widget, to: size)
1045+
}
1046+
10311047
public func createProgressBar() -> Widget {
10321048
todo()
10331049
}

Sources/SwiftCrossUI/Environment/Actions/PresentSingleFileOpenDialogAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Foundation
33
/// Presents an 'Open file' dialog fit for selecting a single file. Some
44
/// backends only allow selecting either files or directories but not both
55
/// in a single dialog. Returns `nil` if the user cancels the operation.
6+
@available(tvOS, unavailable, message: "tvOS does not provide file system access")
67
public struct PresentSingleFileOpenDialogAction: Sendable {
78
let backend: any AppBackend
89
let window: MainActorBox<Any?>

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public struct EnvironmentValues {
138138
/// can decide whether to make it an app modal, a standalone window, or a
139139
/// window of its choosing).
140140
@MainActor
141+
@available(tvOS, unavailable, message: "tvOS does not provide file system access")
141142
public var chooseFile: PresentSingleFileOpenDialogAction {
142143
return PresentSingleFileOpenDialogAction(
143144
backend: backend,

Sources/SwiftCrossUI/Views/ProgressView.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public struct ProgressView<Label: View>: View {
5353
}
5454

5555
/// Makes the ProgressView resize to fit the available space.
56-
/// Only affects `Kind.spinner`.
56+
/// Only affects ``Kind/spinner``.
5757
public func resizable(_ isResizable: Bool = true) -> Self {
5858
var progressView = self
5959
progressView.isSpinnerResizable = isResizable
@@ -111,6 +111,7 @@ extension ProgressView where Label == Text {
111111

112112
struct ProgressSpinnerView: ElementaryView {
113113
let isResizable: Bool
114+
114115
init(isResizable: Bool = false) {
115116
self.isResizable = isResizable
116117
}
@@ -130,26 +131,26 @@ struct ProgressSpinnerView: ElementaryView {
130131
guard isResizable else {
131132
// Required to reset its size when resizability
132133
// gets changed at runtime
133-
backend.setSize(of: widget, to: naturalSize)
134+
backend.setProgressSpinnerSize(widget, naturalSize)
134135
return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize))
135136
}
136-
let min = max(min(proposedSize.x, proposedSize.y), 10)
137+
let minimumDimension = max(min(proposedSize.x, proposedSize.y), 0)
137138
let size = SIMD2(
138-
min,
139-
min
139+
minimumDimension,
140+
minimumDimension
140141
)
141142
if !dryRun {
142143
// Doesn't change the rendered size of ProgressSpinner
143144
// on UIKitBackend, but still sets container size to
144145
// (width: n, height: n) n = min(proposedSize.x, proposedSize.y)
145-
backend.setSize(of: widget, to: size)
146+
backend.setProgressSpinnerSize(widget, size)
146147
}
147148
return ViewUpdateResult.leafView(
148149
size: ViewSize(
149150
size: size,
150151
idealSize: naturalSize,
151-
minimumWidth: 10,
152-
minimumHeight: 10,
152+
minimumWidth: 0,
153+
minimumHeight: 0,
153154
maximumWidth: nil,
154155
maximumHeight: nil
155156
)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#if !os(tvOS)
2+
import SwiftCrossUI
3+
import UIKit
4+
5+
extension UIKitBackend {
6+
final class FilePickerDelegate: NSObject, UIDocumentPickerDelegate {
7+
var resultHandler: ((DialogResult<[URL]>) -> Void)
8+
9+
init(resultHandler: @escaping (DialogResult<[URL]>) -> Void) {
10+
self.resultHandler = resultHandler
11+
}
12+
13+
func documentPicker(
14+
_ controller: UIDocumentPickerViewController,
15+
didPickDocumentsAt urls: [URL]
16+
) {
17+
resultHandler(.success(urls))
18+
}
19+
20+
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
21+
resultHandler(.cancelled)
22+
}
23+
}
24+
25+
public func showOpenDialog(
26+
fileDialogOptions: FileDialogOptions,
27+
openDialogOptions: OpenDialogOptions,
28+
window: UIWindow?,
29+
resultHandler handleResult: @escaping (DialogResult<[URL]>) -> Void
30+
) {
31+
var allowedTypes: [String] = []
32+
33+
if openDialogOptions.allowSelectingDirectories {
34+
allowedTypes.append("public.directory")
35+
}
36+
37+
// TODO(#235): Respect fileDialogOptions.allowedContentTypes and fileDialogOptions.allowOtherContentTypes
38+
if openDialogOptions.allowSelectingFiles {
39+
allowedTypes.append("public.data")
40+
}
41+
42+
let pickerController = UIDocumentPickerViewController(
43+
documentTypes: allowedTypes,
44+
in: .import
45+
)
46+
47+
pickerController.allowsMultipleSelection = openDialogOptions.allowMultipleSelections
48+
pickerController.directoryURL = fileDialogOptions.initialDirectory
49+
50+
pickerController.shouldShowFileExtensions =
51+
fileDialogOptions.allowOtherContentTypes
52+
|| fileDialogOptions.allowedContentTypes.count > 1
53+
54+
let delegate = FilePickerDelegate(resultHandler: handleResult)
55+
pickerController.delegate = delegate
56+
self.filePickerDelegates.setObject(delegate, forKey: pickerController)
57+
58+
guard let window = window ?? Self.mainWindow else {
59+
fatalError(
60+
"Attempting to present an open dialog before any windows have been created"
61+
)
62+
}
63+
64+
window.rootViewController!.present(pickerController, animated: true)
65+
}
66+
}
67+
#endif

Sources/UIKitBackend/UIKitBackend.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public final class UIKitBackend: AppBackend {
4646

4747
private let appDelegateClass: ApplicationDelegate.Type
4848

49+
#if !os(tvOS)
50+
let filePickerDelegates = NSMapTable<UIDocumentPickerViewController, FilePickerDelegate>
51+
.weakToStrongObjects()
52+
#endif
53+
4954
public init() {
5055
self.appDelegateClass = ApplicationDelegate.self
5156
}

0 commit comments

Comments
 (0)