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)
}
}
}
}
}
Related
I have a basic tabview with 2 tabs. Tab 2 has a button to a modal sheet with a Page Style tabview imbedded in a Navigation Stack and added toolbar. When adding the Navigation Stack I get an unwanted white space at the bottom of the sheet view pages. I have tried using .ignoreSafeArea(edges: .bottom) in many places no solution and I'm stumped on this one. What am I missing here? How do I get rid of this unwanted white space? Is my navigation stack in the wrong place? Seems like such a simple design to be such a problem.
iOS 16.1
Xcode 14.2
struct HomeView: View {
#State private var pageIndex = 0
var body: some View {
TabView(selection: $pageIndex) {
PageOne()
.tabItem {
Label("Page 1", systemImage: "star")
}.tag(0)
PageTwo()
.tabItem {
Label("Page 2", systemImage: "bookmark")
}.tag(1)
}
}
}
struct PageOne: View {
var body: some View {
VStack {
Text("Page 1")
}
}
}
struct PageTwo: View {
#State private var sheetIsShowing = false
var body: some View {
VStack {
Text("Page 2")
Button("Show Sheet") {
self.sheetIsShowing.toggle()
}
}
.fullScreenCover(isPresented: $sheetIsShowing) {
SheetView(sheetIsShowing: $sheetIsShowing)
}
}
}
struct SheetView: View {
#Binding var sheetIsShowing: Bool
var body: some View {
NavigationStack {
TabView {
SheetTabView1()
SheetTabView2()
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
self.sheetIsShowing.toggle()
} label: {
Text("Cancel")
}
}
}
}
.ignoresSafeArea(edges: .bottom)
}
}
struct SheetTabView1: View {
#State private var text1 = ""
#State private var text2 = ""
var body: some View {
List {
TextField("Enter Text", text: $text1)
TextField("Enter Text", text: $text2)
}
}
}
struct SheetTabView2: View {
#State private var text1 = ""
#State private var text2 = ""
var body: some View {
List {
TextField("Enter Text", text: $text1)
TextField("Enter Text", text: $text2)
}
}
}
The problem here is the tabViewStyle view modifier. If you get rid of it, you'll get rid of the white space.
SwiftUI has two different forms of text fields, one is SecureField which hides input and TextField which doesn't hide input. Instead of creating two separate views, is there a way to create a single view that takes in a parameter to create both types while repeating as little code as possible?
You just make a View with all the code you want for the SecureTextField and the TextField then all you have to do is call the HybridTextField where ever you need it.
import SwiftUI
struct HybridTextFieldUsageView: View {
#State var password: String = "password"
var body: some View {
//Use this anywhere in your code
HybridTextField(text: $password, titleKey: "password")
}
}
///Contains all the code for the Secure and regular TextFields
struct HybridTextField: View {
#Binding var text: String
#State var isSecure: Bool = true
var titleKey: String
var body: some View {
HStack{
Group{
if isSecure{
SecureField(titleKey, text: $text)
}else{
TextField(titleKey, text: $text)
}
}.textFieldStyle(.roundedBorder)
.animation(.easeInOut(duration: 0.2), value: isSecure)
//Add any common modifiers here so they dont have to be repeated for each Field
Button(action: {
isSecure.toggle()
}, label: {
Image(systemName: !isSecure ? "eye.slash" : "eye" )
})
}//Add any modifiers shared by the Button and the Fields here
}
}
struct HybridTextField_Previews: PreviewProvider {
static var previews: some View {
HybridTextFieldUsageView()
}
}
I create a custom view for PasswordTextField. May be this code will help. I don't know either it helps you, though it fulfilled my requirement. That's why sharing it to you. This is the output of my code
struct PasswordTextField: View {
#Binding var isPasswordVisible: Bool
var hint: String
#Binding var text: String
var isTextChanged: (Bool) -> Void
var body: some View {
HStack {
if isPasswordVisible {
TextFieldView(
hint: hint,
text: $text,
isTextChanged: isTextChanged
)
} else {
SecuredTextFieldView(
hint: hint,
text: $text
)
}
}.overlay(alignment: .trailing) {
Image(systemName: isPasswordVisible ? "eye.fill" : "eye.slash.fill")
.padding()
.onTapGesture {
isPasswordVisible.toggle()
}
}
}
}
struct TextFieldView: View {
var hint: String
#Binding var text: String
var isTextChanged: (Bool) -> Void
var body: some View {
TextField(
hint,
text: $text,
onEditingChanged: isTextChanged
)
.padding()
.overlay(
Rectangle().strokeBorder(
.gray.opacity(0.2),
style: StrokeStyle(lineWidth: 2.0)
)
)
}
}
struct SecuredTextFieldView: View {
var hint: String
#Binding var text: String
var body: some View {
SecureField(
hint,
text: $text
)
.padding()
.overlay(
Rectangle().strokeBorder(
.gray.opacity(0.2),
style: StrokeStyle(lineWidth: 2.0)
)
)
}
}
and call the custom view in your actual view
struct PasswordView: View {
#State var password: String = ""
#State var confirmPassword: String = ""
#State var isPasswordVisible: Bool = false
#State var isConfirmPasswordVisible: Bool = false
var body: some View {
VStack(alignment: .leading, spacing: 15) {
Text("New Password")
.font(.headline)
.fontWeight(.regular)
.padding(.top, 30)
PasswordTextField(
isPasswordVisible: $isPasswordVisible,
hint: "Password having 8 charecture",
text: $password,
isTextChanged: { (changed) in
}
)
Text("Confirm New Password")
.font(.headline)
.fontWeight(.regular)
.padding(.top, 10)
PasswordTextField(
isPasswordVisible: $isConfirmPasswordVisible,
hint: "Password having 8 charecture",
text: $confirmPassword,
isTextChanged: { (changed) in
}
)
Spacer()
}.padding(.horizontal, 25)
}
}
In your view's body you can use a ternary to create the right textfield as needed without using a giant if/else block:
(self.isSecure ? AnyView(SecureField(placeholder, text: $value)) : AnyView(TextField(placeholder, text: $value)))
This will return a view that you can use operators on, which is useful if you're creating a custom text input. For example, the following would be painful if we had to do it twice for each kind of text field. Using a ternary in the actual view body keeps you from having two giant if/else blocks.
VStack {
ZStack(alignment: .leading) {
Text(placeholder)
.foregroundColor(Color(.placeholderText))
.offset(y: $value.wrappedValue.isEmpty ? 0 : -25)
.scaleEffect($value.wrappedValue.isEmpty ? 1 : 0.8, anchor: .leading)
(self.isSecure ? AnyView(SecureField(placeholder, text: $value)) : AnyView(TextField(placeholder, text: $value)))
.onChange(of: self.value) { newValue in
if self.onChange(newValue) != true {
self.value = previousValue
}
DispatchQueue.main.async {
self.previousValue = newValue
}
}
}
.padding(.top, 15)
.animation(.easeInOut(duration: 0.2))
Divider()
.frame(height: 1)
.padding(.horizontal, 30)
.background(Color.black)
}
I want when I finish selecting the language and click the Save button it will return the ContentView page and display the language I have selected. And when I click again, it has to checkmark the language I selected before.
I have successfully displayed the data, but I don't know how to save it when I click the Save button
Here is all my code currently
ContentView
struct ContentView: View {
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView() ) {
Text("Language")
Spacer()
Text("I want to show the language here ")
}
}
}
}
}
LanguageView
struct LanguageView: View {
var body: some View {
VStack {
CustomLanguageView()
Button(action: {
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#State var selectedLanguage: String? = nil
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
There are multiple ways to "Save" something but if you are just trying to get it back to the other view you could do something like this that I quickly setup.
struct ContentView: View {
#State var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView(language: $language)) {
Text("Language")
.padding()
Spacer()
Text(language!)
.padding()
}
}
}
}
}
struct LanguageView: View {
#Binding var language: String?
#State var selectedLanguage: String? = ""
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $selectedLanguage)
Button(action: {
language = selectedLanguage
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
Or if you are actually trying to save it to the device for later use you could use
UserDefaults.standard.setValue(selectedLanguage, forKey: "language")
Then to Retrieve it later do
UserDefaults.standard.value(forKey: "language") as! String
I'm trying to use iOS15's .focused() modifier to enable the user to tap anywhere outside of a text field to remove focus. I am going off of the example provided in: https://www.youtube.com/watch?v=GqXVFXnLVH4. Below is my non-working attempt:
enum Field {
case textField
case notTextField
}
struct ContentView: View {
#State var textInput = ""
#FocusState var focusState:Field?
var body: some View {
VStack {
TextField("Enter some text...", text: $textInput)
.focused($focusState, equals: .textField)
Rectangle()
.focused($focusState, equals: .notTextField)
.onTapGesture {
state = .notTextField
}
}
}
}
I found the solution was not to focus another element, but to switch the textfield's focus to false:
#State var textInput = ""
#FocusState var textFieldIsFocused: Bool
var body: some View {
VStack {
TextField("Enter some text...", text: $textInput)
.focused($textFieldIsFocused)
Rectangle()
.onTapGesture {
textFieldIsFocused = false
}
}
}
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 = ""
}
})
}
}