How to append data to an array from another view (swiftUI)? - arrays

In the first view I have a textfield and a date picker. How do I append that information to the array in another view so that the inputed information is added to a list?
I have this struct:
import Foundation
struct Friend: Identifiable {
let name: String
let bday: String
let id = UUID()
}
and this list:
import SwiftUI
struct FriendView: View {
#State private var friends = [Friend]()
var body: some View {
List(friends) { Friend in
HStack {
Text(Friend.name)
Spacer()
Text(Friend.bday)
}
}
}
}
struct FriendView_Previews: PreviewProvider {
static var previews: some View {
FriendView()
}
}
and this is the view where I want to append the information from the form to the friends array but I keep getting "Value of type 'AddFriendView' has no member 'friends'" error at the addFriend function.
struct AddFriendView: View {
#State var input: String = ""
#State var date: Date = Date()
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}
func addFriend () {
self.friends.append(Friend(name: "name", bday: "birthday")) //ERROR POPS UP HERE
dump(friends)
}
var body: some View {
VStack {
Spacer()
TextField("Enter name", text: $input)
.foregroundColor(.yellow)
.padding(.all)
.multilineTextAlignment(.center)
.disableAutocorrection(true)
.font(.largeTitle)
.onTapGesture {
self.hideKeyboard()
}
Text("was born on a date")
.font(.title)
.foregroundColor(.yellow)
DatePicker(
"",
selection: $date,
displayedComponents: [.date]
)
.labelsHidden()
Spacer()
Button(
"Save birthday"
) {
self.addFriend()
//print("\(self.input) \t \(self.dateFormatter.string(from: self.date))")
self.input = ""
}
.padding(.all, 50)
.accentColor(.white)
.background(Color.yellow)
.cornerRadius(25)
// Spacer()
}
}
}
Any help would be greatly appreciated. Thank you in advance
I tried adding #Binding var friends but then I get "Type annotation missing in pattern" error and also #Binding var friends: [Friend] but then I get "Missing argument for parameter 'friends' in call" error at the end here:
struct AddFriendView_Previews: PreviewProvider {
static var previews: some View {
AddFriendView()
}
}

#Binding var friends: [Friend]
is correct. And the missing argument in the preview can be accomplished by providing a constant, an empty array
struct AddFriendView_Previews: PreviewProvider {
static var previews: some View {
AddFriendView(friends: .constant([]))
}
}
or a sample friend
struct AddFriendView_Previews: PreviewProvider {
static var previews: some View {
AddFriendView(friends: .constant([Friend(name: "John Doe", bday: "Jan 1, 2000")]))
}
}

Related

How do you bind a list view

I have a CoordinateList view that shows a list of CoordinateRow views that have been entered, and are editable in place (in the list view). To add a new point to the list, the user presses a button at the bottom of the list, and it adds a row (without going to another screen). How do I make it update the view to show this new entry? I have tried wrapping the append function of the list of coordinates with a function so that I can call objectWillChange.send() when adding to the list, but it doesn't seem to do anything.
I guess I don't have enough reputation to upload an image, but here's an image:
import SwiftUI
class LocationTime : ObservableObject {
#Published var lat: String = "0.0"
#Published var lon: String = "0.0"
#Published var timestamp: String = "SomeDateTime"
}
class ModelData: ObservableObject {
#Published var positionCoords = [LocationTime]()
func appendPosition(_ loc: LocationTime) {
objectWillChange.send()
positionCoords.append(loc)
}
}
struct CoordinateRow: View {
#EnvironmentObject var modelData: ModelData
var pointIndex : Int
var body: some View {
HStack {
Text("Lon: ")
TextField("40",text:$modelData.positionCoords[pointIndex].lon)
Text("Lat: ")
TextField("",text:$modelData.positionCoords[pointIndex].lat)
Text("Time: ")
TextField("time",text:$modelData.positionCoords[pointIndex].timestamp)
}.padding()
.overlay(RoundedRectangle(cornerRadius:16)
.stroke(Color.blue,lineWidth:4.0))
}
}
struct CoordinateList: View {
#EnvironmentObject var modelData : ModelData
var body: some View {
VStack{
Text("Location Log")
.font(.largeTitle).padding()
List{
ForEach(modelData.positionCoords.indices){
CoordinateRow(pointIndex: $0).environmentObject(modelData)
}
}
Button(action:{
modelData.appendPosition(LocationTime())
print(modelData.positionCoords.count)
}){
Image(systemName: "plus.circle")
.imageScale(.large)
.scaleEffect(2.0)
.padding()
}
Spacer()
}
}
}
You need identify records in ForEach
ForEach(modelData.positionCoords.indices, id: \.self){ // << here !!
CoordinateRow(pointIndex: $0).environmentObject(modelData)
}
and by the way, remove objectWillChange.send()
func appendPosition(_ loc: LocationTime) {
// objectWillChange.send() // << called automatically for #Published
positionCoords.append(loc)
}

