diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift index a4cc203f..feba64d7 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift @@ -85,23 +85,14 @@ struct ModalPreviewHelpers { ContainerRadiusPicker(selection: self.$model.cornerRadius) { Text("Custom 30px").tag(ContainerRadius.custom(30)) } - Picker("Overlay Style", selection: self.$model.overlayStyle) { - Text("Blurred").tag(ModalOverlayStyle.blurred) - Text("Dimmed").tag(ModalOverlayStyle.dimmed) - Text("Transparent").tag(ModalOverlayStyle.transparent) - } + OverlayStylePicker(selection: self.$model.overlayStyle) Picker("Size", selection: self.$model.size) { Text("Small").tag(ModalSize.small) Text("Medium").tag(ModalSize.medium) Text("Large").tag(ModalSize.large) Text("Full").tag(ModalSize.full) } - Picker("Transition", selection: self.$model.transition) { - Text("None").tag(ModalTransition.none) - Text("Fast").tag(ModalTransition.fast) - Text("Normal").tag(ModalTransition.normal) - Text("Slow").tag(ModalTransition.slow) - } + TransitionPicker(selection: self.$model.transition) self.additionalPickers() } } diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index 896f743b..46c3b0fe 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -211,6 +211,20 @@ struct KeyboardTypePicker: View { } } +// MARK: - OverlayStylePicker + +struct OverlayStylePicker: View { + @Binding var selection: ModalOverlayStyle + + var body: some View { + Picker("Overlay Style", selection: self.$selection) { + Text("Blurred").tag(ModalOverlayStyle.blurred) + Text("Dimmed").tag(ModalOverlayStyle.dimmed) + Text("Transparent").tag(ModalOverlayStyle.transparent) + } + } +} + // MARK: - SizePicker struct SizePicker: View { @@ -243,6 +257,21 @@ struct SubmitTypePicker: View { } } +// MARK: - TransitionPicker + +struct TransitionPicker: View { + @Binding var selection: ModalTransition + + var body: some View { + Picker("Transition", selection: self.$selection) { + Text("None").tag(ModalTransition.none) + Text("Fast").tag(ModalTransition.fast) + Text("Normal").tag(ModalTransition.normal) + Text("Slow").tag(ModalTransition.slow) + } + } +} + // MARK: - UniversalColorPicker struct UniversalColorPicker: View { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift new file mode 100644 index 00000000..babbd227 --- /dev/null +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift @@ -0,0 +1,165 @@ +import ComponentsKit +import SwiftUI +import UIKit + +struct AlertPreview: View { + @State private var model = AlertVM { + $0.title = Self.alertTitle + $0.message = AlertMessage.short.rawValue + $0.primaryButton = Self.initialPrimaryButton + $0.secondaryButton = Self.initialSecondaryButton + } + + var body: some View { + VStack { + PreviewWrapper(title: "UIKit") { + UKButton(model: .init { $0.title = "Show Alert" }) { + UIApplication.shared.topViewController?.present( + UKAlertController(model: self.model), + animated: true + ) + } + .preview + } + Form { + Section("Title") { + Toggle("Has Title", isOn: .init( + get: { return self.model.title != nil }, + set: { newValue in + self.model.title = newValue ? Self.alertTitle : nil + } + )) + } + + Section("Message") { + Picker("Alert Message", selection: self.$model.message) { + Text("None").tag(Optional.none) + Text("Short").tag(AlertMessage.short.rawValue) + Text("Long").tag(AlertMessage.long.rawValue) + } + } + + Section("Primary Button") { + Toggle("Has Primary Button", isOn: .init( + get: { return self.model.primaryButton != nil }, + set: { newValue in + self.model.primaryButton = newValue ? Self.initialPrimaryButton : nil + } + )) + if self.model.primaryButton != nil { + Picker("Title", selection: self.primaryButtonVMOrDefault.title) { + Text("Short").tag(PrimaryButtonText.short.rawValue) + Text("Longer").tag(PrimaryButtonText.longer.rawValue) + } + self.buttonPickers(for: self.primaryButtonVMOrDefault) + } + } + + Section("Secondary Button") { + Toggle("Has Secondary Button", isOn: .init( + get: { return self.model.secondaryButton != nil }, + set: { newValue in + self.model.secondaryButton = newValue ? Self.initialSecondaryButton : nil + } + )) + if self.model.secondaryButton != nil { + Picker("Title", selection: self.secondaryButtonVMOrDefault.title) { + Text("Short").tag(SecondaryButtonText.short.rawValue) + Text("Longer").tag(SecondaryButtonText.longer.rawValue) + } + self.buttonPickers(for: self.secondaryButtonVMOrDefault) + } + } + + Section("Main Properties") { + Picker("Background Color", selection: self.$model.backgroundColor) { + Text("Default").tag(Optional.none) + Text("Accent Background").tag(ComponentColor.accent.background) + Text("Success Background").tag(ComponentColor.success.background) + Text("Warning Background").tag(ComponentColor.warning.background) + Text("Danger Background").tag(ComponentColor.danger.background) + } + Toggle("Closes On Overlay Tap", isOn: self.$model.closesOnOverlayTap) + Picker("Content Paddings", selection: self.$model.contentPaddings) { + Text("12px").tag(Paddings(padding: 12)) + Text("16px").tag(Paddings(padding: 16)) + Text("20px").tag(Paddings(padding: 20)) + } + ContainerRadiusPicker(selection: self.$model.cornerRadius) { + Text("Custom 30px").tag(ContainerRadius.custom(30)) + } + OverlayStylePicker(selection: self.$model.overlayStyle) + TransitionPicker(selection: self.$model.transition) + } + } + } + } + + // MARK: - Reusable Pickers + + private func buttonPickers(for buttonVM: Binding) -> some View { + Group { + AnimationScalePicker(selection: buttonVM.animationScale) + ComponentOptionalColorPicker(selection: buttonVM.color) + ComponentRadiusPicker(selection: buttonVM.cornerRadius) { + Text("Custom: 20px").tag(ComponentRadius.custom(20)) + } + Picker("Style", selection: buttonVM.style) { + Text("Filled").tag(ButtonStyle.filled) + Text("Plain").tag(ButtonStyle.plain) + Text("Light").tag(ButtonStyle.light) + Text("Bordered with small border").tag(ButtonStyle.bordered(.small)) + Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) + Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) + } + } + } + + // MARK: - Helpers + + enum AlertMessage: String { + case short = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + case long = """ +Lorem ipsum odor amet, consectetuer adipiscing elit. Vitae vehicula pellentesque lectus orci fames. Cras suscipit dui tortor penatibus turpis ultrices. Laoreet montes adipiscing ante dapibus facilisis. Lorem per fames nec duis quis eleifend imperdiet. Tincidunt id interdum adipiscing eros dis quis platea varius. Potenti eleifend eu molestie laoreet varius sapien. Adipiscing nascetur platea penatibus curabitur tempus nibh laoreet porttitor. Augue et curabitur cras sed semper inceptos nunc montes mollis. + +Lectus arcu pellentesque inceptos tempor fringilla nascetur. Erat curae convallis integer mi, quis facilisi tortor. Phasellus aliquam molestie vehicula odio in dis maximus diam elit. Rutrum gravida amet euismod feugiat fusce. Est egestas velit vulputate senectus sociosqu fringilla eget nibh. Nam pellentesque aenean mi platea tincidunt quam sem purus. Himenaeos suspendisse nec sapien habitasse ultricies maecenas libero odio. Rutrum senectus maximus ultrices, ad nam ultricies placerat. + +Enim habitant laoreet inceptos scelerisque senectus, tellus molestie ut. Eros risus nibh morbi eu aenean. Velit ligula magnis aliquet at luctus. Dapibus vestibulum consectetur euismod vitae per ultrices litora quis. Aptent eleifend dapibus urna lacinia felis nisl. Sit amet fusce nullam feugiat posuere. Urna amet curae velit fermentum interdum vestibulum penatibus. Penatibus vivamus sem ultricies pellentesque congue id mattis diam. Aliquam efficitur mi gravida sollicitudin; amet imperdiet. Rutrum mollis risus justo tortor in duis cursus. +""" + } + enum PrimaryButtonText: String { + case short = "Continue" + case longer = "Remind me later" + } + enum SecondaryButtonText: String { + case short = "Cancel" + case longer = "Cancel, Don't Do That" + } + static let alertTitle = "Alert Title" + static let initialPrimaryButton = AlertButtonVM { + $0.title = PrimaryButtonText.short.rawValue + $0.style = .filled + $0.color = .primary + } + static let initialSecondaryButton = AlertButtonVM { + $0.title = SecondaryButtonText.short.rawValue + $0.style = .light + } + + var primaryButtonVMOrDefault: Binding { + return .init( + get: { self.model.primaryButton ?? Self.initialPrimaryButton }, + set: { self.model.primaryButton = $0 } + ) + } + var secondaryButtonVMOrDefault: Binding { + return .init( + get: { self.model.secondaryButton ?? Self.initialSecondaryButton }, + set: { self.model.secondaryButton = $0 } + ) + } +} + +#Preview { + AlertPreview() +} diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index dc3e340e..df58ad2e 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -27,12 +27,12 @@ struct ButtonPreview: View { Toggle("Full Width", isOn: self.$model.isFullWidth) SizePicker(selection: self.$model.size) Picker("Style", selection: self.$model.style) { - Text("Filled").tag(ButtonVM.Style.filled) - Text("Plain").tag(ButtonVM.Style.plain) - Text("Light").tag(ButtonVM.Style.light) - Text("Bordered with small border").tag(ButtonVM.Style.bordered(.small)) - Text("Bordered with medium border").tag(ButtonVM.Style.bordered(.medium)) - Text("Bordered with large border").tag(ButtonVM.Style.bordered(.large)) + Text("Filled").tag(ButtonStyle.filled) + Text("Plain").tag(ButtonStyle.plain) + Text("Light").tag(ButtonStyle.light) + Text("Bordered with small border").tag(ButtonStyle.bordered(.small)) + Text("Bordered with medium border").tag(ButtonStyle.bordered(.medium)) + Text("Bordered with large border").tag(ButtonStyle.bordered(.large)) } } } diff --git a/Examples/DemosApp/DemosApp/Core/App.swift b/Examples/DemosApp/DemosApp/Core/App.swift index de6bfa47..d8a335bd 100644 --- a/Examples/DemosApp/DemosApp/Core/App.swift +++ b/Examples/DemosApp/DemosApp/Core/App.swift @@ -39,6 +39,9 @@ struct App: View { } Section("Modals") { + NavigationLinkWithTitle("Alert") { + AlertPreview() + } NavigationLinkWithTitle("Bottom Modal") { BottomModalPreview() } diff --git a/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift b/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift new file mode 100644 index 00000000..10289e80 --- /dev/null +++ b/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift @@ -0,0 +1,28 @@ +import Foundation + +/// A model that defines the appearance properties for a button in the alert. +public struct AlertButtonVM: ComponentVM { + /// The text displayed on the button. + public var title: String = "" + + /// The scaling factor for the button's press animation, with a value between 0 and 1. + /// + /// Defaults to `.medium`. + public var animationScale: AnimationScale = .medium + + /// The color of the button. + public var color: ComponentColor? + + /// The corner radius of the button. + /// + /// Defaults to `.medium`. + public var cornerRadius: ComponentRadius = .medium + + /// The visual style of the button. + /// + /// Defaults to `.filled`. + public var style: ButtonStyle = .filled + + /// Initializes a new instance of `AlertButtonVM` with default values. + public init() {} +} diff --git a/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift new file mode 100644 index 00000000..63a61fb8 --- /dev/null +++ b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift @@ -0,0 +1,100 @@ +import Foundation + +/// A model that defines the appearance properties for an alert. +public struct AlertVM: ComponentVM { + /// The title of the alert. + public var title: String? + + /// The message of the alert. + public var message: String? + + /// The modal that defines the appearance properties for a primary button in the alert. + /// + /// If it is `nil`, the primary button will not be displayed. + public var primaryButton: AlertButtonVM? + + /// The modal that defines the appearance properties for a secondary button in the alert. + /// + /// If it is `nil`, the secondary button will not be displayed. + public var secondaryButton: AlertButtonVM? + + /// The background color of the modal. + public var backgroundColor: UniversalColor? + + /// A Boolean value indicating whether the modal should close when tapping on the overlay. + /// + /// Defaults to `false`. + public var closesOnOverlayTap: Bool = false + + /// The padding applied to the modal's content area. + /// + /// Defaults to a padding value of `16` for all sides. + public var contentPaddings: Paddings = .init(padding: 16) + + /// The corner radius of the modal. + /// + /// Defaults to `.medium`. + public var cornerRadius: ContainerRadius = .medium + + /// The style of the overlay displayed behind the modal. + /// + /// Defaults to `.dimmed`. + public var overlayStyle: ModalOverlayStyle = .dimmed + + /// The transition duration of the modal's appearance and dismissal animations. + /// + /// Defaults to `.fast`. + public var transition: ModalTransition = .fast + + /// Initializes a new instance of `AlertVM` with default values. + public init() {} +} + +// MARK: - Helpers + +extension AlertVM { + var modalVM: CenterModalVM { + return CenterModalVM { + $0.backgroundColor = self.backgroundColor + $0.closesOnOverlayTap = self.closesOnOverlayTap + $0.contentPaddings = self.contentPaddings + $0.cornerRadius = self.cornerRadius + $0.overlayStyle = self.overlayStyle + $0.transition = self.transition + $0.size = .small + } + } + + var primaryButtonVM: ButtonVM? { + let buttonVM = self.primaryButton.map(self.mapAlertButtonVM) + if self.secondaryButton.isNotNil { + return buttonVM + } else { + return buttonVM ?? Self.defaultButtonVM + } + } + + var secondaryButtonVM: ButtonVM? { + return self.secondaryButton.map(self.mapAlertButtonVM) + } + + private func mapAlertButtonVM(_ model: AlertButtonVM) -> ButtonVM { + return ButtonVM { + $0.title = model.title + $0.animationScale = model.animationScale + $0.color = model.color + $0.cornerRadius = model.cornerRadius + $0.style = model.style + } + } +} + +extension AlertVM { + static let buttonsSpacing: CGFloat = 12 + + static let defaultButtonVM = ButtonVM { + $0.title = "OK" + $0.color = .primary + $0.style = .filled + } +} diff --git a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift new file mode 100644 index 00000000..3bec03d7 --- /dev/null +++ b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift @@ -0,0 +1,198 @@ +import UIKit + +/// A controller that presents an alert with a title, message, and up to two action buttons. +/// +/// All actions in an alert dismiss the alert after the action runs. If no actions are present, a standard “OK” action is included. +/// +/// - Example: +/// ```swift +/// let alert = UKAlertController( +/// model: .init { alertVM in +/// alertVM.title = "My Alert" +/// alertVM.message = "This is an alert." +/// alertVM.primaryButton = .init { buttonVM in +/// buttonVM.title = "OK" +/// buttonVM.color = .primary +/// buttonVM.style = .filled +/// } +/// alertVM.secondaryButton = .init { buttonVM in +/// buttonVM.title = "Cancel" +/// buttonVM.style = .light +/// } +/// }, +/// primaryAction: { +/// NSLog("Primary button tapped") +/// }, +/// secondaryAction: { +/// NSLog("Secondary button tapped") +/// } +/// ) +/// +/// vc.present(alert, animated: true) +/// ``` +public class UKAlertController: UKCenterModalController { + // MARK: - Properties + + /// A model that defines the appearance properties for an alert. + public let alertVM: AlertVM + + /// The primary action to be executed when the primary button is tapped. + public var primaryAction: (() -> Void)? + /// The secondary action to be executed when the secondary button is tapped. + public var secondaryAction: (() -> Void)? + + // MARK: - Subviews + + /// The label used to display the title of the alert. + public let titleLabel = UILabel() + /// The label used to display the subtitle or message of the alert. + public let subtitleLabel = UILabel() + /// The button representing the primary action in the alert. + public let primaryButton = UKButton() + /// The button representing the secondary action in the alert. + public let secondaryButton = UKButton() + /// A stack view that arranges the primary and secondary buttons. + public let buttonsStackView = UIStackView() + + // MARK: - Initialization + + /// Initializer. + /// + /// - Parameters: + /// - model: A model that defines the appearance properties for an alert. + /// - primaryAction: An optional closure executed when the primary button is tapped. + /// - secondaryAction: An optional closure executed when the secondary button is tapped. + public init( + model: AlertVM, + primaryAction: (() -> Void)? = nil, + secondaryAction: (() -> Void)? = nil + ) { + self.alertVM = model + + self.primaryAction = primaryAction + self.secondaryAction = secondaryAction + + super.init(model: model.modalVM) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Setup + + public override func setup() { + if self.alertVM.title.isNotNilAndEmpty, + self.alertVM.message.isNotNilAndEmpty { + self.header = self.titleLabel + self.body = self.subtitleLabel + } else if self.alertVM.title.isNotNilAndEmpty { + self.body = self.titleLabel + } else { + self.body = self.subtitleLabel + } + self.footer = self.buttonsStackView + + if self.alertVM.primaryButtonVM.isNotNil { + self.buttonsStackView.addArrangedSubview(self.primaryButton) + } + if self.alertVM.secondaryButtonVM.isNotNil { + self.buttonsStackView.addArrangedSubview(self.secondaryButton) + } + + self.primaryButton.action = { [weak self] in + self?.primaryAction?() + self?.dismiss(animated: true) + } + self.secondaryButton.action = { [weak self] in + self?.secondaryAction?() + self?.dismiss(animated: true) + } + + // NOTE: Labels and stack view should be assigned to `header`, `body` + // and `footer` before calling the superview's method, otherwise they + // won't be added to the list of subviews. + super.setup() + } + + // MARK: - Style + + public override func style() { + super.style() + + Self.Style.titleLabel(self.titleLabel, text: self.alertVM.title) + Self.Style.subtitleLabel(self.subtitleLabel, text: self.alertVM.message) + Self.Style.buttonsStackView(self.buttonsStackView) + + if let primaryButtonVM = self.alertVM.primaryButtonVM { + self.primaryButton.model = primaryButtonVM + } + if let secondaryButtonVM = self.alertVM.secondaryButtonVM { + self.secondaryButton.model = secondaryButtonVM + } + } + + // MARK: - Layout + + public override func updateViewConstraints() { + super.updateViewConstraints() + + if self.buttonsStackView.arrangedSubviews.count == 2 { + self.buttonsStackView.axis = .vertical + let primaryButtonWidth = self.primaryButton.intrinsicContentSize.width + let secondaryButtonWidth = self.secondaryButton.intrinsicContentSize.width + + // Since the `maxWidth` of the alert is always less than the width of the + // screen, we can assume that the width of the container is equal to this + // `maxWidth` value. + let containerWidth = self.model.size.maxWidth + let availableButtonsWidth = containerWidth + - AlertVM.buttonsSpacing + - self.model.contentPaddings.leading + - self.model.contentPaddings.trailing + let availableButtonWidth = availableButtonsWidth / 2 + + if primaryButtonWidth <= availableButtonWidth, + secondaryButtonWidth <= availableButtonWidth { + self.buttonsStackView.removeArrangedSubview(self.secondaryButton) + self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 0) + + self.buttonsStackView.axis = .horizontal + } else { + self.buttonsStackView.removeArrangedSubview(self.secondaryButton) + self.buttonsStackView.insertArrangedSubview(self.secondaryButton, at: 1) + + self.buttonsStackView.axis = .vertical + } + } else { + self.buttonsStackView.axis = .vertical + } + } +} + +// MARK: - Style Helpers + +extension UKAlertController { + fileprivate enum Style { + static func titleLabel(_ label: UILabel, text: String?) { + label.text = text + label.font = UniversalFont.mdHeadline.uiFont + label.textColor = UniversalColor.foreground.uiColor + label.textAlignment = .center + label.numberOfLines = 0 + } + + static func subtitleLabel(_ label: UILabel, text: String?) { + label.text = text + label.font = UniversalFont.mdBody.uiFont + label.textColor = UniversalColor.secondaryForeground.uiColor + label.textAlignment = .center + label.numberOfLines = 0 + } + + static func buttonsStackView(_ stackView: UIStackView) { + stackView.distribution = .fillEqually + stackView.spacing = AlertVM.buttonsSpacing + } + } +} diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonStyle.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonStyle.swift deleted file mode 100644 index 787dadba..00000000 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonStyle.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -extension ButtonVM { - /// The buttons appearance style. - public enum Style: Hashable { - /// A button with a filled background. - case filled - /// A button with a transparent background. - case plain - /// A button with a partially transparent background. - case light - /// A button with a transparent background and a border. - case bordered(BorderWidth) - } -} diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index 1d5dac56..1b7ea7a6 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -41,7 +41,7 @@ public struct ButtonVM: ComponentVM { /// The visual style of the button. /// /// Defaults to `.filled`. - public var style: Style = .filled + public var style: ButtonStyle = .filled /// Initializes a new instance of `ButtonVM` with default values. public init() {} diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift index f94747f8..72dc998a 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift @@ -38,13 +38,23 @@ public class UKBottomModalController: UKModalController { /// - header: An optional content block for the modal's header. /// - body: The main content block for the modal. /// - footer: An optional content block for the modal's footer. - public override init( + public init( model: BottomModalVM = .init(), header: Content? = nil, body: Content, footer: Content? = nil ) { - super.init(model: model, header: header, body: body, footer: footer) + super.init(model: model) + + self.header = header?({ [weak self] animated in + self?.dismiss(animated: animated) + }) + self.body = body({ [weak self] animated in + self?.dismiss(animated: animated) + }) + self.footer = footer?({ [weak self] animated in + self?.dismiss(animated: animated) + }) } required public init?(coder: NSCoder) { diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift index a0642d3c..5ba485e6 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift @@ -38,13 +38,27 @@ public class UKCenterModalController: UKModalController { /// - header: An optional content block for the modal's header. /// - body: The main content block for the modal. /// - footer: An optional content block for the modal's footer. - public override init( + public init( model: CenterModalVM = .init(), header: Content? = nil, body: Content, footer: Content? = nil ) { - super.init(model: model, header: header, body: body, footer: footer) + super.init(model: model) + + self.header = header?({ [weak self] animated in + self?.dismiss(animated: animated) + }) + self.body = body({ [weak self] animated in + self?.dismiss(animated: animated) + }) + self.footer = footer?({ [weak self] animated in + self?.dismiss(animated: animated) + }) + } + + override init(model: CenterModalVM) { + super.init(model: model) } required public init?(coder: NSCoder) { diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift index fb33b4eb..45504e04 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift @@ -31,12 +31,7 @@ open class UKModalController: UIViewController { // MARK: - Initialization - init( - model: VM = .init(), - header: Content? = nil, - body: Content, - footer: Content? = nil - ) { + init(model: VM) { self.model = model switch model.overlayStyle { @@ -48,16 +43,6 @@ open class UKModalController: UIViewController { super.init(nibName: nil, bundle: nil) - self.header = header?({ [weak self] animated in - self?.dismiss(animated: animated) - }) - self.body = body({ [weak self] animated in - self?.dismiss(animated: animated) - }) - self.footer = footer?({ [weak self] animated in - self?.dismiss(animated: animated) - }) - self.modalPresentationStyle = .overFullScreen self.modalTransitionStyle = .crossDissolve } diff --git a/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift b/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift new file mode 100644 index 00000000..bc598997 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/ButtonStyle.swift @@ -0,0 +1,13 @@ +import Foundation + +/// The buttons appearance style. +public enum ButtonStyle: Hashable { + /// A button with a filled background. + case filled + /// A button with a transparent background. + case plain + /// A button with a partially transparent background. + case light + /// A button with a transparent background and a border. + case bordered(BorderWidth) +}