SwiftUI:自定义 UITextView UIViewRepresentable 需要在第一个操作上双击才能工作,此后单击可以正常工作

SwiftUI: Custom UITextView UIViewRepresentable requires double tap on the first action to work, works fine with single taps thereafter

我构建了这个小型演示视图,其中有两个 NoteRow,我的目标是能够按 return 键创建一个新行并使其成为急救人员。 这有效,但是,第一次创建新行,但它不会成为第一响应者。 return 键的后续点击既创建行又成为第一响应者。

知道这里出了什么问题吗?谢谢!

import SwiftUI
import Combine

struct FirstResponderDemo: View {
    
    @State private var rows: [NoteRow] = [
        .init(parentNoteId: "1", text: "foo"),
        .init(parentNoteId: "1", text: "bar"),
    ]
    
    @State private var activeRowId: String?
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                VStack {
                    ForEach(rows, id: \.id) { row in
                        ResponderTextView(
                            row: row,
                            text: $login,
                            activeRowId: $activeRowId,
                            returnPressed: returnPressed
                        )
                        .frame(width: 300, height: 44)
                    }
                }.padding(.horizontal, 12)
            }
        }
        .onAppear {
            self.activeRowId = rows[0].id
        }
    }
    
    private func returnPressed() {
        guard let id = activeRowId else { return }
        
        let newRow = NoteRow(parentNoteId: "1", text: "")
        print("new row id", newRow.id)
        
        if let index = rows.firstIndex(where: { [=10=].id == id }) {
            rows.insert(newRow, at: index + 1)
            activeRowId = newRow.id
        }
    }
}

struct FirstResponderDemo_Previews: PreviewProvider {
    static var previews: some View {
        FirstResponderDemo()
    }
}


struct ResponderView<View: UIView>: UIViewRepresentable {
    
    let row: NoteRow
    
    @Binding var activeRowId: String?
    
    var configuration = { (view: View) in }

    func makeUIView(context: Context) -> View { View() }

    func makeCoordinator() -> Coordinator {
        Coordinator(row: row, activeRowId: $activeRowId)
    }

    func updateUIView(_ uiView: View, context: Context) {
        context.coordinator.view = uiView
        
//        print(activeRowId)
        _ = activeRowId == row.id ? uiView.becomeFirstResponder() : uiView.resignFirstResponder()
        
        configuration(uiView)
    }
}

// MARK: - Coordinator

extension ResponderView {
    
    final class Coordinator {
    
        @Binding private var activeRowId: String?
        
        private var anyCancellable: AnyCancellable?
        
        fileprivate weak var view: UIView?

        init(row: NoteRow, activeRowId: Binding<String?>) {
            _activeRowId = activeRowId
            
            self.anyCancellable = Publishers.keyboardHeight.sink(receiveValue: { [weak self] keyboardHeight in
                guard let view = self?.view, let self = self else { return }
                
                DispatchQueue.main.async {
                    if view.isFirstResponder {
                        self.activeRowId = row.id
                        print("active row id is:", self.activeRowId)
                    }
                }
            })
        }
    }
    
}

// MARK: - keyboardHeight

extension Publishers {
    
    static var keyboardHeight: AnyPublisher<CGFloat, Never> {
        let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
            .map { ([=10=].userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0 }

        let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
            .map { _ in CGFloat(0) }

        return MergeMany(willShow, willHide)
            .eraseToAnyPublisher()
    }
    
}

struct ResponderView_Previews: PreviewProvider {
    static var previews: some View {
        ResponderView<UITextView>.init(row: .init(parentNoteId: "1", text: "Hello world"), activeRowId: .constant(nil)) { _ in
        }.previewLayout(.fixed(width: 300, height: 40))
    }
}

struct ResponderTextView: View {
    
    let row: NoteRow
    
    @State var text: String
    
    @Binding var activeRowId: String?
    
    private var textViewDelegate: TextViewDelegate

    init(row: NoteRow, text: Binding<String>, activeRowId: Binding<String?>, returnPressed: @escaping () -> Void) {
        self.row = row
        self._text = State(initialValue: row.text)
        self._activeRowId = activeRowId
        self.textViewDelegate = TextViewDelegate(text: text, returnPressed: returnPressed)
    }

    var body: some View {
        ResponderView<UITextView>(row: row, activeRowId: $activeRowId) {
            [=10=].text = self.text
            [=10=].delegate = self.textViewDelegate
        }
    }
}

// MARK: - TextFieldDelegate

private extension ResponderTextView {
    
    final class TextViewDelegate: NSObject, UITextViewDelegate {
        
        @Binding private(set) var text: String
        
        let returnPressed: () -> Void

        init(text: Binding<String>, returnPressed: @escaping () -> Void) {
            _text = text
            self.returnPressed = returnPressed
        }
        
        func textViewDidChange(_ textView: UITextView) {
            DispatchQueue.main.async {
               self.text = textView.text
            }
        }
        
        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if (text == "\n") {
                returnPressed()
                return false
            }
            
            return true
        }
    }

}

NoteRow的定义:

final class NoteRow: ObservableObject, Identifiable {
    
    let id: String = UUID().uuidString
    
    let parentNoteId: String

    let text: String
   
    init(parentNoteId: String, text: String) {
        self.parentNoteId = parentNoteId
        self.text = text
    }
 
}

extension NoteRow: Equatable {
    
    static func == (lhs: NoteRow, rhs: NoteRow) -> Bool {
        lhs.id == rhs.id &&
        lhs.parentNoteId  == rhs.parentNoteId
    }
    
}

编辑: 调试更多

active row id is: Optional("71D8839A-D046-4DC5-8E02-F124779309E6") // 第一个默认行 活动行 ID 是:可选(“5937B1D0-CBB0-4BE4-A235-4D57835D7B0F”)//第二个默认行

// 我按下了 return 键: 新行 ID F640D1F9-0708-4099-BDA4-2682AF82E3BD 活动行 ID 为:可选(“5937B1D0-CBB0-4BE4-A235-4D57835D7B0F”)// 第二行的 ID 由于某种原因被设置为活动

// 之后,新行ID和活动行ID遵循预期路径:

新行 ID 9FDEB548-E19F-4572-BAD3-00E6CBB951D1 活动行 ID 为:可选(“9FDEB548-E19F-4572-BAD3-00E6CBB951D1”)

新行 ID 4B5C1AA3-15A1-4449-B1A2-9D834013496A 活动行 ID 为:可选(“4B5C1AA3-15A1-4449-B1A2-9D834013496A”)

新行 ID 22A61BE8-1BAD-4209-B46B-15666FF82D9B 活动行 ID 为:可选(“22A61BE8-1BAD-4209-B46B-15666FF82D9B”)

新行 ID 95DD6B33-4421-4A32-8478-DCCBCBB1824E 活动行 ID 为:可选(“95DD6B33-4421-4A32-8478-DCCBCBB1824E”)

好吧,我不知道这段代码,但下面修复了有问题的情况(用 Xcode 12b 测试)

    if (text == "\n") {
        DispatchQueue.main.async {     // << defer to next event !!
            self.returnPressed()
        }
        return false
    }
    return true
}