diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift index eb8fccea..272810a2 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/ModalPreview+Helpers.swift @@ -65,6 +65,7 @@ struct ModalPreviewHelpers { Text("Warning Background").tag(UniversalColor.warningBackground) Text("Danger Background").tag(UniversalColor.dangerBackground) } + BackgroundStylePicker(selection: self.$model.backgroundStyle) BorderWidthPicker(selection: self.$model.borderWidth) Toggle("Closes On Overlay Tap", isOn: self.$model.closesOnOverlayTap) .disabled(self.footer == nil) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift index bf46eec7..72586a2f 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/Helpers/PreviewPickers.swift @@ -32,6 +32,22 @@ struct AutocapitalizationPicker: View { } } +// MARK: - BackgroundStylePicker + +struct BackgroundStylePicker: View { + @Binding var selection: ComponentsKit.BackgroundStyle + + var body: some View { + Picker("Background Style", selection: self.$selection) { + Text("Solid").tag(ComponentsKit.BackgroundStyle.solid) + Text("Blur").tag(ComponentsKit.BackgroundStyle.blur) + if #available(iOS 26.0, *) { + Text("Liquid Glass").tag(ComponentsKit.BackgroundStyle.liquidGlass) + } + } + } +} + // MARK: - BorderWidthPicker struct BorderWidthPicker: View { diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift index 658a73d0..299e5862 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/AlertPreview.swift @@ -89,6 +89,7 @@ struct AlertPreview: View { Text("Warning Background").tag(UniversalColor.warningBackground) Text("Danger Background").tag(UniversalColor.dangerBackground) } + BackgroundStylePicker(selection: self.$model.backgroundStyle) BorderWidthPicker(selection: self.$model.borderWidth) Toggle("Closes On Overlay Tap", isOn: self.$model.closesOnOverlayTap) Picker("Content Paddings", selection: self.$model.contentPaddings) { @@ -111,6 +112,7 @@ struct AlertPreview: View { private func buttonPickers(for buttonVM: Binding) -> some View { Group { AnimationScalePicker(selection: buttonVM.animationScale) + BackgroundStylePicker(selection: buttonVM.backgroundStyle) ComponentOptionalColorPicker(selection: buttonVM.color) ComponentRadiusPicker(selection: buttonVM.cornerRadius) { Text("Custom: 20px").tag(ComponentRadius.custom(20)) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 430476a8..b3e1c945 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift @@ -19,6 +19,7 @@ struct ButtonPreview: View { } Form { AnimationScalePicker(selection: self.$model.animationScale) + BackgroundStylePicker(selection: self.$model.backgroundStyle) ComponentOptionalColorPicker(selection: self.$model.color) Picker("Content Spacing", selection: self.$model.contentSpacing) { Text("4").tag(CGFloat(4)) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift index ce9e600f..ca976dd0 100644 --- a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift +++ b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/CardPreview.swift @@ -17,6 +17,7 @@ struct CardPreview: View { Form { AnimationScalePicker(selection: self.$model.animationScale) Picker("Background Color", selection: self.$model.backgroundColor) { + Text("Clear").tag(Optional.none) Text("Background").tag(UniversalColor.background) Text("Secondary Background").tag(UniversalColor.secondaryBackground) Text("Accent Background").tag(UniversalColor.accentBackground) @@ -24,6 +25,7 @@ struct CardPreview: View { Text("Warning Background").tag(UniversalColor.warningBackground) Text("Danger Background").tag(UniversalColor.dangerBackground) } + BackgroundStylePicker(selection: self.$model.backgroundStyle) Picker("Border Color", selection: self.$model.borderColor) { Text("Divider").tag(UniversalColor.divider) Text("Primary").tag(UniversalColor.primary) diff --git a/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift b/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift index 10289e80..8355ffe6 100644 --- a/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift +++ b/Sources/ComponentsKit/Components/Alert/Models/AlertButtonVM.swift @@ -10,6 +10,11 @@ public struct AlertButtonVM: ComponentVM { /// Defaults to `.medium`. public var animationScale: AnimationScale = .medium + /// Defines how the button renders its background. + /// + /// Defaults to `.solid`. + public var backgroundStyle: BackgroundStyle = .solid + /// The color of the button. public var color: ComponentColor? diff --git a/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift index 5999bfc2..51a6eafb 100644 --- a/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift +++ b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift @@ -21,6 +21,9 @@ public struct AlertVM: ComponentVM { /// The background color of the alert. public var backgroundColor: UniversalColor? + /// Defines how the alert renders its background. + public var backgroundStyle: BackgroundStyle = .solid + /// The border thickness of the alert. /// /// Defaults to `.small`. @@ -61,6 +64,7 @@ extension AlertVM { var modalVM: CenterModalVM { return CenterModalVM { $0.backgroundColor = self.backgroundColor + $0.backgroundStyle = self.backgroundStyle $0.borderWidth = self.borderWidth $0.closesOnOverlayTap = self.closesOnOverlayTap $0.contentPaddings = self.contentPaddings @@ -88,6 +92,7 @@ extension AlertVM { return ButtonVM { $0.title = model.title $0.animationScale = model.animationScale + $0.backgroundStyle = model.backgroundStyle $0.color = model.color $0.cornerRadius = model.cornerRadius $0.style = model.style diff --git a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift index 2af9e366..f6ec2cc2 100644 --- a/Sources/ComponentsKit/Components/Alert/UKAlertController.swift +++ b/Sources/ComponentsKit/Components/Alert/UKAlertController.swift @@ -30,7 +30,7 @@ import UIKit /// /// vc.present(alert, animated: true) /// ``` -public class UKAlertController: UKCenterModalController { +open class UKAlertController: UKCenterModalController { // MARK: - Properties /// A model that defines the appearance properties for an alert. diff --git a/Sources/ComponentsKit/Components/Avatar/Models/AvatarImageSource.swift b/Sources/ComponentsKit/Components/Avatar/Models/AvatarImageSource.swift index 81fcc20d..80e0697a 100644 --- a/Sources/ComponentsKit/Components/Avatar/Models/AvatarImageSource.swift +++ b/Sources/ComponentsKit/Components/Avatar/Models/AvatarImageSource.swift @@ -13,11 +13,5 @@ extension AvatarVM { /// /// - Parameter image: See ``UniversalImage``. case local(_ image: UniversalImage) - - /// An image loaded from a local asset. - @available(*, deprecated, message: "Use `local(_:)` instead.") - public static func local(_ name: String, _ bundle: Bundle? = nil) -> Self { - return .local(.init(name, bundle: bundle)) - } } } diff --git a/Sources/ComponentsKit/Components/Avatar/Models/AvatarPlaceholder.swift b/Sources/ComponentsKit/Components/Avatar/Models/AvatarPlaceholder.swift index b1b05975..9ddb89c2 100644 --- a/Sources/ComponentsKit/Components/Avatar/Models/AvatarPlaceholder.swift +++ b/Sources/ComponentsKit/Components/Avatar/Models/AvatarPlaceholder.swift @@ -17,17 +17,5 @@ extension AvatarVM { /// /// - Parameter image: See ``UniversalImage``. case image(_ image: UniversalImage) - - /// A placeholder that displays an SF Symbol. - @available(*, deprecated, message: "Use `image(_:)` instead.") - public static func sfSymbol(_ name: String) -> Self { - return .image(.init(systemName: name)) - } - - /// A placeholder that displays a custom icon from an asset catalog. - @available(*, deprecated, message: "Use `image(_:)` instead.") - public static func icon(_ name: String, _ bundle: Bundle? = nil) -> Self { - return .image(.init(name, bundle: bundle)) - } } } diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index c42a8865..15e644fb 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -10,6 +10,11 @@ public struct ButtonVM: ComponentVM { /// The color of the button. public var color: ComponentColor? + /// Defines how the button renders its background. + /// + /// Defaults to `.solid`. + public var backgroundStyle: BackgroundStyle = .solid + /// The spacing between the button's title and its image or loading indicator. /// /// Defaults to `8.0`. @@ -33,14 +38,6 @@ public struct ButtonVM: ComponentVM { /// Defaults to `.leading`. public var imageLocation: ImageLocation = .leading - /// Defines how image is rendered. - @available(*, deprecated, message: "Use `image.withRenderingMode(_:)` instead.") - public var imageRenderingMode: ImageRenderingMode? - - /// The source of the image to be displayed. - @available(*, deprecated, message: "Use `image` instead.") - public var imageSrc: ImageSource? - /// A Boolean value indicating whether the button is enabled or disabled. /// /// Defaults to `true`. @@ -84,6 +81,14 @@ extension ButtonVM { var isInteractive: Bool { self.isEnabled && !self.isLoading } + var isCustomTapAnimationEnabled: Bool { + switch self.backgroundStyle { + case .solid, .blur: + return true + case .liquidGlass: + return false + } + } var preferredLoadingVM: LoadingVM { return self.loadingVM ?? .init { $0.color = .init( @@ -187,30 +192,13 @@ extension ButtonVM { } } } - var imageWithLegacyFallback: UniversalImage? { - if let image { return image } - - guard let imageSrc else { return nil } - - let image = switch imageSrc { - case .sfSymbol(let name): - UniversalImage(systemName: name) - case .local(let name, let bundle): - UniversalImage(name, bundle: bundle) - } - if let imageRenderingMode { - return image.withRenderingMode(imageRenderingMode) - } else { - return image - } - } } // MARK: UIKit Helpers extension ButtonVM { var isImageHidden: Bool { - return self.isLoading || self.imageWithLegacyFallback.isNil + return self.isLoading || self.image.isNil } func preferredSize( for contentSize: CGSize, @@ -243,9 +231,10 @@ extension ButtonVM { || self.font != oldModel.font || self.isFullWidth != oldModel.isFullWidth || self.isLoading != oldModel.isLoading - || self.imageWithLegacyFallback != oldModel.imageWithLegacyFallback + || self.image != oldModel.image || self.contentSpacing != oldModel.contentSpacing || self.title != oldModel.title + || self.style != oldModel.style } } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index ef930ed5..66487cc8 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -33,15 +33,32 @@ public struct SUButton: View { HStack(spacing: self.model.contentSpacing) { self.content } + .font(self.model.preferredFont.font) + .lineLimit(1) + .padding(.horizontal, self.model.horizontalPadding) + .frame(maxWidth: self.model.width) + .frame(height: self.model.height) + .contentShape(.rect) + .foregroundStyle(self.model.foregroundColor.color) + .componentBackground( + shape: RoundedRectangle(cornerRadius: self.model.cornerRadius.value()), + backgroundStyle: self.model.backgroundStyle, + backgroundColor: self.model.backgroundColor?.color, + borderColor: self.model.borderColor?.color ?? .clear, + borderWidth: self.model.borderWidth, + isGlassInteractive: self.model.isInteractive + ) } - .buttonStyle(CustomButtonStyle(model: self.model)) - .simultaneousGesture(DragGesture(minimumDistance: 0.0) - .onChanged { _ in - self.scale = self.model.animationScale.value - } - .onEnded { _ in - self.scale = 1.0 - } + .buttonStyle(CustomButtonStyle()) + .simultaneousGesture( + DragGesture(minimumDistance: 0.0) + .onChanged { _ in + self.scale = self.model.animationScale.value + } + .onEnded { _ in + self.scale = 1.0 + }, + isEnabled: self.model.isCustomTapAnimationEnabled ) .disabled(!self.model.isInteractive) .scaleEffect(self.scale, anchor: .center) @@ -50,7 +67,7 @@ public struct SUButton: View { @ViewBuilder private var content: some View { - switch (self.model.isLoading, self.model.imageWithLegacyFallback, self.model.imageLocation) { + switch (self.model.isLoading, self.model.image, self.model.imageLocation) { case (true, _, _) where self.model.title.isEmpty: SULoading(model: self.model.preferredLoadingVM) case (true, _, _): @@ -109,31 +126,7 @@ private struct ButtonImage: View { } private struct CustomButtonStyle: SwiftUI.ButtonStyle { - let model: ButtonVM - func makeBody(configuration: Configuration) -> some View { configuration.label - .font(self.model.preferredFont.font) - .lineLimit(1) - .padding(.horizontal, self.model.horizontalPadding) - .frame(maxWidth: self.model.width) - .frame(height: self.model.height) - .contentShape(.rect) - .foregroundStyle(self.model.foregroundColor.color) - .background(self.model.backgroundColor?.color ?? .clear) - .clipShape( - RoundedRectangle( - cornerRadius: self.model.cornerRadius.value() - ) - ) - .overlay { - RoundedRectangle( - cornerRadius: self.model.cornerRadius.value() - ) - .strokeBorder( - self.model.borderColor?.color ?? .clear, - lineWidth: self.model.borderWidth - ) - } } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index c95a8bd9..5d39cce8 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -18,6 +18,7 @@ open class UKButton: FullWidthComponent, UKComponent { /// A Boolean value indicating whether the button is pressed. public private(set) var isPressed: Bool = false { didSet { + guard self.model.isCustomTapAnimationEnabled else { return } UIView.animate(withDuration: 0.05, delay: 0, options: [.curveEaseOut]) { self.transform = self.isPressed && self.model.isInteractive ? .init( @@ -43,6 +44,9 @@ open class UKButton: FullWidthComponent, UKComponent { /// An optional image displayed alongside the title. public let imageView = UIImageView() + /// The visual effect container used to render blur and liquid glass button backgrounds. + public let backgroundEffectView = UIVisualEffectView() + // MARK: Private Properties private var imageViewConstraints = LayoutConstraints() @@ -79,7 +83,8 @@ open class UKButton: FullWidthComponent, UKComponent { // MARK: Setup private func setup() { - self.addSubview(self.stackView) + self.addSubview(self.backgroundEffectView) + self.backgroundEffectView.contentView.addSubview(self.stackView) self.stackView.addArrangedSubview(self.loaderView) self.stackView.addArrangedSubview(self.titleLabel) @@ -101,6 +106,7 @@ open class UKButton: FullWidthComponent, UKComponent { private func style() { Self.Style.mainView(self, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) Self.Style.titleLabel(self.titleLabel, model: self.model) Self.Style.configureStackView(self.stackView, model: self.model) Self.Style.loaderView(self.loaderView, model: self.model) @@ -110,6 +116,7 @@ open class UKButton: FullWidthComponent, UKComponent { // MARK: Layout private func layout() { + self.backgroundEffectView.allEdges() self.stackView.center() self.imageViewConstraints = self.imageView.size( @@ -121,7 +128,10 @@ open class UKButton: FullWidthComponent, UKComponent { open override func layoutSubviews() { super.layoutSubviews() - self.layer.cornerRadius = self.model.cornerRadius.value(for: self.bounds.height) + let cornerRadius = self.model.cornerRadius.value(for: self.bounds.height) + self.layer.cornerRadius = cornerRadius + self.backgroundEffectView.layer.cornerRadius = cornerRadius + self.backgroundEffectView.contentView.layer.cornerRadius = cornerRadius } // MARK: Update @@ -218,7 +228,8 @@ open class UKButton: FullWidthComponent, UKComponent { // MARK: Helpers @objc private func handleTraitChanges() { - self.layer.borderColor = self.model.borderColor?.uiColor.cgColor + Self.Style.mainView(self, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) } } @@ -227,13 +238,21 @@ open class UKButton: FullWidthComponent, UKComponent { extension UKButton { fileprivate enum Style { static func mainView(_ view: UIView, model: Model) { - view.layer.borderWidth = model.borderWidth - view.layer.borderColor = model.borderColor?.uiColor.cgColor - view.backgroundColor = model.backgroundColor?.uiColor + view.backgroundColor = nil view.layer.cornerRadius = model.cornerRadius.value( for: view.bounds.height ) } + static func backgroundEffectView(_ view: UIVisualEffectView, model: Model) { + view.setBackgroundStyle( + model.backgroundStyle, + backgroundColor: model.backgroundColor?.uiColor, + borderColor: model.borderColor?.uiColor, + borderWidth: model.borderWidth, + cornerRadius: model.cornerRadius.value(for: view.bounds.height), + isGlassInteractive: model.isInteractive + ) + } static func titleLabel(_ label: UILabel, model: Model) { label.textAlignment = .center label.text = model.title @@ -255,7 +274,7 @@ extension UKButton { view.isVisible = model.isLoading } static func imageView(_ imageView: UIImageView, model: Model) { - imageView.image = model.imageWithLegacyFallback?.uiImage + imageView.image = model.image?.uiImage imageView.contentMode = .scaleAspectFit imageView.isHidden = model.isImageHidden imageView.tintColor = model.foregroundColor.uiColor diff --git a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift index 2103b564..ce37b9ee 100644 --- a/Sources/ComponentsKit/Components/Card/Models/CardVM.swift +++ b/Sources/ComponentsKit/Components/Card/Models/CardVM.swift @@ -8,7 +8,10 @@ public struct CardVM: ComponentVM { public var animationScale: AnimationScale = .medium /// The background color of the card. - public var backgroundColor: UniversalColor = .background + public var backgroundColor: UniversalColor? = .background + + /// Defines how the card renders its background. + public var backgroundStyle: BackgroundStyle = .solid /// The border color of the card. public var borderColor: UniversalColor = .divider @@ -41,3 +44,16 @@ public struct CardVM: ComponentVM { /// Initializes a new instance of `CardVM` with default values. public init() {} } + +// MARK: - Helpers + +extension CardVM { + var isTapAnimationEnabled: Bool { + switch self.backgroundStyle { + case .solid, .blur: + return self.isTappable + case .liquidGlass: + return false + } + } +} diff --git a/Sources/ComponentsKit/Components/Card/SUCard.swift b/Sources/ComponentsKit/Components/Card/SUCard.swift index 63736e73..e3afc0e6 100644 --- a/Sources/ComponentsKit/Components/Card/SUCard.swift +++ b/Sources/ComponentsKit/Components/Card/SUCard.swift @@ -47,14 +47,13 @@ public struct SUCard: View { public var body: some View { self.content() .padding(self.model.contentPaddings.edgeInsets) - .background(self.model.backgroundColor.color) - .cornerRadius(self.model.cornerRadius.value) - .overlay( - RoundedRectangle(cornerRadius: self.model.cornerRadius.value) - .strokeBorder( - self.model.borderColor.color, - lineWidth: self.model.borderWidth.value - ) + .componentBackground( + shape: RoundedRectangle(cornerRadius: self.model.cornerRadius.value), + backgroundStyle: self.model.backgroundStyle, + backgroundColor: self.model.backgroundColor?.color, + borderColor: self.model.borderColor.color, + borderWidth: self.model.borderWidth.value, + isGlassInteractive: self.model.isTappable ) .shadow(self.model.shadow) .observeSize { self.contentSize = $0 } @@ -71,7 +70,7 @@ public struct SUCard: View { .onEnded { _ in self.scale = 1.0 }, - isEnabled: self.model.isTappable + isEnabled: self.model.isTapAnimationEnabled ) .scaleEffect(self.scale, anchor: .center) .animation(.easeOut(duration: 0.05), value: self.scale) diff --git a/Sources/ComponentsKit/Components/Card/UKCard.swift b/Sources/ComponentsKit/Components/Card/UKCard.swift index 8c620dd4..14514b94 100644 --- a/Sources/ComponentsKit/Components/Card/UKCard.swift +++ b/Sources/ComponentsKit/Components/Card/UKCard.swift @@ -20,6 +20,8 @@ open class UKCard: UIView, UKComponent { /// The primary content of the card, provided as a custom view. public let content: Content + /// The visual effect container used to render blur and liquid glass card backgrounds. + public let backgroundEffectView = UIVisualEffectView() // MARK: - Public Properties @@ -29,6 +31,7 @@ open class UKCard: UIView, UKComponent { /// A Boolean value indicating whether the button is pressed. public private(set) var isPressed: Bool = false { didSet { + guard self.model.isTapAnimationEnabled else { return } UIView.animate(withDuration: 0.05, delay: 0, options: [.curveEaseOut]) { self.transform = self.isPressed ? .init( @@ -82,7 +85,8 @@ open class UKCard: UIView, UKComponent { /// Sets up the card's subviews. open func setup() { - self.addSubview(self.content) + self.addSubview(self.backgroundEffectView) + self.backgroundEffectView.contentView.addSubview(self.content) if #available(iOS 17.0, *) { self.registerForTraitChanges([UITraitUserInterfaceStyle.self]) { (view: Self, _: UITraitCollection) in @@ -96,24 +100,29 @@ open class UKCard: UIView, UKComponent { /// Applies styling to the card's subviews. open func style() { Self.Style.mainView(self, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) } // MARK: - Layout /// Configures the layout. open func layout() { + self.backgroundEffectView.allEdges() self.contentConstraints = LayoutConstraints.merged { - self.content.top(self.model.contentPaddings.top) - self.content.bottom(self.model.contentPaddings.bottom) - self.content.leading(self.model.contentPaddings.leading) - self.content.trailing(self.model.contentPaddings.trailing) + self.content.top(self.model.contentPaddings.top, to: self.backgroundEffectView.contentView) + self.content.bottom(self.model.contentPaddings.bottom, to: self.backgroundEffectView.contentView) + self.content.leading(self.model.contentPaddings.leading, to: self.backgroundEffectView.contentView) + self.content.trailing(self.model.contentPaddings.trailing, to: self.backgroundEffectView.contentView) } } open override func layoutSubviews() { super.layoutSubviews() - self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + self.layer.shadowPath = UIBezierPath( + roundedRect: self.bounds, + cornerRadius: self.model.cornerRadius.value + ).cgPath } // MARK: - Update @@ -192,17 +201,25 @@ open class UKCard: UIView, UKComponent { @objc private func handleTraitChanges() { Self.Style.mainView(self, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) } } extension UKCard { fileprivate enum Style { static func mainView(_ view: UIView, model: Model) { - view.backgroundColor = model.backgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value - view.layer.borderWidth = model.borderWidth.value - view.layer.borderColor = model.borderColor.cgColor view.shadow(model.shadow) } + static func backgroundEffectView(_ view: UIVisualEffectView, model: Model) { + view.setBackgroundStyle( + model.backgroundStyle, + backgroundColor: model.backgroundColor?.uiColor, + borderColor: model.borderColor.uiColor, + borderWidth: model.borderWidth.value, + cornerRadius: model.cornerRadius.value, + isGlassInteractive: model.isTappable + ) + } } } diff --git a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift index dee6ad47..357c3983 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/SUCircularProgress.swift @@ -16,19 +16,6 @@ public struct SUCircularProgress: View { // MARK: - Initializer - /// Initializer. - /// - Parameters: - /// - currentValue: Current progress. - /// - model: A model that defines the appearance properties. - @available(*, deprecated, message: "Set `currentValue` in the model instead.") - public init( - currentValue: CGFloat = 0, - model: CircularProgressVM = .init() - ) { - self.currentValue = currentValue - self.model = model - } - /// Initializer. /// - Parameters: /// - model: A model that defines the appearance properties. diff --git a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift index c1777454..dce73525 100644 --- a/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift +++ b/Sources/ComponentsKit/Components/CircularProgress/UKCircularProgress.swift @@ -42,24 +42,6 @@ open class UKCircularProgress: UIView, UKComponent { // MARK: - Initialization - /// Initializer. - /// - Parameters: - /// - initialValue: The initial progress value. Defaults to `0`. - /// - model: The model that defines the appearance properties. - @available(*, deprecated, message: "Set `currentValue` in the model instead.") - public init( - initialValue: CGFloat = 0, - model: CircularProgressVM = .init() - ) { - self.model = model - self.currentValue = initialValue - super.init(frame: .zero) - - self.setup() - self.style() - self.layout() - } - /// Initializer. /// - Parameters: /// - model: The model that defines the appearance properties. diff --git a/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift b/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift index e45df5c5..570e48e2 100644 --- a/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift +++ b/Sources/ComponentsKit/Components/Countdown/UKCountdown.swift @@ -3,7 +3,7 @@ import Combine import UIKit /// A UIKit timer component that counts down from a specified duration to zero. -public class UKCountdown: UIView, UKComponent { +open class UKCountdown: UIView, UKComponent { // MARK: - Public Properties /// A model that defines the appearance properties. @@ -61,7 +61,7 @@ public class UKCountdown: UIView, UKComponent { self.layout() } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift index e16a4f8a..30dbfc31 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/BottomModalVM.swift @@ -5,6 +5,9 @@ public struct BottomModalVM: ModalVM { /// The background color of the modal. public var backgroundColor: UniversalColor? + /// Defines how modal renders its background. + public var backgroundStyle: BackgroundStyle = .solid + /// The border thickness of the modal. /// /// Defaults to `.small`. diff --git a/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift index ca0e0b47..ae739b7e 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/CenterModalVM.swift @@ -5,6 +5,9 @@ public struct CenterModalVM: ModalVM { /// The background color of the modal. public var backgroundColor: UniversalColor? + /// Defines how modal renders its background. + public var backgroundStyle: BackgroundStyle = .solid + /// The border thickness of the modal. /// /// Defaults to `.small`. diff --git a/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift b/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift index 55af7ea3..137e178b 100644 --- a/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift +++ b/Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift @@ -5,6 +5,9 @@ public protocol ModalVM: ComponentVM { /// The background color of the modal. var backgroundColor: UniversalColor? { get set } + /// Defines how modal renders its background. + var backgroundStyle: BackgroundStyle { get set } + /// The border thickness of the modal. var borderWidth: BorderWidth { get set } @@ -36,10 +39,15 @@ public protocol ModalVM: ComponentVM { // MARK: - Helpers extension ModalVM { - var preferredBackgroundColor: UniversalColor { - return self.backgroundColor ?? .themed( - light: UniversalColor.background.light, - dark: UniversalColor.secondaryBackground.dark - ) + var preferredBackgroundColor: UniversalColor? { + switch self.backgroundStyle { + case .solid: + return self.backgroundColor ?? .themed( + light: UniversalColor.background.light, + dark: UniversalColor.secondaryBackground.dark + ) + case .liquidGlass, .blur: + return self.backgroundColor + } } } diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift index 96ab9a8f..59011e82 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift @@ -55,11 +55,12 @@ struct ModalContent: View { .padding(.bottom, self.model.contentPaddings.bottom) } .frame(maxWidth: self.model.size.maxWidth, alignment: .leading) - .background(self.model.preferredBackgroundColor.color) - .clipShape(RoundedRectangle(cornerRadius: self.model.cornerRadius.value)) - .overlay( - RoundedRectangle(cornerRadius: self.model.cornerRadius.value) - .strokeBorder(UniversalColor.divider.color, lineWidth: self.model.borderWidth.value) + .componentBackground( + shape: RoundedRectangle(cornerRadius: model.cornerRadius.value), + backgroundStyle: self.model.backgroundStyle, + backgroundColor: self.model.preferredBackgroundColor?.color, + borderColor: UniversalColor.divider.color, + borderWidth: self.model.borderWidth.value ) .padding(self.model.outerPaddings.edgeInsets) } diff --git a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift index 87f549c5..2cd2c4a1 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalOverlay.swift @@ -17,7 +17,7 @@ struct ModalOverlay: View { Group { switch self.model.overlayStyle { case .dimmed: - Color.black.opacity(0.7) + Color.black.opacity(0.35) case .blurred: Color.clear.background(.ultraThinMaterial) case .transparent: diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift index 802a14d7..81dd1ff6 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKBottomModalController.swift @@ -28,7 +28,7 @@ import UIKit /// /// vc.present(bottomModal, animated: true) /// ``` -public class UKBottomModalController: UKModalController { +open class UKBottomModalController: UKModalController { // MARK: - Initialization /// Initializer. diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift index 8f6387c2..3111cec1 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKCenterModalController.swift @@ -28,7 +28,7 @@ import UIKit /// /// vc.present(centerModal, animated: true) /// ``` -public class UKCenterModalController: UKModalController { +open class UKCenterModalController: UKModalController { // MARK: - Initialization /// Initializer. diff --git a/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift index 51e3e238..d8c35efb 100644 --- a/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift +++ b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift @@ -27,6 +27,8 @@ open class UKModalController: UIViewController { public var footer: UIView? /// The content view, holding the header, body, and footer. public let contentView = UIView() + /// The visual effect container used to render blur and liquid glass modal backgrounds. + public let backgroundEffectView = UIVisualEffectView() /// A scrollable wrapper for the body content. public let bodyWrapper: UIScrollView = ContentSizedScrollView() /// The overlay view that appears behind the modal. @@ -76,12 +78,13 @@ open class UKModalController: UIViewController { open func setup() { self.view.addSubview(self.overlay) self.view.addSubview(self.contentView) + self.contentView.addSubview(self.backgroundEffectView) if let header { - self.contentView.addSubview(header) + self.backgroundEffectView.contentView.addSubview(header) } - self.contentView.addSubview(self.bodyWrapper) + self.backgroundEffectView.contentView.addSubview(self.bodyWrapper) if let footer { - self.contentView.addSubview(footer) + self.backgroundEffectView.contentView.addSubview(footer) } self.bodyWrapper.addSubview(self.body) @@ -141,6 +144,7 @@ open class UKModalController: UIViewController { open func style() { Self.Style.overlay(self.overlay, model: self.model) Self.Style.contentView(self.contentView, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) Self.Style.bodyWrapper(self.bodyWrapper) } @@ -149,6 +153,7 @@ open class UKModalController: UIViewController { /// Configures the layout of the modal's subviews. open func layout() { self.overlay.allEdges() + self.backgroundEffectView.allEdges() if let header { header.top(self.model.contentPaddings.top) @@ -242,6 +247,7 @@ open class UKModalController: UIViewController { @objc private func handleTraitChanges() { Self.Style.contentView(self.contentView, model: self.model) + Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model) } } @@ -252,7 +258,7 @@ extension UKModalController { static func overlay(_ view: UIView, model: VM) { switch model.overlayStyle { case .dimmed: - view.backgroundColor = .black.withAlphaComponent(0.7) + view.backgroundColor = .black.withAlphaComponent(0.35) case .transparent: view.backgroundColor = .clear case .blurred: @@ -260,10 +266,16 @@ extension UKModalController { } } static func contentView(_ view: UIView, model: VM) { - view.backgroundColor = model.preferredBackgroundColor.uiColor view.layer.cornerRadius = model.cornerRadius.value - view.layer.borderColor = UniversalColor.divider.cgColor - view.layer.borderWidth = model.borderWidth.value + } + static func backgroundEffectView(_ view: UIVisualEffectView, model: VM) { + view.setBackgroundStyle( + model.backgroundStyle, + backgroundColor: model.preferredBackgroundColor?.uiColor, + borderColor: UniversalColor.divider.uiColor, + borderWidth: model.borderWidth.value, + cornerRadius: model.cornerRadius.value + ) } static func bodyWrapper(_ scrollView: UIScrollView) { scrollView.delaysContentTouches = false diff --git a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift index 96a1e0cc..8c8e790d 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/SUProgressBar.swift @@ -15,19 +15,6 @@ public struct SUProgressBar: View { // MARK: - Initializer - /// Initializer. - /// - Parameters: - /// - currentValue: The current progress value. - /// - model: A model that defines the appearance properties. - @available(*, deprecated, message: "Set `currentValue` in the model instead.") - public init( - currentValue: CGFloat, - model: ProgressBarVM = .init() - ) { - self.currentValue = currentValue - self.model = model - } - /// Initializer. /// - Parameters: /// - model: A model that defines the appearance properties. diff --git a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift index 1583645b..a275336d 100644 --- a/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift +++ b/Sources/ComponentsKit/Components/ProgressBar/UKProgressBar.swift @@ -50,24 +50,6 @@ open class UKProgressBar: FullWidthComponent, UKComponent { // MARK: - Initialization - /// Initializer. - /// - Parameters: - /// - initialValue: The initial progress value. Defaults to `0`. - /// - model: A model that defines the appearance properties. - @available(*, deprecated, message: "Set `currentValue` in the model instead.") - public init( - initialValue: CGFloat = 0, - model: ProgressBarVM = .init() - ) { - self.currentValue = initialValue - self.model = model - super.init(frame: .zero) - - self.setup() - self.style() - self.layout() - } - /// Initializer. /// - Parameters: /// - model: A model that defines the appearance properties. diff --git a/Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift b/Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift index 532ecf7b..2348da38 100644 --- a/Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift +++ b/Sources/ComponentsKit/Components/RadioGroup/UIKit/RadioGroupItemView.swift @@ -2,7 +2,7 @@ import AutoLayout import UIKit /// A view representing a single radio button item in a radio group. -public class RadioGroupItemView: UIView { +open class RadioGroupItemView: UIView { // MARK: Properties /// A view that represents an outer circle and contains an inner circle. @@ -31,7 +31,7 @@ public class RadioGroupItemView: UIView { // MARK: Initialization - init( + public init( isSelected: Bool, groupVM: RadioGroupVM, itemVM: RadioItemVM @@ -47,7 +47,7 @@ public class RadioGroupItemView: UIView { self.layout() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Sources/ComponentsKit/Helpers/SwiftUI/View+ComponentBackground.swift b/Sources/ComponentsKit/Helpers/SwiftUI/View+ComponentBackground.swift new file mode 100644 index 00000000..a46a6927 --- /dev/null +++ b/Sources/ComponentsKit/Helpers/SwiftUI/View+ComponentBackground.swift @@ -0,0 +1,49 @@ +import SwiftUI + +extension View { + @ViewBuilder + func componentBackground( + shape: BackgroundShape, + backgroundStyle: BackgroundStyle, + backgroundColor: Color?, + borderColor: Color, + borderWidth: CGFloat, + isGlassInteractive: Bool = true + ) -> some View { + switch backgroundStyle { + case .solid: + self + .background(backgroundColor ?? .clear) + .clipShape(shape) + .overlay { + shape.strokeBorder(borderColor, lineWidth: borderWidth) + } + case .blur: + self + .background { + shape + .fill(.thinMaterial) + .overlay { + shape.strokeBorder(borderColor, lineWidth: borderWidth) + } + } + .background(backgroundColor) + .clipShape(shape) + case .liquidGlass: + if #available(iOS 26.0, *) { + self + .overlay { + shape.strokeBorder(borderColor, lineWidth: borderWidth) + } + .glassEffect( + .regular + .tint(backgroundColor) + .interactive(isGlassInteractive), + in: shape + ) + } else { + self + } + } + } +} diff --git a/Sources/ComponentsKit/Helpers/UIKit/UIVisualEffectView+BackgroundStyle.swift b/Sources/ComponentsKit/Helpers/UIKit/UIVisualEffectView+BackgroundStyle.swift new file mode 100644 index 00000000..7f0c7f00 --- /dev/null +++ b/Sources/ComponentsKit/Helpers/UIKit/UIVisualEffectView+BackgroundStyle.swift @@ -0,0 +1,38 @@ +import UIKit + +extension UIVisualEffectView { + func setBackgroundStyle( + _ backgroundStyle: BackgroundStyle, + backgroundColor: UIColor?, + borderColor: UIColor?, + borderWidth: CGFloat, + cornerRadius: CGFloat, + isGlassInteractive: Bool = true + ) { + self.contentView.layer.cornerRadius = cornerRadius + self.layer.cornerRadius = cornerRadius + self.layer.borderColor = borderColor?.cgColor + self.layer.borderWidth = borderWidth + self.clipsToBounds = true + + switch backgroundStyle { + case .solid: + self.effect = nil + self.backgroundColor = backgroundColor + case .blur: + self.effect = UIBlurEffect(style: .systemThinMaterial) + self.backgroundColor = backgroundColor + case .liquidGlass: + if #available(iOS 26.0, *) { + let effect = UIGlassEffect(style: .regular) + effect.tintColor = backgroundColor + effect.isInteractive = isGlassInteractive + self.effect = effect + self.backgroundColor = nil + } else { + self.effect = nil + self.backgroundColor = backgroundColor + } + } + } +} diff --git a/Sources/ComponentsKit/Shared/Types/BackgroundStyle.swift b/Sources/ComponentsKit/Shared/Types/BackgroundStyle.swift new file mode 100644 index 00000000..783ab0f7 --- /dev/null +++ b/Sources/ComponentsKit/Shared/Types/BackgroundStyle.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Defines how a component renders its background. +public enum BackgroundStyle { + /// A regular filled background using the component's configured background color. + case solid + /// A system liquid glass effect that lets underlying content show through the component. + @available(iOS 26.0, *) case liquidGlass + /// A system blur material that softens content behind the component. + case blur +}