How to remove special characters from the `TextField` in SwiftUI - swift

I need to apply some validations in my form like remove special character, accept only number, alphabets, alphanumeric, and only specific length of a string.
I have many text fields, in many places in my app. So that I'm creating extensions to Binding, and trying to apply conditions when editing.
These filters/conditions using for #State, #Published, and #Binding variables.
Here is my code:
struct ContentView: View {
#State var name = ""
var body: some View {
InputField(text: $name)
}
}
struct InputField: View {
#Binding var text: String
var body: some View {
VStack {
TextField("Name here...", text: $text.limit(6).alphaNumeric)
.frame(maxWidth: .infinity, minHeight: 48, alignment: .leading)
}.padding()
}
}
extension Binding where Value == String {
var alphaNumeric: Binding<String> {
Binding<String>(get: { self.wrappedValue },
set: {
self.wrappedValue = $0.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)})
}
func limit(_ length: Int) -> Binding<String> {
Binding<String>(get: { self.wrappedValue },
set: {
print($0.prefix(length))
self.wrappedValue = String($0.prefix(length))
})
}
}
Here in the above code $text.limit(6).alphaNumeric, I'm trying to limit the length to 6 characters and only allow the alphaNumeric string.

How about a custom TextField with checks, instead of messing with Binding Wrapper itself:
(with OPs full code to proof that binding works)
struct ContentView: View {
#State var name = ""
#State var number = ""
var body: some View {
InputField(text: $name, number: $number)
}
}
struct InputField: View {
#Binding var text: String
#Binding var number: String
var body: some View {
VStack {
TextFieldWithCheck("Name here...", text: $text, limit: 15, allowed: .alphanumerics)
.frame(maxWidth: .infinity, minHeight: 48, alignment: .leading)
TextFieldWithCheck("Phone no here...", text: $number, limit: 9, allowed: .decimalDigits)
.frame(maxWidth: .infinity, minHeight: 48, alignment: .leading)
}.padding()
}
}
// here is the custom TextField itself
struct TextFieldWithCheck: View {
let label: LocalizedStringKey
#Binding var text: String
let limit: Int
let allowed: CharacterSet
init(_ label: LocalizedStringKey, text: Binding<String>, limit: Int = Int.max, allowed: CharacterSet = .alphanumerics) {
self.label = label
self._text = Binding(projectedValue: text)
self.limit = limit
self.allowed = allowed
}
var body: some View {
TextField(label, text: $text)
.onChange(of: text) { _ in
// all credits to Leo Dabus:
text = String(text.prefix(limit).unicodeScalars.filter(allowed.contains))
}
}
}

You can use .onChange(of: ). Should also work for #Binding etc.
#State private var inputText = ""
let maxCount = 20
var body: some View {
TextField("Input", text: $inputText)
.padding()
.background(.gray.opacity(0.1))
.padding()
.onChange(of: inputText) { _ in
// check character set
var checked = inputText.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
// check length
checked = String(checked.prefix(maxCount)) // regards to Leo Dabus
inputText = checked
}
}

Related

Dynamic list from TextField's data SwiftUI

