11

Sometimes, I need to make device-specific adjustments to layouts. For example, I may need to reduce spacing on an iPhone with a smaller screen or increase spacing on the largest screens. With UIKit (and even Interface Builder) it was easy to make layout exceptions for specific size classes. What is the best way to do conditional device-specific layouts with SwiftUI?

I've been scouring the SwiftUI documentation, and haven't found a way to access and use this type of information in layouts.

Below is an example for an Apple Watch app. Per Apple's design guidelines, I'm adding 8.5 points of padding to the left and right on the 40mm Series 4. However, the 44mm should have 9.5 points of padding, and any Apple Watch older than Series 4 should have no padding.

What is the best way to achieve this with SwiftUI?

struct ContentView : View {

    var body: some View {
        HStack {
            Text("Hello World")
        }.padding([.horizontal], 8.5)
    }
}
gohnjanotis
  • 6,513
  • 6
  • 37
  • 57

3 Answers3

11

In general there are two methods you can use to achieve device specific layouts:

  1. Size classes via @Environment variables
  2. GeometryReader for more fine-grained control

Unfortunately, UserInterfaceSizeClass only has .compact and .regular and is not available on watchOS.

To use the environment:

struct MyView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
}

To use GeometryReader:

var body -> some View {
    GeometryReader { proxy in
      if proxy.size.width > 324.0/2.0 { // 40mm watch resolution in points
        MyBigView()
      } else {
        MySmallView()
      }
    }
}

For reference, here are the watch resolutions:

  • 40mm: 394×324
  • 44mm: 448×368
  • 38mm: 340×272
  • 42mm: 390×312

Divide by 2.0 to get their values in points instead of pixels.

arsenius
  • 12,090
  • 7
  • 58
  • 76
  • When I try it the GeometryReader way I get an error message that says `The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions` and I can't get it to build. (Xcode 11.0 beta 3) Any ideas about how to overcome this, or maybe just an Xcode bug? – gohnjanotis Jul 09 '19 at 20:50
  • 1
    I've had a lot of compiler errors for valid code with `GeometryReader`. One workaround is to put the content in a function like `func mySizedContent(_ proxy: GeometryProxy) -> some View { ... }`. – arsenius Jul 09 '19 at 22:19
  • When I wrap the `GeometryReader` that I'm getting an error that says `Unable to infer complex closure return type; add explicit type to disambiguate` at the beginning of the `GeometryReader` closure, but I'm not sure I understand enough about how Swift and Swift UI to figure out what needs to be specified to make it compile. – gohnjanotis Jul 09 '19 at 22:27
  • Small correction to the answer: The resolutions are @2x. Proxy returns values at 1x. So it should be like proxy.size.width > 324/2 – Mithil Jadhav Jul 26 '19 at 08:23
0

For iPhone, I as able to use the environment like this:

@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?

And then in var body: some View I did this for handling how an image would scale based on screen size. Did a bunch of testing in iPad and iPhone X and larger. I'm sure there are other ways. This is at least a way to use size classes in SwiftUI. There is not much information yet on how to use size classes.

Image("logo")
      .opacity(1.0)
      .scaleEffect(makeCircleTextBig ? (horizontalSizeClass == .compact ? 0.18 : 0.25) : (horizontalSizeClass == .compact ? 0.06 : 0.1))
      .animation(.easeIn(duration: 1.0))

Also check out ControlSize: Apple Docs ControlSize

And check out this for a different approach: Hacking With Swift: SwiftUI size classes

D. Rothschild
  • 659
  • 9
  • 14
0

@gohnjanotis, not sure if its too late, but did you try adding a return in front of GeometryReader? I often get that error when I use some conditions, let etc before the actual view creation. So it should look like the following:

var body: some View {
        return GeometryReader { proxy in
          if proxy.size.width > 324.0/2.0 { // 40mm watch resolution in points
            Text("BIG view here")
          } else {
            Text("small view here")
          }
        }
    }
appsailor
  • 1
  • 1