diff --git a/Passnger.xcodeproj/project.pbxproj b/Passnger.xcodeproj/project.pbxproj index 0090e2f..bf45d18 100644 --- a/Passnger.xcodeproj/project.pbxproj +++ b/Passnger.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 4F25F08B24CDECB200D990B3 /* PasswordInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F25F08924CDECB200D990B3 /* PasswordInfoView.swift */; }; 4F25F08D24DB077A00D990B3 /* SquareNotifierViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F25F08C24DB077900D990B3 /* SquareNotifierViewModifier.swift */; }; 4F25F08E24DB077A00D990B3 /* SquareNotifierViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F25F08C24DB077900D990B3 /* SquareNotifierViewModifier.swift */; }; + 4F4343D52538BC8D00EC4B87 /* KeyboardObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE8B61E252D659B004BF440 /* KeyboardObserving.swift */; }; 4F4DDCC225082C71006CAFB2 /* ManageMasterPasswordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4DDCC125082C71006CAFB2 /* ManageMasterPasswordsView.swift */; }; 4F4DDCC325082C71006CAFB2 /* ManageMasterPasswordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4DDCC125082C71006CAFB2 /* ManageMasterPasswordsView.swift */; }; 4F4DDCC825096663006CAFB2 /* SheetScaffoldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4DDCC725096663006CAFB2 /* SheetScaffoldView.swift */; }; @@ -60,6 +61,7 @@ 4F87051B244398C400769A9D /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F87051A244398C400769A9D /* Model.swift */; }; 4FCF4D1A24632D8700F98F3C /* KeychainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCF4D1924632D8700F98F3C /* KeychainPasswordItem.swift */; }; 4FCF4D1C24632F4000F98F3C /* PassngerKeychainItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCF4D1B24632F4000F98F3C /* PassngerKeychainItem.swift */; }; + 4FE8B61F252D659B004BF440 /* KeyboardObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE8B61E252D659B004BF440 /* KeyboardObserving.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -117,6 +119,7 @@ 4F87051A244398C400769A9D /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 4FCF4D1924632D8700F98F3C /* KeychainPasswordItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainPasswordItem.swift; sourceTree = ""; }; 4FCF4D1B24632F4000F98F3C /* PassngerKeychainItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassngerKeychainItem.swift; sourceTree = ""; }; + 4FE8B61E252D659B004BF440 /* KeyboardObserving.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardObserving.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -206,6 +209,7 @@ 4F870503244379C500769A9D /* Passnger */ = { isa = PBXGroup; children = ( + 4FE8B61E252D659B004BF440 /* KeyboardObserving.swift */, 4F870504244379C500769A9D /* AppDelegate.swift */, 4F870506244379C500769A9D /* SceneDelegate.swift */, 4F870508244379C500769A9D /* ContentView.swift */, @@ -400,6 +404,7 @@ files = ( 4F7DEDCD2473845B0012041B /* Model.swift in Sources */, 4F7DEDD02473845B0012041B /* PassngerKeychainItem.swift in Sources */, + 4F4343D52538BC8D00EC4B87 /* KeyboardObserving.swift in Sources */, 4F4DDCEC2509D696006CAFB2 /* CreatePasswordView.swift in Sources */, 4F4DDCC925096663006CAFB2 /* SheetScaffoldView.swift in Sources */, 4F7DEDDD2485589A0012041B /* MasterPasswordFormModel.swift in Sources */, @@ -430,6 +435,7 @@ files = ( 4F87051B244398C400769A9D /* Model.swift in Sources */, 4F7DEDDC2485589A0012041B /* MasterPasswordFormModel.swift in Sources */, + 4FE8B61F252D659B004BF440 /* KeyboardObserving.swift in Sources */, 4F4DDCEB2509D696006CAFB2 /* CreatePasswordView.swift in Sources */, 4F4DDCC825096663006CAFB2 /* SheetScaffoldView.swift in Sources */, 4FCF4D1C24632F4000F98F3C /* PassngerKeychainItem.swift in Sources */, @@ -534,7 +540,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"MacPassnger/Preview Content\""; DEVELOPMENT_TEAM = 54ZXBTM75W; ENABLE_HARDENED_RUNTIME = YES; @@ -545,7 +551,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.01; + MARKETING_VERSION = 1.02; PRODUCT_BUNDLE_IDENTIFIER = com.nbakhle.passnger; PRODUCT_NAME = Passnger; SDKROOT = macosx; @@ -561,7 +567,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"MacPassnger/Preview Content\""; DEVELOPMENT_TEAM = 54ZXBTM75W; ENABLE_HARDENED_RUNTIME = YES; @@ -572,7 +578,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 1.01; + MARKETING_VERSION = 1.02; PRODUCT_BUNDLE_IDENTIFIER = com.nbakhle.passnger; PRODUCT_NAME = Passnger; SDKROOT = macosx; @@ -701,7 +707,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "\"Passnger/Preview Content\""; DEVELOPMENT_TEAM = 54ZXBTM75W; ENABLE_PREVIEWS = YES; @@ -711,7 +717,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.01; + MARKETING_VERSION = 1.02; PRODUCT_BUNDLE_IDENTIFIER = com.nbakhle.passnger; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -724,7 +730,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "\"Passnger/Preview Content\""; DEVELOPMENT_TEAM = 54ZXBTM75W; ENABLE_PREVIEWS = YES; @@ -734,7 +740,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.01; + MARKETING_VERSION = 1.02; PRODUCT_BUNDLE_IDENTIFIER = com.nbakhle.passnger; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; diff --git a/Passnger/ContentView.swift b/Passnger/ContentView.swift index f0dc6b1..86d6e26 100644 --- a/Passnger/ContentView.swift +++ b/Passnger/ContentView.swift @@ -56,6 +56,7 @@ struct ContentView: View { } } } + .keyboardObserving() .squareNotifier(text: "Copied to\nClipboard", showNotifier: self.showCopied) } } diff --git a/Passnger/KeyboardObserving.swift b/Passnger/KeyboardObserving.swift new file mode 100644 index 0000000..7a05caf --- /dev/null +++ b/Passnger/KeyboardObserving.swift @@ -0,0 +1,59 @@ +// +// KeyboardObserving.swift +// Passenger +// +// Created by Neil Bakhle on 2020-08-31. +// Copyright © 2020 Neil. All rights reserved. +// + +import SwiftUI + +#if os(iOS) +struct KeyboardObserving: ViewModifier { + + @State var keyboardHeight: CGFloat = 0 + + @ViewBuilder + func body(content: Content) -> some View { + if #available(iOS 14.0, *) { + content + } else { + content + .padding([.bottom], keyboardHeight) + .edgesIgnoringSafeArea((keyboardHeight > 0) ? [.bottom] : []) + .onReceive( + NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification) + .receive(on: RunLoop.main), + perform: updateKeyboardHeight + ) + } + } + + func updateKeyboardHeight(_ notification: Notification) { + guard let info = notification.userInfo else { return } + // Get the duration of the keyboard animation + let keyboardAnimationDuration = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25 + + guard let keyboardFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + // If the top of the frame is at the bottom of the screen, set the height to 0. + withAnimation(.easeOut(duration: keyboardAnimationDuration)) { + if keyboardFrame.origin.y == UIScreen.main.bounds.height { + keyboardHeight = 0 + } else { + // IMPORTANT: This height will _include_ the SafeAreaInset height. + keyboardHeight = keyboardFrame.height + } + } + } +} +#endif + +extension View { + func keyboardObserving() -> some View { + #if os(iOS) + return self.modifier(KeyboardObserving()) + #else + return self + #endif + } +} diff --git a/Views/IntroSetupView.swift b/Views/IntroSetupView.swift index 3ae5f8e..07b47cc 100644 --- a/Views/IntroSetupView.swift +++ b/Views/IntroSetupView.swift @@ -12,20 +12,22 @@ struct IntroSetupView: View { @State private var masterPasswordFormModel = MasterPasswordFormModel() let onValidated: (MasterPasswordFormModel) -> Void var body: some View { - VStack { - Text("Enter your master password to get started. This will be the only password you need to remember. This password should only be known to you and not written down anywhere.") - .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0)) - MasterPasswordFormView(formModel: self.$masterPasswordFormModel) - Button(action: { - self.masterPasswordFormModel.hasSubmitted = true - if self.masterPasswordFormModel.validate() { - self.onValidated(self.masterPasswordFormModel) - } - }) { - Text("Submit") - }.padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) - Spacer() - }.padding() + ScrollView { + VStack { + Text("Enter your master password to get started. This will be the only password you need to remember. This password should only be known to you and not written down anywhere.") + .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0)) + MasterPasswordFormView(formModel: self.$masterPasswordFormModel) + Button(action: { + self.masterPasswordFormModel.hasSubmitted = true + if self.masterPasswordFormModel.validate() { + self.onValidated(self.masterPasswordFormModel) + } + }) { + Text("Submit") + }.padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0)) + Spacer() + }.padding() + }.keyboardObserving() } } diff --git a/Views/PasswordFormView.swift b/Views/PasswordFormView.swift index eaac200..6c33d33 100644 --- a/Views/PasswordFormView.swift +++ b/Views/PasswordFormView.swift @@ -100,10 +100,11 @@ struct PasswordFormView: View { } } } + }.macSafeSheet(isPresented: self.$showManageMasterPasswords) { + ManageMasterPasswordsView(masterPasswords: self.masterPasswords, onCancel: { self.showManageMasterPasswords = false }, onDelete: self.removeMasterPasswords, onCreate: self.createMasterPassword) } - }.macSafeSheet(isPresented: self.$showManageMasterPasswords) { - ManageMasterPasswordsView(masterPasswords: self.masterPasswords, onCancel: { self.showManageMasterPasswords = false }, onDelete: self.removeMasterPasswords, onCreate: self.createMasterPassword) } + .keyboardObserving() .frame(minHeight: 340) } } diff --git a/Views/PasswordInfoView.swift b/Views/PasswordInfoView.swift index d799282..9882c3f 100644 --- a/Views/PasswordInfoView.swift +++ b/Views/PasswordInfoView.swift @@ -50,7 +50,7 @@ struct PasswordInfoView: View { #if os(iOS) List { self.contentRows(width: metrics.size.width) - } + }.keyboardObserving() #else ScrollView { VStack(alignment: .leading, spacing: 12) { diff --git a/Views/SheetScaffoldView.swift b/Views/SheetScaffoldView.swift index 4b68541..9b47cd5 100644 --- a/Views/SheetScaffoldView.swift +++ b/Views/SheetScaffoldView.swift @@ -58,11 +58,7 @@ extension View { extension View { func macSafeSheet(isPresented: Binding, content: @escaping () -> Content) -> some View { - #if os(macOS) return self.background(EmptyView().sheet(isPresented: isPresented, content: content)) - #else - return self.sheet(isPresented: isPresented, content: content) - #endif } }