فهرست منبع

toolbar: workaround SwiftUI issues on < iOS 15

osy 3 سال پیش
والد
کامیت
a19ed403ce

+ 44 - 0
Platform/Shared/MenuLabel.swift

@@ -0,0 +1,44 @@
+//
+// Copyright © 2022 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import SwiftUI
+
+struct MenuLabel: View {
+    private let label: Label<Text, Image>
+    
+    init(_ titleKey: LocalizedStringKey, systemImage name: String) {
+        label = Label(titleKey, systemImage: name)
+    }
+    
+    init<S>(_ title: S, systemImage name: String) where S : StringProtocol {
+        label = Label(title, systemImage: name)
+    }
+    
+    var body: some View {
+        if #available(iOS 15, *) {
+            label
+        } else {
+            // prior to iOS 15, menu with title and icon doesn't show up
+            label.labelStyle(.titleOnly)
+        }
+    }
+}
+
+struct MenuLabel_Previews: PreviewProvider {
+    static var previews: some View {
+        MenuLabel("Test", systemImage: "face")
+    }
+}

+ 9 - 9
Platform/iOS/VMToolbarDisplayMenuView.swift

@@ -24,43 +24,43 @@ struct VMToolbarDisplayMenuView: View {
         Menu {
             Menu {
                 Picker("", selection: $state.device) {
-                    Label("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
+                    MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
                     ForEach(session.devices) { device in
                         switch device {
                         case .serial(_, let index):
-                            Label("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "cable.connector").tag(device as VMWindowState.Device?)
+                            MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "cable.connector").tag(device as VMWindowState.Device?)
                         case .display(_, let index):
-                            Label("Display \(index): \(session.qemuConfig.displays[index].hardware.prettyValue)", systemImage: "display").tag(device as VMWindowState.Device?)
+                            MenuLabel("Display \(index): \(session.qemuConfig.displays[index].hardware.prettyValue)", systemImage: "display").tag(device as VMWindowState.Device?)
                         }
                     }
                 }
             } label: {
-                Label("Current Window", systemImage: "rectangle.inset.filled.on.rectangle")
+                MenuLabel("Current Window", systemImage: "rectangle.inset.filled.on.rectangle")
             }
             if let externalWindowBinding = session.externalWindowBinding {
                 Menu {
                     Button {
                         externalWindowBinding.wrappedValue.toggleDisplayResize()
                     } label: {
-                        Label("Zoom/Reset", systemImage: externalWindowBinding.isViewportChanged.wrappedValue ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right")
+                        MenuLabel("Zoom/Reset", systemImage: externalWindowBinding.isViewportChanged.wrappedValue ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right")
                     }
                     Picker("", selection: externalWindowBinding.device) {
-                        Label("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
+                        MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?)
                         ForEach(session.devices) { device in
                             if case .display(_, let index) = device {
-                                Label("Display \(index): \(session.qemuConfig.displays[index].hardware.prettyValue)", systemImage: "display").tag(device as VMWindowState.Device?)
+                                MenuLabel("Display \(index): \(session.qemuConfig.displays[index].hardware.prettyValue)", systemImage: "display").tag(device as VMWindowState.Device?)
                             }
                         }
                     }
                 } label: {
-                    Label("External Monitor", systemImage: "rectangle.on.rectangle")
+                    MenuLabel("External Monitor", systemImage: "rectangle.on.rectangle")
                 }
             }
             Divider()
             Button {
                 UIApplication.shared.requestSceneSessionActivation(nil, userActivity: nil, options: nil, errorHandler: nil)
             } label: {
-                Label("New Window…", systemImage: "plus.rectangle.on.rectangle")
+                MenuLabel("New Window…", systemImage: "plus.rectangle.on.rectangle")
             }
 
         } label: {

+ 4 - 4
Platform/iOS/VMToolbarDriveMenuView.swift

@@ -31,20 +31,20 @@ struct VMToolbarDriveMenuView: View {
                             selectedDrive = legacyDrive
                             isFileImporterShown.toggle()
                         } label: {
-                            Label("Change…", systemImage: "opticaldisc")
+                            MenuLabel("Change…", systemImage: "opticaldisc")
                         }
                         Button {
                             ejectDriveImage(for: legacyDrive)
                         } label: {
-                            Label("Eject…", systemImage: "eject")
+                            MenuLabel("Eject…", systemImage: "eject")
                         }
                     } label: {
-                        Label(legacyDrive.label, systemImage: legacyDrive.status == .ejected ? "opticaldiscdrive" : "opticaldiscdrive.fill")
+                        MenuLabel(legacyDrive.label, systemImage: legacyDrive.status == .ejected ? "opticaldiscdrive" : "opticaldiscdrive.fill")
                     }
                 } else {
                     Button {
                     } label: {
-                        Label(legacyDrive.label, systemImage: "internaldrive")
+                        MenuLabel(legacyDrive.label, systemImage: "internaldrive")
                     }.disabled(true)
                 }
             }

+ 1 - 1
Platform/iOS/VMToolbarUSBMenuView.swift

@@ -34,7 +34,7 @@ struct VMToolbarUSBMenuView: View {
                             session.connectDevice(usbDevice)
                         }
                     } label: {
-                        Label(usbDevice.name ?? usbDevice.description, systemImage: connected ? "checkmark.circle.fill" : "")
+                        MenuLabel(usbDevice.name ?? usbDevice.description, systemImage: connected ? "checkmark.circle.fill" : "")
                     }
                 }
             }

