Skip to content

Commit

Permalink
Merge pull request #1106 from adevinta/component/texteditor-sui
Browse files Browse the repository at this point in the history
[TextEditor] Add the TextEditor in SwiftUI
  • Loading branch information
robergro authored Jan 14, 2025
2 parents 1ee09c9 + e212cbe commit e389021
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 20 deletions.
6 changes: 6 additions & 0 deletions .Demo/Classes/View/Components/ComponentsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ struct ComponentsView: View {
self.navigateToView(TagComponentView())
}

if #available(iOS 16.0, *) {
Button("TextEditor") {
self.navigateToView(TextEditorComponentView())
}
}

Button("TextField") {
self.navigateToView(TextFieldComponentView())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct FormFieldComponentView: View {
@State private var isEnabled = CheckboxSelectionState.selected
@State private var isTitleRequired = CheckboxSelectionState.unselected
@State private var isTrailingAlignment = CheckboxSelectionState.unselected
@State private var isSecondaryHelper = CheckboxSelectionState.unselected

@State private var checkboxGroupItems: [any CheckboxGroupItemProtocol] = [
CheckboxGroupItemDefault(title: "Checkbox 1", id: "1", selectionState: .unselected, isEnabled: true),
Expand Down Expand Up @@ -92,6 +93,16 @@ struct FormFieldComponentView: View {
isEnabled: true,
selectionState: self.$isTrailingAlignment
)

if self.componentStyle.isTextInput {
CheckboxView(
text: "Is Secondary Helper",
checkedImage: DemoIconography.shared.checkmark.image,
theme: theme,
isEnabled: true,
selectionState: self.$isSecondaryHelper
)
}
},
integration: {
FormFieldView(
Expand All @@ -104,6 +115,11 @@ struct FormFieldComponentView: View {
attributedHelper: self.setText(isTitle: false, textStyle: self.helperStyle),
isTitleRequired: self.isTitleRequired == .selected ? true : false
)
.counterIfPossible(on: self.texfieldText, limit: 100, show: self.isSecondaryHelper == .selected)
.titleAccessibilityLabel("The label of the title")
.helperAccessibilityLabel("The label of the helper")
.secondaryHelperAccessibilityLabel("The label of the secondary helper")
.secondaryHelperAccessibilityValue("There are \(self.texfieldText.count) characters out of 100.")
.disabled(self.isEnabled == .selected ? false : true)
.layoutPriority(1)
}
Expand Down Expand Up @@ -224,11 +240,21 @@ struct FormFieldComponentView: View {
groupLayout: .horizontal
)
case .textField:
TextField("Component is not ready yet", text: self.$texfieldText)
.textFieldStyle(.roundedBorder)
TextFieldView(
"Your username",
text: self.$texfieldText,
theme: self.theme,
intent: .neutral,
isReadOnly: false
)
case .addOnTextField:
TextField("Component is not ready yet", text: self.$texfieldText)
.textFieldStyle(.roundedBorder)
TextFieldAddons(
"Your username",
text: self.$texfieldText,
theme: self.theme,
intent: .neutral,
isReadOnly: false
)
case .ratingInput:
RatingInputView(
theme: self.theme,
Expand All @@ -238,3 +264,14 @@ struct FormFieldComponentView: View {
}
}
}

private extension FormFieldView {

func counterIfPossible(on text: String, limit: Int?, show: Bool) -> Self {
if show {
self.counter(on: text, limit: limit)
} else {
self
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ enum FormFieldComponentStyle: String, CaseIterable {
case textField
case addOnTextField
case ratingInput

// MARK: - Properties

var isTextInput: Bool {
switch self {
case .textField, .addOnTextField: true
default: false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,24 @@ final class FormFieldComponentUIView<S: UIView>: ComponentUIView {
}

private func setupTextFieldActions() {
if let textField = self.componentView.component as? TextFieldUIView {
textField.addAction(.init(handler: { [weak self] _ in
guard let self else { return }
self.textCounter = textField.text
self.componentView.setCounterIfPossible(
on: self.textCounter,
limit: self.viewModel.isSecondaryHelper ? 100 : nil
)
func action(inputText: String?) {
self.textCounter = inputText
self.componentView.setCounterIfPossible(
on: inputText,
limit: self.viewModel.isSecondaryHelper ? 100 : nil
)
}

switch self.componentView.component {
case let textInput as TextFieldUIView:
textInput.addAction(.init(handler: { _ in
action(inputText: textInput.text)
}), for: .editingChanged)
case let view as TextFieldAddonsUIView:
view.textField.addAction(.init(handler: { _ in
action(inputText: view.textField.text)
}), for: .editingChanged)
default: break
}
}
}
Expand All @@ -130,11 +139,21 @@ final class FormFieldComponentUIView<S: UIView>: ComponentUIView {
extension FormFieldUIView {

func setCounterIfPossible(on text: String?, limit: Int?) {
if let view = self as? FormFieldUIView<TextFieldUIView> {
let counterChanged: Bool

switch self {
case let view as FormFieldUIView<TextFieldUIView>:
view.setCounter(on: text, limit: limit)
guard let limit else { return }
counterChanged = true
case let view as FormFieldUIView<TextFieldAddonsUIView>:
view.setCounter(on: text, limit: limit)
counterChanged = true
default:
counterChanged = false
}

view.secondaryHelperLabel.accessibilityLabel = "\(text?.count ?? 0) caractères sur \(limit)"
if counterChanged, let limit {
self.secondaryHelperLabel.accessibilityLabel = "\(text?.count ?? 0) caractères sur \(limit)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,25 @@ final class FormFieldsViewController: UICollectionViewController {
)
}

static func makeTextEditor() -> TextEditorUIView {
return TextEditorUIView(
theme: SparkTheme.shared,
intent: .alert
)
}

static func makeTextField() -> TextFieldUIView {
return TextFieldUIView(
theme: SparkTheme.shared,
intent: .alert
)
}

static func makeAddOnTextField() -> TextFieldUIView {
let view = TextFieldUIView(
static func makeAddOnTextField() -> TextFieldAddonsUIView {
return TextFieldAddonsUIView(
theme: SparkTheme.shared,
intent: .alert
)
view.text = "I couldn't add addOnTextField. It is not UIControl for now"
return view
}

static func makeRatingInput() -> RatingInputUIView {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// TextEditorComponentView.swift
// SparkDemo
//
// Created by alican.aycil on 20.06.24.
// Copyright © 2024 Adevinta. All rights reserved.
//

import SwiftUI
@_spi(SI_SPI) import SparkCommon
import SparkCore

@available(iOS 16.0, *)
struct TextEditorComponentView: View {

// MARK: - Constants

private enum Constants {
static let minHeightRange = 75
}

// MARK: - Properties

@State private var theme: Theme = SparkThemePublisher.shared.theme
@State private var intent: TextEditorIntent = .neutral
@State var text: String = ""
@State private var placeholderType: TextEditorContent = .short
@State private var isEnabledState: CheckboxSelectionState = .selected
@State private var heightType: HeightType = .none
@State private var heightValue: Int = Constants.minHeightRange

@ScaledMetric private var scaleFactor: CGFloat = 1.0

// MARK: - View

var body: some View {
Component(
name: "TextEditor",
configuration: {
ThemeSelector(theme: self.$theme)

EnumSelector(
title: "Intent",
dialogTitle: "",
values: TextEditorIntent.allCases,
value: self.$intent
)

EnumSelector(
title: "PlaceHolder Type",
dialogTitle: "",
values: TextEditorContent.allCases,
value: self.$placeholderType
)

Checkbox(title: "Is Enabled", selectionState: $isEnabledState)

EnumSelector(
title: "Height Type",
dialogTitle: "",
values: HeightType.allCases,
value: self.$heightType
)

if self.heightType.shouldSetFrame {
RangeSelector(
title: "Height value",
range: Constants.minHeightRange...400,
selectedValue: self.$heightValue,
stepper: 25
)
}
},
integration: {
VStack {
TextEditorView(
self.contentType(self.placeholderType),
text: self.$text,
theme: self.theme,
intent: self.intent
)
.disabled(self.isEnabledState == .unselected)
.frame(
type: self.heightType,
value: self.heightValue,
scaleFactor: self.scaleFactor
)
}
}
)
}

private func contentType(_ value: TextEditorContent) -> String {
switch value {
case .none:
return ""
case .short:
return "What is Lorem Ipsum?"
case .medium:
return "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
case .long:
return "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English."
}
}
}

// MARK: - Extension

private extension View {

@ViewBuilder
func frame(type: HeightType, value: Int, scaleFactor: CGFloat) -> some View {
switch type {
case .none:
self
case .fixed:
self.frame(height: CGFloat(value) * scaleFactor)
case .minimum:
self.frame(minHeight: CGFloat(value) * scaleFactor)
}
}
}

// MARK: - Enum

private enum HeightType: CaseIterable {
case none
case fixed
case minimum

// MARK: - Properties

var shouldSetFrame: Bool {
switch self {
case .none: false
case .fixed, .minimum: true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct TextFieldComponentView: View {
@State private var isShowingLeftAlert: Bool = false
@State private var isShowingRightAlert: Bool = false

@State var text: String = "Hello"
@State var text: String = ""

var body: some View {
Component(
Expand Down

0 comments on commit e389021

Please sign in to comment.