alexxjk
alexxjk

Reputation: 1701

How to move up SwiftUI `ScrollView` content when keyboard appears?

I'm having a chat view with messages. When message composer gets a focus and keyboard is appeared the height of ScrollView decreases. Now I want all messages to move up a little so users can see the same bottom message she saw before. Is there anyway to achieve this with a pure SwiftUI?

ScrollViewReader { scrollReader in
    ScrollView {
        LazyVStack(spacing: 24) {
            ForEach(messages, id: \.id) {
                MessageContainer(message: $0)
                    .id($0.id)
            }
        }
        .padding(.horizontal, 16)
    }
}

Upvotes: 5

Views: 5890

Answers (4)

ismailp
ismailp

Reputation: 3

If you add edgesIgnoringSafeArea(.bottom) before going to the webview page, the view will rise to the top when the keyboard opens.

Example code:

router.pushTo(view: MainNavigationView.builder.makeView(WebViewContainer(urlString: AppSettings.shared.customerSupportForm, type: .web, isAtBottom: .constant(false), isLoading: $viewModel.isLoading).edgesIgnoringSafeArea(.bottom), withNavigationTitle: userEnvironment.txtCustomerContactForm.localize,isNavBarAlphaAnimationActive: false))

Upvotes: 0

X1opya
X1opya

Reputation: 61

It happens because setting offset does not affect on views size.

You can add Spacer view with height equal to keyboard height and all views moved with changing self size.

I'm using KeyboardGuardian for find keyboard offset and use it like that:

VStack {
  // ... LazyVStack, ChatControls, etc...
  Spacer()
    .frame(height: -kGuardian.slide)
    .savePosition(in: $kGuardian.rects[0])
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.onAppear() {
  kGuardian.addObserver()
}
.onDisappear() {
  kGuardian.removeObserver()
}

Upvotes: 0

ChrisR
ChrisR

Reputation: 12125

Then just try it with .offset:

struct ContentView: View {
    
    let messages = Message.dummyData
    @State private var showNewMessage = false
    @State private var newMessage = ""
    @FocusState private var focus: Bool

    var body: some View {
        
        VStack {
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 24) {
                    ForEach(messages, id: \.id) { message in
                        MessageContainer(message: message)
                            .offset(y: showNewMessage ? -300 : 0)
                    }
                }
                .padding(.horizontal, 16)
            }
            
            
            if showNewMessage == false {
                Button("New Message") {
                    withAnimation {
                        showNewMessage = true
                        focus = true
                    }
                }
            } else {
                Button("Send") {
                        showNewMessage = false
                }
                
                TextEditor(text: $newMessage)
                    .frame(height: 80)
                    .padding()
                    .background(
                        RoundedRectangle(cornerRadius: 20)
                            .stroke(Color.gray, lineWidth: 1)
                    )
                    .padding(.horizontal, 16)
                    .focused($focus)
            }
        }
    }
}

Upvotes: 0

ChrisR
ChrisR

Reputation: 12125

Here is an example that uses ScrollViewReader to scroll to the tapped message for answering it:

enter image description here

struct ContentView: View {
    
    let messages = Message.dummyData
    @State private var tappedMessage: Message?
    @State private var newMessage = ""
    @FocusState private var focus: Bool

    var body: some View {
        
        ScrollViewReader { scrollReader in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 24) {
                    ForEach(messages, id: \.id) { message in
                        MessageContainer(message: message)
                            .id(message.id)
                            .onTapGesture {
                                tappedMessage = message
                                focus = true
                            }
                    }
                }
                .padding(.horizontal, 16)
            }
            if let tappedMessage {
                VStack {
                    TextEditor(text: $newMessage)
                        .frame(height: 80)
                        .padding()
                        .background(
                            RoundedRectangle(cornerRadius: 20)
                                .stroke(Color.gray, lineWidth: 1)
                        )
                        .padding(.horizontal, 16)
                        .focused($focus)
                    
                    Button("Send") { self.tappedMessage = nil }
                }
                .onAppear {
                    DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
                        withAnimation {
                            scrollReader.scrollTo(tappedMessage.id)
                        }
                    }
                }
           }
        }
    }
}

Upvotes: 4

Related Questions