Focus some text fields on appearance (#334)
Feature from iOS 15, use it on: - New profile name - New profile passphrase - Renamed profile name - Account username
This commit is contained in:
parent
6ede6f052a
commit
34f6738b69
|
@ -27,6 +27,14 @@ import PassepartoutLibrary
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AccountView: View {
|
struct AccountView: View {
|
||||||
|
enum Field {
|
||||||
|
case username
|
||||||
|
|
||||||
|
case password
|
||||||
|
|
||||||
|
case seed
|
||||||
|
}
|
||||||
|
|
||||||
@ObservedObject private var providerManager: ProviderManager
|
@ObservedObject private var providerManager: ProviderManager
|
||||||
|
|
||||||
private let providerName: ProviderName?
|
private let providerName: ProviderName?
|
||||||
|
@ -41,6 +49,8 @@ struct AccountView: View {
|
||||||
|
|
||||||
@State private var liveAccount = Profile.Account()
|
@State private var liveAccount = Profile.Account()
|
||||||
|
|
||||||
|
@FocusState private var focusedField: Field?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
providerName: ProviderName?,
|
providerName: ProviderName?,
|
||||||
vpnProtocol: VPNProtocolType,
|
vpnProtocol: VPNProtocolType,
|
||||||
|
@ -71,6 +81,7 @@ struct AccountView: View {
|
||||||
TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username)
|
TextField(usernamePlaceholder ?? L10n.Account.Items.Username.placeholder, text: $liveAccount.username)
|
||||||
.textContentType(.username)
|
.textContentType(.username)
|
||||||
.keyboardType(.emailAddress)
|
.keyboardType(.emailAddress)
|
||||||
|
.focused($focusedField, equals: .username)
|
||||||
.themeRawTextStyle()
|
.themeRawTextStyle()
|
||||||
.withLeadingText(L10n.Account.Items.Username.caption)
|
.withLeadingText(L10n.Account.Items.Username.caption)
|
||||||
|
|
||||||
|
@ -80,12 +91,14 @@ struct AccountView: View {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} else {
|
} else {
|
||||||
themeSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password)
|
themeSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password)
|
||||||
|
.focused($focusedField, equals: .password)
|
||||||
.withLeadingText(L10n.Account.Items.Password.caption)
|
.withLeadingText(L10n.Account.Items.Password.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: interactive, scan QR code
|
|
||||||
case .totp:
|
case .totp:
|
||||||
|
// TODO: interactive, scan QR code
|
||||||
themeSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password, contentType: .oneTimeCode)
|
themeSecureField(L10n.Account.Items.Password.placeholder, text: $liveAccount.password, contentType: .oneTimeCode)
|
||||||
|
.focused($focusedField, equals: .seed)
|
||||||
.withLeadingText(L10n.Account.Items.Seed.caption)
|
.withLeadingText(L10n.Account.Items.Seed.caption)
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
|
@ -111,6 +124,8 @@ struct AccountView: View {
|
||||||
saveAnyway: saveAnyway,
|
saveAnyway: saveAnyway,
|
||||||
onSave: onSave
|
onSave: onSave
|
||||||
)
|
)
|
||||||
|
}.onAppear {
|
||||||
|
focusedField = .username
|
||||||
}.navigationTitle(L10n.Account.title)
|
}.navigationTitle(L10n.Account.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ extension AddHostView {
|
||||||
|
|
||||||
@State private var isEnteringCredentials = false
|
@State private var isEnteringCredentials = false
|
||||||
|
|
||||||
|
@FocusState private var focusedField: AddProfileView.Field?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
url: URL,
|
url: URL,
|
||||||
deletingURLOnSuccess: Bool,
|
deletingURLOnSuccess: Bool,
|
||||||
|
@ -75,7 +77,11 @@ extension AddHostView {
|
||||||
isPresented: $viewModel.isAskingOverwrite,
|
isPresented: $viewModel.isAskingOverwrite,
|
||||||
actions: alertOverwriteActions,
|
actions: alertOverwriteActions,
|
||||||
message: alertOverwriteMessage
|
message: alertOverwriteMessage
|
||||||
).onAppear(perform: requestResourcePermissions)
|
).onChange(of: viewModel.requiresPassphrase) {
|
||||||
|
if $0 {
|
||||||
|
focusedField = .passphrase
|
||||||
|
}
|
||||||
|
}.onAppear(perform: requestResourcePermissions)
|
||||||
.onDisappear(perform: dropResourcePermissions)
|
.onDisappear(perform: dropResourcePermissions)
|
||||||
.navigationTitle(L10n.AddProfile.Shared.title)
|
.navigationTitle(L10n.AddProfile.Shared.title)
|
||||||
.themeSecondaryView()
|
.themeSecondaryView()
|
||||||
|
@ -91,6 +97,7 @@ private extension AddHostView.NameView {
|
||||||
var mainView: some View {
|
var mainView: some View {
|
||||||
AddProfileView.ProfileNameSection(
|
AddProfileView.ProfileNameSection(
|
||||||
profileName: $viewModel.profileName,
|
profileName: $viewModel.profileName,
|
||||||
|
focusedField: $focusedField,
|
||||||
errorMessage: viewModel.errorMessage
|
errorMessage: viewModel.errorMessage
|
||||||
) {
|
) {
|
||||||
processProfile(replacingExisting: false)
|
processProfile(replacingExisting: false)
|
||||||
|
@ -118,7 +125,7 @@ private extension AddHostView.NameView {
|
||||||
Section {
|
Section {
|
||||||
SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) {
|
SecureField(L10n.AddProfile.Host.Sections.Encryption.footer, text: $viewModel.encryptionPassphrase) {
|
||||||
processProfile(replacingExisting: false)
|
processProfile(replacingExisting: false)
|
||||||
}
|
}.focused($focusedField, equals: .passphrase)
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.Global.Strings.encryption)
|
Text(L10n.Global.Strings.encryption)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,12 @@ import PassepartoutLibrary
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum AddProfileView {
|
enum AddProfileView {
|
||||||
|
enum Field {
|
||||||
|
case name
|
||||||
|
|
||||||
|
case passphrase
|
||||||
|
}
|
||||||
|
|
||||||
struct Bindings {
|
struct Bindings {
|
||||||
@Binding var isPresented: Bool
|
@Binding var isPresented: Bool
|
||||||
}
|
}
|
||||||
|
@ -34,6 +40,8 @@ enum AddProfileView {
|
||||||
struct ProfileNameSection: View {
|
struct ProfileNameSection: View {
|
||||||
@Binding var profileName: String
|
@Binding var profileName: String
|
||||||
|
|
||||||
|
@FocusState.Binding var focusedField: Field?
|
||||||
|
|
||||||
let errorMessage: String?
|
let errorMessage: String?
|
||||||
|
|
||||||
let onCommit: () -> Void
|
let onCommit: () -> Void
|
||||||
|
@ -41,11 +49,14 @@ enum AddProfileView {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.Global.Placeholders.profileName, text: $profileName, onCommit: onCommit)
|
TextField(L10n.Global.Placeholders.profileName, text: $profileName, onCommit: onCommit)
|
||||||
|
.focused($focusedField, equals: .name)
|
||||||
.themeValidProfileName()
|
.themeValidProfileName()
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.Global.Strings.name)
|
Text(L10n.Global.Strings.name)
|
||||||
} footer: {
|
} footer: {
|
||||||
themeErrorMessage(errorMessage)
|
themeErrorMessage(errorMessage)
|
||||||
|
}.onAppear {
|
||||||
|
focusedField = .name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ extension AddProviderView {
|
||||||
|
|
||||||
@State private var isEnteringCredentials = false
|
@State private var isEnteringCredentials = false
|
||||||
|
|
||||||
|
@FocusState private var focusedField: AddProfileView.Field?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
profile: Binding<Profile>,
|
profile: Binding<Profile>,
|
||||||
providerMetadata: ProviderMetadata,
|
providerMetadata: ProviderMetadata,
|
||||||
|
@ -57,6 +59,7 @@ extension AddProviderView {
|
||||||
List {
|
List {
|
||||||
AddProfileView.ProfileNameSection(
|
AddProfileView.ProfileNameSection(
|
||||||
profileName: $viewModel.profileName,
|
profileName: $viewModel.profileName,
|
||||||
|
focusedField: $focusedField,
|
||||||
errorMessage: viewModel.errorMessage
|
errorMessage: viewModel.errorMessage
|
||||||
) {
|
) {
|
||||||
saveProfile(replacingExisting: false)
|
saveProfile(replacingExisting: false)
|
||||||
|
|
|
@ -28,6 +28,10 @@ import SwiftUI
|
||||||
|
|
||||||
extension ProfileView {
|
extension ProfileView {
|
||||||
struct RenameView: View {
|
struct RenameView: View {
|
||||||
|
enum Field {
|
||||||
|
case name
|
||||||
|
}
|
||||||
|
|
||||||
@Environment(\.presentationMode) private var presentationMode
|
@Environment(\.presentationMode) private var presentationMode
|
||||||
|
|
||||||
@ObservedObject private var profileManager: ProfileManager
|
@ObservedObject private var profileManager: ProfileManager
|
||||||
|
@ -38,6 +42,8 @@ extension ProfileView {
|
||||||
|
|
||||||
@State private var isOverwritingExistingProfile = false
|
@State private var isOverwritingExistingProfile = false
|
||||||
|
|
||||||
|
@FocusState private var focusedField: Field?
|
||||||
|
|
||||||
init(currentProfile: ObservableProfile) {
|
init(currentProfile: ObservableProfile) {
|
||||||
profileManager = .shared
|
profileManager = .shared
|
||||||
self.currentProfile = currentProfile
|
self.currentProfile = currentProfile
|
||||||
|
@ -48,6 +54,7 @@ extension ProfileView {
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.Global.Placeholders.profileName, text: $newName, onCommit: commitRenaming)
|
TextField(L10n.Global.Placeholders.profileName, text: $newName, onCommit: commitRenaming)
|
||||||
.themeValidProfileName()
|
.themeValidProfileName()
|
||||||
|
.focused($focusedField, equals: .name)
|
||||||
.onAppear(perform: loadCurrentName)
|
.onAppear(perform: loadCurrentName)
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.Profile.Alerts.Rename.title)
|
Text(L10n.Profile.Alerts.Rename.title)
|
||||||
|
@ -96,6 +103,7 @@ private extension ProfileView.RenameView {
|
||||||
private extension ProfileView.RenameView {
|
private extension ProfileView.RenameView {
|
||||||
func loadCurrentName() {
|
func loadCurrentName() {
|
||||||
newName = currentProfile.value.header.name
|
newName = currentProfile.value.header.name
|
||||||
|
focusedField = .name
|
||||||
}
|
}
|
||||||
|
|
||||||
func commitRenaming() {
|
func commitRenaming() {
|
||||||
|
|
Loading…
Reference in New Issue