#macos #swiftui #drag-and-drop
Вопрос:
Я внедряю перетаскивание, и у меня есть случай, когда мне нужно, чтобы пользователь решил, что делать в ответ на удаление. Поэтому я хочу открыть лист, чтобы попросить пользователя ввести данные. Проблема в том, что лист не отображается до тех пор, пока я не перетащу другой элемент в то же представление. Это действительно имеет смысл, поэтому я ищу способ справиться с этим по-другому.
Текущий подход выглядит так (упрощенно):
struct SymbolInfo {
enum SymbolType {
case string, systemName
}
var type: SymbolType
var string: String
}
struct MyView: View, DropDelegate {
@State var sheetPresented = false
@State var droppedText = ""
static let dropTypes = [UTType.utf8PlainText]
var textColor = NSColor.white
private var frameRect: CGRect = .null
private var contentPath: Path = Path()
private var textRect: CGRect = .null
@State private var displayOutput: SymbolInfo
@State private var editPopoverIsPresented = false
// There's an init to set up the display output, the various rects and path
var body: some View {
ZStack(alignment: stackAlignment) {
BackgroundView() // Draws an appropriate background
.frame(width: frameRect.width, height: frameRect.height)
if displayOutput.type == .string {
Text(displayOutput.string)
.frame(width: textRect.width, height: textRect.height, alignment: .center)
.foregroundColor(textColour)
.font(displayFont)
.allowsTightening(true)
.lineLimit(2)
.minimumScaleFactor(0.5)
}
else {
Image(systemName: displayOutput.string)
.frame(width: textRect.width, height: textRect.height, alignment: .center)
.foregroundColor(textColour)
.minimumScaleFactor(0.5)
}
}
.onAppear {
// Retrieve state information from the environment
}
.focusable(false)
.allowsHitTesting(true)
.contentShape(contentPath)
.onHover { entered in
// Populates an inspector
}
.onTapGesture(count: 2) {
// Handle a double click
}
.onTapGesture(count: 1) {
// Handle a single click
}
.popover(isPresented: $editPopoverIsPresented) {
// Handles a popover for editing data
}
.onDrop(of: dropTypes, delegate: self)
.sheet(sheetPresented: $sheetPresented, onDismiss: sheetReturn) {
// sheet to ask for the user's input
}
}
func sheetReturn() {
// act on the user's input
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
if let textData = String(data: textData as! Data, encoding: .utf8) {
if (my condition) {
sheetIsPresented = true
droppedText = textData
}
else {
// handle regular drop
}
}
}
return true
}
return false
}
}
Поэтому я рассуждаю так: отбрасывание устанавливает значение sheetPresented равным true, но затем оно не выполняется до тех пор, пока представление не будет перестроено, например, при перетаскивании в него чего-либо другого. Но я все еще новичок в SwiftUI, так что, возможно, я ошибаюсь.
Есть ли способ справиться с такого рода взаимодействием, которого я не нашел?
Комментарии:
1. Я еще не проверял это, но вы могли бы попробовать:
DispatchQueue.main.async { droppedText = textData sheetIsPresented = true }
2. Похоже, это не помогает. Может быть, мне нужно показать предупреждение вместо листа. Я попробую сделать это на следующей неделе.
3. Концепция вашего кода, похоже, работает нормально. Но ваш код-это не то, что у вас есть в Xcode. Например, у вас есть
sheetPresented
иisSheetPresented
. У вас также естьitemProviders(for: UTType.utf8PlainText)
без[ ]
. Я предполагаю, что это результат попытки привести ваш код в минимально воспроизводимое состояние для вопроса здесь, что является отличной идеей, но это также сигнал о том, что в вашем коде могут быть и другие различия.4. Другие вещи, которые нужно искать: 1) убедитесь, что вы действительно удаляете текст (а не URL, например). 2) Убедитесь, что ваше логическое условие в делегате drop выполняется должным образом.
5. Вы правы в том, что он не скопирован из Xcode, а составлен онлайн при просмотре фактического кода и, следовательно, проблем, которые вы заметили. Я еще раз отредактирую код. Но что касается вашего другого комментария, да, это текст, и да, логическое условие соответствует ожидаемому.
Ответ №1:
Я никогда не мог точно воспроизвести проблему, но проблема была связана с попыткой иметь более одного вида листа, который можно было бы отобразить, в зависимости от условий. Решение состояло в том, чтобы разбить исходное представление на семейство представлений, которые инкапсулировали различные модели поведения, и показать подходящее, а не пытаться заставить одно представление делать все.
Я не буду показывать весь код, так как он слишком глубоко встроен в приложение, но вот демонстрационное приложение, которое работает правильно:
import SwiftUI
import UniformTypeIdentifiers
@main
struct DragAndDropSheetApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
HStack() {
TargetView(viewType: .normal, viewText: "A")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .protected, viewText: "B")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .normal, viewText: "C")
.frame(width: 40, height: 40, alignment: .top)
TargetView(viewType: .protected, viewText: "D")
.frame(width: 40, height: 40, alignment: .top)
}
.padding()
}
}
enum ViewType {
case normal, protected
}
struct TargetView: View, DropDelegate {
@State private var sheetPresented = false
@State var viewType: ViewType
@State var viewText: String
@State private var dropText = ""
@State private var dropType: DropActions = .none
static let dropTypes = [UTType.utf8PlainText]
var body: some View {
ZStack(alignment: .center) {
Rectangle()
.foregroundColor(viewType == .normal ? .blue : .red)
Text(viewText)
.foregroundColor(.white)
.frame(width: nil, height: nil, alignment: .center)
}
.focusable(false)
.allowsHitTesting(true)
.onDrop(of: TargetView.dropTypes, delegate: self)
.sheet(isPresented: $sheetPresented, onDismiss: handleSheetReturn) {
ProtectedDrop(isPresented: $sheetPresented, action: $dropType)
}
}
func handleSheetReturn() {
switch dropType {
case .append:
viewText = dropText
case .replace:
viewText = dropText
case .none:
// Nothing to do
return
}
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: TargetView.dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { textData, error in
if let textData = String(data: textData as! Data, encoding: .utf8) {
if viewType == .normal {
viewText = textData
}
else {
dropText = textData
sheetPresented = true
}
}
}
return true
}
return false
}
}
enum DropActions: Hashable {
case append, replace, none
}
struct ProtectedDrop: View {
@Binding var isPresented: Bool
@Binding var action: DropActions
var body: some View {
VStack() {
Text("This view is protected. What do you want to do?")
Picker("", selection: $action) {
Text("Append the dropped text")
.tag(DropActions.append)
Text("Replace the text")
.tag(DropActions.replace)
}
.pickerStyle(.radioGroup)
HStack() {
Spacer()
Button("Cancel") {
action = .none
isPresented.toggle()
}
.keyboardShortcut(.cancelAction)
Button("OK") {
isPresented.toggle()
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
}
}
Комментарии:
1. Привет, добро пожаловать в SO! Пожалуйста, добавьте рабочий код, демонстрирующий ваш ответ, чтобы другие могли извлечь выгоду. Спасибо!