ios – SwiftUI Chat View: Keyboard pushing complete view up as a substitute of adjusting content material space

0
14
ios – SwiftUI Chat View: Keyboard pushing complete view up as a substitute of adjusting content material space


I am constructing a chat interface in SwiftUI much like ChatGPT’s app, and I would like the keyboard to have the same behaviour to iMessage when a consumer sorts the enter field goes above the keyboard, the chat scrolls and the header stays on the prime. However I am having points with keyboard dealing with. When the keyboard seems, it pushes all the view up as a substitute of simply adjusting the chat content material space whereas preserving the header fastened and enter discipline above the keyboard.

Present Habits

  • When the keyboard seems, all the view (together with header) strikes up
  • The enter discipline typically will get hidden behind the keyboard
  • The scrollable content material space would not regulate correctly

Code

This is my present implementation for my ChatView
(I do even have a SideMenuView which appears to be pushed up too)

import SwiftUI

struct ChatView: View {
    @State non-public var isMenuOpen = false
    @State non-public var showSettings = false
    @State non-public var showModelSelection = false
    @State non-public var dragOffset: CGFloat = 0
    @StateObject non-public var modelSelectionVM = ModelSelectionViewModel()
    @StateObject non-public var chatHistoryVM = ChatHistoryViewModel()
    @StateObject non-public var chatVM: ChatMessageViewModel
    @FocusState non-public var isTextFieldFocused: Bool

    non-public let menuWidth: CGFloat = UIScreen.fundamental.bounds.width * 0.75

    init() {
        let modelVM = ModelSelectionViewModel()
        let historyVM = ChatHistoryViewModel()
        _modelSelectionVM = StateObject(wrappedValue: modelVM)
        _chatHistoryVM = StateObject(wrappedValue: historyVM)
        _chatVM = StateObject(wrappedValue: ChatMessageViewModel(modelSelectionVM: modelVM, chatHistoryVM: historyVM))
    }

