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

UITableView单元格中带matchedGeometryEffect的SwiftUI视图动画跳变问题

UITableView单元格中SwiftUI视图带matchedGeometryEffect的动画跳变问题

问题说明

  • 已制作最小可复现示例(MRE),用于展示UITableView单元格内,带有matchedGeometryEffect的SwiftUI视图在高度更新时,动画过程存在视觉跳变的问题。
  • 最初单元格完全无法调整高度,即便将UIHostingController的sizingOptions设置为intrinsicContentSize或preferredContentSize也无效。
  • 为解决高度不更新的问题,我在视图中添加了onSizeChange闭包,现在单元格高度能正常调整,但动画启动时会出现不符合动画曲线的奇怪跳变。
  • 当前限制:暂时无法使用新的UIHostingConfiguration单元格API(曾尝试使用,但出现未调试的空白单元格,若该方案可行则会考虑)。

最小可复现代码

import SwiftUI
import UIKit
import PlaygroundSupport

// MARK: - SwiftUI View

struct RootView: View {
    // 视图尺寸变化时触发的回调
    var onSizeChange: ((CGSize) -> Void)? = nil
    @State private var isExpanded = false
    @Namespace var namespace

    var body: some View {
        Group {
            if isExpanded {
                subview1
                Text("d")
                Text("e")
                Text("f")
                Text("g")
            } else {
                Text("a")
                Text("b")
                Text("c")
                subview1
            }
        }
        // 通过GeometryReader检测尺寸变化并触发回调
        .background(GeometryReader { geo in
            Color.clear
                .onAppear {
                    onSizeChange?(geo.size)
                }
                .onChange(of: geo.size) { newSize in
                    onSizeChange?(newSize)
                }
        })
        .onTapGesture {
            isExpanded.toggle()
        }
        .background(Color.green)
        .animation(.easeInOut, value: isExpanded)
    }

    var subview1: some View {
        Text("animatable view")
            .border(Color.red)
            .matchedGeometryEffect(id: "1", in: namespace)
            .id("1")
    }
}

// MARK: - 自定义Table View Cell

class HostingTableViewCell: UITableViewCell {
    // 持有UIHostingController的引用
    var host: UIHostingController<RootView>?

    func configure() {
        // 创建带尺寸变化回调的hosting控制器
        let host = UIHostingController(rootView: RootView(onSizeChange: { [weak self] newSize in
            guard let self = self else { return }
            // 失效固有内容尺寸
            self.host?.view.invalidateIntrinsicContentSize()
            // 强制表格视图更新布局
            if let tableView = self.superview as? UITableView ?? self.superview?.superview as? UITableView {
                tableView.beginUpdates()
                tableView.endUpdates()
            }
        }))
        self.host = host

        host.sizingOptions = .intrinsicContentSize
        host.view.translatesAutoresizingMaskIntoConstraints = false
        host.view.backgroundColor = .blue

        contentView.addSubview(host.view)

        NSLayoutConstraint.activate([
            host.view.topAnchor.constraint(equalTo: contentView.topAnchor),
            host.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            host.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            host.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
        ])
    }
}

// MARK: - 宿主视图控制器

class HostViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.delegate = self
        tableView.dataSource = self
        tableView.allowsSelection = false
        tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "cell")

        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HostingTableViewCell
        cell.configure()
        return cell
    }
}

// MARK: - 启动Playground

// PlaygroundPage.current.liveView = UIHostingController(rootView: RootView())
PlaygroundPage.current.liveView = HostViewController()
PlaygroundPage.current.needsIndefiniteExecution = true

内容的提问来源于stack exchange,提问作者fingia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:15:06