How do you bind a list view - arrays

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)
}

Related

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

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")]))
}
}

How to bind a list of textfields that edit a variable within an a core data array?

Swift ui requires a Binding<String> to link to the value you are updating in a text field. Much like the native iPhone Reminders app, I am looking to permit inline editing a list that will persist.
The attached code works only but gives the same name for each item due to them all being bound to the same variable. How can I bind this to the [FruitEntity] array?
class CoreDataViewModel: ObservableObject {
//static let instance = CoreDataViewModel()
let container: NSPersistentContainer
let context: NSManagedObjectContext
#Published var savedEntities: [FruitEntity] = []
}
struct Screen: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: entity.name) { text in
entity.name = questionVariable
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}
}
}
You can just move the TextField to a separate view, with its own #State var for the field and another var for the entity.
Create a view like the following one:
struct ChangeName: View {
// Will change the entity
let entity: FruitEntity
// Will update the field
#State private var questionVariable = ""
var body: some View {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: questionVariable) { text in
entity.name = text
// Remember to save the persistent container/ managed-object-context
}
}
}
Call it in your main view:
struct Screen: View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
ChangeName(entity: entity)
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}

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

SwiftUI FetchRequest and onReceive cause infinite loop

I have a #FetchRequest for NSManagedObject in swiftUI. I’m trying to change the title of the first item in core data when the toggle is used, by calling onReceive. This does not work if the fetched results are used in MainVC. To demonstrate this, the Navigation Buttons title is how many elements are in the fetched result. This ends up creating an infinite loop in the ContentView onRecieve method. If the Navigation Button has regular text instead of using anything from the FetchedResults then there is no loop and everything works as expected. How is this loop being caused, and is there any better way of toggling a specific core data element?
import SwiftUI
import CoreData
final class ContentVCModel : ObservableObject{
#Published var newToDoItem = String()
#Published var shouldTurnOn = false
func createNewToDoItem(){
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let toDoItem = ToDoItem(context: moc)
toDoItem.title = self.newToDoItem
toDoItem.createdAt = Date()
toDoItem.cost = Cost(main: "$\(Int.random(in: 1...99))", tax: "$\(Int.random(in: 1...9))")
toDoItem.isOn = false
ToDoItem.save()
self.newToDoItem = ""
}
}
struct ContentView: View {
#ObservedObject var model = ContentVCModel()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems : FetchedResults<ToDoItem>
var body: some View {
List{
Section(header : Text("Whats next?")){
Toggle("Toggle Test", isOn: $model.shouldTurnOn)
.onReceive(model.$shouldTurnOn) { (newValue) in
self.toDoItems.first?.title = "\(Int.random(in: 23...3423))"
//infitine loop
}
HStack{
TextField("New Item", text: $model.newToDoItem)
Button(action: {
self.model.createNewToDoItem()
}){
Image(systemName: "plus.circle.fill")
.foregroundColor(self.model.newToDoItem.isEmpty ? .gray : .green)
.imageScale(.large)
}.disabled(self.model.newToDoItem.isEmpty)
}
}.font(.headline)
Section(header: Text("To Do's")){
ForEach(toDoItems) { toDoItem in
ToDoItemView(title: toDoItem.title, createdAt: toDoItem.createdAt.description, cost: toDoItem.cost, isOn: true)
}.onDelete { (indexSet) in
let deleteItem = self.toDoItems[indexSet.first!]
self.managedObjectContext.delete(deleteItem)
ToDoItem.save()
}
}
}
.navigationBarTitle(Text("My List"))
.navigationBarItems(trailing: EditButton())
}
}
struct MainVC : View {
#FetchRequest(fetchRequest: ToDoItem.getAllToDoItems()) var toDoItems : FetchedResults<ToDoItem>
var body: some View {
NavigationView{
NavigationLink(destination: ContentView()) {
Text("\(toDoItems.count)") //Using toDoItems causes this to repeat ex: "\(toDoItems.count)"
}
}
}
}

Resources