Is it possible to make a customisable & reusable SwiftUI Form? - swift

I want the to be able to create a Form in SwiftUI by creating a custom model and defining what fields are needed. Eventually, I want the User to be able to do this to make their own custom forms and data models, but for now I just want to reduce the code so that I can just quickly "Write" different types of Form and use a For loop to generate the form.
I.e. I have basically duplicate pages of code for different types of forms, which all work the same but just have different values and it seems like a waste, and I could just have some kind of single view and feed the data needed to that so I can reuse it, and eventually allow users to create their own custom forms in the future.
Here is what I have come up with so far as an example (Just playing around and trying to see what might be possible):
import SwiftUI
import Foundation
var fieldTypes = ["Field", "Slider", "List", "Date"...]
struct Worksheet: Codable, Identifiable, Hashable {
//data for populating Form
var id: String
var name: String?
var fields: [Field]
//data for saving model and values of form
var title: String?
var date: Date
var severity: Double
var trigger: String?
var description: String
var symptoms: [String]
var nInterpretation: String?
var rResponse: String
}
struct Field: Codable, Identifiable, Hashable {
var id: String
var type: String
var title: String
var description: String
var footer: String
}
class Dev_VM: ObservableObject {
var fields = [Field(id: "i", type: "Slider", title: "SliderTitle", description: "Description", footer: "Footer"), Field(id: "id2", type: "TextField", title: "TextFieldTitle", description: "Description", footer: "Footer")]
}
//Form view for data entry
struct Dev_FormView: View {
#Environment(\.presentationMode) var presentation
#ObservedObject var vm = Dev_VM()
static let taskDateFormat: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale.current
return dateFormatter
}()
var body: some View {
Form {
//Section for selecting date.
for field in vm.fields {
switch field.type {
case "TextField":
Section(header: Text(field.title), footer: Text(field.footer)) {
ZStack {
TextEditor(text: $title)
.disabled(!editMode)
Text(title).opacity(0).padding(.all, 8)
}
}
case "Slider":
HStack {
Slider(value: $value, in: 0...100, step: 1)
.disabled(true)
Text("\(Int(severity))")
}
default:
print("Empty")
}
}
}
.navigationBarTitle("Dev_Playground", displayMode: .inline)
}
}
Seems that there are problems, for example on the for statement I get the error Closure containing control flow statement cannot be used with result builder 'ViewBuilder'
Another problem seems to be populating the values, as I will need #State variables, because the fields are dynamic, could I make these dynamic too?
Does anyone think that what I'm trying to do might be possible? Thank you!

Related

write (add) multiple array in firestore

