Skip to content

Commit 97f6596

Browse files
committed
feat: Custom left/right-views on InputField
1 parent 09c56bd commit 97f6596

File tree

4 files changed

+143
-41
lines changed

4 files changed

+143
-41
lines changed

GoodSwiftUI-Sample/GoodSwiftUI-Sample/Screens/InputFieldSampleView.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ struct InputFieldSampleView: View {
4747

4848
@State private var nameEnabled: Bool = true
4949
@State private var passwordEnabled: Bool = true
50+
@State private var showsRightAlert: Bool = false
5051

5152
// MARK: - Properties
5253

@@ -66,6 +67,7 @@ struct InputFieldSampleView: View {
6667
VStack {
6768
nameInputField
6869
pinCodeInputField
70+
customViewsInputField
6971
formattedInputField
7072

7173
// Input field controls
@@ -153,6 +155,29 @@ extension InputFieldSampleView {
153155
.inputFieldTraits(keyboardType: .numbersAndPunctuation)
154156
}
155157

158+
private var customViewsInputField: some View {
159+
InputField(
160+
text: .constant("Custom views"),
161+
title: "Left and right input views",
162+
placeholder: nil,
163+
hint: nil,
164+
leftView: {
165+
Text("+421 \(password)")
166+
},
167+
rightView: {
168+
Button {
169+
showsRightAlert.toggle()
170+
} label: {
171+
VStack {
172+
Text("Hello")
173+
Text("world")
174+
}
175+
}
176+
}
177+
)
178+
.alert("Alert", isPresented: $showsRightAlert, actions: {})
179+
}
180+
156181
private var validityGroups: some View {
157182
Group {
158183
// Checking validity state on fields
@@ -275,7 +300,7 @@ private final class InputFieldSampleViewController: UIViewController {
275300
title: "Title",
276301
text: .placeholder(length: 20),
277302
leftImage: nil,
278-
rightButton: nil,
303+
rightView: nil,
279304
placeholder: "Placeholder",
280305
hint: " ",
281306
traits: .default
@@ -286,7 +311,7 @@ private final class InputFieldSampleViewController: UIViewController {
286311
title: "Title",
287312
text: .placeholder(length: 8),
288313
leftImage: nil,
289-
rightButton: nil,
314+
rightView: nil,
290315
placeholder: "Placeholder",
291316
hint: " ",
292317
traits: .init(isSecureTextEntry: true)
@@ -300,7 +325,7 @@ private final class InputFieldSampleViewController: UIViewController {
300325
title: "Title",
301326
text: "0 %",
302327
leftImage: nil,
303-
rightButton: nil,
328+
rightView: nil,
304329
placeholder: "Placeholder",
305330
hint: " ",
306331
traits: .init(keyboardType: .numberPad)

Package.resolved

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/GRInputField/SwiftUI/InputField.swift

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SwiftUI
1111

1212
// MARK: - View
1313

14-
public struct InputField: UIViewRepresentable {
14+
public struct InputField<LeftView: View, RightView: View>: UIViewRepresentable {
1515

1616
// MARK: - Wrappers
1717

@@ -23,7 +23,9 @@ public struct InputField: UIViewRepresentable {
2323
public let title: String?
2424
public let placeholder: String?
2525
public let hint: String?
26-
public let rightButton: Supplier<UIButton>?
26+
27+
public let leftView: Supplier<LeftView>
28+
public let rightView: Supplier<RightView>
2729

2830
private var hasFormatting: Bool = false
2931

@@ -41,14 +43,14 @@ public struct InputField: UIViewRepresentable {
4143
title: String? = nil,
4244
placeholder: String? = nil,
4345
hint: String? = " ",
44-
rightButton: Supplier<UIButton>? = nil
46+
leftView: @escaping Supplier<LeftView> = { EmptyView() },
47+
rightView: @escaping Supplier<RightView> = { EmptyView() }
4548
) {
4649
self._text = text
4750
self._validityGroup = Binding.constant([:])
4851
self.title = title
4952
self.placeholder = placeholder
5053
self.hint = hint
51-
self.rightButton = rightButton
5254
self.traits = InputFieldTraits()
5355

5456
@ValidatorBuilder @Sendable func alwaysValidCriteria() -> Validator {
@@ -57,6 +59,9 @@ public struct InputField: UIViewRepresentable {
5759

5860
// closures cannot take @ValidationBuilder attribute, must be a function reference
5961
self.criteria = alwaysValidCriteria
62+
63+
self.leftView = leftView
64+
self.rightView = rightView
6065
}
6166

6267
@available(iOS 15.0, *)
@@ -66,7 +71,8 @@ public struct InputField: UIViewRepresentable {
6671
title: String? = nil,
6772
placeholder: String? = nil,
6873
hint: String? = " ",
69-
rightButton: Supplier<UIButton>? = nil
74+
leftView: @escaping Supplier<LeftView> = { EmptyView() },
75+
rightView: @escaping Supplier<RightView> = { EmptyView() }
7076
) where FormatterType.FormatInput == FormattedType, FormatterType.FormatOutput == String {
7177
let formattedBinding = Binding(get: {
7278
let formattedString = format.format(value.wrappedValue)
@@ -80,7 +86,14 @@ public struct InputField: UIViewRepresentable {
8086
}
8187
})
8288

83-
self.init(text: formattedBinding, title: title, placeholder: placeholder, hint: hint, rightButton: rightButton)
89+
self.init(
90+
text: formattedBinding,
91+
title: title,
92+
placeholder: placeholder,
93+
hint: hint,
94+
leftView: leftView,
95+
rightView: rightView
96+
)
8497
self.hasFormatting = true
8598
}
8699

@@ -122,7 +135,8 @@ public struct InputField: UIViewRepresentable {
122135
let view = ValidableInputFieldView()
123136
let model = InputFieldView.Model(
124137
title: title,
125-
rightButton: rightButton?(),
138+
leftView: _UIHostingView(rootView: leftView()),
139+
rightView: _UIHostingView(rootView: rightView()),
126140
placeholder: placeholder,
127141
hint: hint,
128142
traits: traits
@@ -183,6 +197,14 @@ public struct InputField: UIViewRepresentable {
183197
public func updateUIView(_ uiView: ValidableInputFieldView, context: Context) {
184198
uiView.updateText(self.text, force: true)
185199

200+
uiView.setupCustomLeftView(
201+
leftImage: nil,
202+
leftView: _UIHostingView(rootView: leftView())
203+
)
204+
uiView.setupCustomRightView(
205+
rightView: _UIHostingView(rootView: rightView())
206+
)
207+
186208
// Equality check to prevent unintended side effects
187209
if uiView.isEnabled != context.environment.isEnabled {
188210
uiView.isEnabled = context.environment.isEnabled

Sources/GRInputField/UIKit/InputFieldView.swift

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ public class InputFieldView: UIView {
2828

2929
public var title: String?
3030
public var text: String?
31+
32+
/// Custom left image.
3133
public var leftImage: UIImage?
3234

33-
/// Custom right button. This will be ignored when input field is secure.
34-
public var rightButton: UIButton?
35+
/// Custom left view. This will be ignored when `leftImage` property is set.
36+
public var leftView: UIView?
37+
38+
/// Custom right view. This will be ignored when input field is secure.
39+
public var rightView: UIView?
3540
public var placeholder: String?
3641
public var hint: String?
3742
public var traits: InputFieldTraits?
@@ -40,15 +45,17 @@ public class InputFieldView: UIView {
4045
title: String? = nil,
4146
text: String? = nil,
4247
leftImage: UIImage? = nil,
43-
rightButton: UIButton? = nil,
48+
leftView: UIView? = nil,
49+
rightView: UIView? = nil,
4450
placeholder: String? = nil,
4551
hint: String? = nil,
4652
traits: InputFieldTraits? = nil
4753
) {
4854
self.title = title
4955
self.text = text
5056
self.leftImage = leftImage
51-
self.rightButton = rightButton
57+
self.leftView = leftView
58+
self.rightView = rightView
5259
self.placeholder = placeholder
5360
self.hint = hint
5461
self.traits = traits
@@ -82,7 +89,7 @@ public class InputFieldView: UIView {
8289

8390
private let contentView = UIView()
8491

85-
private let leftImageView = UIImageView().then {
92+
private let leftContainerImageView = UIImageView().then {
8693
$0.contentMode = .scaleAspectFit
8794
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
8895
}
@@ -92,7 +99,12 @@ public class InputFieldView: UIView {
9299
$0.setContentHuggingPriority(.defaultLow, for: .horizontal)
93100
}
94101

102+
private let rightView = UIView().then {
103+
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
104+
}
105+
95106
private lazy var eyeButton = UIButton().then {
107+
$0.translatesAutoresizingMaskIntoConstraints = false
96108
$0.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
97109
$0.setContentHuggingPriority(.defaultHigh, for: .horizontal)
98110
}
@@ -332,7 +344,7 @@ private extension InputFieldView {
332344
verticalStackView.addArrangedSubview($0)
333345
}
334346

335-
[leftImageView, textField].forEach {
347+
[leftContainerImageView, textField, rightView].forEach {
336348
horizontalStackView.addArrangedSubview($0)
337349
}
338350

@@ -457,6 +469,66 @@ private extension InputFieldView {
457469

458470
}
459471

472+
// MARK: - Internal
473+
474+
internal extension InputFieldView {
475+
476+
func setupCustomLeftView(leftImage: UIImage?, leftView: UIView?) {
477+
/// Left view/image
478+
leftContainerImageView.subviews.forEach { $0.removeFromSuperview() }
479+
leftContainerImageView.image = nil
480+
481+
if let leftImage {
482+
leftContainerImageView.image = leftImage
483+
leftContainerImageView.isHidden = false
484+
} else if let leftView {
485+
leftContainerImageView.isHidden = false
486+
leftContainerImageView.addSubview(leftView)
487+
488+
leftView.translatesAutoresizingMaskIntoConstraints = false
489+
leftView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
490+
NSLayoutConstraint.activate([
491+
leftView.topAnchor.constraint(equalTo: leftContainerImageView.topAnchor),
492+
leftView.leadingAnchor.constraint(equalTo: leftContainerImageView.leadingAnchor),
493+
leftView.trailingAnchor.constraint(equalTo: leftContainerImageView.trailingAnchor),
494+
leftView.bottomAnchor.constraint(equalTo: leftContainerImageView.bottomAnchor)
495+
])
496+
} else {
497+
leftContainerImageView.isHidden = true
498+
}
499+
}
500+
501+
func setupCustomRightView(rightView rightSubview: UIView?) {
502+
/// Right view
503+
if let rightSubview, !isSecureTextEntry {
504+
rightView.subviews.forEach { $0.removeFromSuperview() }
505+
rightView.addSubview(rightSubview)
506+
507+
rightSubview.translatesAutoresizingMaskIntoConstraints = false
508+
rightSubview.setContentHuggingPriority(.defaultHigh, for: .horizontal)
509+
NSLayoutConstraint.activate([
510+
rightSubview.topAnchor.constraint(equalTo: rightView.topAnchor),
511+
rightSubview.leadingAnchor.constraint(equalTo: rightView.leadingAnchor),
512+
rightSubview.trailingAnchor.constraint(equalTo: rightView.trailingAnchor),
513+
rightSubview.bottomAnchor.constraint(equalTo: rightView.bottomAnchor)
514+
])
515+
} else if isSecureTextEntry {
516+
rightView.subviews.forEach { $0.removeFromSuperview() }
517+
rightView.addSubview(eyeButton)
518+
519+
NSLayoutConstraint.activate([
520+
eyeButton.topAnchor.constraint(equalTo: rightView.topAnchor),
521+
eyeButton.leadingAnchor.constraint(equalTo: rightView.leadingAnchor),
522+
eyeButton.trailingAnchor.constraint(equalTo: rightView.trailingAnchor),
523+
eyeButton.bottomAnchor.constraint(equalTo: rightView.bottomAnchor)
524+
])
525+
526+
setupEyeButtonHandler()
527+
}
528+
}
529+
530+
}
531+
460532
// MARK: - Public
461533

462534
public extension InputFieldView {
@@ -492,26 +564,8 @@ public extension InputFieldView {
492564
setupTraits(traits: model.traits ?? .default)
493565
setupToolbarIfNeeded(traits: model.traits ?? .default)
494566

495-
/// Left image
496-
if let leftImage = model.leftImage {
497-
leftImageView.image = leftImage
498-
leftImageView.isHidden = false
499-
} else {
500-
leftImageView.isHidden = true
501-
}
502-
503-
/// Secure entry
504-
if isSecureTextEntry {
505-
setupEyeButtonHandler()
506-
horizontalStackView.addArrangedSubview(eyeButton)
507-
}
508-
509-
/// Right button
510-
if let rightButton = model.rightButton, !isSecureTextEntry {
511-
rightButton.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
512-
rightButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
513-
horizontalStackView.addArrangedSubview(rightButton)
514-
}
567+
setupCustomLeftView(leftImage: model.leftImage, leftView: model.leftView)
568+
setupCustomRightView(rightView: model.rightView)
515569

516570
/// Input field title
517571
if let title = model.title {

0 commit comments

Comments
 (0)