Skip to content

Commit e84ffd6

Browse files
Merge pull request #89 from componentskit/improve-input-field
New params in the input field
2 parents 9f07a7b + 5cd211e commit e84ffd6

File tree

7 files changed

+303
-83
lines changed

7 files changed

+303
-83
lines changed

Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/InputFieldPreview.swift

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ struct InputFieldPreview: View {
3434
Form {
3535
AutocapitalizationPicker(selection: self.$model.autocapitalization)
3636
Toggle("Autocorrection Enabled", isOn: self.$model.isAutocorrectionEnabled)
37+
Toggle("Caption", isOn: .init(
38+
get: {
39+
return self.model.caption != nil
40+
},
41+
set: { newValue in
42+
self.model.caption = newValue ? Self.caption : nil
43+
}
44+
))
45+
CaptionFontPicker(title: "Caption Font", selection: self.$model.captionFont)
3746
ComponentOptionalColorPicker(selection: self.$model.color)
3847
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
3948
Text("Custom: 20px").tag(ComponentRadius.custom(20))
@@ -46,12 +55,17 @@ struct InputFieldPreview: View {
4655
return self.model.placeholder != nil
4756
},
4857
set: { newValue in
49-
self.model.placeholder = newValue ? "Placeholder" : nil
58+
self.model.placeholder = newValue ? Self.placeholder : nil
5059
}
5160
))
5261
Toggle("Required", isOn: self.$model.isRequired)
5362
Toggle("Secure Input", isOn: self.$model.isSecureInput)
5463
SizePicker(selection: self.$model.size)
64+
Picker("Style", selection: self.$model.style) {
65+
Text("Light").tag(InputFieldVM.Style.light)
66+
Text("Bordered").tag(InputFieldVM.Style.bordered)
67+
Text("Faded").tag(InputFieldVM.Style.faded)
68+
}
5569
SubmitTypePicker(selection: self.$model.submitType)
5670
UniversalColorPicker(
5771
title: "Tint Color",
@@ -62,9 +76,14 @@ struct InputFieldPreview: View {
6276
return self.model.title != nil
6377
},
6478
set: { newValue in
65-
self.model.title = newValue ? "Title" : nil
79+
self.model.title = newValue ? Self.title : nil
6680
}
6781
))
82+
BodyFontPicker(title: "Title Font", selection: self.$model.titleFont)
83+
Picker("Title Position", selection: self.$model.titlePosition) {
84+
Text("Inside").tag(InputFieldVM.TitlePosition.inside)
85+
Text("Outside").tag(InputFieldVM.TitlePosition.outside)
86+
}
6887
}
6988
}
7089
.toolbar {
@@ -79,9 +98,14 @@ struct InputFieldPreview: View {
7998
}
8099
}
81100

