SwiftUI:将视图从一个堆栈移动到另一个堆栈?

Posted

技术标签:

【中文标题】SwiftUI:将视图从一个堆栈移动到另一个堆栈?【英文标题】:SwiftUI: animate movement of a view from one stack to another? 【发布时间】:2019-11-20 22:45:04 【问题描述】:

背景

我正在使用 SwiftUI 实现类似纸牌的纸牌游戏,其中包括堆叠纸牌以及将纸牌从一个堆栈移动到另一个堆栈的能力,例如:

在此处的示例中,当我点击“移动卡片”按钮时,♠️9 从第 1 列移动到第 2 列,位于❤️10 的顶部,动画是♠️9 从第一列,而新的 ♠️9 同时在第二列淡入。

我想要的是让 ♠️9 视图本身在 ❤️10 顶部动画从其原始位置到其最终位置

(在我的实际应用中,卡片也可以在其他位置,但实现类似于ColumnView。)

代码

我有一个由Column 类组成的数据模型,该类包含Card 对象的数组,以及对应的ColumnViewCardView 结构:

public class Column: Identifiable, ObservableObject 
    public let id: Int
    @Published var stack = [Card]()

    public init(id: Int, cards: [Card]? = nil) 
        self.id = id
        self.stack = cards ?? []
    

    public func push(_ card: Card) 
        stack.append(card)
    

    public func pop() -> Card 
        guard !stack.isEmpty else  fatalError() 
        return stack.removeLast()
    

    public func orderIndex(for card: Card) -> Int 
        return stack.firstIndex(of: card) ?? -1
    


public struct Card: Equatable, Identifiable 
    public let suit: Suit
    public let rank: Rank

    public var id: String  return displayTitle 

    public init(suit: Suit, rank: Rank) 
        self.suit = suit
        self.rank = rank
    

    public var displayTitle: String 
        return "\(suit.displayTitle)\(rank.displayTitle)"
    


public struct ColumnView: View 
    @ObservedObject var column: Column

    public init(column: Column) 
        self.column = column
    

    public var body: some View 
        ZStack 
            ForEach(column.stack)  card in
                CardView(card: card)
                    .offset(x: 0, y: 35*CGFloat(self.column.orderIndex(for: card)))
                    .frame(width: 125, height: 187)
            
        
    



public struct CardView: View 
    let card: Card

    public init(card: Card) 
        self.card = card
    

    public var body: some View 
        // Implementation that draws the card
    

最后是一个包装Column 对象的Board 对象,以及一个要显示的对应BoardView(本例中的主屏幕):

struct Board 
    let column1: Column
    let column2: Column

    init() 
        // Convenience method for Card.ten.ofDiamonds, etc not shown.
        column1 = Column(id: 0, cards: [
            Card.ten.ofDiamonds,
            Card.nine.ofSpades
        ])

        column2 = Column(id: 1, cards: [
            Card.king.ofSpades,
            Card.queen.ofDiamonds,
            Card.jack.ofClubs,
            Card.ten.ofHearts
        ])
    

    func moveCard() 
        let card = column1.pop()
        column2.push(card)
    


struct BoardView: View 
    let board = Board()

    var body: some View 
        ZStack(alignment: .center) 
            Rectangle()
                .foregroundColor(.green)
                .frame(maxWidth: .infinity, maxHeight: .infinity)

            HStack(spacing: 20) 
                ColumnView(column: board.column1)
                ColumnView(column: board.column2)
            

            Button(action: 
                withAnimation 
                    self.board.moveCard()
                
            ) 
                Text("Move Card")
                    .font(.system(size: 20, weight: .bold, design: .default))
                    .foregroundColor(.white)
            
            .offset(x: 0, y: 300)
        .edgesIgnoringSafeArea(.all)
    

问题

有道理为什么会发生淡出/淡入 - 从第一个 ColumnView 的角度来看,column1 丢失了一张卡片(卡片已从 column1 数组中移除),因此它会以淡出方式过渡;从第二个ColumnView 的角度来看,column2 获得了一张卡片(卡片已添加到column2 数组中),因此它会以淡入方式过渡。

问题是,有没有一种方法可以实现我想要的,而不必完全重新构建我设置的数据模型和视图层次结构,Cards 在Columns 和CardViews 内在ColumnViews 里面?让数据模型保持原来的样子真的很有帮助,而且将数据模型直接映射到视图层次结构也很有帮助。

我可以想象某种替代视图层次结构,其中只有 52 个CardViews,并根据它们所在的列或位置对位置进行某种修饰,但这感觉很hacky。我考虑过某种自定义transition,但转换是用于插入或删除的视图。有没有办法告诉 SwiftUI Card 结构没有被删除和重新添加,而是 moved,因此执行匹配的移动动画?

