我创建了一个像这里一样的
Sentence creator
(Pure SwiftUI)。按钮拖动和定位工作正常。问题是,
谁能帮我解决这个问题?
struct SwiftUIDraggableButtonView: View {
@State private var buttons: [DraggableButtonModel] = [
DraggableButtonModel(id: 1, text: "me"),
DraggableButtonModel(id: 2, text: "foreign friends."),
DraggableButtonModel(id: 3, text: "to me,"),
DraggableButtonModel(id: 4, text: "to make"),
DraggableButtonModel(id: 5, text: "means")
]
@State private var sentenceButtons: [DraggableButtonModel] = []
var body: some View {
VStack(spacing: 20) {
// Unified FlowLayout for sentence and default areas
FlowLayout(spacing: 5, items: sentenceButtons) { button in
DraggableButtonNew(
model: button,
isInSentence: true
) { action in
handleButtonAction(action, button: button)
}
.transition(.move(edge: .bottom))
}
.padding()
.frame(height: 200) // Set height for sentence area
.background(Color.gray)
.zIndex(0)
// Default button area
FlowLayout(spacing: 10, items: buttons) { button in
DraggableButtonNew(
model: button,
isInSentence: false
) { action in
handleButtonAction(action, button: button)
}
.allowsHitTesting(!button.isDisabled) // Disable interaction for disabled buttons
.transition(.move(edge: .top))
}
.frame(height: 200) // Set height for default button area
.zIndex(1)
Spacer()
}
.padding()
}
private func handleButtonAction(_ action: DraggableButtonAction, button: DraggableButtonModel) {
withAnimation {
switch action {
case .tap, .drag:
// Handle button tap: move button between sentence and default areas
if let index = sentenceButtons.firstIndex(where: { $0.id == button.id }) {
// Button is in the sentence area, move it back to default
sentenceButtons.remove(at: index)
if let defaultIndex = buttons.firstIndex(where: { $0.id == button.id }) {
buttons[defaultIndex].isDisabled = false // Re-enable in default area
}
} else if let defaultIndex = buttons.firstIndex(where: { $0.id == button.id }),
!buttons[defaultIndex].isDisabled {
// Button is in default area, move it to sentence
buttons[defaultIndex].isDisabled = true
sentenceButtons.append(button)
}
}
}
}
}
// FlowLayout for wrapping buttons in multiple lines
struct FlowLayout<Data: RandomAccessCollection, Content: View>: View
where Data.Element: Identifiable {
let spacing: CGFloat
let items: Data
let content: (Data.Element) -> Content
var body: some View {
var width: CGFloat = 0
var height: CGFloat = 0
return GeometryReader { geometry in
ZStack(alignment: .topLeading) {
ForEach(items) { item in
content(item)
.alignmentGuide(.leading) { d in
if abs(width - d.width) > geometry.size.width {
width = 0
height -= d.height + spacing
}
let result = width
if item.id == items.last?.id {
width = 0
} else {
width -= d.width + spacing
}
return result
}
.alignmentGuide(.top) { _ in
let result = height
if item.id == items.last?.id {
height = 0
}
return result
}
}
}
}
.frame(maxHeight: .infinity, alignment: .topLeading)
}
}
// Draggable Button
struct DraggableButtonNew: View {
let model: DraggableButtonModel
let isInSentence: Bool
let actionHandler: (DraggableButtonAction) -> Void
@State private var offset: CGSize = .zero
var body: some View {
Text(model.text)
.padding(8)
.background(isInSentence ? Color.orange : model.isDisabled ? Color.gray : Color.orange)
.foregroundColor(Color.white)
.offset(offset)
.zIndex(offset == .zero ? 0 : 1)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
actionHandler(.drag)
offset = .zero
}
)
.onTapGesture {
actionHandler(.tap)
}
}
}
// Button Model
struct DraggableButtonModel: Identifiable, Equatable {
let id: Int
let text: String
var isDisabled: Bool = false
}
enum DraggableButtonAction {
case tap
case drag
}
如果您使用
.matchedGeometryEffect
来匹配句子中单词和按钮的位置,则拖动手势的变化看起来会更加流畅。重叠的问题也可以这样解决。
使其工作的一种方法如下:
句子区域内的单词和按钮区域内的按钮显示为简单的隐藏占位符。
占位符的id基于按钮(单词)的id,但在句子区域的占位符的id中添加了一个常量。这是为了使所有占位符的 id 都是唯一的。
可拖动的单词显示在父级
VStack
上方的叠加层中。这些单词always显示为已启用(橙色),但它们的位置使用.matchedGeometryEffect
与占位符匹配。换句话说,这些词有一个浮动的位置。
被拖动的单词具有更高的
zIndex
(和以前一样)。这确保了如果将其拖动到其他单词上,它会显示在其他单词上。这解决了帖子中的原始问题。
当在句子中添加单词时,禁用的表单会显示在按钮位置的背景中。
拖动单词时,我在控制台中看到有关“无效示例 AnimatablePair”的错误。可以通过使用 withAnimation
更新偏移量和持续时间 0 来防止这些问题,请参阅
this post了解更多详细信息。
withAnimation
。
isInSentence
中的参数
DraggableButtonNew
是多余的,可以去掉。实际上,将模型参数 isDisabled
重命名为 isInSentence
然后反向使用可能更有意义: false 变为 true。
.foregroundColor
已弃用,请使用
.foregroundStyle
代替。
// SwiftUIDraggableButtonView
let sentenceWord = 1_000_000
@Namespace private var namespace
var body: some View {
VStack(spacing: 20) {
// Unified FlowLayout for sentence and default areas
FlowLayout(spacing: 5, items: sentenceButtons) { button in
// Placeholder for word in sentence
PlainWord(text: button.text)
.hidden()
.matchedGeometryEffect(id: sentenceWord + button.id, in: namespace, isSource: true)
}
.padding()
.frame(height: 200) // Set height for sentence area
.background(.gray)
// Default button area
FlowLayout(spacing: 10, items: buttons) { button in
// Placeholder for a button
PlainWord(text: button.text)
.hidden()
.matchedGeometryEffect(id: button.id, in: namespace, isSource: true)
.background {
if button.isDisabled {
PlainWord(text: button.text, isDisabled: true)
}
}
}
.frame(height: 200) // Set height for default button area
Spacer()
}
.overlay {
ForEach(buttons) { button in
DraggableButtonNew(model: button) { action in
handleButtonAction(action, button: button)
}
.matchedGeometryEffect(
id: button.isDisabled ? sentenceWord + button.id : button.id,
in: namespace,
isSource: false
)
}
}
.padding()
}
struct PlainWord: View {
let text: String
var isDisabled = false
var body: some View {
Text(text)
.padding(8)
.background(isDisabled ? .gray : .orange)
.foregroundStyle(.white)
}
}
// Draggable Button
struct DraggableButtonNew: View {
let model: DraggableButtonModel
let actionHandler: (DraggableButtonAction) -> Void
@State private var offset: CGSize = .zero
var body: some View {
PlainWord(text: model.text, isDisabled: false)
.offset(offset)
.zIndex(offset == .zero ? 0 : 1)
.gesture(
DragGesture()
.onChanged { value in
withAnimation(.easeInOut(duration: 0)) {
offset = value.translation
}
}
.onEnded { _ in
withAnimation {
actionHandler(.drag)
offset = .zero
}
}
)
.onTapGesture {
actionHandler(.tap)
}
}
}