Laggy typing and cursor jumps with TextField? - ios

My app uses TextFields everywhere to modify CoreData entities' String attributes. They work very poorly - typing a space or getting an auto correct event seems to make the cursor jump to the end of the window. Keystrokes are missed and the whole experience is laggy. TextEditors, on the other hand, work fine. The behavior doesn't appear on the simulator, only on (multiple) real devices.
What am I doing wrong here? Am I using TextFields wrong?
Code is below, it's basically the starter Xcode app with a "text: String?" attribute added to the "item" CoreData entity.
struct Detail: View {
#ObservedObject var item: Item
var body: some View {
VStack {
Form {
Section(content: {
TextField("Title", text: $item.text ?? "")
}, header: {
Text("TextField")
})
Section(content: {
TextEditor(text: $item.text ?? "")
}, header: {
Text("TextEditor")
})
}
}
}
}
// Optional binding used
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
Update:
I ended up just putting the TextFields into a subview and then writing their value back to the NSManagedObject via a binding every time the value changes.
I have no idea why, but this fixes the problem for me.
struct CustomTextField: View {
#Binding var string: String?
#State var localString: String
let prompt: String
init(string: Binding<String?>, prompt: String) {
_string = string
_localString = State(initialValue: string.wrappedValue ?? "")
self.prompt = prompt
}
var body: some View {
TextField(prompt, text: $localString, axis: .vertical)
.onChange(of: localString, perform: { _ in
string = localString
})
}
}

Example of using onSubmit, which does not cause CoreData to save the data on every input by the keyboard.
struct Detail: View {
#ObservedObject var item: Item
#State var text: String = "" // for starting with an empty textfield
// Alternative if you want the data from item:
// #State var text: String = item.text.wrappedValue // This only works if text is a binding.
var body: some View {
VStack {
Form {
Section(content: {
TextField("Title", text: $text)
.onSubmit {
item.text = self.text
}
}, header: {
Text("TextField")
})
Section(content: {
TextEditor(text: $text)
.onSubmit {
item.text = self.text
}
}, header: {
Text("TextEditor")
})
}
}
}
}
If that does not help, it would be nice to know how Item looks like.

Related

SwiftUI textfield proxy binding code to ignore spaces not working?

I am trying to make my SwiftUI Textfield input ignore spaces every time the spacebar button is pressed so that account input data does not contain any spaces.
I saw the code below for achieving this with "proxy binding" but the answer is so concise for me and I am new to { get set }.
Ignore left whitespaces on imput in TextField SwiftUI Combine
I want the code in AccountInput to return if the new input is a space so that it does not go into the textfield & loginViewModel.input.
How can I make this code work?
MAIN VIEW
struct LoginView: View {
#StateObject var loginViewModel = LoginViewModel()
var body: some View {
VStack {
AccountInput(placeholder: "", input: $loginViewModel.input)
}
}
}
ACCOUNT INPUT
struct AccountInput: View {
var placeholder: String
#Binding var input: String
var body: some View {
HStack {
TextField(placeholder, text: Binding(
get: { self.input },
set: {
var newValue = $0
if newValue == " " { // How can I make new values return if a space?
return
}
self.input = newValue
}))
}
}
}
No, we should not return, there should be assignment, because it generates feedback.
It can be like
TextField(placeholder, text: Binding(
get: { self.input },
set: {
var newValue = $0
newValue.removeAll { $0 == " " } // << here !!
self.input = newValue
}))

Selecting an existing item on an array or passing a new one to a .sheet

