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
}
我构建了这个小型演示视图,其中有两个 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
}