diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index f20882383..48fe8ee37 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -14,9 +14,28 @@ class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { logger.debug("AppDelegate: didFinishLaunchingWithOptions") application.registerForRemoteNotifications() + if #available(iOS 17.0, *) { trackKeyboard() } return true } + @available(iOS 17.0, *) + private func trackKeyboard() { + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + @available(iOS 17.0, *) + @objc func keyboardWillShow(_ notification: Notification) { + if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { + ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height + } + } + + @available(iOS 17.0, *) + @objc func keyboardWillHide(_ notification: Notification) { + ChatModel.shared.keyboardHeight = 0 + } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02hhx", $0) }.joined() logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)") diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 7ce430ca5..f333b6e77 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -57,6 +57,8 @@ final class ChatModel: ObservableObject { @Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source @Published var draft: ComposeState? @Published var draftChatId: String? + // tracks keyboard height via subscription in AppDelegate + @Published var keyboardHeight: CGFloat = 0 var messageDelivery: Dictionary Void> = [:] diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 6e05e645e..925ec575d 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -22,7 +22,7 @@ struct ChatView: View { @State private var showAddMembersSheet: Bool = false @State private var composeState = ComposeState() @State private var deletingItem: ChatItem? = nil - @FocusState private var keyboardVisible: Bool + @State private var keyboardVisible = false @State private var showDeleteMessage = false @State private var connectionStats: ConnectionStats? @State private var customUserProfile: Profile? @@ -39,6 +39,16 @@ struct ChatView: View { @State private var selectedMember: GroupMember? = nil var body: some View { + if #available(iOS 16.0, *) { + viewBody + .scrollDismissesKeyboard(.immediately) + .keyboardPadding() + } else { + viewBody + } + } + + private var viewBody: some View { let cInfo = chat.chatInfo return VStack(spacing: 0) { if searchMode { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 7f58d90b8..cb49eced1 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -234,7 +234,7 @@ struct ComposeView: View { @EnvironmentObject var chatModel: ChatModel @ObservedObject var chat: Chat @Binding var composeState: ComposeState - @FocusState.Binding var keyboardVisible: Bool + @Binding var keyboardVisible: Bool @State var linkUrl: URL? = nil @State var prevLinkUrl: URL? = nil @@ -943,19 +943,18 @@ struct ComposeView_Previews: PreviewProvider { static var previews: some View { let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) @State var composeState = ComposeState(message: "hello") - @FocusState var keyboardVisible: Bool return Group { ComposeView( chat: chat, composeState: $composeState, - keyboardVisible: $keyboardVisible + keyboardVisible: Binding.constant(true) ) .environmentObject(ChatModel()) ComposeView( chat: chat, composeState: $composeState, - keyboardVisible: $keyboardVisible + keyboardVisible: Binding.constant(true) ) .environmentObject(ChatModel()) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 711ae826c..51deced72 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -16,7 +16,7 @@ struct NativeTextEditor: UIViewRepresentable { @Binding var disableEditing: Bool let height: CGFloat let font: UIFont - @FocusState.Binding var focused: Bool + @Binding var focused: Bool let alignment: TextAlignment let onImagesAdded: ([UploadContent]) -> Void @@ -144,13 +144,12 @@ private class CustomUITextField: UITextView, UITextViewDelegate { struct NativeTextEditor_Previews: PreviewProvider{ static var previews: some View { - @FocusState var keyboardVisible: Bool return NativeTextEditor( text: Binding.constant("Hello, world!"), disableEditing: Binding.constant(false), height: 100, font: UIFont.preferredFont(forTextStyle: .body), - focused: $keyboardVisible, + focused: Binding.constant(false), alignment: TextAlignment.leading, onImagesAdded: { _ in } ) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index fb31eec45..f90784234 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -27,7 +27,7 @@ struct SendMessageView: View { var onMediaAdded: ([UploadContent]) -> Void @State private var holdingVMR = false @Namespace var namespace - @FocusState.Binding var keyboardVisible: Bool + @Binding var keyboardVisible: Bool @State private var teHeight: CGFloat = 42 @State private var teFont: Font = .body @State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body) @@ -401,7 +401,6 @@ struct SendMessageView_Previews: PreviewProvider { @State var composeStateNew = ComposeState() let ci = ChatItem.getSample(1, .directSnd, .now, "hello") @State var composeStateEditing = ComposeState(editingItem: ci) - @FocusState var keyboardVisible: Bool @State var sendEnabled: Bool = true return Group { @@ -412,7 +411,7 @@ struct SendMessageView_Previews: PreviewProvider { composeState: $composeStateNew, sendMessage: { _ in }, onMediaAdded: { _ in }, - keyboardVisible: $keyboardVisible + keyboardVisible: Binding.constant(true) ) } VStack { @@ -422,7 +421,7 @@ struct SendMessageView_Previews: PreviewProvider { composeState: $composeStateEditing, sendMessage: { _ in }, onMediaAdded: { _ in }, - keyboardVisible: $keyboardVisible + keyboardVisible: Binding.constant(true) ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index a9e7e8fed..c6aaf1c8b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -126,6 +126,7 @@ struct GroupChatInfoView: View { logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))") } } + .keyboardPadding() } private func groupInfoHeader() -> some View { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index cf206e74c..03dd24108 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -18,6 +18,14 @@ struct ChatListView: View { @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false var body: some View { + if #available(iOS 16.0, *) { + viewBody.scrollDismissesKeyboard(.immediately) + } else { + viewBody + } + } + + private var viewBody: some View { ZStack(alignment: .topLeading) { NavStackCompat( isActive: Binding( diff --git a/apps/ios/Shared/Views/Helpers/Keyboard.swift b/apps/ios/Shared/Views/Helpers/Keyboard.swift new file mode 100644 index 000000000..76f47089e --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/Keyboard.swift @@ -0,0 +1,9 @@ +// +// Keyboard.swift +// SimpleX (iOS) +// +// Created by Evgeny on 10/07/2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import Foundation diff --git a/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift b/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift new file mode 100644 index 000000000..45d766ddf --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift @@ -0,0 +1,21 @@ +// +// KeyboardPadding.swift +// SimpleX (iOS) +// +// Created by Evgeny on 10/07/2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +extension View { + @ViewBuilder func keyboardPadding() -> some View { + if #available(iOS 17.0, *) { + GeometryReader { g in + self.padding(.bottom, max(0, ChatModel.shared.keyboardHeight - g.safeAreaInsets.bottom)) + } + } else { + self + } + } +} diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 5b6c9b5dc..247b91a04 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -36,7 +36,7 @@ struct AddGroupView: View { } } } else { - createGroupView() + createGroupView().keyboardPadding() } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 006602209..d05ac4458 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -104,6 +104,7 @@ struct CreateProfile: View { } } .padding() + .keyboardPadding() } func textField(_ placeholder: LocalizedStringKey, text: Binding) -> some View { diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index f2c20a2ca..be6ccfd3d 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -18,7 +18,7 @@ struct TerminalView: View { @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State var composeState: ComposeState = ComposeState() - @FocusState private var keyboardVisible: Bool + @State private var keyboardVisible = false @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var terminalItem: TerminalItem? @State private var scrolled = false diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 32f961368..33a510f60 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; + 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; @@ -422,6 +423,7 @@ 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; + 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; @@ -619,6 +621,7 @@ 18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */, 64466DCB29FFE3E800E3D48D /* MailView.swift */, 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */, + 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */, ); path = Helpers; sourceTree = ""; @@ -1142,6 +1145,7 @@ 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, + 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, 5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */, 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,