What is UIView readableContentGuide equivalent thing in the SwiftUI

Viewed 2107

The UIView has the readableContentGuide so layout could be related to screen size and make sure content always readable. I can not find any pieces of information about those things in the SwiftUI.

What's the equivalent thing in the SwiftUI could be using? And if not, could we use the SwiftUI component to build the same effect in UIKit?

Thanks.

4 Answers

There's no direct readableContentGuide alternative in SwiftUI. It should be directly backed into views. Theoretically, SwiftUI should apply device specific padding but it's not happening in v1.0. So, i.e., adding a padding modifier to a TextField should apply a bigger padding in resolutions like an iPad of 12.9".

Even so, you can write your own view modifier to apply custom paddings. Here is an example done by @mecid:

import SwiftUI

private struct ReadableGuidePadding: ViewModifier {

    @Environment(\.horizontalSizeClass) var horizontal

    func body(content: Content) -> some View {
       content.padding(.horizontal, horizontal == .regular ? 84: 16)
    }

}

extension View {

    func readableGuidePadding() -> some View {
        modifier(ReadableGuidePadding())
    }

}

The other solutions didn't work for me because they relied on a hardcoded amount of padding or dropping down to UIKit, which isn't great for multi-platform SwiftUI apps that need to look nice on all the differently-sized iPhones, iPads, and Macs. So as there still isn't a native SwiftUI equivalent (as of iOS 15/macOS 12), I thought I'd make an alternative.

My alternative is to calculate the amount of padding based on:

  • the current width of the view
  • and some ideal, readable width
struct ReadabilityPadding: ViewModifier {
    let isEnabled: Bool
    @ScaledMetric private var unit: CGFloat = 20

    func body(content: Content) -> some View {
        // Use a GeometryReader here to get view width.
        GeometryReader { geometryProxy in
            content
                .padding(.horizontal, padding(for: geometryProxy.size.width))
        }
    }

    private func padding(for width: CGFloat) -> CGFloat {
        guard isEnabled else { return 0 }

        // The internet seems to think the optimal readable width is 50-75
        // characters wide; I chose 70 here. The `unit` variable is the 
        // approximate size of the system font and is wrapped in
        // @ScaledMetric to better support dynamic type. I assume that 
        // the average character width is half of the size of the font. 
        let idealWidth = 70 * unit / 2

        // If the width is already readable then don't apply any padding.
        guard width >= idealWidth else {
            return 0
        }

        // If the width is too large then calculate the padding required
        // on either side until the view's width is readable.
        let padding = round((width - idealWidth) / 2)
        return padding
    }
}

Here is how it looks on different devices (top is normal, bottom is with the ReadabilityPadding view modifier applied):

screenshot comparison

The padding solution suggested here doesn't make sense considering an infinite range of device widths. We need a solution that caters for all types of device sizes and can instead preserve a readable width for content.

The end result can be consumed by any view in a SwiftUI-friendly manner:

SomeReallyLongView()
    .readableGuidePadding()

First, we need a ViewModifier that can fix a view's width to a maximum margin. UIKit actually defines the readable content width as 672 so let's use that.

private struct ReadableGuidePadding: ViewModifier {
    func body(content: Content) -> some View {
        HStack(spacing: 0) {
            Spacer(minLength: 0)
            content.frame(maxWidth: 672)
            Spacer(minLength: 0)
        }
    }
}

Now we have a modifier that clips our contents to 672 points wide and uses horizontal spacers when needed. Note, we use a minLength of 0 otherwise there will always be horizontal padding on smaller screens. We don't want that.

Now we can create a convenient View extension to use our modifier:

extension View {
    func readableGuidePadding() -> some View {
        modifier(ReadableGuidePadding())
    }
}

Enjoy

This should work:


private struct ReadablePadding: ViewModifier {
  private static let readableAera: CGSize = UIViewController().view.readableContentGuide.layoutFrame.size
  @State var orientation = UIDevice.current.orientation
  @Environment(\.horizontalSizeClass) var horizontalSizeClass
  @Environment(\.verticalSizeClass) var verticalSizeClass
  
  func body(content: Content) -> some View {
    content
      .padding(.horizontal, padding())
      .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
        orientation = UIDevice.current.orientation
      }
  }
  
  private func padding() -> CGFloat {
    var width = horizontalSizeClass == .compact ? min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) :
      max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
    
    if horizontalSizeClass == .regular, verticalSizeClass == .regular { // iPads
      print(orientation)
      width = UIScreen.main.bounds.width
    }
    
    return (width - ReadablePadding.readableAera.width) / 2 + 16.0 // 16.0 is a margin
  }
}

extension View {
  func readableGuidePadding() -> some View {
    modifier(ReadablePadding())
  }
}
Related