I have an array that contains the data that is displayed on a list. When the user hits "new", a sheet pops up to allow the user to enter a new item to the list.
I just added a swipe option to edit this item and I wanted to reuse the same sheet to edit the item's text. But I'm having problems understanding how to check whether a specific item was selected (by UUID?) to pass to the sheet, or it's a new item.
Code:
let dateFormatter = DateFormatter()
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
struct ContentView: View {
#EnvironmentObject private var data: DataModel
#State private var selectedItemId: UUID?
#State var searchText: String = ""
#State private var sheetIsShowing = false
NavigationView {
List(filteredNotes) { note in
VStack(alignment: .leading) {
//....
// not relevant code
}
.swipeActions(allowsFullSwipe: false) {
Button(action: {
selectedItemId = note.id
self.sheetIsShowing = true
} ) {
Label("Edit", systemImage: "pencil")
}
}
}
.toolbar {
// new item
Button(action: {
self.sheetIsShowing = true
}) {
Image(systemName: "square.and.pencil")
}
}
.sheet(isPresented: $sheetIsShowing) {
if self.selectedItemId == NULL { // <-- this is giving me an error
let Note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(isVisible: self.$sheetIsShowing, note: Note)
} else {
let index = data.notes.firstIndex(of: selectedItemId)
SheetView(isVisible: self.$sheetIsShowing, note: data.notes[index])
}
}
}
}
My rationale was to check whether self.selectedItemId == NULL was null or not, if not then pass that element to the sheet to be edited, if yes, the as it as a new element.
What am I doing wrong? And if there is a standard way to pass information to the sheet based on whether there is an item select or not, could you show me?
Thanks!
From this post, you can do like this in your case:
struct SheetForNewAndEdit: View {
#EnvironmentObject private var data: DataModel
#State var searchText: String = ""
// The selected row
#State var selectedNote: NoteItem? = nil
#State private var sheetNewNote = false
// for test :
#State private var filteredNotes: [NoteItem] = [
NoteItem(id: UUID(), text: "111"),
NoteItem(id: UUID(), text: "222")];
var body: some View {
NavigationView {
List(filteredNotes) { note in
VStack(alignment: .leading) {
//....
// not relevant code
Text(note.text)
}
.swipeActions(allowsFullSwipe: false) {
Button(action: {
// the action select the note to display
selectedNote = note
} ) {
Label("Edit", systemImage: "pencil")
}
}
}
// sheet is displayed depending on selected note
.sheet(item: $selectedNote, content: {
note in
SheetView(note: note)
})
// moved tool bar one level (inside navigation view)
.toolbar {
// Toolbar item to have toolbar
ToolbarItemGroup(placement: .navigationBarTrailing) {
ZStack {
Button(action: {
// change bool value
self.sheetNewNote.toggle()
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
.sheet(isPresented: $sheetNewNote) {
let Note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(note: Note)
}
}
}
}
Note : SheetView does not need any more a boolean, but you can add one if you orefer
Swift uses nil, not null, so the compiler is complaining when you are comparing selected items to null. However, you will have another issue. Your selectedItemId is optional, so you can't just use it in your else clause to make your note. You are better off using an if let to unwrap it. Change it to:
.sheet(isPresented: $sheetIsShowing) {
if let selectedItemId = selectedItemId,
let index = data.notes.firstIndex(where: { $0.id == selectedItemId }) {
SheetView(isVisible: self.$sheetIsShowing, note: data.notes[index])
} else {
let note = NoteItem(id: UUID(), text: "New Note", date: Date(), tags: [])
SheetView(isVisible: self.$sheetIsShowing, note: note)
}
}
edit:
I realized that you were attempting to use two optionals without unwrapping them, so I changed this to an if let to make sure both are safely unwrapped.

Why does this SwiftUI List require an extra objectWillChange.send?

Here is a simple list view of "Topic" struct items. The goal is to present an editor view when a row of the list is tapped. In this code, tapping a row is expected to cause the selected topic to be stored as "tappedTopic" in an #State var and sets a Boolean #State var that causes the EditorV to be presented.
When the code as shown is run and a line is tapped, its topic name prints properly in the Print statement in the Button action, but then the app crashes because self.tappedTopic! finds tappedTopic to be nil in the EditTopicV(...) line.
If the line "tlVM.objectWillChange.send()" is uncommented, the code runs fine. Why is this needed?
And a second puzzle: in the case where the code runs fine, with the objectWillChange.send() uncommented, a print statement in the EditTopicV init() shows that it runs twice. Why?
Any help would be greatly appreciated. I am using Xcode 13.2.1 and my deployment target is set to iOS 15.1.
Topic.swift:
struct Topic: Identifiable {
var name: String = "Default"
var iconName: String = "circle"
var id: String { name }
}
TopicListV.swift:
struct TopicListV: View {
#ObservedObject var tlVM: TopicListVM
#State var tappedTopic: Topic? = nil
#State var doEditTappedTopic = false
var body: some View {
VStack(alignment: .leading) {
List {
ForEach(tlVM.topics) { topic in
Button(action: {
tappedTopic = topic
// why is the following line needed?
tlVM.objectWillChange.send()
doEditTappedTopic = true
print("Tapped topic = \(tappedTopic!.name)")
}) {
Label(topic.name, systemImage: topic.iconName)
.padding(10)
}
}
}
Spacer()
}
.sheet(isPresented: $doEditTappedTopic) {
EditTopicV(tlVM: tlVM, originalTopic: self.tappedTopic!)
}
}
}
EditTopicV.swift (Editor View):
struct EditTopicV: View {
#ObservedObject var tlVM: TopicListVM
#Environment(\.presentationMode) var presentationMode
let originalTopic: Topic
#State private var editTopic: Topic
#State private var ic = "circle"
let iconList = ["circle", "leaf", "photo"]
init(tlVM: TopicListVM, originalTopic: Topic) {
print("DBG: EditTopicV: originalTopic = \(originalTopic)")
self.tlVM = tlVM
self.originalTopic = originalTopic
self._editTopic = .init(initialValue: originalTopic)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Button("Cancel") {
presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
editTopic.iconName = editTopic.iconName.lowercased()
tlVM.change(topic: originalTopic, to: editTopic)
presentationMode.wrappedValue.dismiss()
}
}
HStack {
Text("Name:")
TextField("name", text: $editTopic.name)
Spacer()
}
Picker("Color Theme", selection: $editTopic.iconName) {
ForEach(iconList, id: \.self) { icon in
Text(icon).tag(icon)
}
}
.pickerStyle(.segmented)
Spacer()
}
.padding()
}
}
TopicListVM.swift (Observable Object View Model):
class TopicListVM: ObservableObject {
#Published var topics = [Topic]()
func append(topic: Topic) {
topics.append(topic)
}
func change(topic: Topic, to newTopic: Topic) {
if let index = topics.firstIndex(where: { $0.name == topic.name }) {
topics[index] = newTopic
}
}
static func ex1() -> TopicListVM {
let tvm = TopicListVM()
tvm.append(topic: Topic(name: "leaves", iconName: "leaf"))
tvm.append(topic: Topic(name: "photos", iconName: "photo"))
tvm.append(topic: Topic(name: "shapes", iconName: "circle"))
return tvm
}
}
Here's what the list looks like:
Using sheet(isPresented:) has the tendency to cause issues like this because SwiftUI calculates the destination view in a sequence that doesn't always seem to make sense. In your case, using objectWillSend on the view model, even though it shouldn't have any effect, seems to delay the calculation of your force-unwrapped variable and avoids the crash.
To solve this, use the sheet(item:) form:
.sheet(item: $tappedTopic) { item in
EditTopicV(tlVM: tlVM, originalTopic: item)
}
Then, your item gets passed in the closure safely and there's no reason for a force unwrap.
You can also capture tappedTopic for a similar result, but you still have to force unwrap it, which is generally something we want to avoid:
.sheet(isPresented: $doEditTappedTopic) { [tappedTopic] in
EditTopicV(tlVM: tlVM, originalTopic: tappedTopic!)
}

SwiftUI Textfields inside Lists

I want a list with rows, with each row having 2 Textfields inside of it. Those rows should be saved in an array, so that I can use the data in an other view for further functions. If the text in the Textfield is changed, the text should be saved inside the right entry in the array.
You also can add new rows to the list via a button, which should also change the array for the rows.
The goal is to have a list of key value pairs, each one editable and those entries getting saved with the current text.
Could someone help me and/or give me hint for fixing this problem?
So far I have tried something like this:
// the array of list entries
#State var list: [KeyValue] = [KeyValue()]
// the List inside of a VStack
List(list) { entry in
KeyValueRow(list.$key, list.$value)
}
// the single item
struct KeyValue: Identifiable {
var id = UUID()
#State var key = ""
#State var value = ""
}
// one row in the list with view elements
struct KeyValueRow: View {
var keyBinding: Binding<String>
var valueBinding: Binding<String>
init(_ keyBinding: Binding<String>, _ valueBinding: Binding<String>){
self.keyBinding = keyBinding
self.valueBinding = valueBinding
}
var body: some View {
HStack() {
TextField("key", text: keyBinding)
Spacer()
TextField("value", text: valueBinding)
Spacer()
}
}
}
Also, about the button for adding new entries.
Problem is that if I do the following, my list in the view goes blank and everything is empty again
(maybe related: SwiftUI TextField inside ListView goes blank after filtering list items ?)
Button("Add", action: {
self.list.append(KeyValue())
})
I am not sure what the best practice is keep a view up to date with state in an array like this, but here is one approach to make it work.
For the models, I added a list class that conforms to Observable object, and each KeyValue item alerts it on changes:
class KeyValueList: ObservableObject {
#Published var items = [KeyValue]()
func update() {
self.objectWillChange.send()
}
func addItem() {
self.items.append(KeyValue(parent: self))
}
}
class KeyValue: Identifiable {
init(parent: KeyValueList) {
self.parent = parent
}
let id = UUID()
private let parent: KeyValueList
var key = "" {
didSet { self.parent.update() }
}
var value = "" {
didSet { self.parent.update() }
}
}
Then I was able to simply the row view to just keep a single piece of state:
struct KeyValueRow: View {
#State var item: KeyValue
var body: some View {
HStack() {
TextField("key", text: $item.key)
Spacer()
TextField("value", text: $item.value)
Spacer()
}
}
}
And for the list view:
struct TextFieldList: View {
#ObservedObject var list = KeyValueList()
var body: some View {
VStack {
List(list.items) { item in
HStack {
KeyValueRow(item: item)
Text(item.key)
}
}
Button("Add", action: {
self.list.addItem()
})
}
}
}
I just threw an extra Text in there for testing to see it update live.
I did not run into the Add button blanking the view as you described. Does this solve that issue for you as well?
Working code example for iOS 15
In SwiftUI, Apple recommends passing the binding directly into the List constructor and using a #Binding in the ViewBuilder block to iterate through with each element.
Apple recommends this approach over using the Indices to iterate over the collection since this doesn't reload the whole list every time a TextField value changes (better efficiency).
The new syntax is also back-deployable to previous releases of SwiftUI apps.
struct ContentView: View {
#State var directions: [Direction] = [
Direction(symbol: "car", color: .mint, text: "Drive to SFO"),
Direction(symbol: "airplane", color: .blue, text: "Fly to SJC"),
Direction(symbol: "tram", color: .purple, text: "Ride to Cupertino"),
Direction(symbol: "bicycle", color: .orange, text: "Bike to Apple Park"),
Direction(symbol: "figure.walk", color: .green, text: "Walk to pond"),
Direction(symbol: "lifepreserver", color: .blue, text: "Swim to the center"),
Direction(symbol: "drop", color: .indigo, text: "Dive to secret airlock"),
Direction(symbol: "tram.tunnel.fill", color: .brown, text: "Ride through underground tunnels"),
Direction(symbol: "key", color: .red, text: "Enter door code:"),
]
var body: some View {
NavigationView {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
}
}
.listStyle(.sidebar)
.navigationTitle("Secret Hideout")
}
}
}
struct Direction: Identifiable {
var id = UUID()
var symbol: String
var color: Color
var text: String
}
No need to mess up with classes, Observable, Identifiable. You can do it all with structs.
Note, that version below will do fine for insertions, but fail if you try to delete array elements:
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct ContentView: View {
#State var boolArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
// id: \.self is obligatory if you need to insert
List(boolArr.indices, id: \.self) { idx in
HStack() {
TextField("key", text: self.$boolArr[idx].key)
Spacer()
TextField("value", text: self.$boolArr[idx].value)
Spacer()
}
}
.navigationBarItems(leading:
Button(action: {
self.boolArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.boolArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.boolArr.removeLast() // causes "Index out of range" error
print(self.boolArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Update:
A little trick to make it work with deletions as well.
import SwiftUI
// the single item
struct KeyValue {
var key: String
var value: String
}
struct KeyValueView: View {
#Binding var model: KeyValue
var body: some View {
HStack() {
TextField("Key", text: $model.key)
Spacer()
TextField("Value", text: $model.value)
Spacer()
}
}
}
struct ContentView: View {
#State var kvArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]
var body: some View {
NavigationView {
List(kvArr.indices, id: \.self) { i in
KeyValueView(model: Binding(
get: {
return self.kvArr[i]
},
set: { (newValue) in
self.kvArr[i] = newValue
}))
}
.navigationBarItems(leading:
Button(action: {
self.kvArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
print(self.kvArr)
})
{ Text("Add") }
, trailing:
Button(action: {
self.kvArr.removeLast() // Works like a charm
print(self.kvArr)
})
{ Text("Remove") })
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Swift 5.5
This version of swift enables one line code for this by passing the bindable item directly from the array.
struct DirectionsList: View {
#Binding var directions: [Direction]
var body: some View {
List($directions) { $direction in
Label {
TextField("Instructions", text: $direction.text)
} icon: {
DirectionsIcon(direction)
}
}
}
}

How can we add `Button` and `TextField` by using `SwiftUI`

I am learning SwiftUI (New framework provided by Apple with iOS 13 and Xcode 11 : SwiftUI by Apple).
I want to add Button and TextField in ListView with action. I want one textfield in that user can add any one number from 1 to 10 and then hit SEND button. Anyone have any idea how to add button in it and also how can we handle touch event of Button with SwiftUI ?
Any help would be appreciate.
Here is a simple view what contains a textfield and a button in a horizontal stack.
To handle the user interaction with in your Button, just overwrite the action closure.
import SwiftUI
struct ButtonAndTextFieldView : View {
#State var text: String = ""
var body: some View {
HStack {
TextField($text,
placeholder: Text("type something here..."))
Button(action: {
// Closure will be called once user taps your button
print(self.$text)
}) {
Text("SEND")
}
}
}
}
#if DEBUG
struct ButtonWithTextFieldView_Previews : PreviewProvider {
static var previews: some View {
ButtonWithTextFieldView()
}
}
#endif
For the Login page design you can use this code section. With textFieldStyle border textfield and content type set.
struct ButtonAndTextFieldView : View {
#State var email: String = ""
#State var password: String = ""
var body: some View {
VStack {
TextField($email,
placeholder: Text("email"))
.textFieldStyle(.roundedBorder)
.textContentType(.emailAddress)
TextField($password,
placeholder: Text("password"))
.textFieldStyle(.roundedBorder)
.textContentType(.password)
Button(action: {
//Get Email and Password
print(self.$email)
print(self.$password)
}) {
Text("Send")
}
}
}
You can add button like that
Button(action: {}) {
Text("Increment Total")
}
And text field.
#State var bindingString: Binding<String> = .constant("")
TextField(bindingString,
placeholder: Text("Hello"),
onEditingChanged: { editing in
print(editing)
}).padding(.all, 40)
You can write a custom TextField which will return you the event in the closure once the user taps on the button. This Custom textfield would contain a HStack with a textfield and a button. Like this.
struct CustomTextField : View {
#Binding var text: String
var editingChanged: (Bool)->() = { _ in }
var commit: ()->() = { }
var action : () -> Void
var buttonTitle : String
var placeholder: String
var isSecuredField = false
var body : some View {
HStack {
if isSecuredField {
SecureField(placeholder, text: $text, onCommit: commit)
} else {
TextField(placeholder, text: $text, onEditingChanged: editingChanged, onCommit: commit)
}
Button(action: action) {
Text(buttonTitle)
}
}
}
}
And to you can use this custom TextField like this. I have used an example from the above-listed answers to make it more clear.
struct ListView: View {
#State var text: String = ""
var body: some View {
List {
ForEach (1..<2) {_ in
Section {
CustomTextField(
text: self.$text,
action: {
print("number is .....\(self.text)")
},
buttonTitle: "Submit",
placeholder: "enter your number")
}
}
}
}
}
ListView with textfield and button. You will need an identifier for each row in case you want to have multiple rows in the List.
struct ListView: View {
#State var text: String = ""
var body: some View {
List {
ForEach (1..<2) {_ in
Section {
HStack(alignment: .center) {
TextField(self.$text, placeholder: Text("type something here...") ).background(Color.red)
Button(action: {
print(self.$text.value)
} ) {
Text("Send")
}
}
}
}
}
}
}

Resources