Показать лист в ответ на падение

#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! Пожалуйста, добавьте рабочий код, демонстрирующий ваш ответ, чтобы другие могли извлечь выгоду. Спасибо!