From 437f7a3995ecc61dc20151995798f78cc5abf029 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Tue, 2 Apr 2024 14:17:25 -0600 Subject: [PATCH] iOS - User Sign In Unmask Password (#1011) --- Swiftfin.xcodeproj/project.pbxproj | 4 + Swiftfin/Components/UnmaskSecureField.swift | 103 ++++++++++++++++++ .../Views/UserSignInView/UserSignInView.swift | 10 +- 3 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 Swiftfin/Components/UnmaskSecureField.swift diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index f0c1607a..e3a66f1e 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -665,6 +665,7 @@ E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */; }; E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; }; E1CFE28028FA606800B7D34C /* ChapterTrack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CFE27F28FA606800B7D34C /* ChapterTrack.swift */; }; + E1D27EE72BBC955F00152D16 /* UnmaskSecureField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D27EE62BBC955F00152D16 /* UnmaskSecureField.swift */; }; E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3043428D1763100587289 /* SeeAllButton.swift */; }; E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D3044328D1991900587289 /* LibraryViewTypeToggle.swift */; }; E1D37F482B9C648E00343D2B /* MaxHeightText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D37F472B9C648E00343D2B /* MaxHeightText.swift */; }; @@ -1246,6 +1247,7 @@ E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectOrientationModifier.swift; sourceTree = ""; }; E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; E1CFE27F28FA606800B7D34C /* ChapterTrack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterTrack.swift; sourceTree = ""; }; + E1D27EE62BBC955F00152D16 /* UnmaskSecureField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnmaskSecureField.swift; sourceTree = ""; }; E1D3043428D1763100587289 /* SeeAllButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeeAllButton.swift; sourceTree = ""; }; E1D3044328D1991900587289 /* LibraryViewTypeToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewTypeToggle.swift; sourceTree = ""; }; E1D37F472B9C648E00343D2B /* MaxHeightText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxHeightText.swift; sourceTree = ""; }; @@ -1855,6 +1857,7 @@ E1D3043428D1763100587289 /* SeeAllButton.swift */, E1D5C39728DF914100CDBEFB /* Slider */, E1581E26291EF59800D6C640 /* SplitContentView.swift */, + E1D27EE62BBC955F00152D16 /* UnmaskSecureField.swift */, E157562F29355B7900976E1F /* UpdateView.swift */, E192607F28D28AAD002314B4 /* UserProfileButton.swift */, ); @@ -3873,6 +3876,7 @@ E1AD104D26D96CE3003E4A08 /* BaseItemDto.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, 535870AD2669D8DD00D05A09 /* ItemFilterCollection.swift in Sources */, + E1D27EE72BBC955F00152D16 /* UnmaskSecureField.swift in Sources */, E1CAF65D2BA345830087D991 /* MediaType.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGuidPair.swift in Sources */, E18A8E7D28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, diff --git a/Swiftfin/Components/UnmaskSecureField.swift b/Swiftfin/Components/UnmaskSecureField.swift new file mode 100644 index 00000000..630157e8 --- /dev/null +++ b/Swiftfin/Components/UnmaskSecureField.swift @@ -0,0 +1,103 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct UnmaskSecureField: UIViewRepresentable { + + @Binding + private var text: String + + let title: String + + init(_ title: String, text: Binding) { + self.title = title + self._text = text + } + + func makeUIView(context: Context) -> some UIView { + + let textField = UITextField() + textField.isSecureTextEntry = true + textField.keyboardType = .asciiCapable + textField.placeholder = title + textField.text = text + textField.addTarget(context.coordinator, action: #selector(Coordinator.textDidChange), for: .editingChanged) + + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(context.coordinator, action: #selector(Coordinator.buttonPressed), for: .touchUpInside) + button.setImage(UIImage(systemName: "eye.fill"), for: .normal) + + NSLayoutConstraint.activate([ + button.heightAnchor.constraint(equalToConstant: 50), + button.widthAnchor.constraint(equalToConstant: 50), + ]) + + textField.rightView = button + textField.rightViewMode = .always + + context.coordinator.button = button + context.coordinator.textField = textField + context.coordinator.textDidChange() + context.coordinator.textBinding = _text + + return textField + } + + func updateUIView(_ uiView: UIViewType, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator { + + weak var button: UIButton? + weak var textField: UITextField? + var textBinding: Binding = .constant("") + + @objc + func buttonPressed() { + guard let textField else { return } + textField.toggleSecureEntry() + + let eye = textField.isSecureTextEntry ? "eye.fill" : "eye.slash" + button?.setImage(UIImage(systemName: eye), for: .normal) + } + + @objc + func textDidChange() { + guard let textField, let text = textField.text else { return } + button?.isEnabled = !text.isEmpty + textBinding.wrappedValue = text + } + } +} + +private extension UITextField { + + // https://stackoverflow.com/a/48115361 + func toggleSecureEntry() { + + isSecureTextEntry.toggle() + + if let existingText = text, isSecureTextEntry { + deleteBackward() + + if let textRange = textRange(from: beginningOfDocument, to: endOfDocument) { + replace(textRange, withText: existingText) + } + } + + if let existingSelectedTextRange = selectedTextRange { + selectedTextRange = nil + selectedTextRange = existingSelectedTextRange + } + } +} diff --git a/Swiftfin/Views/UserSignInView/UserSignInView.swift b/Swiftfin/Views/UserSignInView/UserSignInView.swift index cda18a4a..40fd454e 100644 --- a/Swiftfin/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/UserSignInView.swift @@ -32,12 +32,12 @@ struct UserSignInView: View { private var signInSection: some View { Section { TextField(L10n.username, text: $username) - .disableAutocorrection(true) - .autocapitalization(.none) + .autocorrectionDisabled() + .textInputAutocapitalization(.none) - SecureField(L10n.password, text: $password) - .disableAutocorrection(true) - .autocapitalization(.none) + UnmaskSecureField(L10n.password, text: $password) + .autocorrectionDisabled() + .textInputAutocapitalization(.none) if viewModel.isLoading { Button(role: .destructive) {