I just started to learn Swift programing language and have a question.
I'm trying to create a simple one-page application where you can add movies to a favorite list. Movies must have 2 properties: title (string, mandatory) and year (integer, mandatory). But I have a problem, I don't know how to put it in one row.
And also, how to ignore duplicate movies?
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
var title: String
}
class Model: ObservableObject {
#Published var movies: [Movie] = []
}
struct DynamicList: View {
#StateObject var model = Model()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
List {
ForEach(model.movies) { movie in
MovieRow(title: movie.title)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let newMovie = Movie(title: text)
model.movies.append(newMovie)
text = ""
let newYear = Movie(title: year)
model.movies.append(newYear)
year = ""
}
}
struct MovieRow: View {
let title: String
var body: some View {
Label (
title: { Text(title)},
icon: { Image(systemName: "film") }
)
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}
Here is the solution. It will show the data in one row and also how to ignore duplicate movies to show into the list. Check the below code:
import SwiftUI
struct Movie: Identifiable {
var id = UUID()
let title: String
let year: String
}
class MoviesViewModel: ObservableObject {
#Published var movies: [Movie] = []
}
struct ContentView: View {
#State var boolValue = false
#StateObject var viewModel = MoviesViewModel()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
// Show the data in list form
List {
ForEach(viewModel.movies) { movie in
MovieRow(title: movie.title, year: movie.year)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
// Condition to check whether the data is already exit or not
boolValue = false
let newMovie = Movie(title: text, year: year)
for movie in viewModel.movies{
if ((movie.title.contains(text)) && (movie.year.contains(year))){
boolValue = true
}
}
// check if boolValue is false so the data will store into the array.
if boolValue == false{
viewModel.movies.append(newMovie)
text = ""
year = ""
}
}
}
struct MovieRow: View {
let title: String
let year: String
var body: some View {
// Show the data insert into the textfield
HStack{
Label (
title: { Text(title)},
icon: { Image(systemName: "film") }
)
Spacer()
Label (
title: { Text(year)},
icon: { Image(systemName: "calendar") }
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe someone will need a similar solution, here is my result:
import SwiftUI
struct Movie: Identifiable {
let id = UUID()
var title: String
var year: String
}
class Model: ObservableObject {
#Published var movies: [Movie] = []
}
struct DynamicList: View {
#StateObject var model = Model()
#State var text = ""
#State var year = ""
var body: some View {
VStack {
Section() {
TextField("Title", text: $text)
.padding()
.border(.gray)
TextField("Year", text: $year)
.padding()
.border(.gray)
.keyboardType(.numberPad)
Button(action: {
self.addToList()
}, label: {
Text("Add")
.frame(width: 80, height: 40, alignment: .center)
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
})
.padding()
}
List {
ForEach(model.movies) { movie in
MovieRow(title: movie.title, year: movie.year)
}
}
}
.padding()
}
func addToList() {
guard !text.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
guard !year.trimmingCharacters(in: .whitespaces).isEmpty else {
return
}
let newMovie = Movie(title: text, year: year)
model.movies.append(newMovie)
text = ""
year = ""
}
}
struct MovieRow: View {
let title: String
let year: String
var body: some View {
Label (
title: { Text(title + " " + year)},
icon: { Image(systemName: "film") }
)
}
}
struct DynamicList_Previews: PreviewProvider {
static var previews: some View {
DynamicList()
}
}

SWIFTUI/ How to delay .task and let it work only if the data from textFields are passed

I just tried to make an API app in SwiftUI with loveCalculator from rapidapi.com
The problem is that API first needs names from me before it gives me the results.
My program works but fetching data form API earlier that I want (when I click to show my data in next view, first show default data, then show the data that should be displayed when I click).
Also Is it possible to initialize #Published var loveData (in LoveViewModel) without passing any default data or empty String?
Something like make the data from LoveData optional ?
Can You tell me when I make mistake?
MY CODE IS :
LoveData (for api)
struct LoveData: Codable {
let percentage: String
let result: String
}
LoveViewModel
class LoveViewModel: ObservableObject {
#Published var loveData = LoveData(percentage: "50", result: "aaa")
let baseURL = "https://love-calculator.p.rapidapi.com/getPercentage?"
let myApi = "c6c134a7f0msh980729b528fe273p1f337fjsnd17137cb2f24"
func loveCal (first: String, second: String) async {
let completedurl = "\(baseURL)&rapidapi-key=\(myApi)&sname=\(first)&fname=\(second)"
guard let url = URL(string: completedurl) else {return }
do {
let decoder = JSONDecoder()
let (data, _) = try await URLSession.shared.data(from: url)
if let safeData = try? decoder.decode(LoveData.self, from: data) {
print("succesfully saved data")
self.loveData = safeData
}
} catch {
print(error)
}
}
}
LoveCalculator View
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onEditingChanged: { isBegin in
if isBegin == false {
print("finish get names")
}
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage, description: loveViewModel.loveData.result)
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
.task {
await loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
LoveResults View
struct LoveResults: View {
var percentage: String
var description: String
var body: some View {
ZStack{
Color(.green).ignoresSafeArea()
VStack{
Text("RESULTS :")
.padding()
Text(percentage)
.font(.system(size: 80, weight: .heavy))
.foregroundColor(.red)
.padding()
Text(description)
}
}
}
}
Thanks for help!
Regards,
Michal
.task is the same modifier as .onAppear in terms of view lifecycle, meaning it will fire immediately when the view appears. You have to move your API call to a more controlled place, where you can call it once you have all the required data.
If you want to fire the API request only after both names are entered, you can create a computed variable, that checks for desired state of TextFields and when that variable turns to true, then you call the API.
Like in this example:
struct SomeView: View {
#State var firstName: String = ""
#State var secondName: String = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty // << validation logic here
}
var body: some View {
Form {
TextField("Enter first name", text: $firstName)
TextField("Enter second name", text: $secondName)
}
.onChange(of: namesAreValid) { isValid in
if isValid {
// Call API
}
}
}
}
You can also set your loveData to optional using #Published var loveData: LoveData? and disable/hide the navigation link, until your data is not nil. In that case, you might need to provide default values with ?? to handle optional string errors in LoveResults view initializer
It was very helpful but still not enough for me, .onChange work even if I was type 1 letter in my Form.
I find out how to use Task { } and .onCommit on my TextField, now everything working well !
My code now looks like that :
LoveCalculator View :
struct LoveCalculator: View {
#State var firstName = ""
#State var secondName = ""
var namesAreValid: Bool {
!firstName.isEmpty && !secondName.isEmpty
}
func makeApiCall() {
if namesAreValid {
DispatchQueue.main.async {
Task {
await self.loveViewModel.loveCal(first: firstName, second: secondName)
}
}
}
}
#ObservedObject var loveViewModel = LoveViewModel()
var body: some View {
NavigationView {
ZStack{
Color(.systemTeal).ignoresSafeArea()
VStack(alignment: .center) {
Text("Love Calculator")
.padding(.top, 100)
Text("Enter first name:")
.padding()
TextField("first name", text: $firstName, onCommit: {makeApiCall()})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
Text("Enter second name:")
.padding()
TextField("second name", text: $secondName, onCommit: {
makeApiCall()
})
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
NavigationLink {
LoveResults(percentage: loveViewModel.loveData.percentage ?? "", description: loveViewModel.loveData.result ?? "")
} label: {
Text("CHECK IT!")
}
Spacer()
}
}
}
}
}
Thank You one more time for very helpful tip!
Peace!
Michał ;)
Editing :
Just look at Apple documentation and I see that they say that .onCommit is deprecated whatever it means.
So instead of this I use .onSubmit and works the same !
TextField("second name", text: $secondName)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
.padding(.bottom)
.onSubmit {
makeApiCall()
}
Peace! :)

How do I programmatically set secure text field and normal text field in swiftUI

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)
}

Remove padding on TextEditor

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)
}
}
}
}
}