101+
private static let title = "Email"
102+
private static let placeholder = "Enter your email"
103+
private static let caption = "Your email address will be used to send a verification code"
82104
private static var initialModel: InputFieldVM {
83105
return .init {
84-
$0.title = "Title"
106+
$0.title = Self.title
107+
$0.placeholder = Self.placeholder
108+
$0.caption = Self.caption
85109
}
86110
}
87111
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
extension InputFieldVM {
4+
/// The input fields appearance style.
5+
public enum Style: Hashable {
6+
/// An input field with a partially transparent background.
7+
case light
8+
/// An input field with a transparent background and a border.
9+
case bordered
10+
/// An input field with a partially transparent background and a border.
11+
case faded
12+
}
13+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
extension InputFieldVM {
4+
/// Specifies the position of the title relative to the input field.
5+
public enum TitlePosition {
6+
/// The title is displayed inside the input field.
7+
case inside
8+
/// The title is displayed above the input field.
9+
case outside
10+
}
11+
}

Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ public struct InputFieldVM: ComponentVM {
88
/// Defaults to `.sentences`, which capitalizes the first letter of each sentence.
99
public var autocapitalization: TextAutocapitalization = .sentences
1010

11+
/// The caption displayed below the input field.
12+
public var caption: String?
13+
14+
/// The font used for the input field's caption.
15+
///
16+
/// If not provided, the font is automatically calculated based on the input field's size.
17+
public var captionFont: UniversalFont?
18+
1119
/// The color of the input field.
1220
public var color: ComponentColor?
1321

@@ -54,6 +62,11 @@ public struct InputFieldVM: ComponentVM {
5462
/// Defaults to `.medium`.
5563
public var size: ComponentSize = .medium
5664

65+
/// The visual style of the input field.
66+
///
67+
/// Defaults to `.light`.
68+
public var style: Style = .light
69+
5770
/// The type of the submit button on the keyboard.
5871
///
5972
/// Defaults to `.return`.
@@ -67,6 +80,16 @@ public struct InputFieldVM: ComponentVM {
6780
/// The title displayed on the input field.
6881
public var title: String?
6982

83+
/// The font used for the input field's title.
84+
///
85+
/// If not provided, the font is automatically calculated based on the input field's size.
86+
public var titleFont: UniversalFont?
87+
88+
/// The position of the title relative to the input field.
89+
///
90+
/// Defaults to `.inside`.
91+
public var titlePosition: TitlePosition = .inside
92+
7093
/// Initializes a new instance of `InputFieldVM` with default values.
7194
public init() {}
7295
}
@@ -88,6 +111,34 @@ extension InputFieldVM {
88111
return .lgBody
89112
}
90113
}
114+
var preferredTitleFont: UniversalFont {
115+
if let titleFont {
116+
return titleFont
117+
}
118+
119+
switch self.size {
120+
case .small:
121+
return .smBody
122+
case .medium:
123+
return .mdBody
124+
case .large:
125+
return .lgBody
126+
}
127+
}
128+
var preferredCaptionFont: UniversalFont {
129+
if let captionFont {
130+
return captionFont
131+
}
132+
133+
switch self.size {
134+
case .small:
135+
return .smCaption
136+
case .medium:
137+
return .mdCaption
138+
case .large:
139+
return .lgCaption
140+
}
141+
}
91142
var height: CGFloat {
92143
return switch self.size {
93144
case .small: 40
@@ -104,14 +155,23 @@ extension InputFieldVM {
104155
}
105156
}
106157
var spacing: CGFloat {
107-
return self.title.isNotNilAndEmpty ? 12 : 0
158+
switch self.titlePosition {
159+
case .inside:
160+
return 12
161+
case .outside:
162+
return 8
163+
}
108164
}
109165
var backgroundColor: UniversalColor {
110-
return self.color?.background ?? .content1
166+
switch self.style {
167+
case .light, .faded:
168+
return self.color?.background ?? .content1
169+
case .bordered:
170+
return .background
171+
}
111172
}
112173
var foregroundColor: UniversalColor {
113-
let color = self.color?.main ?? .foreground
114-
return color.enabled(self.isEnabled)
174+
return (self.color?.main ?? .foreground).enabled(self.isEnabled)
115175
}
116176
var placeholderColor: UniversalColor {
117177
if let color {
@@ -120,6 +180,27 @@ extension InputFieldVM {
120180
return .secondaryForeground.enabled(self.isEnabled)
121181
}
122182
}
183+
var captionColor: UniversalColor {
184+
return (self.color?.main ?? .secondaryForeground).enabled(self.isEnabled)
185+
}
186+
var borderWidth: CGFloat {
187+
switch self.style {
188+
case .light:
189+
return 0
190+
case .bordered, .faded:
191+
switch self.size {
192+
case .small:
193+
return BorderWidth.small.value
194+
case .medium:
195+
return BorderWidth.medium.value
196+
case .large:
197+
return BorderWidth.large.value
198+
}
199+
}
200+
}
201+
var borderColor: UniversalColor {
202+
return (self.color?.main ?? .content3).enabled(self.isEnabled)
203+
}
123204
}
124205

125206
// MARK: - UIKit Helpers
@@ -146,7 +227,7 @@ extension InputFieldVM {
146227
attributedString.append(NSAttributedString(
147228
string: title,
148229
attributes: [
149-
.font: self.preferredFont.uiFont,
230+
.font: self.preferredTitleFont.uiFont,
150231
.foregroundColor: self.foregroundColor.uiColor
151232
]
152233
))
@@ -160,21 +241,23 @@ extension InputFieldVM {
160241
attributedString.append(NSAttributedString(
161242
string: "*",
162243
attributes: [
163-
.font: self.preferredFont.uiFont,
164-
.foregroundColor: UniversalColor.danger.uiColor
244+
.font: self.preferredTitleFont.uiFont,
245+
.foregroundColor: UniversalColor.danger.enabled(self.isEnabled).uiColor
165246
]
166247
))
167248
}
168249
return attributedString
169250
}
251+
func shouldUpdateTitlePosition(_ oldModel: Self) -> Bool {
252+
return self.titlePosition != oldModel.titlePosition
253+
}
170254
func shouldUpdateLayout(_ oldModel: Self) -> Bool {
171255
return self.size != oldModel.size
172256
|| self.horizontalPadding != oldModel.horizontalPadding
173257
|| self.spacing != oldModel.spacing
174258
|| self.cornerRadius != oldModel.cornerRadius
175-
}
176-
func shouldUpdateCornerRadius(_ oldModel: Self) -> Bool {
177-
return self.cornerRadius != oldModel.cornerRadius
259+
|| self.titlePosition != oldModel.titlePosition
260+
|| self.title.isNilOrEmpty != oldModel.title.isNilOrEmpty
178261
}
179262
}
180263

Sources/ComponentsKit/Components/InputField/SUInputField.swift

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -49,46 +49,68 @@ public struct SUInputField<FocusValue: Hashable>: View {
4949
// MARK: Body
5050

5151
public var body: some View {
52-
HStack(spacing: self.model.spacing) {
53-
if let title = self.model.attributedTitle {
52+
VStack(alignment: .leading, spacing: self.model.spacing) {
53+
if let title = self.model.attributedTitle,
54+
self.model.titlePosition == .outside {
5455
Text(title)
55-
.font(self.model.preferredFont.font)
5656
}
5757

58-
Group {
59-
if self.model.isSecureInput {
60-
SecureField(text: self.$text, label: {
61-
Text(self.model.placeholder ?? "")
62-
.foregroundStyle(self.model.placeholderColor.color)
63-
})
64-
} else {
65-
TextField(text: self.$text, label: {
66-
Text(self.model.placeholder ?? "")
67-
.foregroundStyle(self.model.placeholderColor.color)
68-
})
58+
HStack(spacing: self.model.spacing) {
59+
if let title = self.model.attributedTitle,
60+
self.model.titlePosition == .inside {
61+
Text(title)
6962
}
63+
64+
Group {
65+
if self.model.isSecureInput {
66+
SecureField(text: self.$text, label: {
67+
Text(self.model.placeholder ?? "")
68+
.foregroundStyle(self.model.placeholderColor.color)
69+
})
70+
} else {
71+
TextField(text: self.$text, label: {
72+
Text(self.model.placeholder ?? "")
73+
.foregroundStyle(self.model.placeholderColor.color)
74+
})
75+
}
76+
}
77+
.tint(self.model.tintColor.color)
78+
.font(self.model.preferredFont.font)
79+
.foregroundStyle(self.model.foregroundColor.color)
80+
.applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus)
81+
.disabled(!self.model.isEnabled)
82+
.keyboardType(self.model.keyboardType)
83+
.submitLabel(self.model.submitType.submitLabel)
84+
.autocorrectionDisabled(!self.model.isAutocorrectionEnabled)
85+
.textInputAutocapitalization(self.model.autocapitalization.textInputAutocapitalization)
7086
}
71-
.tint(self.model.tintColor.color)
72-
.font(self.model.preferredFont.font)
73-
.foregroundStyle(self.model.foregroundColor.color)
74-
.applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus)
75-
.disabled(!self.model.isEnabled)
76-
.keyboardType(self.model.keyboardType)
77-
.submitLabel(self.model.submitType.submitLabel)
78-
.autocorrectionDisabled(!self.model.isAutocorrectionEnabled)
79-
.textInputAutocapitalization(self.model.autocapitalization.textInputAutocapitalization)
80-
}
81-
.padding(.horizontal, self.model.horizontalPadding)
82-
.frame(height: self.model.height)
83-
.background(self.model.backgroundColor.color)
84-
.onTapGesture {
85-
self.globalFocus?.wrappedValue = self.localFocus
86-
}
87-
.clipShape(
88-
RoundedRectangle(
89-
cornerRadius: self.model.cornerRadius.value()
87+
.padding(.horizontal, self.model.horizontalPadding)
88+
.frame(height: self.model.height)
89+
.background(self.model.backgroundColor.color)
90+
.onTapGesture {
91+
self.globalFocus?.wrappedValue = self.localFocus
92+
}
93+
.clipShape(
94+
RoundedRectangle(
95+
cornerRadius: self.model.cornerRadius.value()
96+
)
9097
)
91-
)
98+
.overlay(
99+
RoundedRectangle(
100+
cornerRadius: self.model.cornerRadius.value()
101+
)
102+
.stroke(
103+
self.model.borderColor.color,
104+
lineWidth: self.model.borderWidth
105+
)
106+
)
107+
108+
if let caption = self.model.caption, caption.isNotEmpty {
109+
Text(caption)
110+
.font(self.model.preferredCaptionFont.font)
111+
.foregroundStyle(self.model.captionColor.color)
112+
}
113+
}
92114
}
93115
}
94116

@@ -98,7 +120,7 @@ extension View {
98120
@ViewBuilder
99121
fileprivate func applyFocus<FocusValue: Hashable>(
100122
globalFocus: FocusState<FocusValue>.Binding?,
101-
localFocus: FocusValue,
123+
localFocus: FocusValue
102124
) -> some View {
103125
if let globalFocus {
104126
self.focused(globalFocus, equals: localFocus)

0 commit comments

Comments
 (0)