SwiftUI:以动画方式将视图从一个堆栈移动到另一个堆栈?

时间:2019-11-20 22:45:04

标签: ios swiftui

背景

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

Card movement

在这里的示例中,当我点击“移动卡”按钮时,♠️9从第1列移至第2列,位于❤️10的顶部,动画是the️9从第一列,而新的♠️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
    }
}

最后是一个Board对象,该对象包装了Column对象,并显示了一个对应的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阵列中),因此它以淡入的方式过渡。

问题是,有没有一种方法可以实现我想要的功能,而不必完全重新构建已建立的数据模型和视图层次结构,并且Card内有Column个,并且CardView内的ColumnView个?保持数据模型的原样确实有帮助,将数据模型直接映射到视图层次结构也很有帮助。

我可以想象某种替代视图层次结构,其中只有52个CardView,并根据其所在的列或位置对位置进行了某种修饰,但这确实很棘手。我考虑过某种定制的transition,但是过渡是针对要插入或删除的视图的。有没有办法告诉SwiftUI Card结构不是被删除并重新添加,而是被移动,从而执行匹配的移动动画?

2 个答案:

答案 0 :(得分:0)

对于您的情况,我认为您需要为此行为计算每张卡相对于某些@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)

    }
}

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

答案 1 :(得分:0)

(从对问题的评论中复制)

从Xcode 11.3开始,iOS 13.3:

可悲的是,我向Apple提交了DTS票证,他们说目前无法在SwiftUI中完成我想做的事情。最后,我将“所有52张卡片”数组公开为计算属性,并使用锚点首选项和偏移量设置了卡片的位置以使其具有动画效果。 (换句话说,本质上是我希望避免的解决方案。)希望可以在iOS 14 / Xcode 12中对其进行改进。