ForEach NavigationLink with State and Binding - ios

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.

Related

how do I get a binding of computed property in SwiftUI?

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

SwiftUI Dependency Injection

I have a SwiftUI app, its a tab based app
struct Tab_View: View {
var body: some View {
TabView{
Main1_View().tabItem {
Text("Blah 1")
Image("TabBar1")
}
Main2_View().tabItem {
Text("Blah 2")
Image("TabBar2")
}
}
}
}
Each view has its own view controller
struct Main1_View: View {
#ObservedObject var viewModel: Main1_ViewModel = Main1_ViewModel()
var body: some View {
VStack(spacing:0){
<<< VIEW CODE >>>
}
}
}
ViewModel Example
class Main1_ViewModel: ObservableObject {
#ObservableObject var settings: GameSettings
func Randomise(){
dataSource = settings.selectedFramework;
}
}
The class GameSettings is used by multiple viewmodels, is an ObservableObject, same instance of the class everywhere.
My background is C# and using CastleWindsor for dependency injection.
My Question: Is there a SwiftUI equivalent to pass around an instance of GameSettings?
Because of your requirement to use an ObservableObject (GameSettings) inside another ObservableObject (the view model) and use dependency injection, things are going to be a little bit convoluted.
In order to get a dependency-injected ObservableObject to a View, the normal solution is to use #EnvironmentObject. But, then you'll have to pass the object from the view to its view model. In my example, I've done that in onAppear. The side effect is that the object is an optional property on the view model (you could potentially solve this by setting a dummy initial value).
Because nested ObservableObjects don't work out-of-the box with #Published types (which work with value types, not reference types), you'll want to make sure that you use objectWillChange to pass along any changes from GameSettings to the parent view model, which I've done using Combine.
The DispatchQueue.main.asyncAfter part is there just to show that the view does in fact update when the value inside GameSettings is changed.
(Note, I've also changed your type names to use the Swift conventions of camel case)
import SwiftUI
import Combine
struct ContentView: View {
#StateObject private var settings = GameSettings()
var body: some View {
TabView {
Main1View().tabItem {
Text("Blah 1")
Image("TabBar1")
}
Main2View().tabItem {
Text("Blah 2")
Image("TabBar2")
}
}.environmentObject(settings)
}
}
struct Main1View: View {
#EnvironmentObject var settings: GameSettings
#StateObject var viewModel: Main1ViewModel = Main1ViewModel()
var body: some View {
VStack(spacing:0){
Text("Game settings: \(viewModel.settings?.myValue ?? "no value")")
}.onAppear {
viewModel.settings = settings
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
settings.myValue = "changed"
}
}
}
}
class GameSettings : ObservableObject {
#Published var myValue : String = "Test"
}
class Main1ViewModel: ObservableObject {
private var cancellable : AnyCancellable?
var settings: GameSettings? {
didSet {
print("Running init on Main1ViewModel")
self.objectWillChange.send()
cancellable = settings?.objectWillChange.sink(receiveValue: { _ in
print("Sending...")
self.objectWillChange.send()
})
}
}
}
struct Main2View : View {
var body: some View {
Text("Hello, world!")
}
}
Dependency injection, DI is the practice of providing an object with the other objects it depends on rather than creating them internally.
My Opinion
For SwiftUI you can use #EnvironmentObject. #EnvironmentObject and the View Model Factory both provide a clean solution to this.
Check this tutorial
https://mokacoding.com/blog/swiftui-dependency-injection/

SwiftUI / Combine Pass data between two models

I have question regarding how to pass data between two models.
struct SettingsCell: View {
#State var isOn: Bool
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject var model = SettingsModel()
var body: some View {
List {
SettingsCell(isOn: model.someValue)
}
}
}
So i want to pass isOn state from cell, to main model, and react there. Send requests for example.
You need to declare isOn as #Binding in SettingsCell.
#State should only be used for properties initialised inside the View itself and must always be private. If you want to pass in a value that should update the View whenever it changes, but the value is created outside the View, you need to use Binding.
Another really important thing to note is that #ObservedObjects must always be injected into Views, you must not initialise them inside the view itself. This is because whenever an #ObservedObject is updated, it updates the view itself, so if you initialised the object inside the view, whenever the object updates the view, the view would create a new #ObservedObject and hence your changes wouldn't be persisted from the view to the model.
If you are targeting iOS 14 and want to create the model inside the view, you can use #StateObject instead.
struct SettingsCell: View {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
self._isOn = isOn
}
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject private var model: SettingsModel
init(model: SettingsModel) {
self.model = model
}
var body: some View {
List {
SettingsCell(isOn: $model.someValue)
}
}
}
Binding is used in cases where the data is "owned" by a parent view - i.e. the parent holds the source of truth - and needs the child view to update it:
struct SettingsCell: View {
#Binding var isOn: Bool // change to Binding
var body: some View {
Toggle(name, isOn: $isOn)
}
}
struct SettingsView: View {
// unrelated, but better to use StateObject
#StateObject var model = SettingsModel()
var body: some View {
List {
// pass the binding by prefixing with $
SettingsCell(isOn: $model.someValue)
}
}
}

