diff options
| author | Mojgan <Mojgan.jelodar@codic.se> | 2023-06-07 16:11:59 +0200 |
|---|---|---|
| committer | Mojgan <Mojgan.jelodar@codic.se> | 2023-06-08 19:21:09 +0200 |
| commit | 91b480d0c5b3562c715bcfa831ea1459415a580b (patch) | |
| tree | f404183aa44c93c795216bc4e0f3a6765ef18c2a | |
| parent | e204ff2565a99058cf9a79f69ddc14d82a1b531d (diff) | |
| download | mullvadvpn-91b480d0c5b3562c715bcfa831ea1459415a580b.tar.xz mullvadvpn-91b480d0c5b3562c715bcfa831ea1459415a580b.zip | |
Format text based on group size via separator
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 41 | ||||
| -rw-r--r-- | ios/MullvadVPN/Classes/InputTextFormatter.swift (renamed from ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift) | 107 | ||||
| -rw-r--r-- | ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift | 3 | ||||
| -rw-r--r-- | ios/MullvadVPN/View controllers/Login/AccountTextField.swift | 19 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/AccountTokenInputTests.swift | 132 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/InputTextFormatterTests.swift | 157 |
6 files changed, 262 insertions, 197 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 85f36e1a15..73a534a971 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -84,8 +84,7 @@ 58293FB125124117005D0BB5 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB025124117005D0BB5 /* CustomTextField.swift */; }; 58293FB3251241B4005D0BB5 /* CustomTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58293FB2251241B3005D0BB5 /* CustomTextView.swift */; }; 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */; }; - 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; }; - 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */; }; + 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; }; 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1AE229566420055B6EF /* SettingsCell.swift */; }; 582BB1B1229569620055B6EF /* UINavigationBar+Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */; }; 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5835B7CB233B76CB0096D79F /* TunnelManager.swift */; }; @@ -240,7 +239,7 @@ 58B26E282943527300D5980C /* SystemNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E272943527300D5980C /* SystemNotificationProvider.swift */; }; 58B26E2A2943545A00D5980C /* NotificationManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B26E292943545A00D5980C /* NotificationManagerDelegate.swift */; }; 58B43C1925F77DB60002C8C3 /* TunnelControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* TunnelControlView.swift */; }; - 58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */; }; + 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */; }; 58B8644629C7972F005E107C /* CustomDateComponentsFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */; }; 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B93A1226C3F13600A55733 /* TunnelState.swift */; }; 58B993B12608A34500BA7811 /* LoginContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B993B02608A34500BA7811 /* LoginContentView.swift */; }; @@ -403,6 +402,7 @@ E158B360285381C60002F069 /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; E1FD0DF528AA7CE400299DB4 /* StatusActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */; }; F03580252A13842C00E5DAFD /* IncreasedHitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */; }; + F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */; }; F07CFF2029F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */; }; F0C2AEFD2A0BB5CC00986207 /* NotificationProviderIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */; }; /* End PBXBuildFile section */ @@ -847,8 +847,7 @@ 58293FB025124117005D0BB5 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; }; 58293FB2251241B3005D0BB5 /* CustomTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextView.swift; sourceTree = "<group>"; }; 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedWidthIntegerArithmeticsTests.swift; sourceTree = "<group>"; }; - 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountTokenInput.swift; sourceTree = "<group>"; }; - 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTokenInputTests.swift; sourceTree = "<group>"; }; + 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatter.swift; sourceTree = "<group>"; }; 582BB1AE229566420055B6EF /* SettingsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCell.swift; sourceTree = "<group>"; }; 582BB1B0229569620055B6EF /* UINavigationBar+Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Appearance.swift"; sourceTree = "<group>"; }; 582FFA82290A84E700895745 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; @@ -1115,6 +1114,7 @@ E158B35F285381C60002F069 /* String+AccountFormatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AccountFormatting.swift"; sourceTree = "<group>"; }; E1FD0DF428AA7CE400299DB4 /* StatusActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityView.swift; sourceTree = "<group>"; }; F03580242A13842C00E5DAFD /* IncreasedHitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreasedHitButton.swift; sourceTree = "<group>"; }; + F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextFormatterTests.swift; sourceTree = "<group>"; }; F07CFF1F29F2720E008C0343 /* RegisteredDeviceInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisteredDeviceInAppNotificationProvider.swift; sourceTree = "<group>"; }; F0C2AEFC2A0BB5CC00986207 /* NotificationProviderIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationProviderIdentifier.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -1426,19 +1426,19 @@ 583FE01629C196E8006E85F9 /* View controllers */ = { isa = PBXGroup; children = ( - 583FE02529C1AD0E006E85F9 /* Launch */, - 583FE02229C1AC68006E85F9 /* TermsOfService */, - 583FE02129C1A0F4006E85F9 /* Login */, 583FE02029C1A0B1006E85F9 /* Account */, - 583FE01E29C197D5006E85F9 /* Tunnel */, + 5878F4FA29CDA2D4003D4BE2 /* ChangeLog */, 583FE01D29C197C1006E85F9 /* DeviceList */, - 583FE01C29C19793006E85F9 /* RevokedDevice */, + 583FE02529C1AD0E006E85F9 /* Launch */, + 583FE02129C1A0F4006E85F9 /* Login */, 583FE01B29C19786006E85F9 /* OutOfTime */, - 5878F4FA29CDA2D4003D4BE2 /* ChangeLog */, 583FE01A29C19777006E85F9 /* Preferences */, 583FE01929C19760006E85F9 /* ProblemReport */, - 583FE01829C19709006E85F9 /* Settings */, + 583FE01C29C19793006E85F9 /* RevokedDevice */, 583FE01729C196F3006E85F9 /* SelectLocation */, + 583FE01829C19709006E85F9 /* Settings */, + 583FE02229C1AC68006E85F9 /* TermsOfService */, + 583FE01E29C197D5006E85F9 /* Tunnel */, ); path = "View controllers"; sourceTree = "<group>"; @@ -1577,12 +1577,11 @@ 583FE02129C1A0F4006E85F9 /* Login */ = { isa = PBXGroup; children = ( - 58B993B02608A34500BA7811 /* LoginContentView.swift */, - 58CE5E65224146200008646E /* LoginViewController.swift */, - 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */, - 582AE30F2440A6CA00E6733A /* AccountTokenInput.swift */, 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, 58CCA01D2242787B004F3011 /* AccountTextField.swift */, + 58B993B02608A34500BA7811 /* LoginContentView.swift */, + 58CAF9FF2983FF0200BE19F7 /* LoginInteractor.swift */, + 58CE5E65224146200008646E /* LoginViewController.swift */, ); path = Login; sourceTree = "<group>"; @@ -1662,6 +1661,7 @@ 5896AE83246D5889005B36CB /* CustomDateComponentsFormatting.swift */, 58138E60294871C600684F0C /* DeviceDataThrottling.swift */, 7A7AD28C29DC677800480EF1 /* FirstTimeLaunch.swift */, + 582AE30F2440A6CA00E6733A /* InputTextFormatter.swift */, 58CC40EE24A601900019D96E /* ObserverList.swift */, 5872D6E7286304DE00DB5F4E /* TermsOfService.swift */, ); @@ -1854,11 +1854,11 @@ 58B0A2A1238EE67E00BC001D /* MullvadVPNTests */ = { isa = PBXGroup; children = ( - 582AE3112440CA0D00E6733A /* AccountTokenInputTests.swift */, 5896AE85246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift */, 58915D622A25F8400066445B /* DeviceCheckOperationTests.swift */, 582A8A3928BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift */, 58B0A2A4238EE67E00BC001D /* Info.plist */, + F07BF2572A26112D00042943 /* InputTextFormatterTests.swift */, 584B26F3237434D00073B10E /* RelaySelectorTests.swift */, 5807E2C1243203D000F5FF30 /* StringTests.swift */, 58165EBD2A262CBB00688EAD /* WgKeyRotationTests.swift */, @@ -2838,12 +2838,12 @@ buildActionMask = 2147483647; files = ( 58915D6A2A26031B0066445B /* DNSSettings.swift in Sources */, - 58B8644529C7971B005E107C /* AccountTokenInput.swift in Sources */, + 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */, 58915D692A2601FB0066445B /* WgKeyRotation.swift in Sources */, - 582AE3122440CA0D00E6733A /* AccountTokenInputTests.swift in Sources */, 580810E62A30E13D00B74552 /* DeviceStateAccessorProtocol.swift in Sources */, 58915D642A25F8B30066445B /* DeviceCheckOperation.swift in Sources */, 58915D652A25F9E20066445B /* TunnelSettingsV2.swift in Sources */, + 58B8644529C7971B005E107C /* InputTextFormatter.swift in Sources */, 582A8A3A28BCE19B00D0F9FB /* FixedWidthIntegerArithmeticsTests.swift in Sources */, 58915D632A25F8400066445B /* DeviceCheckOperationTests.swift in Sources */, 5896AE86246D6AD8005B36CB /* CustomDateComponentsFormattingTests.swift in Sources */, @@ -2852,6 +2852,7 @@ 58165EBE2A262CBB00688EAD /* WgKeyRotationTests.swift in Sources */, 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, 580810E92A30E17300B74552 /* DeviceCheckRemoteServiceProtocol.swift in Sources */, + F07BF2582A26112D00042943 /* InputTextFormatterTests.swift in Sources */, 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2933,7 +2934,7 @@ 58138E61294871C600684F0C /* DeviceDataThrottling.swift in Sources */, 5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */, 7A818F1F29F0305800C7F0F4 /* RootConfiguration.swift in Sources */, - 582AE3102440A6CA00E6733A /* AccountTokenInput.swift in Sources */, + 582AE3102440A6CA00E6733A /* InputTextFormatter.swift in Sources */, 5820EDAB288FF0D2006BF4E4 /* DeviceRowView.swift in Sources */, 5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */, 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */, diff --git a/ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift b/ios/MullvadVPN/Classes/InputTextFormatter.swift index 1fef51a479..89e1df7607 100644 --- a/ios/MullvadVPN/View controllers/Login/AccountTokenInput.swift +++ b/ios/MullvadVPN/Classes/InputTextFormatter.swift @@ -1,5 +1,5 @@ // -// AccountTokenInput.swift +// InputTextFormatter.swift // MullvadVPN // // Created by pronebird on 08/04/2020. @@ -11,15 +11,33 @@ import UIKit /// A class describing the account token input and caret management. /// Suitable to be used with `UITextField`. -class AccountTokenInput: NSObject { - /// The group separator character - static let groupSeparator: Character = " " +class InputTextFormatter: NSObject, UITextFieldDelegate, UITextPasteDelegate { + enum AllowedInput { + case numeric, alphanumeric(isUpperCase: Bool) + } + + struct Configuration { + /// Allowed characters. + var allowedInput: AllowedInput + + /// Separator between groups of characters. + var groupSeparator: Character - /// The character size of each group of digits - static let groupSize = 4 + /// The size of each group of characters . + var groupSize: UInt8 - /// Parsed account token string - private(set) var parsedString = "" + /// Maximum number of groups of characters allowed. + var maxGroups: UInt + } + + var configuration: Configuration { + didSet { + replace(with: string) + } + } + + /// Parsed string + private(set) var string = "" /// Formatted string private(set) var formattedString = "" @@ -27,7 +45,8 @@ class AccountTokenInput: NSObject { // Computed caret position private(set) var caretPosition = 0 - init(string: String = "") { + init(string: String = "", configuration: Configuration) { + self.configuration = configuration super.init() replace(with: string) @@ -45,13 +64,8 @@ class AccountTokenInput: NSObject { } /// Replace characters in range maintaining the caret position - /// - /// - Parameter range: a range within a string to replace - /// - Parameter replacementString: a string to replace the characters in the given range - /// - Parameter emptySelection: a hint to indicate if the text field selection is empty. - /// This is normally the default state unless a text range is - /// selected. - /// + /// Note: `emptySelection` hints that text field selection is empty. This is normally + /// the default state unless a text range is selected. func replaceCharacters( in range: Range<String.Index>, replacementString: String, @@ -61,10 +75,12 @@ class AccountTokenInput: NSObject { // Since removing separator alone makes no sense, this computation extends the string range // to include the digit preceding a separator. - if replacementString.isEmpty, emptySelection, !formattedString.isEmpty { + if replacementString.isEmpty, emptySelection, + !formattedString.isEmpty + { let precedingDigitIndex = formattedString .prefix(through: stringRange.lowerBound) - .lastIndex { Self.isDigit($0) } ?? formattedString.startIndex + .lastIndex { isAllowed($0) } ?? formattedString.startIndex stringRange = precedingDigitIndex ..< stringRange.upperBound } @@ -96,7 +112,7 @@ class AccountTokenInput: NSObject { for (index, element) in newString.enumerated() { // Skip disallowed characters - if !Self.isDigit(element) { + if !isAllowed(element) { // Adjust the caret position for characters removed before the insertion location if originalCaretPosition > index { newCaretPosition -= 1 @@ -104,9 +120,24 @@ class AccountTokenInput: NSObject { continue } + // Apply cap on number of groups of characters that can be entered. + if configuration.maxGroups > 0 { + let numGroups = reparsedString.count / Int(configuration.groupSize) + + if numGroups >= configuration.maxGroups { + if originalCaretPosition > index { + newCaretPosition = reformattedString.count + } + break + } + } + // Add separator between the groups of digits - if numDigits > 0, numDigits % Self.groupSize == 0 { - reformattedString.append(Self.groupSeparator) + if numDigits > 0, + configuration.groupSize > 0, + numDigits % Int(configuration.groupSize) == 0 + { + reformattedString.append(configuration.groupSeparator) if originalCaretPosition > index { // Adjust the caret position to account for separators added before the @@ -120,22 +151,15 @@ class AccountTokenInput: NSObject { numDigits += 1 } + if case AllowedInput.alphanumeric(true) = configuration.allowedInput { + reformattedString = reformattedString.uppercased() + } + caretPosition = newCaretPosition formattedString = reformattedString - parsedString = reparsedString + string = reparsedString } - private class func isDigit(_ character: Character) -> Bool { - switch character { - case "0" ... "9": - return true - default: - return false - } - } -} - -extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate { /// Update the text and caret position in the given text field func updateTextField(_ textField: UITextField) { updateTextField(textField, notifyDelegate: false) @@ -197,7 +221,7 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate { // MARK: - Private - /// A caret position as utf-16 offset compatible for use with `NSString` and `UITextField` + /// A caret position as utf-16 offset compatible for use with `NSString` and `UITextField`. private var caretPositionUtf16: Int { let startIndex = formattedString.startIndex let endIndex = formattedString.index(startIndex, offsetBy: caretPosition) @@ -205,7 +229,7 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate { return formattedString.utf16.distance(from: startIndex, to: endIndex) } - /// Convert the computed caret position to an empty `UITextRange` within the given text field + /// Convert the computed caret position to an empty `UITextRange` within the given text field. private func caretTextRange(in textField: UITextField) -> UITextRange? { guard let position = textField.position( from: textField.beginningOfDocument, @@ -216,7 +240,7 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate { } /// A helper to update the text and caret in the given text field, and optionally post - /// `UITextField.textDidChange` notification + /// `UITextField.textDidChange` notification. private func updateTextField(_ textField: UITextField, notifyDelegate: Bool) { textField.text = formattedString textField.selectedTextRange = caretTextRange(in: textField) @@ -226,11 +250,20 @@ extension AccountTokenInput: UITextFieldDelegate, UITextPasteDelegate { } } - /// Post `UITextField.textDidChange` notification + /// Posts `UITextField.textDidChange` notification. private class func notifyTextDidChange(in textField: UITextField) { NotificationCenter.default.post( name: UITextField.textDidChangeNotification, object: textField ) } + + private func isAllowed(_ character: Character) -> Bool { + switch configuration.allowedInput { + case .numeric: + return character.isASCII && character.isNumber + case .alphanumeric: + return character.isASCII && character.isLetter + } + } } diff --git a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift index 4ea71c6b9c..ec298656ca 100644 --- a/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift +++ b/ios/MullvadVPN/View controllers/Login/AccountInputGroupView.swift @@ -9,9 +9,10 @@ import UIKit private let animationDuration: TimeInterval = 0.25 -private let minimumAccountTokenLength = 10 final class AccountInputGroupView: UIView { + private let minimumAccountTokenLength = 10 + enum Style { case normal, error, authenticating } diff --git a/ios/MullvadVPN/View controllers/Login/AccountTextField.swift b/ios/MullvadVPN/View controllers/Login/AccountTextField.swift index 74c6c1c45e..cf3412ab6c 100644 --- a/ios/MullvadVPN/View controllers/Login/AccountTextField.swift +++ b/ios/MullvadVPN/View controllers/Login/AccountTextField.swift @@ -9,7 +9,12 @@ import UIKit class AccountTextField: CustomTextField, UITextFieldDelegate { - private let input = AccountTokenInput() + private let inputFormatter = InputTextFormatter(configuration: InputTextFormatter.Configuration( + allowedInput: .numeric, + groupSeparator: " ", + groupSize: 4, + maxGroups: 4 + )) var onReturnKey: ((AccountTextField) -> Bool)? @@ -20,7 +25,7 @@ class AccountTextField: CustomTextField, UITextFieldDelegate { cornerRadius = 0 delegate = self - pasteDelegate = input + pasteDelegate = inputFormatter NotificationCenter.default.addObserver( self, @@ -36,16 +41,16 @@ class AccountTextField: CustomTextField, UITextFieldDelegate { var autoformattingText: String { set { - input.replace(with: newValue) - input.updateTextField(self) + inputFormatter.replace(with: newValue) + inputFormatter.updateTextField(self) } get { - input.formattedString + inputFormatter.formattedString } } var parsedToken: String { - return input.parsedString + return inputFormatter.string } var enableReturnKey = true { @@ -70,7 +75,7 @@ class AccountTextField: CustomTextField, UITextFieldDelegate { shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool { - return input.textField( + return inputFormatter.textField( textField, shouldChangeCharactersIn: range, replacementString: string diff --git a/ios/MullvadVPNTests/AccountTokenInputTests.swift b/ios/MullvadVPNTests/AccountTokenInputTests.swift deleted file mode 100644 index 82a09e1285..0000000000 --- a/ios/MullvadVPNTests/AccountTokenInputTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// AccountTokenInputTests.swift -// MullvadVPNTests -// -// Created by pronebird on 10/04/2020. -// Copyright © 2020 Mullvad VPN AB. All rights reserved. -// - -import XCTest - -private let kSampleToken = "12345678" - -class AccountTokenInputTests: XCTestCase { - func testInitialValue() { - let input = AccountTokenInput(string: kSampleToken) - - XCTAssertEqual(input.formattedString, "1234 5678") - XCTAssertEqual(input.caretPosition, 9) - } - - func testReplacingValue() { - let input = AccountTokenInput() - input.replace(with: "00000000") - - XCTAssertEqual(input.formattedString, "0000 0000") - XCTAssertEqual(input.caretPosition, 9) - } - - func testRemovingSeparator() { - let input = AccountTokenInput(string: kSampleToken) - - input.replaceCharacters( - in: input.formattedString.range(withOffset: 4, length: 1), - replacementString: "", - emptySelection: true - ) - - XCTAssertEqual(input.formattedString, "1235 678") - XCTAssertEqual(input.caretPosition, 3) - } - - func testRemovingSeparatorRange() { - let input = AccountTokenInput(string: kSampleToken) - - input.replaceCharacters( - in: input.formattedString.range(withOffset: 4, length: 1), - replacementString: "", - emptySelection: false - ) - - XCTAssertEqual(input.formattedString, "1234 5678") - XCTAssertEqual(input.caretPosition, 4) - } - - func testRemovingRange() { - let input = AccountTokenInput(string: kSampleToken) - - input.replaceCharacters( - in: input.formattedString.range(withOffset: 7, length: 2), - replacementString: "", - emptySelection: false - ) - - XCTAssertEqual(input.formattedString, "1234 56") - XCTAssertEqual(input.caretPosition, 7) - } - - func testInserting() { - let input = AccountTokenInput(string: kSampleToken) - - input.replaceCharacters( - in: input.formattedString.range(withOffset: 5, length: 0), - replacementString: "0000", - emptySelection: true - ) - - XCTAssertEqual(input.formattedString, "1234 0000 5678") - XCTAssertEqual(input.caretPosition, 9) - } - - func testReplacingRange() { - let input = AccountTokenInput(string: kSampleToken) - - input.replaceCharacters( - in: input.formattedString.range(withOffset: 5, length: 4), - replacementString: "0000", - emptySelection: false - ) - - XCTAssertEqual(input.formattedString, "1234 0000") - XCTAssertEqual(input.caretPosition, 9) - } - - func testInvalidCharactersReplacesTextFieldTextWithFormattedString() { - let input = AccountTokenInput(string: kSampleToken) - let invalidRange = NSRange(location: kSampleToken.count + 1, length: 0) - let textField = UITextField() - - _ = input.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "´") - - XCTAssertEqual(textField.text, input.formattedString) - } - - func testDeleteCharacterOutsideOfTokenBoundaryDoesNotDeleteAnything() { - let input = AccountTokenInput(string: kSampleToken) - let invalidRange = NSRange(location: kSampleToken.count + 1, length: 1) - let textField = UITextField() - - _ = input.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "") - - XCTAssertEqual(textField.text, input.formattedString) - } - - func testDeleteLastCharacter() { - let input = AccountTokenInput(string: kSampleToken) - let lastCharacterRange = NSRange(location: kSampleToken.count, length: 1) - let textField = UITextField() - - _ = input.textField(textField, shouldChangeCharactersIn: lastCharacterRange, replacementString: "") - - XCTAssertEqual(textField.text, "1234 567") - } -} - -private extension String { - func range(withOffset offset: Int, length: Int) -> Range<String.Index> { - let start = index(startIndex, offsetBy: offset) - let end = index(start, offsetBy: length) - - return start ..< end - } -} diff --git a/ios/MullvadVPNTests/InputTextFormatterTests.swift b/ios/MullvadVPNTests/InputTextFormatterTests.swift new file mode 100644 index 0000000000..5ff8d0daef --- /dev/null +++ b/ios/MullvadVPNTests/InputTextFormatterTests.swift @@ -0,0 +1,157 @@ +// +// InputTextFormatterTests.swift +// MullvadVPNTests +// +// Created by pronebird on 10/04/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +import XCTest + +class InputTextFormatterTests: XCTestCase { + private let accountNumber = "12345678" + private var inputTextFormatter: InputTextFormatter! + + private let configuration = InputTextFormatter.Configuration( + allowedInput: .numeric, + groupSeparator: " ", + groupSize: 4, + maxGroups: 4 + ) + + override func setUp() { + inputTextFormatter = InputTextFormatter( + string: accountNumber, + configuration: configuration + ) + } + + override func tearDown() { + inputTextFormatter = nil + } + + func testInitialValue() { + XCTAssertEqual(inputTextFormatter.formattedString, "1234 5678") + XCTAssertEqual(inputTextFormatter.caretPosition, 9) + } + + func testReplacingValue() { + inputTextFormatter.replace(with: "00000000") + + XCTAssertEqual(inputTextFormatter.formattedString, "0000 0000") + XCTAssertEqual(inputTextFormatter.caretPosition, 9) + } + + func testRemovingSeparator() { + guard let range = inputTextFormatter.formattedString.range(withOffset: 4, length: 1) else { + return XCTAssertNil("Out of range", file: #file, line: #line) + } + + inputTextFormatter.replaceCharacters( + in: range, + replacementString: "", + emptySelection: true + ) + + XCTAssertEqual(inputTextFormatter.formattedString, "1235 678") + XCTAssertEqual(inputTextFormatter.caretPosition, 3) + } + + func testRemovingSeparatorRange() { + guard let range = inputTextFormatter.formattedString.range(withOffset: 4, length: 1) else { + return XCTAssertNil("Out of range", file: #file, line: #line) + } + + inputTextFormatter.replaceCharacters( + in: range, + replacementString: "", + emptySelection: false + ) + + XCTAssertEqual(inputTextFormatter.formattedString, "1234 5678") + XCTAssertEqual(inputTextFormatter.caretPosition, 4) + } + + func testRemovingRange() { + guard let range = inputTextFormatter.formattedString.range(withOffset: 7, length: 2) else { + return XCTAssertNil("Out of range", file: #file, line: #line) + } + + inputTextFormatter.replaceCharacters( + in: range, + replacementString: "", + emptySelection: false + ) + + XCTAssertEqual(inputTextFormatter.formattedString, "1234 56") + XCTAssertEqual(inputTextFormatter.caretPosition, 7) + } + + func testInserting() { + guard let range = inputTextFormatter.formattedString.range(withOffset: 5, length: 0) else { + return XCTAssertNil("Out of range", file: #file, line: #line) + } + + inputTextFormatter.replaceCharacters( + in: range, + replacementString: "0000", + emptySelection: true + ) + + XCTAssertEqual(inputTextFormatter.formattedString, "1234 0000 5678") + XCTAssertEqual(inputTextFormatter.caretPosition, 9) + } + + func testReplacingRange() { + guard let range = inputTextFormatter.formattedString.range(withOffset: 5, length: 4) else { + return XCTAssertNil("Out of range", file: #file, line: #line) + } + + inputTextFormatter.replaceCharacters( + in: range, + replacementString: "0000", + emptySelection: false + ) + + XCTAssertEqual(inputTextFormatter.formattedString, "1234 0000") + XCTAssertEqual(inputTextFormatter.caretPosition, 9) + } + + func testInvalidCharactersReplacesTextFieldTextWithFormattedString() { + let invalidRange = NSRange(location: accountNumber.count + 1, length: 0) + let textField = UITextField() + + _ = inputTextFormatter.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "´") + + XCTAssertEqual(textField.text, inputTextFormatter.formattedString) + } + + func testDeleteCharacterOutsideOfTokenBoundaryDoesNotDeleteAnything() { + let invalidRange = NSRange(location: accountNumber.count + 1, length: 1) + let textField = UITextField() + + _ = inputTextFormatter.textField(textField, shouldChangeCharactersIn: invalidRange, replacementString: "") + + XCTAssertEqual(textField.text, inputTextFormatter.formattedString) + } + + func testDeleteLastCharacter() { + let lastCharacterRange = NSRange(location: accountNumber.count, length: 1) + let textField = UITextField() + + _ = inputTextFormatter.textField(textField, shouldChangeCharactersIn: lastCharacterRange, replacementString: "") + + XCTAssertEqual(textField.text, "1234 567") + } +} + +private extension String { + func range(withOffset offset: Int, length: Int) -> Range<String.Index>? { + guard let start = index(startIndex, offsetBy: offset, limitedBy: endIndex), + let end = index(start, offsetBy: length, limitedBy: endIndex) + else { + return nil + } + return start ..< end + } +} |
