视图自动跳转到多行文本字段

View automatically jumps to Multiline Text Field

我的 SwiftUI 视图有点奇怪,因为我添加了一个 MultilineTextField。当按下列表中的项目时,视图种类来回跳转,然后自动跳转到视图中的最后一个文本字段,如 this 视频中所示。这只是在我最后添加了一个 MultilineTextField 之后发生的。

多行文本字段定义:

import Foundation
import SwiftUI
import UIKit

fileprivate struct UITextViewWrapper: UIViewRepresentable {
    typealias UIViewType = UITextView

    @Binding var text: String
    @Binding var calculatedHeight: CGFloat
    var onDone: (() -> Void)?

    func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
        let textField = UITextView()
        textField.delegate = context.coordinator

        textField.isEditable = true
        textField.font = UIFont.preferredFont(forTextStyle: .body)
        textField.isSelectable = true
        textField.isUserInteractionEnabled = true
        textField.isScrollEnabled = false
        textField.backgroundColor = UIColor.clear
        if nil != onDone {
            textField.returnKeyType = .done
        }

        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return textField
    }

    func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
        if uiView.text != self.text {
            uiView.text = self.text
        }
        if uiView.window != nil, !uiView.isFirstResponder {
            uiView.becomeFirstResponder()
        }
        UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
    }

    fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
        let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if result.wrappedValue != newSize.height {
            DispatchQueue.main.async {
                result.wrappedValue = newSize.height // !! must be called asynchronously
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
    }

    final class Coordinator: NSObject, UITextViewDelegate {
        var text: Binding<String>
        var calculatedHeight: Binding<CGFloat>
        var onDone: (() -> Void)?

        init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
            self.text = text
            self.calculatedHeight = height
            self.onDone = onDone
        }

        func textViewDidChange(_ uiView: UITextView) {
            text.wrappedValue = uiView.text
            UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if let onDone = self.onDone, text == "\n" {
                textView.resignFirstResponder()
                onDone()
                return false
            }
            return true
        }
    }

}

struct MultilineTextField: View {

    private var placeholder: String
    private var onCommit: (() -> Void)?

    @Binding private var text: String
    private var internalText: Binding<String> {
        Binding<String>(get: { self.text } ) {
            self.text = [=10=]
            self.showingPlaceholder = [=10=].isEmpty
        }
    }

    @State private var dynamicHeight: CGFloat = 100
    @State private var showingPlaceholder = false

    init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
        self.placeholder = placeholder
        self.onCommit = onCommit
        self._text = text
        self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
    }

    var body: some View {
        UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
            .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
            .background(placeholderView, alignment: .topLeading)
    }

    var placeholderView: some View {
        Group {
            if showingPlaceholder {
                Text(placeholder).foregroundColor(.gray)
                    .padding(.leading, 4)
                    .padding(.top, 8)
            }
        }
    }
}

#if DEBUG
struct MultilineTextField_Previews: PreviewProvider {
    static var test:String = ""//some very very very long description string to be initially wider than screen"
    static var testBinding = Binding<String>(get: { test }, set: {
//        print("New value: \([=10=])")
        test = [=10=] } )

    static var previews: some View {
        VStack(alignment: .leading) {
            Text("Description:")
            MultilineTextField("Enter some text here", text: testBinding, onCommit: {
                print("Final text: \(test)")
            })
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
            Text("Something static here...")
            Spacer()
        }
        .padding()
    }
}
#endif

代码:

struct DetailZwei : View {
    
    
  @State var data : dataTypeZwei
    @State var viewModel = GerätEditieren()
    @Environment(\.presentationMode) var presentationMode
    @State private var showingAlert = false
    
    
    var body : some View {
        
        NavigationView {
            ScrollView {
                VStack {
        Group {
        Section(header: Text("")) {
                Text("Seriennummer")
            TextField("Seriennummer", text: $data.sn).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Objekt")
            TextField("Objekt", text: $data.objekt).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Gerätetyp")
            TextField("Gerätetyp", text: $data.typ).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Geräteposition")
            TextField("Geräteposition", text: $data.pos).textFieldStyle(RoundedBorderTextFieldStyle())
                    }
        Group {
                Text("Installationsdatum")
            TextField("Installationsdatum", text: $data.ida).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Leasing oder Gekauft?")
            TextField("Leasing oder Gekauft?", text: $data.lg).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Ablaufdatum Leasing")
            TextField("Ablaufdatum Leasing", text: $data.la).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Ablaufdatum Garantie")
            TextField("Ablaufdatum Garantie", text: $data.ga).textFieldStyle(RoundedBorderTextFieldStyle())
            }
                    
        Section(header: Text("")) {
                Text("Strasse")
            TextField("Strasse", text: $data.str).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Hausnummer")
            TextField("Hausnummer", text: $data.nr).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Postleitzahl")
            TextField("Postleitzahl", text: $data.plz).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Ort")
            TextField("Ort", text: $data.ort).textFieldStyle(RoundedBorderTextFieldStyle())
            }
                
        Section(header: Text("")) {
                Text("Ansprechperson")
            TextField("Ansprechperson", text: $data.vp).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Telefonnummer")
            TextField("Telefonnummer", text: $data.tel).textFieldStyle(RoundedBorderTextFieldStyle())
            }
        Section(header: Text("VDS").bold()) {
                Text("Eingetragen durch")
            TextField("Eingetragen durch", text: $data.ed).textFieldStyle(RoundedBorderTextFieldStyle())
                Text("Lieferdatum VDS")
            TextField("Lieferdatum VDS", text: $data.ldvds).textFieldStyle(RoundedBorderTextFieldStyle())
            }

// This is the Text Field
            
        Section(header: Text("")) {
                Text("Zusätzliche Informationen")
            MultilineTextField("Zusätzliche Informationen", text: $data.zusatz).overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
            }
            }.padding()
            
            .navigationBarTitle("Gerät bearbeiten", displayMode: .inline)
                    .navigationBarItems(leading: Button(action: { self.handleCancelTapped() }, label: {
                        Text("Abbrechen")
                    }),
                                        trailing: Button(action: { self.handleDoneTapped() }, label: {
                                            Text("Speichern")
                    })
                   // .disabled(!viewModel.modified)
                )
                }.alert(isPresented: $showingAlert) {
                    Alert(title: Text("Änderungen gespeichert"), message: Text("Die Änderungen vom Gerät \(data.sn) wurden erfolgreich gespeichert!"), dismissButton: .default(Text("Zurück").bold()){
                        self.handleCancelTapped()
                        })
                }
            }
        }
    }

当您打开视图时,您的自定义文本字段会调用 firstResponders。只需删除加载时调用 firstResponder,您的视图就会从头开始。

    func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
        if uiView.text != self.text {
            uiView.text = self.text
        }
        if uiView.window != nil, !uiView.isFirstResponder {
            //uiView.becomeFirstResponder() << Here calling firstResponder 
        }
        UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
    }

SwiftUI TextFields 尚不支持 firstResponder,但是使用 Representable 并使用 UIKit 是可能的,就像在您的解决方案中一样,Grüezi