+ 33 - 5
Platform/iOS/VMToolbarView.swift

@@ -123,7 +123,7 @@ struct VMToolbarView: View {
                 } label: {
                     Label("Keyboard", systemImage: "keyboard")
                 }.offset(offset(for: 1))
-            }.buttonStyle(.toolbar)
+            }.buttonStyle(.toolbar(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass))
             .menuStyle(.toolbar)
             .disabled(state.isBusy)
             .opacity(isCollapsed ? 0 : 1)
@@ -138,7 +138,7 @@ struct VMToolbarView: View {
                 }
             } label: {
                 Label("Hide", systemImage: isCollapsed ? nameOfHideIcon : nameOfShowIcon)
-            }.buttonStyle(.toolbar)
+            }.buttonStyle(.toolbar(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass))
             .opacity(toolbarToggleOpacity)
             .modifier(Shake(shake: shake))
             .position(position(for: geometry))
@@ -249,14 +249,15 @@ protocol ToolbarButtonBaseStyle<Label, Content> {
     associatedtype Label: View
     associatedtype Content: View
     
+    var horizontalSizeClass: UserInterfaceSizeClass? { get }
+    var verticalSizeClass: UserInterfaceSizeClass? { get }
+    
     func makeBodyBase(label: Label, isPressed: Bool) -> Content
 }
 
 extension ToolbarButtonBaseStyle {
     private var size: CGFloat {
-        let horizontalSizeClass = UITraitCollection.current.horizontalSizeClass
-        let verticalSizeClass = UITraitCollection.current.verticalSizeClass
-        return (horizontalSizeClass == .compact || verticalSizeClass == .compact) ? 32 : 48
+        (horizontalSizeClass == .compact || verticalSizeClass == .compact) ? 32 : 48
     }
     
     func makeBodyBase(label: Label, isPressed: Bool) -> some View {
@@ -280,6 +281,25 @@ extension ToolbarButtonBaseStyle {
 struct ToolbarButtonStyle: ButtonStyle, ToolbarButtonBaseStyle {
     typealias Label = Configuration.Label
     
+    @Environment(\.horizontalSizeClass) private var horizontalSizeClassEnvironment
+    @Environment(\.verticalSizeClass) private var verticalSizeClassEnvironment
+    
+    var horizontalSizeClass: UserInterfaceSizeClass?
+    var verticalSizeClass: UserInterfaceSizeClass?
+    
+    init(horizontalSizeClass: UserInterfaceSizeClass? = nil, verticalSizeClass: UserInterfaceSizeClass? = nil) {
+        if horizontalSizeClass != nil {
+            self.horizontalSizeClass = horizontalSizeClass
+        } else {
+            self.horizontalSizeClass = horizontalSizeClassEnvironment
+        }
+        if verticalSizeClass != nil {
+            self.verticalSizeClass = verticalSizeClass
+        } else {
+            self.verticalSizeClass = verticalSizeClassEnvironment
+        }
+    }
+    
     func makeBody(configuration: Configuration) -> some View {
         return makeBodyBase(label: configuration.label, isPressed: configuration.isPressed)
     }
@@ -288,6 +308,9 @@ struct ToolbarButtonStyle: ButtonStyle, ToolbarButtonBaseStyle {
 struct ToolbarMenuStyle: MenuStyle, ToolbarButtonBaseStyle {
     typealias Label = Menu<Configuration.Label, Configuration.Content>
     
+    @Environment(\.horizontalSizeClass) internal var horizontalSizeClass
+    @Environment(\.verticalSizeClass) internal var verticalSizeClass
+    
     func makeBody(configuration: Configuration) -> some View {
         return makeBodyBase(label: Menu(configuration), isPressed: false)
     }
@@ -314,6 +337,11 @@ extension ButtonStyle where Self == ToolbarButtonStyle {
     static var toolbar: ToolbarButtonStyle {
         ToolbarButtonStyle()
     }
+    
+    // this is needed to workaround a SwiftUI bug on < iOS 15
+    static func toolbar(horizontalSizeClass: UserInterfaceSizeClass?, verticalSizeClass: UserInterfaceSizeClass?) -> ToolbarButtonStyle {
+        ToolbarButtonStyle(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)
+    }
 }
 
 extension MenuStyle where Self == ToolbarMenuStyle {

+ 8 - 0
UTM.xcodeproj/project.pbxproj

@@ -224,6 +224,9 @@
 		84E6F6FE289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6F6FC289319AE00080EEF /* VMToolbarDisplayMenuView.swift */; };
 		84F746B9276FF40900A20C87 /* VMDisplayAppleWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F746B8276FF40900A20C87 /* VMDisplayAppleWindowController.swift */; };
 		84F746BB276FF70700A20C87 /* VMDisplayQemuDisplayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F746BA276FF70700A20C87 /* VMDisplayQemuDisplayController.swift */; };
+		84F909FF289488F90008DBE2 /* MenuLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F909FE289488F90008DBE2 /* MenuLabel.swift */; };
+		84F90A00289488F90008DBE2 /* MenuLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F909FE289488F90008DBE2 /* MenuLabel.swift */; };
+		84F90A01289488F90008DBE2 /* MenuLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F909FE289488F90008DBE2 /* MenuLabel.swift */; };
 		84FCABBA268CE05E0036196C /* UTMQemuVirtualMachine.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FCABB9268CE05E0036196C /* UTMQemuVirtualMachine.m */; };
 		84FCABBB268CE05E0036196C /* UTMQemuVirtualMachine.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FCABB9268CE05E0036196C /* UTMQemuVirtualMachine.m */; };
 		84FCABBC268CE05E0036196C /* UTMQemuVirtualMachine.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FCABB9268CE05E0036196C /* UTMQemuVirtualMachine.m */; };