    var physique: some View {
        ZStack(alignment: .main) {
            Coloration(.systemBackground)
                .ignoresSafeArea()

            // Dim background when menu is open
            if isMenuOpen {
                Coloration.black.opacity(0.4)
                    .ignoresSafeArea()
                    .onTapGesture { closeMenu() }
            }

            // Facet Menu
            SideMenuView(isMenuOpen: $isMenuOpen,
                         showSettings: $showSettings,
                         dragOffset: $dragOffset,
                         chatHistoryVM: chatHistoryVM,
                         chatVM: chatVM)
            .offset(x: isMenuOpen ? 0 : -menuWidth)

            VStack(spacing: 0) {
                // **Fastened Header**
                HStack {
                    Button(motion: toggleMenu) {
                        Picture(systemName: "line.horizontal.3")
                            .font(.title)
                            .foregroundColor(.blue)
                    }
                    .body(width: 44, peak: 44)

                    Spacer()

                    Button(motion: { showModelSelection = true }) {
                        Textual content(modelSelectionVM.selectedModel?.identify ?? "Choose Mannequin")
                            .foregroundColor(modelSelectionVM.selectedModel == nil ? .pink : .major)
                    }

                    Spacer()

                    Button(motion: {
                        if chatVM.messages.isEmpty {
                            chatVM.isTemporaryChat.toggle()
                        } else {
                            chatHistoryVM.selectedChatId = nil
                            chatVM.clearMessages()
                        }
                    }) {
                        Picture(systemName: chatVM.messages.isEmpty ?
                            (chatVM.isTemporaryChat ? "timer.circle.fill" : "timer.circle") :
                            "plus.circle.fill")
                            .font(.title)
                            .foregroundColor(chatVM.isTemporaryChat ? .orange : .blue)
                    }
                    .body(width: 44, peak: 44)
                }
                .padding(.horizontal)
                .padding(.prime, 50)
                .body(peak: 50) // Repair header peak
                .background(Coloration(.systemBackground))
                .zIndex(1) // Guarantee header stays fastened

                // **Scrollable Messages**
                ScrollViewReader { proxy in
                    ScrollView {
                        LazyVStack(spacing: 8) {
                            if chatVM.isTemporaryChat && chatVM.messages.isEmpty {
                                Textual content("This chat will not seem in historical past, use or create recollections, or be used to coach our fashions. For security functions, we might make a copy of this chat for as much as 30 days.")
                                    .foregroundColor(.secondary)
                                    .multilineTextAlignment(.heart)
                                    .padding()
                            }

                            ForEach(chatVM.messages) { message in
                                ChatMessageView(message: message)
                                    .id(message.id)
                            }
                        }
                        .padding(.backside, 8)
                    }
                    .onChange(of: chatVM.messages.rely) { _ in
                        scrollToBottom(proxy: proxy)
                    }
                }
                .padding(.backside, 10)

                // **Enter Area**
                VStack(spacing: 0) {
                    Divider()
                    HStack(spacing: 16) {
                        TextField("Sort a message...", textual content: $chatVM.currentInput)
                            .padding(.horizontal, 16)
                            .padding(.vertical, 12)
                            .background(Coloration(.systemGray6))
                            .cornerRadius(20)
                            .centered($isTextFieldFocused)
                            .submitLabel(.ship)
                            .onSubmit {
                                chatVM.sendMessage()
                            }
                        
                        Button(motion: {
                            chatVM.sendMessage()
                        }) 
                        .disabled(chatVM.currentInput.isEmpty || chatVM.isTyping)
                    }
                    .padding(.horizontal)
                    .padding(.vertical, 8)
                }
                .background(Coloration(.systemBackground))
                .ignoresSafeArea(.keyboard, edges: .backside)
            }
            .body(width: UIScreen.fundamental.bounds.width)
            .background(Coloration(.systemBackground))
            .offset(x: dragOffset + (isMenuOpen ? menuWidth : 0))
        }
        .gesture(
            DragGesture()
                .onChanged { gesture in
                    let translation = gesture.translation.width
                    if isMenuOpen {
                        dragOffset = Swift.min(0, translation)
                    } else {
                        dragOffset = Swift.max(0, translation)
                    }
                }
                .onEnded { gesture in
                    let translation = gesture.translation.width
                    let velocity = gesture.velocity.width

                    withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
                        if isMenuOpen  velocity > 500
                         else  velocity > 500
                        
                        dragOffset = 0
                    }
                }
        )
        .overlay {
            if showSettings {
                SettingsView(isPresented: $showSettings)
                    .transition(.transfer(edge: .backside))
            }

            if showModelSelection {
                ModelSelectionView(isPresented: $showModelSelection, viewModel: modelSelectionVM)
                    .transition(.transfer(edge: .backside))
            }
        }
    }

Anticipated Habits

  • Header ought to stay fastened on the prime
  • Chat content material space ought to regulate its dimension and scroll when keyboard seems
  • Enter discipline ought to keep seen and transfer up with the keyboard
  • Just like how iMessage handles keyboard look

What I’ve Tried

  1. Utilizing .ignoresSafeArea(.keyboard, edges: .backside)
  2. Including keyboard observers and manually adjusting view positions
  3. Utilizing GeometryReader to handle format
  4. Varied mixtures of ZStack and VStack
  5. Totally different approaches with safeAreaInsets

Setting

  • iOS 17+
  • SwiftUI
  • Xcode 15.2

Query

How can I correctly implement keyboard dealing with on this chat interface to:

  1. Preserve the header fastened on the prime
  2. Enable the chat content material to regulate and scroll appropriately
  3. Preserve the enter discipline seen above the keyboard
  4. Keep a easy animation throughout keyboard look/disappearance

Any assist or steerage can be significantly appreciated!

LEAVE A REPLY

Please enter your comment!
Please enter your name here