Skip to main content

Using the Readable Content Guides in SwiftUI

When making apps with UIKit, I often use the readable content guides in my layouts. They are auto layout guides that you can use to constrain the width of a view so that the text within it is easy to read. As Apple states it in the documentation:

This layout guide defines an area that can easily be read without forcing users to move their head to track the lines.

When I started building apps in SwiftUI, I wanted to use it there too. But this API is currently not exposed to SwiftUI.

The StackOverflow solutions for this all use some hard-coded value for the readable content guide. I wanted to use Apple’s implementation though, because it is consistent with my UIKit code.

We can achieve this using a custom view modifier.

import SwiftUI
import UIKit

struct ReadableContentWidth: ViewModifier {
  private let measureViewController = UIViewController()

  @State private var orientation: UIDeviceOrientation = UIDevice.current.orientation

  func body(content: Content) -> some View {
    content
      .frame(maxWidth: readableWidth(for: orientation))
      .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
        orientation = UIDevice.current.orientation
      }
  }

  private func readableWidth(for _: UIDeviceOrientation) -> CGFloat {
    measureViewController.view.frame = UIScreen.main.bounds
    let readableContentSize = measureViewController.view.readableContentGuide.layoutFrame.size
    return readableContentSize.width
  }
}

public extension View {
  func readableContentWidth() -> some View {
    modifier(ReadableContentWidth())
  }
}

The view modifier uses a UIViewController under the hood to measure the readable content width of the device. It then constrains the view using this value as the maximum width.

Note that we are listening to device orientation changes. This is because the readable width may change, especially on iPhones.

iPhone landscape iPhone portrait

We need to pass the orientation as a parameter func readableWidth(for _: UIDeviceOrientation), even though it is not used within the function.
This is necessary to let SwiftUI know that the view depends on the orientation. Otherwise the view wouldn’t be updated when the orientation changes.

We can use the modifier in a view like so:

import SwiftUI

struct ContentView: View {
  var body: some View {
    ScrollView {
      VStack(alignment: .leading, spacing: 20) {
        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec urna magna, mollis eget tincidunt sed, sollicitudin nec quam. Quisque sed massa eu augue volutpat porta ut eget tortor. Quisque dolor urna, varius in turpis sed, dignissim fermentum dolor. Vivamus sollicitudin massa non est malesuada, ac auctor purus tincidunt. Cras id sodales augue, a vestibulum lectus. Duis ornare mauris nec eleifend sodales. Nunc pharetra augue eu felis ullamcorper, a efficitur lacus euismod.")

        Text("Aliquam facilisis nisl convallis arcu euismod pulvinar. Suspendisse dignissim ullamcorper ex bibendum sollicitudin. In in lectus non eros elementum convallis eu eu augue. Nulla feugiat dignissim eleifend. Nulla vestibulum ligula eleifend massa consectetur, quis lacinia enim mollis. Aliquam leo erat, lacinia sed varius non, accumsan ut dui. In rutrum, leo eu porta faucibus, libero velit porta libero, ut dignissim dui nisl eget enim. Pellentesque laoreet congue ante, a fermentum nisi imperdiet non. Fusce vel iaculis nunc.")
      }
      .readableContentWidth()
      .padding()
    }
  }
}

The final result on an iPad looks like this:

iPad landscape iPad portrait