Bind to a struct property in a SwiftUI Mac app

I'm building a macOS unit converter app in SwiftUI. In the example below, I have an acceleration view that has many text fields. These text fields represent different units of acceleration.
// AccelerationView.swift
struct AccelerationView: View {
#State private var accel = Acceleration()
#EnvironmentObject var formatter: Formatter
var body: some View {
VStack(alignment: .leading) {
Text("Metric").font(.subheadline)
HStack {
TextField("kilometer per second squared", value: $accel.kilometerPerSecondSquare, formatter: formatter.numberFormatter).frame(width: 120)
Text("km/s²").frame(width: 60, alignment: .leading)
}
HStack {
TextField("meter per second squared", value: $accel.meterPerSecondSquare, formatter: formatter.numberFormatter).frame(width: 120)
Text("m/s²").frame(width: 60, alignment: .leading)
}
HStack {
TextField("millimeter per second squared", value: $accel.millimeterPerSecondSquare, formatter: formatter.numberFormatter).frame(width: 120)
Text("mm/s²").frame(width: 60, alignment: .leading)
}
// and so on ...
}
.padding()
}
}
I would like to place the HStack into its own file to clean up the code in the view. My attempt at this is shown below:
// UnitTextField.swift
struct UnitTextField: View {
#State var value: Double = 0.0
let placeHolder: String
let label: String
var fieldWidth: CGFloat = 120
var labelWidth: CGFloat = 60
#EnvironmentObject private var formatter: Formatter
var body: some View {
HStack {
TextField(placeHolder, value: $value, formatter: formatter.numberFormatter)
.frame(width: fieldWidth)
.multilineTextAlignment(.trailing)
Text(label)
.frame(width: labelWidth, alignment: .leading)
}
}
}
This does not work because the UnitTextField value does not properly bind to the acceleration struct. I'm trying to accomplish something like this that I can use in the AccelerationView:
// AccelerationView.swift
struct AccelerationView: View {
#State private var accel = Acceleration()
#EnvironmentObject var formatter: Formatter
var body: some View {
VStack(alignment: .leading) {
Text("Metric").font(.subheadline)
UnitTextField(value: $accel.kilometerPerSecondSquare, placeHolder: "kilometer per second squared", label: "km/s²")
UnitTextField(value: $accel.meterPerSecondSquare, placeHolder: "meter per second squared", label: "m/s²")
UnitTextField(value: $accel.millimeterPerSecondSquare, placeHolder: "millimeter per second squared", label: "mm/s²")
// and so on ...
}
.padding()
}
}
Any suggestions on how to properly implement this in SwiftUI for a macOS application?
If to go from what you're going to accomplish (last snapshot), then I assume you need
struct UnitTextField: View {
#Binding var value: Double
instead of
struct UnitTextField: View {
#State var value: Double = 0.0
It could be like this with binding and environmentObject :
class MyFormat: Formatter, ObservableObject{
var numberFormat = NumberFormatter()
}
struct UnitTextField: View {
#Binding var value: Double
let placeHolder: String
let label: String
var fieldWidth: CGFloat = 120
var labelWidth: CGFloat = 60
#EnvironmentObject var formatter: MyFormat
var body: some View {
HStack {
TextField(placeHolder, value: $value, formatter: formatter.numberFormat)
.frame(width: fieldWidth)
.multilineTextAlignment(.trailing)
Text(label)
.frame(width: labelWidth, alignment: .leading)
}
}
}
struct Acceleration{
var kilometerPerSecondSquare : Double = 0.0
var meterPerSecondSquare : Double = 1.0
var millimeterPerSecondSquare : Double = 2.0
}
struct AccelerationView: View {
#State private var accel = Acceleration()
#EnvironmentObject var formatter: MyFormat
var body: some View {
VStack(alignment: .leading) {
Text("Metric").font(.subheadline)
UnitTextField(value: $accel.kilometerPerSecondSquare, placeHolder: "kilometer per second squared", label: "km/s²").environmentObject(formatter)
UnitTextField(value: $accel.meterPerSecondSquare, placeHolder: "meter per second squared", label: "m/s²").environmentObject(formatter)
UnitTextField(value: $accel.millimeterPerSecondSquare, placeHolder: "millimeter per second squared", label: "mm/s²").environmentObject(formatter)
// and so on ...
}
.padding()
}
}