Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ struct InputFieldPreview: View {
Form {
AutocapitalizationPicker(selection: self.$model.autocapitalization)
Toggle("Autocorrection Enabled", isOn: self.$model.isAutocorrectionEnabled)
Toggle("Caption", isOn: .init(
get: {
return self.model.caption != nil
},
set: { newValue in
self.model.caption = newValue ? Self.caption : nil
}
))
CaptionFontPicker(title: "Caption Font", selection: self.$model.captionFont)
ComponentOptionalColorPicker(selection: self.$model.color)
ComponentRadiusPicker(selection: self.$model.cornerRadius) {
Text("Custom: 20px").tag(ComponentRadius.custom(20))
Expand All @@ -46,12 +55,17 @@ struct InputFieldPreview: View {
return self.model.placeholder != nil
},
set: { newValue in
self.model.placeholder = newValue ? "Placeholder" : nil
self.model.placeholder = newValue ? Self.placeholder : nil
}
))
Toggle("Required", isOn: self.$model.isRequired)
Toggle("Secure Input", isOn: self.$model.isSecureInput)
SizePicker(selection: self.$model.size)
Picker("Style", selection: self.$model.style) {
Text("Light").tag(InputFieldVM.Style.light)
Text("Bordered").tag(InputFieldVM.Style.bordered)
Text("Faded").tag(InputFieldVM.Style.faded)
}
SubmitTypePicker(selection: self.$model.submitType)
UniversalColorPicker(
title: "Tint Color",
Expand All @@ -62,9 +76,14 @@ struct InputFieldPreview: View {
return self.model.title != nil
},
set: { newValue in
self.model.title = newValue ? "Title" : nil
self.model.title = newValue ? Self.title : nil
}
))
BodyFontPicker(title: "Title Font", selection: self.$model.titleFont)
Picker("Title Position", selection: self.$model.titlePosition) {
Text("Inside").tag(InputFieldVM.TitlePosition.inside)
Text("Outside").tag(InputFieldVM.TitlePosition.outside)
}
}
}
.toolbar {
Expand All @@ -79,9 +98,14 @@ struct InputFieldPreview: View {
}
}

