iosmacosswiftuicross-platformsize-classes

SwiftUI: UserInterfaceSizeClass for Universal (macOS & iOS) Views


Attempting to reference the @Environment objects horizontalSizeClass and verticalSizeClass on macOS (native, not Catalyst) results in the following errors:

'horizontalSizeClass' is unavailable in macOS

'verticalSizeClass' is unavailable in macOS

I appreciate that these properties aren't really applicable for macOS, but this presents a big barrier to creating SwiftUI Views which work universally (i.e. cross-platform across macOS, iOS, etc.).

One workaround is to wrap all the size-class-specific code inside conditional compilation, but the result is lots of duplication and redundancy (see below example).

Isn't there a more efficient way to handle this?

Example Universal View:

struct ExampleView: View {

    #if !os(macOS)
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    #endif

    private var item1: some View {
        Text("Example Item 1")
    }
    private var item2: some View {
        Text("Example Item 2")
    }
    private var item3: some View {
        Text("Example Item 3")
    }

    var body: some View {
        VStack {
            #if !os(macOS)
            if horizontalSizeClass == .compact {
                VStack {
                    item1
                    item2
                    item3
                }
            } else {
                HStack {
                    item1
                    item2
                    item3
                }
            }
            #else
            HStack {
                item1
                item2
                item3
            }
            #endif
        }
    }
}

Solution

  • It's true that macOS doesn't support horizontalSizeClass and verticalSizeClass natively, but the good news is it's easy to add them.

    You can define your own @Environment objects by making a struct that conforms to EnvironmentKey, then using it to extend EnvironmentValues.

    All you need to implement size classes on macOS is the following. They just return .regular at all times, but it's enough to function exactly the same as on iOS.

    #if os(macOS)
    enum UserInterfaceSizeClass {
        case compact
        case regular
    }
    
    struct HorizontalSizeClassEnvironmentKey: EnvironmentKey {
        static let defaultValue: UserInterfaceSizeClass = .regular
    }
    struct VerticalSizeClassEnvironmentKey: EnvironmentKey {
        static let defaultValue: UserInterfaceSizeClass = .regular
    }
    
    extension EnvironmentValues {
        var horizontalSizeClass: UserInterfaceSizeClass {
            get { return self[HorizontalSizeClassEnvironmentKey.self] }
            set { self[HorizontalSizeClassEnvironmentKey.self] = newValue }
        }
        var verticalSizeClass: UserInterfaceSizeClass {
            get { return self[VerticalSizeClassEnvironmentKey.self] }
            set { self[VerticalSizeClassEnvironmentKey.self] = newValue }
        }
    }
    #endif
    

    With this in place, you don't need anything special for macOS. For example:

    struct ExampleView: View {
    
        @Environment(\.horizontalSizeClass) var horizontalSizeClass
        
        private var item1: some View {
            Text("Example Item 1")
        }
        private var item2: some View {
            Text("Example Item 2")
        }
        private var item3: some View {
            Text("Example Item 3")
        }
        
        var body: some View {
            VStack {
                if horizontalSizeClass == .compact {
                    VStack {
                        item1
                        item2
                        item3
                    }
                } else {
                    HStack {
                        item1
                        item2
                        item3
                    }
                }
            }
        }
    }
    

    You can even put these extensions into a framework for broader use, so long as you define them as public.