import Foundation
import SwiftUI
struct Item: Identifiable, Codable{
var id = UUID()
var image: String
var name: String
var price: Int
var isFavorite: Bool
}
class Model: ObservableObject{
#Published var group = [Item]() {
didSet {
if let encoded = try? JSONEncoder().encode(group){
UserDefaults.standard.set(encoded, forKey: "peopleKey")
}
}
}
init(){
if let savedItems = UserDefaults.standard.data(forKey: "peopleKey"),
let decodedItems = try? JSONDecoder().decode([Item].self, from: savedItems) {
group = decodedItems
} else {
group = itemData
}
}
var itemData: [Item] = [
Item(image: "imageGIFT", name: "Flower",price: 5 , isFavorite: false),
Item(image: "imageGIFT", name: "Coffe Cup",price: 9 , isFavorite: false),
Item(image: "imageGIFT", name: "Teddy Bear",price: 2 , isFavorite: false),
Item(image: "imageGIFT", name: "Parfume",price: 8 , isFavorite: false)
]
}
I am trying to change variables on this struct and I define as var but after encode and decode they has been let. I changed let part to var then Xcode gived an error.
here is my test code that shows doing model.people[0].myPerson.toggle() in the Button
does work. I have made some minor mods and added some comments to the code.
I suggest again, read the very basics of Swift,
in particular the array section at: https://docs.swift.org/swift-book/LanguageGuide/CollectionTypes.html.
Without understanding these very basic concepts you will keep struggling to code your App.
Note, there is probably no need for the myPeople array in your Model, but if that's what you want to have.
struct Person: Identifiable{
let id = UUID()
var name: String
var age: Int
var job: String
var myPerson: Bool
}
class Model: ObservableObject {
#Published var people: [Person] = []
#Published var myPeople: [Person] = []
init(){
addPeople()
}
// completely useless
func addPeople(){
people = peopleData
myPeople = peopleData.filter { $0.myPerson }
}
// here inside the class and using `Person` not `person`
var peopleData = [
Person(name: "Bob", age: 22, job: "Student", myPerson: false),
Person(name: "John", age: 26, job: "Chef", myPerson: false)
]
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
VStack {
// this `myPeople` array is empty at first, nothing is displayed
ForEach(model.myPeople) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.blue)
Text("Age: \(person.age)").foregroundColor(.blue)
Text("Job: \(person.job)").foregroundColor(.blue)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.blue)
}.padding()
}
}
VStack {
// this `people` array has two items in it
ForEach(model.people) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.red)
Text("Age: \(person.age)").foregroundColor(.red)
Text("Job: \(person.job)").foregroundColor(.red)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.red)
}.padding()
}
}
Button("Click") {
print("\n--> before name: \(model.people[0].name) ")
print("--> before myPerson: \(model.people[0].myPerson) ")
model.people[0].name = "Franz"
model.people[0].myPerson.toggle()
print("\n--> after name: \(model.people[0].name) ")
print("--> after myPerson: \(model.people[0].myPerson) ")
// update the myPeople array (the blue items)
model.myPeople = model.people.filter { $0.myPerson }
}
}
}
}
Alternatively, you could use this code using only one array of people: [Person]:
class Model: ObservableObject {
#Published var people: [Person] = []
init(){
addPeople()
}
func addPeople() {
people = peopleData
}
// here inside the class and using `Person` not `person`
var peopleData = [
Person(name: "Bob", age: 22, job: "Student", myPerson: false),
Person(name: "John", age: 26, job: "Chef", myPerson: false)
]
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
VStack {
// here filter on myPerson=true
ForEach(model.people.filter { $0.myPerson }) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.blue)
Text("Age: \(person.age)").foregroundColor(.blue)
Text("Job: \(person.job)").foregroundColor(.blue)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.blue)
}.padding()
}
}
VStack {
// here filter on myPerson=false
ForEach(model.people.filter { !$0.myPerson }) { person in
VStack(alignment: .leading){
Text("Name: \(person.name)").foregroundColor(.red)
Text("Age: \(person.age)").foregroundColor(.red)
Text("Job: \(person.job)").foregroundColor(.red)
Text("myPerson: " + String(person.myPerson)).foregroundColor(.red)
}.padding()
}
}
Button("Click") {
model.people[0].name = "Franz"
model.people[0].myPerson.toggle()
}
}
}
}
Related
var someProtocol = [SurveyItems : [Surveys]]()
sectionLabels.forEach{ a in
var finalSurveys = [Surveys]()
surveys.forEach{ b in
if a.groupHeader == b.group_survey {
finalSurveys.append(b)
}
someProtocol[a] = finalSurveys
}
}
I wanted to use that someProtocol to dynamically display the label section and the surveys under that section.
for (Surveys, SurveyItems) in someProtocol {
Text(Surveys.sectionTitle)
for survey in SurveyItems {
Text(survey.label)
}
}
I tried ViewBuider but getting some error.
To loop and display your someProtocol dictionary in a View, try this example code:
Adjust the code for your own purpose. Note that in a SwiftUI View you need to use a ForEach not the "normal" swift for x in ... to loop over a sequence.
struct ContentView: View {
#State var someProtocol = [SurveyItems : [Surveys]]()
var body: some View {
List(Array(someProtocol.keys), id: \.self) { key in
VStack {
if let surveys = someProtocol[key] {
Text(key.title).foregroundColor(.red)
ForEach(surveys, id: \.self) { survey in
Text("survey \(survey.label)")
}
}
}
}
.onAppear {
// for testing
someProtocol[SurveyItems(id: "1", number: 1, title: "title-1")] = [Surveys(id: "s1", label: "label-1"), Surveys(id: "s2", label: "label-2")]
someProtocol[SurveyItems(id: "2", number: 2, title: "title-2")] = [Surveys(id: "s3", label: "label-3")]
}
}
}
struct SurveyItems: Identifiable, Hashable {
let id: String
let number: Int
var title: String
}
struct Surveys: Identifiable, Hashable {
let id: String
let label: String
}
import Foundation
import SwiftUI
struct Person: Identifiable, Codable{
var id = UUID()
let name: String
var isFavorite: Bool
}
class People: ObservableObject{
#Published var group = [Person]() {
didSet {
if let encoded = try? JSONEncoder().encode(peopleData){
UserDefaults.standard.set(encoded, forKey: "peopleKey")
}
}
}
init(){
if let savedItems = UserDefaults.standard.data(forKey: "peopleKey"),
let decodedItems = try? JSONDecoder().decode([Person].self, from: savedItems) {
group = decodedItems
} else {
group = peopleData
}
}
var peopleData: [Person] = [
Person(name: "Bob", isFavorite: false),
Person(name: "John", isFavorite: false),
Person(name: "Kayle", isFavorite: false),
Person(name: "Alise", isFavorite: false)
]
}
I am tryn to save a chance on array. But when I relaunch its not saved.
import SwiftUI
struct ContentView: View {
#StateObject var model = People()
var body: some View {
VStack(alignment: .leading, spacing: 10){
Text(model.group[0].name)
.opacity(model.group[0].isFavorite ? 1:0)
Button(model.group[0].isFavorite ? "Remove from favorite" : "add to favorites") {
model.group[0].isFavorite.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I can toggle the isFavorite bool with button. But when I relaunch its not saved.
in the which part of this code I made a mistake and why its not working I can't figure out.
I tried a lot of way for save data in array but all attempts was failed.
currently you are saving peopleData, where you should be saving group. So try using:
class People: ObservableObject{
#Published var group = [Person]() {
didSet {
if let encoded = try? JSONEncoder().encode(group){ // <-- here group
UserDefaults.standard.set(encoded, forKey: "peopleKey")
}
}
}
// ...
I want to create simple program for edit this JSON : https://pastebin.com/7jXyvi6Y
I created Smoothie struct and read smoothies into array.
Now I want create new Smoothie instance which I should pass as parameter into SmoothieForm. In Smoothie form I should complete fields with values and then this smoothie should be added to array and array should be saved in json.
How to create new instance of this Smoothie struct ? And how append into array ?
I have struct with my smoothies
import Foundation
import SwiftUI
struct Smoothie : Hashable, Codable, Identifiable {
var id: Int
var name: String
var category: Category
var wasDone: Bool
var isFavorite: Bool
var time: String
var ingedients: [Ingedients]
var steps: [Steps]
var image : Image {
Image(imageName)
}
enum Category: String, CaseIterable, Codable {
case forest = "Forest fruit"
case garden = "Garden fruit"
case egzotic = "Exotic"
case vegatble = "Vegetables"
}
private var imageName: String
struct Steps: Hashable, Codable {
var id: Int
var description: String
}
struct Ingedients: Hashable, Codable {
var id: Int
var name: String
var quantity: Double
var unit: String
}
}
And now I builded form view with first few fields:
struct SmoothieForm: View {
var body: some View {
VStack {
Text("Add smooth")
HStack {
Text("Name")
TextField("Placeholder", text: .constant(""))
}
HStack {
Text("Category")
TextField("Placeholder", text: .constant(""))
}
HStack {
Text("Time")
TextField("Placeholder", text: .constant(""))
}
Divider()
}
.padding(.all)
}
}
struct SmoothieForm_Previews: PreviewProvider {
static var previews: some View {
SmoothieForm()
}
}
Class for load data from json :
import Foundation
final class ModelData:ObservableObject{
#Published var smoothies: [Smoothie] = load("smoothieData.json")
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename,withExtension: nil) else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
I work with c # on a daily basis
import SwiftUI
//You need default values so you can initialize an empyty item
struct Smoothie : Hashable, Codable, Identifiable {
//Find a way to make this unique maybe switch to UUID
var id: Int = 999999
var name: String = ""
var category: Category = Category.unknown
var wasDone: Bool = false
var isFavorite: Bool = false
var time: String = ""
var ingedients: [Ingedients] = []
var steps: [Steps] = []
var image : Image {
if !imageName.isEmpty{
return Image(imageName)
}else{
return Image(systemName: "photo")
}
}
enum Category: String, CaseIterable, Codable {
case forest = "Forest fruit"
case garden = "Garden fruit"
case egzotic = "Exotic"
case vegatble = "Vegetables"
case unknown
}
private var imageName: String = ""
struct Steps: Hashable, Codable {
var id: Int
var description: String
}
struct Ingedients: Hashable, Codable {
var id: Int
var name: String
var quantity: Double
var unit: String
}
}
struct SmothieForm: View {
//Give the View access to the Array
#StateObject var vm: ModelData = ModelData()
//Your new smoothie will be an empty item
#State var newSmoothie: Smoothie = Smoothie()
var body: some View {
VStack {
Text("Add smooth")
HStack {
Text("Name")
//reference the new smoothie .constant should only be used in Preview Mode
TextField("Placeholder", text: $newSmoothie.name)
}
VStack {
Text("Category")
//reference the new smoothie .constant should only be used in Preview Mode
Picker(selection: $newSmoothie.category, label: Text("Category"), content: {
ForEach(Smoothie.Category.allCases, id: \.self){ category in
Text(category.rawValue).tag(category)
}
})
}
HStack {
Text("Time")
//reference the new smoothie .constant should only be used in Preview Mode
TextField("Placeholder", text: $newSmoothie.time)
}
Divider()
//Append to array when the user Saves
Button("Save - \(vm.smoothies.count)", action: {
vm.smoothies.append(newSmoothie)
})
}
.padding(.all)
}
}
Simple question to ask but I probably forgot something in the code.
Let's explain better using an image:
I need basically to update "COUNT" and "PRICE" while selecting/deselecting items.
I have a code structure like this:
Model:
class ServiceSelectorModel: Identifiable, ObservableObject {
var id = UUID()
var serviceName: String
var price: Double
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
#Published var selected: Bool = false
}
ViewModel:
class ServiceSelectorViewModel: ObservableObject {
#Published var services = [ServiceSelectorModel]()
init() {
self.services = [
ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
]
}
}
ToggleView
struct ServiceToggleView: View {
#ObservedObject var model: ServiceSelectorModel
var body: some View {
VStack(alignment: .center) {
HStack {
Text(model.serviceName)
Toggle(isOn: $model.selected) {
Text(String(format: "€ +%.2f", model.price))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
.background(model.selected ? Color.yellow : Color.clear)
}
}
}
ServiceSelectorView
struct ServiceSelectorView: View {
#ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
var body: some View {
VStack {
VStack(alignment: .leading) {
ForEach (serviceSelectorVM.services, id: \.id) { service in
ServiceToggleView(model: service)
}
}
let price = serviceSelectorVM.services.filter{ $0.selected }.map{ $0.price }.reduce(0, +)
Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.selected }.count)")
Text(String(format: "TOTAL PRICE: €%.2f", price))
}
}
}
In this code I'm able to update the selected status of the model but the view model that contains all the models and should refresh the PRICE not updates.
Seems that the model in the array doesn't change.
what have i forgotten?
Probably most simple is to make your model as value type, then changing its properties the view model, holding it, will be updated. And to update view to use binding to those values
struct ServiceSelectorModel: Identifiable {
var id = UUID()
var serviceName: String
var price: Double
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
var selected: Bool = false
}
struct ServiceToggleView: View {
#Binding var model: ServiceSelectorModel
...
}
...
ForEach (serviceSelectorVM.services.indices, id: \.self) { i in
ServiceToggleView(model: $serviceSelectorVM.services[i])
}
Note: written inline, not tested, some typo might needed to be fixed
You have created two different Single source of truth if you notice carefully. That’s the reason parent is not updating, as it’s not linked to child.
One way is to create ServiceSelectorModel as a struct, and pass services array as #Binding in child view. Below is the working example.
ViewModel-:
struct ServiceSelectorModel {
var id = UUID().uuidString
var serviceName: String
var price: Double
var isSelected:Bool = false
init(serviceName: String, price: Double) {
self.serviceName = serviceName
self.price = price
}
}
class ServiceSelectorViewModel: ObservableObject,Identifiable {
#Published var services = [ServiceSelectorModel]()
var id = UUID().uuidString
init() {
self.services = [
ServiceSelectorModel(serviceName: "SERVICE 1", price: 1.80),
ServiceSelectorModel(serviceName: "SERVICE 2", price: 10.22),
ServiceSelectorModel(serviceName: "SERVICE 3", price: 2.55)
]
}
}
Views-:
struct ServiceToggleView: View {
#Binding var model: [ServiceSelectorModel]
var body: some View {
VStack(alignment: .center) {
ForEach(0..<model.count) { index in
HStack{
Text(model[index].serviceName)
Toggle(isOn: $model[index].isSelected) {
Text(String(format: "€ +%.2f", model[index].price))
.frame(maxWidth: .infinity, alignment: .trailing)
}
}.background(model[index].isSelected ? Color.yellow : Color.clear)
}
}
}
}
struct ServiceSelectorView: View {
#ObservedObject var serviceSelectorVM = ServiceSelectorViewModel()
var body: some View {
VStack {
VStack(alignment: .leading) {
ServiceToggleView(model: $serviceSelectorVM.services)
}
let price = serviceSelectorVM.services.filter{ $0.isSelected }.map{ $0.price }.reduce(0, +)
Text("SELECTED: \(serviceSelectorVM.services.filter{ $0.isSelected }.count)")
Text(String(format: "TOTAL PRICE: €%.2f", price))
}
}
}
import SwiftUI
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects = [Subjects](repeating: Subjects(name: "", grade: ""), count: 10)
var body: some View {
NavigationView {
Group {
Form {
Section(header: Text("Student details")) {
TextField("Name", text: $name)
TextField("Number of subjects", text: $numberOfSubjects)
}
let count = Int(numberOfSubjects) ?? 0
Text("Count: \(count)")
Section(header: Text("Subject grades")) {
if count>0 && count<10 {
ForEach(0 ..< count, id: \.self) { number in
TextField("Subjects", text: $subjects[number].name)
TextField("Grade", text: $subjects[number].grade)
}
}
}
}
VStack {
ForEach(students.details) { student in
Text(student.name)
ForEach(student.subjects) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.id) { subject in //Does not work as expected
//ForEach(student.subjects, id:\.self) { subject in //works fine with this
HStack {
Text("Subject: \(subject.name)")
Text("Grade: \(subject.grade)")
}
}
}
}
}
.navigationTitle("Student grades")
.navigationBarItems(trailing:
Button(action: {
let details = Details(name: name, subjects: subjects)
students.details.append(details)
}, label: {
Text("Save")
})
)
}
}
}
struct TestStudentView_Previews: PreviewProvider {
static var previews: some View {
TestStudentView()
}
}
class Students: ObservableObject {
#Published var details = [Details]()
}
struct Details: Identifiable {
let id = UUID()
var name: String
var subjects: [Subjects]
}
struct Subjects: Identifiable, Hashable {
let id = UUID()
var name: String
var grade: String
}
When I use - "ForEach(student.subjects, id:.id) { subject in" under normal circumstances it is supposed to work as id = UUID and the incorrect output is as follows:
then as the class conforms to Identifiable I tried - "ForEach(student.subjects) { subject in" it still does not work correctly. However, when I do - "ForEach(student.subjects, id:.self) { subject in" except I had to have the class conform to hashable and gives me the correct expected output. The correct output which is shown:
You need to use a map instead of repeating.
By using Array.init(repeating:) will invoke the Subjects to initialize only one time, and then insert that object into the array multiple times.
So all, in this case, all id is same.
You can check by just print all id in by this .onAppear() { print(subjects.map({ (sub) in print(sub.id) }))
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subjects: [Subjects] = (0...10).map { _ in
Subjects(name: "", grade: "")
} //<-- Here