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..9615a69d 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) { diff --git a/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift b/Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift index 5999bfc2..a619a29a 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 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..67722c00 100644 --- a/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift +++ b/Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift @@ -55,11 +55,9 @@ 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) + .modalBackground( + shape: RoundedRectangle(cornerRadius: model.cornerRadius.value), + model: self.model ) .padding(self.model.outerPaddings.edgeInsets) } @@ -74,3 +72,43 @@ struct ModalContent: View { return self.bodySize.height + self.bodyTopPadding + self.bodyBottomPadding } } + +extension View { + @ViewBuilder + fileprivate func modalBackground( + shape: BackgroundShape, + model: any ModalVM + ) -> some View { + switch model.backgroundStyle { + case .solid: + self.background(model.preferredBackgroundColor?.color) + .clipShape(shape) + .overlay( + shape + .strokeBorder(UniversalColor.divider.color, lineWidth: model.borderWidth.value) + ) + case .blur: + self + .background { + shape + .fill(.thinMaterial) + .overlay { + shape.strokeBorder(UniversalColor.divider.color, lineWidth: model.borderWidth.value) + } + } + .background(model.preferredBackgroundColor?.color) + .clipShape(shape) + case .liquidGlass: + if #available(iOS 26.0, *) { + self.glassEffect( + .regular + .tint(model.preferredBackgroundColor?.color) + .interactive(), + in: shape + ) + } else { + self + } + } + } +} 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/UKModalController.swift b/Sources/ComponentsKit/Components/Modal/UIKit/UKModalController.swift index 51e3e238..d0bce106 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,31 @@ extension UKModalController { } } static func contentView(_ view: UIView, model: VM) { - view.backgroundColor = model.preferredBackgroundColor.uiColor + view.layer.cornerRadius = model.cornerRadius.value + } + static func backgroundEffectView(_ view: UIVisualEffectView, model: VM) { view.layer.cornerRadius = model.cornerRadius.value view.layer.borderColor = UniversalColor.divider.cgColor view.layer.borderWidth = model.borderWidth.value + view.clipsToBounds = true + + switch model.backgroundStyle { + case .solid: + view.effect = nil + view.backgroundColor = model.preferredBackgroundColor?.uiColor + case .blur: + view.effect = UIBlurEffect(style: .systemThinMaterial) + view.backgroundColor = model.preferredBackgroundColor?.uiColor + case .liquidGlass: + if #available(iOS 26.0, *) { + let effect = UIGlassEffect(style: .regular) + effect.tintColor = model.preferredBackgroundColor?.uiColor + effect.isInteractive = true + view.effect = effect + } else { + view.effect = nil + } + } } static func bodyWrapper(_ scrollView: UIScrollView) { scrollView.delaysContentTouches = false 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 +}