Reputation: 6998
I've built this small demo view where I have two NoteRow
s and my goal is to be able to press the return key to create a new row and for it to become the first responder. This works, however, the first time around, the new row is created but it doesn't become the first responder. Subsequent taps of the return key both create the row and become first responder.
Any ideas what's going wrong here? Thanks!
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: { $0.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 { ($0.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) {
$0.text = self.text
$0.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
}
}
}
And the definition of 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
}
}
Edit: Debugging this more
active row id is: Optional("71D8839A-D046-4DC5-8E02-F124779309E6") // first default row active row id is: Optional("5937B1D0-CBB0-4BE4-A235-4D57835D7B0F") // second default row
// I hit return key: new row id F640D1F9-0708-4099-BDA4-2682AF82E3BD active row id is: Optional("5937B1D0-CBB0-4BE4-A235-4D57835D7B0F") // ID for 2nd row is set as active for some reason
// After that, new row id and active row ID follow the expected path:
new row id 9FDEB548-E19F-4572-BAD3-00E6CBB951D1 active row id is: Optional("9FDEB548-E19F-4572-BAD3-00E6CBB951D1")
new row id 4B5C1AA3-15A1-4449-B1A2-9D834013496A active row id is: Optional("4B5C1AA3-15A1-4449-B1A2-9D834013496A")
new row id 22A61BE8-1BAD-4209-B46B-15666FF82D9B active row id is: Optional("22A61BE8-1BAD-4209-B46B-15666FF82D9B")
new row id 95DD6B33-4421-4A32-8478-DCCBCBB1824E active row id is: Optional("95DD6B33-4421-4A32-8478-DCCBCBB1824E")
Upvotes: 0
Views: 389
Reputation: 257739
Ok, I did not get idea of this code, but the below fixes case in question (tested with Xcode 12b)
if (text == "\n") {
DispatchQueue.main.async { // << defer to next event !!
self.returnPressed()
}
return false
}
return true
}
Upvotes: 1