12

Good day! In SwiftUI, is it possible to use a modifier only for a certain os target? In the following code I would like to use the modifier .listStyle(SidebarListStyle()) only for the MacOS target because it does not exist for the iOS target. Thanks for you help.

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()
  }
}
Wild8x
  • 459
  • 4
  • 13
  • 1
    Did you try using `#if os(OSX)`? [Source](https://stackoverflow.com/questions/24065017/how-to-determine-device-type-from-swift-os-x-or-ios) – DoesData Apr 23 '20 at 12:11
  • Yes I try using #is os(macOS) around the modifier itself but a error message appears: "Unexpected platform condition (expected 'os', 'arch', or 'swift')" ... I will try to do it around the HStack. – Wild8x Apr 23 '20 at 12:32
  • Wild8x, just replace ".listStyle(SidebarListStyle())" with ".navigationViewStyle(DefaultNavigationViewStyle()) " to achieve what you're after. See also my other comment. – workingdog support Ukraine Apr 23 '20 at 13:09
  • Thanks Workingdog. I have just tried it and it is fine! Finally! – Wild8x Apr 23 '20 at 13:16

7 Answers7

20

You can create a View extension and use it like this:

List {
    // ...
}
.ifOS(.macOS) {
    $0.listStyle(SidebarListStyle())
}

Here's the implementation:

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
        }
    }
}

However, this will not work if you try to use a method that is not available for all the platforms you target. The only way to make that work is to use #if os(…) directly.

I have an extension that makes it easier to do that:

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 Sorhus
  • 62,972
  • 39
  • 168
  • 232
  • Thank you so much Sindre for your reply. I will test it soon. Finally, for the moment I took the decision to do only independent Apps to avoid the mess of having plenty "if OS" statements ... but I am sure it will help others. – Wild8x Jun 18 '20 at 10:30
  • 1
    This works whenever the modifier is available in multiple OS. Compiler error in some cases where the modifier statement is not available in some OS. Example: StackNavigationViewStyle is not available in macOS .ifOS(.iOS) { $0.navigationViewStyle(StackNavigationViewStyle()) } Compiler error: 'StackNavigationViewStyle' is unavailable in macOS – user3204765 Jul 04 '20 at 13:07
  • If you have some special method for platform use group for wrap : .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 } } – YanSte Nov 29 '20 at 16:59
  • The `@escaping` can be removed from the `ifOS` call, it is not actually escaping. You can do the `modify` like that to avoid the AnyView: `func modify(@ViewBuilder modifier: ( Self ) -> T) -> T { modifier(self) }`. The `nil` is not necessary, just return $0 if no modifications are wanted. – hnh Mar 09 '21 at 16:05
  • @hnh Good catch with the moot `@escaping`. I have your version of `modify` in my projects too, but it's less flexible as it requires you to be in a `@ViewBuilder` context, which has a lot of limitations, like no `guard`. But yours is indeed better in most cases. – Sindre Sorhus Mar 09 '21 at 18:27
11

Swift 5.5

From the Swift 5.5 version, you can use add conditions directly to the withing modifier.

  } // End NavigationView
    #if os(macOS)
    .listStyle(SidebarListStyle())
    #else
    .navigationViewStyle(DefaultNavigationViewStyle())
    #endif
Raja Kishan
  • 16,767
  • 2
  • 26
  • 52
5

your better off doing this:

 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
 }
1

Thanks DoesData for giving me the direction.

The solution was to use #is os(macOS) around the entire code and not only around the modifier itself.

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()
  }
}
Wild8x
  • 459
  • 4
  • 13
0

WorkingDog, I try your elegante code with a very simple code to change the text color depending on the Target... but the text stays blue on both target and does not go red on 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()
    }
}
Wild8x
  • 459
  • 4
  • 13
0

More elegant, appropriate and reusable approach to conditionally add modifier would be by using EmptyModifier().

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

Let's say you want apply frame modifier based on OS Conditions, First create a custom ViewModifier like so:

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

Now create an instance of CustomFrame ViewModifier and add conditions as per the business logic:

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.
    }
}

This will let you add modifiers conditionally to any view in a Swifty way.

Kush Bhavsar
  • 909
  • 2
  • 12
0

I was here exploring other solution. I was bothered to continually apply navigationBarTitleDisplayMode.

I wrote a small modifier / ext to avoid me to change 34 occurrences and cluttering them with #if os(macOS).. #else..

(And if in future we have to add another OS.. is a matter of one line in modifier)

// 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))
    }
    
}

Usage:

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
    }
}
ingconti
  • 10,876
  • 3
  • 61
  • 48