Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GEN-1584: Add hFreeTextInputField component #1141

Merged
merged 7 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Projects/hCoreUI/Sources/hForm/hDatePickerField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ public struct hDatePickerField: View {
private let onShowDatePicker: (() -> Void)?

@State private var animate = false

@State private var date: Date = Date()
private var selectedDate: Date?

@Binding var error: String?
@State private var disposeBag = DisposeBag()
private var placeholderText: String?
Expand Down
21 changes: 20 additions & 1 deletion Projects/hCoreUI/Sources/hForm/hFloatingField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct hFloatingField: View {
private let onTap: () -> Void
@Environment(\.hFieldTrailingView) var fieldTrailingView
@Environment(\.isEnabled) var isEnabled
@Environment(\.hWithoutFixedHeight) var hWithoutFixedHeight

public var shouldMoveLabel: Binding<Bool> {
Binding(
Expand Down Expand Up @@ -44,7 +45,8 @@ public struct hFloatingField: View {
)
if !value.isEmpty {
HStack {
getTextLabel.frame(height: HFontTextStyle.title3.fontSize)
getTextLabel
.frame(height: hWithoutFixedHeight ?? false ? .infinity : HFontTextStyle.title3.fontSize)
Spacer()
fieldTrailingView
}
Expand Down Expand Up @@ -128,6 +130,23 @@ extension View {
}
}

private struct EnvironmentWithoutFixedHeight: EnvironmentKey {
static let defaultValue: Bool? = false
}

extension EnvironmentValues {
public var hWithoutFixedHeight: (Bool)? {
get { self[EnvironmentWithoutFixedHeight.self] }
set { self[EnvironmentWithoutFixedHeight.self] = newValue }
}
}

extension View {
public var hWithoutFixedHeight: some View {
self.environment(\.hWithoutFixedHeight, true)
}
}

private struct EnvironmentHFieldLockedState: EnvironmentKey {
static let defaultValue = false
}
Expand Down
163 changes: 163 additions & 0 deletions Projects/hCoreUI/Sources/hForm/hFreeTextInputField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import Flow
import Presentation
import SwiftUI
import hCore

public struct hFreeTextInputField: View {
private var placeholder: String
@State private var animate = false
private var selectedValue: String?
@Binding var error: String?
@State private var value: String
@State private var disposeBag = DisposeBag()
private let onContinue: (_ text: String) -> Void
private let infoCardText: String?

public var shouldMoveLabel: Binding<Bool> {
Binding(
get: { !(selectedValue?.isEmpty ?? true) },
set: { _ in }
)
}

public init(
selectedValue: String?,
placeholder: String? = nil,
error: Binding<String?>? = nil,
onContinue: @escaping (_ text: String) -> Void = { _ in },
infoCardText: String? = nil
) {
self.placeholder = placeholder ?? ""
self.selectedValue = selectedValue
self._error = error ?? Binding.constant(nil)
self.onContinue = onContinue
self.value = ""
self.infoCardText = infoCardText
}

public var body: some View {
VStack(alignment: .leading, spacing: 0) {
hFloatingField(value: selectedValue ?? "", placeholder: placeholder) {
showFreeTextField()
}
.hWithoutFixedHeight
}
}

private func showFreeTextField() {
let continueAction = ReferenceAction {}
let cancelAction = ReferenceAction {}

value = selectedValue ?? ""

let view = freeTextInputView(
continueAction: continueAction,
cancelAction: cancelAction,
value: $value,
infoCardText: infoCardText
)

let journey = HostingJourney(
rootView: view,
style: .detented(.scrollViewContentSize),
options: [.largeNavigationBar, .blurredBackground]
)
.configureTitle(placeholder)

let freeTextFieldJourney = journey.addConfiguration { presenter in
continueAction.execute = {
self.onContinue(value)
presenter.dismisser(JourneyError.cancelled)
}
cancelAction.execute = {
presenter.dismisser(JourneyError.cancelled)
}
}
let vc = UIApplication.shared.getTopViewController()
if let vc {
disposeBag += vc.present(freeTextFieldJourney)
}
}

private struct freeTextInputView: View {
fileprivate let continueAction: ReferenceAction
fileprivate let cancelAction: ReferenceAction
@Binding fileprivate var value: String
private let maxCharacters = 140
private let infoCardText: String?

public init(
continueAction: ReferenceAction,
cancelAction: ReferenceAction,
value: Binding<String>,
infoCardText: String? = nil
) {
self.continueAction = continueAction
self.cancelAction = cancelAction
self._value = value
self.infoCardText = infoCardText
}

public var body: some View {
hForm {
VStack(spacing: 16) {
VStack {
hTextField(
masking: Masking(type: .none),
value: $value,
placeholder: L10n.textInputFieldPlaceholder
)
.hTextFieldOptions([.useLineBreak, .minimumHeight(height: 96)])
Spacer()
hText("\(value.count)/\(maxCharacters)", style: .standardSmall)
.foregroundColor(getTextColor)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding([.horizontal, .top], 16)
.padding(.bottom, 12)
.background(
Squircle.default()
.fill(hFillColor.opaqueOne)
)
if let infoCardText {
InfoCard(text: infoCardText, type: .info)
}
}
.padding(.horizontal, 16)
}
.hFormAttachToBottom {
VStack(spacing: 8) {
hButton.LargeButton(type: .primary) {
continueAction.execute()
} content: {
hText(L10n.generalSaveButton)
}
.disabled(value.count > maxCharacters)
hButton.LargeButton(type: .ghost) {
cancelAction.execute()
} content: {
hText(L10n.generalCancelButton)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 16)
}
}

@hColorBuilder
var getTextColor: some hColor {
if value.count < maxCharacters {
hTextColor.tertiary
} else {
hSignalColor.redElement
}
}
}
}

#Preview{
VStack(spacing: 4) {
hFreeTextInputField(selectedValue: "", placeholder: "Type of damage")
hFreeTextInputField(selectedValue: "value", placeholder: "placeholder")
}
}
43 changes: 32 additions & 11 deletions Projects/hCoreUI/Sources/hForm/hTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import hCore
public enum hTextFieldOptions: Hashable {
case showDivider
case minimumHeight(height: CGFloat)
case useLineBreak
}

extension Set where Element == hTextFieldOptions {
Expand All @@ -24,6 +25,10 @@ extension Set where Element == hTextFieldOptions {
var showDivider: Bool {
self.contains(.showDivider)
}

var useLineBreak: Bool {
self.contains(.useLineBreak)
}
}

private struct EnvironmentHTextFieldOptions: EnvironmentKey {
Expand Down Expand Up @@ -85,18 +90,34 @@ public struct hTextField: View {
public var body: some View {
VStack(spacing: 0) {
HStack {
SwiftUI.TextField(placeholder ?? "", text: $innerValue)
.modifier(hFontModifier(style: .body))
.modifier(masking)
.tint(hTextColor.primary)
.onReceive(Just(innerValue != previousInnerValue)) { shouldUpdate in
if shouldUpdate {
value = masking.maskValue(text: innerValue, previousText: previousInnerValue)
innerValue = value
previousInnerValue = value
if options.useLineBreak, #available(iOS 16.0, *) {
SwiftUI.TextField(placeholder ?? "", text: $innerValue, axis: .vertical)
.lineLimit(5...10)
.modifier(hFontModifier(style: .body))
.modifier(masking)
.tint(hTextColor.primary)
.onReceive(Just(innerValue != previousInnerValue)) { shouldUpdate in
if shouldUpdate {
value = masking.maskValue(text: innerValue, previousText: previousInnerValue)
innerValue = value
previousInnerValue = value
}
}
}
.frame(minHeight: options.minimumHeight)
.frame(minHeight: options.minimumHeight)
} else {
SwiftUI.TextField(placeholder ?? "", text: $innerValue)
.modifier(hFontModifier(style: .body))
.modifier(masking)
.tint(hTextColor.primary)
.onReceive(Just(innerValue != previousInnerValue)) { shouldUpdate in
if shouldUpdate {
value = masking.maskValue(text: innerValue, previousText: previousInnerValue)
innerValue = value
previousInnerValue = value
}
}
.frame(minHeight: options.minimumHeight)
}
}
if options.showDivider {
SwiftUI.Divider()
Expand Down
Loading