In SwiftUI, is it possible to use a modifier only for a certain os target?
Asked Answered
A

7

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()
  }
}
Androgen answered 23/4, 2020 at 12:8 Comment(4)
Did you try using #if os(OSX)? SourceChanna
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.Androgen
Wild8x, just replace ".listStyle(SidebarListStyle())" with ".navigationViewStyle(DefaultNavigationViewStyle()) " to achieve what you're after. See also my other comment.Venose
Thanks Workingdog. I have just tried it and it is fine! Finally!Androgen
V
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
 }
Venose answered 23/4, 2020 at 13:13 Comment(2)
But by doing this don't you think that the modifier will be around the HStack instead of around the NavigationView as it was originaly?Androgen
it will propagate to the NavigationView.Venose
K
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)
    }
}
Kathrynekathy answered 30/5, 2020 at 8:11 Comment(5)
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.Androgen
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 macOSGalvanism
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 } }Dihedron
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<T: View>(@ViewBuilder modifier: ( Self ) -> T) -> T { modifier(self) }. The nil is not necessary, just return $0 if no modifications are wanted.Ad
@Ad 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.Kathrynekathy
E
12

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
Eucken answered 11/6, 2021 at 13:15 Comment(0)
V
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
 }
Venose answered 23/4, 2020 at 13:13 Comment(2)
But by doing this don't you think that the modifier will be around the HStack instead of around the NavigationView as it was originaly?Androgen
it will propagate to the NavigationView.Venose
A
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()
  }
}
Androgen answered 23/4, 2020 at 12:41 Comment(0)
A
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()
    }
}
Androgen answered 23/4, 2020 at 14:14 Comment(2)
you are correct. I've edited my answer. I should have used: #if targetEnvironment(macCatalyst)Venose
Thanks a lot it works fine. The project is an iOS Target with iOS, iPad and Mac selected that uses macCatalyst.Androgen
D
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.

Datolite answered 21/7, 2022 at 17:31 Comment(0)
H
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
    }
}
Highstepper answered 19/8, 2023 at 8:28 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.