I try to add multiple array in firestore using struct codable in swiftui but when I add, it updates the same array added but i want add more array in my list...
as you can see in the image, every time I add a text, it updates my index "0" but I want to add multiple indexes, for example O, 1, 2... in my array" tags"
ViewModel
class DataManager: ObservableObject {
#Published var books = Book(tags: [Tag(title: "", color: "")])
func addBookNew(nom: String, prenom: String){
//guard let userID = Auth.auth().currentUser?.uid else {return}
let db = Firestore.firestore()
let collectionRef = db.collection("Users").document("TEST")
do {
let data = Book(tags: [Tag(title: nom, color: prenom)])
try collectionRef.setData(from: data)
}
catch {
print(error)
}
}
STRUCT BOOK
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Book: Codable {
#DocumentID var id: String?
let tags: [Tag]
}
STRUCT TAG
import Foundation
import FirebaseFirestoreSwift
struct Tag: Codable, Hashable {
let title: String
let color: String
}
LIST VIEW
import SwiftUI
import Firebase
struct ListView: View {
#StateObject var dataManager = DataManager()
#State private var showPopUp = false
#EnvironmentObject var viewModel: AppViewModel
var books: [Book]
#State var nomTF = ""
#State var prenomTF = ""
#State var dismiss = false
var body: some View {
NavigationView{
VStack{
TextField("nom", text: $nomTF )
TextField("prenom", text: $prenomTF)
Button("save") {
dataManager.addBookNew(nom: nomTF, prenom: prenomTF)
}
List{
ForEach(dataManager.books.tags, id: \.self) { book in
HStack{
Text("T: \(book.title)")
Text("C: \(book.color)")
}
}
}
}.onAppear(){
dataManager.fetchtest()
}
The problem is here:
let data = Book(tags: [Tag(title: nom, color: prenom)])
try collectionRef.setData(from: data)
This code tells Firestore to set the tags field to the array that you specify in the call.
If you want to add unique items to existing array, you can use FieldValue.arrayUnion([...]).
let data = Book(tags: FieldValue.arrayUnion([Tag(title: nom, color: prenom)]))
try collectionRef.setData(from: data)
Note that this will only add the item if it is not already in the array. If you want to allow for duplicate items, you will have to read the array from the document first, then updates in your application code, and finally write the entire array back to the database.
Update: it seems your Book class can only accept Tag in its tags field, so you can't specify an FieldValue.arrayUnion there. It's probably easiest to then simply perform the union in the database call:
try collectionRef.updateData([
"tags": FieldValue.arrayUnion([Tag(title: nom, color: prenom)])
])
So instead of writing the entire book here, we're updating a single field in the document, and in there perform the array union.
washingtonRef.updateData([
"regions": FieldValue.arrayUnion(["greater_virginia"])
])

How to use the Transferable protocol in SwiftUI tables

I am building a macOS app in Swift, using SwiftUI. I have the following struct:
struct focusArticle: Identifiable {
let category: String
let headline: String
let body: String
let publishedDate: String
let source: String
var id = UUID()
}
I have created an array with instances of focusArticle that are being used to populate a Table element, like so
struct ArticleView: View {
#State private var selectedArticles = Set<focusArticle.ID>()
var body: some View {
Table(articles, selection: $selectedArticles) {
TableColumn("Category", value: \.category)
.width(70)
TableColumn("Headline", value: \.headline)
.width(350)
TableColumn("Published", value: \.publishedDate)
}
.frame(width: 600, height: 200)
}
}
I want to make each row in this table draggable, so I can drag-drop from this table, to a similar table in another view. From reading Apple's documentation, it seems like I need to do two things.
Make my focusArticle struct conform to the Transferable protocol.
Use the .draggable modifier on the table to make the elements draggable.
I can't seem to do either of these things. Copying over the example code from the documentation into my struct makes it throw an error — Static method 'buildBlock' requires that 'String' conform to 'TransferRepresentation'. Adding a .draggable modifier to the table throws an expected error: Instance method 'draggable' requires that '[focusArticle]' conform to 'Transferable'
EDIT: This is one of the ways I have tried to make focusArticles confirm to Transferable.
struct focusArticle: Identifiable, Codable, Transferable {
let category: String
let headline: String
let body: String
let publishedDate: String
let source: String
var id = UUID()
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(contentType: .focusArticle)
}
}
This shows an error Type 'UTType' has no member 'focusArticle'
You need to add a UTType, as an extension like this:
extension UTType {
static var focusArticle = UTType(exportedAs: "com.yourdomain.yourapp.focusarticle")
}

SwiftUI - Why won't my #Published variable update within my #ObservedObject call?

In essence, I'm learning by creating a simple app that takes an ISBN and returns the book information. I've created a view file to display this information along with a text entry for capturing the ISBN. The logic side of things as pertaining to getting the information from the internet is fine, however, when I try to update the view with the modified variables, it only prints them as declared, and NOT redrawing them after modification. Any advice would be appreciated. Thanks!
This specific example references #Published var test = "Testing 1 2 3 as the default, and after the search, the var is modified to test = modified text which is then printed in the text view of TestView.swift. The print statement correctly displays the modified text, however it does not update in the view in which it is passed. The goal is to pass the final dictionary, but for testing purposes I'm using a simple string variable.
TestFile.swift
import Foundation
import SwiftyXMLParser
import SwiftUI
class getdata: ObservableObject {
#Published var outputDict: [String:String] = [:]
#Published var test = "Testing 1 2 3"
.
.
//code to grab xml from url
.
.
func parseData(input: String) {
var title: String?
var author: String?
let xml = try! XML.parse(input)
if let text = xml["GoodreadsResponse", "book", "title"].text {
title = text
outputDict["title"] = title
}
if let text = xml["GoodreadsResponse", "book", "authors", "author", "name"].text {
author = text
outputDict["author"] = author
}
print("Title: \(outputDict["title"]!), Author: \(outputDict["author"]!)")
test = "modified text"
print(test)
}
}
TestView.swift
import SwiftUI
struct testView: View {
#State var textEntry: String = ""
#ObservedObject var viewModel: getdata
var body: some View {
let try1 = getdata()
VStack {
TextField("Enter ISBN", text: $textEntry)
.padding()
Button(action: {try1.getData(bookID: textEntry)}, label: {
Text("Search")
})
Text("Title: \(self.viewModel.test)")
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView(viewModel: getdata())
}
}
Although I don't think your example compiles, and you haven't provided the definitions of some of the functions, I can point you at one error:
Inside the body of the view you are creating a new Observable object:
let try1 = getdata()
And your Button calls messages on this:
Button(action: {try1.getData(bookID: textEntry)}...
But your TextView reads from the original viewModel:
Text("Title: \(self.viewModel.test)")
You are interacting with try1, but deriving your view from viewModel. They are separate objects.

Binding Array of Structs with Strings in an Array of Structs

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}

Why does picker binding not update when using SwiftUI?

I have just begun learning Swift (and even newer at Swift UI!) so apologies if this is a newbie error.
I am trying to write a very simple programme where a user chooses someone's name from a picker and then sees text below that displays a greeting for that person.
But, the bound var chosenPerson does not update when a new value is picked using the picker. This means that instead of showing a greeting like "Hello Harry", "Hello no-one" is shown even when I've picked a person.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson: String? = nil
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach ((0..<people.count), id: \.self) { person in
Text(self.people[person])
}
}
}
Section{
Text("Hello \(chosenPerson ?? "no-one")")
}
}
}
}
}
(I have included one or two pieces of the original formatting in case this is making a difference)
I've had a look at this question, it seemed like it might be a similar problem but adding .tag(person) to Text(self.people[person])did not solve my issue.
How can I get the greeting to show the picked person's name?
Bind to the index, not to the string. Using the picker, you are not doing anything that would ever change the string! What changes when a picker changes is the selected index.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson = 0
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach(0..<people.count) { person in
Text(self.people[person])
}
}
}
Section {
Text("Hello \(people[chosenPerson])")
}
}
}
}
}
The accepted answer is right if you are using simple arrays, but It was not working for me because I was using an array of custom model structs with and id defined as string, and in this situation the selection must be of the same type as this id.
Example:
struct CustomModel: Codable, Identifiable, Hashable{
var id: String // <- ID of type string
var name: String
var imageUrl: String
And then, when you are going to use the picker:
struct UsingView: View {
#State private var chosenCustomModel: String = "" //<- String as ID
#State private var models: [CustomModel] = []
var body: some View {
VStack{
Picker("Picker", selection: $chosenCustomModel){
ForEach(models){ model in
Text(model.name)
.foregroundColor(.blue)
}
}
}
Hope it helps somebody.