在 SwiftUI DragGesture 中更改数组顺序
Change array sequence in SwiftUI DragGesture
我目前正在尝试构建一副纸牌来代表用户纸牌。
这是我的第一个原型,请忽略样式问题。我希望用户能够更改卡片的顺序,我试图通过拖动手势来实现。
我的想法是,当拖动手势的平移大于卡片偏移量(或小于卡片负偏移量)时,我只是交换数组中的两个元素以交换视图中的卡片(和 viewModel 因为绑定)。不幸的是,这不起作用,我不知道为什么。这是我的观点:
struct CardHand: View {
@Binding var cards: [Card]
@State var activeCardIndex: Int?
@State var activeCardOffset: CGSize = CGSize.zero
var body: some View {
ZStack {
ForEach(cards.indices) { index in
GameCard(card: cards[index])
.offset(x: CGFloat(-30 * index), y: self.activeCardIndex == index ? -20 : 0)
.offset(activeCardIndex == index ? activeCardOffset : CGSize.zero)
.rotationEffect(Angle.degrees(Double(-2 * Double(index - cards.count))))
.zIndex(activeCardIndex == index ? 100 : Double(index))
.gesture(getDragGesture(for: index))
}
// Move the stack back to the center of the screen
.offset(x: CGFloat(12 * cards.count), y: 0)
}
}
private func getDragGesture(for index: Int) -> some Gesture {
DragGesture()
.onChanged { gesture in
self.activeCardIndex = index
self.activeCardOffset = gesture.translation
}
.onEnded { gesture in
if gesture.translation.width > 30, index < cards.count {
cards.swapAt(index, index + 1)
}
self.activeCardIndex = nil
// DEBUG: Try removing the card after drag gesture ended. Not working either.
cards.remove(at: index)
}
}
}
游戏卡:
struct GameCard: View {
let card: Card
var symbol: (String, Color) {
switch card.suit.name {
case "diamonds":
return ("♦", .red)
case "hearts":
return ("♥", .red)
case "spades":
return ("♠", .black)
case "clubs":
return ("♣", .black)
default:
return ("none", .black)
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white)
.addBorder(Color.black, width: 3, cornerRadius: 25)
VStack {
Text(self.card.rank.type.description())
Text(self.symbol.0)
.font(.system(size: 100))
.foregroundColor(self.symbol.1)
}
}
.frame(minWidth: 20, idealWidth: 80, maxWidth: 80, minHeight: 100, idealHeight: 100, maxHeight: 100, alignment: .center)
}
}
背后的模型:
public struct Card: Identifiable, Hashable {
public var id: UUID = UUID()
public var suit: Suit
public var rank: Rank
}
public enum Type: Identifiable, Hashable {
case face(String)
case numeric(rankNumber: Int)
public var id: Type { self }
public func description() -> String{
switch self {
case .face(let description):
return description
case .numeric(let rankNumber):
return String(rankNumber)
}
}
}
public struct Rank: RankProtocol, Identifiable, Hashable {
public var id: UUID = UUID()
public var type: Type
public var value: Int
public var ranking: Int
}
public struct Suit: SuitProtocol, Identifiable, Hashable {
public var id: UUID = UUID()
public var name: String
public var value: Int
public init(name: String, value: Int) {
self.name = name
self.value = value
}
}
public protocol RankProtocol {
var type: Type { get }
var value: Int { get }
}
public protocol SuitProtocol {
var name: String { get }
}
谁能告诉我为什么这没有像我预期的那样工作?我错过了什么基本的东西吗?
谢谢!
这里有很多小错误。这是一个(粗略的)解决方案,我将在下面详细说明:
struct CardHand: View {
@Binding var cards: [Card]
@State var activeCardIndex: Int?
@State var activeCardOffset: CGSize = CGSize.zero
func offsetForCardIndex(index: Int) -> CGSize {
var initialOffset = CGSize(width: CGFloat(-30 * index), height: 0)
guard index == activeCardIndex else {
return initialOffset
}
initialOffset.width += activeCardOffset.width
initialOffset.height += activeCardOffset.height
return initialOffset
}
var body: some View {
ZStack {
ForEach(cards.indices) { index in
GameCard(card: cards[index])
.offset(offsetForCardIndex(index: index))
.rotationEffect(Angle.degrees(Double(-2 * Double(index - cards.count))))
.zIndex(activeCardIndex == index ? 100 : Double(index))
.gesture(getDragGesture(for: index))
}
// Move the stack back to the center of the screen
.offset(x: CGFloat(12 * cards.count), y: 0)
}
}
private func getDragGesture(for index: Int) -> some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { gesture in
self.activeCardIndex = index
self.activeCardOffset = gesture.translation
}
.onEnded { gesture in
if gesture.translation.width > 30 {
if index - 1 > 0 {
cards.swapAt(index, index - 1)
}
}
if gesture.translation.width < -30 {
cards.swapAt(index, index + 1)
}
self.activeCardIndex = nil
}
}
}
- 尝试连续进行两次
offset()
调用是有问题的。我把两者合并成 offsetForCardIndex
- 因为您正在执行
ZStack
,请记住 cards
数组中的第一项将位于堆栈的 底部 并朝向屏幕的右边。这影响了后来的逻辑
- 您需要确保检查
swapAt
中数组的边界,这样您就不会最终尝试交换超出索引的范围(这是之前发生的事情)
我的“解决方案”仍然很粗略——应该进行更多检查以确保数组不会越界。此外,在你的原作和我的原作中,从用户体验的角度来看,能够交换多个位置可能更有意义。但是,这是一个更大的问题,超出了这个问题的范围。
我目前正在尝试构建一副纸牌来代表用户纸牌。
这是我的第一个原型,请忽略样式问题。我希望用户能够更改卡片的顺序,我试图通过拖动手势来实现。
我的想法是,当拖动手势的平移大于卡片偏移量(或小于卡片负偏移量)时,我只是交换数组中的两个元素以交换视图中的卡片(和 viewModel 因为绑定)。不幸的是,这不起作用,我不知道为什么。这是我的观点:
struct CardHand: View {
@Binding var cards: [Card]
@State var activeCardIndex: Int?
@State var activeCardOffset: CGSize = CGSize.zero
var body: some View {
ZStack {
ForEach(cards.indices) { index in
GameCard(card: cards[index])
.offset(x: CGFloat(-30 * index), y: self.activeCardIndex == index ? -20 : 0)
.offset(activeCardIndex == index ? activeCardOffset : CGSize.zero)
.rotationEffect(Angle.degrees(Double(-2 * Double(index - cards.count))))
.zIndex(activeCardIndex == index ? 100 : Double(index))
.gesture(getDragGesture(for: index))
}
// Move the stack back to the center of the screen
.offset(x: CGFloat(12 * cards.count), y: 0)
}
}
private func getDragGesture(for index: Int) -> some Gesture {
DragGesture()
.onChanged { gesture in
self.activeCardIndex = index
self.activeCardOffset = gesture.translation
}
.onEnded { gesture in
if gesture.translation.width > 30, index < cards.count {
cards.swapAt(index, index + 1)
}
self.activeCardIndex = nil
// DEBUG: Try removing the card after drag gesture ended. Not working either.
cards.remove(at: index)
}
}
}
游戏卡:
struct GameCard: View {
let card: Card
var symbol: (String, Color) {
switch card.suit.name {
case "diamonds":
return ("♦", .red)
case "hearts":
return ("♥", .red)
case "spades":
return ("♠", .black)
case "clubs":
return ("♣", .black)
default:
return ("none", .black)
}
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white)
.addBorder(Color.black, width: 3, cornerRadius: 25)
VStack {
Text(self.card.rank.type.description())
Text(self.symbol.0)
.font(.system(size: 100))
.foregroundColor(self.symbol.1)
}
}
.frame(minWidth: 20, idealWidth: 80, maxWidth: 80, minHeight: 100, idealHeight: 100, maxHeight: 100, alignment: .center)
}
}
背后的模型:
public struct Card: Identifiable, Hashable {
public var id: UUID = UUID()
public var suit: Suit
public var rank: Rank
}
public enum Type: Identifiable, Hashable {
case face(String)
case numeric(rankNumber: Int)
public var id: Type { self }
public func description() -> String{
switch self {
case .face(let description):
return description
case .numeric(let rankNumber):
return String(rankNumber)
}
}
}
public struct Rank: RankProtocol, Identifiable, Hashable {
public var id: UUID = UUID()
public var type: Type
public var value: Int
public var ranking: Int
}
public struct Suit: SuitProtocol, Identifiable, Hashable {
public var id: UUID = UUID()
public var name: String
public var value: Int
public init(name: String, value: Int) {
self.name = name
self.value = value
}
}
public protocol RankProtocol {
var type: Type { get }
var value: Int { get }
}
public protocol SuitProtocol {
var name: String { get }
}
谁能告诉我为什么这没有像我预期的那样工作?我错过了什么基本的东西吗?
谢谢!
这里有很多小错误。这是一个(粗略的)解决方案,我将在下面详细说明:
struct CardHand: View {
@Binding var cards: [Card]
@State var activeCardIndex: Int?
@State var activeCardOffset: CGSize = CGSize.zero
func offsetForCardIndex(index: Int) -> CGSize {
var initialOffset = CGSize(width: CGFloat(-30 * index), height: 0)
guard index == activeCardIndex else {
return initialOffset
}
initialOffset.width += activeCardOffset.width
initialOffset.height += activeCardOffset.height
return initialOffset
}
var body: some View {
ZStack {
ForEach(cards.indices) { index in
GameCard(card: cards[index])
.offset(offsetForCardIndex(index: index))
.rotationEffect(Angle.degrees(Double(-2 * Double(index - cards.count))))
.zIndex(activeCardIndex == index ? 100 : Double(index))
.gesture(getDragGesture(for: index))
}
// Move the stack back to the center of the screen
.offset(x: CGFloat(12 * cards.count), y: 0)
}
}
private func getDragGesture(for index: Int) -> some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { gesture in
self.activeCardIndex = index
self.activeCardOffset = gesture.translation
}
.onEnded { gesture in
if gesture.translation.width > 30 {
if index - 1 > 0 {
cards.swapAt(index, index - 1)
}
}
if gesture.translation.width < -30 {
cards.swapAt(index, index + 1)
}
self.activeCardIndex = nil
}
}
}
- 尝试连续进行两次
offset()
调用是有问题的。我把两者合并成offsetForCardIndex
- 因为您正在执行
ZStack
,请记住cards
数组中的第一项将位于堆栈的 底部 并朝向屏幕的右边。这影响了后来的逻辑 - 您需要确保检查
swapAt
中数组的边界,这样您就不会最终尝试交换超出索引的范围(这是之前发生的事情)
我的“解决方案”仍然很粗略——应该进行更多检查以确保数组不会越界。此外,在你的原作和我的原作中,从用户体验的角度来看,能够交换多个位置可能更有意义。但是,这是一个更大的问题,超出了这个问题的范围。