private static let title = "Email"
private static let placeholder = "Enter your email"
private static let caption = "Your email address will be used to send a verification code"
private static var initialModel: InputFieldVM {
return .init {
$0.title = "Title"
$0.title = Self.title
$0.placeholder = Self.placeholder
$0.caption = Self.caption
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

extension InputFieldVM {
/// The input fields appearance style.
public enum Style: Hashable {
/// An input field with a partially transparent background.
case light
/// An input field with a transparent background and a border.
case bordered
/// An input field with a partially transparent background and a border.
case faded
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

extension InputFieldVM {
/// Specifies the position of the title relative to the input field.
public enum TitlePosition {
/// The title is displayed inside the input field.
case inside
/// The title is displayed above the input field.
case outside
}
}
103 changes: 93 additions & 10 deletions Sources/ComponentsKit/Components/InputField/Models/InputFieldVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ public struct InputFieldVM: ComponentVM {
/// Defaults to `.sentences`, which capitalizes the first letter of each sentence.
public var autocapitalization: TextAutocapitalization = .sentences

/// The caption displayed below the input field.
public var caption: String?

/// The font used for the input field's caption.
///
/// If not provided, the font is automatically calculated based on the input field's size.
public var captionFont: UniversalFont?

/// The color of the input field.
public var color: ComponentColor?

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

/// The visual style of the input field.
///
/// Defaults to `.light`.
public var style: Style = .light

/// The type of the submit button on the keyboard.
///
/// Defaults to `.return`.
Expand All @@ -67,6 +80,16 @@ public struct InputFieldVM: ComponentVM {
/// The title displayed on the input field.
public var title: String?

/// The font used for the input field's title.
///
/// If not provided, the font is automatically calculated based on the input field's size.
public var titleFont: UniversalFont?

/// The position of the title relative to the input field.
///
/// Defaults to `.inside`.
public var titlePosition: TitlePosition = .inside

/// Initializes a new instance of `InputFieldVM` with default values.
public init() {}
}
Expand All @@ -88,6 +111,34 @@ extension InputFieldVM {
return .lgBody
}
}
var preferredTitleFont: UniversalFont {
if let titleFont {
return titleFont
}

switch self.size {
case .small:
return .smBody
case .medium:
return .mdBody
case .large:
return .lgBody
}
}
var preferredCaptionFont: UniversalFont {
if let captionFont {
return captionFont
}

switch self.size {
case .small:
return .smCaption
case .medium:
return .mdCaption
case .large:
return .lgCaption
}
}
var height: CGFloat {
return switch self.size {
case .small: 40
Expand All @@ -104,14 +155,23 @@ extension InputFieldVM {
}
}
var spacing: CGFloat {
return self.title.isNotNilAndEmpty ? 12 : 0
switch self.titlePosition {
case .inside:
return 12
case .outside:
return 8
}
}
var backgroundColor: UniversalColor {
return self.color?.background ?? .content1
switch self.style {
case .light, .faded:
return self.color?.background ?? .content1
case .bordered:
return .background
}
}
var foregroundColor: UniversalColor {
let color = self.color?.main ?? .foreground
return color.enabled(self.isEnabled)
return (self.color?.main ?? .foreground).enabled(self.isEnabled)
}
var placeholderColor: UniversalColor {
if let color {
Expand All @@ -120,6 +180,27 @@ extension InputFieldVM {
return .secondaryForeground.enabled(self.isEnabled)
}
}
var captionColor: UniversalColor {
return (self.color?.main ?? .secondaryForeground).enabled(self.isEnabled)
}
var borderWidth: CGFloat {
switch self.style {
case .light:
return 0
case .bordered, .faded:
switch self.size {
case .small:
return BorderWidth.small.value
case .medium:
return BorderWidth.medium.value
case .large:
return BorderWidth.large.value
}
}
}
var borderColor: UniversalColor {
return (self.color?.main ?? .content3).enabled(self.isEnabled)
}
}

// MARK: - UIKit Helpers
Expand All @@ -146,7 +227,7 @@ extension InputFieldVM {
attributedString.append(NSAttributedString(
string: title,
attributes: [
.font: self.preferredFont.uiFont,
.font: self.preferredTitleFont.uiFont,
.foregroundColor: self.foregroundColor.uiColor
]
))
Expand All @@ -160,21 +241,23 @@ extension InputFieldVM {
attributedString.append(NSAttributedString(
string: "*",
attributes: [
.font: self.preferredFont.uiFont,
.foregroundColor: UniversalColor.danger.uiColor
.font: self.preferredTitleFont.uiFont,
.foregroundColor: UniversalColor.danger.enabled(self.isEnabled).uiColor
]
))
}
return attributedString
}
func shouldUpdateTitlePosition(_ oldModel: Self) -> Bool {
return self.titlePosition != oldModel.titlePosition
}
func shouldUpdateLayout(_ oldModel: Self) -> Bool {
return self.size != oldModel.size
|| self.horizontalPadding != oldModel.horizontalPadding
|| self.spacing != oldModel.spacing
|| self.cornerRadius != oldModel.cornerRadius
}
func shouldUpdateCornerRadius(_ oldModel: Self) -> Bool {
return self.cornerRadius != oldModel.cornerRadius
|| self.titlePosition != oldModel.titlePosition
|| self.title.isNilOrEmpty != oldModel.title.isNilOrEmpty
}
}

Expand Down
92 changes: 57 additions & 35 deletions Sources/ComponentsKit/Components/InputField/SUInputField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,46 +49,68 @@ public struct SUInputField<FocusValue: Hashable>: View {
// MARK: Body

public var body: some View {
HStack(spacing: self.model.spacing) {
if let title = self.model.attributedTitle {
VStack(alignment: .leading, spacing: self.model.spacing) {
if let title = self.model.attributedTitle,
self.model.titlePosition == .outside {
Text(title)
.font(self.model.preferredFont.font)
}

Group {
if self.model.isSecureInput {
SecureField(text: self.$text, label: {
Text(self.model.placeholder ?? "")
.foregroundStyle(self.model.placeholderColor.color)
})
} else {
TextField(text: self.$text, label: {
Text(self.model.placeholder ?? "")
.foregroundStyle(self.model.placeholderColor.color)
})
HStack(spacing: self.model.spacing) {
if let title = self.model.attributedTitle,
self.model.titlePosition == .inside {
Text(title)
}

Group {
if self.model.isSecureInput {
SecureField(text: self.$text, label: {
Text(self.model.placeholder ?? "")
.foregroundStyle(self.model.placeholderColor.color)
})
} else {
TextField(text: self.$text, label: {
Text(self.model.placeholder ?? "")
.foregroundStyle(self.model.placeholderColor.color)
})
}
}
.tint(self.model.tintColor.color)
.font(self.model.preferredFont.font)
.foregroundStyle(self.model.foregroundColor.color)
.applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus)
.disabled(!self.model.isEnabled)
.keyboardType(self.model.keyboardType)
.submitLabel(self.model.submitType.submitLabel)
.autocorrectionDisabled(!self.model.isAutocorrectionEnabled)
.textInputAutocapitalization(self.model.autocapitalization.textInputAutocapitalization)
}
.tint(self.model.tintColor.color)
.font(self.model.preferredFont.font)
.foregroundStyle(self.model.foregroundColor.color)
.applyFocus(globalFocus: self.globalFocus, localFocus: self.localFocus)
.disabled(!self.model.isEnabled)
.keyboardType(self.model.keyboardType)
.submitLabel(self.model.submitType.submitLabel)
.autocorrectionDisabled(!self.model.isAutocorrectionEnabled)
.textInputAutocapitalization(self.model.autocapitalization.textInputAutocapitalization)
}
.padding(.horizontal, self.model.horizontalPadding)
.frame(height: self.model.height)
.background(self.model.backgroundColor.color)
.onTapGesture {
self.globalFocus?.wrappedValue = self.localFocus
}
.clipShape(
RoundedRectangle(
cornerRadius: self.model.cornerRadius.value()
.padding(.horizontal, self.model.horizontalPadding)
.frame(height: self.model.height)
.background(self.model.backgroundColor.color)
.onTapGesture {
self.globalFocus?.wrappedValue = self.localFocus
}
.clipShape(
RoundedRectangle(
cornerRadius: self.model.cornerRadius.value()
)
)
)
.overlay(
RoundedRectangle(
cornerRadius: self.model.cornerRadius.value()
)
.stroke(
self.model.borderColor.color,
lineWidth: self.model.borderWidth
)
)

if let caption = self.model.caption, caption.isNotEmpty {
Text(caption)
.font(self.model.preferredCaptionFont.font)
.foregroundStyle(self.model.captionColor.color)
}
}
}
}

Expand All @@ -98,7 +120,7 @@ extension View {
@ViewBuilder
fileprivate func applyFocus<FocusValue: Hashable>(
globalFocus: FocusState<FocusValue>.Binding?,
localFocus: FocusValue,
localFocus: FocusValue
) -> some View {
if let globalFocus {
self.focused(globalFocus, equals: localFocus)
Expand Down
Loading