diff --git a/BitwardenShared/UI/Platform/Application/Extensions/View+Backport.swift b/BitwardenShared/UI/Platform/Application/Extensions/View+Backport.swift new file mode 100644 index 000000000..1135dc748 --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Extensions/View+Backport.swift @@ -0,0 +1,84 @@ +import SwiftUI + +// MARK: - View + +/// Extension of `View` to have the `backport` object availalbe to ease +/// with available APIs. +/// +/// Adapted from https://davedelong.com/blog/2021/10/09/simplifying-backwards-compatibility-in-swift/ +/// +extension View { + /// Helper to apply backport operations for available APIs. + var backport: Backport { Backport(self) } +} + +// MARK: - Backport + +/// Backport for `View` content. +/// +/// Adapted from https://davedelong.com/blog/2021/10/09/simplifying-backwards-compatibility-in-swift/ +/// +extension Backport where Content: View { + /// On iOS 16+, configures the scroll view to dismiss the keyboard immediately. + /// + func dismissKeyboardImmediately() -> some View { + if #available(iOS 16, *) { + return content.scrollDismissesKeyboard(.immediately) + } else { + return content + } + } + + /// On iOS 16+, configures the scroll view to dismiss the keyboard interactively. + /// + func dismissKeyboardInteractively() -> some View { + if #available(iOS 16, *) { + return content.scrollDismissesKeyboard(.interactively) + } else { + return content + } + } + + //// Configures the content margin for scroll content of a specific view. + /// + /// Use this modifier to customize the content margins of different + /// kinds of views. For example, you can use this modifier to customize + /// the scroll content margins of scrollable views like ``ScrollView``. In the + /// following example, the scroll view will automatically inset + /// its content by the safe area plus an additional 20 points + /// on the leading and trailing edge. + /// + /// ScrollView(.horizontal) { + /// // ... + /// } + /// .contentMargins(.horizontal, 20.0) + /// + /// - Parameters: + /// - edges: The edges to add the margins to. + /// - length: The amount of margins to add. + @ViewBuilder + func scrollContentMargins(_ edges: Edge.Set = .all, _ length: CGFloat?) -> some View { + if #available(iOS 17.0, *) { + content.contentMargins(edges, length, for: .scrollContent) + } else { + content + } + } +} + +// MARK: - Backport + +/// Helper to deal with available APIs and provide backport operations +/// +/// Adapted from https://davedelong.com/blog/2021/10/09/simplifying-backwards-compatibility-in-swift/ +/// +public struct Backport { + /// The content to apply backport operations. + public let content: Content + + /// Initializes a backport with some content to apply backrpot operations. + /// - Parameter content: The content to apply backport operations. + public init(_ content: Content) { + self.content = content + } +} diff --git a/BitwardenShared/UI/Platform/Application/Extensions/View.swift b/BitwardenShared/UI/Platform/Application/Extensions/View.swift index 42400b494..57912aeee 100644 --- a/BitwardenShared/UI/Platform/Application/Extensions/View.swift +++ b/BitwardenShared/UI/Platform/Application/Extensions/View.swift @@ -10,26 +10,6 @@ extension View { block(self) } - /// On iOS 16+, configures the scroll view to dismiss the keyboard immediately. - /// - func dismissKeyboardImmediately() -> some View { - if #available(iOSApplicationExtension 16, *) { - return self.scrollDismissesKeyboard(.immediately) - } else { - return self - } - } - - /// On iOS 16+, configures the scroll view to dismiss the keyboard interactively. - /// - func dismissKeyboardInteractively() -> some View { - if #available(iOSApplicationExtension 16, *) { - return self.scrollDismissesKeyboard(.interactively) - } else { - return self - } - } - /// Focuses next field in sequence, from the given `FocusState`. /// Requires a currently active focus state and a next field available in the sequence. /// (https://stackoverflow.com/a/71531523) diff --git a/BitwardenShared/UI/Platform/Application/Utilities/KeyboardResponder.swift b/BitwardenShared/UI/Platform/Application/Utilities/KeyboardResponder.swift new file mode 100644 index 000000000..6efd69298 --- /dev/null +++ b/BitwardenShared/UI/Platform/Application/Utilities/KeyboardResponder.swift @@ -0,0 +1,36 @@ +import Combine +import Foundation +import UIKit + +/// An observable responder to handle keyboard show/hide notifications. +final class KeyboardResponder: ObservableObject { + // MARK: Properties + + /// Whether the keyboard is shown. + @Published var isShown: Bool = false + + /// A publisher when the keyboard will hide. + var keyboardWillHideNotification = NotificationCenter.default.publisher( + for: UIResponder.keyboardWillHideNotification + ) + + /// A publisher when the keyboard will show. + var keyboardWillShowNotification = NotificationCenter.default.publisher( + for: UIResponder.keyboardWillShowNotification + ) + + // MARK: Initializer + + /// Initializes a `KeyboardResponder`. + init() { + keyboardWillHideNotification.map { _ in + false + } + .assign(to: &$isShown) + + keyboardWillShowNotification.map { _ in + true + } + .assign(to: &$isShown) + } +} diff --git a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift index 7bced70c9..6a668afee 100644 --- a/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift +++ b/BitwardenShared/UI/Tools/Send/SendItem/AddEditSendItem/AddEditSendItemView.swift @@ -60,7 +60,7 @@ struct AddEditSendItemView: View { // swiftlint:disable:this type_body_length profileSwitcher } - .dismissKeyboardInteractively() + .backport.dismissKeyboardInteractively() .background(Asset.Colors.backgroundPrimary.swiftUIColor.ignoresSafeArea()) .navigationBar( title: store.state.mode.navigationTitle, diff --git a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift index ed05358c5..b25607c90 100644 --- a/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift +++ b/BitwardenShared/UI/Vault/Vault/VaultList/VaultListItemTests.swift @@ -441,4 +441,4 @@ private extension BitwardenSdk.LoginView { static func usernameFixture() -> BitwardenSdk.LoginView { .fixture(username: FakeData.email1) } -} +} // swiftlint:disable:this file_length diff --git a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift index 1fa758b0e..edfa1dc05 100644 --- a/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift +++ b/BitwardenShared/UI/Vault/VaultItem/AddEditItem/AddEditItemView.swift @@ -8,6 +8,9 @@ import SwiftUI struct AddEditItemView: View { // MARK: Private Properties + /// A responder to keyboard visibility events. + @ObservedObject private var keyboard = KeyboardResponder() + /// An object used to open urls in this view. @Environment(\.openURL) private var openURL @@ -93,12 +96,13 @@ struct AddEditItemView: View { .padding(12) } .animation(.default, value: store.state.collectionsForOwner) - .dismissKeyboardImmediately() + .backport.dismissKeyboardImmediately() .background( Asset.Colors.backgroundPrimary.swiftUIColor .ignoresSafeArea() ) .navigationBarTitleDisplayMode(.inline) + .backport.scrollContentMargins(Edge.Set.bottom, keyboard.isShown ? 30.0 : 0.0) } @ViewBuilder private var cardItems: some View {