在SwiftUI中,是否可以仅针对特定的操作系统目标使用修饰符?

12

你好!在SwiftUI中,能否仅针对特定的操作系统目标使用修饰符? 在下面的代码中,我想仅在MacOS目标上使用.listStyle(SidebarListStyle())修饰符,因为它在iOS目标上是不存在的。感谢您的帮助。

import SwiftUI

struct ContentView: View {

  @State var selection: Int?

  var body: some View {

    HStack() {
      NavigationView {
        List () {
          NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
            Text("Click Me To Display The First View")
          } // End Navigation Link

          NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
            Text("Click Me To Display The Second View")
          } // End Navigation Link

        } // End list
        .frame(minWidth: 350, maxWidth: 350)
        .onAppear {
            self.selection = 0
        }

      } // End NavigationView
        .listStyle(SidebarListStyle())
        .frame(maxWidth: .infinity, maxHeight: .infinity)

    } // End HStack
  } // End some View
} // End ContentView

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

1
你尝试使用 #if os(OSX) 了吗?来源 - DoesData
是的,我尝试在修饰符周围使用#is os(macOS),但出现错误消息:"意外的平台条件(期望为'os'、'arch'或'swift')"......我将尝试在HStack周围进行操作。 - Wild8x
Wild8x,只需将“.listStyle(SidebarListStyle())”替换为“.navigationViewStyle(DefaultNavigationViewStyle())”,您就可以实现您想要的效果。另请参阅我的其他评论。 - workingdog support Ukraine
谢谢Workingdog。我刚试了一下,很好!终于成功了! - Wild8x
7个回答

20
您可以创建一个 View 扩展并像这样使用它:
List {
    // ...
}
.ifOS(.macOS) {
    $0.listStyle(SidebarListStyle())
}

以下是实现方式:
enum OperatingSystem {
    case macOS
    case iOS
    case tvOS
    case watchOS

    #if os(macOS)
    static let current = macOS
    #elseif os(iOS)
    static let current = iOS
    #elseif os(tvOS)
    static let current = tvOS
    #elseif os(watchOS)
    static let current = watchOS
    #else
    #error("Unsupported platform")
    #endif
}

extension View {
    /**
    Conditionally apply modifiers depending on the target operating system.

    ```
    struct ContentView: View {
        var body: some View {
            Text("Unicorn")
                .font(.system(size: 10))
                .ifOS(.macOS, .tvOS) {
                    $0.font(.system(size: 20))
                }
        }
    }
    ```
    */
    @ViewBuilder
    func ifOS<Content: View>(
        _ operatingSystems: OperatingSystem...,
        modifier: (Self) -> Content
    ) -> some View {
        if operatingSystems.contains(OperatingSystem.current) {
            modifier(self)
        } else {
            self
        }
    }
}

然而,如果您尝试使用不适用于所有目标平台的方法,则无法运作。使其正常工作的唯一方法是直接使用#if os(...)。 我有一个扩展可以使这更容易实现:
extension View {
    /**
    Modify the view in a closure. This can be useful when you need to conditionally apply a modifier that is unavailable on certain platforms.

    For example, imagine this code needing to run on macOS too where `View#actionSheet()` is not available:

    ```
    struct ContentView: View {
        var body: some View {
            Text("Unicorn")
                .modify {
                    #if os(iOS)
                    $0.actionSheet(…)
                    #else
                    $0
                    #endif
                }
        }
    }
    ```
    */
    func modify<T: View>(@ViewBuilder modifier: (Self) -> T) -> T {
        modifier(self)
    }
}

非常感謝你的回复,Sindre。我很快就會進行測試。最後,我決定只做獨立的應用程序,以避免大量的“if OS”語句混亂...但我確信它將對其他人有所幫助。 - Wild8x
1
只要修改器在多个操作系统中可用,这就有效。在某些操作系统中,如果修改器语句不可用,则会出现编译器错误。例如:StackNavigationViewStyle 在 macOS 中不可用。.ifOS(.iOS) { $0.navigationViewStyle(StackNavigationViewStyle()) }编译器错误:'StackNavigationViewStyle' 在 macOS 中不可用。 - user3204765
如果您有一些特殊的平台使用方法来包装组: .platform(.iOS) { view in return Group { #if os(iOS) view.background(Color(viewModel.colorName)) .frame(height: UIScreen.main.bounds.height / 1.5) #else view #endif } } - YannSteph
@escaping 可以从 ifOS 调用中移除,因为它并没有真正地逃逸。你可以像这样进行 modify 以避免使用 AnyView:func modify<T: View>(@ViewBuilder modifier: ( Self ) -> T) -> T { modifier(self) }。如果不需要进行任何修改,那么 nil 并不是必需的,只需返回 $0 即可。 - hnh
@hnh 对于 @escaping 这个关键字的使用,你发现得非常好。我在我的项目中也使用了你的 modify 版本,但它的灵活性较差,因为它需要在 @ViewBuilder 上下文中使用,而这有很多限制,比如没有 guard。不过在大多数情况下,你的确更好。 - Sindre Sorhus

11

Swift 5.5

从Swift 5.5版本开始,您可以直接在"within"修饰符中添加条件。

  } // End NavigationView
    #if os(macOS)
    .listStyle(SidebarListStyle())
    #else
    .navigationViewStyle(DefaultNavigationViewStyle())
    #endif

5

你最好这样做:

 import SwiftUI

 struct ContentView: View {

@State var selection: Int?

var body: some View {
    #if targetEnvironment(macCatalyst)
    return theList.listStyle(SidebarListStyle())
    #else
    return theList.navigationViewStyle(DefaultNavigationViewStyle())
    #endif
}

 var theList: some View {
 HStack() {
   NavigationView {
     List () {
       NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
         Text("Click Me To Display The First View")
       } // End Navigation Link

       NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
         Text("Click Me To Display The Second View")
       } // End Navigation Link

     } // End list
     .frame(minWidth: 350, maxWidth: 350)
     .onAppear {
         self.selection = 0
     }

   } // End NavigationView
     .frame(maxWidth: .infinity, maxHeight: .infinity)

 } // End HStack
 } // End some View
 } // End ContentView
 }

