Working with Dynamic Form and field States in SwiftUI - ios

I'm kind of lost on how to work with states with Dynamic forms.
I surely cannot create states for each Field because I don't know how many fields are there as these Forms are basically built from a JSON Response.
Here is basically what I have right now which is what I'm looking to change.
Initially I created a state for each field back when the forms were not built dynamically and now I'm stuck on how to proceed.
I thought about using Dictionary but I ain't sure how good of a solution is that.
#State var textfieldText: String = ""
#State var SKU: String = ""
#State private var showScanner: Bool = false
var currentForm: FormModel
#State var RecordDate: Date = Date.now
#State var Formresponse: [String: Any] = [:]//This one is set to any because the value can vary from a string to [] to even a whole object
How I'm Rendering my form :
ForEach(currentForm.item, id:\.idf) { it in
if (!it.questionItem.hidden)
{
switch it.questionItem.questionType {
case .dateQuestion :
DateField(title: it.title, currentDate: $RecordDate)
case .choiceQuestion:
Text("choice question")
case .scannerQuestion:
ScannerField(title: it.title, SKU: $SKU, showScanner: $showScanner)
case .textQuestion:
TextQuestionField(title: it.title, email: currentForm.owner, text: $textfieldText)
}
}
}
I'll eventually have to submit this data in a form of dictionary which is why I thought about using a Dict ["fieldID":"FieldInput","fieldId2":"FieldInput2"..]

I think you only need one State and that is for the formResponse. You can pass that as a Binding to each input field view and within that view you can create a custom Binding to get and set the answer to the formResponse. Something like this:
struct FormFieldInputView: View {
#Binding var formResponse: [String: Any]
let field: String
var body: some View {
TextField(field, text: Binding(
get: {
formResponse[field] as? String ?? ""
},
set: { newValue in
formResponse[field] = newValue
})
)
}
}

define enum for the questions type:
enum FormItemType {
case dataQuestion
case choiceQuestion
case scannerQuestion
case textQuestion
}
define the item model for the questions type:
struct FormItemModel : Identifiable {
var type : FormItemType
var itemObject : Any
var userInput : String?
let id : UUID
}
define the form view model:
final class FormModel : ObservableObject {
#Published var items : [FormItemModel] = []
}
and the view :
struct ContentView: View {
#ObservedObject var formViewModel: FormModel
#Binding var currentInput : String
var body: some View {
List {
ForEach(formViewModel.items, id: \.id, content: { item in
switch item.type {
case .dataQuestion:
Text(item.itemObject as? String ?? "")
case .scannerQuestion:
Text("\(item.itemObject as? Int ?? 0 )")
case .choiceQuestion:
if let dic = item.itemObject as? [String:String]{
VStack{
Text(dic["Q"]!)
Text(dic["A1"]!)
Text(dic["A2"]!)
Text(dic["A3"]!)
}
}
case .textQuestion:
VStack{
Text(item.itemObject as? String ?? "")
TextEditor(text: $currentInput)
}
}
})//ForEach
}//List
}
}//View
and here's the dummy values for the form:
items = [FormItemModel(type: .textQuestion, itemObject: "Tell Me About Yourself...", id: UUID()),
FormItemModel(type: .choiceQuestion,
itemObject: ["Q":"How much is 1+1?", "A1":"1", "A2":"2", "A3":"3"],
id: UUID()),
FormItemModel(type: .scannerQuestion, itemObject: 1110111011, id: UUID())
]
and the result:

Related

Firebase is not saving the data - swiftui

