diff --git a/Riot/Modules/Room/RoomViewController.swift b/Riot/Modules/Room/RoomViewController.swift index e9260105e..3468ffe30 100644 --- a/Riot/Modules/Room/RoomViewController.swift +++ b/Riot/Modules/Room/RoomViewController.swift @@ -200,15 +200,7 @@ extension RoomViewController { optionalTextView?.becomeFirstResponder() originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) } - // This tirggers a SwiftUI update that is handled correctly on iOS 16, but needs to be dispatchted async on older versions - // Dispatching on iOS 16 instead causes some weird SwiftUI update behaviours - if #available(iOS 16, *) { - wysiwygInputToolbar.showKeyboard() - } else { - DispatchQueue.main.async { - wysiwygInputToolbar.showKeyboard() - } - } + roomInputToolbarContainer.removeFromSuperview() let dimmingView = UIView() dimmingView.translatesAutoresizingMaskIntoConstraints = false @@ -235,7 +227,18 @@ extension RoomViewController { } let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPanRoomToolbarContainer(_ :))) roomInputToolbarContainer.addGestureRecognizer(panGesture) - optionalTextView?.removeFromSuperview() + if let optionalTextView { + // This tirggers a SwiftUI update that is handled correctly on iOS 16, but needs to be dispatchted async on older versions + // Dispatching on iOS 16 instead causes some weird SwiftUI update behaviours + if #available(iOS 16, *) { + wysiwygInputToolbar.showKeyboard() + } else { + DispatchQueue.main.async { + wysiwygInputToolbar.showKeyboard() + } + } + optionalTextView.removeFromSuperview() + } } else { let originalRect = wysiwygInputToolbar.convert(wysiwygInputToolbar.frame, to: view) var optionalTextView: UITextView? @@ -244,7 +247,6 @@ extension RoomViewController { optionalTextView = textView self.view.window?.addSubview(textView) optionalTextView?.becomeFirstResponder() - wysiwygInputToolbar.showKeyboard() } self.roomInputToolbarContainer.removeFromSuperview() maximisedToolbarDimmingView?.removeFromSuperview() @@ -257,7 +259,10 @@ extension RoomViewController { self.view.layoutIfNeeded() } roomInputToolbarContainer.gestureRecognizers?.removeAll() - optionalTextView?.removeFromSuperview() + if let optionalTextView { + wysiwygInputToolbar.showKeyboard() + optionalTextView.removeFromSuperview() + } } } diff --git a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift index 5484ea1ce..ed0462c47 100644 --- a/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift +++ b/Riot/Modules/Room/Views/WYSIWYGInputToolbar/WysiwygInputToolbarView.swift @@ -192,6 +192,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp } func showKeyboard() { + self.wysiwygViewModel.textView.becomeFirstResponder() self.viewModel.showKeyboard() } diff --git a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift index 0070b5f7f..6ed032748 100644 --- a/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift +++ b/RiotSwiftUI/Modules/Room/Composer/View/Composer.swift @@ -32,6 +32,7 @@ struct Composer: View { @Environment(\.theme) private var theme: ThemeSwiftUI @State private var isActionButtonShowing = false + @FocusState private var focused: Bool private let horizontalPadding: CGFloat = 12 private let borderHeight: CGFloat = 40 @@ -58,17 +59,8 @@ struct Composer: View { viewModel.viewState.shouldDisplayContext ? contextBannerHeight + standardVerticalPadding + verticalComponentSpacing : 0 } - /// Computes the total height of the composer (excluding the RTE formatting bar). - /// This height includes the text view, as well as the context banner - /// and user suggestion list when displayed. - private var composerHeight: CGFloat { - wysiwygViewModel.idealHeight - + composerTopPadding - + composerVerticalPadding - // Extra padding added on top of the VStack containing the composer - + standardVerticalPadding - + additionalHeightForContextBanner - } + /// the total height of the composer (excluding the RTE formatting bar). + @State private var composerHeight: CGFloat = .zero private var cornerRadius: CGFloat { if shouldFixRoundCorner { @@ -139,22 +131,39 @@ struct Composer: View { .padding(.horizontal, horizontalPadding) } HStack(alignment: shouldFixRoundCorner ? .top : .center, spacing: 0) { - WysiwygComposerView( - placeholder: viewModel.viewState.placeholder ?? "", - viewModel: wysiwygViewModel, - itemProviderHelper: nil, - keyCommandHandler: handleKeyCommand, - pasteHandler: nil - ) - .tint(theme.colors.accent) - .onAppear { - if wysiwygViewModel.isContentEmpty { - wysiwygViewModel.setup() + // Use a GeometryReader to force the composer to fill the HStack + GeometryReader { _ in + WysiwygComposerView( + placeholder: viewModel.viewState.placeholder ?? "", + viewModel: wysiwygViewModel, + itemProviderHelper: nil, + keyCommandHandler: handleKeyCommand, + pasteHandler: nil + ) + .clipped() + .tint(theme.colors.accent) + .focused($focused, equals: true) + .onChange(of: focused) { newValue in + viewModel.focused = newValue + } + .onChange(of: viewModel.focused) { newValue in + guard focused != newValue else { return } + focused = newValue + } + .onAppear { + if wysiwygViewModel.isContentEmpty { + wysiwygViewModel.setup() + } } } + if !viewModel.viewState.isMinimiseForced { Button { - wysiwygViewModel.maximised.toggle() + viewModel.focused = true + // Use a dispatched block so the focus state will be up to date when the composer size changes. + DispatchQueue.main.async { + wysiwygViewModel.maximised.toggle() + } } label: { Image(toggleButtonImageName) .resizable() @@ -169,15 +178,14 @@ struct Composer: View { .padding(.horizontal, horizontalPadding) .padding(.top, composerTopPadding) .padding(.bottom, composerVerticalPadding) + .layoutPriority(1) } .clipShape(rect) .overlay(rect.stroke(borderColor, lineWidth: 1)) .animation(.easeInOut(duration: resizeAnimationDuration), value: wysiwygViewModel.idealHeight) .padding(.top, standardVerticalPadding) .onTapGesture { - if viewModel.focused { - viewModel.focused = true - } + viewModel.focused = true } } @@ -231,6 +239,18 @@ struct Composer: View { } } + /// Computes the total height of the composer (excluding the RTE formatting bar). + /// This height includes the text view, as well as the context banner + /// and user suggestion list when displayed. + private func updateComposerHeight(idealHeight: CGFloat) { + composerHeight = idealHeight + + composerTopPadding + + composerVerticalPadding + // Extra padding added on top of the VStack containing the composer + + standardVerticalPadding + + additionalHeightForContextBanner + } + // MARK: Public init( @@ -300,6 +320,15 @@ struct Composer: View { .onChange(of: wysiwygViewModel.suggestionPattern) { newValue in sendMentionPattern(pattern: newValue) } + .onChange(of: wysiwygViewModel.idealHeight) { newValue in + updateComposerHeight(idealHeight: newValue) + } + .onChange(of: viewModel.viewState.shouldDisplayContext) { _ in + updateComposerHeight(idealHeight: wysiwygViewModel.idealHeight) + } + .task { + updateComposerHeight(idealHeight: wysiwygViewModel.idealHeight) + } } private func storeCurrentSelection() {