求SwiftUI中.glassEffect及菜单容器变形动画的UIKit等效实现
在UIKit中复刻SwiftUI菜单的容器变形动画效果
核心思路
SwiftUI的Menu展开时的容器动画本质是基于自动布局的尺寸过渡动画,配合玻璃态效果(对应UIKit的UIBlurEffect+UIVisualEffectView)。我们需要手动实现菜单的展开/收起状态切换,同时通过Auto Layout约束的动画更新来模拟原生变形效果。
具体实现步骤
1. 搭建基础玻璃态容器
用UIVisualEffectView实现和SwiftUI一致的玻璃效果,作为承载按钮和菜单的容器:
class GlassContainerView: UIVisualEffectView { init() { let blurEffect = UIBlurEffect(style: .systemMaterial) super.init(effect: blurEffect) setupView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupView() { layer.cornerRadius = 12 // 匹配SwiftUI默认圆角 layer.masksToBounds = true clipsToBounds = true // 模拟.interactive()交互态玻璃效果 contentView.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.1) } }
2. 实现图标按钮组件
封装和SwiftUILabel(.iconOnly)、.buttonStyle(.plain)一致的图标按钮:
class IconButton: UIButton { init(systemImageName: String) { super.init(frame: .zero) let config = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium) setImage(UIImage(systemName: systemImageName, withConfiguration: config), for: .normal) tintColor = .label contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12) setupStyle() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupStyle() { // 取消默认点击高亮效果,匹配.plain按钮样式 showsTouchWhenHighlighted = false adjustsImageWhenHighlighted = false } }
3. 实现菜单容器与变形动画
核心用UIStackView管理按钮和菜单选项,通过修改菜单选项的isHidden属性+Auto Layout动画实现平滑过渡:
class MenuContainerViewController: UIViewController { private let glassContainer = GlassContainerView() private let stackView = UIStackView() private let plusButton = IconButton(systemImageName: "plus") private let qrButton = IconButton(systemImageName: "qrcode.viewfinder") // 菜单选项按钮 private let meButton = IconButton(systemImageName: "location.north.fill") private let finButton = IconButton(systemImageName: "flag.pattern.checkered.2.crossed") private var isMenuExpanded = false override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground setupLayout() setupMenuActions() } private func setupLayout() { // 配置StackView,匹配SwiftUI的VStack(spacing:4) stackView.axis = .vertical stackView.spacing = 4 stackView.alignment = .center // 添加初始显示的按钮 stackView.addArrangedSubview(plusButton) stackView.addArrangedSubview(qrButton) // 添加菜单选项(默认隐藏) stackView.addArrangedSubview(meButton) stackView.addArrangedSubview(finButton) meButton.isHidden = true finButton.isHidden = true // 嵌套布局 glassContainer.contentView.addSubview(stackView) view.addSubview(glassContainer) // Auto Layout约束 stackView.translatesAutoresizingMaskIntoConstraints = false glassContainer.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: glassContainer.contentView.topAnchor), stackView.bottomAnchor.constraint(equalTo: glassContainer.contentView.bottomAnchor), stackView.leadingAnchor.constraint(equalTo: glassContainer.contentView.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: glassContainer.contentView.trailingAnchor), glassContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), glassContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) } private func setupMenuActions() { plusButton.addTarget(self, action: #selector(toggleMenu), for: .touchUpInside) // 菜单选项点击事件 meButton.addTarget(self, action: #selector(meButtonTapped), for: .touchUpInside) finButton.addTarget(self, action: #selector(finButtonTapped), for: .touchUpInside) } @objc private func toggleMenu() { isMenuExpanded.toggle() // 用弹簧动画模拟SwiftUI原生菜单的弹性变形效果 UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5) { self.meButton.isHidden = !self.isMenuExpanded self.finButton.isHidden = !self.isMenuExpanded self.view.layoutIfNeeded() // 触发约束更新,实现容器平滑变形 } } @objc private func meButtonTapped() { // 处理Me按钮逻辑 toggleMenu() // 点击后自动收起菜单 } @objc private func finButtonTapped() { // 处理Fin按钮逻辑 toggleMenu() // 点击后自动收起菜单 } }
关键细节说明
- 玻璃效果匹配:使用
UIBlurEffect(style: .systemMaterial)配合半透明背景,还原SwiftUI.glassEffect(.regular.interactive())的视觉效果。 - 动画曲线:通过弹簧参数(damping:0.8, velocity:0.5)模拟SwiftUI原生菜单的弹性过渡动画。
- 布局逻辑:利用
UIStackView的isHidden属性控制菜单选项的显示/隐藏,配合layoutIfNeeded()触发容器尺寸的平滑变化,和SwiftUI自动布局动画逻辑完全一致。
内容的提问来源于stack exchange,提问作者Amir M
相关产品推荐
相关产品推荐

