diff --git a/.Demo/Classes/Enum/UIComponent.swift b/.Demo/Classes/Enum/UIComponent.swift index c740a5234..a052ea963 100644 --- a/.Demo/Classes/Enum/UIComponent.swift +++ b/.Demo/Classes/Enum/UIComponent.swift @@ -32,6 +32,7 @@ struct UIComponent: RawRepresentable, CaseIterable, Equatable { .switch, .tab, .tag, + .textEditor, .textField, .textFieldAddons, .textLink @@ -60,6 +61,7 @@ struct UIComponent: RawRepresentable, CaseIterable, Equatable { static let star = UIComponent(rawValue: "Star") static let `switch` = UIComponent(rawValue: "Switch") static let tab = UIComponent(rawValue: "Tab") + static let textEditor = UIComponent(rawValue: "TextEditor") static let tag = UIComponent(rawValue: "Tag") static let textField = UIComponent(rawValue: "TextField") static let textFieldAddons = UIComponent(rawValue: "TextFieldAddons") diff --git a/.Demo/Classes/View/Components/ComponentsViewController.swift b/.Demo/Classes/View/Components/ComponentsViewController.swift index 00cfba432..42e2876a2 100644 --- a/.Demo/Classes/View/Components/ComponentsViewController.swift +++ b/.Demo/Classes/View/Components/ComponentsViewController.swift @@ -116,6 +116,8 @@ extension ComponentsViewController { viewController = TabComponentUIViewController.build() case .tag: viewController = TagComponentUIViewController.build() + case .textEditor: + viewController = TextEditorComponentUIViewController.build() case .textField: viewController = TextFieldComponentUIViewController.build() case .textFieldAddons: diff --git a/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIView.swift b/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIView.swift new file mode 100644 index 000000000..652092d84 --- /dev/null +++ b/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIView.swift @@ -0,0 +1,133 @@ +// +// TextEditorComponentUIView.swift +// SparkDemo +// +// Created by alican.aycil on 12.06.24. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import Combine +import SparkCore +import UIKit +@_spi(SI_SPI) import SparkCommon + +final class TextEditorComponentUIView: ComponentUIView, UIGestureRecognizerDelegate { + + // MARK: - Components + + private var componentView: TextEditorUIView + + // MARK: - Properties + + private let viewModel: TextEditorComponentUIViewModel + private var cancellables: Set = [] + + private var widthLayoutConstraint: NSLayoutConstraint + private var heightLayoutConstraint: NSLayoutConstraint + + // MARK: - Initializer + + init(viewModel: TextEditorComponentUIViewModel) { + self.viewModel = viewModel + self.componentView = Self.makeTextEditorView(viewModel) + + // Constraints + self.widthLayoutConstraint = self.componentView.widthAnchor.constraint(equalToConstant: 300) + self.heightLayoutConstraint = self.componentView.heightAnchor.constraint(equalToConstant: 100) + + super.init( + viewModel: viewModel, + componentView: self.componentView + ) + + // Setup + let tap = UITapGestureRecognizer(target: self, action: #selector(self.viewDidTapped)) + tap.delegate = self + addGestureRecognizer(tap) + + self.setupSubscriptions() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Subscribe + private func setupSubscriptions() { + self.viewModel.$theme.subscribe(in: &self.cancellables) { [weak self] theme in + guard let self = self else { return } + let themes = self.viewModel.themes + let themeTitle: String? = theme is SparkTheme ? themes.first?.title : themes.last?.title + + self.viewModel.themeConfigurationItemViewModel.buttonTitle = themeTitle + self.viewModel.configurationViewModel.update(theme: theme) + + self.componentView.theme = theme + } + + self.viewModel.$intent.subscribe(in: &self.cancellables) { [weak self] intent in + guard let self = self else { return } + self.viewModel.intentConfigurationItemViewModel.buttonTitle = intent.name + self.componentView.intent = intent + } + + self.viewModel.$text.subscribe(in: &self.cancellables) { [weak self] type in + guard let self = self else { return } + self.viewModel.textConfigurationItemViewModel.buttonTitle = type.name + self.componentView.text = Self.setText(type: type) + } + + self.viewModel.$placeholder.subscribe(in: &self.cancellables) { [weak self] type in + guard let self = self else { return } + self.viewModel.placeholderConfigurationItemViewModel.buttonTitle = type.name + self.componentView.placeholder = Self.setText(type: type) + } + + self.viewModel.$isEnabled.subscribe(in: &self.cancellables) { [weak self] isEnabled in + guard let self = self else { return } + self.componentView.isEnabled = isEnabled + } + + self.viewModel.$isEditable.subscribe(in: &self.cancellables) { [weak self] isEditable in + guard let self = self else { return } + self.componentView.isEditable = isEditable + } + + self.viewModel.$isStaticSizes.subscribe(in: &self.cancellables) { [weak self] isStaticSizes in + guard let self = self else { return } + self.widthLayoutConstraint.isActive = isStaticSizes + self.heightLayoutConstraint.isActive = isStaticSizes + self.componentView.isScrollEnabled = isStaticSizes + } + } + + static private func makeTextEditorView(_ viewModel: TextEditorComponentUIViewModel) -> TextEditorUIView { + let view = TextEditorUIView( + theme: viewModel.theme, + intent: viewModel.intent + ) + view.text = TextEditorComponentUIView.setText(type: viewModel.text) + view.placeholder = TextEditorComponentUIView.setText(type: viewModel.placeholder) + view.isEnabled = viewModel.isEnabled + view.isEditable = viewModel.isEditable + view.isScrollEnabled = viewModel.isStaticSizes + return view + } + + static private func setText(type: TextEditorContent) -> String { + switch type { + 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." + } + } + + @objc private func viewDidTapped() { + _ = self.componentView.resignFirstResponder() + } +} diff --git a/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIViewController.swift b/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIViewController.swift new file mode 100644 index 000000000..a1b6a26b9 --- /dev/null +++ b/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIViewController.swift @@ -0,0 +1,128 @@ +// +// TextEditorComponentUIViewController.swift +// SparkDemo +// +// Created by alican.aycil on 29.05.24. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import Combine +import SparkCore +import SwiftUI +import UIKit +@_spi(SI_SPI) import SparkCommon + +final class TextEditorComponentUIViewController: UIViewController { + + // MARK: - Published Properties + @ObservedObject private var themePublisher = SparkThemePublisher.shared + + // MARK: - Properties + let componentView: TextEditorComponentUIView + let viewModel: TextEditorComponentUIViewModel + private var cancellables: Set = [] + + // MARK: - Initializer + init(viewModel: TextEditorComponentUIViewModel) { + self.viewModel = viewModel + self.componentView = TextEditorComponentUIView(viewModel: viewModel) + super.init(nibName: nil, bundle: nil) + + self.componentView.viewController = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + override func loadView() { + super.loadView() + view = componentView + } + + // MARK: - ViewDidLoad + override func viewDidLoad() { + super.viewDidLoad() + self.navigationItem.title = "TextEditor" + addPublisher() + } + + // MARK: - Add Publishers + private func addPublisher() { + + self.themePublisher + .$theme + .sink { [weak self] theme in + guard let self = self else { return } + self.viewModel.theme = theme + self.navigationController?.navigationBar.tintColor = theme.colors.main.main.uiColor + } + .store(in: &self.cancellables) + + self.viewModel.showThemeSheet.subscribe(in: &self.cancellables) { intents in + self.presentThemeActionSheet(intents) + } + + self.viewModel.showIntentSheet.subscribe(in: &self.cancellables) { intents in + self.presentIntentActionSheet(intents) + } + + self.viewModel.showTextSheet.subscribe(in: &self.cancellables) { types in + self.presentTextActionSheet(types) + } + + self.viewModel.showPlaceholderSheet.subscribe(in: &self.cancellables) { types in + self.presentPlaceholdderActionSheet(types) + } + } +} + +// MARK: - Builder +extension TextEditorComponentUIViewController { + + static func build() -> TextEditorComponentUIViewController { + let viewModel = TextEditorComponentUIViewModel(theme: SparkThemePublisher.shared.theme) + return TextEditorComponentUIViewController(viewModel: viewModel) + } +} + +// MARK: - Navigation +extension TextEditorComponentUIViewController { + + private func presentThemeActionSheet(_ themes: [ThemeCellModel]) { + let actionSheet = SparkActionSheet.init( + values: themes.map { $0.theme }, + texts: themes.map { $0.title }) { theme in + self.themePublisher.theme = theme + } + self.present(actionSheet, isAnimated: true) + } + + private func presentIntentActionSheet(_ intents: [TextEditorIntent]) { + let actionSheet = SparkActionSheet.init( + values: intents, + texts: intents.map { $0.name }) { intent in + self.viewModel.intent = intent + } + self.present(actionSheet, isAnimated: true) + } + + private func presentTextActionSheet(_ types: [TextEditorContent]) { + let actionSheet = SparkActionSheet.init( + values: types, + texts: types.map{ $0.name }) { text in + self.viewModel.text = text + } + self.present(actionSheet, isAnimated: true) + } + + private func presentPlaceholdderActionSheet(_ types: [TextEditorContent]) { + let actionSheet = SparkActionSheet.init( + values: types, + texts: types.map{ $0.name }) { text in + self.viewModel.placeholder = text + } + self.present(actionSheet, isAnimated: true) + } +} diff --git a/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIViewModel.swift b/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIViewModel.swift new file mode 100644 index 000000000..748f6dc30 --- /dev/null +++ b/.Demo/Classes/View/Components/TextEditor/UIKit/TextEditorComponentUIViewModel.swift @@ -0,0 +1,186 @@ +// +// TextEditorComponentUIViewModel.swift +// SparkDemo +// +// Created by alican.aycil on 12.06.24. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import Combine +@_spi(SI_SPI) import SparkCommon +import SparkCore +import UIKit + +final class TextEditorComponentUIViewModel: ComponentUIViewModel { + + // MARK: - Published Properties + var showThemeSheet: AnyPublisher<[ThemeCellModel], Never> { + showThemeSheetSubject + .eraseToAnyPublisher() + } + + var showIntentSheet: AnyPublisher<[TextEditorIntent], Never> { + showIntentSheetSubject + .eraseToAnyPublisher() + } + + var showTextSheet: AnyPublisher<[TextEditorContent], Never> { + showTextSheetSubject + .eraseToAnyPublisher() + } + + var showPlaceholderSheet: AnyPublisher<[TextEditorContent], Never> { + showPlaceHolderSheetSubject + .eraseToAnyPublisher() + } + + // MARK: - Private Properties + private var showThemeSheetSubject: PassthroughSubject<[ThemeCellModel], Never> = .init() + private var showIntentSheetSubject: PassthroughSubject<[TextEditorIntent], Never> = .init() + private var showTextSheetSubject: PassthroughSubject<[TextEditorContent], Never> = .init() + private var showPlaceHolderSheetSubject: PassthroughSubject<[TextEditorContent], Never> = .init() + + // MARK: - Items Properties + lazy var themeConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Theme", + type: .button, + target: (source: self, action: #selector(self.presentThemeSheet)) + ) + }() + + lazy var intentConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Intent", + type: .button, + target: (source: self, action: #selector(self.presentIntentSheet)) + ) + }() + + lazy var textConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Text Type", + type: .button, + target: (source: self, action: #selector(self.presentTextSheet)) + ) + }() + + lazy var placeholderConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Placeholder Type", + type: .button, + target: (source: self, action: #selector(self.presentPlaceholderSheet)) + ) + }() + + lazy var isEnabledConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "is Enabled", + type: .checkbox(title: "", isOn: self.isEnabled), + target: (source: self, action: #selector(self.enabledChanged(_:)))) + }() + + lazy var isEditableConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Is Editable", + type: .checkbox(title: "", isOn: self.isEditable), + target: (source: self, action: #selector(self.isEditableChanged(_:)))) + }() + + lazy var sizesConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Is Static Sizes", + type: .checkbox(title: "", isOn: self.isStaticSizes), + target: (source: self, action: #selector(self.staticSizesChanged(_:)))) + }() + + // MARK: - Methods + + override func configurationItemsViewModel() -> [ComponentsConfigurationItemUIViewModel] { + return [ + self.themeConfigurationItemViewModel, + self.intentConfigurationItemViewModel, + self.textConfigurationItemViewModel, + self.placeholderConfigurationItemViewModel, + self.isEnabledConfigurationItemViewModel, + self.isEditableConfigurationItemViewModel, + self.sizesConfigurationItemViewModel + ] + } + + // MARK: - Inherited Properties + + var themes = ThemeCellModel.themes + + // MARK: - Default Value Properties + let image: UIImage = UIImage(named: "alert") ?? UIImage() + + // MARK: - Initialization + @Published var theme: Theme + @Published var intent: TextEditorIntent + @Published var text: TextEditorContent + @Published var placeholder: TextEditorContent + @Published var isEnabled: Bool + @Published var isEditable: Bool + @Published var isStaticSizes: Bool + + init( + theme: Theme, + intent: TextEditorIntent = .neutral, + text: TextEditorContent = .medium, + placeholder: TextEditorContent = .short, + isEnabled: Bool = true, + isEditable: Bool = true, + isDynamicHeight: Bool = true, + isStaticSizes: Bool = false + ) { + self.theme = theme + self.intent = intent + self.text = text + self.placeholder = placeholder + self.isEnabled = isEnabled + self.isEditable = isEditable + self.isStaticSizes = isStaticSizes + + super.init(identifier: "TextEditor") + } +} + +// MARK: - Navigation +extension TextEditorComponentUIViewModel { + + @objc func presentThemeSheet() { + self.showThemeSheetSubject.send(themes) + } + + @objc func presentIntentSheet() { + self.showIntentSheetSubject.send(TextEditorIntent.allCases) + } + + @objc func presentTextSheet() { + self.showTextSheetSubject.send(TextEditorContent.allCases) + } + + @objc func presentPlaceholderSheet() { + self.showPlaceHolderSheetSubject.send(TextEditorContent.allCases) + } + + @objc func enabledChanged(_ isSelected: Any?) { + self.isEnabled = isTrue(isSelected) + } + + @objc func isEditableChanged(_ isSelected: Any?) { + self.isEditable = isTrue(isSelected) + } + + @objc func staticSizesChanged(_ isSelected: Any?) { + self.isStaticSizes = isTrue(isSelected) + } +} + +enum TextEditorContent: CaseIterable { + case none + case short + case medium + case long +} diff --git a/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift index 4c34fbcc1..75069961c 100644 --- a/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift +++ b/.Demo/Classes/View/Components/TextField/UIKit/TextFieldComponentUIView.swift @@ -11,7 +11,6 @@ import UIKit import SparkCore @_spi(SI_SPI) import SparkCommon -// swiftlint:disable no_debugging_method final class TextFieldComponentUIView: ComponentUIView { private let viewModel: TextFieldComponentUIViewModel @@ -30,6 +29,7 @@ final class TextFieldComponentUIView: ComponentUIView { self.textField.placeholder = "Placeholder" self.textField.delegate = self + self.setupSubscriptions() } diff --git a/Package.swift b/Package.swift index a2dd78a52..93c4ef1d6 100644 --- a/Package.swift +++ b/Package.swift @@ -58,7 +58,7 @@ let package = Package( ), .package( url: "https://github.com/adevinta/spark-ios-component-divider.git", -// path: "../spark-ios-component-divider" + // path: "../spark-ios-component-divider" /*version*/ "0.0.1"..."999.999.999" ), .package( @@ -72,8 +72,8 @@ let package = Package( /*version*/ "0.0.1"..."999.999.999" ), .package( - url: "https://github.com/adevinta/spark-ios-component-popover.git", -// path: "../spark-ios-component-popover" + url: "https://github.com/adevinta/spark-ios-component-popover.git", + // path: "../spark-ios-component-popover" /*version*/ "0.0.1"..."999.999.999" ), .package( @@ -127,8 +127,8 @@ let package = Package( /*version*/ "0.0.1"..."999.999.999" ), .package( - url: "https://github.com/adevinta/spark-ios-component-text-field.git", - // path: "../spark-ios-component-text-field" + url: "https://github.com/adevinta/spark-ios-component-text-input.git", + // path: "../spark-ios-component-text-input" /*version*/ "0.0.1"..."999.999.999" ), .package( @@ -228,8 +228,8 @@ let package = Package( package: "spark-ios-component-tag" ), .product( - name: "SparkTextField", - package: "spark-ios-component-text-field" + name: "SparkTextInput", + package: "spark-ios-component-text-input" ), .product( name: "SparkTextLink", diff --git a/Sources/Core/Core.swift b/Sources/Core/Core.swift index 552187d06..003f19111 100644 --- a/Sources/Core/Core.swift +++ b/Sources/Core/Core.swift @@ -22,5 +22,5 @@ @_exported import SparkSwitch @_exported import SparkTab @_exported import SparkTag -@_exported import SparkTextField +@_exported import SparkTextInput @_exported import SparkTextLink