但是这样做,你不觉得修饰符会围绕着HStack而不是原来的NavigationView吗? - Wild8x
它将传播到NavigationView。 - workingdog support Ukraine

1
感谢DoesData给了我方向。
解决方案是在整个代码周围使用#is os(macOS),而不仅仅是在修饰符本身周围。
import SwiftUI

struct ContentView: View {

  @State var selection: Int?

  var body: some View {

    #if os(macOS)
    HStack() {
      NavigationView {
        List () {
          NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
            Text("Click Me To Display The First View")
          } // End Navigation Link

          NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
            Text("Click Me To Display The Second View")
          } // End Navigation Link

        } // End list
          .frame(minWidth: 350, maxWidth: 350)
          .onAppear {
            self.selection = 0
        }

      } // End NavigationView
        .listStyle(SidebarListStyle())
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    } // End HStack

    #elseif os(iOS)
    HStack() {
      NavigationView {
        List () {
          NavigationLink(destination: FirstView(), tag: 0, selection: self.$selection) {
            Text("Click Me To Display The First View")
          } // End Navigation Link

          NavigationLink(destination: SecondView(), tag: 1, selection: self.$selection) {
            Text("Click Me To Display The Second View")
          } // End Navigation Link

        } // End list
          .frame(minWidth: 350, maxWidth: 350)
          .onAppear {
            self.selection = 0
        }

      } // End NavigationView
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    } // End HStack
    #endif

  } // End some View
} // End ContentView

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

0

更加优雅、合适和可重用的条件添加修饰符的方法是使用 EmptyModifier()

We can use the empty modifier to switch modifiers at compile time during development.

假设您想根据操作系统条件应用框架修饰符,首先创建一个自定义的 ViewModifier,如下所示:

struct CustomFrame: ViewModifier {
    func body(content: Content) -> some View {
            content
            .frame(width: 120)
        }
}

现在创建一个 CustomFrame ViewModifier 的 instance,并根据业务逻辑添加条件:
struct ContentView: View {
    
    var frameModifier: some ViewModifier {
        #if os(iOS)
        return CustomFrame()
        #elseif os(macOS)
        return EmptyModifier() // <- EmptyModifier()
        #endif
    }
    
    var body: some View {
        HStack {
            // a bunch of stuff in this stack
        }
        .modifier(frameModifier)  // <- Use custom modifiers like this.
    }
}

这将让您以Swifty的方式向任何视图有条件地添加修饰符。

0
WorkingDog,我尝试了你优雅的代码,并用一个非常简单的代码来改变文本颜色,取决于目标...但是在MacOS上,文本仍然保持蓝色,而不会变成红色!
import SwiftUI

struct ContentView: View {

    var body: some View {

      #if os(macOS)
      return monText.foregroundColor(Color.red)
      #elseif os(iOS)
       return monText.foregroundColor(Color.blue)
      #endif
      }

  var monText: some View {
    Text("Hello, World!")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

你是正确的。我已经编辑了我的答案。我应该使用:#if targetEnvironment(macCatalyst)。 - workingdog support Ukraine
非常感谢,它运行良好。该项目是一个 iOS 目标,选择了 iOS、iPad 和 Mac,并使用了 macCatalyst。 - Wild8x

0
我在这里探索其他解决方案。我感到烦恼,需要不断应用navigationBarTitleDisplayMode
我写了一个小修改器/扩展,以避免我改变34个出现的地方并且混乱地使用#if os(macOS).. #else..
(而且如果将来我们需要添加另一个操作系统..只需要修改一行代码就可以解决)
// MARK: navigationBarTitleDisplayMode

//fake:
#if os(iOS)

public typealias TitleDisplayMode = NavigationBarItem.TitleDisplayMode

#elseif os(macOS)

public enum TitleDisplayMode {
    case automatic
    case inline
    case large
}
#endif


struct PortableNavigationBarTitleDisplayModeViewModifier: ViewModifier {
    let mode: TitleDisplayMode
    
    func body(content: Content) -> some View {
        
        #if os(iOS)
        content
        .navigationBarTitleDisplayMode(mode)

        #elseif os(macOS)
        content
        // nada. We throw away

        #endif
        
    }
}


public extension View {
    @ViewBuilder
    
    func portableNavigationBarTitleDisplayMode(_ mode:  TitleDisplayMode) -> some View {
        self.modifier(PortableNavigationBarTitleDisplayModeViewModifier(mode: mode))
    }
    
}

使用方法:

struct ContentView: View {

    var body: some View {
        NavigationStack {
            List {
                Text("Hello1")
                Text("Hello2")
                Text("Hello3")
            }
            .navigationTitle("Menu")
        }
        .portableNavigationBarTitleDisplayMode(.inline)
        //.navigationBarTitleDisplayMode(.inline) // You wil have: 'navigationBarTitleDisplayMode' is unavailable in macOS
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接