Skip to content

Commit 31e260e

Browse files
committed
chore: symbols for obfuscation and placeholder, borders, cursor location on focus (#998)
Signed-off-by: Pierre-Yves Lapersonne <pierreyves.lapersonne@orange.com>
1 parent 8d35721 commit 31e260e

File tree

5 files changed

+120
-15
lines changed

5 files changed

+120
-15
lines changed

OUDS/Core/Components/Sources/Controls/PinCodeInput/Internal/PinCode+Backspace.swift

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import SwiftUI
3131
struct BackspaceDetectingTextField: UIViewRepresentable {
3232

3333
@Binding var text: String
34+
@Binding var displayText: String
35+
let font: UIFont
3436
let index: Int
3537
let onBackspace: () -> Void
3638

@@ -48,6 +50,8 @@ struct BackspaceDetectingTextField: UIViewRepresentable {
4850
textField.onBackspace = onBackspace
4951
textField.fieldIndex = index
5052

53+
textField.font = font
54+
5155
// Set low priorities to allow the frame modifier to control the size
5256
textField.setContentHuggingPriority(.defaultLow, for: .vertical)
5357
textField.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -67,8 +71,8 @@ struct BackspaceDetectingTextField: UIViewRepresentable {
6771
uiView.onBackspace = onBackspace
6872

6973
// Only update the text if it's different to avoid unnecessary updates
70-
if uiView.text != text {
71-
uiView.text = text
74+
if uiView.text != displayText {
75+
uiView.text = displayText
7276
}
7377
}
7478

@@ -108,17 +112,19 @@ struct BackspaceDetectingTextField: UIViewRepresentable {
108112
return true
109113
}
110114

111-
// For character additions, update the binding
112-
let currentText = textField.text ?? ""
113-
guard let stringRange = Range(range, in: currentText) else {
114-
return false
115-
}
116-
let updatedText = currentText.replacingCharacters(in: stringRange, with: string)
115+
// Filter to only allow numeric characters and take only the first one
116+
let filtered = String(string.filter(\.isNumber).prefix(1))
117117

118-
// Update the binding for additions (this will trigger onChange in SwiftUI)
119-
text = updatedText
118+
DispatchQueue.main.async { [weak self] in
119+
guard let self else { return }
120+
if !filtered.isEmpty {
121+
// Update the binding with the real digit
122+
text = filtered
123+
}
124+
}
120125

121-
return true
126+
// false: prevent direct display of text ; it will be updated with updateUIView using displayText
127+
return false
122128
}
123129
}
124130
}
@@ -159,5 +165,24 @@ final class BackspaceTextField: UITextField {
159165
override var intrinsicContentSize: CGSize {
160166
CGSize(width: 0, height: 0)
161167
}
168+
169+
/// To put the cursor in the field after any symbol
170+
override func becomeFirstResponder() -> Bool {
171+
let result = super.becomeFirstResponder()
172+
173+
if result {
174+
DispatchQueue.main.async { [weak self] in
175+
guard let self else { return }
176+
177+
// Positionner le curseur à la fin du texte
178+
if let text, !text.isEmpty {
179+
let endPosition = endOfDocument
180+
selectedTextRange = textRange(from: endPosition, to: endPosition)
181+
}
182+
}
183+
}
184+
185+
return result
186+
}
162187
}
163188
#endif

OUDS/Core/Components/Sources/Controls/PinCodeInput/Internal/PinCodeInputContainer.swift

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
import OUDSTokensSemantic
1616
import SwiftUI
1717

18+
// MARK: - Characters
19+
20+
/// The symbol to use to hide the written digit
21+
let kPinCodeInputObfuscationCharacter = ""
22+
23+
/// The symbol to use as placeholder for digit to fill
24+
let kPinCodeInputPlaceholderCharacter = "-"
25+
26+
// MARK: - Pin Code Input Container
27+
1828
struct PinCodeInputContainer: View {
1929

2030
// MARK: - Properties
@@ -33,6 +43,8 @@ struct PinCodeInputContainer: View {
3343
/// The digits written one by one by the user before being exposed through `value`
3444
@State private var digits: [String]
3545

46+
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
47+
@Environment(\.verticalSizeClass) private var verticalSizeClass
3648
@Environment(\.colorScheme) private var colorScheme
3749
@Environment(\.theme) private var theme
3850

@@ -136,9 +148,36 @@ struct PinCodeInputContainer: View {
136148

137149
// MARK: - Digits fields
138150

151+
/// Returns the string to display for a digit field:
152+
/// - `kPinCodeInputPlaceholderCharacter` for placeholder on not focused input
153+
/// - `kPinCodeInputObfuscationCharacter` for filled input
154+
/// - empty string for input focused without text yet
155+
///
156+
/// - Parameter index: The index of the field
157+
/// - Returns: The character to display in the field
158+
private func displayText(for index: Int) -> String {
159+
if digits[index].isEmpty {
160+
if focusedIndex == index {
161+
""
162+
} else {
163+
kPinCodeInputPlaceholderCharacter
164+
}
165+
} else {
166+
kPinCodeInputObfuscationCharacter
167+
}
168+
}
169+
139170
private func digitField(at index: Int) -> some View {
140-
BackspaceDetectingTextField(
171+
// Compute at this level the typography to use to be sure environement values for size class
172+
// are retrieved in the suitable thread at the best moment
173+
let uiFont = TypographyModifier.makeUIFont(
174+
family: nil,
175+
from: theme.fonts.bodyDefaultMedium,
176+
isCompact: horizontalSizeClass == .compact || verticalSizeClass == .compact)
177+
return BackspaceDetectingTextField(
141178
text: $digits[index],
179+
displayText: .constant(displayText(for: index)),
180+
font: uiFont,
142181
index: index,
143182
onBackspace: {
144183
handleBackspace(at: index)

OUDS/Core/Components/Sources/_/ViewModifiers/TypographyModifiers/TypographyModifier.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ struct TypographyModifier: ViewModifier {
117117
lineSpacing / 2
118118
}
119119

120+
// MARK: - View Modifier
121+
120122
/// Applies to the `Content` the *adaptive font* (i.e. *font family*, *font weight* and *font size*)
121123
/// depending to the current `MultipleFontCompositeSemanticToken`.
122124
func body(content: Content) -> some View {
@@ -139,6 +141,39 @@ struct TypographyModifier: ViewModifier {
139141
.modifier(OnChangeSizeCategoryModifier(sizeCategory: sizeCategory))
140142
}
141143

144+
// MARK: - UIKit util
145+
146+
/// Génère une UIFont à partir des tokens sans dépendre de @Environment
147+
/// - Parameters:
148+
/// - family: La famille de police personnalisée (optionnelle)
149+
/// - fontToken: Le token de police à utiliser
150+
/// - isCompact: Si true, utilise le mode compact, sinon le mode regular
151+
/// - sizeCategory: La catégorie de taille pour Dynamic Type
152+
/// - Returns: La UIFont configurée
153+
static func makeUIFont(
154+
family: FontFamilyRawToken?,
155+
from fontToken: MultipleFontCompositeSemanticToken,
156+
isCompact: Bool,
157+
sizeCategory: ContentSizeCategory = .medium) -> UIFont
158+
{
159+
let adaptiveFontToken = isCompact ? fontToken.compact : fontToken.regular
160+
161+
#if os(macOS)
162+
let scaledFontSize = adaptiveFontToken.size
163+
#else
164+
let scaledFontSize = UIFontMetrics.default.scaledValue(for: adaptiveFontToken.size)
165+
#endif
166+
167+
if let family {
168+
let composedFontFamily = kApplePostScriptFontNames[orKey: PSFNMK(family, Font.Weight(weight: adaptiveFontToken.weight))]
169+
if let customFont = UIFont(name: composedFontFamily, size: scaledFontSize) {
170+
return customFont
171+
}
172+
}
173+
174+
return UIFont.systemFont(ofSize: scaledFontSize, weight: Font.Weight(weight: adaptiveFontToken.weight).nativeFontWeight)
175+
}
176+
142177
// MARK: - On Change Size Category Modifier
143178

144179
private struct OnChangeSizeCategoryModifier: ViewModifier {

OUDS/Core/Components/Tests/ContentDisplay/BulletListItem+Tests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
@testable import OUDSComponents
1515
import Testing
1616

17-
struct BulletListItemTests {
17+
struct OUDSBulletListItemTests {
1818

1919
// MARK: - Latin alphabet, without prior label
2020

OUDS/Core/Components/Tests/Controls/PinCodeInput/OUDSPinCodeInputTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@
1212
//
1313

1414
#if !os(watchOS) && !os(macOS)
15-
import OUDSComponents
15+
@testable import OUDSComponents
1616
import Testing
1717

1818
/// Tests some API for `OUDSPinCodeInput`
1919
struct OUDSPinCodeInputTests {
2020

2121
/// Test the raw values for the length of the component
22-
func pinCodeInputLengthValues() {
22+
@Test func pinCodeInputLengthValues() {
2323
#expect(OUDSPinCodeInput.Length.four.rawValue == 4)
2424
#expect(OUDSPinCodeInput.Length.six.rawValue == 6)
2525
#expect(OUDSPinCodeInput.Length.eight.rawValue == 8)
2626
}
27+
28+
/// Test the string values used for obfuscation and placeholders
29+
@Test func pinCodeInputPredefinedSymbols() {
30+
#expect(kPinCodeInputObfuscationCharacter == "")
31+
#expect(kPinCodeInputPlaceholderCharacter == "-")
32+
}
2733
}
2834
#endif

0 commit comments

Comments
 (0)