I have a problem that Firebase not saving the data and it shows like that :
enter image description here
First I created a model :
struct Box: Identifiable, Hashable, Codable {
#DocumentID var id: String?
var boxName: String
var boxSize: String
enum CodingKeys: String, CodingKey {
case id
case boxName
case boxSize
}
var dictionary: [String: Any] {
let data = (try? JSONEncoder().encode(self)) ?? Data()
return (try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]) ?? [:]
}
}
Then I created ViewModel:
class BoxViewModel: ObservableObject {
#Published var box: Box
private var db = Firestore.firestore()
init(box: Box = CashBox(boxName: "", boxSize: "")) {
self.box = box
}
private func addNewBox(_ box: Box){
do {
print("the name \(box.boxName)")
let _ = db.collection("Box")
.addDocument(data: box.dictionary)
}
}
func addBox() {
self.addNewBox(box)
}
func setBoxData(boxName: String, boxSize: String){
self.box.boxName = boxName
self.box.boxSize = boxSize
}
}
Finally here is the view:
struct CashBoxView: View {
#State var boxName = ""
#State var boxSize = ""
#EnvironmentObject var boxViewModel: BoxViewModel
var body: some View {
Text("Enter box name")
TextField("", text: $boxName)
Text("Enter box size")
TextField("", text: $boxSize)
Button( action: {
boxViewModel.setBoxData(boxName: boxName, boxSize: boxSize)
boxViewModel.addBox()
}) {
Text("Done")
}
}
First I thought that the problem is the box is empty but when I tried to print the box name print("the name \(box.boxName)") and printed it
is the problem the the viewModel is #EnvironmentObject ? or what is the problem ?
Thank you,

Why my published var is not being updated by my view?

I am making a personal project to study SwiftUI. All was going well, the I noticed a bug on my app.
I have the simple view bellow, that saves a description, a value and some tags on my ViewModel. I am having an issue with the $viewModel.value. That variable is not being filled with values from the view.
I supose that my #Published var value: Double? from my ViewModel should be updated whenever the user types some value. Thing is, it is not updating on any iPhone 11 and up, but it works perfectly on the iPhone 8.
public struct AddBillView: View {
#ObservedObject private var viewModel: AddBillViewModel
#Environment(\.presentationMode) var presentationMode
public let onExpenseCreated: ((_ expense: Expense)->Void)
public var body: some View {
Text("Add Expense")
VStack {
TextField("Descrição", text: $viewModel.name)
HStack {
Text("Valor \(NumberFormatter.currency.currencySymbol)")
CurrencyTextField("Value", value: $viewModel.value)
.multilineTextAlignment(TextAlignment.leading)
}
HStack {
Text("Tags")
TextField("car pets home",
text: $viewModel.tags)
}
Picker("Type", selection: $viewModel.type) {
Text("Paid").tag("Paid")
Text("Unpaid").tag("Unpaid")
Text("Credit").tag("Credit")
}
}.navigationTitle("+ Expense")
Button("Adicionar") {
if !viewModel.hasExpense() {
return
}
onExpenseCreated(viewModel.expense())
self.presentationMode.wrappedValue.dismiss()
}
}
public init(viewModel outViewModel: AddBillViewModel,
onExpenseCreated: #escaping ((_ expense: Expense)->Void)) {
self.viewModel = outViewModel
self.onExpenseCreated = onExpenseCreated
}
}
And I have a ViewModel:
public class AddBillViewModel: ObservableObject {
#Published var name: String = ""
#Published var type: String = "Paid"
#Published var tags: String = ""
#Published var value: Double?
init(expense: Expense?=nil) {
self.name = expense?.name ?? ""
self.type = expense?.type.rawValue ?? "Paid"
self.tags = expense?.tags?.map { String($0.name) }.joined(separator: " ") ?? ""
self.value = expense?.value
}
func hasExpense() -> Bool {
if self.name.isEmpty ||
self.value == nil ||
self.value?.isZero == true {
return false
}
return true
}
func expense() -> Expense {
let tags = self.tags.split(separator: " ").map { Tag(name: String($0)) }
return Expense(name: self.name, value: self.value ?? 0.0 ,
type: ExpenseType(rawValue: self.type)!,
id: UUID().uuidString,
tags: tags)
}
}
Then I use my view:
AddBillView(viewModel: AddBillViewModel()) { expense in
viewModel.add(expense: expense)
viewModel.state = .idle
}
I already google it and spend a couple of hours looking for an answer, with no luck. Someone have any ideas?
Edited
Here is the code for the CurrencyTextField. I`m using this component:
https://github.com/youjinp/SwiftUIKit/blob/master/Sources/SwiftUIKit/views/CurrencyTextField.swift
But the component works perfectly fine on iPhone 8 simulator and with a #State property inside my view. It does not work only with my ViewModel
I figured it out! The problem was that my AddBillViewModel is an ObservableObject and I was marking each property with #Published. This was causing some kind of double observable object.
I removed the #Published and it started working again.

onReceive in SwiftUI with array of ObservableObjects/State/Binding causes infinite loop

I'm using .onReceive(_:perform:) to add pattern masks in my text field text. There is an infinite loop that happens when I use array of ObservableObjects, Binding, State objects or Published properties.
Example
struct MyTextField: View {
#Binding var text: String
...
}
extension MyTextField {
func mask(_ mask: String) -> some View {
self.onReceive(Just(text)) { newValue in
// `string(withMask:)` returns another string (already formatted correctly)
text = newValue.string(withMask: mask) // Obviously when I comment this line of code, the problem stops
}
}
}
Usage
final class FormItem: ObservableObject, Identifiable {
let id = UUID()
#Published var text = ""
let mask: String
init(mask: String) {
self.mask = mask
}
}
#State var volunteerForm: [FormItem] = [
FormItem(mask: "999.999.999-99")
]
var body: some View {
VStack {
ForEach(volunteerForm.indices, id: \.self) { index in
MyTextField("", text: volunteerForm[index].$text, onCommit: onCommit)
.mask(volunteerForm[index].mask)
}
}
}
But when I use a single property just like this #State var formItem: FormItem = ... this infinite loop doesn't happen. Also when I use an array of String instead of array of my Class FormItem, #State var volunteerTexts: [String] = [""], it doesn't happen too.
I wonder if this happen when we use a custom struct or class.
I've tried creating the model without ObservableObject and Published, just like a struct, but the infinite loop keeps happening:
struct FormItem: Identifiable {
let id = UUID()
var text = ""
let mask: String
}
VStack {
ForEach(volunteerForm.indices, id: \.self) { index in
TextField("", text: $volunteerForm[index].text, onCommit: onCommit)
.mask(volunteerForm[index].mask)
}
}
Do you have any ideia why is this infinite loop occurring?

Cannot resolve "Type of expression is ambiguous without more context" error. Can someone check my code?

I'm relatively new to SwiftUI and time to time getting errors and solving them by searching over the internet but this time I could not find any solution to my problem and decided to ask for some help over here, stack overflow. I hope the code below helps you to find my issue.
Both my struct are Identifiable and I actually used ShoppingList struct in the same view to make a List of it with the same technique and it works without an error. But when I try to use ForEach for a variable of ShoppingList struct (which is also a struct and conforms to Identifiable protocol) I get this error "Type of expression is ambiguous without more context"
This is the view that I get my error:
struct ListDetailView: View {
#EnvironmentObject var session: SessionStore
var item: ShoppingList
#State private var isAddNewViewActive: Bool = false
var body: some View {
List {
Section(header: Text("Products")) {
ForEach(self.item.products, id: \.id) { product in <<<--- ERROR LINE
Text(product.name)
}
}
Section(header: Text("")) {
Button(action: { self.isAddNewViewActive.toggle() } ) {
Text("Click to add new product")
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(self.item.name)
.sheet(isPresented: $isAddNewViewActive) {
AddNewItemView(session: self.session, item: self.item, isViewActive: self.$isAddNewViewActive)
}
}
}
These are the structs that are in the code
struct ShoppingList: Identifiable, Equatable {
var id: UUID
var name: String
var coverPhoto: String
var products: [Product]
init(id: UUID = UUID(), name: String, coverPhoto: String = "cart", products: [Product] = [Product]()) {
self.id = id
self.name = name
self.coverPhoto = coverPhoto
self.products = products
}
mutating func addProduct(product: Product) {
products.append(product)
print(products)
}
}
struct Product: Identifiable, Equatable {
var id: UUID
var name: String
var brand: String
var imageURL: String
var links: [Int: String]
var description: String
init(id: UUID = UUID(), name: String, brand: String = "", imageURL: String = "", links: [Int: String] = [:], description: String = "") {
self.id = id
self.name = name
self.brand = brand
self.imageURL = imageURL
self.description = description
self.links = links
}
}
Thanks in advance to all StackOverflow Community.
i properly conform to the Equatable protocol
struct ShoppingList: Identifiable, Equatable {
static func == (lhs: ShoppingList, rhs: ShoppingList) -> Bool {
return lhs.id == rhs.id && rhs.id == lhs.id
}
var id: UUID()
...
init(name: String, brand: String = "", imageURL: String = "", links: [Int: String] = [:], description: String = "") {
...
}
}
no need to init UUID, UUID() will self generate
Apparently, there was an error in a completely unrelated part of the code snippet I posted here (sheet View that pops when I click the button on View that has error) and that was causing the error :/
The code I posted here works just fine.

Cannot convert value of type 'NavigationLink<some View, EditView>' to closure result type '_'

I get the following error:
Cannot convert value of type 'NavigationLink' to
closure result type '_'
Do you know what's wrong here?
My ContentView file:
#ObservedObject var voucherData = VoucherData()
var body: some View {
NavigationView {
ZStack {
List {
ForEach(voucherData.voucherList) { voucher in
NavigationLink(destination: EditView(value:voucher.value, currency: voucher.currency, shopName: voucher.shopName)) {
VStack() {
And in an other file:
struct Voucher : Identifiable {
let id = UUID()
var value : String = ""
var currency : String = ""
var shopName : String = ""
}
final class VoucherData: ObservableObject {
#Published var voucherList: [Voucher] = [
.init(value: "100", currency: "USD", shopName: "FlyBurger")]
}
I assume your EditView is just missing the parameter voucher:
struct EditView: View {
let voucher: Voucher
...
}
Now you can pass the voucher like this:
NavigationLink(destination: EditView(voucher: voucher)) {

Resources