SwiftUI - How to append data to nested struct?

I have Student class that is connected to a struct - Details. Which has a nested struct - Subjects. I know how to append to a normal struct or class but I am having difficulties trying to append to a nested struct. What I have is a Form where a student's name and number of subjects are asked after which they have to enter the subject name and grade. Then, press the save button in the Toolbar items/ NavigationBarItems.
class Students: ObservableObject {
#Published var details = [Details]()
}
struct Details: Identifiable {
let id = UUID()
let name: String
struct Subjects: Identifiable {
let id = UUID()
let name: String
let grade: String
}
let subjects: [Subjects]
}
The View class:
import SwiftUI
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subject = [String](repeating: "", count: 10)
#State private var grade = [String](repeating: "", count: 10)
#State private var details = [Details.Subjects]()
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) { number in
TextField("Subject", text: $subject[number])
TextField("Grade", text: $grade[number])
}
}
}
}
VStack {
ForEach(students.details) { student in
Text(student.name)
ForEach(student.subjects) { subject in
HStack {
Text("Subject: \(subject.name)")
Text("Grade: \(subject.grade)")
}
}
}
}
}
.navigationTitle("Student grades")
.navigationBarItems(trailing:
Button(action: {
//let details = Details(name: name, subjects: [Details.Subjects(name: "Physics", grade: "A"), Details.Subjects(name: "Computer Science", grade: "A*")])
//students.details.append(details)
//^Above to append
}, label: {
Text("Save")
})
)
}
}
}
I have tried creating a variable of type [Subjects] but that would not let me append to it after the Textfield values are entered it gives me the error : “Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols” (Which makes sense, as it would require a button). I have also tried appending to it once the save button is pressed using a ForEach but that also gives me the same error.
I think you want the model change to be the responsibility of your Students class. Try adding a public method to Students and call it from your view, like this:
class Students: ObservableObject {
#Published var details = [Details]()
public func addDetails(_ details : Details) {
details.append(details)
}
}
Then in your button action in the View, replace students.details.append(details) with a call to this method:
let details = Details(name: name, subjects: [Details.Subjects(name: "Physics", grade: "A"), Details.Subjects(name: "Computer Science", grade: "A*")])
students.details.append(details)
Is that what you're trying to do?
Your view tries to do too much for a single view. Your trying to add students and grades in a single view. The answer of Asperi is correct. However your error is indicating that something else is wrong with your code. Try to run the code below in an isolated environment and it should work fine.
For now I just added a save button for each grade it will add the grade to the first student always.
import SwiftUI
class Students: ObservableObject {
#Published var details = [Details]()
}
struct Details: Identifiable {
let id = UUID()
let name: String
struct Subjects: Identifiable {
let id = UUID()
let name: String
let grade: String
}
var subjects: [Subjects]
}
struct TestStudentView: View {
#StateObject var students = Students()
#State private var name = ""
#State private var numberOfSubjects = ""
#State private var subject = [String](repeating: "", count: 10)
#State private var grade = [String](repeating: "", count: 10)
#State private var details = [Details.Subjects]()
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) { number in
TextField("Subject", text: $subject[number])
TextField("Grade", text: $grade[number])
Button(action: {
if students.details.count > 0 {
var test = students.details[0]
students.details[0].subjects.append(Details.Subjects(name: subject[number], grade: grade[number]))
}
}) {
Text("Save")
}
}
}
}
}
VStack {
ForEach(students.details) { student in
Text(student.name)
ForEach(student.subjects) { subject in
HStack {
Text("Subject: \(subject.name)")
Text("Grade: \(subject.grade)")
}
}
}
}
}
.navigationTitle("Student grades")
.navigationBarItems(trailing:
Button(action: {
let details = Details(name: name, subjects: [Details.Subjects(name: "Physics", grade: "A"), Details.Subjects(name: "Computer Science", grade: "A*")])
students.details.append(details)
//^Above to append
}, label: {
Text("Save")
})
)
}
}
}

SwiftUI - How do I create a Picker that selects values from a property?

