SwiftUI Adaptive Design: Creating Flexible UIs for All Devices

In the ever-evolving world of iOS development, creating adaptive user interfaces is crucial for delivering a seamless experience across various devices and orientations. SwiftUI provides several powerful tools to help developers craft flexible layouts that respond gracefully to different screen sizes and configurations. Let’s explore some key techniques for adaptive design in SwiftUI.

GP Forecast Adaptive Designs

ViewThatFits: Automatic Layout Adaptation

ViewThatFits is a SwiftUI container view that automatically selects the first child view that fits within the available space. This powerful tool simplifies the process of creating adaptive layouts by allowing you to define multiple layout options and letting SwiftUI choose the most appropriate one. Here’s an example of how to use ViewThatFits:

ViewThatFits {
    HStack {
        Image(systemName: "star.fill")
        Text("Favorite")
    }

    Image(systemName: "star.fill")
}

In this example, SwiftUI will display the HStack with both the image and text if there’s enough space. If not, it will fall back to showing just the image.

Size Classes: Adapting to Device Characteristics

Size Classes in SwiftUI allow you to adapt your layout based on the available space and device characteristics. By using the @Environment property wrapper, you can access the current horizontal and vertical size classes to make layout decisions.
Here’s how you can use size classes to adjust your layout:

@Environment(\.horizontalSizeClass) var horizontalSizeClass

var body: some View {
    Group {
        if horizontalSizeClass == .compact {
            VStack {
                // Compact layout
            }
        } else {
            HStack {
                // Regular layout
            }
        }
    }
}

This approach allows you to create distinct layouts for different device configurations, such as iPhones in portrait mode versus iPads in landscape mode. Here you can find all the size classes for each Apple Device.

GeometryReader: Fine-Grained Layout Control

GeometryReader is a powerful tool that provides detailed information about the size and position of its parent view. This allows for precise control over layout and sizing based on the available space. Here’s an example of using GeometryReader to create a responsive layout:

GeometryReader { geometry in
    VStack {
        Text("Hello, World!")
            .font(.system(size: geometry.size.width / 10))

        Rectangle()
            .fill(Color.blue)
            .frame(width: geometry.size.width * 0.8, height: 50)
    }
}

In this example, the text size and rectangle width are dynamically calculated based on the available width, ensuring the layout remains proportional across different screen sizes.

UIScreen.main.bounds.width: A Practice to Avoid

While it might be tempting to use UIScreen.main.bounds.width for layout calculations, this approach is considered bad practice in SwiftUI. It doesn’t account for factors like Split View on iPad, rotation changes, or future device form factors.
Instead of relying on UIScreen.main.bounds.width, it’s better to use SwiftUI’s built-in layout system, including GeometryReader, size classes, and flexible spacing. These tools provide a more robust and future-proof way to create adaptive layouts.

Conclusion

Adaptive design in SwiftUI is about creating flexible, responsive layouts that work well across all iOS devices. By leveraging tools like ViewThatFits, size classes, and GeometryReader, you can create UIs that automatically adjust to different screen sizes and orientations. Remember to avoid hard-coding dimensions or relying on specific device characteristics, and instead embrace SwiftUI’s powerful layout system for truly adaptive designs.

Here is a Login Screen example;

Login Screen example
import SwiftUI

struct LoginView: View {
    @State private var username = ""
    @State private var password = ""
    @State private var rememberMe = false
    @State private var showingAlert = false
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack(spacing: 20) {
                    // Avatar Image
                    Image(systemName: "person.circle.fill")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: horizontalSizeClass == .compact ? geometry.size.width * 0.3 : geometry.size.width * 0.2)
                        .foregroundColor(.blue)
                        .padding(.top, 40)
                    
                    // Welcome Text
                    Text("Welcome Back!")
                        .font(.system(size: geometry.size.width * 0.06))
                        .fontWeight(.bold)
                    
                    // Login Form
                    VStack(spacing: 15) {
                        TextField("Username", text: $username)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .autocapitalization(.none)
                        
                        SecureField("Password", text: $password)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                        
                        Toggle("Remember Me", isOn: $rememberMe)
                    }
                    .padding(.horizontal)
                    .frame(width: horizontalSizeClass == .compact ? geometry.size.width * 0.5 : geometry.size.width * 0.3)
                    
