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

tvOS:如何通过SwiftUI实现类似.card样式的平移/焦点效果?

在tvOS SwiftUI中实现类似.card的平移/焦点效果

SwiftUI没有直接提供和tvOS原生.card样式完全一致的开箱即用接口,但可以通过两种方式实现接近的平移/视差焦点效果:

1. 自定义ButtonStyle + 几何动画

你可以结合FocusState监听焦点状态,再用GeometryEffect实现自定义的偏移和缩放动画,复刻类似.card的视差感:

// 自定义视差效果的几何 modifier
struct ParallaxEffect: GeometryEffect {
    var offset: CGSize
    var scale: CGFloat

    // 支持动画过渡
    var animatableData: AnimatablePair<CGSize.AnimatableData, CGFloat> {
        get { AnimatablePair(offset.animatableData, scale) }
        set {
            offset.animatableData = newValue.first
            scale = newValue.second
        }
    }

    func effectValue(size: CGSize) -> ProjectionTransform {
        ProjectionTransform(CGAffineTransform(translationX: offset.width, y: offset.height)
            .scaledBy(x: scale, y: scale))
    }
}

// 自定义按钮样式
struct CardButtonStyle: ButtonStyle {
    @FocusState private var isFocused: Bool

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .focused($isFocused)
            // 焦点状态下的偏移和缩放参数可按需调整
            .modifier(ParallaxEffect(
                offset: isFocused ? CGSize(width: -12, height: -12) : .zero,
                scale: isFocused ? 1.08 : 1.0
            ))
            // 增加阴影增强立体感,匹配原生card的视觉效果
            .shadow(color: .black.opacity(0.4), radius: isFocused ? 20 : 8, x: isFocused ? 12 : 0, y: isFocused ? 12 : 0)
            .animation(.interactiveSpring(response: 0.2, dampingFraction: 0.8), value: isFocused)
    }
}

// 使用示例
Button("Card Style Button") {
    // 按钮操作
}
.buttonStyle(CardButtonStyle())

这个实现的核心是通过FocusState感知焦点变化,再用ParallaxEffect处理平移和缩放,动画参数可以根据你的视觉需求微调,比如偏移量、缩放比例、动画曲线等。

2. 封装UIKit原生组件(完全匹配系统效果)

如果需要100%还原tvOS原生.card的焦点动效,最稳妥的方式是通过UIViewRepresentable封装UICollectionView——因为原生UICollectionViewCell在焦点变化时自带系统级的平移、阴影和视差效果,无需手动调整参数:

struct NativeCardCollection: UIViewRepresentable {
    let items: [String]
    let onSelect: (String) -> Void

    func makeUIView(context: Context) -> UICollectionView {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 300, height: 200)
        layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.register(NativeCardCell.self, forCellWithReuseIdentifier: "CardCell")
        collectionView.delegate = context.coordinator
        collectionView.dataSource = context.coordinator
        // 开启原生焦点效果
        collectionView.allowsFocus = true
        return collectionView
    }

    func updateUIView(_ uiView: UICollectionView, context: Context) {
        uiView.reloadData()
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, UICollectionViewDelegate, UICollectionViewDataSource {
        let parent: NativeCardCollection

        init(_ parent: NativeCardCollection) {
            self.parent = parent
        }

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            parent.items.count
        }

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCell", for: indexPath) as! NativeCardCell
            cell.label.text = parent.items[indexPath.item]
            return cell
        }

        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            parent.onSelect(parent.items[indexPath.item])
        }
    }
}

// 自定义原生cell,启用card样式
class NativeCardCell: UICollectionViewCell {
    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupView() {
        label.frame = contentView.bounds
        label.textAlignment = .center
        label.textColor = .white
        contentView.addSubview(label)
        // 启用原生card样式
        contentView.backgroundColor = .systemBlue
        self.focusStyle = .card
    }
}

// 使用示例
NativeCardCollection(items: ["Item 1", "Item 2", "Item 3"]) { selectedItem in
    print("Selected: \(selectedItem)")
}

这种方式直接复用系统原生的.card焦点行为,动效和系统自带的完全一致,适合对视觉一致性要求高的场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.03 06:12:37