You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 02:44:50