diff --git a/.github/workflows/swift-uikit.yml b/.github/workflows/swift-uikit.yml new file mode 100644 index 00000000000..5f3d5d826d4 --- /dev/null +++ b/.github/workflows/swift-uikit.yml @@ -0,0 +1,61 @@ +name: Build UIKitBackend + +on: + push: + branches-ignore: + - 'gh-pages' + pull_request: + branches-ignore: + - 'gh-pages' + +jobs: + build-uikit: + runs-on: macos-14 + strategy: + matrix: + devicetype: + - iPhone + - iPad + - TV + steps: + - name: Force Xcode 15.4 + run: sudo xcode-select -switch /Applications/Xcode_15.4.app + - uses: actions/checkout@v3 + - name: Build + run: | + set -uo pipefail + devicetype=${{ matrix.devicetype }} + set +e + deviceid=$(xcrun simctl list devices $devicetype available | grep -v -- -- | tail -n 1 | grep -oE '[0-9A-F\-]{36}') + if [ $? -eq 0 ]; then + set -e + ( + buildtarget () { + xcodebuild -scheme "$1" -destination "id=$deviceid" build + } + + buildtarget SwiftCrossUI + buildtarget UIKitBackend + + cd Examples + + buildtarget CounterExample + buildtarget GreetingGeneratorExample + buildtarget NavigationExample + buildtarget StressTestExample + buildtarget NotesExample + + if [ $devicetype != TV ]; then + # Slider is not implemented for tvOS + buildtarget ControlsExample + buildtarget RandomNumberGeneratorExample + fi + + if [ $devicetype = iPad ]; then + # NavigationSplitView is only implemented for iPad + buildtarget SplitExample + fi + ) + else + echo "No $devicetype simulators found" >&2 + fi diff --git a/Sources/UIKitBackend/KeyboardToolbar.swift b/Sources/UIKitBackend/KeyboardToolbar.swift index 2bd64c0cd0d..bc0056e0d5b 100644 --- a/Sources/UIKitBackend/KeyboardToolbar.swift +++ b/Sources/UIKitBackend/KeyboardToolbar.swift @@ -8,6 +8,7 @@ import UIKit /// items will not cause the toolbar to be updated. The toolbar is only updated when the view /// containing the ``View/keyboardToolbar(animateChanges:body:)`` modifier is updated, so any /// state necessary for the toolbar should live in the view itself. +@available(tvOS, unavailable) public protocol ToolbarItem { /// The type of bar button item used to represent this item in UIKit. associatedtype ItemType: UIBarButtonItem @@ -19,6 +20,7 @@ public protocol ToolbarItem { func updateBarButtonItem(_ item: inout ItemType) } +@available(tvOS, unavailable) @resultBuilder public enum ToolbarBuilder { public enum Component { @@ -56,6 +58,7 @@ public enum ToolbarBuilder { } } +@available(tvOS, unavailable) extension Button: ToolbarItem { public final class ItemType: UIBarButtonItem { var callback: () -> Void @@ -90,7 +93,11 @@ extension Button: ToolbarItem { } } -@available(iOS 14, macCatalyst 14, tvOS 14, *) +// Despite the fact that this is unavailable on tvOS, the `introduced: 14` +// clause is required for all current Swift versions to accept it. +// See https://forums.swift.org/t/contradictory-available-s-are-required/78831 +@available(iOS 14, macCatalyst 14, *) +@available(tvOS, unavailable, introduced: 14) extension Spacer: ToolbarItem { public func createBarButtonItem() -> UIBarButtonItem { if let minLength, minLength > 0 { @@ -110,6 +117,7 @@ extension Spacer: ToolbarItem { } } +@available(tvOS, unavailable) struct FixedWidthToolbarItem: ToolbarItem { var base: Base var width: Int? @@ -131,7 +139,8 @@ struct FixedWidthToolbarItem: ToolbarItem { } // Setting width on a flexible space is ignored, you must use a fixed space from the outset -@available(iOS 14, macCatalyst 14, tvOS 14, *) +@available(iOS 14, macCatalyst 14, *) +@available(tvOS, unavailable, introduced: 14) struct FixedWidthSpacerItem: ToolbarItem { var width: Int? @@ -148,6 +157,7 @@ struct FixedWidthSpacerItem: ToolbarItem { } } +@available(tvOS, unavailable) struct ColoredToolbarItem: ToolbarItem { var base: Base var color: Color @@ -164,13 +174,14 @@ struct ColoredToolbarItem: ToolbarItem { } } +@available(tvOS, unavailable) extension ToolbarItem { /// A toolbar item with the specified width. /// /// If `width` is positive, the item will have that exact width. If `width` is zero or /// nil, the item will have its natural size. public func frame(width: Int?) -> any ToolbarItem { - if #available(iOS 14, macCatalyst 14, tvOS 14, *), + if #available(iOS 14, macCatalyst 14, *), self is Spacer || self is FixedWidthSpacerItem { FixedWidthSpacerItem(width: width) @@ -185,6 +196,7 @@ extension ToolbarItem { } } +@available(tvOS, unavailable) indirect enum ToolbarItemLocation: Hashable { case expression(inside: ToolbarItemLocation?) case block(index: Int, inside: ToolbarItemLocation?) @@ -194,6 +206,7 @@ indirect enum ToolbarItemLocation: Hashable { case eitherSecond(inside: ToolbarItemLocation?) } +@available(tvOS, unavailable) final class KeyboardToolbar: UIToolbar { var locations: [ToolbarItemLocation: UIBarButtonItem] = [:] @@ -205,7 +218,7 @@ final class KeyboardToolbar: UIToolbar { var newLocations: [ToolbarItemLocation: UIBarButtonItem] = [:] visitItems(component: components, inside: nil) { location, expression in - var item = + let item = if let oldItem = locations[location] { updateErasedItem(expression, oldItem) } else { @@ -270,10 +283,12 @@ final class KeyboardToolbar: UIToolbar { } } +@available(tvOS, unavailable) enum ToolbarKey: EnvironmentKey { static let defaultValue: ((KeyboardToolbar) -> Void)? = nil } +@available(tvOS, unavailable) extension EnvironmentValues { var updateToolbar: ((KeyboardToolbar) -> Void)? { get { self[ToolbarKey.self] } @@ -287,6 +302,7 @@ extension View { /// - animateChanges: Whether to animate updates when an item is added, removed, or /// updated /// - body: The toolbar's contents + @available(tvOS, unavailable) public func keyboardToolbar( animateChanges: Bool = true, @ToolbarBuilder body: @escaping () -> ToolbarBuilder.FinalResult diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index e37fa81abd2..69b572cbe7b 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -227,14 +227,17 @@ extension UIKitBackend { textFieldWidget.onChange = onChange textFieldWidget.onSubmit = onSubmit - if let updateToolbar = environment.updateToolbar { - let toolbar = - (textFieldWidget.child.inputAccessoryView as? KeyboardToolbar) ?? KeyboardToolbar() - updateToolbar(toolbar) - textFieldWidget.child.inputAccessoryView = toolbar - } else { - textFieldWidget.child.inputAccessoryView = nil - } + #if os(iOS) + if let updateToolbar = environment.updateToolbar { + let toolbar = + (textFieldWidget.child.inputAccessoryView as? KeyboardToolbar) + ?? KeyboardToolbar() + updateToolbar(toolbar) + textFieldWidget.child.inputAccessoryView = toolbar + } else { + textFieldWidget.child.inputAccessoryView = nil + } + #endif } public func setContent(ofTextField textField: Widget, to content: String) {