Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions Sources/ComponentsKit/Components/Alert/Models/AlertVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
18 changes: 13 additions & 5 deletions Sources/ComponentsKit/Components/Modal/Models/ModalVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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
}
}
}
48 changes: 43 additions & 5 deletions Sources/ComponentsKit/Components/Modal/SwiftUI/ModalContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,9 @@ struct ModalContent<VM: ModalVM, Header: View, Body: View, Footer: View>: 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)
}
Expand All @@ -74,3 +72,43 @@ struct ModalContent<VM: ModalVM, Header: View, Body: View, Footer: View>: View {
return self.bodySize.height + self.bodyTopPadding + self.bodyBottomPadding
}
}

extension View {
@ViewBuilder
fileprivate func modalBackground<BackgroundShape: InsettableShape>(
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.fill(model.preferredBackgroundColor?.color ?? .clear)
}
.overlay {
shape.strokeBorder(UniversalColor.divider.color, lineWidth: model.borderWidth.value)
}
}
case .liquidGlass:
if #available(iOS 26.0, *) {
self.glassEffect(
.regular
.tint(model.preferredBackgroundColor?.color)
.interactive(),
in: shape
)
} else {
self
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct ModalOverlay<VM: ModalVM>: 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ open class UKModalController<VM: ModalVM>: 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.
Expand Down Expand Up @@ -76,12 +78,13 @@ open class UKModalController<VM: ModalVM>: 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)
Expand Down Expand Up @@ -141,6 +144,7 @@ open class UKModalController<VM: ModalVM>: 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)
}

Expand All @@ -149,6 +153,7 @@ open class UKModalController<VM: ModalVM>: 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)
Expand Down Expand Up @@ -242,6 +247,7 @@ open class UKModalController<VM: ModalVM>: UIViewController {

@objc private func handleTraitChanges() {
Self.Style.contentView(self.contentView, model: self.model)
Self.Style.backgroundEffectView(self.backgroundEffectView, model: self.model)
}
}

Expand All @@ -252,19 +258,39 @@ 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:
(view as? UIVisualEffectView)?.effect = UIBlurEffect(style: .systemUltraThinMaterial)
}
}
static func contentView(_ view: UIView, model: VM) {
view.backgroundColor = model.preferredBackgroundColor.uiColor
view.backgroundColor = model.preferredBackgroundColor?.uiColor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep effect backgrounds transparent when tinted

When backgroundStyle is .blur or .liquidGlass and callers also set backgroundColor, this line paints the whole contentView behind the UIVisualEffectView with that color. Because the effect view is then blurring/glassing a same-sized solid parent background instead of the presenting content, the UIKit modal no longer shows the content behind it; SwiftUI instead applies the color as a tint overlay. The filled background should be limited to .solid (or otherwise moved into the effect/tint layer) so tinted blur/glass stays translucent.

Useful? React with 👍 / 👎.

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.layer.cornerRadius = model.cornerRadius.value
view.clipsToBounds = true

switch model.backgroundStyle {
case .solid:
view.effect = nil
case .blur:
view.effect = UIBlurEffect(style: .systemThinMaterial)
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
scrollView.contentInsetAdjustmentBehavior = .never
Expand Down
11 changes: 11 additions & 0 deletions Sources/ComponentsKit/Shared/Types/BackgroundStyle.swift
Original file line number Diff line number Diff line change
@@ -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
}