diff --git a/.Demo/Classes/Enum/UIComponent.swift b/.Demo/Classes/Enum/UIComponent.swift index 7f0784874..35e8493b3 100644 --- a/.Demo/Classes/Enum/UIComponent.swift +++ b/.Demo/Classes/Enum/UIComponent.swift @@ -16,6 +16,7 @@ struct UIComponent: RawRepresentable, CaseIterable, Equatable { .iconButton, .checkbox, .chip, + .divider, .formField, .icon, .popover, @@ -43,6 +44,7 @@ struct UIComponent: RawRepresentable, CaseIterable, Equatable { static let button = UIComponent(rawValue: "Button") static let checkbox = UIComponent(rawValue: "Checkbox") static let chip = UIComponent(rawValue: "Chip") + static let divider = UIComponent(rawValue: "Divider") static let formField = UIComponent(rawValue: "FormField") static let icon = UIComponent(rawValue: "Icon") static let iconButton = UIComponent(rawValue: "Icon Button") diff --git a/.Demo/Classes/View/Components/ComponentsView.swift b/.Demo/Classes/View/Components/ComponentsView.swift index 4e7ae964b..82658c5a3 100644 --- a/.Demo/Classes/View/Components/ComponentsView.swift +++ b/.Demo/Classes/View/Components/ComponentsView.swift @@ -51,6 +51,10 @@ struct ComponentsView: View { } } + Button("Divider") { + self.navigateToView(DividerComponentView()) + } + Button("FormField") { self.navigateToView(FormFieldComponentView()) } diff --git a/.Demo/Classes/View/Components/ComponentsViewController.swift b/.Demo/Classes/View/Components/ComponentsViewController.swift index fa6876b9e..de698986c 100644 --- a/.Demo/Classes/View/Components/ComponentsViewController.swift +++ b/.Demo/Classes/View/Components/ComponentsViewController.swift @@ -83,6 +83,8 @@ extension ComponentsViewController { ) case .chip: viewController = ChipComponentViewController.build() + case .divider: + viewController = DividerComponentUIViewController.build() case .formField: viewController = FormFieldComponentUIViewController.build() case .icon: diff --git a/.Demo/Classes/View/Components/Divider/SwiftUI/DividerComponentView.swift b/.Demo/Classes/View/Components/Divider/SwiftUI/DividerComponentView.swift new file mode 100644 index 000000000..70268e12e --- /dev/null +++ b/.Demo/Classes/View/Components/Divider/SwiftUI/DividerComponentView.swift @@ -0,0 +1,85 @@ +// +// DividerComponentView.swift +// SparkDemo +// +// Created by louis.borlee on 31/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import SwiftUI +import SparkDivider + +struct DividerComponentView: View { + + private let theme = SparkTheme() + + @State private var text: String = "Text" + @State private var intent: DividerIntent = .outline + @State private var axis: DividerAxis = .horizontal + @State private var alignment: DividerAlignment = .center + + @State private var withText: CheckboxSelectionState = .unselected + + var body: some View { + Component( + name: "Divider") { + EnumSelector( + title: "Intent", + dialogTitle: "Select an intent", + values: DividerIntent.allCases, + value: self.$intent + ) + EnumSelector( + title: "Axis", + dialogTitle: "Select an axis", + values: DividerAxis.allCases, + value: self.$axis + ) + EnumSelector( + title: "Alignment", + dialogTitle: "Select an alignment", + values: DividerAlignment.allCases, + value: self.$alignment + ) + HStack(spacing: 12) { + Text("Text") + TextField("Divider text", text: $text) + .textFieldStyle(.roundedBorder) + } + } integration: { + let divider = divider() + if axis == .vertical { + divider + .frame(height: 300) + } else { + divider + } + } + } + + @ViewBuilder + private func divider() -> some View { + HStack { + Spacer(minLength: 0) + if text.isEmpty == false { + DividerView( + theme: theme, + intent: intent, + axis: axis, + alignment: alignment, + text: { + Text(text) + } + ) + } else { + DividerView( + theme: SparkTheme(), + intent: intent, + axis: axis, + alignment: alignment + ) + } + Spacer(minLength: 0) + } + } +} diff --git a/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIView.swift b/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIView.swift new file mode 100644 index 000000000..118d32a8c --- /dev/null +++ b/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIView.swift @@ -0,0 +1,103 @@ +// +// DividerComponentUIView.swift +// SparkDemo +// +// Created by louis.borlee on 31/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import Combine +import UIKit +@_spi(SI_SPI) import SparkCommon + +final class DividerComponentUIView: ComponentUIView { + + // MARK: - Components + private let componentView: DividerUIView + + // MARK: - Properties + + private let viewModel: DividerComponentUIViewModel + private var cancellables: Set = [] + + private var heightConstraint = NSLayoutConstraint() + + // MARK: - Initializer + init(viewModel: DividerComponentUIViewModel) { + self.viewModel = viewModel + + let divider = DividerUIView( + theme: self.viewModel.theme, + intent: self.viewModel.intent + ) + divider.label.text = self.viewModel.text + divider.label.numberOfLines = 0 + divider.axis = self.viewModel.axis + divider.alignment = self.viewModel.alignment + + self.componentView = divider + + super.init( + viewModel: viewModel, + integrationStackViewAlignment: .fill, + componentView: self.componentView + ) + + // Setup + 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.$axis.subscribe(in: &self.cancellables) { [weak self] axis in + guard let self = self else { return } + self.viewModel.axisConfigurationItemViewModel.buttonTitle = axis.name + self.componentView.axis = axis + if axis == .vertical { + self.heightConstraint = self.componentView.heightAnchor.constraint(equalToConstant: 300) + self.heightConstraint.isActive = true + } else { + self.heightConstraint.isActive = false + self.componentView.removeConstraint(self.heightConstraint) + } + } + + self.viewModel.$alignment.subscribe(in: &self.cancellables) { [weak self] alignment in + guard let self = self else { return } + self.viewModel.alignmentConfigurationItemViewModel.buttonTitle = alignment.name + self.componentView.alignment = alignment + } + + self.viewModel.$text.subscribe(in: &self.cancellables) { [weak self] text in + guard let self = self else { return } + self.viewModel.textConfigurationItemViewModel.buttonTitle = text + self.componentView.label.text = text + if let text, text.isEmpty == false { + self.componentView.showLabel = true + } else { + self.componentView.showLabel = false + } + } + } +} diff --git a/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIViewController.swift b/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIViewController.swift new file mode 100644 index 000000000..b89b5b3b4 --- /dev/null +++ b/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIViewController.swift @@ -0,0 +1,128 @@ +// +// DividerComponentUIViewController.swift +// SparkDemo +// +// Created by louis.borlee on 31/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import Combine +import SparkCore +import SwiftUI +import UIKit +@_spi(SI_SPI) import SparkCommon + +final class DividerComponentUIViewController: UIViewController { + + // MARK: - Published Properties + @ObservedObject private var themePublisher = SparkThemePublisher.shared + + // MARK: - Properties + let componentView: DividerComponentUIView + let viewModel: DividerComponentUIViewModel + private var cancellables: Set = [] + + // MARK: - Initializer + init(viewModel: DividerComponentUIViewModel) { + self.viewModel = viewModel + self.componentView = DividerComponentUIView(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 = "Divider" + self.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.showAxisSheet.subscribe(in: &self.cancellables) { axis in + self.presentAxisActionSheet(axis) + } + + self.viewModel.showAlignmentSheet.subscribe(in: &self.cancellables) { alignments in + self.presentAlignmentActionSheet(alignments) + } + } +} + +// MARK: - Builder +extension DividerComponentUIViewController { + + static func build() -> DividerComponentUIViewController { + let viewModel = DividerComponentUIViewModel(theme: SparkThemePublisher.shared.theme) + return DividerComponentUIViewController(viewModel: viewModel) + } +} + +// MARK: - Navigation +extension DividerComponentUIViewController { + + 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: [DividerIntent]) { + let actionSheet = SparkActionSheet.init( + values: intents, + texts: intents.map { $0.name }) { intent in + self.viewModel.intent = intent + } + self.present(actionSheet, isAnimated: true) + } + + private func presentAxisActionSheet(_ axis: [DividerAxis]) { + let actionSheet = SparkActionSheet.init( + values: axis, + texts: axis.map { $0.name }) { axis in + self.viewModel.axis = axis + } + self.present(actionSheet, isAnimated: true) + } + + private func presentAlignmentActionSheet(_ alignments: [DividerAlignment]) { + let actionSheet = SparkActionSheet.init( + values: alignments, + texts: alignments.map { $0.name }) { alignment in + self.viewModel.alignment = alignment + } + self.present(actionSheet, isAnimated: true) + } +} diff --git a/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIViewModel.swift b/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIViewModel.swift new file mode 100644 index 000000000..7b457d378 --- /dev/null +++ b/.Demo/Classes/View/Components/Divider/UIKit/DividerComponentUIViewModel.swift @@ -0,0 +1,122 @@ +// +// DividerComponentUIViewModel.swift +// SparkDemo +// +// Created by louis.borlee on 31/07/2024. +// Copyright © 2024 Adevinta. All rights reserved. +// + +import UIKit +import Combine + +final class DividerComponentUIViewModel: ComponentUIViewModel, ObservableObject { + + let themes = ThemeCellModel.themes + + @Published var theme: Theme + @Published var text: String? + @Published var intent = DividerIntent.outline + @Published var axis = DividerAxis.horizontal + @Published var alignment = DividerAlignment.center + + // MARK: - Published Properties + var showThemeSheet: AnyPublisher<[ThemeCellModel], Never> { + self.showThemeSheetSubject + .eraseToAnyPublisher() + } + var showIntentSheet: AnyPublisher<[DividerIntent], Never> { + self.showIntentSheetSubject + .eraseToAnyPublisher() + } + var showAxisSheet: AnyPublisher<[DividerAxis], Never> { + self.showAxisSheetSubject + .eraseToAnyPublisher() + } + var showAlignmentSheet: AnyPublisher<[DividerAlignment], Never> { + self.showAlignmentSheetSubject + .eraseToAnyPublisher() + } + + private var showThemeSheetSubject: PassthroughSubject<[ThemeCellModel], Never> = .init() + private var showIntentSheetSubject: PassthroughSubject<[DividerIntent], Never> = .init() + private var showAxisSheetSubject: PassthroughSubject<[DividerAxis], Never> = .init() + private var showAlignmentSheetSubject: PassthroughSubject<[DividerAlignment], Never> = .init() + + 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 axisConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Axis", + type: .button, + target: (source: self, action: #selector(self.presentAxisSheet)) + ) + }() + + lazy var alignmentConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Alignment", + type: .button, + target: (source: self, action: #selector(self.presentAlignmentSheet)) + ) + }() + + lazy var textConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = { + return .init( + name: "Text", + type: .input(text: self.text), + target: (source: self, action: #selector(self.textChanged(_:)))) + }() + + override func configurationItemsViewModel() -> [ComponentsConfigurationItemUIViewModel] { + return [ + self.themeConfigurationItemViewModel, + self.intentConfigurationItemViewModel, + self.axisConfigurationItemViewModel, + self.alignmentConfigurationItemViewModel, + self.textConfigurationItemViewModel + ] + } + + init(theme: any Theme) { + self.theme = theme + super.init(identifier: "Divider") + } + + @objc func presentThemeSheet() { + self.showThemeSheetSubject.send(self.themes) + } + + @objc func presentIntentSheet() { + self.showIntentSheetSubject.send(DividerIntent.allCases) + } + + @objc func presentAxisSheet() { + self.showAxisSheetSubject.send(DividerAxis.allCases) + } + + @objc func presentAlignmentSheet() { + self.showAlignmentSheetSubject.send(DividerAlignment.allCases) + } + + @objc func textChanged(_ textField: UITextField) { + if textField.text?.isEmpty == false { + self.text = textField.text + } else { + self.text = nil + } + } +} diff --git a/Package.swift b/Package.swift index ec191d22f..36e6d0f0a 100644 --- a/Package.swift +++ b/Package.swift @@ -56,6 +56,11 @@ let package = Package( // path: "../spark-ios-component-chip" /*version*/ "0.0.1"..."999.999.999" ), + .package( + url: "https://github.com/adevinta/spark-ios-component-divider.git", +// path: "../spark-ios-component-divider" + /*version*/ "0.0.1"..."999.999.999" + ), .package( url: "https://github.com/adevinta/spark-ios-component-form-field.git", // path: "../spark-ios-component-form-field" @@ -161,6 +166,10 @@ let package = Package( name: "SparkChip", package: "spark-ios-component-chip" ), + .product( + name: "SparkDivider", + package: "spark-ios-component-divider" + ), .product( name: "SparkFormField", package: "spark-ios-component-form-field" diff --git a/Sources/Core/Core.swift b/Sources/Core/Core.swift index b50f94994..598812cb2 100644 --- a/Sources/Core/Core.swift +++ b/Sources/Core/Core.swift @@ -8,6 +8,7 @@ @_exported import SparkButton @_exported import SparkCheckbox @_exported import SparkChip +@_exported import SparkDivider @_exported import SparkFormField @_exported import SparkIcon @_exported import SparkPopover