SwiftUI: Sheet Modal, unexpected resize behavior

Hey Everyone, I've been remaking an app using SwiftUI, and I am running into a weird animation bug/hitch using .sheet(isPresented:).

I have a GIF illustrating the issue, but can't seem to attach it to this forum post. Let me know if there's anyway to share this.

Regardless, my issue:

To describe the issue: I'm using two Detents for the sheet, [.medium, .large]. I have a TextField that's displayed in the sheet, and when opening it, the keyboard moves my content upward (expected, working fine).

My issue comes when I programmatically resign the TextField (using .focused($isFocused). The content on the sheet jumps up, beyond the upper bound of the sheet. My hypothesis is that the sheet's content is immediately redrawn, using the medium detent frame, but before the animation has finished going from large to medium.

It's possible this is not a SwiftUI bug, but something wrong with my implementation. I'll provide the relevant code below. Any help is greatly appreciated!

Onboarding.swift (presents the sheet)

@ViewBuilder
    var content: some View {
        VStack {
            headline
                .foregroundStyle(.white.opacity(0.95))
            subHeadline
                .foregroundStyle(.white)
            Spacer()
            messages
                .foregroundStyle(.white)
            Spacer()
            callToAction
        }
        .ignoresSafeArea(.keyboard)
        .sheet(isPresented: $showJoin) {
            join
        }
    }
    
    var join: some View {
        Join()
            .ignoresSafeArea()
            .presentationCornerRadius(40)
            .presentationDragIndicator(.hidden)
            .presentationBackground {
                Rectangle()
                    .fill(.ultraThinMaterial)
                    .padding(.bottom, -1000)
            }
            .presentationDetents([.medium, .large])
    }

Join.swift (holds the sheet's content, and displays the heading)

struct Join: View {
        
    @State private var didSignUp = false
    
    var body: some View {
        VStack {
            heading
            Divider()
            contentView
        }
        .animation(.easeInOut, value: didSignUp)
        .transition(.opacity)
        .interactiveDismissDisabled(didSignUp)
    }
    
    var heading: some View {
        VStack(spacing: 8) {
            Text(didSignUp ? "Verify" : "Start here")
                .frame(maxWidth : .infinity, alignment: .leading)
                .font(.largeTitle.bold())
                .foregroundColor(.primary)
                .blendMode(.overlay)
            Text(didSignUp ? "Enter code" : "Create an account")
                .frame(maxWidth : .infinity, alignment: .leading)
                .font(.callout)
                .foregroundColor(.primary)
                .blendMode(.overlay)
        }
        .padding(.top, 20)
        .padding(.horizontal)
    }
    
    var contentView: some View {
        Group {
            if didSignUp {
                Verification()
                    .transition(.move(edge: .trailing).combined(with: .opacity))
            } else {
                SignUp(didSignUp: $didSignUp)
                    .transition(.move(edge: .leading).combined(with: .opacity))
            }
        }
        .padding(.horizontal)
    }
    
}

SignUp.swift (the sheet content)

struct SignUp: View {
    
    @Binding var didSignUp: Bool
    
    @State private var phoneNumber: String = ""
    
    @State private var sendingTextMessage = false
    
    @FocusState private var isFocused: Bool
    
    private let notice =
    """
    By creating an account, you agree to our **[Terms of Service](https://cordia.app/tos)** and **[Privacy Policy](https://cordia.app/privacy)**
    """
    
    var body: some View {
        VStack {
            VStack {
                phoneNumberLabel
                phoneNumberInput
            }
            .padding()
            if sendingTextMessage {
                LoadingIndicator(isVisible: $sendingTextMessage)
                    .padding()
            } else {
                continueButton
                    .padding()
            }
            Spacer()
            termsAndConditions
                .padding(.bottom)
        }
    }
    
    var phoneNumberLabel: some View {
        Text("Enter your phone number")
            .font(.title3)
            .foregroundColor(.primary)
            .blendMode(.overlay)
            .frame(maxWidth: .infinity, alignment: .leading)
    }
    
    var phoneNumberInput: some View {
        iPhoneNumberField("(***) 867-5309", text: $phoneNumber)
            .maximumDigits(10)
            .formatted()
            .clearsOnEditingBegan(true)
            .clearButtonMode(.always)
            .font(.system(size: 25, weight: .semibold))
            .padding()
            .glass(cornerRadius: 10)
            .focused($isFocused)
    }
    
    var termsAndConditions: some View {
        Text(addPolicyLinks() ?? AttributedString(notice))
            .multilineTextAlignment(.center)
            .fixedSize(horizontal: false, vertical: true)
            .font(.body)
            .blendMode(.overlay)
            .padding()
    }
    
    var continueButton: some View {
        Button(action: {
            guard !sendingTextMessage else { return }
            sendingTextMessage = true
            isFocused = false
            UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
            Auth.signUp(with: phoneNumber) { user, error in
                didSignUp = true
            }
        }) {
            Text("Join Cordia")
                .font(.system(size: 25, weight: .semibold))
                .foregroundColor(.primary.opacity(0.8))
                .frame(width: 200, height: 60)
                .tintedGlass(
                    cornerRadius: 20,
                    strokeColor: .cordiaGoldDark,
                    strokeSize: 1.0,
                    tint: .cordiaGold
                )
        }
    }
    
    private func addPolicyLinks() -> AttributedString? {
        var string = try? AttributedString(markdown: notice)
        if let range = string?.range(of: "Terms of Service") {
            string?[range].foregroundColor = .cordiaGold
        }
        if let range = string?.range(of: "Privacy Policy") {
            string?[range].foregroundColor = .cordiaGold
        }
        return string
    }
}

Replies

Still experiencing this issue with iOS 17.2.1 and Xcode 15.1

Bump - still experiencing this

I'm experiencing the same issue only when the sheet has an input, any kind of input in it.

What is even stranger is that the drag indicator disappears when an input is present in the content of the sheet.

Might this be the default behaviour?

Hi, I faced the same phenomenon when using sheet view with Text field in it's content. Something like, showing text field in only large detent. Currently in medium detent, and when swipe up to large detent, the sheet jerking and still keep the medium detent. Not sure the reason why (maybe swiftUI bug), but can pass it by 2 workaround.

  1. Write the sheet view content directly, don't use another struct view.
  2. Using @ViewBuilder instead of new struct view.

Pls try.