Can't create binding in TextField - 'cannot find $social' in scope' - ios

I can't create a binding with my 'username' property - Xcode gives me the error 'cannot find $social in scope'. Here is some of the essential code:
My problematic view:
struct ProfileSetter: View {
#Binding var profile: Profile
var body: some View {
ForEach(profile.socials, id: \.id) { social in
TextField(social.medium.rawValue, text: $social.username) //-> cannot find $social in scope
}
}
}
Its parent:
struct ProfileView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
ProfileSetter(profile: $viewModel.myProfile)
}
}
The simplified view model:
class ViewModel: ObservableObject {
#Published var myProfile = Profile(socials: [//multiple instances of Social])
}
And finally, the models:
struct Profile {
// other properties
var socials: [Social]
}
struct Social {
// other properties
var username: String
}
Replacing the text field with Text(social.username) works fine, so creating the binding seems to be the problem.

You cannot bind directly to value, you should do it via parent object, like
ForEach(profile.socials.indices, id: \.self) { index in
TextField(profile.socials[index].medium.rawValue,
text: $profile.socials[index].username)
}

Related

SwiftUI Bind to #ObservableObject in array

How do I pass a bindable object into a view inside a ForEach loop?
Minimum reproducible code below.
class Person: Identifiable, ObservableObject {
let id: UUID = UUID()
#Published var healthy: Bool = true
}
class GroupOfPeople {
let people: [Person] = [Person(), Person(), Person()]
}
public struct GroupListView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
//MARK: Other properties
let group: GroupOfPeople = GroupOfPeople()
//MARK: Body
public var body: some View {
ForEach(group.people) { person in
//ERROR: Cannot find '$person' in scope
PersonView(person: $person)
}
}
//MARK: Init
}
public struct PersonView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
#Binding var person: Person
//MARK: Other properties
//MARK: Body
public var body: some View {
switch person.healthy {
case true:
Text("Healthy")
case false:
Text("Not Healthy")
}
}
//MARK: Init
init(person: Binding<Person>) {
self._person = person
}
}
The error I get is Cannot find '$person' in scope. I understand that the #Binding part of the variable is not in scope while the ForEach loop is executing. I'm looking for advice on a different pattern to accomplish #Binding objects to views in a List in SwiftUI.
The SwiftUI way would be something like this:
// struct instead of class
struct Person: Identifiable {
let id: UUID = UUID()
var healthy: Bool = true
}
// class publishing an array of Person
class GroupOfPeople: ObservableObject {
#Published var people: [Person] = [
Person(), Person(), Person()
]
}
struct GroupListView: View {
// instantiating the class
#StateObject var group: GroupOfPeople = GroupOfPeople()
var body: some View {
List {
// now you can use the $ init of ForEach
ForEach($group.people) { $person in
PersonView(person: $person)
}
}
}
}
struct PersonView: View {
#Binding var person: Person
var body: some View {
HStack {
// ternary instead of switch
Text(person.healthy ? "Healthy" : "Not Healthy")
Spacer()
// Button to change, so Binding makes some sense :)
Button("change") {
person.healthy.toggle()
}
}
}
}
You don't need Binding. You need ObservedObject.
for anyone still wondering... it looks like this has been added
.onContinuousHover(perform: { phase in
switch phase {
case .active(let location):
print(location.x)
case .ended:
print("ended")
}
})

Trying to Update Previous View on Form Submission SwiftUI

