I have a search bar in a list that filters an array of students and stores them in a computed property called searchResults of type [Student], I want to pass a binding of each filtered student in the list to another view that accepts a binding.
If I tried to pass a binding of searchResults in the list, it says: Cannot find '$searchResults' in scope, I think because searchResults is not a state, and I cannot make it a state since it is a computed property.
struct StudentsSection: View {
#Binding var course: Course
#State var searchText = ""
var searchResults: [Student] { course.students.filter { $0.fullName.contains(searchText) } }
var body: some View {
List($searchResults) { $student in // error: Cannot find '$searchResults' in scope
StudentRow(student: $student, course: $course)
}
.searchable(text: $searchText)
}
}
I want to achieve the same result as the code below:
struct StudentsSection: View {
#Binding var course: Course
var body: some View {
List($course.students) { $student in // no errors
StudentRow(student: $student, course: $course)
}
}
}
Related
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")
}
})
I am trying to create a binding to a FetchedResults item, error is on $items[i]:
struct NavView: View {
#Binding var item : Card
...
}
struct ContentView: View {
private var items: FetchedResults<Card>
var body: some View {
List {
ForEach(items.indices, id:\.self) { i in
NavigationLink {
NavView(item: $items[i])
}
}
}
}
}
Changing the Binding to ObservedObject compiles and seems to work properly, although I feel like I'm violating single source of truth policy by creating a new ObservedObject.
struct NavView: View {
#ObservedObject var item : Card
...
}
struct ContentView: View {
private var items: FetchedResults<Card>
var body: some View {
List {
ForEach(items) { item in
NavigationLink {
NavView(item: item)
}
}
}
}
}
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
I'm building an app that fetches data from the The Movie Database API and presents a List of movies by genre.
In my view model I have a function that calls the api and makes the data available via an observable object.
import Foundation
class MovieListViewModel: ObservableObject {
#Published var movies = [Movie]()
private var fetchedMovies = [MovieList]()
var page = 1
func fetchMovies(genre: Int) {
WebService().getMoviesByGenre(genre: genre, page: page) { movie in
if let movie = movie {
self.fetchedMovies.append(movie)
for movie in movie.movies {
self.movies.append(movie)
}
}
}
page += 1
print(page)
}
}
Inside the view, I'm using onAppear to call the function and pass the genre id (Int) received from the previous view. This all works as expected.
import SwiftUI
struct MovielistView: View {
#ObservedObject private var movielistVM = MovieListViewModel()
var genre: GenreElement
var body: some View {
List {
...
}
}.onAppear {
self.movielistVM.fetchMovies(genre: self.genre.id)
}
.navigationBarTitle(genre.name)
}
}
The problem is because I'm using onAppear, the api is being hit/the view refreshed every time the user navigates back to the view.
Ideally, I'd like to call the api once during the initialising of the view model but because the genre property hasn't been initialised yet, it can't pass the parameter to the view model.
I'v tried this (below) but get the error 'self' used before all stored properties are initialized
import SwiftUI
struct MovielistView: View {
#ObservedObject private var movielistVM = MovieListViewModel()
var genre: GenreElement
init() {
movielistVM.fetchMovies(genre: genre.id) // Error here
}
var body: some View {
List {
...
}
.navigationBarTitle(genre.name)
}
}
Any help would be appreciated.
Add a genre parameter for the init and initialize the genre property of MovielistView before using it:
struct MovielistView: View {
var genre: GenreElement
//...
init(genre: GenreElement) {
self.genre = genre
movielistVM.fetchMovies(genre: genre.id)
}
//...
}
I'm trying to generate a ForEach with a NavigationLink and use State and Binding to pass some entity around:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(
entity: MyEntity.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \MyEntity.name, ascending: true)
]
) var entries: FetchedResults<MyEntity>
var body: some View {
NavigationView {
List {
ForEach(entries, id: \.self) { (entry: MyEntity) in
NavigationLink(destination: DetailView(myEntry: $entry)) {
Text(entry.name!)
}
}.
}
}
}
}
And then the following view:
struct DetailView: View {
#Binding var myEntry: MyEntity
var body: some View {
Text(myEntry.name!)
}
}
The problem is I can not pass the value to Detail view since the error:
Use of unresolved identifier '$entry'
What is wrong here and how to solve this?
If I just have a simple #State its no problem to pass it via the binding, but I want/need to use it in the ForEach for the FetchedResults
EDIT: If I remove the $ I get Cannot convert value of type 'MyEntity' to expected argument type 'Binding<MyEntity>'
EDIT2: The purpose is to pass some object to DetailView and then pass it back later to ContentView
Use the following in ForEach
ForEach(entries, id: \.self) { entry in
NavigationLink(destination: DetailView(myEntry: entry)) {
Text(entry.name!)
}
}
and the following in DetailView
struct DetailView: View {
var myEntry: MyEntity
var body: some View {
Text(myEntry.name!)
}
}
The MyEntity is class, so you pass object by reference and changing it in DetailView you change same object.
FetchedResults<MyEntity> entities does not conform to Binding or DynamicProperty. For the $ symbol to workout must conform to Binding.
You can make a FetchedResultsController in an ObservableObject to get the same functionality as an #FetchRequestand have the ability to pass the values in an EnvironmentObject or ObservedObject.