Storing restoring text upon rebuild of `TextEditor` in `LazyVStack` - swiftui

I'm using LazyVStack to list TextEditor objects. But it rebuilds while scrolling (as expected in Lazystacks) but I want to restore the text I was typing in each Text Editor. I'm thinking of using objectIndex and saving it to String array to save and retrieve the text. I don't wanna use Non-Lazy stacks. Any other better ideas to store and restore the text?
import SwiftUI
struct ContentView: View {
#State private var text: String = ""
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(0..<20, id: \.self) { objectIndex in
VStack {
TextEditorObject (text: text)
}
}
}
}
}
}
}
struct TextEditorObject: View {
#State var text: String
var body: some View {
VStack {
VStack {
TextEditor (text: $text)
.frame(width: 200, height: 200, alignment: .leading)
}
}
}
}

In TextEditorObject change #State var text: String to #Binding var text: String

Related

The searchable modifier on tvOS, when inside a NavigationView, doesn't allow refocusing the search bar

When using a NavigationView and a ScrollView with searchable, as soon as you focus a item in the LazyVGrid the search bar collapses the keyboard, and it's no longer possible to re-focus the search bar to change the query.
It doesn't matter if the .searchable modifier is applied to the ScrollView or the NavigationView.
The more I look at it, the more it appears to be a SwiftUI bug on tvOS, but I would still like to find a workaround, if possible.
Sample code which reproduces the problem:
import SwiftUI
struct ContentView: View {
private var fruits = ["Apples", "Pears", "Oranges", "Plums", "Pineapples", "Bananas"]
#State private var items: [String]
#State private var searchText: String = ""
init() {
self.items = fruits
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 300))], spacing: 40) {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(text: item)) {
Text(item)
}
}
}
}
.searchable(text: $searchText)
.onChange(of: searchText) { query in
if query.isEmpty {
items = fruits
} else {
items = fruits.filter { $0.contains(query) }
}
}
}
}
}
struct DetailView: View {
let text: String
var body: some View {
Text(text)
}
}
Gif illustrating the problem:

SwiftUI - modifiying variable foreach on View before onTapGesture()