Core Data list relationship of Parent Object

i have two Core-Data entities. One called "Program" and one called "Exercise". Program has a one-to-many relationship to Exercise.
I add Exercises to a Program in another view.
I have one view with all the programs listed and a navigation link which leads to this view.
In this view I want to list all the exercises I assigned earlier to the program. However if I try this code, I get the error message: Generic struct 'ForEach' requires that 'Exercise' conform to 'RandomAccessCollection'.
I am new to Core Data Relationships and think I might have forgotten to declare something in the CoreDataClass File. The Problem is that I cant find any other solutions to this question.
struct ProgramView: View {
#ObservedObject var ProgramDetail: Program
var body: some View {
List {
ForEach(ProgramDetail.exercise!, id: \.self) { exercise in
Text(exercise.name!)
}
}
This generates the List of all Programs:
struct ProgramList: View {
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Program.date, ascending: false)]) var programFetch: FetchedResults<Program>
var body: some View {
NavigationView {
List {
ForEach(programFetch, id: \.self) { program in
NavigationLink(destination: ProgramView(ProgramDetail: program)) {
Text(program.title!)
}
}
}.navigationBarItems(
trailing:
NavigationLink(destination: CreateProgram()) {
Image(systemName: "plus.circle").font(.system(size: 25))
})
}
}
Here is a screenshot of the Interface?:
Interface
The error message is explaining that you must have a Random Access Collection, so that ForEach can iterate through the collection.
For example, Swift's Array conforms to RandomAccessCollection protocol, so you could use an array.
I'd suggest that you read up on collections, either in the Swift documentation or on Apple's Developer website.
There are probably a few ways to achieve a solution, but perhaps the easiest method is as follows...
struct ProgramView: View {
#ObservedObject var programDetail: Program
#FetchRequest(entity: Exercise.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Exercise.name, ascending: true)],
) var allExercises: FetchedResults<Exercise>
var body: some View {
List {
// in this line you create an array of fetched results
// filtered to contain only exercises for programDetail
let exercisesForProgram = allExercises.filter { $0.program == programDetail }
ForEach(exercisesForProgram) { exercise in
Text(exercise.name!)
}
}
}
}
Assuming you haven't changed the Arrangement of your To Many relationship to Ordered, ProgramDetail.exercise is an NSSet?.
You could define a computed property to transform it to an Array of Exercise:
extension Program {
var exerciseArray: [Exercise] {
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
return exercise?.sortedArray(using: [sortDescriptor]) as? [Exercise] ?? [Exercise]()
}
}
In ProgramView now you could then create your List using the Array as follows:
List {
ForEach(programDetail.exerciseArray, id: \.self) { exercise in
Text("\(exercise.name ?? "")")
}
}
In case exercise is an Ordered relationship, then ProgramDetail.exercise will be an NSOrderedSet?.
In this case the definition of exerciseArray is even simpler:
var exerciseArray: [Exercise] {
return exercise?.array as? [Exercise] ?? [Exercise]()
}

Cannot assign to property: 'self' is immutable while assigning value to #ObservedObject

struct ParentView: View {
#ObservedObject var selectedFruit:Fruit
#State var showingSheet = false
var body: some View {
VStack {
HStack(spacing:0.6) {
ForEach(self.viewModel.fruits, id: \.id) { record in
Text(record).onTapGesture {
self.selectedFruit = Fruit() //Cannot assign to property: 'self' is immutable
self.showingSheet.toggle()
}
}
}
}.sheet(isPresented: $showingSheet,onDismiss: {
print("reload?")
}) {
DetailView(fruit: self.selectedFruit)
}
}
}
struct DetailView: View {
#ObservedObject var fruit: Fruit
var body: some View {
Text(fruit.text)
}
}
When I try to assign value to the observable selectedFruit from ParentView, I get the following compile error:
Cannot assign to property: 'self' is immutable
But if I replace #ObservedObject with #State, it works. How can I make it work with #ObservedObject? I'm worried, if I go with #State, the update I make on that object on DetailView will not trigger update on ParentView.
Can someone help me on this?
Even if you could mutate self, you don't want to do this.
An #ObservedObject is a reference to an object instance that is injected into your view, typically from a superview, or if ParentView is your root view, from your scene delegate.
By assigning a new value to selectedFruit you would be changing the object referred to in this view. but the parent view would still refer to the original object.
Your selectedFruit should be an #Published property of your viewModel. You would then have self.viewModel.selectedFruit = record in your onTapGesture.
This Paul Hudson article provides a good overview of the difference between an observable object, a state property and the environment.

Resources