【问题讨论】:

很想知道除了下面的答案之外,您是否找到了任何相关信息! 遗憾的是,我向 Apple 提交了 DTS 票证,他们说目前无法在 SwiftUI 中做我想做的事情。我最终将“所有 52 张卡片”数组公开为计算属性,并使用锚首选项和偏移量来设置卡片的位置以允许它们进行动画处理。 (换句话说,基本上是我希望避免的解决方案。) 当...谢谢你让我知道,至少很高兴听到 Apple 的确认(建议你自己用那个答案回答这个问题,也许指定当前的 Xcode 版本,因为希望它会在未来改变!) 我想知道是否有可能实现你在 UIKit 中可能做的事情,当一张卡片需要离开一个视图时让它出现在 zstack 的顶部,动画它移动到它的目标视图和然后将它插入那里......估计它可能会变得混乱,但我可能会试一试! 如何将 VStacks 用于“堆栈”(列),然后锚定到它的底部,将“9”卡移动到那里,然后对用户没有动画和跟踪,移除初始卡从视图层次结构中并在其位置创建一个新的“9”卡,位于 VStack 内?您不必再暴露所有卡片,只需暴露 VStacks。下次你在这里移动一张卡片时,VStack 的底部锚点会更低。我认为给出了列数,这有助于解决此问题。 【参考方案1】:

我认为对于您的情况,您需要为这种行为计算每张卡与某些 @State var(或更多变量)的偏移量。我拿了你的代码并更改了BoardView。当您点击按钮时,每张卡片偏移都会发生变化。这只是显示卡片如何移动到新位置并可能为您提供正确方向的示例:

struct BoardView: View 
    let board = Board()

    @State var cards = [
        Card.ten.ofDiamonds,
        Card.nine.ofSpades,
        Card.king.ofSpades,
        Card.queen.ofDiamonds,
        Card.jack.ofClubs,
        Card.ten.ofHearts
    ]

    @State var column1: Int = 2

    var body: some View 

        ZStack(alignment: .center) 
            Rectangle()
                .foregroundColor(.green)
                .frame(maxWidth: .infinity, maxHeight: .infinity)


            ZStack 
                ForEach(0..<self.cards.count, id: \.self)  cardIndex in
                    CardView(card: self.cards[cardIndex])
                        .offset(x: cardIndex < self.column1 ? 0 : 135,
                                y: 35*CGFloat(cardIndex >= self.column1 ? cardIndex - self.column1 : cardIndex))
                        .frame(width: 125, height: 187)
                
            .offset(x: -80, y: 0)


            Button(action: 
                withAnimation 
                    self.column1 -= 1

                
            ) 
                Text("Move Card")
                    .font(.system(size: 20, weight: .bold, design: .default))
                    .foregroundColor(.white)
            
            .offset(x: 0, y: 300)
        .edgesIgnoringSafeArea(.all)

    

所以也许正确的解决方案是将卡片从一个数组移动到另一个数组,而不是改变它的偏移量。

【讨论】:

谢谢 - 这就是我所说的“我可以想象某种替代视图层次结构,其中只有 52 个 CardViews,并根据它们所在的列或位置对位置进行某种修饰,但这感觉很hacky。”将 52 张卡片放在一个没有组织的巨型视图中,感觉不是很 Swift-y 或 SwiftUI-y。此外,我的实际使用类似于纸牌,并且卡片位置可能比仅在列中更多 - 卡片也可以在其他位置 - 所以即使仅使用偏移量单独处理列也无济于事。【参考方案2】:

(复制自对该问题的评论)

从 Xcode 11.3、ios 13.3 开始:

遗憾的是,我向 Apple 提交了 DTS 请求,他们说目前无法在 SwiftUI 中完成我想做的事情。我最终将“所有 52 张卡片”数组公开为计算属性,并使用锚首选项和偏移量来设置卡片的位置以允许它们进行动画处理。 (换句话说,基本上是我希望避免的解决方案。)希望这在 iOS 14 / Xcode 12 中得到改进。

【讨论】:

以上是关于SwiftUI:将视图从一个堆栈移动到另一个堆栈?的主要内容,如果未能解决你的问题,请参考以下文章

如何通过拖动在SwiftUI ZStack中重新排列视图

SwiftUI - 在导航堆栈中弹回不会取消分配视图

没有导航控制器堆栈的视图控制器从一个到另一个的动画转换

如何在我的 Xcode Swift 项目的不同导航堆栈中从一个视图控制器屏幕切换到另一个?

从一个 SwiftUI 视图移动到另一个

如何从 SwiftUI 推送到现有的 UINavigationController 堆栈?