I'm trying to store a user's text field entry as a number so it's easier to work with in formulas. I thought I was being slick by storing it as a string, but now using the input in mathematical formulas is becoming a real pain in the neck. It should be noted that these field entries are being stored in CoreData currently as a string entity.
Here's an MRE of one of my fields:
import SwiftUI
struct EntryMRE: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var showingResults: Int? = 1
#FocusState private var isTextFieldFocused: Bool
#State var isDone = false
#State var isSaving = false //used to periodically save data
#State var saveInterval: Int = 5 //after how many seconds the data is automatically saved
//DataPoints Chemistry
#State var potassium = ""
var body: some View {
List {
Section(header: Text("🧪 Chemistry")) {
Group {
HStack {
Text("K")
+ Text("+")
.font(.system(size: 15.0))
.baselineOffset(4.0)
Spacer()
TextField("mEq/L", text: $potassium)
.focused($isTextFieldFocused)
.foregroundColor(Color(UIColor.systemBlue))
.modifier(TextFieldClearButton(text: $potassium))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
if let numberValue = Double(potassium) { // Cast String to Double
if (3.5...5.5) ~= numberValue {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(UIColor.systemGreen))
}
else {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(Color(UIColor.systemRed))
}
}
}
}
}
}
}
}
While not specifically using the $potassium value, here is what I'm currently having to do for formulas:
import SwiftUI
struct ResultView: View {
#Binding var isDone: Bool
var EV: EntryView
let decimalPlaces: Int = 2
var body: some View {
List {
Section(header: Text("Formula")) {
HStack {
Text("Corrected CO2")
Spacer()
Text("\(1.5 * (Double(EV.hCo3) ?? 0) + 8, specifier: "%.\(decimalPlaces)f")")
}
If you want your #State var potassium to be of type Double you can use a different initializer for your TextField.
#State var potassium = 0.0
TextField("mEq/L", value: $potassium, format: .number)
Of course you would need to change your CoreData model to to be of type Double but I think this should have been a Double in the first place.
Related
This file is for the main structure of the application. This is where the error is coming from which is "Missing argument for parameter 'numberOfDoors' in call". This is because it wants me to add
ContentView(numberOfDoors: <#Int#>)
but im having trouble finding out how I can get what the user chooses to be the int instead of me putting a number in there statically.
import SwiftUI
#main
struct NumOfDoorsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
This is my project file.
import SwiftUI
struct ContentView: View {
#State var numberOfDoors: Int
#State var multiOptions: Array<String>
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.multiOptions = [String](repeating: "", count: numberOfDoors)
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
One of the most important parts of SwiftUI programming is creating an appropriate model for your views.
#State properties are OK for local, often independent properties, but they typically aren't a good solution for your main data model or where a model needs to be manipulated by the user.
In your case you want the size of the array to change based on the selected number of doors, so you need somewhere for that procedural code to live; The model is that place.
Here is a simple model object you can use
class DoorModel: ObservableObject {
#Published var numberOfDoors: Int {
didSet {
self.adjustArray(newSize: numberOfDoors)
}
}
#Published var doors:[String]
init(numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
self.doors = [String](repeating: "", count: numberOfDoors)
}
private func adjustArray(newSize: Int) {
let delta = newSize - doors.count
print("new size = \(newSize) Delta = \(delta)")
if delta > 0 {
doors.append(contentsOf:[String](repeating: "", count: delta))
} else if delta < 0 {
doors.removeLast(-delta)
}
}
}
Note that you still need to supply a starting number of doors via the initialiser for your model. Whenever that value changes, the didSet property observer calls a function to add or remove elements from the end of the array.
You can use this model in your view with an #StateObject decorator. This ensures that a single instance is created and reused as your view is redrawn
struct ContentView: View {
#StateObject var model = DoorModel(numberOfDoors: 1)
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $model.numberOfDoors) {
ForEach(1 ..< 64) { index in
Text("\(index) doors").tag(index)
}
}
ForEach($model.doors.indices, id: \.self) { index in
TextField("Enter your option...", text: $model.doors[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
}
}
}
I added the .tag modifier to ensure that the picker works correctly with your 1-based list; By default the tag will be 0 based.
There are, to my opinion, two issues with your code.
Don't initialize ContentView with a parameter. Especially, since you want to start with a value of 0 doors and let the user choose how many doors.
You can't initialize an #State value directly like: self.numberOfDoors = numberOfDoors.
I suggest the following code, it worked for me.
import SwiftUI
struct ContentView: View {
#State private var numberOfDoors: Int
#State private var multiOptions: Array<String>
init() {
_numberOfDoors = State(wrappedValue: 0)
_multiOptions = State(wrappedValue: [String](repeating: "", count: 1))
}
var body: some View {
NavigationView {
Form{
Section {
Picker("Number of doors", selection: $numberOfDoors) {
ForEach(1 ..< 64) {
Text("\($0) doors")
}
}
ForEach(multiOptions.indices, id: \.self) { index in
TextField("Enter your option...", text: $multiOptions[index])
.padding()
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Section {
Text("\(numberOfDoors + 1)")
}
}
}
}
}
The issue is, that when you initialize ContentView, the #State or #StateObject variables don't yet exist. You first need to initialize the View, before you can assign an #State variable.
An #State variable is wrapped in a property wrapper, where the original variable at initialization is _variable. Therefore, you need to initialize an #State variable like this:
_myVariable = State(wrappedValue: myContent)
If you don't so it is like this, you get the error as you mentioned.
In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.
I have a ContentView and a NumericView. The selectedIndex is a #State in ContentView. What I want is that if selectedIndex changes, it should reset the answer field in NumericView from CoreData. Below is the code. "av" contains the value of the answer field from CoreData.
struct NumericEntry: View {
#EnvironmentObject var questionAnswerStore: QuestionAnswerStore
#State var answer: String
var questionIndex: Int
init(questionIndex: Int, av: String) {
self.questionIndex = questionIndex
_answer = State(initialValue: av)
}
var body: some View {
VStack {
TextField("Answer", text: $answer)
.textFieldStyle(CustomTextFieldStyle())
.onChange(of: answer) { newValue in
self.questionAnswerStore.getUserAttemptData(selectedIndex: questionIndex).answer = newValue
PersistenceController.shared.saveContext()
}
.keyboardType(.decimalPad)
.padding()
}
}
private struct CustomTextFieldStyle : TextFieldStyle {
public func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding(10)
.background(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color.secondary.opacity(0.5), lineWidth: 1))
}
}
}
struct ContentView: View {
#State var selectedIndex: Int = 0
#ObservedObject private var questionAnswerStore: QuestionAnswerStore = QuestionAnswerStore.sharedInstance
var body: some View {
NumericEntry(questionIndex: selectedIndex, av: self.questionAnswerStore.getUserAttemptData(selectedIndex: selectedIndex).answer ?? "")
// some code that keeps changing the value of selectedIndex
}
}
I read somewhere that _stateVariable = State(initialValue: "abcd") should set the state of #State stateVariable. In the above code the code
_answer = State(initialValue: av)
executes fine but when it reaches
TextField("Answer", text: $answer)
$answer is still "".
I would prefer a solution where I don't even have to send "av" from the parent component, just selectedIndex should check QuestionAnswerStore for the value of "answer". This could be solved using .onAppear but in my case, the NumericView appears only once and then its parent just keeps changing selectedIndex value, so onAppear doesn't get called again.
Of course, if that is not possible then what's the way out using "av" as above?
I am trying to increment or decrement a value using Stepper, however I cannot fathom how to take the new value and update my model.
I've also tried to use the onIncrement, onDecrement version, but here there appears to be no option to disable the + or - when the range has been reached.
Here's my example code:
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
struct MainView: View {
#ObservedObject var updateAge = UpdateAge()
#State private var showAgeEditor = false
#State var age: Int = 0
var body: some View {
VStack {
/// Show the current age.
Text("Age \(age)")
Image(systemName: "keyboard")
.onTapGesture {
self.showAgeEditor = true
}
/// Present the sheet to update the age.
.sheet(isPresented: $showAgeEditor) {
SheetView(showAgeEditor: self.$showAgeEditor)
.environmentObject(self.updateAge)
.frame(minWidth: 300, minHeight: 400)
}
}
/// Calls the viewModel (UpdateAge) to fetch the age from the model.
.onAppear(perform: {self.age = self.updateAge.withAge})
}
}
struct SheetView: View {
#EnvironmentObject var updateAge: UpdateAge
#Binding var showAgeEditor: Bool
/// Steppers state.
#State private var age: Int = 18
let maxAge = 21
let minAge = 18
var body: some View {
return VStack {
Text("Age of person = \(age)")
/// When I increment or Decrement the stepper I want the Age to increase or decrease.
Stepper<Text>(value: $age, in: minAge...maxAge, step: 1) {
/// And when it does, to store the value in the model via the viewModel.
/// However I appear to have created an infinite loop. Just uncomment the line below.
// self.updateAge.with(age: age)
/// Then display the value in the label.
return Text("\(age)")
}
}.onAppear(perform: {self.age = self.updateAge.withAge})
}
}
class UpdateAge: ObservableObject {
#Published var model = Model()
func with(age value: Int) {
print("Value passed \(value)")
self.model.setDrinkingAge(with: value)
}
var withAge: Int {
get { model.drinkingAge }
}
}
struct Model {
var drinkingAge: Int = 18
mutating func setDrinkingAge(with value: Int) {
drinkingAge = value
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can bind to model directly via view model projected value, like below (so local state age is redundant)
Stepper(value: $updateAge.model.drinkingAge, in: minAge...maxAge, step: 1) {
Text("\(updateAge.model.drinkingAge)")
}
I am pulling my hair out trying to figure out how to get my picker to show the already stored CoreData value. I want it to show on the right side of the picker as if the user just selected it. I have tried adding self.updatedItemAttribute = self.editItem.attribute ?? "" prior to the picker to set the initial value but that does not build. I also tried defining it in #State (e.g. #State var updatedItemAttribute: String = self.editItem.attribute) but that does not build either. If I add a TextField prior to the picker it will set the value, but I do not want to have a TextField with the value just to get it to set. Any ideas on how I get updatedItemAttribute set to editItem.attribute just prior to the picker? Thanks.
import CoreData
import SwiftUI
struct EditItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#ObservedObject var editItem: Item
#State var updatedItemName: String = ""
#State var updatedItemAttribute: String = ""
let attributes = ["Red", "Purple", "Yellow", "Gold"]
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $updatedItemName)
.onAppear {
self.updatedItemName = self.editItem.name ?? ""
}
Picker("Test attribute", selection: self.$updatedItemAttribute) {
ForEach(attributes, id: \.self) {
Text($0)
.onAppear {
self.updatedItemAttribute = self.editItem.attribute ?? ""
}
}
}
}
...
You have to this in init as shown below
struct EditItemView: View {
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
#ObservedObject var editItem: Item
#State private var updatedItemName: String
#State private var updatedItemAttribute: String
init(editItem item: Item) { // << updated here
self.editItem = item
self._updatedItemName = State<String>(initialValue: item.name ?? "")
self._updatedItemAttribute = State<String>(initialValue: item.attribute ?? "")
}
let attributes = ["Red", "Purple", "Yellow", "Gold"]
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of item", text: $updatedItemName)
Picker("Test attribute", selection: self.$updatedItemAttribute) {
ForEach(attributes, id: \.self) {
Text($0)
}
}
}
}
}
}
}