From 0d8465779f794c64568a680b7dd07455b413f680 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Fri, 12 May 2023 15:18:44 +0200 Subject: [PATCH 01/31] Added ui view for badge with models --- .../BadgeAccessabilityIdentifier.swift | 17 ++++ .../Badge/Formatter/BadgeFormatter.swift | 28 ++++++ .../Components/Badge/Model/BadgeColors.swift | 26 ++++++ .../Badge/Model/BadgeIntentColors.swift | 25 ++++++ .../Badge/Model/BadgeIntentType.swift | 20 +++++ .../Components/Badge/View/BadgeUIView.swift | 68 +++++++++++++++ .../Badge/ViewModel/BadgeViewModel.swift | 85 +++++++++++++++++++ .../Components/Badge/Bage+UIPresentable.swift | 33 +++++++ 8 files changed, 302 insertions(+) create mode 100644 core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift create mode 100644 core/Sources/Components/Badge/Formatter/BadgeFormatter.swift create mode 100644 core/Sources/Components/Badge/Model/BadgeColors.swift create mode 100644 core/Sources/Components/Badge/Model/BadgeIntentColors.swift create mode 100644 core/Sources/Components/Badge/Model/BadgeIntentType.swift create mode 100644 core/Sources/Components/Badge/View/BadgeUIView.swift create mode 100644 core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift create mode 100644 spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift diff --git a/core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift b/core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift new file mode 100644 index 000000000..a637c4a10 --- /dev/null +++ b/core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift @@ -0,0 +1,17 @@ +// +// BadgeAccessabilityIdentifier.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + + +public enum BadgeAccessabilityIdentifier { + + // MARK: - Properties + + public static let text = "spark-badge-text" +} diff --git a/core/Sources/Components/Badge/Formatter/BadgeFormatter.swift b/core/Sources/Components/Badge/Formatter/BadgeFormatter.swift new file mode 100644 index 000000000..1e09bda09 --- /dev/null +++ b/core/Sources/Components/Badge/Formatter/BadgeFormatter.swift @@ -0,0 +1,28 @@ +// +// BadgeFormatter.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +public protocol BadgeFormatting { + func badgeText(value: String?) -> String +} + +/// With this formatter you can define behaviour of Badge label. +/// +/// Use **standart** for regular counting behavior with numbers. +/// If there would be more than 99 notifications -- it will show 99+ +/// +/// **Thousands** counter allows you to use counting for bigger numbers: 35k -> 35000 +/// +/// You can defune your custom behavior by using **custom** type. But in that case +/// Fromatter should be implemented and conform to **BadgeFormatting** protocol +public enum BadgeFormatter { + case standart + case thousandsCounter + case custom(formatter: BadgeFormatting) +} diff --git a/core/Sources/Components/Badge/Model/BadgeColors.swift b/core/Sources/Components/Badge/Model/BadgeColors.swift new file mode 100644 index 000000000..1b788643c --- /dev/null +++ b/core/Sources/Components/Badge/Model/BadgeColors.swift @@ -0,0 +1,26 @@ +// +// BadgeColors.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +// sourcery: AutoMockable +protocol BadgeColorables { + var backgroundColor: ColorToken { get } + var borderColor: ColorToken { get } + var foregroundColor: ColorToken { get } +} + +struct BadgeColors: BadgeColorables { + + // MARK: - Properties + + let backgroundColor: ColorToken + let borderColor: ColorToken + let foregroundColor: ColorToken +} + diff --git a/core/Sources/Components/Badge/Model/BadgeIntentColors.swift b/core/Sources/Components/Badge/Model/BadgeIntentColors.swift new file mode 100644 index 000000000..f5882478f --- /dev/null +++ b/core/Sources/Components/Badge/Model/BadgeIntentColors.swift @@ -0,0 +1,25 @@ +// +// BadgeIntentColors.swift +// Spark +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +// sourcery: AutoMockable +protocol BadgeIntentColorables { + var color: ColorToken { get } + var onColor: ColorToken { get } + var surface: ColorToken { get } +} + +struct BadgeIntentColors: BadgeIntentColorables { + + // MARK: - Properties + + let color: ColorToken + let onColor: ColorToken + let surface: ColorToken +} diff --git a/core/Sources/Components/Badge/Model/BadgeIntentType.swift b/core/Sources/Components/Badge/Model/BadgeIntentType.swift new file mode 100644 index 000000000..6566c2abe --- /dev/null +++ b/core/Sources/Components/Badge/Model/BadgeIntentType.swift @@ -0,0 +1,20 @@ +// +// BadgeIntentType.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +// sourcery: AutoMockable +public enum BadgeIntentType { + case primary + case secondary + case success + case neutral + case danger + case alert + case info +} diff --git a/core/Sources/Components/Badge/View/BadgeUIView.swift b/core/Sources/Components/Badge/View/BadgeUIView.swift new file mode 100644 index 000000000..592b8458e --- /dev/null +++ b/core/Sources/Components/Badge/View/BadgeUIView.swift @@ -0,0 +1,68 @@ +// +// BadgeUIView.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import UIKit + +public class BadgeUIView: UILabel { + + private var viewModel: BadgeViewModel + + public init(viewModel: BadgeViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + setupBadge() + } + + required init?(coder: NSCoder) { + fatalError("Not implemented") + } + + private func setupBadge() { + setupBadgeText() + setupAppearance() + } + + private func setupBadgeText() { + text = viewModel.badgeText + textColor = viewModel.textColor + font = viewModel.textFont.uiFont + textAlignment = .center + } + + private func setupAppearance() { + backgroundColor = viewModel.backgroundColor + translatesAutoresizingMaskIntoConstraints = false + layer.borderWidth = viewModel.borderWidth + layer.borderColor = viewModel.borderColor.uiColor.cgColor + clipsToBounds = true + } + + public override var intrinsicContentSize: CGSize { + let size = CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude) + let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) + let attributes = [NSAttributedString.Key.font: font] + let estimatedSize = NSString(string: text ?? "").boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil) + if viewModel.badgeText.isEmpty { + return viewModel.emptySize + } else { + return CGSize(width: ceil(estimatedSize.width + viewModel.horizontalOffset), height: ceil(estimatedSize.height + viewModel.verticalOffset)) + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + preferredMaxLayoutWidth = frame.size.width + layer.cornerRadius = frame.size.height / 2.0 + } + + public override func didMoveToSuperview() { + super.didMoveToSuperview() + numberOfLines = 0 + lineBreakMode = .byWordWrapping + } +} diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift new file mode 100644 index 000000000..fba508aae --- /dev/null +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -0,0 +1,85 @@ +// +// BadgeViewModel.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import UIKit + +public enum BadgeSize { + case normal + case small +} + +public struct BadgeStyle { + let badgeSize: BadgeSize + let badgeType: BadgeIntentType + let badgeColors: BadgeColorables + let isBadgeOutlined: Bool + + public init(badgeSize: BadgeSize, badgeType: BadgeIntentType, isBadgeOutlined: Bool, theme: Theme) { + self.badgeSize = badgeSize + self.badgeType = badgeType + self.isBadgeOutlined = isBadgeOutlined + self.badgeColors = BadgeGetColorsUseCase().execute(from: theme, badgeType: badgeType) + } +} + +public class BadgeViewModel { + + private var formatter: BadgeFormatter + public var badgeText: String { + switch formatter { + case .standart: + return value ?? "" + case .thousandsCounter: + if let value, let intValue = Int(value) { + return "\(Int(intValue / 1000))k" + } else { + return value ?? "" + } + case .custom(let formatter): + return formatter.badgeText(value: value) + } + } + private var value: String? + private(set) var badgeStyle: BadgeStyle + private var theme: Theme + public var backgroundColor: UIColor { + badgeStyle.badgeColors.backgroundColor.uiColor + } + public var borderColor: ColorToken { + badgeStyle.isBadgeOutlined ? badgeStyle.badgeColors.borderColor : ColorTokenDefault.clear + } + public var textColor: UIColor { + badgeStyle.badgeColors.foregroundColor.uiColor + } + public var textFont: TypographyFontToken { + switch badgeStyle.badgeSize { + case .small: + return theme.typography.smallHighlight + case .normal: + return theme.typography.captionHighlight + } + } + public var verticalOffset: CGFloat { + theme.layout.spacing.small * 2 + } + public var horizontalOffset: CGFloat { + theme.layout.spacing.medium * 2 + } + + public var borderWidth: CGFloat { + badgeStyle.isBadgeOutlined ? 2 : 0 + } + public let emptySize: CGSize = .init(width: 12, height: 12) + + public init(formatter: BadgeFormatter, theme: Theme, badgeStyle: BadgeStyle, value: String?) { + self.formatter = formatter + self.value = value + self.badgeStyle = badgeStyle + self.theme = theme + } +} diff --git a/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift new file mode 100644 index 000000000..0dd87769f --- /dev/null +++ b/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift @@ -0,0 +1,33 @@ +// +// Bage+UIPresentable.swift +// Spark +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import SwiftUI +import SparkCore +import Spark + +struct UIBadgeView: UIViewRepresentable { + + func makeUIView(context: Context) -> some UIView { + let uiview = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 300, height: 50))) + uiview.backgroundColor = .cyan + let badgeStyle = BadgeStyle(badgeSize: .small, badgeType: .danger, isBadgeOutlined: false, theme: SparkTheme.shared) + let badgeView = BadgeUIView( + viewModel: .init(formatter: .thousandsCounter, theme: SparkTheme.shared, badgeStyle: badgeStyle, value: "423522342")) + uiview.addSubview(badgeView) + NSLayoutConstraint.activate([ + badgeView.trailingAnchor.constraint(equalTo: uiview.trailingAnchor, constant: 0), + badgeView.centerYAnchor.constraint(equalTo: uiview.centerYAnchor, constant: 0) + ]) + return uiview + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } +} + From 74ec3e27f43b6b3e44001e4d0e522dadad4f9220 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Fri, 12 May 2023 15:18:59 +0200 Subject: [PATCH 02/31] Added SwiftUI badge with previews --- .../GetColors/GetBadgeColorsUseCase.swift | 31 ++++++++ .../GetBadgeIntentColorsUseCase.swift | 70 +++++++++++++++++++ .../Components/Badge/View/BadgeView.swift | 29 ++++++++ .../Components/Badge/BadgeComponentView.swift | 48 +++++++++++++ .../View/Components/ComponentsView.swift | 2 +- 5 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift create mode 100644 core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift create mode 100644 core/Sources/Components/Badge/View/BadgeView.swift create mode 100644 spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift diff --git a/core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift b/core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift new file mode 100644 index 000000000..531fd697f --- /dev/null +++ b/core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift @@ -0,0 +1,31 @@ +// +// GetBadgeColorsUseCase.swift +// Spark +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +// sourcery: AutoMockable +protocol BadgeGetColorsUseCaseable { + func execute(from theme: Theme, + badgeType: BadgeIntentType) -> BadgeColorables +} + +struct BadgeGetColorsUseCase: BadgeGetColorsUseCaseable { + private let getIntentColorsUseCase: BadgeGetIntentColorsUseCaseable + + // MARK: - Initialization + + init(getIntentColorsUseCase: BadgeGetIntentColorsUseCaseable = BadgeGetIntentColorsUseCase()) { + self.getIntentColorsUseCase = getIntentColorsUseCase + } + + // MARK: - Getting colors + + func execute(from theme: Theme, badgeType: BadgeIntentType) -> BadgeColorables { + getIntentColorsUseCase.execute(intentType: badgeType, on: theme.colors) + } +} diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift new file mode 100644 index 000000000..afd3e257c --- /dev/null +++ b/core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift @@ -0,0 +1,70 @@ +// +// BadgeIntentColors.swift +// Spark +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +// sourcery: AutoMockable +protocol BadgeGetIntentColorsUseCaseable { + func execute(intentType: BadgeIntentType, + on colors: Colors) -> BadgeColorables +} + +class BadgeGetIntentColorsUseCase: BadgeGetIntentColorsUseCaseable { + + // MARK: - Methods + + func execute(intentType: BadgeIntentType, + on colors: Colors) -> BadgeColorables { + let surfaceColor = colors.base.surface + + switch intentType { + case .primary: + return BadgeColors( + backgroundColor: colors.primary.primary, + borderColor: surfaceColor, + foregroundColor: colors.primary.onPrimary + ) + case .secondary: + return BadgeColors( + backgroundColor: colors.secondary.secondary, + borderColor: surfaceColor, + foregroundColor: colors.secondary.onSecondary + ) + case .success: + return BadgeColors( + backgroundColor: colors.feedback.success, + borderColor: surfaceColor, + foregroundColor: colors.feedback.onSuccess + ) + case .neutral: + return BadgeColors( + backgroundColor: colors.feedback.neutral, + borderColor: surfaceColor, + foregroundColor: colors.feedback.onNeutral + ) + case .danger: + return BadgeColors( + backgroundColor: colors.feedback.error, + borderColor: surfaceColor, + foregroundColor: colors.feedback.onError + ) + case .alert: + return BadgeColors( + backgroundColor: colors.feedback.alert, + borderColor: surfaceColor, + foregroundColor: colors.feedback.onAlert + ) + case .info: + return BadgeColors( + backgroundColor: colors.feedback.info, + borderColor: surfaceColor, + foregroundColor: colors.feedback.onInfo + ) + } + } +} diff --git a/core/Sources/Components/Badge/View/BadgeView.swift b/core/Sources/Components/Badge/View/BadgeView.swift new file mode 100644 index 000000000..f73f511b0 --- /dev/null +++ b/core/Sources/Components/Badge/View/BadgeView.swift @@ -0,0 +1,29 @@ +// +// BadgeView.swift +// SparkCore +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import SwiftUI + +public struct BadgeView: View { + @Binding public var viewModel: BadgeViewModel + + public var body: some View { + GeometryReader { proxy in + Text(viewModel.badgeText) + .padding(.init(vertical: viewModel.verticalOffset / 2.0, horizontal: viewModel.horizontalOffset / 2.0)) + .font(viewModel.textFont.font) + .foregroundColor(viewModel.badgeStyle.badgeColors.foregroundColor.color) + .background(viewModel.badgeStyle.badgeColors.backgroundColor.color) + .border(width: viewModel.borderWidth, radius: proxy.size.height / 2.0, colorToken: viewModel.borderColor) + } + .offset(x: viewModel.borderWidth / 2.0) + } + + public init(viewModel: Binding) { + self._viewModel = viewModel + } +} diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift new file mode 100644 index 000000000..5d2829e7e --- /dev/null +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -0,0 +1,48 @@ +// +// BadgeComponentView.swift +// Spark +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import SparkCore +import Spark +import SwiftUI + +private struct MyFormatter: BadgeFormatting { + func badgeText(value: String?) -> String { + guard let value else { + return "_" + } + return "My Formatter \(value)" + } +} + +struct BadgeComponentView: View { + @State private var viewModel = BadgeViewModel( + formatter: .custom(formatter: MyFormatter()), + theme: SparkTheme.shared, + badgeStyle: BadgeStyle( + badgeSize: .small, + badgeType: .danger, + isBadgeOutlined: true, + theme: SparkTheme.shared + ), + value: "23" + ) + + var body: some View { + VStack { + UIBadgeView() + BadgeView(viewModel: $viewModel) + .background(Color.blue) + } + } +} + +struct BadgeComponentView_Previews: PreviewProvider { + static var previews: some View { + BadgeComponentView() + } +} diff --git a/spark/Demo/Classes/View/Components/ComponentsView.swift b/spark/Demo/Classes/View/Components/ComponentsView.swift index dc34d58f1..5f9fe2063 100644 --- a/spark/Demo/Classes/View/Components/ComponentsView.swift +++ b/spark/Demo/Classes/View/Components/ComponentsView.swift @@ -13,7 +13,7 @@ struct ComponentsView: View { NavigationView { List { NavigationLink("Badge") { - Text("TODO") + BadgeComponentView() } NavigationLink("Button") { From a1b2608e56ceff3cf181bede215fd0d1e59cf23d Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Mon, 15 May 2023 08:05:46 +0200 Subject: [PATCH 03/31] Updated formatter and badge view --- .../Badge/Formatter/BadgeFormatter.swift | 28 ---- .../Badge/Model/BadgeIntentType.swift | 10 +- .../Private}/BadgeColors.swift | 0 .../Private}/BadgeIntentColors.swift | 0 .../Badge/Properties/Public/BadgeBorder.swift | 19 +++ .../Badge/Properties/Public/BadgeFormat.swift | 41 ++++++ .../Badge/Properties/Public/BadgeSize.swift | 14 ++ .../GetColors/GetBadgeColorsUseCase.swift | 31 ----- ...wift => BadgeGetIntentColorsUseCase.swift} | 39 +++--- .../Components/Badge/View/BadgeUIView.swift | 12 +- .../Components/Badge/View/BadgeView.swift | 32 +++-- .../Badge/ViewModel/BadgeViewModel.swift | 115 +++++++--------- .../Components/Badge/BadgeComponentView.swift | 125 +++++++++++++++--- .../Components/Badge/Bage+UIPresentable.swift | 7 +- 14 files changed, 289 insertions(+), 184 deletions(-) delete mode 100644 core/Sources/Components/Badge/Formatter/BadgeFormatter.swift rename core/Sources/Components/Badge/{Model => Properties/Private}/BadgeColors.swift (100%) rename core/Sources/Components/Badge/{Model => Properties/Private}/BadgeIntentColors.swift (100%) create mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift create mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift create mode 100644 core/Sources/Components/Badge/Properties/Public/BadgeSize.swift delete mode 100644 core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift rename core/Sources/Components/Badge/UseCase/GetIntentColors/{GetBadgeIntentColorsUseCase.swift => BadgeGetIntentColorsUseCase.swift} (97%) diff --git a/core/Sources/Components/Badge/Formatter/BadgeFormatter.swift b/core/Sources/Components/Badge/Formatter/BadgeFormatter.swift deleted file mode 100644 index 1e09bda09..000000000 --- a/core/Sources/Components/Badge/Formatter/BadgeFormatter.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// BadgeFormatter.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -public protocol BadgeFormatting { - func badgeText(value: String?) -> String -} - -/// With this formatter you can define behaviour of Badge label. -/// -/// Use **standart** for regular counting behavior with numbers. -/// If there would be more than 99 notifications -- it will show 99+ -/// -/// **Thousands** counter allows you to use counting for bigger numbers: 35k -> 35000 -/// -/// You can defune your custom behavior by using **custom** type. But in that case -/// Fromatter should be implemented and conform to **BadgeFormatting** protocol -public enum BadgeFormatter { - case standart - case thousandsCounter - case custom(formatter: BadgeFormatting) -} diff --git a/core/Sources/Components/Badge/Model/BadgeIntentType.swift b/core/Sources/Components/Badge/Model/BadgeIntentType.swift index 6566c2abe..440953ece 100644 --- a/core/Sources/Components/Badge/Model/BadgeIntentType.swift +++ b/core/Sources/Components/Badge/Model/BadgeIntentType.swift @@ -9,12 +9,12 @@ import Foundation // sourcery: AutoMockable -public enum BadgeIntentType { +public enum BadgeIntentType: CaseIterable { + case alert + case danger + case info + case neutral case primary case secondary case success - case neutral - case danger - case alert - case info } diff --git a/core/Sources/Components/Badge/Model/BadgeColors.swift b/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift similarity index 100% rename from core/Sources/Components/Badge/Model/BadgeColors.swift rename to core/Sources/Components/Badge/Properties/Private/BadgeColors.swift diff --git a/core/Sources/Components/Badge/Model/BadgeIntentColors.swift b/core/Sources/Components/Badge/Properties/Private/BadgeIntentColors.swift similarity index 100% rename from core/Sources/Components/Badge/Model/BadgeIntentColors.swift rename to core/Sources/Components/Badge/Properties/Private/BadgeIntentColors.swift diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift new file mode 100644 index 000000000..833b72d05 --- /dev/null +++ b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift @@ -0,0 +1,19 @@ +// +// BadgeBorder.swift +// SparkDemo +// +// Created by alex.vecherov on 17.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +public struct BadgeBorder { + var width: CGFloat + let radius: CGFloat + let color: ColorToken + + mutating func setWidth(_ width: CGFloat) { + self.width = width + } +} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift new file mode 100644 index 000000000..06bbf0623 --- /dev/null +++ b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift @@ -0,0 +1,41 @@ +// +// BadgeFormat.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +public protocol BadgeFormatting { + func formatText(for value: Int?) -> String +} + +public enum BadgeFormat { + + // MARK: - Properties + + case standart + case overflowCounter(maxValue: Int) + case custom(formatter: BadgeFormatting) + + // MARK: - Getting text + + func badgeText(_ value: Int?) -> String { + switch self { + case .standart: + guard let value else { + return "" + } + return "\(value)" + case .overflowCounter(let maxValue): + guard let value else { + return "" + } + return value > maxValue ? "\(maxValue)+" : "\(value)" + case .custom(let formatter): + return formatter.formatText(for: value) + } + } +} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift new file mode 100644 index 000000000..c0490e0e0 --- /dev/null +++ b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift @@ -0,0 +1,14 @@ +// +// BadgeSize.swift +// SparkDemo +// +// Created by alex.vecherov on 17.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +public enum BadgeSize { + case normal + case small +} diff --git a/core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift b/core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift deleted file mode 100644 index 531fd697f..000000000 --- a/core/Sources/Components/Badge/UseCase/GetColors/GetBadgeColorsUseCase.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// GetBadgeColorsUseCase.swift -// Spark -// -// Created by alex.vecherov on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol BadgeGetColorsUseCaseable { - func execute(from theme: Theme, - badgeType: BadgeIntentType) -> BadgeColorables -} - -struct BadgeGetColorsUseCase: BadgeGetColorsUseCaseable { - private let getIntentColorsUseCase: BadgeGetIntentColorsUseCaseable - - // MARK: - Initialization - - init(getIntentColorsUseCase: BadgeGetIntentColorsUseCaseable = BadgeGetIntentColorsUseCase()) { - self.getIntentColorsUseCase = getIntentColorsUseCase - } - - // MARK: - Getting colors - - func execute(from theme: Theme, badgeType: BadgeIntentType) -> BadgeColorables { - getIntentColorsUseCase.execute(intentType: badgeType, on: theme.colors) - } -} diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift similarity index 97% rename from core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift rename to core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift index afd3e257c..e6f322dfa 100644 --- a/core/Sources/Components/Badge/UseCase/GetIntentColors/GetBadgeIntentColorsUseCase.swift +++ b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift @@ -1,5 +1,5 @@ // -// BadgeIntentColors.swift +// BadgeGetIntentColorsUseCase.swift // Spark // // Created by alex.vecherov on 10.05.23. @@ -23,23 +23,23 @@ class BadgeGetIntentColorsUseCase: BadgeGetIntentColorsUseCaseable { let surfaceColor = colors.base.surface switch intentType { - case .primary: + case .alert: return BadgeColors( - backgroundColor: colors.primary.primary, + backgroundColor: colors.feedback.alert, borderColor: surfaceColor, - foregroundColor: colors.primary.onPrimary + foregroundColor: colors.feedback.onAlert ) - case .secondary: + case .danger: return BadgeColors( - backgroundColor: colors.secondary.secondary, + backgroundColor: colors.feedback.error, borderColor: surfaceColor, - foregroundColor: colors.secondary.onSecondary + foregroundColor: colors.feedback.onError ) - case .success: + case .info: return BadgeColors( - backgroundColor: colors.feedback.success, + backgroundColor: colors.feedback.info, borderColor: surfaceColor, - foregroundColor: colors.feedback.onSuccess + foregroundColor: colors.feedback.onInfo ) case .neutral: return BadgeColors( @@ -47,24 +47,25 @@ class BadgeGetIntentColorsUseCase: BadgeGetIntentColorsUseCaseable { borderColor: surfaceColor, foregroundColor: colors.feedback.onNeutral ) - case .danger: + case .primary: return BadgeColors( - backgroundColor: colors.feedback.error, + backgroundColor: colors.primary.primary, borderColor: surfaceColor, - foregroundColor: colors.feedback.onError + foregroundColor: colors.primary.onPrimary ) - case .alert: + case .secondary: return BadgeColors( - backgroundColor: colors.feedback.alert, + backgroundColor: colors.secondary.secondary, borderColor: surfaceColor, - foregroundColor: colors.feedback.onAlert + foregroundColor: colors.secondary.onSecondary ) - case .info: + case .success: return BadgeColors( - backgroundColor: colors.feedback.info, + backgroundColor: colors.feedback.success, borderColor: surfaceColor, - foregroundColor: colors.feedback.onInfo + foregroundColor: colors.feedback.onSuccess ) + } } } diff --git a/core/Sources/Components/Badge/View/BadgeUIView.swift b/core/Sources/Components/Badge/View/BadgeUIView.swift index 592b8458e..98cf99da6 100644 --- a/core/Sources/Components/Badge/View/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/BadgeUIView.swift @@ -28,17 +28,17 @@ public class BadgeUIView: UILabel { } private func setupBadgeText() { - text = viewModel.badgeText - textColor = viewModel.textColor + text = viewModel.text + textColor = viewModel.textColor.uiColor font = viewModel.textFont.uiFont textAlignment = .center } private func setupAppearance() { - backgroundColor = viewModel.backgroundColor + backgroundColor = viewModel.backgroundColor.uiColor translatesAutoresizingMaskIntoConstraints = false - layer.borderWidth = viewModel.borderWidth - layer.borderColor = viewModel.borderColor.uiColor.cgColor + layer.borderWidth = viewModel.badgeBorder.width + layer.borderColor = viewModel.badgeBorder.color.uiColor.cgColor clipsToBounds = true } @@ -47,7 +47,7 @@ public class BadgeUIView: UILabel { let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) let attributes = [NSAttributedString.Key.font: font] let estimatedSize = NSString(string: text ?? "").boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil) - if viewModel.badgeText.isEmpty { + if viewModel.text.isEmpty { return viewModel.emptySize } else { return CGSize(width: ceil(estimatedSize.width + viewModel.horizontalOffset), height: ceil(estimatedSize.height + viewModel.verticalOffset)) diff --git a/core/Sources/Components/Badge/View/BadgeView.swift b/core/Sources/Components/Badge/View/BadgeView.swift index f73f511b0..273977f52 100644 --- a/core/Sources/Components/Badge/View/BadgeView.swift +++ b/core/Sources/Components/Badge/View/BadgeView.swift @@ -9,21 +9,33 @@ import SwiftUI public struct BadgeView: View { - @Binding public var viewModel: BadgeViewModel + @ObservedObject public var viewModel: BadgeViewModel public var body: some View { - GeometryReader { proxy in - Text(viewModel.badgeText) - .padding(.init(vertical: viewModel.verticalOffset / 2.0, horizontal: viewModel.horizontalOffset / 2.0)) + if viewModel.text.isEmpty { + Circle() + .foregroundColor(viewModel.backgroundColor.color) + .border( + width: viewModel.badgeBorder.width, + radius: viewModel.badgeBorder.radius, + colorToken: viewModel.badgeBorder.color + ) + .frame(width: viewModel.emptySize.width, height: viewModel.emptySize.height) + } else { + Text(viewModel.text) .font(viewModel.textFont.font) - .foregroundColor(viewModel.badgeStyle.badgeColors.foregroundColor.color) - .background(viewModel.badgeStyle.badgeColors.backgroundColor.color) - .border(width: viewModel.borderWidth, radius: proxy.size.height / 2.0, colorToken: viewModel.borderColor) + .foregroundColor(viewModel.textColor.color) + .padding(.init(vertical: viewModel.verticalOffset / 2.0, horizontal: viewModel.horizontalOffset / 2.0)) + .background(viewModel.backgroundColor.color) + .border( + width: viewModel.badgeBorder.width, + radius: viewModel.badgeBorder.radius, + colorToken: viewModel.badgeBorder.color + ) } - .offset(x: viewModel.borderWidth / 2.0) } - public init(viewModel: Binding) { - self._viewModel = viewModel + public init(viewModel: BadgeViewModel) { + self.viewModel = viewModel } } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index fba508aae..dea5a95ad 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -7,79 +7,60 @@ // import UIKit +import Combine +import SwiftUI -public enum BadgeSize { - case normal - case small -} +public class BadgeViewModel: ObservableObject { + + @Published private var value: Int? = nil -public struct BadgeStyle { - let badgeSize: BadgeSize - let badgeType: BadgeIntentType - let badgeColors: BadgeColorables - let isBadgeOutlined: Bool - - public init(badgeSize: BadgeSize, badgeType: BadgeIntentType, isBadgeOutlined: Bool, theme: Theme) { - self.badgeSize = badgeSize - self.badgeType = badgeType - self.isBadgeOutlined = isBadgeOutlined - self.badgeColors = BadgeGetColorsUseCase().execute(from: theme, badgeType: badgeType) - } -} + @Published var text: String + @Published var textFont: TypographyFontToken + @Published var textColor: ColorToken -public class BadgeViewModel { - - private var formatter: BadgeFormatter - public var badgeText: String { - switch formatter { - case .standart: - return value ?? "" - case .thousandsCounter: - if let value, let intValue = Int(value) { - return "\(Int(intValue / 1000))k" - } else { - return value ?? "" - } - case .custom(let formatter): - return formatter.badgeText(value: value) - } - } - private var value: String? - private(set) var badgeStyle: BadgeStyle - private var theme: Theme - public var backgroundColor: UIColor { - badgeStyle.badgeColors.backgroundColor.uiColor - } - public var borderColor: ColorToken { - badgeStyle.isBadgeOutlined ? badgeStyle.badgeColors.borderColor : ColorTokenDefault.clear - } - public var textColor: UIColor { - badgeStyle.badgeColors.foregroundColor.uiColor - } - public var textFont: TypographyFontToken { - switch badgeStyle.badgeSize { - case .small: - return theme.typography.smallHighlight - case .normal: - return theme.typography.captionHighlight - } - } - public var verticalOffset: CGFloat { - theme.layout.spacing.small * 2 - } - public var horizontalOffset: CGFloat { - theme.layout.spacing.medium * 2 - } + @Published var backgroundColor: ColorToken + + @Published var verticalOffset: CGFloat + @Published var horizontalOffset: CGFloat + + @Published var badgeBorder: BadgeBorder - public var borderWidth: CGFloat { - badgeStyle.isBadgeOutlined ? 2 : 0 + @Published var theme: Theme + @Published private(set) var badgeFormat: BadgeFormat + + let emptySize: CGSize = .init(width: 12, height: 12) + + // MARK: - Initializer + + public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, initValue: Int? = nil, format: BadgeFormat = .standart, isOutlined: Bool = true) { + let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeType, on: theme.colors) + + value = initValue + text = format.badgeText(initValue) + textFont = badgeSize == .normal ? theme.typography.captionHighlight : theme.typography.smallHighlight + textColor = badgeColors.foregroundColor + + backgroundColor = badgeColors.backgroundColor + + verticalOffset = theme.layout.spacing.small * 2 + horizontalOffset = theme.layout.spacing.medium * 2 + + badgeBorder = BadgeBorder( + width: isOutlined ? + theme.border.width.medium : + theme.border.width.none, + radius: theme.border.radius.full, + color: badgeColors.borderColor + ) + + self.theme = theme + badgeFormat = .standart } - public let emptySize: CGSize = .init(width: 12, height: 12) - public init(formatter: BadgeFormatter, theme: Theme, badgeStyle: BadgeStyle, value: String?) { - self.formatter = formatter + // MARK: - Update configuration function + + public func setBadgeValue(_ value: Int?) { self.value = value - self.badgeStyle = badgeStyle - self.theme = theme + self.text = badgeFormat.badgeText(value) } } diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index 5d2829e7e..1a47e044e 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -10,34 +10,127 @@ import SparkCore import Spark import SwiftUI -private struct MyFormatter: BadgeFormatting { - func badgeText(value: String?) -> String { +private struct BadgePreviewFormatter: BadgeFormatting { + func formatText(for value: Int?) -> String { guard let value else { return "_" } - return "My Formatter \(value)" + return "Test \(value)" } } struct BadgeComponentView: View { - @State private var viewModel = BadgeViewModel( - formatter: .custom(formatter: MyFormatter()), + + @StateObject var standartBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .normal, + initValue: 6 + ) + + @State var smallCustomWithoutBorder = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .small, + initValue: 22, + format: .overflowCounter(maxValue: 20) + ) + + @State var standartDangerBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger, + initValue: 10, + format: .custom( + formatter: BadgePreviewFormatter() + ) + ) + + @State var standartInfoBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .info + ) + + @State var standartNeutralBadge = BadgeViewModel( theme: SparkTheme.shared, - badgeStyle: BadgeStyle( - badgeSize: .small, - badgeType: .danger, - isBadgeOutlined: true, - theme: SparkTheme.shared - ), - value: "23" + badgeType: .neutral, + isOutlined: false ) + @State var standartPrimaryBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .primary + ) + + @State var standartSecondaryBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .secondary + ) + + @State var standartSuccessBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .success + ) + + @State var value: Int? = 3 + @State var isOutlined: Bool = false + var body: some View { - VStack { - UIBadgeView() - BadgeView(viewModel: $viewModel) - .background(Color.blue) + ScrollView { + Button("Change value") { + standartBadge.setBadgeValue(23) + smallCustomWithoutBorder.setBadgeValue(18) + } + VStack(spacing: 100) { + HStack(spacing: 50) { + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartBadge) + .offset(x: 25, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: smallCustomWithoutBorder) + .offset(x: 25, y: -15) + } + } + + HStack(spacing: 50) { + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartDangerBadge) + .offset(x: 25, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartInfoBadge) + .offset(x: 25, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartNeutralBadge) + .offset(x: 25, y: -15) + } + } + + HStack(spacing: 50) { + HStack { + Text("Text") + BadgeView(viewModel: standartPrimaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSecondaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSuccessBadge) + } + } + } + .offset(y: 30) + .frame(minWidth: 375) } + .background(Color.gray) } } diff --git a/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift index 0dd87769f..409241bbc 100644 --- a/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift +++ b/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift @@ -15,9 +15,12 @@ struct UIBadgeView: UIViewRepresentable { func makeUIView(context: Context) -> some UIView { let uiview = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 300, height: 50))) uiview.backgroundColor = .cyan - let badgeStyle = BadgeStyle(badgeSize: .small, badgeType: .danger, isBadgeOutlined: false, theme: SparkTheme.shared) let badgeView = BadgeUIView( - viewModel: .init(formatter: .thousandsCounter, theme: SparkTheme.shared, badgeStyle: badgeStyle, value: "423522342")) + viewModel: BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger + ) + ) uiview.addSubview(badgeView) NSLayoutConstraint.activate([ badgeView.trailingAnchor.constraint(equalTo: uiview.trailingAnchor, constant: 0), From d9c1b66d3b5a411d5c4e3d708110ad89ea110515 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Mon, 15 May 2023 12:09:44 +0200 Subject: [PATCH 04/31] Added missing docs --- .../Badge/Properties/Public/BadgeBorder.swift | 6 +++++ .../Badge/Properties/Public/BadgeFormat.swift | 15 +++++++++++ .../Public}/BadgeIntentType.swift | 1 + .../Badge/Properties/Public/BadgeSize.swift | 5 ++++ .../Components/Badge/View/BadgeUIView.swift | 1 + .../Components/Badge/View/BadgeView.swift | 26 +++++++++++++++++++ .../Badge/ViewModel/BadgeViewModel.swift | 22 ++++++++++++++++ 7 files changed, 76 insertions(+) rename core/Sources/Components/Badge/{Model => Properties/Public}/BadgeIntentType.swift (86%) diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift index 833b72d05..317283480 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift @@ -8,6 +8,12 @@ import Foundation +/// Structure that is used for configuring border of ``BadgeView`` +/// +/// List of properties: +/// - width +/// - radius +/// - color returned as ColorToken public struct BadgeBorder { var width: CGFloat let radius: CGFloat diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift index 06bbf0623..8ec60f263 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift @@ -8,10 +8,25 @@ import Foundation +/// Protocol that defines custom behaviour of ``BadgeView`` text +/// Create your own implementation of badge text by using it public protocol BadgeFormatting { func formatText(for value: Int?) -> String } +/// With this formatter you can define behaviour of Badge label. +/// +/// Use **standart** for regular counting behavior with numbers. +/// +/// Use **overflowCounter(maxValue)** +/// If badge **value** would be greater than passed **maxValue** into formatter +/// then badge will show **maxValue+** +/// +/// You can define your custom behavior by using **custom** type. But in that case +/// Fromatter should be implemented and conform to **BadgeFormatting** protocol +/// For example you can define thouthand counter to show 96k instead of 96000 +/// +/// To get text you need to call **badgeText(value) -> String** function public enum BadgeFormat { // MARK: - Properties diff --git a/core/Sources/Components/Badge/Model/BadgeIntentType.swift b/core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift similarity index 86% rename from core/Sources/Components/Badge/Model/BadgeIntentType.swift rename to core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift index 440953ece..7d5469a37 100644 --- a/core/Sources/Components/Badge/Model/BadgeIntentType.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeIntentType.swift @@ -9,6 +9,7 @@ import Foundation // sourcery: AutoMockable +/// **BadgeIntentType** defines color of ``BadgeView`` public enum BadgeIntentType: CaseIterable { case alert case danger diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift index c0490e0e0..6f98af8cc 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift @@ -8,6 +8,11 @@ import Foundation +/// Enum that sets ``BadgeView`` size +/// +/// There are two possible sizes: +/// - normal +/// - small public enum BadgeSize { case normal case small diff --git a/core/Sources/Components/Badge/View/BadgeUIView.swift b/core/Sources/Components/Badge/View/BadgeUIView.swift index 98cf99da6..2e5ef5a0d 100644 --- a/core/Sources/Components/Badge/View/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/BadgeUIView.swift @@ -8,6 +8,7 @@ import UIKit +/// This is the UIKit version for the ``BadgeView`` public class BadgeUIView: UILabel { private var viewModel: BadgeViewModel diff --git a/core/Sources/Components/Badge/View/BadgeView.swift b/core/Sources/Components/Badge/View/BadgeView.swift index 273977f52..be9711ce3 100644 --- a/core/Sources/Components/Badge/View/BadgeView.swift +++ b/core/Sources/Components/Badge/View/BadgeView.swift @@ -8,6 +8,32 @@ import SwiftUI +/// This is SwiftUI badge view to show notifications count +/// +/// Badge view is created by pasing: +/// - **Theme** +/// - ``BadgeViewModel`` +/// +/// **Example** +/// This example shows how to create view with horizontal alignment of Badge +/// ```swift +/// @StateObject var viewModel = BadgeViewModel( +/// theme: SparkTheme.shared, +/// badgeType: .alert, +/// badgeSize: .normal, +/// initValue: 0 +/// ) +/// @State var value: Int? = 3 +/// var body: any View { +/// Button("Change Notifications Number") { +/// viewModel.setBadgeValue(5) +/// } +/// HStack { +/// Text("Some text") +/// BadgeView(viewModel) +/// } +/// } +/// ``` public struct BadgeView: View { @ObservedObject public var viewModel: BadgeViewModel diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index dea5a95ad..5d28e310f 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -10,6 +10,28 @@ import UIKit import Combine import SwiftUI +/// **BadgeViewModel** is a view model that required for +/// configuring ``BadgeView`` and changing it's properties. +/// +/// **Initializer** +/// - A theme -- app theme +/// - Badge type -- intent type of Badge see ``BadgeIntentType`` +/// - Badge size -- see ``BadgeSize`` +/// - Initial value -- Value that should be set on view creation +/// - Formatter -- see ``BadgeFormat`` +/// - isOutlined -- property to show or hide border +/// +/// List of properties: +/// - value -- property that represents **Int** displayed in ``BadgeView`` +/// - text -- is property that represents text in ``BadgeView``. Appearance of it +/// is configured via ``BadgeFormat`` and based on **value** property. +/// - textColor -- property for coloring text +/// - backgroundColor -- changes color of ``BadgeView`` and based on ``BadgeIntentType`` +/// - verticalOffset & horizontalOffset -- are offsets of **text** inside of ``BadgeView`` +/// - badgeBorder -- is property that helps you to configure ``BadgeView`` with +/// border radius, width and color. See ``BadgeBorder`` +/// - theme is representer of **Theme** used in the app +/// - badgeFormat -- see ``BadgeFormat`` as a formater of **text** public class BadgeViewModel: ObservableObject { @Published private var value: Int? = nil From acece920fe5a9fadd2a0c72eaa32e4728075f37b Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Mon, 22 May 2023 08:10:32 +0200 Subject: [PATCH 05/31] Added badge tests --- .../BadgeGetIntentColorsUseCaseTests.swift | 136 ++++++++++++++++++ .../Badge/View/BadgeViewTests.swift | 129 +++++++++++++++++ .../Badge/ViewModel/BadgeViewModelTests.swift | 93 ++++++++++++ .../BorderGeneratedMock+ExtenstionTests.swift | 23 +++ ...pographyGeneratedMock+ExtensionTests.swift | 4 + 5 files changed, 385 insertions(+) create mode 100644 core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift create mode 100644 core/Sources/Components/Badge/View/BadgeViewTests.swift create mode 100644 core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift create mode 100644 core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtenstionTests.swift diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift new file mode 100644 index 000000000..071f5fffc --- /dev/null +++ b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift @@ -0,0 +1,136 @@ +// +// BadgeGetColorsUseCaseTests.swift +// SparkDemo +// +// Created by alex.vecherov on 15.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import XCTest +import SwiftUI +@testable import SparkCore + +final class BadgeGetColorsUseCaseTests: XCTestCase { + + // MARK: - Tests + + func test_execute_for_all_variant_cases() throws { + // Given + + let mockedExpectedColors = ColorsGeneratedMock.mocked() + let mockedExpectedSurfaceColor = mockedExpectedColors.base.surface + + let items: [BadgeGetColors] = [ + .init( + givenIntent: .alert, + expectedBackgroundToken: mockedExpectedColors.feedback.alert, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.feedback.onAlert + ), + .init( + givenIntent: .danger, + expectedBackgroundToken: mockedExpectedColors.feedback.error, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.feedback.onError + ), + .init( + givenIntent: .info, + expectedBackgroundToken: mockedExpectedColors.feedback.info, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.feedback.onInfo + ), + .init( + givenIntent: .neutral, + expectedBackgroundToken: mockedExpectedColors.feedback.neutral, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.feedback.onNeutral + ), + .init( + givenIntent: .primary, + expectedBackgroundToken: mockedExpectedColors.primary.primary, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.primary.onPrimary + ), + .init( + givenIntent: .secondary, + expectedBackgroundToken: mockedExpectedColors.secondary.secondary, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.secondary.onSecondary + ), + .init( + givenIntent: .success, + expectedBackgroundToken: mockedExpectedColors.feedback.success, + expectedBorderToken: mockedExpectedSurfaceColor, + expectedTextToken: mockedExpectedColors.feedback.onSuccess + ) + ] + + for item in items { + let useCase = BadgeGetIntentColorsUseCase() + + // When + let colors = useCase.execute(intentType: item.givenIntent, on: mockedExpectedColors) + + try Tester.testColorsProperties( + givenColors: colors, + getColors: item + ) + } + } +} + +private struct Tester { + + static func testColorsProperties( + givenColors: BadgeColorables, + getColors: BadgeGetColors + ) throws { + // Background Color + try self.testColor( + givenColorProperty: givenColors.backgroundColor, + givenPropertyName: "backgroundColor", + givenIntent: getColors.givenIntent, + expectedColorToken: getColors.expectedBackgroundToken + ) + + // Border Color + try self.testColor( + givenColorProperty: givenColors.borderColor, + givenPropertyName: "borderColor", + givenIntent: getColors.givenIntent, + expectedColorToken: getColors.expectedBorderToken + ) + + // Foreground Color + try self.testColor( + givenColorProperty: givenColors.foregroundColor, + givenPropertyName: "foregroundColor", + givenIntent: getColors.givenIntent, + expectedColorToken: getColors.expectedTextToken + ) + } + + private static func testColor( + givenColorProperty: ColorToken?, + givenPropertyName: String, + givenIntent: BadgeIntentType, + expectedColorToken: ColorToken? + ) throws { + let errorPrefixMessage = "\(givenPropertyName) for .\(givenIntent) case" + + if let givenColorProperty { + let color = try XCTUnwrap(givenColorProperty as? ColorTokenGeneratedMock, "Wrong " + errorPrefixMessage) + XCTAssertIdentical(color, expectedColorToken as? ColorTokenGeneratedMock, "Wrong value " + errorPrefixMessage) + } else { + XCTAssertNil(givenColorProperty, "Should be nil" + errorPrefixMessage) + } + } +} + +private struct BadgeGetColors { + let givenIntent: BadgeIntentType + + let expectedBackgroundToken: ColorToken + let expectedBorderToken: ColorToken + let expectedTextToken: ColorToken +} diff --git a/core/Sources/Components/Badge/View/BadgeViewTests.swift b/core/Sources/Components/Badge/View/BadgeViewTests.swift new file mode 100644 index 000000000..72b788ea6 --- /dev/null +++ b/core/Sources/Components/Badge/View/BadgeViewTests.swift @@ -0,0 +1,129 @@ +// +// BadgeViewTests.swift +// SparkCoreTests +// +// Created by alex.vecherov on 22.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import XCTest +import SnapshotTesting +import SwiftUI + +@testable import SparkCore +@testable import Spark + +private struct TestBadgeFormatting: BadgeFormatting { + func formatText(for value: Int?) -> String { + guard let value else { + return "No Value" + } + return "Test Value \(value)" + } +} + +final class BadgeViewTests: SwiftUIComponentTestCase { + + var theme: Theme! + + override func setUpWithError() throws { + try super.setUpWithError() + + theme = SparkTheme() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + + theme = nil + } + + func test_badge_all_cases_no_text() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType) + ).fixedSize() + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_\(badgeIntentType)") + } + } + + func test_badge_all_cases_text() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + initValue: 23 + ) + ).fixedSize() + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)") + } + } + + func test_badge_all_cases_text_smal_size() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + badgeSize: .small, + initValue: 23 + ) + ).fixedSize() + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)_small_size") + } + } + + func test_badge_all_cases_text_overflow_format() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + initValue: 23, + format: .overflowCounter(maxValue: 20) + ) + ).fixedSize() + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_overflow_format_text_\(badgeIntentType)") + } + } + + func test_badge_all_cases_text_custom_format() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + initValue: 23, + format: .custom( + formatter: TestBadgeFormatting() + ) + ) + ).fixedSize() + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_custom_format_text_\(badgeIntentType)") + } + } + + func test_badge_all_cases_no_text_custom_format() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + format: .custom( + formatter: TestBadgeFormatting() + ) + ) + ).fixedSize() + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_custom_format_no_text_\(badgeIntentType)") + } + } +} diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift new file mode 100644 index 000000000..6fca947c2 --- /dev/null +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -0,0 +1,93 @@ +// +// BadgeViewModelTests.swift +// SparkCore +// +// Created by alex.vecherov on 17.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Combine +@testable import SparkCore +import SwiftUI +import XCTest + +final class BadgeViewModelTests: XCTestCase { + + var theme: ThemeGeneratedMock! + + // MARK: - Setup + override func setUpWithError() throws { + try super.setUpWithError() + + self.theme = ThemeGeneratedMock.mock + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + + self.theme = nil + } + + // MARK: - Tests + func test_init() throws { + for badgeIntentType in BadgeIntentType.allCases { + // Given + + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType) + + let badgeExpectedColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeIntentType, on: theme.colors) + + // Then + + XCTAssertIdentical(viewModel.textColor as? ColorTokenGeneratedMock, badgeExpectedColors.foregroundColor as? ColorTokenGeneratedMock, "Text color doesn't match expected foreground") + + XCTAssertIdentical(viewModel.theme as? ThemeGeneratedMock, theme, "Badge theme doesn't match expected theme") + + XCTAssertTrue(viewModel.badgeBorder.isEqual(to: theme, isOutlined: true), "Border border doesn't match expected") + } + } + + func test_set_value() throws { + for badgeIntentType in BadgeIntentType.allCases { + // Given + + let expectedInitText = "20" + let expectedUpdatedText = "233" + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + + // Then + + XCTAssertEqual(expectedInitText, viewModel.text, "Text doesn't match init value with standart format") + + viewModel.setBadgeValue(233) + + XCTAssertEqual(expectedUpdatedText, viewModel.text, "Text doesn't match incremented value with standart format") + } + } +} + +private extension BadgeBorder { + func isEqual(to theme: Theme, isOutlined: Bool) -> Bool { + return (isOutlined ? width == theme.border.width.medium : width == theme.border.width.none) && + radius == theme.border.radius.full && + color.color == theme.colors.base.surface.color + } +} + +private extension Theme where Self == ThemeGeneratedMock { + static var mock: Self { + let theme = ThemeGeneratedMock() + let colors = ColorsGeneratedMock.mocked() + + colors.base = ColorsBaseGeneratedMock.mocked() + colors.primary = ColorsPrimaryGeneratedMock.mocked() + colors.feedback = ColorsFeedbackGeneratedMock.mocked() + theme.colors = colors + theme.dims = DimsGeneratedMock.mocked() + theme.typography = TypographyGeneratedMock.mocked() + theme.layout = LayoutGeneratedMock.mocked() + theme.border = BorderGeneratedMock.mocked() + + return theme + } +} diff --git a/core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtenstionTests.swift b/core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtenstionTests.swift new file mode 100644 index 000000000..74794c8d5 --- /dev/null +++ b/core/Sources/Theming/Content/Border/BorderGeneratedMock+ExtenstionTests.swift @@ -0,0 +1,23 @@ +// +// BorderGeneratedMock+ExtenstionTests.swift +// SparkCore +// +// Created by alex.vecherov on 20.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +extension BorderGeneratedMock { + + // MARK: - Methods + + static func mocked() -> BorderGeneratedMock { + let borderGeneratedMock = BorderGeneratedMock() + + borderGeneratedMock.width = BorderWidthGeneratedMock() + borderGeneratedMock.radius = BorderRadiusGeneratedMock() + + return borderGeneratedMock + } +} diff --git a/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift index 29a6d18d1..a59410101 100644 --- a/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift +++ b/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift @@ -21,8 +21,12 @@ extension TypographyGeneratedMock { let caption = TypographyFontTokenGeneratedMock() caption.font = .caption + let captionHighlight = TypographyFontTokenGeneratedMock() + captionHighlight.font = .caption.bold() + typography.body1 = body1 typography.caption = caption + typography.captionHighlight = captionHighlight return typography } From ef7b711647393c8ba573f38a94a3ed7d3d794c2c Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 08:15:05 +0200 Subject: [PATCH 06/31] Added scaled spacing and badge uikit preview --- core/Demo/Classes/BadgeUIView_Previewes.swift | 126 +++++++++++++++ core/Demo/Classes/BadgeView_Previews.swift | 143 ++++++++++++++++++ .../BadgeAccessibilityIdentifier.swift} | 4 +- .../Badge/Constants/BadgeConstants.swift | 13 ++ .../Badge/Properties/Public/BadgeFormat.swift | 14 +- .../Components/Badge/View/BadgeUIView.swift | 69 --------- .../Badge/View/{ => SwiftUI}/BadgeView.swift | 31 +++- .../View/{ => SwiftUI}/BadgeViewTests.swift | 0 .../Badge/View/UIKit/BadgeUIView.swift | 132 ++++++++++++++++ .../Badge/View/UIKit/BadgeUIViewTests.swift | 42 +++++ .../Badge/ViewModel/BadgeViewModel.swift | 24 +-- .../Badge/Badge+UIPresentable.swift | 112 ++++++++++++++ .../Components/Badge/BadgeComponentView.swift | 2 + .../Components/Badge/Bage+UIPresentable.swift | 36 ----- 14 files changed, 615 insertions(+), 133 deletions(-) create mode 100644 core/Demo/Classes/BadgeUIView_Previewes.swift create mode 100644 core/Demo/Classes/BadgeView_Previews.swift rename core/Sources/Components/Badge/{AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift => AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift} (72%) create mode 100644 core/Sources/Components/Badge/Constants/BadgeConstants.swift delete mode 100644 core/Sources/Components/Badge/View/BadgeUIView.swift rename core/Sources/Components/Badge/View/{ => SwiftUI}/BadgeView.swift (62%) rename core/Sources/Components/Badge/View/{ => SwiftUI}/BadgeViewTests.swift (100%) create mode 100644 core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift create mode 100644 core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift create mode 100644 spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift delete mode 100644 spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift diff --git a/core/Demo/Classes/BadgeUIView_Previewes.swift b/core/Demo/Classes/BadgeUIView_Previewes.swift new file mode 100644 index 000000000..2d4edac59 --- /dev/null +++ b/core/Demo/Classes/BadgeUIView_Previewes.swift @@ -0,0 +1,126 @@ +// +// BadgeUIView_Previewes.swift +// SparkCoreDemo +// +// Created by alex.vecherov on 22.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import SwiftUI +import SparkCore + +private struct BadgePreviewFormatter: BadgeFormatting { + func formatText(for value: Int?) -> String { + guard let value else { + return "_" + } + return "\(value)€" + } +} + +struct UIBadgeView: UIViewRepresentable { + + private var viewModels: [BadgeViewModel] = + [ + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .normal, + initValue: 6 + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .small, + initValue: 22, + format: .overflowCounter(maxValue: 20) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger, + initValue: 10, + format: .custom( + formatter: BadgePreviewFormatter() + ) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .info + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .neutral, + isOutlined: false + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .primary + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .secondary + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .success + ) + ] + + func makeUIView(context: Context) -> some UIView { + let badgeViews = viewModels.enumerated().map { index, viewModel in + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + let badgeView = BadgeUIView(viewModel: viewModel) + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "badge_\(index)" + containerView.addSubview(label) + containerView.addSubview(badgeView) + containerView.backgroundColor = .blue + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor), + label.topAnchor.constraint(equalTo: containerView.topAnchor), + label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + if index >= 3 && index <= 6 { + NSLayoutConstraint.activate([ + badgeView.centerXAnchor.constraint(equalTo: label.trailingAnchor), + badgeView.centerYAnchor.constraint(equalTo: label.topAnchor) + ]) + } else { + NSLayoutConstraint.activate([ + badgeView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 5), + badgeView.centerYAnchor.constraint(equalTo: label.centerYAnchor, constant: 0) + ]) + } + + return containerView + } + let badgesStackView = UIStackView(arrangedSubviews: badgeViews) + badgesStackView.axis = .vertical + badgesStackView.alignment = .leading + badgesStackView.spacing = 30 + badgesStackView.distribution = .fill + return badgesStackView + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } +} + +struct BadgeUIView_Previews: PreviewProvider { + + struct BadgeUIViewBridge: View { + var body: some View { + UIBadgeView() + .frame(height: 400) + } + } + + static var previews: some View { + BadgeUIViewBridge() + .background(Color.gray.opacity(0.4)) + } +} diff --git a/core/Demo/Classes/BadgeView_Previews.swift b/core/Demo/Classes/BadgeView_Previews.swift new file mode 100644 index 000000000..961500dc1 --- /dev/null +++ b/core/Demo/Classes/BadgeView_Previews.swift @@ -0,0 +1,143 @@ +// +// BadgeView_Previews.swift +// SparkCoreDemo +// +// Created by alex.vecherov on 22.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import SwiftUI +import SparkCore + +private struct BadgePreviewFormatter: BadgeFormatting { + func formatText(for value: Int?) -> String { + guard let value else { + return "_" + } + return "\(value)€" + } +} + +struct BadgeView_Previews: PreviewProvider { + + struct BadgeContainerView: View { + + @StateObject var standartBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .normal, + initValue: 6 + ) + + @State var smallCustomWithoutBorder = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .small, + initValue: 22, + format: .overflowCounter(maxValue: 20) + ) + + @State var standartDangerBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger, + initValue: 10, + format: .custom( + formatter: BadgePreviewFormatter() + ) + ) + + @State var standartInfoBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .info + ) + + @State var standartNeutralBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .neutral, + isOutlined: false + ) + + @State var standartPrimaryBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .primary + ) + + @State var standartSecondaryBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .secondary + ) + + @State var standartSuccessBadge = BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .success + ) + + @State var value: Int? = 3 + @State var isOutlined: Bool = false + @ScaledMetric var hOffset: CGFloat + @ScaledMetric var vOffset: CGFloat + + var body: some View { + ScrollView { + Button("Change value") { + standartBadge.setBadgeValue(23) + smallCustomWithoutBorder.setBadgeValue(18) + } + VStack(spacing: 100) { + HStack(spacing: 100) { + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartBadge) + .offset(x: hOffset, y: -vOffset) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: smallCustomWithoutBorder) + .offset(x: hOffset, y: -vOffset) + } + } + + HStack(spacing: 100) { + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartDangerBadge) + .offset(x: hOffset, y: -vOffset) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartInfoBadge) + .offset(x: hOffset, y: -vOffset) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartNeutralBadge) + .offset(x: hOffset, y: -vOffset) + } + } + + HStack(spacing: 100) { + HStack { + Text("Text") + BadgeView(viewModel: standartPrimaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSecondaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSuccessBadge) + } + } + } + .offset(y: 30) + .frame(minWidth: 375) + } + .background(Color.gray) + } + } + + static var previews: some View { + BadgeContainerView(hOffset: SparkTheme.shared.layout.spacing.xxLarge, vOffset: SparkTheme.shared.layout.spacing.medium * 1.5) + } +} diff --git a/core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift b/core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift similarity index 72% rename from core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift rename to core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift index a637c4a10..674395518 100644 --- a/core/Sources/Components/Badge/AccessabilityIdentifier/BadgeAccessabilityIdentifier.swift +++ b/core/Sources/Components/Badge/AccessibilityIdentifier/BadgeAccessibilityIdentifier.swift @@ -1,5 +1,5 @@ // -// BadgeAccessabilityIdentifier.swift +// BadgeAccessibilityIdentifier.swift // Spark // // Created by alex.vecherov on 04.05.23. @@ -9,7 +9,7 @@ import Foundation -public enum BadgeAccessabilityIdentifier { +public enum BadgeAccessibilityIdentifier { // MARK: - Properties diff --git a/core/Sources/Components/Badge/Constants/BadgeConstants.swift b/core/Sources/Components/Badge/Constants/BadgeConstants.swift new file mode 100644 index 000000000..be758348b --- /dev/null +++ b/core/Sources/Components/Badge/Constants/BadgeConstants.swift @@ -0,0 +1,13 @@ +// +// BadgeConstants.swift +// SparkCoreDemo +// +// Created by alex.vecherov on 22.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation + +enum BadgeConstants { + static let emptySize = CGSize(width: 12, height: 12) +} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift index 8ec60f263..eff162da7 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift @@ -16,7 +16,7 @@ public protocol BadgeFormatting { /// With this formatter you can define behaviour of Badge label. /// -/// Use **standart** for regular counting behavior with numbers. +/// Use **default** for regular counting behavior with numbers. /// /// Use **overflowCounter(maxValue)** /// If badge **value** would be greater than passed **maxValue** into formatter @@ -31,7 +31,7 @@ public enum BadgeFormat { // MARK: - Properties - case standart + case `default` case overflowCounter(maxValue: Int) case custom(formatter: BadgeFormatting) @@ -39,11 +39,6 @@ public enum BadgeFormat { func badgeText(_ value: Int?) -> String { switch self { - case .standart: - guard let value else { - return "" - } - return "\(value)" case .overflowCounter(let maxValue): guard let value else { return "" @@ -51,6 +46,11 @@ public enum BadgeFormat { return value > maxValue ? "\(maxValue)+" : "\(value)" case .custom(let formatter): return formatter.formatText(for: value) + default: + guard let value else { + return "" + } + return "\(value)" } } } diff --git a/core/Sources/Components/Badge/View/BadgeUIView.swift b/core/Sources/Components/Badge/View/BadgeUIView.swift deleted file mode 100644 index 2e5ef5a0d..000000000 --- a/core/Sources/Components/Badge/View/BadgeUIView.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// BadgeUIView.swift -// Spark -// -// Created by alex.vecherov on 04.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import UIKit - -/// This is the UIKit version for the ``BadgeView`` -public class BadgeUIView: UILabel { - - private var viewModel: BadgeViewModel - - public init(viewModel: BadgeViewModel) { - self.viewModel = viewModel - super.init(frame: .zero) - setupBadge() - } - - required init?(coder: NSCoder) { - fatalError("Not implemented") - } - - private func setupBadge() { - setupBadgeText() - setupAppearance() - } - - private func setupBadgeText() { - text = viewModel.text - textColor = viewModel.textColor.uiColor - font = viewModel.textFont.uiFont - textAlignment = .center - } - - private func setupAppearance() { - backgroundColor = viewModel.backgroundColor.uiColor - translatesAutoresizingMaskIntoConstraints = false - layer.borderWidth = viewModel.badgeBorder.width - layer.borderColor = viewModel.badgeBorder.color.uiColor.cgColor - clipsToBounds = true - } - - public override var intrinsicContentSize: CGSize { - let size = CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude) - let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) - let attributes = [NSAttributedString.Key.font: font] - let estimatedSize = NSString(string: text ?? "").boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil) - if viewModel.text.isEmpty { - return viewModel.emptySize - } else { - return CGSize(width: ceil(estimatedSize.width + viewModel.horizontalOffset), height: ceil(estimatedSize.height + viewModel.verticalOffset)) - } - } - - public override func layoutSubviews() { - super.layoutSubviews() - preferredMaxLayoutWidth = frame.size.width - layer.cornerRadius = frame.size.height / 2.0 - } - - public override func didMoveToSuperview() { - super.didMoveToSuperview() - numberOfLines = 0 - lineBreakMode = .byWordWrapping - } -} diff --git a/core/Sources/Components/Badge/View/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift similarity index 62% rename from core/Sources/Components/Badge/View/BadgeView.swift rename to core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index be9711ce3..0f6e69a30 100644 --- a/core/Sources/Components/Badge/View/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -36,32 +36,47 @@ import SwiftUI /// ``` public struct BadgeView: View { @ObservedObject public var viewModel: BadgeViewModel + @ScaledMetric private var smallOffset: CGFloat + @ScaledMetric private var mediumOffset: CGFloat + @ScaledMetric private var emptySize: CGFloat public var body: some View { if viewModel.text.isEmpty { Circle() .foregroundColor(viewModel.backgroundColor.color) + .frame(width: self.emptySize, height: self.emptySize) .border( width: viewModel.badgeBorder.width, radius: viewModel.badgeBorder.radius, colorToken: viewModel.badgeBorder.color ) - .frame(width: viewModel.emptySize.width, height: viewModel.emptySize.height) + .fixedSize() } else { Text(viewModel.text) .font(viewModel.textFont.font) .foregroundColor(viewModel.textColor.color) - .padding(.init(vertical: viewModel.verticalOffset / 2.0, horizontal: viewModel.horizontalOffset / 2.0)) - .background(viewModel.backgroundColor.color) - .border( - width: viewModel.badgeBorder.width, - radius: viewModel.badgeBorder.radius, - colorToken: viewModel.badgeBorder.color - ) + .padding(.init(vertical: self.smallOffset, horizontal: self.mediumOffset)) + .background(viewModel.backgroundColor.color) + .border( + width: viewModel.badgeBorder.width, + radius: viewModel.badgeBorder.radius, + colorToken: viewModel.badgeBorder.color + ) + .fixedSize() } } public init(viewModel: BadgeViewModel) { self.viewModel = viewModel + + self._smallOffset = + .init(wrappedValue: + viewModel.verticalOffset / 2 + ) + self._mediumOffset = + .init(wrappedValue: + viewModel.horizontalOffset / 2 + ) + self._emptySize = .init(wrappedValue: BadgeConstants.emptySize.width) } } diff --git a/core/Sources/Components/Badge/View/BadgeViewTests.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift similarity index 100% rename from core/Sources/Components/Badge/View/BadgeViewTests.swift rename to core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift new file mode 100644 index 000000000..a70813d65 --- /dev/null +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -0,0 +1,132 @@ +// +// BadgeUIView.swift +// Spark +// +// Created by alex.vecherov on 04.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import UIKit + +/// This is the UIKit version for the ``BadgeView`` +public class BadgeUIView: UILabel { + + private var viewModel: BadgeViewModel + + @ScaledUIMetric private var horizontalSpacing: CGFloat = 0 + @ScaledUIMetric private var verticalSpacing: CGFloat = 0 + @ScaledUIMetric private var emptyBadgeSize: CGFloat = 0 + + private var heightConstraint: NSLayoutConstraint? + private var widthConstraint: NSLayoutConstraint? + + public init(viewModel: BadgeViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + self.setupBadge() + } + + required init?(coder: NSCoder) { + fatalError("Not implemented") + } + + private func setupBadge() { + setupBadgeText() + setupAppearance() + setupLayouts() + } + + private func setupBadgeText() { + self.text = self.viewModel.text + self.textColor = self.viewModel.textColor.uiColor + self.font = self.viewModel.textFont.uiFont + self.adjustsFontForContentSizeCategory = true + self.textAlignment = .center + } + + private func setupAppearance() { + self.backgroundColor = self.viewModel.backgroundColor.uiColor + self.translatesAutoresizingMaskIntoConstraints = false + self.layer.borderWidth = self.viewModel.badgeBorder.width + self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor + self.layer.cornerRadius = self.viewModel.badgeBorder.radius + self.clipsToBounds = true + } + + private func setupLayouts() { + var estimatedSize = self.estimatedSize(for: self.viewModel.text) + reloadUISize() + if viewModel.text.isEmpty { + estimatedSize = .init(width: self.emptyBadgeSize, height: self.emptyBadgeSize) + } else { + estimatedSize = .init(width: ceil(estimatedSize.width + self.horizontalSpacing), height: ceil(estimatedSize.height + self.verticalSpacing)) + } + + setupSizeConstraints(with: estimatedSize) + + self.layer.cornerRadius = estimatedSize.height / 2.0 + } + + private func estimatedSize(for text: String) -> CGSize { + if self.viewModel.text.isEmpty { + return BadgeConstants.emptySize + } else { + let size = CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude) + let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) + let attributes = [NSAttributedString.Key.font: font] + return NSString(string: text).boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil).size + } + } + + private func setupSizeConstraints(with size: CGSize) { + if let heightConstraint { + heightConstraint.constant = size.height + } else { + self.heightConstraint = self.heightAnchor.constraint(equalToConstant: size.height) + self.heightConstraint?.isActive = true + } + if let widthConstraint { + widthConstraint.constant = size.width + } else { + self.widthConstraint = self.widthAnchor.constraint(equalToConstant: size.width) + self.widthConstraint?.isActive = true + } + } + + private func reloadUISize() { + if self.viewModel.text.isEmpty { + self.emptyBadgeSize = BadgeConstants.emptySize.width + self._emptyBadgeSize.update(traitCollection: self.traitCollection) + } else { + self.horizontalSpacing = self.viewModel.horizontalOffset + self._horizontalSpacing.update(traitCollection: self.traitCollection) + self.verticalSpacing = self.viewModel.verticalOffset + self._verticalSpacing.update(traitCollection: self.traitCollection) + } + } + + private func reloadUIFromColors() { + self.backgroundColor = self.viewModel.backgroundColor.uiColor + self.textColor = self.viewModel.textColor.uiColor + self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor + } + + public override func didMoveToSuperview() { + super.didMoveToSuperview() + self.numberOfLines = 0 + self.lineBreakMode = .byWordWrapping + } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + self.reloadUIFromColors() + } + + self.setupBadgeText() + self.reloadUISize() + self.setupLayouts() + self.layoutIfNeeded() + } +} diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift new file mode 100644 index 000000000..1bc65dde1 --- /dev/null +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift @@ -0,0 +1,42 @@ +// +// BadgeUIViewTests.swift +// SparkCoreTests +// +// Created by alex.vecherov on 22.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import XCTest + +final class BadgeUIViewTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 5d28e310f..9f210b43c 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -50,24 +50,26 @@ public class BadgeViewModel: ObservableObject { @Published var theme: Theme @Published private(set) var badgeFormat: BadgeFormat - let emptySize: CGSize = .init(width: 12, height: 12) // MARK: - Initializer - public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, initValue: Int? = nil, format: BadgeFormat = .standart, isOutlined: Bool = true) { + public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, initValue: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeType, on: theme.colors) - value = initValue - text = format.badgeText(initValue) - textFont = badgeSize == .normal ? theme.typography.captionHighlight : theme.typography.smallHighlight - textColor = badgeColors.foregroundColor + self.value = initValue + self.text = format.badgeText(initValue) + self.textFont = badgeSize == .normal ? theme.typography.captionHighlight : theme.typography.smallHighlight + self.textColor = badgeColors.foregroundColor - backgroundColor = badgeColors.backgroundColor + self.backgroundColor = badgeColors.backgroundColor - verticalOffset = theme.layout.spacing.small * 2 - horizontalOffset = theme.layout.spacing.medium * 2 + let verticalOffset = theme.layout.spacing.small * 2 + let horizontalOffset = theme.layout.spacing.medium * 2 - badgeBorder = BadgeBorder( + self._verticalOffset = .init(wrappedValue: verticalOffset) + self._horizontalOffset = .init(wrappedValue: horizontalOffset) + + self.badgeBorder = BadgeBorder( width: isOutlined ? theme.border.width.medium : theme.border.width.none, @@ -76,7 +78,7 @@ public class BadgeViewModel: ObservableObject { ) self.theme = theme - badgeFormat = .standart + self.badgeFormat = .default } // MARK: - Update configuration function diff --git a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift new file mode 100644 index 000000000..8021c1cf7 --- /dev/null +++ b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift @@ -0,0 +1,112 @@ +// +// Bage+UIPresentable.swift +// Spark +// +// Created by alex.vecherov on 10.05.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import SwiftUI +import SparkCore +import Spark + +private struct BadgePreviewFormatter: BadgeFormatting { + func formatText(for value: Int?) -> String { + guard let value else { + return "_" + } + return "Test \(value)" + } +} + +struct UIBadgeView: UIViewRepresentable { + + private var viewModels: [BadgeViewModel] = + [ + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .normal, + initValue: 6 + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .small, + initValue: 22, + format: .overflowCounter(maxValue: 20) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger, + initValue: 10, + format: .custom( + formatter: BadgePreviewFormatter() + ) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .info + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .neutral, + isOutlined: false + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .primary + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .secondary + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .success + ) + ] + + func makeUIView(context: Context) -> some UIView { + let badgeViews = viewModels.enumerated().map { index, viewModel in + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + let badgeView = BadgeUIView(viewModel: viewModel) + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "badge_\(index)" + containerView.addSubview(label) + containerView.addSubview(badgeView) + containerView.backgroundColor = .blue + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor), + label.topAnchor.constraint(equalTo: containerView.topAnchor), + label.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + if index >= 3 && index <= 6 { + NSLayoutConstraint.activate([ + badgeView.centerXAnchor.constraint(equalTo: label.trailingAnchor), + badgeView.centerYAnchor.constraint(equalTo: label.topAnchor) + ]) + } else { + NSLayoutConstraint.activate([ + badgeView.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 5), + badgeView.centerYAnchor.constraint(equalTo: label.centerYAnchor, constant: 0) + ]) + } + + return containerView + } + let badgesStackView = UIStackView(arrangedSubviews: badgeViews) + badgesStackView.axis = .vertical + badgesStackView.alignment = .leading + badgesStackView.spacing = 30 + return badgesStackView + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + + } +} + diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index 1a47e044e..74ac5e550 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -80,6 +80,8 @@ struct BadgeComponentView: View { standartBadge.setBadgeValue(23) smallCustomWithoutBorder.setBadgeValue(18) } + UIBadgeView() + .frame(height: 400) VStack(spacing: 100) { HStack(spacing: 50) { ZStack(alignment: .leading) { diff --git a/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift deleted file mode 100644 index 409241bbc..000000000 --- a/spark/Demo/Classes/View/Components/Badge/Bage+UIPresentable.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Bage+UIPresentable.swift -// Spark -// -// Created by alex.vecherov on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import SwiftUI -import SparkCore -import Spark - -struct UIBadgeView: UIViewRepresentable { - - func makeUIView(context: Context) -> some UIView { - let uiview = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 300, height: 50))) - uiview.backgroundColor = .cyan - let badgeView = BadgeUIView( - viewModel: BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .danger - ) - ) - uiview.addSubview(badgeView) - NSLayoutConstraint.activate([ - badgeView.trailingAnchor.constraint(equalTo: uiview.trailingAnchor, constant: 0), - badgeView.centerYAnchor.constraint(equalTo: uiview.centerYAnchor, constant: 0) - ]) - return uiview - } - - func updateUIView(_ uiView: UIViewType, context: Context) { - - } -} - From 5cf182c3fa937d235e42c6b6e22aceee79d5fbd7 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 08:23:38 +0200 Subject: [PATCH 07/31] Fixed UI preview --- ...UIView_Previewes.swift => BadgeUIView_Previews.swift} | 9 +++++---- .../View/Components/Badge/Badge+UIPresentable.swift | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) rename core/Demo/Classes/{BadgeUIView_Previewes.swift => BadgeUIView_Previews.swift} (95%) diff --git a/core/Demo/Classes/BadgeUIView_Previewes.swift b/core/Demo/Classes/BadgeUIView_Previews.swift similarity index 95% rename from core/Demo/Classes/BadgeUIView_Previewes.swift rename to core/Demo/Classes/BadgeUIView_Previews.swift index 2d4edac59..bd44d6fdc 100644 --- a/core/Demo/Classes/BadgeUIView_Previewes.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -1,5 +1,5 @@ // -// BadgeUIView_Previewes.swift +// BadgeUIView_Previews.swift // SparkCoreDemo // // Created by alex.vecherov on 22.05.23. @@ -58,7 +58,8 @@ struct UIBadgeView: UIViewRepresentable { ), BadgeViewModel( theme: SparkTheme.shared, - badgeType: .secondary + badgeType: .secondary, + initValue: 23 ), BadgeViewModel( theme: SparkTheme.shared, @@ -85,8 +86,8 @@ struct UIBadgeView: UIViewRepresentable { ]) if index >= 3 && index <= 6 { NSLayoutConstraint.activate([ - badgeView.centerXAnchor.constraint(equalTo: label.trailingAnchor), - badgeView.centerYAnchor.constraint(equalTo: label.topAnchor) + badgeView.centerXAnchor.constraint(equalTo: label.trailingAnchor, constant: 5), + badgeView.centerYAnchor.constraint(equalTo: label.topAnchor, constant: -5) ]) } else { NSLayoutConstraint.activate([ diff --git a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift index 8021c1cf7..84ae6d02b 100644 --- a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift +++ b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift @@ -59,7 +59,8 @@ struct UIBadgeView: UIViewRepresentable { ), BadgeViewModel( theme: SparkTheme.shared, - badgeType: .secondary + badgeType: .secondary, + initValue: 6 ), BadgeViewModel( theme: SparkTheme.shared, @@ -86,8 +87,8 @@ struct UIBadgeView: UIViewRepresentable { ]) if index >= 3 && index <= 6 { NSLayoutConstraint.activate([ - badgeView.centerXAnchor.constraint(equalTo: label.trailingAnchor), - badgeView.centerYAnchor.constraint(equalTo: label.topAnchor) + badgeView.centerXAnchor.constraint(equalTo: label.trailingAnchor, constant: 5), + badgeView.centerYAnchor.constraint(equalTo: label.topAnchor, constant: -5) ]) } else { NSLayoutConstraint.activate([ From 7fb46b01e4960a609a904de8908242fd5c443a80 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 08:24:32 +0200 Subject: [PATCH 08/31] Fixed RadioButtonUIGroup_Previews redeclaration --- .../Demo/Classes/View/Components/Chip/ChipComponentUIView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spark/Demo/Classes/View/Components/Chip/ChipComponentUIView.swift b/spark/Demo/Classes/View/Components/Chip/ChipComponentUIView.swift index 6e40111c7..4e18171c5 100644 --- a/spark/Demo/Classes/View/Components/Chip/ChipComponentUIView.swift +++ b/spark/Demo/Classes/View/Components/Chip/ChipComponentUIView.swift @@ -213,7 +213,7 @@ final class ChipComponentUIViewController: UIViewController { } } -struct RadioButtonUIGroup_Previews: PreviewProvider { +struct ChipComponentUI_Previews: PreviewProvider { static var previews: some View { ChipComponentUIView() } From 090725ccfd566c5fb5f0a8d2c355ab87c1ec9e98 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 08:29:23 +0200 Subject: [PATCH 09/31] Added UIKit component snapshot tests --- .../Badge/View/UIKit/BadgeUIViewTests.swift | 122 +++++++++++++++--- 1 file changed, 104 insertions(+), 18 deletions(-) diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift index 1bc65dde1..cf07514d0 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift @@ -7,36 +7,122 @@ // import XCTest +import SnapshotTesting -final class BadgeUIViewTests: XCTestCase { +@testable import SparkCore +@testable import Spark - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. +private struct TestBadgeFormatting: BadgeFormatting { + func formatText(for value: Int?) -> String { + guard let value else { + return "No Value" + } + return "Test Value \(value)" + } +} - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false +final class BadgeUIViewTests: UIKitComponentTestCase { + + var theme: Theme! + + override func setUpWithError() throws { + try super.setUpWithError() - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + theme = SparkTheme() } override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + try super.tearDownWithError() + + theme = nil + } + + func test_badge_all_cases_no_text() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeUIView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType) + ) + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_\(badgeIntentType)") + } + } + + func test_badge_all_cases_text() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeUIView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + initValue: 23 + ) + ) + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)") + } } - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() + func test_badge_all_cases_text_smal_size() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeUIView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + badgeSize: .small, + initValue: 23 + ) + ) - // Use XCTAssert and related functions to verify your tests produce the correct results. + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)_small_size") + } } - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } + func test_badge_all_cases_text_overflow_format() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeUIView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + initValue: 23, + format: .overflowCounter(maxValue: 20) + ) + ) + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_overflow_format_text_\(badgeIntentType)") + } + } + + func test_badge_all_cases_text_custom_format() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeUIView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + initValue: 23, + format: .custom( + formatter: TestBadgeFormatting() + ) + ) + ) + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_custom_format_text_\(badgeIntentType)") + } + } + + func test_badge_all_cases_no_text_custom_format() throws { + for badgeIntentType in BadgeIntentType.allCases { + let view = BadgeUIView( + viewModel: BadgeViewModel( + theme: theme, + badgeType: badgeIntentType, + format: .custom( + formatter: TestBadgeFormatting() + ) + ) + ) + + assertSnapshotInDarkAndLight(matching: view, named: "test_badge_custom_format_no_text_\(badgeIntentType)") } } } From 59fdd7ddf0e265c9a72a95b9aa527a1e8f36a311 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 16:48:36 +0200 Subject: [PATCH 10/31] Updated Badge UIView to subclass from UIView --- core/Demo/Classes/BadgeUIView_Previews.swift | 1 - .../Badge/View/UIKit/BadgeUIView.swift | 155 ++++++++++++------ .../Badge/ViewModel/BadgeViewModel.swift | 4 +- 3 files changed, 104 insertions(+), 56 deletions(-) diff --git a/core/Demo/Classes/BadgeUIView_Previews.swift b/core/Demo/Classes/BadgeUIView_Previews.swift index bd44d6fdc..857608436 100644 --- a/core/Demo/Classes/BadgeUIView_Previews.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -25,7 +25,6 @@ struct UIBadgeView: UIViewRepresentable { BadgeViewModel( theme: SparkTheme.shared, badgeType: .alert, - badgeSize: .normal, initValue: 6 ), BadgeViewModel( diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index a70813d65..b87aa96dd 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -9,20 +9,46 @@ import UIKit /// This is the UIKit version for the ``BadgeView`` -public class BadgeUIView: UILabel { +public class BadgeUIView: UIView { private var viewModel: BadgeViewModel + /// Constraints for badge size in empty state + /// In this case badge is shown like a circle + private var emptyHeightConstraint: NSLayoutConstraint? + private var emptyWidthConstraint: NSLayoutConstraint? + + /// Dynamicaly sized properties for badge + /// ``emptyBadgeSize`` represents size of the circle in empty state of Badge + /// + /// ``horizontalSpacing`` and ``verticalSpacing`` are properties + /// that used for space between badge background and text + @ScaledUIMetric private var emptyBadgeSize: CGFloat = 0 @ScaledUIMetric private var horizontalSpacing: CGFloat = 0 @ScaledUIMetric private var verticalSpacing: CGFloat = 0 - @ScaledUIMetric private var emptyBadgeSize: CGFloat = 0 - private var heightConstraint: NSLayoutConstraint? - private var widthConstraint: NSLayoutConstraint? + private var badgeLabel: UILabel = UILabel() + + /// Constraints for badge in non-empty state. + /// All of them are set to the badge text label + /// After that Badge view size is based on the size of the text + private var badgeTopConstraint: NSLayoutConstraint? + private var badgeLeadingConstraint: NSLayoutConstraint? + private var badgeTrailingConstraint: NSLayoutConstraint? + private var badgeBottomConstraint: NSLayoutConstraint? + private var badgeWidthConstraint: NSLayoutConstraint? + private var badgeHeightConstraint: NSLayoutConstraint? + private var badgeConstraints: [NSLayoutConstraint?] { + [badgeTopConstraint, badgeLeadingConstraint, badgeTrailingConstraint, badgeBottomConstraint, badgeWidthConstraint, badgeHeightConstraint] + } + + // MARK: - Init public init(viewModel: BadgeViewModel) { self.viewModel = viewModel + super.init(frame: .zero) + self.setupBadge() } @@ -30,103 +56,126 @@ public class BadgeUIView: UILabel { fatalError("Not implemented") } + // MARK: - Badge configuration + private func setupBadge() { setupBadgeText() setupAppearance() + setupSizing() setupLayouts() } private func setupBadgeText() { - self.text = self.viewModel.text - self.textColor = self.viewModel.textColor.uiColor - self.font = self.viewModel.textFont.uiFont - self.adjustsFontForContentSizeCategory = true - self.textAlignment = .center + self.addSubview(badgeLabel) + self.badgeLabel.adjustsFontForContentSizeCategory = true + self.badgeLabel.textAlignment = .center + self.badgeLabel.text = self.viewModel.text + self.badgeLabel.textColor = self.viewModel.textColor.uiColor + self.badgeLabel.font = self.viewModel.textFont.uiFont + self.badgeLabel.translatesAutoresizingMaskIntoConstraints = false } private func setupAppearance() { - self.backgroundColor = self.viewModel.backgroundColor.uiColor self.translatesAutoresizingMaskIntoConstraints = false + self.backgroundColor = self.viewModel.backgroundColor.uiColor self.layer.borderWidth = self.viewModel.badgeBorder.width self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor - self.layer.cornerRadius = self.viewModel.badgeBorder.radius self.clipsToBounds = true } - private func setupLayouts() { - var estimatedSize = self.estimatedSize(for: self.viewModel.text) - reloadUISize() - if viewModel.text.isEmpty { - estimatedSize = .init(width: self.emptyBadgeSize, height: self.emptyBadgeSize) + private func setupSizing() { + if self.viewModel.text.isEmpty { + self.emptyBadgeSize = BadgeConstants.emptySize.width } else { - estimatedSize = .init(width: ceil(estimatedSize.width + self.horizontalSpacing), height: ceil(estimatedSize.height + self.verticalSpacing)) + self.horizontalSpacing = self.viewModel.horizontalOffset + self.verticalSpacing = self.viewModel.verticalOffset } - - setupSizeConstraints(with: estimatedSize) - - self.layer.cornerRadius = estimatedSize.height / 2.0 } - private func estimatedSize(for text: String) -> CGSize { + // MARK: - Layouts setup + + private func setupLayouts() { if self.viewModel.text.isEmpty { - return BadgeConstants.emptySize + self.setupEmptySizeConstraints() } else { - let size = CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude) - let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin) - let attributes = [NSAttributedString.Key.font: font] - return NSString(string: text).boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil).size + let textSize = badgeLabel.intrinsicContentSize + self.setupBadgeConstraints(for: textSize) } } - private func setupSizeConstraints(with size: CGSize) { - if let heightConstraint { - heightConstraint.constant = size.height + private func setupEmptySizeConstraints() { + if let emptyHeightConstraint { + emptyHeightConstraint.constant = emptyBadgeSize } else { - self.heightConstraint = self.heightAnchor.constraint(equalToConstant: size.height) - self.heightConstraint?.isActive = true + self.emptyHeightConstraint = self.heightAnchor.constraint(equalToConstant: emptyBadgeSize) + self.emptyHeightConstraint?.isActive = true } - if let widthConstraint { - widthConstraint.constant = size.width + if let emptyWidthConstraint { + emptyWidthConstraint.constant = emptyBadgeSize } else { - self.widthConstraint = self.widthAnchor.constraint(equalToConstant: size.width) - self.widthConstraint?.isActive = true + self.emptyWidthConstraint = self.widthAnchor.constraint(equalToConstant: emptyBadgeSize) + self.emptyWidthConstraint?.isActive = true } } - private func reloadUISize() { - if self.viewModel.text.isEmpty { - self.emptyBadgeSize = BadgeConstants.emptySize.width - self._emptyBadgeSize.update(traitCollection: self.traitCollection) + private func setupBadgeConstraints(for textSize: CGSize) { + if let badgeTopConstraint, let badgeBottomConstraint, let badgeLeadingConstraint, let badgeTrailingConstraint, let badgeWidthConstraint, let badgeHeightConstraint { + badgeLeadingConstraint.constant = self.horizontalSpacing + badgeTrailingConstraint.constant = -self.horizontalSpacing + badgeTopConstraint.constant = self.verticalSpacing + badgeBottomConstraint.constant = -self.verticalSpacing + badgeWidthConstraint.constant = textSize.width + badgeHeightConstraint.constant = textSize.height } else { - self.horizontalSpacing = self.viewModel.horizontalOffset - self._horizontalSpacing.update(traitCollection: self.traitCollection) - self.verticalSpacing = self.viewModel.verticalOffset - self._verticalSpacing.update(traitCollection: self.traitCollection) + badgeLeadingConstraint = badgeLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: self.horizontalSpacing) + badgeTopConstraint = badgeLabel.topAnchor.constraint(equalTo: topAnchor, constant: self.verticalSpacing) + badgeTrailingConstraint = badgeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -self.horizontalSpacing) + badgeBottomConstraint = badgeLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -self.verticalSpacing) + badgeWidthConstraint = badgeLabel.widthAnchor.constraint(equalToConstant: textSize.width) + badgeHeightConstraint = badgeLabel.heightAnchor.constraint(equalToConstant: textSize.height) + NSLayoutConstraint.activate(badgeConstraints.compactMap({ $0 })) } } - private func reloadUIFromColors() { + public override func layoutSubviews() { + super.layoutSubviews() + + self.layer.cornerRadius = frame.height / 2.0 + } + + // MARK: - Updates on Trait Collection Change + + private func reloadColors() { self.backgroundColor = self.viewModel.backgroundColor.uiColor - self.textColor = self.viewModel.textColor.uiColor + badgeLabel.textColor = self.viewModel.textColor.uiColor self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor } - public override func didMoveToSuperview() { - super.didMoveToSuperview() - self.numberOfLines = 0 - self.lineBreakMode = .byWordWrapping + private func reloadBadgeFontIfNeeded() { + guard !self.viewModel.text.isEmpty else { + return + } + self.badgeLabel.font = self.viewModel.textFont.uiFont + } + + private func reloadUISize() { + if self.viewModel.text.isEmpty { + self._emptyBadgeSize.update(traitCollection: self.traitCollection) + } else { + self._horizontalSpacing.update(traitCollection: self.traitCollection) + self._verticalSpacing.update(traitCollection: self.traitCollection) + } } public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if self.traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { - self.reloadUIFromColors() + self.reloadColors() } - self.setupBadgeText() + self.reloadBadgeFontIfNeeded() self.reloadUISize() self.setupLayouts() - self.layoutIfNeeded() } } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 9f210b43c..fd5c5d840 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -63,8 +63,8 @@ public class BadgeViewModel: ObservableObject { self.backgroundColor = badgeColors.backgroundColor - let verticalOffset = theme.layout.spacing.small * 2 - let horizontalOffset = theme.layout.spacing.medium * 2 + let verticalOffset = theme.layout.spacing.small + let horizontalOffset = theme.layout.spacing.medium self._verticalOffset = .init(wrappedValue: verticalOffset) self._horizontalOffset = .init(wrappedValue: horizontalOffset) From 5f2d0f2703eb4606f5598b3c3bc19c812c264a26 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 16:48:55 +0200 Subject: [PATCH 11/31] Updated badge view model to remove unnecessary Publishers --- .../Badge/View/SwiftUI/BadgeView.swift | 4 +-- .../Badge/ViewModel/BadgeViewModel.swift | 27 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index 0f6e69a30..f2c275728 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -71,11 +71,11 @@ public struct BadgeView: View { self._smallOffset = .init(wrappedValue: - viewModel.verticalOffset / 2 + viewModel.verticalOffset ) self._mediumOffset = .init(wrappedValue: - viewModel.horizontalOffset / 2 + viewModel.horizontalOffset ) self._emptySize = .init(wrappedValue: BadgeConstants.emptySize.width) } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index fd5c5d840..8b8c08613 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -36,20 +36,19 @@ public class BadgeViewModel: ObservableObject { @Published private var value: Int? = nil - @Published var text: String - @Published var textFont: TypographyFontToken - @Published var textColor: ColorToken + // MARK: - Text Properties + var text: String + var textFont: TypographyFontToken + var textColor: ColorToken - @Published var backgroundColor: ColorToken - - @Published var verticalOffset: CGFloat - @Published var horizontalOffset: CGFloat - - @Published var badgeBorder: BadgeBorder - - @Published var theme: Theme - @Published private(set) var badgeFormat: BadgeFormat + // MARK: - Appearance Properties + private(set) var badgeFormat: BadgeFormat + var badgeBorder: BadgeBorder + var backgroundColor: ColorToken + var theme: Theme + var verticalOffset: CGFloat + var horizontalOffset: CGFloat // MARK: - Initializer @@ -66,8 +65,8 @@ public class BadgeViewModel: ObservableObject { let verticalOffset = theme.layout.spacing.small let horizontalOffset = theme.layout.spacing.medium - self._verticalOffset = .init(wrappedValue: verticalOffset) - self._horizontalOffset = .init(wrappedValue: horizontalOffset) + self.verticalOffset = verticalOffset + self.horizontalOffset = horizontalOffset self.badgeBorder = BadgeBorder( width: isOutlined ? From 60913c40553cfd49e04d4daf72183ca60188c0b6 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Tue, 23 May 2023 17:01:30 +0200 Subject: [PATCH 12/31] Added missing accessibility ids --- core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift | 1 + core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index f2c275728..e75fdf2f4 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -63,6 +63,7 @@ public struct BadgeView: View { colorToken: viewModel.badgeBorder.color ) .fixedSize() + .accessibilityIdentifier(BadgeAccessibilityIdentifier.text) } } diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index b87aa96dd..5a5fc3807 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -67,6 +67,7 @@ public class BadgeUIView: UIView { private func setupBadgeText() { self.addSubview(badgeLabel) + self.badgeLabel.accessibilityIdentifier = BadgeAccessibilityIdentifier.text self.badgeLabel.adjustsFontForContentSizeCategory = true self.badgeLabel.textAlignment = .center self.badgeLabel.text = self.viewModel.text From 6143b996c30ebba349c641abb63aba0f5f528b33 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 24 May 2023 14:31:15 +0200 Subject: [PATCH 13/31] Added scaling for border width --- .../Components/Badge/View/SwiftUI/BadgeView.swift | 6 ++++-- .../Components/Badge/View/UIKit/BadgeUIView.swift | 14 +++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index e75fdf2f4..8e34634b1 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -39,6 +39,7 @@ public struct BadgeView: View { @ScaledMetric private var smallOffset: CGFloat @ScaledMetric private var mediumOffset: CGFloat @ScaledMetric private var emptySize: CGFloat + @ScaledMetric private var borderWidth: CGFloat public var body: some View { if viewModel.text.isEmpty { @@ -46,7 +47,7 @@ public struct BadgeView: View { .foregroundColor(viewModel.backgroundColor.color) .frame(width: self.emptySize, height: self.emptySize) .border( - width: viewModel.badgeBorder.width, + width: borderWidth, radius: viewModel.badgeBorder.radius, colorToken: viewModel.badgeBorder.color ) @@ -58,7 +59,7 @@ public struct BadgeView: View { .padding(.init(vertical: self.smallOffset, horizontal: self.mediumOffset)) .background(viewModel.backgroundColor.color) .border( - width: viewModel.badgeBorder.width, + width: borderWidth, radius: viewModel.badgeBorder.radius, colorToken: viewModel.badgeBorder.color ) @@ -79,5 +80,6 @@ public struct BadgeView: View { viewModel.horizontalOffset ) self._emptySize = .init(wrappedValue: BadgeConstants.emptySize.width) + self._borderWidth = .init(wrappedValue: viewModel.badgeBorder.width) } } diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index 5a5fc3807..25821b490 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -26,6 +26,7 @@ public class BadgeUIView: UIView { @ScaledUIMetric private var emptyBadgeSize: CGFloat = 0 @ScaledUIMetric private var horizontalSpacing: CGFloat = 0 @ScaledUIMetric private var verticalSpacing: CGFloat = 0 + @ScaledUIMetric private var borderWidth: CGFloat = 0 private var badgeLabel: UILabel = UILabel() @@ -59,9 +60,9 @@ public class BadgeUIView: UIView { // MARK: - Badge configuration private func setupBadge() { + setupScalables() setupBadgeText() setupAppearance() - setupSizing() setupLayouts() } @@ -79,18 +80,19 @@ public class BadgeUIView: UIView { private func setupAppearance() { self.translatesAutoresizingMaskIntoConstraints = false self.backgroundColor = self.viewModel.backgroundColor.uiColor - self.layer.borderWidth = self.viewModel.badgeBorder.width + self.layer.borderWidth = self.borderWidth self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor self.clipsToBounds = true } - private func setupSizing() { + private func setupScalables() { if self.viewModel.text.isEmpty { self.emptyBadgeSize = BadgeConstants.emptySize.width } else { self.horizontalSpacing = self.viewModel.horizontalOffset self.verticalSpacing = self.viewModel.verticalOffset } + self.borderWidth = self.viewModel.badgeBorder.width } // MARK: - Layouts setup @@ -168,6 +170,11 @@ public class BadgeUIView: UIView { } } + private func reloadBorderWidth() { + self._borderWidth.update(traitCollection: self.traitCollection) + self.layer.borderWidth = self.borderWidth + } + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -177,6 +184,7 @@ public class BadgeUIView: UIView { self.reloadBadgeFontIfNeeded() self.reloadUISize() + self.reloadBorderWidth() self.setupLayouts() } } From 2f082902c613d29dea5147eda8acd041530c7644 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 31 May 2023 13:04:40 +0200 Subject: [PATCH 14/31] Fixed typos --- .../Badge/Properties/Public/BadgeFormat.swift | 27 ++++++++++--------- .../Badge/View/SwiftUI/BadgeView.swift | 2 +- .../Badge/ViewModel/BadgeViewModel.swift | 10 +++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift index eff162da7..461003900 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift @@ -15,28 +15,31 @@ public protocol BadgeFormatting { } /// With this formatter you can define behaviour of Badge label. -/// -/// Use **default** for regular counting behavior with numbers. -/// -/// Use **overflowCounter(maxValue)** -/// If badge **value** would be greater than passed **maxValue** into formatter -/// then badge will show **maxValue+** -/// -/// You can define your custom behavior by using **custom** type. But in that case -/// Fromatter should be implemented and conform to **BadgeFormatting** protocol -/// For example you can define thouthand counter to show 96k instead of 96000 -/// -/// To get text you need to call **badgeText(value) -> String** function +/// avalabled formats: +/// - ``default`` +/// - ``overflowCounter(maxValue:)`` +/// - ``custom(formatter:)`` public enum BadgeFormat { // MARK: - Properties + /// Use **default** for regular counting behavior with numbers. case `default` + + /// Use **overflowCounter(maxValue)** + /// If badge **value** would be greater than passed **maxValue** into formatter + /// then badge will show **maxValue+** case overflowCounter(maxValue: Int) + + /// You can define your custom behavior by using **custom** type. But in that case + /// Formatter should be implemented and conform to **BadgeFormatting** protocol + /// For example you can define thousand counter to show 96k instead of 96000 case custom(formatter: BadgeFormatting) // MARK: - Getting text + /// This function will return text value for your badge + /// wiht conformation to the selected **BadgeFormat** type func badgeText(_ value: Int?) -> String { switch self { case .overflowCounter(let maxValue): diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index 8e34634b1..851d8bd18 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -10,7 +10,7 @@ import SwiftUI /// This is SwiftUI badge view to show notifications count /// -/// Badge view is created by pasing: +/// Badge view is created by passing: /// - **Theme** /// - ``BadgeViewModel`` /// diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 8b8c08613..c18438268 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -10,7 +10,7 @@ import UIKit import Combine import SwiftUI -/// **BadgeViewModel** is a view model that required for +/// **BadgeViewModel** is a view model that is required for /// configuring ``BadgeView`` and changing it's properties. /// /// **Initializer** @@ -23,7 +23,7 @@ import SwiftUI /// /// List of properties: /// - value -- property that represents **Int** displayed in ``BadgeView`` -/// - text -- is property that represents text in ``BadgeView``. Appearance of it +/// - text -- property that represents text in ``BadgeView``. Appearance of it /// is configured via ``BadgeFormat`` and based on **value** property. /// - textColor -- property for coloring text /// - backgroundColor -- changes color of ``BadgeView`` and based on ``BadgeIntentType`` @@ -31,9 +31,9 @@ import SwiftUI /// - badgeBorder -- is property that helps you to configure ``BadgeView`` with /// border radius, width and color. See ``BadgeBorder`` /// - theme is representer of **Theme** used in the app -/// - badgeFormat -- see ``BadgeFormat`` as a formater of **text** -public class BadgeViewModel: ObservableObject { - +/// - badgeFormat -- see ``BadgeFormat`` as a formatter of **text** +public final class BadgeViewModel: ObservableObject { + @Published private var value: Int? = nil // MARK: - Text Properties From c249fb32a94a9c2cf4b012c7204280501c1f3924 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 31 May 2023 13:05:31 +0200 Subject: [PATCH 15/31] Removed unnecessary protocols, fixed tests --- core/Demo/Classes/BadgeView_Previews.swift | 9 +++-- .../Properties/Private/BadgeColors.swift | 8 +---- .../Private/BadgeIntentColors.swift | 25 -------------- .../Badge/Properties/Public/BadgeBorder.swift | 6 +++- .../BadgeGetIntentColorsUseCase.swift | 6 ++-- .../BadgeGetIntentColorsUseCaseTests.swift | 2 +- .../Badge/View/SwiftUI/BadgeViewTests.swift | 14 +------- .../Badge/View/UIKit/BadgeUIViewTests.swift | 14 +------- .../Badge/ViewModel/BadgeViewModelTests.swift | 33 +------------------ 9 files changed, 20 insertions(+), 97 deletions(-) delete mode 100644 core/Sources/Components/Badge/Properties/Private/BadgeIntentColors.swift diff --git a/core/Demo/Classes/BadgeView_Previews.swift b/core/Demo/Classes/BadgeView_Previews.swift index 961500dc1..0e5df613c 100644 --- a/core/Demo/Classes/BadgeView_Previews.swift +++ b/core/Demo/Classes/BadgeView_Previews.swift @@ -34,7 +34,8 @@ struct BadgeView_Previews: PreviewProvider { badgeType: .alert, badgeSize: .small, initValue: 22, - format: .overflowCounter(maxValue: 20) + format: .overflowCounter(maxValue: 20), + isOutlined: false ) @State var standartDangerBadge = BadgeViewModel( @@ -80,8 +81,12 @@ struct BadgeView_Previews: PreviewProvider { var body: some View { ScrollView { Button("Change value") { - standartBadge.setBadgeValue(23) smallCustomWithoutBorder.setBadgeValue(18) + smallCustomWithoutBorder.isBadgeOutlined = true + smallCustomWithoutBorder.badgeType = .primary + standartBadge.setBadgeValue(23) + standartDangerBadge.badgeType = .neutral + smallCustomWithoutBorder.badgeSize = .normal } VStack(spacing: 100) { HStack(spacing: 100) { diff --git a/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift b/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift index 1b788643c..5a3c90419 100644 --- a/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift +++ b/core/Sources/Components/Badge/Properties/Private/BadgeColors.swift @@ -9,13 +9,7 @@ import Foundation // sourcery: AutoMockable -protocol BadgeColorables { - var backgroundColor: ColorToken { get } - var borderColor: ColorToken { get } - var foregroundColor: ColorToken { get } -} - -struct BadgeColors: BadgeColorables { +struct BadgeColors { // MARK: - Properties diff --git a/core/Sources/Components/Badge/Properties/Private/BadgeIntentColors.swift b/core/Sources/Components/Badge/Properties/Private/BadgeIntentColors.swift deleted file mode 100644 index f5882478f..000000000 --- a/core/Sources/Components/Badge/Properties/Private/BadgeIntentColors.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// BadgeIntentColors.swift -// Spark -// -// Created by alex.vecherov on 10.05.23. -// Copyright © 2023 Adevinta. All rights reserved. -// - -import Foundation - -// sourcery: AutoMockable -protocol BadgeIntentColorables { - var color: ColorToken { get } - var onColor: ColorToken { get } - var surface: ColorToken { get } -} - -struct BadgeIntentColors: BadgeIntentColorables { - - // MARK: - Properties - - let color: ColorToken - let onColor: ColorToken - let surface: ColorToken -} diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift index 317283480..b622454a1 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeBorder.swift @@ -17,9 +17,13 @@ import Foundation public struct BadgeBorder { var width: CGFloat let radius: CGFloat - let color: ColorToken + var color: ColorToken mutating func setWidth(_ width: CGFloat) { self.width = width } + + mutating func setColor(_ color: ColorToken) { + self.color = color + } } diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift index e6f322dfa..d49b61896 100644 --- a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift +++ b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCase.swift @@ -11,15 +11,15 @@ import Foundation // sourcery: AutoMockable protocol BadgeGetIntentColorsUseCaseable { func execute(intentType: BadgeIntentType, - on colors: Colors) -> BadgeColorables + on colors: Colors) -> BadgeColors } -class BadgeGetIntentColorsUseCase: BadgeGetIntentColorsUseCaseable { +final class BadgeGetIntentColorsUseCase: BadgeGetIntentColorsUseCaseable { // MARK: - Methods func execute(intentType: BadgeIntentType, - on colors: Colors) -> BadgeColorables { + on colors: Colors) -> BadgeColors { let surfaceColor = colors.base.surface switch intentType { diff --git a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift index 071f5fffc..4d2263b62 100644 --- a/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift +++ b/core/Sources/Components/Badge/UseCase/GetIntentColors/BadgeGetIntentColorsUseCaseTests.swift @@ -82,7 +82,7 @@ final class BadgeGetColorsUseCaseTests: XCTestCase { private struct Tester { static func testColorsProperties( - givenColors: BadgeColorables, + givenColors: BadgeColors, getColors: BadgeGetColors ) throws { // Background Color diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift index 72b788ea6..c8763189c 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift @@ -24,19 +24,7 @@ private struct TestBadgeFormatting: BadgeFormatting { final class BadgeViewTests: SwiftUIComponentTestCase { - var theme: Theme! - - override func setUpWithError() throws { - try super.setUpWithError() - - theme = SparkTheme() - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - - theme = nil - } + private let theme: Theme! = SparkTheme() func test_badge_all_cases_no_text() throws { for badgeIntentType in BadgeIntentType.allCases { diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift index cf07514d0..c2321fb30 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift @@ -23,19 +23,7 @@ private struct TestBadgeFormatting: BadgeFormatting { final class BadgeUIViewTests: UIKitComponentTestCase { - var theme: Theme! - - override func setUpWithError() throws { - try super.setUpWithError() - - theme = SparkTheme() - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - - theme = nil - } + private let theme: Theme! = SparkTheme() func test_badge_all_cases_no_text() throws { for badgeIntentType in BadgeIntentType.allCases { diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index 6fca947c2..f0ad63cbf 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -13,20 +13,7 @@ import XCTest final class BadgeViewModelTests: XCTestCase { - var theme: ThemeGeneratedMock! - - // MARK: - Setup - override func setUpWithError() throws { - try super.setUpWithError() - - self.theme = ThemeGeneratedMock.mock - } - - override func tearDownWithError() throws { - try super.tearDownWithError() - - self.theme = nil - } + var theme: ThemeGeneratedMock! = ThemeGeneratedMock.mocked() // MARK: - Tests func test_init() throws { @@ -73,21 +60,3 @@ private extension BadgeBorder { color.color == theme.colors.base.surface.color } } - -private extension Theme where Self == ThemeGeneratedMock { - static var mock: Self { - let theme = ThemeGeneratedMock() - let colors = ColorsGeneratedMock.mocked() - - colors.base = ColorsBaseGeneratedMock.mocked() - colors.primary = ColorsPrimaryGeneratedMock.mocked() - colors.feedback = ColorsFeedbackGeneratedMock.mocked() - theme.colors = colors - theme.dims = DimsGeneratedMock.mocked() - theme.typography = TypographyGeneratedMock.mocked() - theme.layout = LayoutGeneratedMock.mocked() - theme.border = BorderGeneratedMock.mocked() - - return theme - } -} From 15e3f754f78ba8b7b7efd5094cb87d2a2213dea8 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 31 May 2023 14:06:53 +0200 Subject: [PATCH 16/31] Updated view model and tests --- .../Badge/ViewModel/BadgeViewModel.swift | 76 ++++++++++++++++--- .../Badge/ViewModel/BadgeViewModelTests.swift | 71 +++++++++++++++++ ...pographyGeneratedMock+ExtensionTests.swift | 1 + 3 files changed, 137 insertions(+), 11 deletions(-) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index c18438268..d85abb6a4 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -37,18 +37,47 @@ public final class BadgeViewModel: ObservableObject { @Published private var value: Int? = nil // MARK: - Text Properties - var text: String - var textFont: TypographyFontToken - var textColor: ColorToken + public var text: String + private(set) var textFont: TypographyFontToken + private(set) var textColor: ColorToken // MARK: - Appearance Properties - private(set) var badgeFormat: BadgeFormat - var badgeBorder: BadgeBorder - var backgroundColor: ColorToken - var theme: Theme + private var badgeFormat: BadgeFormat - var verticalOffset: CGFloat - var horizontalOffset: CGFloat + @Published public var badgeBorder: BadgeBorder + @Published public var badgeSize: BadgeSize { + didSet { + guard oldValue != badgeSize else { + return + } + + reloadSize() + } + } + @Published public var badgeType: BadgeIntentType { + didSet { + guard oldValue != badgeType else { + return + } + + reloadColors() + } + } + @Published public var isBadgeOutlined: Bool { + didSet { + guard oldValue != isBadgeOutlined else { + return + } + + reloadOutline() + } + } + + public var backgroundColor: ColorToken + public var theme: Theme + + public var verticalOffset: CGFloat + public var horizontalOffset: CGFloat // MARK: - Initializer @@ -77,13 +106,38 @@ public final class BadgeViewModel: ObservableObject { ) self.theme = theme - self.badgeFormat = .default + + self.badgeFormat = format + self.badgeSize = badgeSize + self.badgeType = badgeType + self.isBadgeOutlined = isOutlined } - // MARK: - Update configuration function + // MARK: - Update configuration functions public func setBadgeValue(_ value: Int?) { self.value = value self.text = badgeFormat.badgeText(value) } + + private func reloadSize() { + self.textFont = badgeSize == .normal ? theme.typography.captionHighlight : theme.typography.smallHighlight + } + + private func reloadColors() { + let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeType, on: theme.colors) + + self.textColor = badgeColors.foregroundColor + + self.backgroundColor = badgeColors.backgroundColor + + self.badgeBorder.setColor(badgeColors.borderColor) + } + + private func reloadOutline() { + self.badgeBorder.setWidth(isBadgeOutlined ? + theme.border.width.medium : + theme.border.width.none + ) + } } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index f0ad63cbf..44d7ce07e 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -49,8 +49,79 @@ final class BadgeViewModelTests: XCTestCase { viewModel.setBadgeValue(233) XCTAssertEqual(expectedUpdatedText, viewModel.text, "Text doesn't match incremented value with standart format") + + XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.medium, "Border is not visible") + + viewModel.isBadgeOutlined = false + + XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.none, "Border should be hidden") + + XCTAssertEqual(viewModel.textFont.font, theme.typography.captionHighlight.font, "Font is wrong") + + viewModel.badgeSize = .small + + XCTAssertEqual(viewModel.textFont.font, theme.typography.smallHighlight.font, "Font is wrong") + } + } + + func test_update_outline() throws { + for badgeIntentType in BadgeIntentType.allCases { + // Given + + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + + // Then + + XCTAssertTrue(viewModel.isBadgeOutlined, "Badge should be outlined by default") + + XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.medium, "Border is not visible") + + viewModel.isBadgeOutlined = false + + XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.none, "Border should be hidden") } } + + func test_update_size() throws { + for badgeIntentType in BadgeIntentType.allCases { + // Given + + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + + // Then + + XCTAssertEqual(viewModel.badgeSize, .normal, "Badge should be .normal sized by default") + + XCTAssertEqual(viewModel.textFont.font, theme.typography.captionHighlight.font, "Font is wrong") + + viewModel.badgeSize = .small + + XCTAssertEqual(viewModel.textFont.font, theme.typography.smallHighlight.font, "Font is wrong") + } + } + + func test_update_intent() throws { + for badgeIntentType in BadgeIntentType.allCases { + // Given + + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + + // Then + + XCTAssertEqual(viewModel.badgeType, badgeIntentType, "Intent type was set wrong") + + viewModel.badgeType = randomizeIntentAndExceptingCurrent(badgeIntentType) + + XCTAssertNotEqual(viewModel.badgeType, badgeIntentType, "Intent type was set wrong") + } + } + + private func randomizeIntentAndExceptingCurrent(_ currentIntentType: BadgeIntentType) -> BadgeIntentType { + let filteredIntentTypes = BadgeIntentType.allCases.filter({ $0 != currentIntentType }) + let randomIndex = Int.random(in: 0...filteredIntentTypes.count - 1) + + return filteredIntentTypes[randomIndex] + } } private extension BadgeBorder { diff --git a/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift b/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift index 74d91a5c2..cd53e8c5c 100644 --- a/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift +++ b/core/Sources/Theming/Content/Typography/TypographyGeneratedMock+ExtensionTests.swift @@ -21,6 +21,7 @@ extension TypographyGeneratedMock { typography.body2 = TypographyFontTokenGeneratedMock.mocked(.body) typography.caption = TypographyFontTokenGeneratedMock.mocked(.caption) typography.captionHighlight = TypographyFontTokenGeneratedMock.mocked(.caption.bold()) + typography.smallHighlight = TypographyFontTokenGeneratedMock.mocked(.caption2.bold()) return typography } From fc780db93abfe5e3eba197125a75069f66fb18f0 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 31 May 2023 14:28:47 +0200 Subject: [PATCH 17/31] Fixed typo --- .../Components/Badge/Properties/Public/BadgeFormat.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift index 461003900..fdf08301e 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeFormat.swift @@ -15,7 +15,7 @@ public protocol BadgeFormatting { } /// With this formatter you can define behaviour of Badge label. -/// avalabled formats: +/// available formats: /// - ``default`` /// - ``overflowCounter(maxValue:)`` /// - ``custom(formatter:)`` From d275fc2f8ff69f31381871e58ec0d8f0d4ed0dc3 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 31 May 2023 14:38:30 +0200 Subject: [PATCH 18/31] Fixed test data declaration --- core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift | 2 +- core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift | 2 +- .../Components/Badge/ViewModel/BadgeViewModelTests.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift index c8763189c..d38cf18b5 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift @@ -24,7 +24,7 @@ private struct TestBadgeFormatting: BadgeFormatting { final class BadgeViewTests: SwiftUIComponentTestCase { - private let theme: Theme! = SparkTheme() + private let theme: Theme = SparkTheme() func test_badge_all_cases_no_text() throws { for badgeIntentType in BadgeIntentType.allCases { diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift index c2321fb30..74abc92cc 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift @@ -23,7 +23,7 @@ private struct TestBadgeFormatting: BadgeFormatting { final class BadgeUIViewTests: UIKitComponentTestCase { - private let theme: Theme! = SparkTheme() + private let theme: Theme = SparkTheme() func test_badge_all_cases_no_text() throws { for badgeIntentType in BadgeIntentType.allCases { diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index 44d7ce07e..fb7724a29 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -13,7 +13,7 @@ import XCTest final class BadgeViewModelTests: XCTestCase { - var theme: ThemeGeneratedMock! = ThemeGeneratedMock.mocked() + var theme: ThemeGeneratedMock = ThemeGeneratedMock.mocked() // MARK: - Tests func test_init() throws { From 59ea0d8b51635d3b775cbe0cecb5555ca04e2ab4 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 31 May 2023 17:34:35 +0200 Subject: [PATCH 19/31] Updated view model --- core/Demo/Classes/BadgeUIView_Previews.swift | 101 ++++++++-------- core/Demo/Classes/BadgeView_Previews.swift | 113 ++++++++--------- .../Badge/View/SwiftUI/BadgeView.swift | 24 ++-- .../Badge/ViewModel/BadgeViewModel.swift | 45 +++---- .../Badge/ViewModel/BadgeViewModelTests.swift | 24 ---- .../Components/Badge/BadgeComponentView.swift | 114 ++++++++++-------- 6 files changed, 204 insertions(+), 217 deletions(-) diff --git a/core/Demo/Classes/BadgeUIView_Previews.swift b/core/Demo/Classes/BadgeUIView_Previews.swift index 857608436..1c80bd744 100644 --- a/core/Demo/Classes/BadgeUIView_Previews.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -20,51 +20,7 @@ private struct BadgePreviewFormatter: BadgeFormatting { struct UIBadgeView: UIViewRepresentable { - private var viewModels: [BadgeViewModel] = - [ - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - initValue: 6 - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .small, - initValue: 22, - format: .overflowCounter(maxValue: 20) - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .danger, - initValue: 10, - format: .custom( - formatter: BadgePreviewFormatter() - ) - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .info - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .neutral, - isOutlined: false - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .primary - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .secondary, - initValue: 23 - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .success - ) - ] + var viewModels: [BadgeViewModel] func makeUIView(context: Context) -> some UIView { let badgeViews = viewModels.enumerated().map { index, viewModel in @@ -113,9 +69,60 @@ struct UIBadgeView: UIViewRepresentable { struct BadgeUIView_Previews: PreviewProvider { struct BadgeUIViewBridge: View { + private var viewModels = [ + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + initValue: 6 + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .small, + initValue: 22, + format: .overflowCounter(maxValue: 20) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger, + initValue: 10, + format: .custom( + formatter: BadgePreviewFormatter() + ) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .info + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .neutral, + isOutlined: false + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .primary + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .secondary, + initValue: 23 + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .success + ) + ] + var body: some View { - UIBadgeView() - .frame(height: 400) + List { + Button("Tap Me") { + viewModels[0].setBadgeValue(23) + } + UIBadgeView(viewModels: viewModels) + .frame(height: 400) + .listRowBackground(Color.gray.opacity(0.3)) + } } } diff --git a/core/Demo/Classes/BadgeView_Previews.swift b/core/Demo/Classes/BadgeView_Previews.swift index 0e5df613c..9433575f9 100644 --- a/core/Demo/Classes/BadgeView_Previews.swift +++ b/core/Demo/Classes/BadgeView_Previews.swift @@ -22,7 +22,7 @@ struct BadgeView_Previews: PreviewProvider { struct BadgeContainerView: View { - @StateObject var standartBadge = BadgeViewModel( + @State var standartBadge = BadgeViewModel( theme: SparkTheme.shared, badgeType: .alert, badgeSize: .normal, @@ -34,7 +34,7 @@ struct BadgeView_Previews: PreviewProvider { badgeType: .alert, badgeSize: .small, initValue: 22, - format: .overflowCounter(maxValue: 20), + format: .overflowCounter(maxValue: 10), isOutlined: false ) @@ -79,66 +79,71 @@ struct BadgeView_Previews: PreviewProvider { @ScaledMetric var vOffset: CGFloat var body: some View { - ScrollView { - Button("Change value") { - smallCustomWithoutBorder.setBadgeValue(18) - smallCustomWithoutBorder.isBadgeOutlined = true - smallCustomWithoutBorder.badgeType = .primary - standartBadge.setBadgeValue(23) - standartDangerBadge.badgeType = .neutral - smallCustomWithoutBorder.badgeSize = .normal - } - VStack(spacing: 100) { - HStack(spacing: 100) { - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartBadge) - .offset(x: hOffset, y: -vOffset) - } - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: smallCustomWithoutBorder) - .offset(x: hOffset, y: -vOffset) - } + List { + Section(header: Text("SwiftUI Badge")) { + Button("Change Default Badge Value") { + standartBadge.setBadgeValue(23) } - - HStack(spacing: 100) { - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartDangerBadge) - .offset(x: hOffset, y: -vOffset) - } - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartInfoBadge) - .offset(x: hOffset, y: -vOffset) - } - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartNeutralBadge) - .offset(x: hOffset, y: -vOffset) - } + Button("Change Small Custom Badge") { + smallCustomWithoutBorder.setBadgeValue(18) + smallCustomWithoutBorder.isBadgeOutlined = true + smallCustomWithoutBorder.badgeType = .primary + smallCustomWithoutBorder.badgeSize = .normal } - - HStack(spacing: 100) { - HStack { - Text("Text") - BadgeView(viewModel: standartPrimaryBadge) + Button("Change Dange Badge") { + standartDangerBadge.badgeType = .neutral + } + VStack(spacing: 100) { + HStack(spacing: 50) { + ZStack(alignment: .leading) { + Text("Default Badge") + BadgeView(viewModel: standartBadge) + .offset(x: 100, y: -15) + } + ZStack(alignment: .leading) { + Text("Small Custom") + BadgeView(viewModel: smallCustomWithoutBorder) + .offset(x: 100, y: -15) + } } - HStack { - Text("Text") - BadgeView(viewModel: standartSecondaryBadge) + + HStack(spacing: 55) { + ZStack(alignment: .leading) { + Text("Danger Badge") + BadgeView(viewModel: standartDangerBadge) + .offset(x: 100, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartInfoBadge) + .offset(x: 25, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartNeutralBadge) + .offset(x: 25, y: -15) + } } - HStack { - Text("Text") - BadgeView(viewModel: standartSuccessBadge) + + HStack(spacing: 50) { + HStack { + Text("Text") + BadgeView(viewModel: standartPrimaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSecondaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSuccessBadge) + } } } + .padding(.vertical, 15) + .listRowBackground(Color.gray.opacity(0.3)) } - .offset(y: 30) - .frame(minWidth: 375) } - .background(Color.gray) } } diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index 851d8bd18..c4e6d7324 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -42,26 +42,26 @@ public struct BadgeView: View { @ScaledMetric private var borderWidth: CGFloat public var body: some View { - if viewModel.text.isEmpty { + if self.viewModel.text.isEmpty { Circle() - .foregroundColor(viewModel.backgroundColor.color) + .foregroundColor(self.viewModel.backgroundColor.color) .frame(width: self.emptySize, height: self.emptySize) .border( - width: borderWidth, - radius: viewModel.badgeBorder.radius, - colorToken: viewModel.badgeBorder.color + width: self.viewModel.isBadgeOutlined ? borderWidth : 0, + radius: self.viewModel.badgeBorder.radius, + colorToken: self.viewModel.badgeBorder.color ) .fixedSize() } else { - Text(viewModel.text) - .font(viewModel.textFont.font) - .foregroundColor(viewModel.textColor.color) + Text(self.viewModel.text) + .font(self.viewModel.textFont.font) + .foregroundColor(self.viewModel.textColor.color) .padding(.init(vertical: self.smallOffset, horizontal: self.mediumOffset)) - .background(viewModel.backgroundColor.color) + .background(self.viewModel.backgroundColor.color) .border( - width: borderWidth, - radius: viewModel.badgeBorder.radius, - colorToken: viewModel.badgeBorder.color + width: self.viewModel.isBadgeOutlined ? borderWidth : 0, + radius: self.viewModel.badgeBorder.radius, + colorToken: self.viewModel.badgeBorder.color ) .fixedSize() .accessibilityIdentifier(BadgeAccessibilityIdentifier.text) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index d85abb6a4..4da2c748a 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -38,13 +38,10 @@ public final class BadgeViewModel: ObservableObject { // MARK: - Text Properties public var text: String - private(set) var textFont: TypographyFontToken - private(set) var textColor: ColorToken + var textFont: TypographyFontToken + var textColor: ColorToken - // MARK: - Appearance Properties - private var badgeFormat: BadgeFormat - - @Published public var badgeBorder: BadgeBorder + // MARK: - Appearance Public Properties @Published public var badgeSize: BadgeSize { didSet { guard oldValue != badgeSize else { @@ -63,21 +60,18 @@ public final class BadgeViewModel: ObservableObject { reloadColors() } } - @Published public var isBadgeOutlined: Bool { - didSet { - guard oldValue != isBadgeOutlined else { - return - } + @Published public var isBadgeOutlined: Bool - reloadOutline() - } - } + // MARK: - Appearance Internal Properties + var backgroundColor: ColorToken + var badgeBorder: BadgeBorder + var theme: Theme - public var backgroundColor: ColorToken - public var theme: Theme + var verticalOffset: CGFloat + var horizontalOffset: CGFloat - public var verticalOffset: CGFloat - public var horizontalOffset: CGFloat + // MARK: - Appearance Private Properties + private var badgeFormat: BadgeFormat // MARK: - Initializer @@ -98,9 +92,7 @@ public final class BadgeViewModel: ObservableObject { self.horizontalOffset = horizontalOffset self.badgeBorder = BadgeBorder( - width: isOutlined ? - theme.border.width.medium : - theme.border.width.none, + width: theme.border.width.medium, radius: theme.border.radius.full, color: badgeColors.borderColor ) @@ -113,7 +105,7 @@ public final class BadgeViewModel: ObservableObject { self.isBadgeOutlined = isOutlined } - // MARK: - Update configuration functions + // MARK: - Badge update functions public func setBadgeValue(_ value: Int?) { self.value = value @@ -121,7 +113,7 @@ public final class BadgeViewModel: ObservableObject { } private func reloadSize() { - self.textFont = badgeSize == .normal ? theme.typography.captionHighlight : theme.typography.smallHighlight + self.textFont = self.badgeSize == .normal ? self.theme.typography.captionHighlight : self.theme.typography.smallHighlight } private func reloadColors() { @@ -133,11 +125,4 @@ public final class BadgeViewModel: ObservableObject { self.badgeBorder.setColor(badgeColors.borderColor) } - - private func reloadOutline() { - self.badgeBorder.setWidth(isBadgeOutlined ? - theme.border.width.medium : - theme.border.width.none - ) - } } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index fb7724a29..8e83c4d62 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -50,12 +50,6 @@ final class BadgeViewModelTests: XCTestCase { XCTAssertEqual(expectedUpdatedText, viewModel.text, "Text doesn't match incremented value with standart format") - XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.medium, "Border is not visible") - - viewModel.isBadgeOutlined = false - - XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.none, "Border should be hidden") - XCTAssertEqual(viewModel.textFont.font, theme.typography.captionHighlight.font, "Font is wrong") viewModel.badgeSize = .small @@ -64,24 +58,6 @@ final class BadgeViewModelTests: XCTestCase { } } - func test_update_outline() throws { - for badgeIntentType in BadgeIntentType.allCases { - // Given - - let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) - - // Then - - XCTAssertTrue(viewModel.isBadgeOutlined, "Badge should be outlined by default") - - XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.medium, "Border is not visible") - - viewModel.isBadgeOutlined = false - - XCTAssertEqual(viewModel.badgeBorder.width, theme.border.width.none, "Border should be hidden") - } - } - func test_update_size() throws { for badgeIntentType in BadgeIntentType.allCases { // Given diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index 74ac5e550..ee664a1ec 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -33,7 +33,8 @@ struct BadgeComponentView: View { badgeType: .alert, badgeSize: .small, initValue: 22, - format: .overflowCounter(maxValue: 20) + format: .overflowCounter(maxValue: 20), + isOutlined: false ) @State var standartDangerBadge = BadgeViewModel( @@ -75,64 +76,77 @@ struct BadgeComponentView: View { @State var isOutlined: Bool = false var body: some View { - ScrollView { - Button("Change value") { - standartBadge.setBadgeValue(23) - smallCustomWithoutBorder.setBadgeValue(18) + List { + Section(header: Text("UIKit Badge")) { + UIBadgeView() + .frame(height: 400) } - UIBadgeView() - .frame(height: 400) - VStack(spacing: 100) { - HStack(spacing: 50) { - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartBadge) - .offset(x: 25, y: -15) - } - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: smallCustomWithoutBorder) - .offset(x: 25, y: -15) - } - } + .listRowBackground(Color.gray.opacity(0.3)) - HStack(spacing: 50) { - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartDangerBadge) - .offset(x: 25, y: -15) - } - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartInfoBadge) - .offset(x: 25, y: -15) - } - ZStack(alignment: .leading) { - Text("Text") - BadgeView(viewModel: standartNeutralBadge) - .offset(x: 25, y: -15) - } + Section(header: Text("SwiftUI Badge")) { + Button("Change Default Badge Value") { + standartBadge.setBadgeValue(23) } - - HStack(spacing: 50) { - HStack { - Text("Text") - BadgeView(viewModel: standartPrimaryBadge) + Button("Change Small Custom Badge") { + smallCustomWithoutBorder.setBadgeValue(18) + smallCustomWithoutBorder.isBadgeOutlined = true + smallCustomWithoutBorder.badgeType = .primary + smallCustomWithoutBorder.badgeSize = .normal + } + Button("Change Dange Badge") { + standartDangerBadge.badgeType = .neutral + } + VStack(spacing: 100) { + HStack(spacing: 50) { + ZStack(alignment: .leading) { + Text("Default Badge") + BadgeView(viewModel: standartBadge) + .offset(x: 100, y: -15) + } + ZStack(alignment: .leading) { + Text("Small Custom") + BadgeView(viewModel: smallCustomWithoutBorder) + .offset(x: 100, y: -15) + } } - HStack { - Text("Text") - BadgeView(viewModel: standartSecondaryBadge) + + HStack(spacing: 55) { + ZStack(alignment: .leading) { + Text("Danger Badge") + BadgeView(viewModel: standartDangerBadge) + .offset(x: 100, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartInfoBadge) + .offset(x: 25, y: -15) + } + ZStack(alignment: .leading) { + Text("Text") + BadgeView(viewModel: standartNeutralBadge) + .offset(x: 25, y: -15) + } } - HStack { - Text("Text") - BadgeView(viewModel: standartSuccessBadge) + + HStack(spacing: 50) { + HStack { + Text("Text") + BadgeView(viewModel: standartPrimaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSecondaryBadge) + } + HStack { + Text("Text") + BadgeView(viewModel: standartSuccessBadge) + } } } + .padding(.vertical, 15) + .listRowBackground(Color.gray.opacity(0.3)) } - .offset(y: 30) - .frame(minWidth: 375) } - .background(Color.gray) } } From ae92e2be82a5fc80e68acecf73138c1f66aaa27e Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 1 Jun 2023 13:27:22 +0200 Subject: [PATCH 20/31] Updated view model to change value without function --- .../Badge/View/SwiftUI/BadgeView.swift | 4 +- .../Badge/ViewModel/BadgeViewModel.swift | 21 ++++-- .../Badge/ViewModel/BadgeViewModelTests.swift | 2 +- .../Components/Badge/BadgeComponentView.swift | 68 ++++++++++++++++++- 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index c4e6d7324..ec6198479 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -35,14 +35,14 @@ import SwiftUI /// } /// ``` public struct BadgeView: View { - @ObservedObject public var viewModel: BadgeViewModel + @ObservedObject private var viewModel: BadgeViewModel @ScaledMetric private var smallOffset: CGFloat @ScaledMetric private var mediumOffset: CGFloat @ScaledMetric private var emptySize: CGFloat @ScaledMetric private var borderWidth: CGFloat public var body: some View { - if self.viewModel.text.isEmpty { + if self.viewModel.isBadgeEmpty { Circle() .foregroundColor(self.viewModel.backgroundColor.color) .frame(width: self.emptySize, height: self.emptySize) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 4da2c748a..16b0d4fcc 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -34,21 +34,28 @@ import SwiftUI /// - badgeFormat -- see ``BadgeFormat`` as a formatter of **text** public final class BadgeViewModel: ObservableObject { - @Published private var value: Int? = nil - // MARK: - Text Properties public var text: String var textFont: TypographyFontToken var textColor: ColorToken // MARK: - Appearance Public Properties + @Published public var value: Int? = nil { + didSet { + guard oldValue != value else { + return + } + + self.updateBadgeValue() + } + } @Published public var badgeSize: BadgeSize { didSet { guard oldValue != badgeSize else { return } - reloadSize() + self.reloadSize() } } @Published public var badgeType: BadgeIntentType { @@ -57,7 +64,7 @@ public final class BadgeViewModel: ObservableObject { return } - reloadColors() + self.reloadColors() } } @Published public var isBadgeOutlined: Bool @@ -66,6 +73,9 @@ public final class BadgeViewModel: ObservableObject { var backgroundColor: ColorToken var badgeBorder: BadgeBorder var theme: Theme + var isBadgeEmpty: Bool { + self.badgeFormat.badgeText(value).isEmpty + } var verticalOffset: CGFloat var horizontalOffset: CGFloat @@ -107,8 +117,7 @@ public final class BadgeViewModel: ObservableObject { // MARK: - Badge update functions - public func setBadgeValue(_ value: Int?) { - self.value = value + private func updateBadgeValue() { self.text = badgeFormat.badgeText(value) } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index 8e83c4d62..1c29d6663 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -46,7 +46,7 @@ final class BadgeViewModelTests: XCTestCase { XCTAssertEqual(expectedInitText, viewModel.text, "Text doesn't match init value with standart format") - viewModel.setBadgeValue(233) + viewModel.value = 233 XCTAssertEqual(expectedUpdatedText, viewModel.text, "Text doesn't match incremented value with standart format") diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index ee664a1ec..a1ea8df5e 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -21,6 +21,53 @@ private struct BadgePreviewFormatter: BadgeFormatting { struct BadgeComponentView: View { + private var viewModels: [BadgeViewModel] = + [ + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .normal, + initValue: 6 + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .alert, + badgeSize: .small, + initValue: 22, + format: .overflowCounter(maxValue: 20) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .danger, + initValue: 10, + format: .custom( + formatter: BadgePreviewFormatter() + ) + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .info + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .neutral, + isOutlined: false + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .primary + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .secondary, + initValue: 6 + ), + BadgeViewModel( + theme: SparkTheme.shared, + badgeType: .success + ) + ] + @StateObject var standartBadge = BadgeViewModel( theme: SparkTheme.shared, badgeType: .alert, @@ -78,17 +125,32 @@ struct BadgeComponentView: View { var body: some View { List { Section(header: Text("UIKit Badge")) { - UIBadgeView() + Button("Change UIKit Badge 0 Type") { + viewModels[0].badgeType = BadgeIntentType.allCases.randomElement() ?? .alert + } + Button("Change UIKit Badge 1 Value") { + if viewModels[1].value == 10 { + viewModels[1].value = nil + } else if viewModels[1].value == 22 { + viewModels[1].value = 10 + } else { + viewModels[1].value = 22 + } + } + Button("Change UIKit Badge 2 Outline") { + viewModels[2].isBadgeOutlined.toggle() + } + UIBadgeView(viewModels: viewModels) .frame(height: 400) } .listRowBackground(Color.gray.opacity(0.3)) Section(header: Text("SwiftUI Badge")) { Button("Change Default Badge Value") { - standartBadge.setBadgeValue(23) + standartBadge.value = 23 } Button("Change Small Custom Badge") { - smallCustomWithoutBorder.setBadgeValue(18) + smallCustomWithoutBorder.value = 18 smallCustomWithoutBorder.isBadgeOutlined = true smallCustomWithoutBorder.badgeType = .primary smallCustomWithoutBorder.badgeSize = .normal From feabd99b1a8d8e6d591bfe4b606226326c666f9b Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 1 Jun 2023 13:27:55 +0200 Subject: [PATCH 21/31] Updated BadgeUIView with subscribers --- .../Badge/View/UIKit/BadgeUIView.swift | 167 +++++++++++------- 1 file changed, 104 insertions(+), 63 deletions(-) diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index 25821b490..b36425310 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -6,6 +6,7 @@ // Copyright © 2023 Adevinta. All rights reserved. // +import Combine import UIKit /// This is the UIKit version for the ``BadgeView`` @@ -13,36 +14,51 @@ public class BadgeUIView: UIView { private var viewModel: BadgeViewModel - /// Constraints for badge size in empty state - /// In this case badge is shown like a circle - private var emptyHeightConstraint: NSLayoutConstraint? - private var emptyWidthConstraint: NSLayoutConstraint? - - /// Dynamicaly sized properties for badge - /// ``emptyBadgeSize`` represents size of the circle in empty state of Badge - /// - /// ``horizontalSpacing`` and ``verticalSpacing`` are properties - /// that used for space between badge background and text + // Dynamicaly sized properties for badge + // emptyBadgeSize represents size of the circle in empty state of Badge + // horizontalSpacing and verticalSpacing are properties + // that used for space between badge background and text @ScaledUIMetric private var emptyBadgeSize: CGFloat = 0 @ScaledUIMetric private var horizontalSpacing: CGFloat = 0 @ScaledUIMetric private var verticalSpacing: CGFloat = 0 @ScaledUIMetric private var borderWidth: CGFloat = 0 - private var badgeLabel: UILabel = UILabel() - - /// Constraints for badge in non-empty state. - /// All of them are set to the badge text label - /// After that Badge view size is based on the size of the text - private var badgeTopConstraint: NSLayoutConstraint? - private var badgeLeadingConstraint: NSLayoutConstraint? - private var badgeTrailingConstraint: NSLayoutConstraint? - private var badgeBottomConstraint: NSLayoutConstraint? + // Constraints for badge size + // Thess constraints containes text size with + // vertical and horizontal offsets private var badgeWidthConstraint: NSLayoutConstraint? private var badgeHeightConstraint: NSLayoutConstraint? - private var badgeConstraints: [NSLayoutConstraint?] { - [badgeTopConstraint, badgeLeadingConstraint, badgeTrailingConstraint, badgeBottomConstraint, badgeWidthConstraint, badgeHeightConstraint] + private var badgeSizeConstraints: [NSLayoutConstraint?] { + [badgeWidthConstraint, badgeHeightConstraint] } + // MARK: - Badge Text Label properties + private var badgeLabel: UILabel = UILabel() + + // Constraints for badge text label. + // All of these are applied to the badge text label + private var badgeLabelTopConstraint: NSLayoutConstraint? + private var badgeLabelLeadingConstraint: NSLayoutConstraint? + private var badgeLabelTrailingConstraint: NSLayoutConstraint? + private var badgeLabelBottomConstraint: NSLayoutConstraint? + + // Array of badge text label constraints for + // easier activation + private var badgeLabelConstraints: [NSLayoutConstraint?] { + [badgeLabelTopConstraint, badgeLabelLeadingConstraint, badgeLabelTrailingConstraint, badgeLabelBottomConstraint] + } + + // Bool property that determines wether we should + // install and activate text label constraints or not + private var shouldSetupLabelConstrains: Bool { + self.badgeLabelTopConstraint == nil || + self.badgeLabelBottomConstraint == nil || + self.badgeLabelLeadingConstraint == nil || + self.badgeLabelTrailingConstraint == nil + } + + private var cancellables = Set() + // MARK: - Init public init(viewModel: BadgeViewModel) { @@ -64,6 +80,44 @@ public class BadgeUIView: UIView { setupBadgeText() setupAppearance() setupLayouts() + subscribe() + } + + private func subscribe() { + self.viewModel.$badgeSize + .receive(on: DispatchQueue.main) + .sink { [weak self] badgeSize in + self?.reloadBadgeFontIfNeeded() + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + self.viewModel.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.badgeLabel.text = self?.viewModel.text + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + self.viewModel.$badgeType + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.reloadColors() + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + self.viewModel.$isBadgeOutlined + .receive(on: DispatchQueue.main) + .sink { [weak self] isBadgeOutlined in + if isBadgeOutlined { + self?.reloadBorderWidth() + } else { + self?.layer.borderWidth = 0 + } + } + .store(in: &cancellables) } private func setupBadgeText() { @@ -86,64 +140,51 @@ public class BadgeUIView: UIView { } private func setupScalables() { - if self.viewModel.text.isEmpty { - self.emptyBadgeSize = BadgeConstants.emptySize.width - } else { - self.horizontalSpacing = self.viewModel.horizontalOffset - self.verticalSpacing = self.viewModel.verticalOffset - } + self.emptyBadgeSize = BadgeConstants.emptySize.width + self.horizontalSpacing = self.viewModel.horizontalOffset + self.verticalSpacing = self.viewModel.verticalOffset self.borderWidth = self.viewModel.badgeBorder.width } // MARK: - Layouts setup private func setupLayouts() { - if self.viewModel.text.isEmpty { - self.setupEmptySizeConstraints() - } else { - let textSize = badgeLabel.intrinsicContentSize - self.setupBadgeConstraints(for: textSize) - } + let textSize = badgeLabel.intrinsicContentSize + + self.setupSizeConstraint(for: textSize) + self.setupBadgeConstraintsIfNeeded(for: textSize) } - private func setupEmptySizeConstraints() { - if let emptyHeightConstraint { - emptyHeightConstraint.constant = emptyBadgeSize - } else { - self.emptyHeightConstraint = self.heightAnchor.constraint(equalToConstant: emptyBadgeSize) - self.emptyHeightConstraint?.isActive = true - } - if let emptyWidthConstraint { - emptyWidthConstraint.constant = emptyBadgeSize + private func setupSizeConstraint(for textSize: CGSize) { + let widht = self.viewModel.isBadgeEmpty ? self.emptyBadgeSize : textSize.width + (self.horizontalSpacing * 2) + let height = self.viewModel.isBadgeEmpty ? self.emptyBadgeSize : textSize.height + (self.verticalSpacing * 2) + + if let badgeWidthConstraint, let badgeHeightConstraint { + badgeWidthConstraint.constant = widht + badgeHeightConstraint.constant = height } else { - self.emptyWidthConstraint = self.widthAnchor.constraint(equalToConstant: emptyBadgeSize) - self.emptyWidthConstraint?.isActive = true + self.badgeWidthConstraint = self.widthAnchor.constraint(equalToConstant: widht) + self.badgeHeightConstraint = self.heightAnchor.constraint(equalToConstant: height) + NSLayoutConstraint.activate(badgeSizeConstraints.compactMap({ $0 })) } } - private func setupBadgeConstraints(for textSize: CGSize) { - if let badgeTopConstraint, let badgeBottomConstraint, let badgeLeadingConstraint, let badgeTrailingConstraint, let badgeWidthConstraint, let badgeHeightConstraint { - badgeLeadingConstraint.constant = self.horizontalSpacing - badgeTrailingConstraint.constant = -self.horizontalSpacing - badgeTopConstraint.constant = self.verticalSpacing - badgeBottomConstraint.constant = -self.verticalSpacing - badgeWidthConstraint.constant = textSize.width - badgeHeightConstraint.constant = textSize.height - } else { - badgeLeadingConstraint = badgeLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: self.horizontalSpacing) - badgeTopConstraint = badgeLabel.topAnchor.constraint(equalTo: topAnchor, constant: self.verticalSpacing) - badgeTrailingConstraint = badgeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -self.horizontalSpacing) - badgeBottomConstraint = badgeLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -self.verticalSpacing) - badgeWidthConstraint = badgeLabel.widthAnchor.constraint(equalToConstant: textSize.width) - badgeHeightConstraint = badgeLabel.heightAnchor.constraint(equalToConstant: textSize.height) - NSLayoutConstraint.activate(badgeConstraints.compactMap({ $0 })) + private func setupBadgeConstraintsIfNeeded(for textSize: CGSize) { + guard shouldSetupLabelConstrains else { + return } + + self.badgeLabelLeadingConstraint = self.badgeLabel.leadingAnchor.constraint(equalTo: leadingAnchor) + self.badgeLabelTopConstraint = self.badgeLabel.topAnchor.constraint(equalTo: topAnchor) + self.badgeLabelTrailingConstraint = self.badgeLabel.trailingAnchor.constraint(equalTo: trailingAnchor) + self.badgeLabelBottomConstraint = self.badgeLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + NSLayoutConstraint.activate(badgeLabelConstraints.compactMap({ $0 })) } public override func layoutSubviews() { super.layoutSubviews() - self.layer.cornerRadius = frame.height / 2.0 + self.layer.cornerRadius = min(frame.width, frame.height) / 2.0 } // MARK: - Updates on Trait Collection Change @@ -155,14 +196,14 @@ public class BadgeUIView: UIView { } private func reloadBadgeFontIfNeeded() { - guard !self.viewModel.text.isEmpty else { + guard !self.viewModel.isBadgeEmpty else { return } self.badgeLabel.font = self.viewModel.textFont.uiFont } private func reloadUISize() { - if self.viewModel.text.isEmpty { + if self.viewModel.isBadgeEmpty { self._emptyBadgeSize.update(traitCollection: self.traitCollection) } else { self._horizontalSpacing.update(traitCollection: self.traitCollection) From 01d312b73251405cbe01fde1c5ea5be9ffd9f6be Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 1 Jun 2023 13:28:02 +0200 Subject: [PATCH 22/31] Updated previews --- core/Demo/Classes/BadgeUIView_Previews.swift | 16 ++++++- core/Demo/Classes/BadgeView_Previews.swift | 4 +- .../Badge/Badge+UIPresentable.swift | 48 +------------------ 3 files changed, 17 insertions(+), 51 deletions(-) diff --git a/core/Demo/Classes/BadgeUIView_Previews.swift b/core/Demo/Classes/BadgeUIView_Previews.swift index 1c80bd744..e35ef5b8d 100644 --- a/core/Demo/Classes/BadgeUIView_Previews.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -116,8 +116,20 @@ struct BadgeUIView_Previews: PreviewProvider { var body: some View { List { - Button("Tap Me") { - viewModels[0].setBadgeValue(23) + Button("Change UIKit Badge 0 Type") { + viewModels[0].badgeType = BadgeIntentType.allCases.randomElement() ?? .alert + } + Button("Change UIKit Badge 1 Value") { + if viewModels[1].value == 10 { + viewModels[1].value = nil + } else if viewModels[1].value == 22 { + viewModels[1].value = 10 + } else { + viewModels[1].value = 22 + } + } + Button("Change UIKit Badge 2 Outline") { + viewModels[2].isBadgeOutlined.toggle() } UIBadgeView(viewModels: viewModels) .frame(height: 400) diff --git a/core/Demo/Classes/BadgeView_Previews.swift b/core/Demo/Classes/BadgeView_Previews.swift index 9433575f9..927f82029 100644 --- a/core/Demo/Classes/BadgeView_Previews.swift +++ b/core/Demo/Classes/BadgeView_Previews.swift @@ -82,10 +82,10 @@ struct BadgeView_Previews: PreviewProvider { List { Section(header: Text("SwiftUI Badge")) { Button("Change Default Badge Value") { - standartBadge.setBadgeValue(23) + standartBadge.value = 23 } Button("Change Small Custom Badge") { - smallCustomWithoutBorder.setBadgeValue(18) + smallCustomWithoutBorder.value = 18 smallCustomWithoutBorder.isBadgeOutlined = true smallCustomWithoutBorder.badgeType = .primary smallCustomWithoutBorder.badgeSize = .normal diff --git a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift index 84ae6d02b..fab63d223 100644 --- a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift +++ b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift @@ -20,53 +20,7 @@ private struct BadgePreviewFormatter: BadgeFormatting { } struct UIBadgeView: UIViewRepresentable { - - private var viewModels: [BadgeViewModel] = - [ - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .normal, - initValue: 6 - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .small, - initValue: 22, - format: .overflowCounter(maxValue: 20) - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .danger, - initValue: 10, - format: .custom( - formatter: BadgePreviewFormatter() - ) - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .info - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .neutral, - isOutlined: false - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .primary - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .secondary, - initValue: 6 - ), - BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .success - ) - ] + var viewModels: [BadgeViewModel] func makeUIView(context: Context) -> some UIView { let badgeViews = viewModels.enumerated().map { index, viewModel in From 4fea8b3e12ce8b0eb0b4ddfd583f3e9d1b8212e4 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Mon, 5 Jun 2023 16:25:29 +0200 Subject: [PATCH 23/31] Fixed BadgeView with theme update --- core/Demo/Classes/BadgeUIView_Previews.swift | 26 ++-- core/Demo/Classes/BadgeView_Previews.swift | 126 ++++++++-------- .../Badge/Properties/Public/BadgeSize.swift | 9 ++ .../Badge/View/SwiftUI/BadgeView.swift | 47 ++++-- .../Badge/View/SwiftUI/BadgeViewTests.swift | 73 +++++----- .../Badge/View/UIKit/BadgeUIView.swift | 40 ++++-- .../Badge/View/UIKit/BadgeUIViewTests.swift | 8 +- .../Badge/ViewModel/BadgeViewModel.swift | 104 ++++---------- .../Badge/ViewModel/BadgeViewModelTests.swift | 6 +- .../Components/Badge/BadgeComponentView.swift | 135 +++++++++--------- 10 files changed, 283 insertions(+), 291 deletions(-) diff --git a/core/Demo/Classes/BadgeUIView_Previews.swift b/core/Demo/Classes/BadgeUIView_Previews.swift index e35ef5b8d..9e7f33ffd 100644 --- a/core/Demo/Classes/BadgeUIView_Previews.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -58,11 +58,11 @@ struct UIBadgeView: UIViewRepresentable { badgesStackView.alignment = .leading badgesStackView.spacing = 30 badgesStackView.distribution = .fill + return badgesStackView } func updateUIView(_ uiView: UIViewType, context: Context) { - } } @@ -73,40 +73,41 @@ struct BadgeUIView_Previews: PreviewProvider { BadgeViewModel( theme: SparkTheme.shared, badgeType: .alert, - initValue: 6 + value: 6 ), BadgeViewModel( theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .small, - initValue: 22, + badgeType: .primary, + badgeSize: .normal, + value: 22, format: .overflowCounter(maxValue: 20) ), BadgeViewModel( theme: SparkTheme.shared, badgeType: .danger, - initValue: 10, + value: 10, format: .custom( formatter: BadgePreviewFormatter() ) ), BadgeViewModel( theme: SparkTheme.shared, - badgeType: .info + badgeType: .info, + value: 20 ), BadgeViewModel( theme: SparkTheme.shared, - badgeType: .neutral, - isOutlined: false + badgeType: .primary ), BadgeViewModel( theme: SparkTheme.shared, - badgeType: .primary + badgeType: .neutral, + isOutlined: false ), BadgeViewModel( theme: SparkTheme.shared, badgeType: .secondary, - initValue: 23 + value: 23 ), BadgeViewModel( theme: SparkTheme.shared, @@ -131,6 +132,9 @@ struct BadgeUIView_Previews: PreviewProvider { Button("Change UIKit Badge 2 Outline") { viewModels[2].isBadgeOutlined.toggle() } + Button("Change UIKit Badge 3 Size") { + viewModels[3].badgeSize = .small + } UIBadgeView(viewModels: viewModels) .frame(height: 400) .listRowBackground(Color.gray.opacity(0.3)) diff --git a/core/Demo/Classes/BadgeView_Previews.swift b/core/Demo/Classes/BadgeView_Previews.swift index 927f82029..a9eab8b6c 100644 --- a/core/Demo/Classes/BadgeView_Previews.swift +++ b/core/Demo/Classes/BadgeView_Previews.swift @@ -21,60 +21,20 @@ private struct BadgePreviewFormatter: BadgeFormatting { struct BadgeView_Previews: PreviewProvider { struct BadgeContainerView: View { - - @State var standartBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .normal, - initValue: 6 - ) - @State var smallCustomWithoutBorder = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .small, - initValue: 22, - format: .overflowCounter(maxValue: 10), - isOutlined: false - ) + @State var theme: Theme = SparkTheme.shared - @State var standartDangerBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .danger, - initValue: 10, - format: .custom( - formatter: BadgePreviewFormatter() - ) - ) + @State var standartBadgeValue: Int? = 3 + @State var standartBadgeIsOutlined: Bool = true - @State var standartInfoBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .info - ) + @State var smallCustomBadgeValue: Int? = 14 + @State var smallCustomBadgeSize: BadgeSize = .small + @State var smallCustomBadgeIsOutlined: Bool = true + @State var smallCustomBadgeType: BadgeIntentType = .alert + @State var badgeFormat: BadgeFormat = .default - @State var standartNeutralBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .neutral, - isOutlined: false - ) + @State var standartDangerBadgeType: BadgeIntentType = .danger - @State var standartPrimaryBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .primary - ) - - @State var standartSecondaryBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .secondary - ) - - @State var standartSuccessBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .success - ) - - @State var value: Int? = 3 - @State var isOutlined: Bool = false @ScaledMetric var hOffset: CGFloat @ScaledMetric var vOffset: CGFloat @@ -82,45 +42,72 @@ struct BadgeView_Previews: PreviewProvider { List { Section(header: Text("SwiftUI Badge")) { Button("Change Default Badge Value") { - standartBadge.value = 23 + standartBadgeValue = 23 + standartBadgeIsOutlined.toggle() + badgeFormat = .overflowCounter(maxValue: 20) } Button("Change Small Custom Badge") { - smallCustomWithoutBorder.value = 18 - smallCustomWithoutBorder.isBadgeOutlined = true - smallCustomWithoutBorder.badgeType = .primary - smallCustomWithoutBorder.badgeSize = .normal + smallCustomBadgeValue = 18 + smallCustomBadgeSize = .normal + smallCustomBadgeIsOutlined.toggle() + smallCustomBadgeType = .primary } Button("Change Dange Badge") { - standartDangerBadge.badgeType = .neutral + standartDangerBadgeType = .neutral } VStack(spacing: 100) { HStack(spacing: 50) { ZStack(alignment: .leading) { Text("Default Badge") - BadgeView(viewModel: standartBadge) - .offset(x: 100, y: -15) + BadgeView( + theme: theme, + badgeType: .primary, + value: standartBadgeValue + ) + .format(badgeFormat) + .outlined(standartBadgeIsOutlined) + .offset(x: 100, y: -15) } ZStack(alignment: .leading) { Text("Small Custom") - BadgeView(viewModel: smallCustomWithoutBorder) - .offset(x: 100, y: -15) + BadgeView( + theme: SparkTheme.shared, + badgeType: smallCustomBadgeType, + value: 22 + ) + .outlined(smallCustomBadgeIsOutlined) + .size(smallCustomBadgeSize) + .offset(x: 100, y: -15) } } HStack(spacing: 55) { ZStack(alignment: .leading) { Text("Danger Badge") - BadgeView(viewModel: standartDangerBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: standartDangerBadgeType, + value: 10 + ) + .format(.custom( + formatter: BadgePreviewFormatter() + )) .offset(x: 100, y: -15) } ZStack(alignment: .leading) { Text("Text") - BadgeView(viewModel: standartInfoBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .info + ) .offset(x: 25, y: -15) } ZStack(alignment: .leading) { Text("Text") - BadgeView(viewModel: standartNeutralBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .neutral + ) .offset(x: 25, y: -15) } } @@ -128,15 +115,24 @@ struct BadgeView_Previews: PreviewProvider { HStack(spacing: 50) { HStack { Text("Text") - BadgeView(viewModel: standartPrimaryBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .primary + ) } HStack { Text("Text") - BadgeView(viewModel: standartSecondaryBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .secondary + ) } HStack { Text("Text") - BadgeView(viewModel: standartSuccessBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .success + ) } } } diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift index 6f98af8cc..e282d81ba 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift @@ -16,4 +16,13 @@ import Foundation public enum BadgeSize { case normal case small + + func fontSize(for theme: Theme) -> TypographyFontToken { + switch self { + case .normal: + return theme.typography.captionHighlight + case .small: + return theme.typography.smallHighlight + } + } } diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index ec6198479..e3c6f6307 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -11,26 +11,20 @@ import SwiftUI /// This is SwiftUI badge view to show notifications count /// /// Badge view is created by passing: -/// - **Theme** -/// - ``BadgeViewModel`` +/// - ``Theme`` +/// - ``BadgeIntentType`` +/// - Badge Value as ``Int?``. You can set value to nil, to make ``BadgeView`` without text /// +/// Badge border and offsets of it's text are ``@ScaledMetric`` variables and alligned to user's **Accessibility** +/// /// **Example** /// This example shows how to create view with horizontal alignment of Badge /// ```swift -/// @StateObject var viewModel = BadgeViewModel( -/// theme: SparkTheme.shared, -/// badgeType: .alert, -/// badgeSize: .normal, -/// initValue: 0 -/// ) /// @State var value: Int? = 3 /// var body: any View { -/// Button("Change Notifications Number") { -/// viewModel.setBadgeValue(5) -/// } /// HStack { /// Text("Some text") -/// BadgeView(viewModel) +/// BadgeView(theme: YourTheme.shared, badgeType: .alert, value: value) /// } /// } /// ``` @@ -68,7 +62,8 @@ public struct BadgeView: View { } } - public init(viewModel: BadgeViewModel) { + public init(theme: Theme, badgeType: BadgeIntentType, value: Int? = nil) { + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeType, value: value) self.viewModel = viewModel self._smallOffset = @@ -82,4 +77,30 @@ public struct BadgeView: View { self._emptySize = .init(wrappedValue: BadgeConstants.emptySize.width) self._borderWidth = .init(wrappedValue: viewModel.badgeBorder.width) } + + /// Controll outline state of the Badge. By default Badge has an outline + /// base on current ``Theme``. You can show/hide the outline with + /// this function. Also, for example, you can use @State variable to control outline + /// based on this variable. + public func outlined(_ isOutlined: Bool) -> Self { + self.viewModel.isBadgeOutlined = isOutlined + return self + } + + /// Controll text size of the Badge. By default size of the ``BadgeSize`` is ``BadgeSize.normal`` + /// Text font size is based on ``BadgeSize`` value and current ``Theme``. + /// You can set ``BadgeSize`` with this function. + /// Also, for example, you can use @State variable to control ``BadgeSize`` based on this variable. + public func size(_ badgeSize: BadgeSize) -> Self { + self.viewModel.badgeSize = badgeSize + return self + } + + /// Controll text format of the Badge. See more details in ``BadgeFormat`` + /// You can set ``BadgeFormat`` with this function. + /// Also, for example, you can use @State variable to control ``BadgeFormat`` based on this variable. + public func format(_ badgeFormat: BadgeFormat) -> Self { + self.viewModel.badgeFormat = badgeFormat + return self + } } diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift index d38cf18b5..162566c6d 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeViewTests.swift @@ -28,11 +28,8 @@ final class BadgeViewTests: SwiftUIComponentTestCase { func test_badge_all_cases_no_text() throws { for badgeIntentType in BadgeIntentType.allCases { - let view = BadgeView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType) - ).fixedSize() + let view = BadgeView(theme: theme, badgeType: badgeIntentType) + .fixedSize() assertSnapshotInDarkAndLight(matching: view, named: "test_badge_\(badgeIntentType)") } @@ -41,11 +38,9 @@ final class BadgeViewTests: SwiftUIComponentTestCase { func test_badge_all_cases_text() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - initValue: 23 - ) + theme: theme, + badgeType: badgeIntentType, + value: 23 ).fixedSize() assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)") @@ -55,13 +50,12 @@ final class BadgeViewTests: SwiftUIComponentTestCase { func test_badge_all_cases_text_smal_size() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - badgeSize: .small, - initValue: 23 - ) - ).fixedSize() + theme: theme, + badgeType: badgeIntentType, + value: 23 + ) + .size(.small) + .fixedSize() assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)_small_size") } @@ -70,13 +64,12 @@ final class BadgeViewTests: SwiftUIComponentTestCase { func test_badge_all_cases_text_overflow_format() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - initValue: 23, - format: .overflowCounter(maxValue: 20) - ) - ).fixedSize() + theme: theme, + badgeType: badgeIntentType, + value: 23 + ) + .format(.overflowCounter(maxValue: 20)) + .fixedSize() assertSnapshotInDarkAndLight(matching: view, named: "test_badge_overflow_format_text_\(badgeIntentType)") } @@ -85,15 +78,14 @@ final class BadgeViewTests: SwiftUIComponentTestCase { func test_badge_all_cases_text_custom_format() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - initValue: 23, - format: .custom( - formatter: TestBadgeFormatting() - ) - ) - ).fixedSize() + theme: theme, + badgeType: badgeIntentType, + value: 23 + ) + .format(.custom( + formatter: TestBadgeFormatting() + )) + .fixedSize() assertSnapshotInDarkAndLight(matching: view, named: "test_badge_custom_format_text_\(badgeIntentType)") } @@ -102,14 +94,13 @@ final class BadgeViewTests: SwiftUIComponentTestCase { func test_badge_all_cases_no_text_custom_format() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - format: .custom( - formatter: TestBadgeFormatting() - ) - ) - ).fixedSize() + theme: theme, + badgeType: badgeIntentType + ) + .format(.custom( + formatter: TestBadgeFormatting() + )) + .fixedSize() assertSnapshotInDarkAndLight(matching: view, named: "test_badge_custom_format_no_text_\(badgeIntentType)") } diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index b36425310..806f4c9cc 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -84,14 +84,6 @@ public class BadgeUIView: UIView { } private func subscribe() { - self.viewModel.$badgeSize - .receive(on: DispatchQueue.main) - .sink { [weak self] badgeSize in - self?.reloadBadgeFontIfNeeded() - self?.reloadUISize() - self?.setupLayouts() - } - .store(in: &cancellables) self.viewModel.$value .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -100,6 +92,7 @@ public class BadgeUIView: UIView { self?.setupLayouts() } .store(in: &cancellables) + self.viewModel.$badgeType .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -108,6 +101,7 @@ public class BadgeUIView: UIView { self?.setupLayouts() } .store(in: &cancellables) + self.viewModel.$isBadgeOutlined .receive(on: DispatchQueue.main) .sink { [weak self] isBadgeOutlined in @@ -118,6 +112,35 @@ public class BadgeUIView: UIView { } } .store(in: &cancellables) + + self.viewModel.$badgeSize + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.reloadBadgeFontIfNeeded() + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + + self.viewModel.$badgeFormat + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.badgeLabel.text = self?.viewModel.text + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + + self.viewModel.$theme + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.viewModel.updateScalings() + self?.reloadColors() + self?.reloadBadgeFontIfNeeded() + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) } private func setupBadgeText() { @@ -190,6 +213,7 @@ public class BadgeUIView: UIView { // MARK: - Updates on Trait Collection Change private func reloadColors() { + self.viewModel.updateColors() self.backgroundColor = self.viewModel.backgroundColor.uiColor badgeLabel.textColor = self.viewModel.textColor.uiColor self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift index 74abc92cc..e59a8084c 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift @@ -43,7 +43,7 @@ final class BadgeUIViewTests: UIKitComponentTestCase { viewModel: BadgeViewModel( theme: theme, badgeType: badgeIntentType, - initValue: 23 + value: 23 ) ) @@ -58,7 +58,7 @@ final class BadgeUIViewTests: UIKitComponentTestCase { theme: theme, badgeType: badgeIntentType, badgeSize: .small, - initValue: 23 + value: 23 ) ) @@ -72,7 +72,7 @@ final class BadgeUIViewTests: UIKitComponentTestCase { viewModel: BadgeViewModel( theme: theme, badgeType: badgeIntentType, - initValue: 23, + value: 23, format: .overflowCounter(maxValue: 20) ) ) @@ -87,7 +87,7 @@ final class BadgeUIViewTests: UIKitComponentTestCase { viewModel: BadgeViewModel( theme: theme, badgeType: badgeIntentType, - initValue: 23, + value: 23, format: .custom( formatter: TestBadgeFormatting() ) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 16b0d4fcc..2a7f1e6df 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -10,87 +10,39 @@ import UIKit import Combine import SwiftUI -/// **BadgeViewModel** is a view model that is required for -/// configuring ``BadgeView`` and changing it's properties. -/// -/// **Initializer** -/// - A theme -- app theme -/// - Badge type -- intent type of Badge see ``BadgeIntentType`` -/// - Badge size -- see ``BadgeSize`` -/// - Initial value -- Value that should be set on view creation -/// - Formatter -- see ``BadgeFormat`` -/// - isOutlined -- property to show or hide border -/// -/// List of properties: -/// - value -- property that represents **Int** displayed in ``BadgeView`` -/// - text -- property that represents text in ``BadgeView``. Appearance of it -/// is configured via ``BadgeFormat`` and based on **value** property. -/// - textColor -- property for coloring text -/// - backgroundColor -- changes color of ``BadgeView`` and based on ``BadgeIntentType`` -/// - verticalOffset & horizontalOffset -- are offsets of **text** inside of ``BadgeView`` -/// - badgeBorder -- is property that helps you to configure ``BadgeView`` with -/// border radius, width and color. See ``BadgeBorder`` -/// - theme is representer of **Theme** used in the app -/// - badgeFormat -- see ``BadgeFormat`` as a formatter of **text** public final class BadgeViewModel: ObservableObject { - // MARK: - Text Properties - public var text: String - var textFont: TypographyFontToken - var textColor: ColorToken - - // MARK: - Appearance Public Properties - @Published public var value: Int? = nil { - didSet { - guard oldValue != value else { - return - } - - self.updateBadgeValue() - } - } - @Published public var badgeSize: BadgeSize { - didSet { - guard oldValue != badgeSize else { - return - } - - self.reloadSize() - } - } - @Published public var badgeType: BadgeIntentType { - didSet { - guard oldValue != badgeType else { - return - } - - self.reloadColors() - } - } + // MARK: - Appearance Internal Properties + @Published public var value: Int? = nil + @Published public var badgeType: BadgeIntentType @Published public var isBadgeOutlined: Bool + @Published public var badgeSize: BadgeSize + @Published public var badgeFormat: BadgeFormat + @Published public var theme: Theme - // MARK: - Appearance Internal Properties var backgroundColor: ColorToken var badgeBorder: BadgeBorder - var theme: Theme var isBadgeEmpty: Bool { self.badgeFormat.badgeText(value).isEmpty } - var verticalOffset: CGFloat var horizontalOffset: CGFloat - // MARK: - Appearance Private Properties - private var badgeFormat: BadgeFormat + // MARK: - Internal Text Properties + var text: String { + badgeFormat.badgeText(value) + } + var textFont: TypographyFontToken { + badgeSize.fontSize(for: self.theme) + } + var textColor: ColorToken // MARK: - Initializer - public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, initValue: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { + public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeType, on: theme.colors) - self.value = initValue - self.text = format.badgeText(initValue) - self.textFont = badgeSize == .normal ? theme.typography.captionHighlight : theme.typography.smallHighlight + self.value = value self.textColor = badgeColors.foregroundColor self.backgroundColor = badgeColors.backgroundColor @@ -115,18 +67,8 @@ public final class BadgeViewModel: ObservableObject { self.isBadgeOutlined = isOutlined } - // MARK: - Badge update functions - - private func updateBadgeValue() { - self.text = badgeFormat.badgeText(value) - } - - private func reloadSize() { - self.textFont = self.badgeSize == .normal ? self.theme.typography.captionHighlight : self.theme.typography.smallHighlight - } - - private func reloadColors() { - let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeType, on: theme.colors) + func updateColors() { + let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: self.badgeType, on: self.theme.colors) self.textColor = badgeColors.foregroundColor @@ -134,4 +76,14 @@ public final class BadgeViewModel: ObservableObject { self.badgeBorder.setColor(badgeColors.borderColor) } + + func updateScalings() { + let verticalOffset = self.theme.layout.spacing.small + let horizontalOffset = self.theme.layout.spacing.medium + + self.verticalOffset = verticalOffset + self.horizontalOffset = horizontalOffset + + self.badgeBorder.setWidth(self.theme.border.width.medium) + } } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index 1c29d6663..a369d8956 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -40,7 +40,7 @@ final class BadgeViewModelTests: XCTestCase { let expectedInitText = "20" let expectedUpdatedText = "233" - let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, value: 20) // Then @@ -62,7 +62,7 @@ final class BadgeViewModelTests: XCTestCase { for badgeIntentType in BadgeIntentType.allCases { // Given - let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, value: 20) // Then @@ -80,7 +80,7 @@ final class BadgeViewModelTests: XCTestCase { for badgeIntentType in BadgeIntentType.allCases { // Given - let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, initValue: 20) + let viewModel = BadgeViewModel(theme: theme, badgeType: badgeIntentType, value: 20) // Then diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index a1ea8df5e..e544fa2fb 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -27,19 +27,19 @@ struct BadgeComponentView: View { theme: SparkTheme.shared, badgeType: .alert, badgeSize: .normal, - initValue: 6 + value: 6 ), BadgeViewModel( theme: SparkTheme.shared, badgeType: .alert, badgeSize: .small, - initValue: 22, + value: 22, format: .overflowCounter(maxValue: 20) ), BadgeViewModel( theme: SparkTheme.shared, badgeType: .danger, - initValue: 10, + value: 10, format: .custom( formatter: BadgePreviewFormatter() ) @@ -60,7 +60,7 @@ struct BadgeComponentView: View { BadgeViewModel( theme: SparkTheme.shared, badgeType: .secondary, - initValue: 6 + value: 6 ), BadgeViewModel( theme: SparkTheme.shared, @@ -68,59 +68,18 @@ struct BadgeComponentView: View { ) ] - @StateObject var standartBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .normal, - initValue: 6 - ) + @State var theme: Theme = SparkTheme.shared - @State var smallCustomWithoutBorder = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .small, - initValue: 22, - format: .overflowCounter(maxValue: 20), - isOutlined: false - ) + @State var standartBadgeValue: Int? = 3 + @State var standartBadgeIsOutlined: Bool = true - @State var standartDangerBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .danger, - initValue: 10, - format: .custom( - formatter: BadgePreviewFormatter() - ) - ) - - @State var standartInfoBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .info - ) - - @State var standartNeutralBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .neutral, - isOutlined: false - ) - - @State var standartPrimaryBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .primary - ) + @State var smallCustomBadgeValue: Int? = 14 + @State var smallCustomBadgeSize: BadgeSize = .small + @State var smallCustomBadgeIsOutlined: Bool = true + @State var smallCustomBadgeType: BadgeIntentType = .alert + @State var badgeFormat: BadgeFormat = .default - @State var standartSecondaryBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .secondary - ) - - @State var standartSuccessBadge = BadgeViewModel( - theme: SparkTheme.shared, - badgeType: .success - ) - - @State var value: Int? = 3 - @State var isOutlined: Bool = false + @State var standartDangerBadgeType: BadgeIntentType = .danger var body: some View { List { @@ -147,45 +106,72 @@ struct BadgeComponentView: View { Section(header: Text("SwiftUI Badge")) { Button("Change Default Badge Value") { - standartBadge.value = 23 + standartBadgeValue = 23 + standartBadgeIsOutlined.toggle() + badgeFormat = .overflowCounter(maxValue: 20) } Button("Change Small Custom Badge") { - smallCustomWithoutBorder.value = 18 - smallCustomWithoutBorder.isBadgeOutlined = true - smallCustomWithoutBorder.badgeType = .primary - smallCustomWithoutBorder.badgeSize = .normal + smallCustomBadgeValue = 18 + smallCustomBadgeSize = .normal + smallCustomBadgeIsOutlined.toggle() + smallCustomBadgeType = .primary } Button("Change Dange Badge") { - standartDangerBadge.badgeType = .neutral + standartDangerBadgeType = .neutral } VStack(spacing: 100) { HStack(spacing: 50) { ZStack(alignment: .leading) { Text("Default Badge") - BadgeView(viewModel: standartBadge) - .offset(x: 100, y: -15) + BadgeView( + theme: theme, + badgeType: .primary, + value: standartBadgeValue + ) + .format(badgeFormat) + .outlined(standartBadgeIsOutlined) + .offset(x: 100, y: -15) } ZStack(alignment: .leading) { Text("Small Custom") - BadgeView(viewModel: smallCustomWithoutBorder) - .offset(x: 100, y: -15) + BadgeView( + theme: SparkTheme.shared, + badgeType: smallCustomBadgeType, + value: 22 + ) + .outlined(smallCustomBadgeIsOutlined) + .size(smallCustomBadgeSize) + .offset(x: 100, y: -15) } } HStack(spacing: 55) { ZStack(alignment: .leading) { Text("Danger Badge") - BadgeView(viewModel: standartDangerBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: standartDangerBadgeType, + value: 10 + ) + .format(.custom( + formatter: BadgePreviewFormatter() + )) .offset(x: 100, y: -15) } ZStack(alignment: .leading) { Text("Text") - BadgeView(viewModel: standartInfoBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .info + ) .offset(x: 25, y: -15) } ZStack(alignment: .leading) { Text("Text") - BadgeView(viewModel: standartNeutralBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .neutral + ) .offset(x: 25, y: -15) } } @@ -193,15 +179,24 @@ struct BadgeComponentView: View { HStack(spacing: 50) { HStack { Text("Text") - BadgeView(viewModel: standartPrimaryBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .primary + ) } HStack { Text("Text") - BadgeView(viewModel: standartSecondaryBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .secondary + ) } HStack { Text("Text") - BadgeView(viewModel: standartSuccessBadge) + BadgeView( + theme: SparkTheme.shared, + badgeType: .success + ) } } } From 8de076c20144ed2b0af92a10f3eb3efbd01f31e1 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Mon, 5 Jun 2023 16:56:09 +0200 Subject: [PATCH 24/31] Documentation updated --- .../Badge/View/SwiftUI/BadgeView.swift | 32 +++++++++---------- .../Badge/ViewModel/BadgeViewModel.swift | 16 ++++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index e3c6f6307..1be16fb37 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -10,12 +10,7 @@ import SwiftUI /// This is SwiftUI badge view to show notifications count /// -/// Badge view is created by passing: -/// - ``Theme`` -/// - ``BadgeIntentType`` -/// - Badge Value as ``Int?``. You can set value to nil, to make ``BadgeView`` without text -/// -/// Badge border and offsets of it's text are ``@ScaledMetric`` variables and alligned to user's **Accessibility** +/// Badge border and offsets of it's text are **@ScaledMetric** variables and alligned to user's **Accessibility** /// /// **Example** /// This example shows how to create view with horizontal alignment of Badge @@ -62,6 +57,9 @@ public struct BadgeView: View { } } + /// - Parameter theme: ``Theme`` + /// - Parameter badgeType: ``BadgeIntentType`` + /// - Parameter value: **Int?** You can set value to nil, to make ``BadgeView`` without text public init(theme: Theme, badgeType: BadgeIntentType, value: Int? = nil) { let viewModel = BadgeViewModel(theme: theme, badgeType: badgeType, value: value) self.viewModel = viewModel @@ -78,27 +76,29 @@ public struct BadgeView: View { self._borderWidth = .init(wrappedValue: viewModel.badgeBorder.width) } - /// Controll outline state of the Badge. By default Badge has an outline - /// base on current ``Theme``. You can show/hide the outline with - /// this function. Also, for example, you can use @State variable to control outline - /// based on this variable. + // MARK: - Badge Modification Functions + + /// Controlls outline state of the Badge. + /// By default Badge has an outline based on current ``Theme``. + /// + /// Use @State variable to control outline based on this variable. public func outlined(_ isOutlined: Bool) -> Self { self.viewModel.isBadgeOutlined = isOutlined return self } - /// Controll text size of the Badge. By default size of the ``BadgeSize`` is ``BadgeSize.normal`` + /// Controlls text size of the Badge. By ``BadgeSize`` is *.normal*. + /// /// Text font size is based on ``BadgeSize`` value and current ``Theme``. - /// You can set ``BadgeSize`` with this function. - /// Also, for example, you can use @State variable to control ``BadgeSize`` based on this variable. + /// Use @State variable to control ``BadgeSize`` based on this variable. public func size(_ badgeSize: BadgeSize) -> Self { self.viewModel.badgeSize = badgeSize return self } - /// Controll text format of the Badge. See more details in ``BadgeFormat`` - /// You can set ``BadgeFormat`` with this function. - /// Also, for example, you can use @State variable to control ``BadgeFormat`` based on this variable. + /// Controlls text format of the Badge. See more details in ``BadgeFormat``. + /// + /// Use @State variable to control ``BadgeFormat`` based on this variable. public func format(_ badgeFormat: BadgeFormat) -> Self { self.viewModel.badgeFormat = badgeFormat return self diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 2a7f1e6df..a8a5ab76d 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -10,6 +10,22 @@ import UIKit import Combine import SwiftUI +/// **BadgeViewModel** is a view model that required for +/// configuring ``BadgeView`` and changing it's properties. +/// +/// List of properties: +/// - value -- property that represents **Int?** displayed in ``BadgeView``. +/// Set *value* to nil to show empty Badge as circle +/// +/// - badgeType -- changes ``BadgeIntentType`` +/// +/// - isBadgeOutlined -- ``Bool``, changes outline of the Badge +/// +/// - badgeSize -- changes ``BadgeSize`` and text font +/// +/// - badgeFormat -- see ``BadgeFormat`` as a formater of **Badge Text** +/// +/// - theme -- represents ``Theme`` used in the app public final class BadgeViewModel: ObservableObject { // MARK: - Appearance Internal Properties From 11fe53f71be907b8ef193182042ad6499999beee Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Mon, 5 Jun 2023 16:59:45 +0200 Subject: [PATCH 25/31] Fixed typo --- core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index a8a5ab76d..c1a767642 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -10,7 +10,7 @@ import UIKit import Combine import SwiftUI -/// **BadgeViewModel** is a view model that required for +/// **BadgeViewModel** is a view model that is required for /// configuring ``BadgeView`` and changing it's properties. /// /// List of properties: From 314c7080b5c7796ccc2b20a411ba16853fc211ff Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 7 Jun 2023 11:45:22 +0200 Subject: [PATCH 26/31] Updated badge view model and badge view --- .../Badge/Properties/Public/BadgeSize.swift | 9 - .../Badge/View/UIKit/BadgeUIView.swift | 184 +++++++++++------- .../Badge/ViewModel/BadgeViewModel.swift | 98 +++++++--- 3 files changed, 188 insertions(+), 103 deletions(-) diff --git a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift index e282d81ba..6f98af8cc 100644 --- a/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift +++ b/core/Sources/Components/Badge/Properties/Public/BadgeSize.swift @@ -16,13 +16,4 @@ import Foundation public enum BadgeSize { case normal case small - - func fontSize(for theme: Theme) -> TypographyFontToken { - switch self { - case .normal: - return theme.typography.captionHighlight - case .small: - return theme.typography.smallHighlight - } - } } diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index 806f4c9cc..e1048b512 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -61,8 +61,8 @@ public class BadgeUIView: UIView { // MARK: - Init - public init(viewModel: BadgeViewModel) { - self.viewModel = viewModel + public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { + self.viewModel = BadgeViewModel(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isOutlined: isOutlined) super.init(frame: .zero) @@ -83,64 +83,11 @@ public class BadgeUIView: UIView { subscribe() } - private func subscribe() { - self.viewModel.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.badgeLabel.text = self?.viewModel.text - self?.reloadUISize() - self?.setupLayouts() - } - .store(in: &cancellables) - - self.viewModel.$badgeType - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.reloadColors() - self?.reloadUISize() - self?.setupLayouts() - } - .store(in: &cancellables) - - self.viewModel.$isBadgeOutlined - .receive(on: DispatchQueue.main) - .sink { [weak self] isBadgeOutlined in - if isBadgeOutlined { - self?.reloadBorderWidth() - } else { - self?.layer.borderWidth = 0 - } - } - .store(in: &cancellables) - - self.viewModel.$badgeSize - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.reloadBadgeFontIfNeeded() - self?.reloadUISize() - self?.setupLayouts() - } - .store(in: &cancellables) - - self.viewModel.$badgeFormat - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.badgeLabel.text = self?.viewModel.text - self?.reloadUISize() - self?.setupLayouts() - } - .store(in: &cancellables) - - self.viewModel.$theme - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.viewModel.updateScalings() - self?.reloadColors() - self?.reloadBadgeFontIfNeeded() - self?.reloadUISize() - self?.setupLayouts() - } - .store(in: &cancellables) + private func setupScalables() { + self.emptyBadgeSize = BadgeConstants.emptySize.width + self.horizontalSpacing = self.viewModel.horizontalOffset + self.verticalSpacing = self.viewModel.verticalOffset + self.borderWidth = self.viewModel.badgeBorder.width } private func setupBadgeText() { @@ -162,13 +109,6 @@ public class BadgeUIView: UIView { self.clipsToBounds = true } - private func setupScalables() { - self.emptyBadgeSize = BadgeConstants.emptySize.width - self.horizontalSpacing = self.viewModel.horizontalOffset - self.verticalSpacing = self.viewModel.verticalOffset - self.borderWidth = self.viewModel.badgeBorder.width - } - // MARK: - Layouts setup private func setupLayouts() { @@ -209,11 +149,90 @@ public class BadgeUIView: UIView { self.layer.cornerRadius = min(frame.width, frame.height) / 2.0 } +} + +// MARK: - Badge Subscribers +extension BadgeUIView { + private func subscribe() { + self.subscribeToTextChanges() + self.subscribeToBorderChanges() + self.subscribeToColorChanges() + } - // MARK: - Updates on Trait Collection Change + private func subscribeToTextChanges() { + self.viewModel.$text + .receive(on: DispatchQueue.main) + .sink { [weak self] text in + self?.badgeLabel.text = text + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + self.viewModel.$textFont + .receive(on: DispatchQueue.main) + .sink { [weak self] textFont in + self?.badgeLabel.font = textFont.uiFont + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + self.viewModel.$isBadgeEmpty + .receive(on: DispatchQueue.main) + .sink { [weak self] isBadgeOutlined in + self?.badgeLabel.text = self?.viewModel.text + self?.reloadUISize() + self?.setupLayouts() + } + .store(in: &cancellables) + } + + private func subscribeToBorderChanges() { + self.viewModel.$isBadgeOutlined + .receive(on: DispatchQueue.main) + .sink { [weak self] isBadgeOutlined in + guard let self else { + return + } + self.updateBorder(self.viewModel.badgeBorder) + } + .store(in: &cancellables) + self.viewModel.$badgeBorder + .receive(on: DispatchQueue.main) + .sink { [weak self] badgeBorder in + self?.updateBorder(badgeBorder) + } + .store(in: &cancellables) + } + + private func subscribeToColorChanges() { + self.viewModel.$textColor + .receive(on: DispatchQueue.main) + .sink { [weak self] textColor in + self?.badgeLabel.textColor = textColor.uiColor + } + .store(in: &cancellables) + self.viewModel.$backgroundColor + .receive(on: DispatchQueue.main) + .sink { [weak self] backgroundColor in + self?.backgroundColor = backgroundColor.uiColor + } + .store(in: &cancellables) + } +} + +// MARK: - Updates on Trait Collection Change +extension BadgeUIView { + private func updateBorder(_ badgeBorder: BadgeBorder) { + self.layer.borderColor = badgeBorder.color.uiColor.cgColor + if self.viewModel.isBadgeOutlined { + self.setupScalables() + self.reloadBorderWidth() + } else { + self.layer.borderWidth = 0 + } + } private func reloadColors() { - self.viewModel.updateColors() self.backgroundColor = self.viewModel.backgroundColor.uiColor badgeLabel.textColor = self.viewModel.textColor.uiColor self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor @@ -253,3 +272,30 @@ public class BadgeUIView: UIView { self.setupLayouts() } } + +// MARK: - Badge Update Functions +public extension BadgeUIView { + func setBadgeType(_ badgeType: BadgeIntentType) { + self.viewModel.badgeType = badgeType + } + + func setBadgeOutlineEnabled(_ isOutlined: Bool) { + self.viewModel.isBadgeOutlined = isOutlined + } + + func setBadgeValue(_ value: Int?) { + self.viewModel.value = value + } + + func setBadgeFormat(_ format: BadgeFormat) { + self.viewModel.badgeFormat = format + } + + func setBadgeSize(_ badgeSize: BadgeSize) { + self.viewModel.badgeSize = badgeSize + } + + func setBadgeTheme(_ theme: Theme) { + self.viewModel.theme = theme + } +} diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index c1a767642..ac92bc472 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -28,37 +28,66 @@ import SwiftUI /// - theme -- represents ``Theme`` used in the app public final class BadgeViewModel: ObservableObject { - // MARK: - Appearance Internal Properties - @Published public var value: Int? = nil - @Published public var badgeType: BadgeIntentType - @Published public var isBadgeOutlined: Bool - @Published public var badgeSize: BadgeSize - @Published public var badgeFormat: BadgeFormat - @Published public var theme: Theme - - var backgroundColor: ColorToken - var badgeBorder: BadgeBorder - var isBadgeEmpty: Bool { - self.badgeFormat.badgeText(value).isEmpty + // MARK: - Badge Configuration Public Properties + public var value: Int? = nil { + didSet { + updateText() + } + } + public var badgeType: BadgeIntentType { + didSet { + updateColors() + } + } + public var badgeSize: BadgeSize { + didSet { + updateFont() + } + } + public var badgeFormat: BadgeFormat { + didSet { + updateText() + } + } + public var theme: Theme { + didSet { + updateColors() + updateScalings() + } } + + // MARK: - Internal Published Properties + @Published var text: String + @Published var textFont: TypographyFontToken + @Published var textColor: ColorToken + @Published var isBadgeEmpty: Bool + + @Published var backgroundColor: ColorToken + @Published var badgeBorder: BadgeBorder + + @Published public var isBadgeOutlined: Bool + + // MARK: - Internal Appearance Properties + var colorsUseCase: BadgeGetIntentColorsUseCaseable var verticalOffset: CGFloat var horizontalOffset: CGFloat - // MARK: - Internal Text Properties - var text: String { - badgeFormat.badgeText(value) - } - var textFont: TypographyFontToken { - badgeSize.fontSize(for: self.theme) - } - var textColor: ColorToken // MARK: - Initializer - public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { - let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: badgeType, on: theme.colors) + init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true, colorsUseCase: BadgeGetIntentColorsUseCaseable) { + let badgeColors = colorsUseCase.execute(intentType: badgeType, on: theme.colors) self.value = value + + self.text = format.badgeText(value) + self.isBadgeEmpty = format.badgeText(value).isEmpty + switch badgeSize { + case .normal: + self.textFont = theme.typography.captionHighlight + case .small: + self.textFont = theme.typography.smallHighlight + } self.textColor = badgeColors.foregroundColor self.backgroundColor = badgeColors.backgroundColor @@ -81,10 +110,15 @@ public final class BadgeViewModel: ObservableObject { self.badgeSize = badgeSize self.badgeType = badgeType self.isBadgeOutlined = isOutlined + self.colorsUseCase = colorsUseCase + } + + convenience public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { + self.init(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isOutlined: isOutlined, colorsUseCase: BadgeGetIntentColorsUseCase()) } - func updateColors() { - let badgeColors = BadgeGetIntentColorsUseCase().execute(intentType: self.badgeType, on: self.theme.colors) + private func updateColors() { + let badgeColors = self.colorsUseCase.execute(intentType: self.badgeType, on: self.theme.colors) self.textColor = badgeColors.foregroundColor @@ -93,7 +127,21 @@ public final class BadgeViewModel: ObservableObject { self.badgeBorder.setColor(badgeColors.borderColor) } - func updateScalings() { + private func updateText() { + self.text = self.badgeFormat.badgeText(self.value) + self.isBadgeEmpty = self.badgeFormat.badgeText(value).isEmpty + } + + private func updateFont() { + switch badgeSize { + case .normal: + self.textFont = self.theme.typography.captionHighlight + case .small: + self.textFont = self.theme.typography.smallHighlight + } + } + + private func updateScalings() { let verticalOffset = self.theme.layout.spacing.small let horizontalOffset = self.theme.layout.spacing.medium From 87fbf2e1d215e8472460f256d0825fcf4987edef Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Wed, 7 Jun 2023 11:46:49 +0200 Subject: [PATCH 27/31] Updated previews --- core/Demo/Classes/BadgeUIView_Previews.swift | 43 +++++++-------- .../Badge/Badge+UIPresentable.swift | 13 ++--- .../Components/Badge/BadgeComponentView.swift | 53 +++++++++---------- 3 files changed, 50 insertions(+), 59 deletions(-) diff --git a/core/Demo/Classes/BadgeUIView_Previews.swift b/core/Demo/Classes/BadgeUIView_Previews.swift index 9e7f33ffd..369def6d2 100644 --- a/core/Demo/Classes/BadgeUIView_Previews.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -20,13 +20,13 @@ private struct BadgePreviewFormatter: BadgeFormatting { struct UIBadgeView: UIViewRepresentable { - var viewModels: [BadgeViewModel] + var views: [BadgeUIView] func makeUIView(context: Context) -> some UIView { - let badgeViews = viewModels.enumerated().map { index, viewModel in + let badgesStackView = UIStackView() + views.enumerated().forEach { index, badgeView in let containerView = UIView() containerView.translatesAutoresizingMaskIntoConstraints = false - let badgeView = BadgeUIView(viewModel: viewModel) let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "badge_\(index)" @@ -51,9 +51,8 @@ struct UIBadgeView: UIViewRepresentable { ]) } - return containerView + badgesStackView.addArrangedSubview(containerView) } - let badgesStackView = UIStackView(arrangedSubviews: badgeViews) badgesStackView.axis = .vertical badgesStackView.alignment = .leading badgesStackView.spacing = 30 @@ -69,20 +68,20 @@ struct UIBadgeView: UIViewRepresentable { struct BadgeUIView_Previews: PreviewProvider { struct BadgeUIViewBridge: View { - private var viewModels = [ - BadgeViewModel( + private var views = [ + BadgeUIView( theme: SparkTheme.shared, badgeType: .alert, value: 6 ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .primary, badgeSize: .normal, value: 22, format: .overflowCounter(maxValue: 20) ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .danger, value: 10, @@ -90,26 +89,26 @@ struct BadgeUIView_Previews: PreviewProvider { formatter: BadgePreviewFormatter() ) ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .info, value: 20 ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .primary ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .neutral, isOutlined: false ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .secondary, value: 23 ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .success ) @@ -118,24 +117,18 @@ struct BadgeUIView_Previews: PreviewProvider { var body: some View { List { Button("Change UIKit Badge 0 Type") { - viewModels[0].badgeType = BadgeIntentType.allCases.randomElement() ?? .alert + views[0].setBadgeType(BadgeIntentType.allCases.randomElement() ?? .alert) } Button("Change UIKit Badge 1 Value") { - if viewModels[1].value == 10 { - viewModels[1].value = nil - } else if viewModels[1].value == 22 { - viewModels[1].value = 10 - } else { - viewModels[1].value = 22 - } + views[1].setBadgeValue(2) } Button("Change UIKit Badge 2 Outline") { - viewModels[2].isBadgeOutlined.toggle() + views[2].setBadgeOutlineEnabled(false) } Button("Change UIKit Badge 3 Size") { - viewModels[3].badgeSize = .small + views[3].setBadgeSize(.small) } - UIBadgeView(viewModels: viewModels) + UIBadgeView(views: views) .frame(height: 400) .listRowBackground(Color.gray.opacity(0.3)) } diff --git a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift index fab63d223..c6078312f 100644 --- a/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift +++ b/spark/Demo/Classes/View/Components/Badge/Badge+UIPresentable.swift @@ -20,13 +20,14 @@ private struct BadgePreviewFormatter: BadgeFormatting { } struct UIBadgeView: UIViewRepresentable { - var viewModels: [BadgeViewModel] + + var views: [BadgeUIView] func makeUIView(context: Context) -> some UIView { - let badgeViews = viewModels.enumerated().map { index, viewModel in + let badgesStackView = UIStackView() + views.enumerated().forEach { index, badgeView in let containerView = UIView() containerView.translatesAutoresizingMaskIntoConstraints = false - let badgeView = BadgeUIView(viewModel: viewModel) let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "badge_\(index)" @@ -51,17 +52,17 @@ struct UIBadgeView: UIViewRepresentable { ]) } - return containerView + badgesStackView.addArrangedSubview(containerView) } - let badgesStackView = UIStackView(arrangedSubviews: badgeViews) badgesStackView.axis = .vertical badgesStackView.alignment = .leading badgesStackView.spacing = 30 + badgesStackView.distribution = .fill + return badgesStackView } func updateUIView(_ uiView: UIViewType, context: Context) { - } } diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index e544fa2fb..4805eedea 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -21,22 +21,20 @@ private struct BadgePreviewFormatter: BadgeFormatting { struct BadgeComponentView: View { - private var viewModels: [BadgeViewModel] = - [ - BadgeViewModel( + private var views = [ + BadgeUIView( theme: SparkTheme.shared, badgeType: .alert, - badgeSize: .normal, value: 6 ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, - badgeType: .alert, - badgeSize: .small, + badgeType: .primary, + badgeSize: .normal, value: 22, format: .overflowCounter(maxValue: 20) ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .danger, value: 10, @@ -44,25 +42,26 @@ struct BadgeComponentView: View { formatter: BadgePreviewFormatter() ) ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, - badgeType: .info + badgeType: .info, + value: 20 ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, - badgeType: .neutral, - isOutlined: false + badgeType: .primary ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, - badgeType: .primary + badgeType: .neutral, + isOutlined: false ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .secondary, - value: 6 + value: 23 ), - BadgeViewModel( + BadgeUIView( theme: SparkTheme.shared, badgeType: .success ) @@ -85,22 +84,20 @@ struct BadgeComponentView: View { List { Section(header: Text("UIKit Badge")) { Button("Change UIKit Badge 0 Type") { - viewModels[0].badgeType = BadgeIntentType.allCases.randomElement() ?? .alert + views[0].setBadgeType(BadgeIntentType.allCases.randomElement() ?? .alert) } Button("Change UIKit Badge 1 Value") { - if viewModels[1].value == 10 { - viewModels[1].value = nil - } else if viewModels[1].value == 22 { - viewModels[1].value = 10 - } else { - viewModels[1].value = 22 - } + views[1].setBadgeValue(2) } Button("Change UIKit Badge 2 Outline") { - viewModels[2].isBadgeOutlined.toggle() + views[2].setBadgeOutlineEnabled(false) + } + Button("Change UIKit Badge 3 Size") { + views[3].setBadgeSize(.small) } - UIBadgeView(viewModels: viewModels) + UIBadgeView(views: views) .frame(height: 400) + .listRowBackground(Color.gray.opacity(0.3)) } .listRowBackground(Color.gray.opacity(0.3)) From 1f1985375b942112690185ba9f52893e92b54e18 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 8 Jun 2023 09:28:37 +0200 Subject: [PATCH 28/31] Fixed tests compile --- .../Badge/View/UIKit/BadgeUIViewTests.swift | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift index e59a8084c..b2cdf7203 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIViewTests.swift @@ -28,10 +28,8 @@ final class BadgeUIViewTests: UIKitComponentTestCase { func test_badge_all_cases_no_text() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeUIView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType) - ) + theme: theme, + badgeType: badgeIntentType) assertSnapshotInDarkAndLight(matching: view, named: "test_badge_\(badgeIntentType)") } @@ -40,11 +38,9 @@ final class BadgeUIViewTests: UIKitComponentTestCase { func test_badge_all_cases_text() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeUIView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - value: 23 - ) + theme: theme, + badgeType: badgeIntentType, + value: 23 ) assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)") @@ -54,12 +50,10 @@ final class BadgeUIViewTests: UIKitComponentTestCase { func test_badge_all_cases_text_smal_size() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeUIView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - badgeSize: .small, - value: 23 - ) + theme: theme, + badgeType: badgeIntentType, + badgeSize: .small, + value: 23 ) assertSnapshotInDarkAndLight(matching: view, named: "test_badge_with_text_\(badgeIntentType)_small_size") @@ -69,12 +63,10 @@ final class BadgeUIViewTests: UIKitComponentTestCase { func test_badge_all_cases_text_overflow_format() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeUIView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - value: 23, - format: .overflowCounter(maxValue: 20) - ) + theme: theme, + badgeType: badgeIntentType, + value: 23, + format: .overflowCounter(maxValue: 20) ) assertSnapshotInDarkAndLight(matching: view, named: "test_badge_overflow_format_text_\(badgeIntentType)") @@ -84,13 +76,11 @@ final class BadgeUIViewTests: UIKitComponentTestCase { func test_badge_all_cases_text_custom_format() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeUIView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - value: 23, - format: .custom( - formatter: TestBadgeFormatting() - ) + theme: theme, + badgeType: badgeIntentType, + value: 23, + format: .custom( + formatter: TestBadgeFormatting() ) ) @@ -101,12 +91,10 @@ final class BadgeUIViewTests: UIKitComponentTestCase { func test_badge_all_cases_no_text_custom_format() throws { for badgeIntentType in BadgeIntentType.allCases { let view = BadgeUIView( - viewModel: BadgeViewModel( - theme: theme, - badgeType: badgeIntentType, - format: .custom( - formatter: TestBadgeFormatting() - ) + theme: theme, + badgeType: badgeIntentType, + format: .custom( + formatter: TestBadgeFormatting() ) ) From 1a836136f730e2c0f0035c85e69631ec247b85a5 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 8 Jun 2023 09:53:54 +0200 Subject: [PATCH 29/31] Updated view model --- .../Sources/Components/Badge/View/UIKit/BadgeUIView.swift | 2 +- .../Components/Badge/ViewModel/BadgeViewModel.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index e1048b512..ccfd6831b 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -234,7 +234,7 @@ extension BadgeUIView { private func reloadColors() { self.backgroundColor = self.viewModel.backgroundColor.uiColor - badgeLabel.textColor = self.viewModel.textColor.uiColor + self.badgeLabel.textColor = self.viewModel.textColor.uiColor self.layer.borderColor = self.viewModel.badgeBorder.color.uiColor.cgColor } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index ac92bc472..5bc145cf7 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -65,7 +65,7 @@ public final class BadgeViewModel: ObservableObject { @Published var backgroundColor: ColorToken @Published var badgeBorder: BadgeBorder - @Published public var isBadgeOutlined: Bool + @Published var isBadgeOutlined: Bool // MARK: - Internal Appearance Properties var colorsUseCase: BadgeGetIntentColorsUseCaseable @@ -75,7 +75,7 @@ public final class BadgeViewModel: ObservableObject { // MARK: - Initializer - init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true, colorsUseCase: BadgeGetIntentColorsUseCaseable) { + init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isBadgeOutlined: Bool = true, colorsUseCase: BadgeGetIntentColorsUseCaseable = BadgeGetIntentColorsUseCase()) { let badgeColors = colorsUseCase.execute(intentType: badgeType, on: theme.colors) self.value = value @@ -109,12 +109,12 @@ public final class BadgeViewModel: ObservableObject { self.badgeFormat = format self.badgeSize = badgeSize self.badgeType = badgeType - self.isBadgeOutlined = isOutlined + self.isBadgeOutlined = isBadgeOutlined self.colorsUseCase = colorsUseCase } convenience public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { - self.init(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isOutlined: isOutlined, colorsUseCase: BadgeGetIntentColorsUseCase()) + self.init(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isBadgeOutlined: isOutlined) } private func updateColors() { From 1abe2b059f6f37cfb1379307ba79179b02f1973a Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 8 Jun 2023 10:04:31 +0200 Subject: [PATCH 30/31] Reverted public init --- core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 5bc145cf7..7744f4559 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -113,7 +113,7 @@ public final class BadgeViewModel: ObservableObject { self.colorsUseCase = colorsUseCase } - convenience public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { + convenience init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { self.init(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isBadgeOutlined: isOutlined) } From c2d9ff746d151c3fc7a23f5a4c89099bb05576d5 Mon Sep 17 00:00:00 2001 From: Alex Vecherov Date: Thu, 8 Jun 2023 10:08:43 +0200 Subject: [PATCH 31/31] Removed convenience init --- core/Demo/Classes/BadgeUIView_Previews.swift | 2 +- core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift | 4 ++-- core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift | 4 ---- .../Classes/View/Components/Badge/BadgeComponentView.swift | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/Demo/Classes/BadgeUIView_Previews.swift b/core/Demo/Classes/BadgeUIView_Previews.swift index 369def6d2..387d0680b 100644 --- a/core/Demo/Classes/BadgeUIView_Previews.swift +++ b/core/Demo/Classes/BadgeUIView_Previews.swift @@ -101,7 +101,7 @@ struct BadgeUIView_Previews: PreviewProvider { BadgeUIView( theme: SparkTheme.shared, badgeType: .neutral, - isOutlined: false + isBadgeOutlined: false ), BadgeUIView( theme: SparkTheme.shared, diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index ccfd6831b..b53f290b6 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -61,8 +61,8 @@ public class BadgeUIView: UIView { // MARK: - Init - public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { - self.viewModel = BadgeViewModel(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isOutlined: isOutlined) + public init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isBadgeOutlined: Bool = true) { + self.viewModel = BadgeViewModel(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isBadgeOutlined: isBadgeOutlined) super.init(frame: .zero) diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index 7744f4559..8db95184c 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -113,10 +113,6 @@ public final class BadgeViewModel: ObservableObject { self.colorsUseCase = colorsUseCase } - convenience init(theme: Theme, badgeType: BadgeIntentType, badgeSize: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isOutlined: Bool = true) { - self.init(theme: theme, badgeType: badgeType, badgeSize: badgeSize, value: value, format: format, isBadgeOutlined: isOutlined) - } - private func updateColors() { let badgeColors = self.colorsUseCase.execute(intentType: self.badgeType, on: self.theme.colors) diff --git a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift index 4805eedea..909d5196c 100644 --- a/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift +++ b/spark/Demo/Classes/View/Components/Badge/BadgeComponentView.swift @@ -54,7 +54,7 @@ struct BadgeComponentView: View { BadgeUIView( theme: SparkTheme.shared, badgeType: .neutral, - isOutlined: false + isBadgeOutlined: false ), BadgeUIView( theme: SparkTheme.shared,