Skip to content

Commit a0b503d

Browse files
committed
refactor: Reorganize PrimerInputFieldContainer structure
Split container into focused files for better maintainability: - Core struct and convenience initializer in main file - View rendering logic in +Rendering extension - Styling properties in +Styling extension - Preview helpers in +PreviewHelpers extension Updated preview format to use #Preview macro and changed access control to internal to support multi-file organization.
1 parent 03c8baf commit a0b503d

File tree

4 files changed

+301
-263
lines changed

4 files changed

+301
-263
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//
2+
// PrimerInputFieldContainer+PreviewHelpers.swift
3+
//
4+
// Copyright © 2025 Primer API Ltd. All rights reserved.
5+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
6+
7+
import SwiftUI
8+
9+
#if DEBUG
10+
@available(iOS 15.0, *)
11+
struct PreviewContainer: View {
12+
let label: String?
13+
let text: String
14+
let errorMessage: String?
15+
16+
@State private var currentText: String
17+
@State private var isValid = true
18+
@State private var currentErrorMessage: String?
19+
@State private var isFocused = false
20+
21+
init(label: String?, text: String, errorMessage: String?) {
22+
self.label = label
23+
self.text = text
24+
self.errorMessage = errorMessage
25+
self._currentText = State(initialValue: text)
26+
self._currentErrorMessage = State(initialValue: errorMessage)
27+
}
28+
29+
var body: some View {
30+
PrimerInputFieldContainer(
31+
label: label,
32+
styling: nil,
33+
text: $currentText,
34+
isValid: $isValid,
35+
errorMessage: $currentErrorMessage,
36+
isFocused: $isFocused
37+
) {
38+
TextField("Placeholder", text: $currentText, onEditingChanged: { focused in
39+
isFocused = focused
40+
})
41+
.textFieldStyle(.plain)
42+
}
43+
}
44+
}
45+
46+
@available(iOS 15.0, *)
47+
struct PreviewContainerWithRightComponent: View {
48+
let label: String?
49+
let text: String
50+
51+
@State private var currentText: String
52+
@State private var isValid = true
53+
@State private var errorMessage: String?
54+
@State private var isFocused = false
55+
@Environment(\.designTokens) private var tokens
56+
57+
init(label: String?, text: String) {
58+
self.label = label
59+
self.text = text
60+
self._currentText = State(initialValue: text)
61+
}
62+
63+
var body: some View {
64+
PrimerInputFieldContainer(
65+
label: label,
66+
styling: nil,
67+
text: $currentText,
68+
isValid: $isValid,
69+
errorMessage: $errorMessage,
70+
isFocused: $isFocused,
71+
textFieldBuilder: {
72+
TextField("Placeholder", text: $currentText, onEditingChanged: { focused in
73+
isFocused = focused
74+
})
75+
.textFieldStyle(.plain)
76+
},
77+
rightComponent: {
78+
let iconSize = PrimerSize.medium(tokens: tokens)
79+
Image(systemName: "info.circle")
80+
.resizable()
81+
.aspectRatio(contentMode: .fit)
82+
.frame(width: iconSize, height: iconSize)
83+
.foregroundColor(PrimerCheckoutColors.textSecondary(tokens: tokens))
84+
}
85+
)
86+
}
87+
}
88+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// PrimerInputFieldContainer+Rendering.swift
3+
//
4+
// Copyright © 2025 Primer API Ltd. All rights reserved.
5+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
6+
7+
import SwiftUI
8+
9+
// MARK: - Label
10+
@available(iOS 15.0, *)
11+
extension PrimerInputFieldContainer {
12+
func makeLabel(_ label: String) -> some View {
13+
Text(label)
14+
.font(labelFont)
15+
.foregroundColor(labelForegroundColor)
16+
.frame(height: PrimerComponentHeight.label)
17+
}
18+
}
19+
20+
// MARK: - TextField Container
21+
@available(iOS 15.0, *)
22+
extension PrimerInputFieldContainer {
23+
func makeTextFieldContainer() -> some View {
24+
HStack(spacing: PrimerSpacing.small(tokens: tokens), content: makeTextFieldContainerContent)
25+
.padding(.leading, styling?.padding?.leading ?? PrimerSpacing.medium(tokens: tokens))
26+
.padding(.trailing, styling?.padding?.trailing ?? PrimerSpacing.medium(tokens: tokens))
27+
.frame(height: styling?.fieldHeight ?? PrimerSize.xxlarge(tokens: tokens))
28+
.background(makeTextFieldContainerBackground())
29+
}
30+
31+
func makeTextFieldContainerContent() -> some View {
32+
Group {
33+
textFieldBuilder()
34+
Spacer()
35+
rightComponent?()
36+
if hasError { makeTextFieldContainerWarning() }
37+
}
38+
}
39+
40+
func makeTextFieldContainerBackground() -> some View {
41+
RoundedRectangle(cornerRadius: PrimerRadius.small(tokens: tokens))
42+
.strokeBorder(borderColor, lineWidth: textFieldContainerBackgroundLineWidth)
43+
.background(makeTextFieldContainerBackgroundBackground())
44+
.animation(AnimationConstants.focusAnimation, value: isFocused)
45+
}
46+
47+
func makeTextFieldContainerBackgroundBackground() -> some View {
48+
RoundedRectangle(cornerRadius: PrimerRadius.small(tokens: tokens))
49+
.fill(styling?.backgroundColor ?? PrimerCheckoutColors.background(tokens: tokens))
50+
}
51+
52+
func makeTextFieldContainerWarning() -> some View {
53+
let iconSize = PrimerSize.medium(tokens: tokens)
54+
return Image(systemName: "exclamationmark.triangle.fill")
55+
.resizable()
56+
.aspectRatio(contentMode: .fit)
57+
.frame(width: iconSize, height: iconSize)
58+
.foregroundColor(PrimerCheckoutColors.iconNegative(tokens: tokens))
59+
.offset(x: hasError ? 0 : -10)
60+
.opacity(hasError ? 1.0 : 0.0)
61+
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: hasError)
62+
}
63+
}
64+
65+
// MARK: - Error
66+
@available(iOS 15.0, *)
67+
extension PrimerInputFieldContainer {
68+
func makeErrorMessage(_ errorMessage: String) -> some View {
69+
Text(errorMessage)
70+
.font(errorMessageFont)
71+
.foregroundColor(errorMessageForegroundColor)
72+
.frame(height: errorMessageHeight)
73+
.offset(y: hasError ? 0 : -10)
74+
.opacity(hasError ? 1.0 : 0.0)
75+
.padding(.top, errorMessageTopPadding)
76+
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: hasError)
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// PrimerInputFieldContainer+Styling.swift
3+
//
4+
// Copyright © 2025 Primer API Ltd. All rights reserved.
5+
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
6+
7+
import SwiftUI
8+
9+
// MARK: - Colors
10+
@available(iOS 15.0, *)
11+
extension PrimerInputFieldContainer {
12+
var borderColor: Color {
13+
if errorMessage?.isEmpty == false {
14+
errorBorderColor
15+
} else {
16+
isFocused ? focusedBorderColor : defaultBorderColor
17+
}
18+
}
19+
20+
var labelForegroundColor: Color {
21+
styling?.labelColor ?? PrimerCheckoutColors.textPrimary(tokens: tokens)
22+
}
23+
24+
var errorMessageForegroundColor: Color {
25+
PrimerCheckoutColors.textNegative(tokens: tokens)
26+
}
27+
28+
var errorBorderColor: Color {
29+
styling?.errorBorderColor ?? PrimerCheckoutColors.borderError(tokens: tokens)
30+
}
31+
32+
var focusedBorderColor: Color {
33+
styling?.focusedBorderColor ?? PrimerCheckoutColors.borderFocus(tokens: tokens)
34+
}
35+
36+
var defaultBorderColor: Color {
37+
styling?.borderColor ?? PrimerCheckoutColors.borderDefault(tokens: tokens)
38+
}
39+
}
40+
41+
// MARK: - Fonts
42+
@available(iOS 15.0, *)
43+
extension PrimerInputFieldContainer {
44+
var errorMessageFont: Font { PrimerFont.bodySmall(tokens: tokens) }
45+
var labelFont: Font { styling?.labelFont ?? PrimerFont.bodySmall(tokens: tokens) }
46+
}
47+
48+
// MARK: - Spacing & Frame
49+
@available(iOS 15.0, *)
50+
extension PrimerInputFieldContainer {
51+
var textFieldContainerBackgroundLineWidth: CGFloat { styling?.borderWidth ?? PrimerBorderWidth.standard }
52+
var errorMessageHeight: CGFloat { hasError ? PrimerComponentHeight.errorMessage : 0 }
53+
var errorMessageTopPadding: CGFloat { hasError ? PrimerSpacing.xsmall(tokens: tokens) : 0 }
54+
}

0 commit comments

Comments
 (0)