From 0153bfe8d84e867b8c087878da3450729d3bfd14 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 5 Jun 2026 18:41:36 +0300 Subject: [PATCH 1/2] add background style in buttons --- .../PreviewPages/ButtonPreview.swift | 1 + .../Components/Button/Models/ButtonVM.swift | 14 +++ .../Components/Button/SUButton.swift | 104 ++++++++++++------ .../Components/Button/UKButton.swift | 49 ++++++++- 4 files changed, 131 insertions(+), 37 deletions(-) diff --git a/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift b/Examples/DemosApp/DemosApp/ComponentsPreview/PreviewPages/ButtonPreview.swift index 430476a..b3e1c94 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/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index c42a886..d910b38 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`. @@ -84,6 +89,14 @@ extension ButtonVM { var isInteractive: Bool { self.isEnabled && !self.isLoading } + var isTapAnimationEnabled: Bool { + switch self.backgroundStyle { + case .solid, .blur: + return self.isInteractive + case .liquidGlass: + return false + } + } var preferredLoadingVM: LoadingVM { return self.loadingVM ?? .init { $0.color = .init( @@ -246,6 +259,7 @@ extension ButtonVM { || self.imageWithLegacyFallback != oldModel.imageWithLegacyFallback || 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 ef930ed..0e30182 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -33,15 +33,28 @@ 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) + .buttonBackground( + shape: RoundedRectangle(cornerRadius: self.model.cornerRadius.value()), + model: self.model + ) } - .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.isTapAnimationEnabled ) .disabled(!self.model.isInteractive) .scaleEffect(self.scale, anchor: .center) @@ -109,31 +122,60 @@ 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 - ) + } +} + +extension View { + @ViewBuilder + fileprivate func buttonBackground( + shape: BackgroundShape, + model: ButtonVM + ) -> some View { + switch model.backgroundStyle { + case .solid: + self + .background(model.backgroundColor?.color ?? .clear) + .clipShape(shape) + .overlay { + shape.strokeBorder( + model.borderColor?.color ?? .clear, + lineWidth: model.borderWidth + ) + } + case .blur: + self + .background { + shape + .fill(.thinMaterial) + .overlay { + shape.strokeBorder( + model.borderColor?.color ?? .clear, + lineWidth: model.borderWidth + ) + } + } + .background(model.backgroundColor?.color) + .clipShape(shape) + case .liquidGlass: + if #available(iOS 26.0, *) { + self + .overlay { + shape.strokeBorder( + model.borderColor?.color ?? .clear, + lineWidth: model.borderWidth + ) + } + .glassEffect( + .regular + .tint(model.backgroundColor?.color) + .interactive(model.isInteractive), + in: shape + ) + } else { + self } + } } } diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index c95a8bd..f2a5560 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.isTapAnimationEnabled 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,39 @@ 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) { + let cornerRadius = model.cornerRadius.value(for: view.bounds.height) + view.contentView.layer.cornerRadius = cornerRadius + view.layer.cornerRadius = cornerRadius + view.layer.borderColor = model.borderColor?.uiColor.cgColor + view.layer.borderWidth = model.borderWidth + view.clipsToBounds = true + + switch model.backgroundStyle { + case .solid: + view.effect = nil + view.backgroundColor = model.backgroundColor?.uiColor + case .blur: + view.effect = UIBlurEffect(style: .systemThinMaterial) + view.backgroundColor = model.backgroundColor?.uiColor + case .liquidGlass: + if #available(iOS 26.0, *) { + let effect = UIGlassEffect(style: .regular) + effect.tintColor = model.backgroundColor?.uiColor + effect.isInteractive = model.isInteractive + view.effect = effect + view.backgroundColor = nil + } else { + view.effect = nil + view.backgroundColor = model.backgroundColor?.uiColor + } + } + } static func titleLabel(_ label: UILabel, model: Model) { label.textAlignment = .center label.text = model.title From f815aa216e6f1ffef09b9f8f2364234b8ac08997 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 5 Jun 2026 19:09:46 +0300 Subject: [PATCH 2/2] address warning about custom tap animations in buttons --- Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift | 4 ++-- Sources/ComponentsKit/Components/Button/SUButton.swift | 2 +- Sources/ComponentsKit/Components/Button/UKButton.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift index d910b38..fc90691 100644 --- a/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift +++ b/Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift @@ -89,10 +89,10 @@ extension ButtonVM { var isInteractive: Bool { self.isEnabled && !self.isLoading } - var isTapAnimationEnabled: Bool { + var isCustomTapAnimationEnabled: Bool { switch self.backgroundStyle { case .solid, .blur: - return self.isInteractive + return true case .liquidGlass: return false } diff --git a/Sources/ComponentsKit/Components/Button/SUButton.swift b/Sources/ComponentsKit/Components/Button/SUButton.swift index 0e30182..76f331a 100644 --- a/Sources/ComponentsKit/Components/Button/SUButton.swift +++ b/Sources/ComponentsKit/Components/Button/SUButton.swift @@ -54,7 +54,7 @@ public struct SUButton: View { .onEnded { _ in self.scale = 1.0 }, - isEnabled: self.model.isTapAnimationEnabled + isEnabled: self.model.isCustomTapAnimationEnabled ) .disabled(!self.model.isInteractive) .scaleEffect(self.scale, anchor: .center) diff --git a/Sources/ComponentsKit/Components/Button/UKButton.swift b/Sources/ComponentsKit/Components/Button/UKButton.swift index f2a5560..b730db1 100644 --- a/Sources/ComponentsKit/Components/Button/UKButton.swift +++ b/Sources/ComponentsKit/Components/Button/UKButton.swift @@ -18,7 +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.isTapAnimationEnabled else { return } + guard self.model.isCustomTapAnimationEnabled else { return } UIView.animate(withDuration: 0.05, delay: 0, options: [.curveEaseOut]) { self.transform = self.isPressed && self.model.isInteractive ? .init(