I have these two models, the first one:
import SwiftUI
import Combine
class FolderModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var folderName : String
var values : [ValueModel] = []
init(folderName: String) {
self.folderName = folderName
}
}
And the second one:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
And these storages:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageValues : [ValueModel] = []
typealias StorageValues = [ValueModel]
#Published var storageFolder : [FolderModel] = []
typealias StorageFolder = [FolderModel]
//The rest of the code
}
And then I have a Detail View of the Value, which shows all of his properties. From there, I would like to select the folder that the user wants to put it in (which in code translates to appending that value into the array "values" of the FolderModel).
To do this, I tried to create a Picker that display all the folders (by name) and that can be selected, so that when I press "Save", I can do something like "selectedFolder.append(value)". The Picker I tried to create is this:
import SwiftUI
struct DetailValueView: View {
#ObservedObject var dm : DataManager
#State private var selector = 0
#State var selectedFolder : FolderModel?
var body: some View {
Form {
Section(header: Text("Properties")) {
folderCell
if hasFolder == true {
picker
}
}
}
}
var folderCell : some View {
VStack {
Toggle(isOn: $hasFolder) {
if hasFolder == true {
VStack(alignment: .leading) {
Text("Folder: " + "//Here I would like to display the selected value")
}
} else if hasFolder == false {
Text("Folder")
}
}
}
}
var picker : some View {
Picker(selection: $selector, label: Text("Select Folder")) {
ForEach(dm.storageFolder) { foldersForEach in
Button(action: {
selectedFolder = foldersForEach
}, label: {
Text(foldersForEach.folderName)
})
}
}.pickerStyle(DefaultPickerStyle())
}
I tried to find a solution online but I don't really understand how the Picker works, I don't understand how to use that "#State private var selector = 0" to get the value that I want.
Thanks to everyone who will help me!
Two things to stress here: First, you need to either wrap your form in a NavigationView or change the picker style to WheelPickerStyle. Otherwise the picker won't work (see here for a detailed explanation). Second, your state selector is of type integer. So make sure to loop through integers as well. Now your state selector holds the index of the selected folder from the list of folders.
Please see my working example below:
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var selector = 0
#State private var hasFolder = false
var body: some View {
NavigationView {
Form {
Section(header: Text("Properties")) {
folderCell
if !dm.storageFolder.isEmpty {
picker
}
}
}
}
}
var folderCell : some View {
VStack {
Toggle(isOn: $hasFolder) {
if hasFolder == true {
VStack(alignment: .leading) {
Text("Folder: \(dm.storageFolder[selector].folderName)")
}
} else if hasFolder == false {
Text("Folder")
}
}
}
}
var picker : some View {
Picker(selection: $selector, label: Text("Select Folder")) {
ForEach(0 ..< dm.storageFolder.count) {
Text(dm.storageFolder[$0].folderName)
}
}.pickerStyle(DefaultPickerStyle())
}
}

How do I make the Observable Object update the list?