                    // Login Button
                    Button(action: {
                        // Perform login action
                        showingAlert = true
                    }) {
                        Text("Log In")
                            .fontWeight(.semibold)
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding()
                            .background(Color.blue)
                            .cornerRadius(10)
                    }
                    .padding(.horizontal)
                    .frame(width: geometry.size.width * 0.3, height: geometry.size.width * 0.3)
                    
                    // Forgot Password Link
                    Button("Forgot Password?") {
                        // Handle forgot password action
                    }
                    .foregroundColor(.blue)
                    
                    // Social Login Options
                    ViewThatFits {
                        HStack(spacing: 20) {
                            socialLoginButton(imageName: "apple.logo", text: "Sign in with Apple")
                            socialLoginButton(imageName: "g.circle", text: "Sign in with Google")
                        }
                        VStack(spacing: 10) {
                            socialLoginButton(imageName: "apple.logo", text: "Sign in with Apple")
                            socialLoginButton(imageName: "g.circle", text: "Sign in with Google")
                        }
                    }
                    .padding(.top)
                    
                    // Sign Up Link
                    HStack {
                        Text("Don't have an account?")

                        Button("Sign Up") {
                            // Handle sign up action
                        }
                        .foregroundColor(.blue)
                    }
                    .padding(.top)
                }
                .padding()
                .frame(minHeight: geometry.size.height)
            }
        }
        .alert(isPresented: $showingAlert) {
            Alert(title: Text("Login Attempt"), message: Text("Username: \(username)\nPassword: \(password)"), dismissButton: .default(Text("OK")))
        }
    }
    
    private func socialLoginButton(imageName: String, text: String) -> some View {
        Button(action: {
            // Handle social login action
        }) {
            HStack {
                Image(systemName: imageName)

                Text(text)
            }
            .padding()
            .frame(maxWidth: .infinity)
            .background(Color.gray.opacity(0.2))
            .cornerRadius(10)
        }
    }
}


#Preview {
    LoginView()
}

Xcode SwiftUI Preview Tips

Loved by some, hated by many developers — is the SwiftUI preview. I was also in the latter camp to be honest, until recently.

Here are some things you can do to enjoy your SwiftUI previews again.

When you get an error, tap on the error and read it carefully, often it gives a hint. For example, you have been changing code left and right, then you went to this certain view, and you activated the preview pane with Option+Command+Enter — and getting an error.

Make sure before activating a preview canvas, that your code builds and if not, then fix the error and try again.

If you have done this and you still get an error, make sure you don’t have another tab open with a preview active for another target, close those and try again.

If that also fails you can try to clear the preview cache with this terminal command, I made an alias in .zprofile called simprevdel.

Compiler flag PREVIEW

You can add the PREVIEW compiler flag to use it as a compiler directive to load preview mock data.

Don’t forget to inject the EnvironmentObject model in the Preview View.

Enjoy your previews!

SwiftUI autocomplete tricks

Fonts

There are some view properties which you use a lot and are difficult to type like:

.font(.caption)

here you type .font [ENTER] -> this makes .font(), you have to cursor back into the brackets to type .cap[ENTER]

This you can achieve faster by typing .fontcaption[ENTER], or .fonttitle[ENTER]

Frames

You probably know already the properties trick to type the first two characters of the properties to get them preselected like so typing .framewihe[ENTER]:

If you only want one of those sub properties you can type directly

.width[ENTER]

Xcode tip – rename

When you start developing an app, you don’t often know yet what the real name of your app will be, even you think you know. What do I mean by that?

Well, say you call your app TrainSleeper. You create the new app like this in Xcode and all is fine. Once you create the app in App Store Connect, it might shout at you that someone else owns this name and you cannot use it. You start searching for an app with this name in the store to see it, but it does not exist. This is because you can add an app with a certain name but never publish it. That name stays locked to the person who did this.

Anyway we need a lot of renaming during each development cycle, there is nothing to be ashamed about, it’s a very common task.

For this reason I added a keyboard shortcut to rename.

Give the rename menu function a shortcut.

Go to Settings with CMD + ,

Go to the Key Bindings tab

In filter type rename

In the key column after the rename word press all the keys you want, I selected;

SHIFT + CONTROL + OPTION + COMMAND + R

Why so many keys? Well Xcode has many shortcuts, this avoids conflict with other shortcuts