I am working on a project where users will be able to enter their stock trades (lots) and later view some statistics. My problem is that when going from the list view to a detail edit view and pressing save, the list view doesn't get notified of this change (neither does the home view)
I could always try to pass the previous view models down to the edit view to invalidate them but this seems like a hack to me, so I am wondering if I am missing some important piece of SwiftUI.
Here is the code for the List View
struct StockView: View {
#ObservedObject var vm: StockViewModel
init(_ symbol: String) {
vm = StockViewModel(symbol)
}
var body: some View {
List {
ForEach(vm.lots, id: \.id) { lot in
NavigationLink(destination: LotEditView(lot.id)) {
...
}
}
}.navigationTitle("My Lots")
.listStyle(PlainListStyle())
}
}
and the code for the Edit View
struct LotEditView: View {
#Environment(\.presentationMode) var mode
#ObservedObject var vm: LotEditViewModel
init(_ id: UUID) {
vm = LotEditViewModel(id)
}
var body: some View {
VStack {
Form {
...
}
}.navigationTitle("Edit Lot")
.toolbar {
Button("Done") {
vm.save()
mode.wrappedValue.dismiss()
}
}
}
}
The List ViewModel is initialized with a stock name and then uses that to get the lots from a mock database, then the id of each of these lots is passed to the Edit ViewModel. The vm.save() simply updates the database which in this case is just an array which I've confirmed is being updated.
So what you want to do is to have one instance of that Mock database and inject it into viewModels.
I think the easiest way here would be to make this mock database as a #StateObject. Create it in the inital view(might be homeView or even Appdelegate) and pass it on as environmentObject to other views.
struct StockView: View {
#ObservedObject var vm: StockViewModel
#StateObject var dbService = MyMockDatabase()
init(_ symbol: String) {
vm = StockViewModel(dbService: dbService, symbol: symbol)
}
var body: some View {
List {
ForEach(vm.lots, id: \.id) { lot in
NavigationLink(destination: LotEditView(lot.id).environmentObject(dbService)) {
...
}
}
}.navigationTitle("My Lots")
.listStyle(PlainListStyle())
}
}
And in your next screen use it as:
struct LotEditView: View {
#Environment(\.presentationMode) var mode
#EnvironmentObject var dbService: MyMockDatabase
#ObservedObject var vm: LotEditViewModel
init(_ id: UUID) {
vm = LotEditViewModel(dbService: dbService, id: id)
}
var body: some View {
VStack {
Form {
...
}
}.navigationTitle("Edit Lot")
.toolbar {
Button("Done") {
vm.save()
mode.wrappedValue.dismiss()
}
}
}
}
MockArray would look something like:
class MyMockDatabase: ObservableObject {
#Published var array = [...]
}

How to share published model between two view models in SwiftUI?

