How to show the placeholder if the value of the TextField is equal to "0" in SwiftUI ?
Here is a right way of doing this, with custom Binding:
struct ContentView: View {
#State private var stringOfTextField: String = String()
var body: some View {
VStack(spacing: 20.0) {
TextField("Input your value Here!", text: Binding.init(get: { () -> String in
if (stringOfTextField != "0") { return stringOfTextField }
else { return "" } },
set: { (newValue) in stringOfTextField = newValue }))
.textFieldStyle(RoundedBorderTextFieldStyle())
HStack {
Button("set value to 1000") { stringOfTextField = "1000" }
Spacer()
Button("set value to 0") { stringOfTextField = "0" }
}
}
.padding()
}
}
You can use ZStack with TextField + Text and apply opacity depending on current text:
#State
var text = ""
var body: some View {
let isZero = text == "0"
ZStack(alignment: .leading) {
TextField("", text: $text)
.opacity(isZero ? 0 : 1)
Text("this is zero")
.opacity(isZero ? 1 : 0)
}
}
I would do something like this.
Keep in mind this will block the user from entering "0" as the first character.
struct ContentView: View {
#State var text = ""
var body: some View {
TextField("Placeholder", text: $text)
.onChange(of: text, perform: { value in
if value == "0" {
text = ""
}
})
}
}
If your var is #Binding then use this:
struct ContentView: View {
#Binding var text: String
var body: some View {
TextField("Placeholder", text: $text)
.onChange(of: $text.wrappedValue, perform: { value in
if value == "0" {
text = ""
}
})
}
}
Related
My custom text editor below once you click on the pen to edit, a new space appears so the text from before is not on the same line as the new one. How can I fix this? Here's a simple reproducible example:
struct SwiftUIView: View {
#State var name: String = "test"
#State var showEdit: Bool = true
var body: some View {
HStack {
HStack {
if(showEdit) {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
} else {
Text(name)
.font(.headline)
}
}
Spacer()
Button(action: {
showEdit.toggle()
}) {
Image(systemName: "pencil")
.foregroundColor(.secondary)
}
}
}
}
struct CustomTextEditor: View {
let placeholder: String
#Binding var text: String
var body: some View {
ZStack {
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color.primary.opacity(0.25))
}
TextEditor(text: $text)
}.onAppear() {
UITextView.appearance().backgroundColor = .clear
}.onDisappear() {
UITextView.appearance().backgroundColor = nil
}
}
}
I want it to have the same padding properies as inserting a simple Text("") so when I switch between Text("xyz") and TextEditor(text: $xyz) it has the same padding alignment. Right now TextEditor has a weird padding.
You will drive yourself insane trying to line up a Text and a TextEditor (or a TextField, for that matter), so don't try. Use another, disabled, TextEditor instead, and control the .opacity() on the top one depending upon whether the bound variable is empty or not. Like this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
}
}
}
This view will show the placeholder if there is no text, and hide the placeholder as soon as there is text.
Edit:
You don't need the button, etc. in your other view. It becomes simply:
struct SwiftUIView: View {
#State var name: String = ""
var body: some View {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
.padding()
}
}
and if you need a "Done" button on the keyboard, change your CustomTextEditor() to this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
#FocusState var isFocused: Bool
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
.focused($isFocused)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button {
isFocused = false
} label: {
Text("Done")
.foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
}
}
#State var multiOptions = [""]
I'm trying to create textfields for a user to populate with numbers but I want them to choose the amount of textfields they will populate. To do this I have a ForEach loop.
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
This loop creates a textfield for every empty string in multiOptions. I will include a way for the user to specify the amount of textfields they want but I was wondering if there was a way to use the users number and multiply it to multiOptions so I have the amount of empty strings that the user would like. So if the user chooses to have 6 textfields then multiOptions will have 6 empty strings.
Here is a possible approach for you:
struct ContentView: View {
#State private var options: [String] = [String]()
#State private var countOfOptions: Int = Int()
var body: some View {
VStack(spacing: 5.0) {
HStack {
TextField("Enter count of Options", text: Binding(get: { () -> String in return (countOfOptions != 0) ? String(describing: countOfOptions) : "" },
set: { newValue in countOfOptions = Int(newValue) ?? 0 }), onCommit: { optionBuilder() })
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Create Options") { optionBuilder() }
Button("removeAll") { options.removeAll(); countOfOptions = 0 }.foregroundColor(.red)
}
ForEach(options.indices, id: \.self) { index in
TextField("Enter your option...", text: Binding(get: { () -> String in return options[index] },
set: { newValue in options[index] = newValue }))
.textFieldStyle(RoundedBorderTextFieldStyle())
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
}
.padding(.horizontal)
.animation(Animation.default, value: options)
}
private func optionBuilder() {
if options.count < countOfOptions {
for _ in 0..<countOfOptions {
if (options.count < countOfOptions) { options.append(String()) }
else { break }
}
}
else {
for _ in 0..<options.count {
if (options.count > countOfOptions) { options.removeLast() }
else { break }
}
}
}
}
Here is a relatively simple answer, that is very simple to implement:
struct VariableTextFields: View {
#State var fieldCount: Int = 1
#State var multiOptions = [""]
var body: some View {
VStack {
Button {
multiOptions.append("")
} label: {
Text("Add Field")
}
List {
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.onDelete { offsets in
multiOptions.remove(atOffsets: offsets)
}
}
}
}
}
SwiftUI will keep track of the count of multiOptions in the ForEach, so all you need to do is allow the user to add and subtract from it. I implemented this behavior with a button and an .onDelete() on a List.
I have a TextEditor that calls actionSheet when user tap on it, and displays action sheet content inside. Is it possible to keep TextEditor interactable but not editable ? I've tried .disabled() - but in this case, TextEditor becomes untapable, so user can't call ActionSheet. I would like to avoid using UIViewRepresentable to solving this.
import SwiftUI
import NavigationStack
struct CompleteSessionView: View {
#StateObject var viewModel = CompleteSessionViewModel()
#EnvironmentObject private var navigationStack: NavigationStack
#Binding var isPresented: Bool
#State var sessionManager: SessionManager
#State var showingSkatingStyleSheet = false
var body: some View {
VStack(alignment: .leading, spacing: 5) {
TextBox(text: $viewModel.style, placeholder: "", multiLine:false)
.onTapGesture {
showingSkatingStyleSheet = true
}
}
.actionSheet(isPresented: $showingSkatingStyleSheet) {
styleActionSheet()
}
.onAppear {
viewModel.style = sessionManager.skatingStyle?.localizedString ?? ""
}
}
func styleActionSheet() -> ActionSheet {
return ActionSheet(title: Text("new_sessions_page_style_action_sheet_title".localized), message: Text("new_sessions_page_style_action_sheet_msg".localized), buttons: [
.default(Text(SkatingStyle.streetSkating.localizedString)) {
sessionManager.skatingStyle = .streetSkating
viewModel.style = SkatingStyle.streetSkating.localizedString
},
.default(Text(SkatingStyle.trailSkating.localizedString)) {
sessionManager.skatingStyle = .trailSkating
viewModel.style = SkatingStyle.trailSkating.localizedString
},
.default(Text(SkatingStyle.rollersDance.localizedString)) {
sessionManager.skatingStyle = .rollersDance
viewModel.style = SkatingStyle.rollersDance.localizedString
},
.default(Text(SkatingStyle.freestyle.localizedString)) {
sessionManager.skatingStyle = .freestyle
viewModel.style = SkatingStyle.freestyle.localizedString
},
.default(Text(SkatingStyle.rollerDerby.localizedString)) {
sessionManager.skatingStyle = .rollerDerby
viewModel.style = SkatingStyle.rollerDerby.localizedString
},
.default(Text(SkatingStyle.aggresive.localizedString)) {
sessionManager.skatingStyle = .aggresive
viewModel.style = SkatingStyle.aggresive.localizedString
},
.default(Text(SkatingStyle.indoorRinks.localizedString)) {
sessionManager.skatingStyle = .indoorRinks
viewModel.style = SkatingStyle.indoorRinks.localizedString
},
.default(Text(SkatingStyle.artistic.localizedString)) {
sessionManager.skatingStyle = .artistic
viewModel.style = SkatingStyle.artistic.localizedString
},
.cancel()
])
}
}
struct TextBox: View {
#Binding var text: String
var placeholder: String
var multiLine: Bool
var body: some View {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
.introspectTextField { textfield in
textfield.returnKeyType = .next
}
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.padding(.horizontal, 8)
if text.isEmpty {
Text(placeholder)
.foregroundColor(.black.opacity(0.25))
.padding(.top, 8)
.padding(.horizontal, 12)
.allowsHitTesting(false)
}
}
}
}
Is it possible to keep TextEditor interactable but not editable?
Yes. You can give a constant to the expected parameter text. You can interact with it the normal way but there is no way to edit the text.
E.g.
struct ContentView: View {
#State var text = "Placeholder"
var body: some View {
SecondView(text: self.$text)
}
}
struct SecondView: View {
#Binding var text: String
var body: some View {
TextEditor(text: .constant(self.text))
.padding(50)
}
}
First of all, sorry for the title which is not precise at all, but I didn't know how else I could title this question. So:
1. What I want to achieve:
I want to create an interface where the user can add sections to input different fields, which are fully customizable by the user himself.
2. What I have done so far
I was able to create the interface, I can add new sections easily (structured with a "Section Name" on top and a TextField below) and they are customizable, but only in the TextField. They are also deletable, even though I had to do a complicated workaround since the Binding text of the TextField caused the app to crash because the index at which I was trying to remove the item resulted as "out of range". It's not the perfect workaround, but it works, and for now this is the most important thing. When I'll save these sections, I'll save them as an array of Dictionaries where every Dictionary has the section name and its value. However, there's still a few things I wasn't able to do:
3. What I haven't done yet
There are still 3 things that I couldn't do yet.
First of all, I'd like for the name of the section to be editable.
Secondly, I'd like to have the sections that the user adds displayed inside a Form and divided by Sections. As header, I'd like to have each different section name, and grouped inside all the related sections that share the same name.
Last but not least, I'd like to allow the user to add multiple TextFields to the same section. I have no idea how to handle this or even if it's possible.
4. Code:
ContentView:
import SwiftUI
struct ContentView: View {
#State var editSections = false
#State var arraySections : [SectionModel] = [SectionModel(name: "Prova", value: ""), SectionModel(name: "Prova 2", value: ""), SectionModel(name: "Prova", value: "")]
#State var showProgressView = false
#State var arraySectionsForDeleting = [SectionModel]()
#State var openSheetAdditionalSections = false
var body: some View {
Form {
if showProgressView == false {
if editSections == false {
ForEach(arraySections, id: \.id) { sect in
Section(header: Text(sect.name)) {
ForEach(arraySections, id: \.id) { sez in
if sez.name == sect.name {
SectionView(section: sez)
}
}
}
}
} else {
forEachViewSectionsForDeleting
}
if arraySections.count > 0 {
buttoneditSections
}
} else {
loadingView
}
Section {
addSections
}
}
.sheet(isPresented: $openSheetAdditionalSections, content: {
AdditionalSectionsSheet(closeSheet: $openSheetAdditionalSections, contView: self)
})
}
var forEachViewSectionsForDeleting : some View {
Section {
ForEach(arraySections, id: \.id) { sez in
HStack {
Text("\(sez.name) - \(sez.value)")
.foregroundColor(.black)
Spacer()
Button(action: {
showProgressView = true
let idx = arraySections.firstIndex(where: { $0.id == sez.id })
arraySectionsForDeleting.remove(at: idx!)
arraySections = []
arraySections = arraySectionsForDeleting
showProgressView = false
}, label: {
Image(systemName: "minus.circle")
.foregroundColor(.yellow)
}).buttonStyle(BorderlessButtonStyle())
}
}
}
}
var buttoneditSections : some View {
Button(action: {
editSections.toggle()
}, label: {
Text(editSections == true ? "Done" : "Edit Sections")
.foregroundColor(.yellow)
})
}
var forEachviewSezioniNonModifica : some View {
Section {
ForEach(arraySections, id: \.id) { sez in
Text(sez.name)
.foregroundColor(.black)
Text(sez.value)
.foregroundColor(.black)
}
}
}
var addSections : some View {
Button(action: {
openSheetAdditionalSections = true
}, label: {
HStack {
Text("Add sections")
.foregroundColor(.yellow)
Spacer()
Image(systemName: "plus.circle")
.foregroundColor(.yellow)
}
})
}
var loadingView : some View {
Section {
HStack {
Spacer()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .black))
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddSectionSheet and SectionView:
import SwiftUI
struct AdditionalSectionsSheet: View {
#Binding var closeSheet : Bool
#Environment(\.colorScheme) var colorScheme
var contView : ContentView
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#GestureState private var dragOffset = CGSize.zero
var body: some View {
NavigationView {
Form {
buttonPhone
buttonUrl
buttonEmail
buttonAddress
}
.navigationBarTitle(Text("Add section"), displayMode: .inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: Button(action : {
closeSheet = false
}){
Text("Close")
.foregroundColor(.yellow)
})
}
}
var buttonPhone : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "Phone", value: ""))
contView.showProgressView = true
closeSheet = false
}, label: {
HStack {
Text("Phone")
.foregroundColor(.black)
Spacer()
}
})
}
var buttonUrl : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "URL", value: ""))
closeSheet = false
}, label: {
HStack {
Text("URL")
.foregroundColor(.black)
Spacer()
}
})
}
var buttonAddress : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "Address", value: ""))
contView.showProgressView = true
closeSheet = false
}, label: {
HStack {
Text("Address")
.foregroundColor(.black)
Spacer()
}
})
}
var buttonEmail : some View {
Button(action: {
contView.editSections = false
contView.arraySections.append(SectionModel(name: "Email", value: ""))
contView.showProgressView = true
closeSheet = false
}, label: {
HStack {
Text("Email")
.foregroundColor(.black)
Spacer()
}
})
}
}
struct SectionView : View {
#Environment(\.colorScheme) var colorScheme
#ObservedObject var section : SectionModel
var body : some View {
Section {
Text(section.name)
.foregroundColor(.black)
TextField(section.name, text: self.$section.value)
.foregroundColor(.black)
}
}
}
SectionModel:
import SwiftUI
import Combine
class SectionModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var value : String
init(name: String, value: String) {
self.name = name
self.value = value
}
static func == (lhs: SectionModel, rhs: SectionModel) -> Bool {
true
}
static func < (lhs: SectionModel, rhs: SectionModel) -> Bool {
true
}
}
I hope I wrote things clear enough to be understood, thanks to everyone who will help!
I want to animate the appearance of an item in a list. The list looks like this:
Text("Jim")
Text("Jonas")
TextField("New Player")
TextField("New Player") //should be animated when appearing (not shown until a name is typed in the first "New Player")
The last TextField should be hidden until newPersonName.count > 0 and then appear with an animation.
This is the code:
struct V_NewSession: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
var body: some View {
VStack(alignment: .leading) {
ForEach(0...self.participants.count + 1, id: \.self) { i in
// Without this if statement, the animation works
// but the Textfield shouldn't be shown until a name is typed
if (!(self.newPersonName.count == 0 && i == self.participants.count + 1)) {
HStack {
if(i < self.participants.count) {
Text(self.participants[i])
} else {
TextField("New Player",
text: $newPersonName,
onEditingChanged: { focused in
if (self.newPersonName.count == 0) {
if (!focused) {
handleNewPlayerEntry()
}
}
})
}
}
}
}
.transition(.scale)
Spacer()
}
}
func handleNewPlayerEntry() {
if(newPersonName.count > 0) {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
}
}
}
}
I know withAnimation(...) only applies to participants.append(newPersonName), but how can I get the animation to work on the property change in the if-statement?
if ((!(self.newPersonName.count == 0 && i == self.participants.count + 1)).animation()) doesn't work.
Your example code won't compile for me, but here's a trivial example of using Combine inside a ViewModel to control whether a second TextField appears based on a condition:
import SwiftUI
import Combine
class ViewModel : ObservableObject {
#Published var textFieldValue1 = ""
#Published var textFieldValue2 = "Second field"
#Published var showTextField2 = false
private var cancellable : AnyCancellable?
init() {
cancellable = $textFieldValue1.sink { value in
withAnimation {
self.showTextField2 = !value.isEmpty
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
TextField("", text: $viewModel.textFieldValue1)
.textFieldStyle(RoundedBorderTextFieldStyle())
if viewModel.showTextField2 {
TextField("", text: $viewModel.textFieldValue2)
.textFieldStyle(RoundedBorderTextFieldStyle())
.transition(.scale)
}
}
}
}
Is that approaching what you're attempting to build ?
struct ContentView: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
#State var editingNewPlayer = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(participants, id: \.self) { participant in
Text(participant)
.padding(.trailing)
Divider()
}
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName, onEditingChanged: { edit in
editingNewPlayer = edit
}, onCommit: handleNewPlayerEntry)
})
if editingNewPlayer {
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName) { edit in
editingNewPlayer = false
}
})
}
}
.padding(.leading)
.frame(maxHeight: .infinity, alignment: .top)
.transition(.opacity)
.animation(.easeIn)
}
func handleNewPlayerEntry() {
if newPersonName.count > 0 {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
editingNewPlayer = false
}
}
}
}