@@ -1644,6 +1647,7 @@
 		84E6F6FC289319AE00080EEF /* VMToolbarDisplayMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMToolbarDisplayMenuView.swift; sourceTree = "<group>"; };
 		84F746B8276FF40900A20C87 /* VMDisplayAppleWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayAppleWindowController.swift; sourceTree = "<group>"; };
 		84F746BA276FF70700A20C87 /* VMDisplayQemuDisplayController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayQemuDisplayController.swift; sourceTree = "<group>"; };
+		84F909FE289488F90008DBE2 /* MenuLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuLabel.swift; sourceTree = "<group>"; };
 		84FCABB8268CE05E0036196C /* UTMQemuVirtualMachine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMQemuVirtualMachine.h; sourceTree = "<group>"; };
 		84FCABB9268CE05E0036196C /* UTMQemuVirtualMachine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMQemuVirtualMachine.m; sourceTree = "<group>"; };
 		84FCABBD268CE4080036196C /* UTMVirtualMachine-Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMVirtualMachine-Private.h"; sourceTree = "<group>"; };
@@ -3084,6 +3088,7 @@
 				CE772AAB25C8B0F600E4E379 /* ContentView.swift */,
 				8471770927CCA60600D3A50B /* DefaultPicker.swift */,
 				8471770527CC974F00D3A50B /* DefaultTextField.swift */,
+				84F909FE289488F90008DBE2 /* MenuLabel.swift */,
 				CED234EC254796E500ED0A57 /* NumberTextField.swift */,
 				CE19392526DCB093005CEC17 /* RAMSlider.swift */,
 				CE2D954324AD4F980059923A /* VMCardView.swift */,
@@ -3554,6 +3559,7 @@
 				CED814EC24C7C2850042F0F1 /* VMConfigInfoView.swift in Sources */,
 				CE2D92A324AD46670059923A /* qapi-util.c in Sources */,
 				CE2D92A424AD46670059923A /* qapi-commands-job.c in Sources */,
+				84F909FF289488F90008DBE2 /* MenuLabel.swift in Sources */,
 				CE2D92A524AD46670059923A /* VMKeyboardView.m in Sources */,
 				CE2D92A624AD46670059923A /* qapi-commands-net.c in Sources */,
 				CE2D92A724AD46670059923A /* qapi-types-common.c in Sources */,
@@ -3798,6 +3804,7 @@
 				CE0B6D2324AD57FC00FE012D /* qapi-commands-crypto.c in Sources */,
 				841619AC284315F9000034B2 /* UTMConfigurationInfo.swift in Sources */,
 				CEF0305326A2AFBF00667B63 /* Spinner.swift in Sources */,
+				84F90A01289488F90008DBE2 /* MenuLabel.swift in Sources */,
 				CE0B6CFA24AD568400FE012D /* UTMLegacyQemuConfiguration+System.m in Sources */,
 				CEBBF1A824B921F000C15049 /* VMDetailsView.swift in Sources */,
 				CE0B6D7E24AD584D00FE012D /* qapi-events-misc.c in Sources */,
@@ -4101,6 +4108,7 @@
 				CEA45E55263519B5002FA97D /* qapi-types-net.c in Sources */,
 				CEA45E56263519B5002FA97D /* qapi-types-rdma.c in Sources */,
 				CEA45E57263519B5002FA97D /* ImagePicker.swift in Sources */,
+				84F90A00289488F90008DBE2 /* MenuLabel.swift in Sources */,
 				84C4D9032880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */,
 				CEA45E58263519B5002FA97D /* qapi-commands-rocker.c in Sources */,
 				CEA45E5A263519B5002FA97D /* VMConfigSystemView.swift in Sources */,