So I know my items are being added to the 'vitallist'(through printing the list in the terminal), but I am not seeing them appear on list view. I think it has something to do with the 'ObservedObject' not being linked correctly. Any suggestions?
struct Vital: Identifiable {
let id = UUID()
var name: String
}
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
}
struct Row: View {
var vital: Vital
#State var completed:Bool = false
var body: some View {
HStack{
Image(systemName: completed ? "checkmark.circle.fill" : "circle").onTapGesture {
self.completed.toggle()
}
Text(vital.name)
}
}
}
struct Lists: View {
#ObservedObject var vitallist = VitalList()
var body: some View {
NavigationView{
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
I also had same problem.
I am not sure why, but it works that creating a new element in the array, not changing the element itself. I confirmed directly updating works only in data, but not for binding UI.
In my code, element change in TobuyData class.
class Tobuy: Identifiable {
let id = UUID()
var thing: String
var isDone = false
init(_ thing: String, isDone: Bool = false) {
self.thing = thing
self.isDone = isDone
}
}
class TobuyData: ObservableObject {
#Published var tobuys: [Tobuy]
init() {
self.tobuys = [
Tobuy("banana"),
Tobuy("bread"),
Tobuy("pencil"),
]
}
func toggleDone(_ tobuy: Tobuy) {
if let j = self.tobuys.firstIndex(where: { $0.id == tobuy.id }) {
self.tobuys[j] = Tobuy(self.tobuys[j].thing, isDone: !self.tobuys[j].isDone)
// self.tobuys[j].isDone.toggle() // this works only in data, but not for binding UI
}
}
}
In View
struct ContentView: View {
#EnvironmentObject var tobuyData: TobuyData
var body: some View {
List {
ForEach(tobuyData.tobuys) { tobuy in
Text(tobuy.thing)
.strikethrough(tobuy.isDone)
.onTapGesture { self.tobuyData.toggleDone(tobuy) }
...
p.s.
Changing Tobuy Class to Struct made direct element updating work, the comment out part above. This referenced to Apple's official tutorial: "Handling User Input"
change
#ObservedObject var vitallist = VitalList()
to
#EnvironmentObject var vitallist = VitalList()
The code seems fine. I added a simple add method to VitalList
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
func addVital(){
self.vitallist.append(Vital(name: UUID().description))
}
}
And a Button to the body
var body: some View {
NavigationView{
VStack{
Button(action: {self.vitallist.addVital()}, label: {Text("add-vital")})
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
The list updates as expected. check your code that adds your items to
#Published var vitallist = [Vital]()
Are you using the same instance of VitalList? A singleton might help.
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton

Creating a series of master detail lists from a single JSON file in SwiftUI

I'm trying to work through understanding how I can make data flow nicely through an app I'm building. I just want a basic master detail view where it starts with a list of all the top level objects(users), tapping one of them lets you see all the second level objects related to that top level (userX -> cities), and tapping one of them lets you see all the third level objects (userX -> cityX -> towns).
This is my JSON file:
[
{
"id": 1001,
"first_name": "Jimmy",
"last_name": "Simms",
"cities": [{
"name": "New York City",
"towns": [{
"name": "Brooklyn"
},
{
"name": "Manhatten"
}
]
},
{
"name": "Tokyo",
"towns": [{
"name": "Churo"
},
{
"name": "Riponggi"
}
]
}
]
}
...
]
I have a model that I think will work well for this:
import SwiftUI
struct UserModel: Codable, Identifiable {
let id: Int
let firstName: String
let lastName: String
let cities: [CityModel]
enum CodingKeys: String, CodingKey {
case id
case firstName = "first_name"
case lastName = "last_name"
case cities
}
}
struct CityModel: Codable {
let name: String
let towns: [TownModel]
}
struct TownModel: Codable {
let name: String
}
However, what I'm struggling to do is to build this all into a series of list views that are connected to each other. I have the top level one, UserList.swift at least showing a list of the users.
import SwiftUI
struct UserList: View {
var body: some View {
NavigationView {
List(userData) { user in
NavigationLink(destination: UserRow(user: user)) {
UserRow(user: user)
}
}
.navigationBarTitle(Text("Users"))
}
}
}
struct UserList_Previews: PreviewProvider {
static var previews: some View {
UserList()
}
}
And it's assistant view, UserRow:
import SwiftUI
struct UserRow: View {
var user: UserModel
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(user.firstName)
.font(.headline)
Text(user.lastName)
.font(.body)
.foregroundColor(Color.gray)
}
Spacer()
}
}
}
struct UserRow_Previews: PreviewProvider {
static var previews: some View {
UserRow(user: userData[0])
}
}
UserList.swift Preview:
What I can't figure out is how to write CityList/CityRow and TownList/TownRow such that I can drill down from the main screen and get a list related to the objected I tapped into.
Your CityModel and TownModel need to conform to Identifiable, just add an id to them like you did in UserModel.
Than you need to edit your UserList NavigationLink:
NavigationLink(destination: CityList(cities: user.cities)) {
Text(user.firstName)
}
The Navigation is now like this: UserList -> CityList -> TownList
CityList:
struct CityList: View {
var cities: [CityModel]
var body: some View {
List (cities) { city in
NavigationLink(destination: TownList(towns: city.towns)) {
Text(city.name)
}
}
}
}
TownList:
struct TownList: View {
var towns: [TownModel]
var body: some View {
List (towns) { town in
Text(town.name)
}
}
}
I hope that helps, in my test project it works!
first you have to create CityListView and CityRow, like you did for users:
struct CityListView: View {
var user: UserModel
var body: some View {
// don't forget to make CityModel Identifiable
List(user.cities) { city in
CityRowView(city: city)
}
.navigationBarTitle(Text("Cities"))
}
}
}
struct CityRowView: View {
var city: CityModel
var body: some View {
HStack {
Text(city. name)
.font(.headline)
Spacer()
}
}
}
after that you need to change destination in NavigationLink (not UserRow, but new CityListView)
...
//NavigationLink(destination: UserRow(user: user)) {
NavigationLink(destination: CityListView(user: user)) {
UserRow(user: user)
}
...
Another way is to declare variable "cities" as an array of CityModel and receive it from user:
struct CityListView: View {
var cities: [UserModel]
// list for array of cities
}
// in UserList
NavigationLink(destination: CityListView(cities: user.cities)) {
UserRow(user: user)
}
P.S. Apple made excellent tutorial for navigations in SwiftUI: https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation

Resources