Foreach on view must be presented with a View to process.
struct Home : View {
private var numberOfImages = 3
#State var isPresented : Bool = false
#State var currentImage : String = ""
var body: some View {
VStack {
TabView {
ForEach(1..<numberOfImages+1, id: \.self) { num in
Image("someimage")
.resizable()
.scaledToFill()
.onTapGesture() {
currentImage = "top_00\(num)"
isPresented.toggle()
}
}
}.fullScreenCover(isPresented: $isPresented, content: {FullScreenModalView(imageName: currentImage) } )
}
}
I'm trying to display an image in fullScreenCover. My problem is that the first image is empty. Yes, we can solve this defining at the beginning, however, this will complicate the code according to my experiences.
My question is, is it possible to assign a value to currentImage before the onTapGesture processed.
In short, what is the good practice here.
What you need is to use this modifier to present your full screen modal:
func fullScreenCover<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: #escaping (Item) -> Content) -> some View where Item : Identifiable, Content : View
You pass in a binding to an optional and uses the non optional value to construct a destination:
struct ContentView: View {
private let imageNames = ["globe.americas.fill", "globe.europe.africa.fill", "globe.asia.australia.fill"]
#State var selectedImage: String?
var body: some View {
VStack {
TabView {
ForEach(imageNames, id: \.self) { imageName in
Image(systemName: imageName)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageName
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageName in
Destination(imageName: imageName)
}
}
}
}
struct Destination: View {
let imageName: String
var body: some View {
ZStack {
Color.blue
Image(
systemName: imageName
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
}
}
You will have to make String identifiable for this example to work (not recommended):
extension String: Identifiable {
public var id: String { self }
}
Building upon #LuLuGaGa’s answer (accept that answer, not this one), instead of making String Identifiable, create a new Identifiable struct to hold the image info. This guarantees each image will have a unique identity, even if they use the same base image name. Also, the ForEach loop now becomes ForEach(imageInfos) since the array contains Identifiable objects.
Use map to turn image name strings into [ImageInfo] by calling the ImageInfo initializer with each name.
This example also puts the displayed image into its own view which can be dismissed with a tap.
import SwiftUI
struct ImageInfo: Identifiable {
let name: String
let id = UUID()
}
struct ContentView: View {
private let imageInfos = [
"globe.americas.fill",
"globe.europe.africa.fill",
"globe.asia.australia.fill"
].map(ImageInfo.init)
#State var selectedImage: ImageInfo?
var body: some View {
VStack {
TabView {
ForEach(imageInfos) { imageInfo in
Image(systemName: imageInfo.name)
.resizable()
.scaledToFit()
.padding()
.onTapGesture() {
selectedImage = imageInfo
}
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
.fullScreenCover(item: $selectedImage) { imageInfo in
ImageDisplay(info: imageInfo)
}
}
}
}
struct ImageDisplay: View {
let info: ImageInfo
#Environment(\.dismiss) var dismiss
var body: some View {
ZStack {
Color.blue
Image(
systemName: info.name
)
.resizable()
.scaledToFit()
.foregroundColor(.green)
}
.edgesIgnoringSafeArea(.all)
.onTapGesture {
dismiss()
}
}
}

SwiftUI TextFields based on #Published array not updating

I am trying to lay out a bunch of CustomTextViews which can toggle between a SwiftUI TextField or Text view.
Consider this example.
import SwiftUI
struct ContentView: View {
#StateObject var doc: Document = Document()
var body: some View {
ForEach(doc.lines, id: \.self) { line in
HStack {
ForEach(line, id: \.self) { word in
CustomTextView(text: word, document: doc)
.fixedSize()
}
Spacer()
}
}
.frame(width: 300, height: 300)
.background(.cyan)
}
}
struct CustomTextView: View {
#State var text: String
#State var isEditing: Bool = false
#ObservedObject var document: Document
var body: some View {
if isEditing {
TextField("", text: $text)
.onSubmit {
isEditing.toggle()
// NOTE: reset document anytime a word ends in "?"
if text.last! == "?" {
print("resetting")
document.lines = [["Reset"]]
print(document.lines)
}
}
} else {
Text(text)
.onTapGesture {
isEditing.toggle()
}
}
}
}
class Document: ObservableObject {
#Published var lines: [[String]] = [["Hello"]]
}
What I want to happen is that I should be able to indefinitely reset the text. But instead, the view only resets correctly once (see gif). All further updates to reset document.lines are not correct, even though the print statements show that the #Published property lines is clearly changing.
What am I doing wrong?
You need to update the textfield text with document.lines[index] where index is index of line into document.lines array. So you need to update like below.
struct CustomTextView: View {
#State var isEditing: Bool = false
#ObservedObject var document: Document
var body: some View {
if isEditing {
TextField("", text: $document.lines[index])
.onSubmit {
isEditing.toggle()
// NOTE: reset document anytime a word ends in "?"
if document.lines[index].last! == "?" {
print("resetting")
document.lines = [["Reset"]]
print(document.lines)
}
}
} else {
Text(text)
.onTapGesture {
isEditing.toggle()
}
}
}
}

SwiftUI tap picker like a button

Is it possible to make a SwiftUI picker tap like a button. The following only lets you set the picker if you tap on the text, but I would like to tap the picker like a button.
struct ContentView: View {
#State var myvar: String = ""
var body: some View {
Picker(selection: $myvar, label:
Text("Phone Type")) {
Text("Home").tag(0)
Text("Service").tag(1)
Text("Work").tag(2)
Text("Cell").tag(3)
Text("Other").tag(4)
}.padding().border(Color.gray)
}
}
Thanks for any help!
you could try this with .pickerStyle. Note that your tag(...) needs to match the type in myvar, so using string, like this:
struct ContentView: View {
#State var myvar: String = ""
var body: some View {
Picker(selection: $myvar, label: Text("Phone Type")) {
Text("Home").tag("Home")
Text("Service").tag("Service")
Text("Work").tag("Work")
Text("Cell").tag("Cell")
Text("Other").tag("Other")
}
.pickerStyle(.segmented)
.padding().border(Color.gray)
}
}
#Asperi was right in their comment. The Menu is better than the picker when wanting it to display as a button. Here is a simple code example.
struct ContentView: View {
#State var myvar: String = ""
var body: some View {
Menu {
// This should be done in a ForEach loop
Button("Home", action: {myvar="Home"})
Button("Service", action: {myvar="Service"})
Button("Work", action: {myvar="Work"})
Button("Cell", action: {myvar="Cell"})
Button("Other", action: {myvar="Other"})
} label: {
//THIS ALLOWS CLICKING TO OPEN ON THE BLUE BACKGROUND
Text("ClickHere")
.frame(width: 200, height: 100, alignment: .center)
.background(Color.blue)
.foregroundColor(Color.white)
}
}
}

SwiftUI: Add ClearButton to TextField

I am trying to add a ClearButton to TextField in SwiftUI when the particular TextField is selected.
The closest I got was creating a ClearButton ViewModifier and adding it to the TextField using .modifer()
The only problem is ClearButton is permanent and does not disappear when TextField is deselected
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier {
#Binding var text: String
public func body(content: Content) -> some View {
HStack {
content
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
}
}
}
}
Use ZStack to position the clear button appear inside the TextField.
TextField("Some Text" , text: $someBinding).modifier(ClearButton(text: $someBinding))
struct ClearButton: ViewModifier
{
#Binding var text: String
public func body(content: Content) -> some View
{
ZStack(alignment: .trailing)
{
content
if !text.isEmpty
{
Button(action:
{
self.text = ""
})
{
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
.padding(.trailing, 8)
}
}
}
}
Use .appearance() to activate the button
var body: some View {
UITextField.appearance().clearButtonMode = .whileEditing
return TextField(...)
}
For reuse try with this:
func TextFieldUIKit(text: Binding<String>) -> some View{
UITextField.appearance().clearButtonMode = .whileEditing
return TextField("Nombre", text: text)
}
=== solution 1(best): Introspect https://github.com/siteline/SwiftUI-Introspect
import Introspect
TextField("", text: $text)
.introspectTextField(customize: {
$0.clearButtonMode = .whileEditing
})
=== solution 2: ViewModifier
public struct ClearButton: ViewModifier {
#Binding var text: String
public init(text: Binding<String>) {
self._text = text
}
public func body(content: Content) -> some View {
HStack {
content
Spacer()
Image(systemName: "multiply.circle.fill")
.foregroundColor(.secondary)
.opacity(text == "" ? 0 : 1)
.onTapGesture { self.text = "" } // onTapGesture or plainStyle button
}
}
}
Usage:
#State private var name: String
...
Form {
Section() {
TextField("NAME", text: $name).modifier(ClearButton(text: $name))
}
}
=== solution 3: global appearance
UITextField.appearance().clearButtonMode = .whileEditing
You can add another Binding in your modifier:
#Binding var visible: Bool
then bind it to opacity of the button:
.opacity(visible ? 1 : 0)
then add another State for checking textField:
#State var showClearButton = true
And lastly update the textfield:
TextField("Some Text", text: $someBinding, onEditingChanged: { editing in
self.showClearButton = editing
}, onCommit: {
self.showClearButton = false
})
.modifier( ClearButton(text: $someBinding, visible: $showClearButton))
Not exactly what you're looking for, but this will let you show/hide the button based on the text contents:
HStack {
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle")
}
}
}
After initializing a new project we need to create a simple view modifier which we will apply later to our text field. The view modifier has the tasks to check for content in the text field element and display a clear button inside of it, if content is available. It also handles taps on the button and clears the content.
Let’s have a look at that view modifier:
import SwiftUI
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = "" },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.opaqueSeparator))
}
)
}
}
}
}
The code itself should be self explanatory and easy to understand as there is no fancy logic included in our tasks.
We just wrap the textfield inside a HStack and add the button, if the text field is not empty. The button itself has a single action of deleting the value of the text field.
For the clear icon we use the delete.left icon from the SF Symbols 2 library by Apple, but you could also use another one or even your own custom one.
The binding of the modifier is the same as the one we apply to the text field. Without it we would not be able to check for content or clear the field itself.
Inside the ContentView.swift we now simply add a TextField element and apply our modifier to it — that’s all!
import SwiftUI
struct ContentView: View {
#State var exampleText: String = ""
var body: some View {
NavigationView {
Form {
Section {
TextField("Type in your Text here...", text: $exampleText)
.modifier(TextFieldClearButton(text: $exampleText))
.multilineTextAlignment(.leading)
}
}
.navigationTitle("Clear button example")
}
}
}
The navigation view and form inside of the ContentView are not required. You could also just add the TextField inside the body, but with a form it’s much clearer and beautiful. 🙈
And so our final result looks like this:
I found this answer from #NigelGee on "Hacking with Swift".
.onAppear {
UITextField.appearance().clearButtonMode = .whileEditing
}
It really helped me out.
Simplest solution I came up with
//
// ClearableTextField.swift
//
// Created by Fred on 21.11.22.
//
import SwiftUI
struct ClearableTextField: View {
var title: String
#Binding var text: String
init(_ title: String, text: Binding<String>) {
self.title = title
_text = text
}
var body: some View {
ZStack(alignment: .trailing) {
TextField(title, text: $text)
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
.onTapGesture {
text = ""
}
}
}
}
struct ClearableTextField_Previews: PreviewProvider {
#State static var text = "some value"
static var previews: some View {
Form {
// replace TextField("Original", text: $text) with
ClearableTextField("Clear me", text: $text)
}
}
}