I am trying to access the same shared model within two different view models. Both associated views need to access the model within the view model and need to edit the model. So I can't just use the EnvironmentObject to access the model.
I could pass the model to the view model through the view, but this wouldn't keep both model versions in sync. Is there something that could work like binding? Because with binding I can access the model but then it won't publish the changes in this view.
Simplified Example:
First view in NavigationView with adjacent view two:
struct ContentView1: View {
#StateObject var contentView1Model = ContentView1Model()
var body: some View {
NavigationView {
VStack{
TextField("ModelName", text: $contentView1Model.model.name)
NavigationLink(destination: ContentView2(model: contentView1Model.model)){
Text("ToContentView2")
}
}
}
}
}
class ContentView1Model: ObservableObject {
#Published var model = Model()
//Some methods that modify the model
}
Adjacent view 2 which needs access to Model:
struct ContentView2: View {
#StateObject var contentView2Model: ContentView2Model
init(model: Model) {
self._contentView2Model = StateObject(wrappedValue: ContentView2Model(model: model))
}
var body: some View {
TextField("ModelName", text: $contentView2Model.model.name)
}
}
class ContentView2Model: ObservableObject {
#Published var model: Model // Tried binding but this won't publish the changes.
init(model: Model) {
self.model = model
}
}
Model:
struct Model {
var name = ""
}
Thanks for the help!
Ok, Model is struct, so it is just copied when you pass it from ContentViewModeltoContentView2Model` via
ContentView2(model: contentView1Model.model)
This is the case when it is more preferable to have model as standalone ObservableObject, so it will be passed by reference from one view model into another.
class Model: ObservableObject {
#Published var name = ""
}
and then you can inject it and modify in any needed subview, like
struct ContentView1: View {
#StateObject var contentView1Model = ContentView1Model()
var body: some View {
NavigationView {
VStack{
ModelEditView(model: contentView1Model.model) // << !!
NavigationLink(destination: ContentView2(model: contentView1Model.model)){
Text("ToContentView2")
}
}
}
}
}
struct ContentView2: View {
#StateObject var contentView2Model: ContentView2Model
init(model: Model) {
self._contentView2Model = StateObject(wrappedValue: ContentView2Model(model: model))
}
var body: some View {
ModelEditView(model: contentView2Model.model) // << !!
}
}
struct ModelEditView: View {
#ObservedObject var model: Model
var body: some View {
TextField("ModelName", text: $model.name)
}
}

Embedded #Binding does not changing a value

Following this cheat sheet I'm trying to figure out data flow in SwiftUI. So:
Use #Binding when your view needs to mutate a property owned by an ancestor view, or owned by an observable object that an ancestor has a reference to.
And that is exactly what I need so my embedded model is:
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
init(values: [String] = []) {
self.values = values
}
}
and my View has two fields:
struct SimpleModelView: View {
#Binding var model: SimpleModel
#Binding var strings: [String]
var body: some View {
VStack {
HStack {
Text(self.strings[0])
TextField("name", text: self.$strings[0])
}
HStack {
Text(self.model.values[0])
EmbeddedView(strings: self.$model.values)
}
}
}
}
struct EmbeddedView: View {
#Binding var strings: [String]
var body: some View {
VStack {
TextField("name", text: self.$strings[0])
}
}
}
So I expect the view to change Text when change in input field will occur. And it's working for [String] but does not work for embedded #Binding object:
Why it's behaving differently?
Make property published
class SimpleModel: Identifiable, ObservableObject {
#Published var values: [String] = []
and model observed
struct SimpleModelView: View {
#ObservedObject var model: SimpleModel
Note: this in that direction - if you introduced ObservableObject then corresponding view should have ObservedObject wrapper to observe changes of that observable object's published properties.
In SimpleModelView, try changing:
#Binding var model: SimpleModel
to:
#ObservedObject var model: SimpleModel
#ObservedObjects provide binding values as well, and are required if you want state changes from classes conforming to ObservableObject

Using Identifiable set of objects in List View in SwiftUI

I have a DataBase handled by DataCore. I am trying to retrieve any object of "assignment" and insert it to a View List. The assignment class itself is identifiable but I am getting an error while trying to create the List in the View :
Initializer 'init(_:id:rowContent:)' requires that 'Set<NSManagedObject>' conform to 'RandomAccessCollection'
Is the set itself is not identifiable even though the objects are identifiable ? How can I present all the objects in the set inside the View's list ?
The view:
import SwiftUI
struct AssignmentList: View {
#ObservedObject var assignmentViewModel = assignmentViewModel()
var body: some View {
VStack {
NavigationView {
//**The error is in the following line : **
List(assignmentViewModel.allAssignments, id: \.self) { assignment in
AssignmentRow(assignmentName: assignment.assignmentName, notes: assignment.notes) //This view works by itself and just present the data as text under HStack
}
.navigationBarTitle(Text("Assignments"))
}
Button(action: {
self.assignmentViewModel.retrieveAllAssignments()
}) {
Text("Retrieve")
}
}
}
}
This is the assignment class:
import Foundation
import CoreData
extension Assignment: Identifiable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Assignment> {
return NSFetchRequest<Assignment>(entityName: "Assignment")
}
#NSManaged public var id: UUID?
#NSManaged public var assignmentName: String?
#NSManaged public var notes: String?
}
This is the ViewModel that connects to the view using binding:
class AssignmentViewModel : ObservableObject
{
private var assignmentModel = AssignmentModel()
/*AssignmentModel is a different class, we assume all methods working correctly and it's not a part of the question.*/
#Published var allAssignments : Set<Assignment>
init()
{
allAssignments=[]
}
func retrieveAllAssignment()
{
allAssignments=assignmentModel.retrieveAllAssignments()
}
}
Try the following
List(Array(assignmentViewModel.allAssignments), id: \.self) { assignment in

Resources