SwiftUI core data, grouped list fetch result - swift

using core data im storing some airport and for every airport i'm storing different note
I have created the entity Airport and the entity Briefing
Airport have 1 attribute called icaoAPT and Briefing have 4 attribute category, descript, icaoAPT, noteID
On my detailsView I show the list all the noted related to that airport, I managed to have a dynamic fetch via another view called FilterList
import SwiftUI
import CoreData
struct FilterLIst: View {
var fetchRequest: FetchRequest<Briefing>
#Environment(\.managedObjectContext) var dbContext
init(filter: String) {
fetchRequest = FetchRequest<Briefing>(entity: Briefing.entity(), sortDescriptors: [], predicate: NSPredicate(format: "airportRel.icaoAPT == %#", filter))
}
func update(_ result : FetchedResults<Briefing>) ->[[Briefing]]{
return Dictionary(grouping: result) { (sequence : Briefing) in
sequence.category
}.values.map{$0}
}
var body: some View {
List{
ForEach(update(self.fetchRequest.wrappedValue), id: \.self) { (section : Briefing) in
Section(header: Text(section.category!)) {
ForEach(section, id: \.self) { note in
Text("hello")
/// Xcode error Cannot convert value of type 'Text' to closure result type '_'
}
}
}
}
}
}
on this view I'm try to display all the section divided by category using the func update...
but Xcode give me this error , I can't understand why..Cannot convert value of type 'Text' to closure result type '_'
fore reference I list below my detailsView
import SwiftUI
struct DeatailsView: View {
#Environment(\.managedObjectContext) var dbContext
#Environment(\.presentationMode) var presentation
#State var airport : Airport
#State var note = ""
#State var noteTitle = ["SAFTY NOTE", "TAXI NOTE", "CPNOTE"]
#State var notaTitleSelected : Int = 0
#State var notaID = ""
var body: some View {
Form{
Section(header: Text("ADD NOTE Section")) {
TextField("notaID", text: self.$notaID)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("add Note descrip", text: self.$note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Picker(selection: $notaTitleSelected, label: Text("Class of Note")) {
ForEach(0 ..< noteTitle.count) {
Text(self.noteTitle[$0])
}
}
HStack{
Spacer()
Button(action: {
let nota = Briefing(context: self.dbContext)
nota.airportRel = self.airport
nota.icaoAPT = self.airport.icaoAPT
nota.descript = self.note
nota.category = self.noteTitle[self.notaTitleSelected]
nota.noteID = self.notaID
do {
try self.dbContext.save()
debugPrint("salvato notazione")
} catch {
print("errore nel salva")
}
}) {
Text("Salva NOTA")
}
Spacer()
}
}
Section(header: Text("View Note")) {
FilterLIst(filter: airport.icaoAPT ?? "NA")
}
}
}
}
thanks for the help

This is because you try to iterate over a single Briefing object and a ForEach loop expects a collection:
List {
ForEach(update(self.fetchRequest.wrappedValue), id: \.self) { (section: Briefing) in
Section(header: Text(section.category!)) {
ForEach(section, id: \.self) { note in // <- section is a single object
Text("hello")
/// Xcode error Cannot convert value of type 'Text' to closure result type '_'
}
}
}
}
I'd recommend you to extract the second ForEach to another method for clarity. This way you can also be sure you're passing the argument of right type ([Briefing]):
func categoryView(section: [Briefing]) -> some View {
ForEach(section, id: \.self) { briefing in
Text("hello")
}
}
Note that the result of your update method is of type [[Briefing]], which means the parameter in the ForEach is section: [Briefing] (and not Briefing):
var body: some View {
let data: [[Briefing]] = update(self.fetchRequest.wrappedValue)
return List {
ForEach(data, id: \.self) { (section: [Briefing]) in
Section(header: Text("")) { // <- can't be `section.category!`
self.categoryView(section: section)
}
}
}
}
This also means you can't write section.category! in the header as the section is an array.
You may need to access a Briefing object to get a category:
Text(section[0].category!)
(if you're sure the first element exists).
For clarity I specified types explicitly. It's also a good way to be sure you always use the right type.
let data: [[Briefing]] = update(self.fetchRequest.wrappedValue)
However, Swift can infer types automatically. In the example below, the data will be of type [[Briefing]]:
let data = update(self.fetchRequest.wrappedValue)

Related

Call viewModel in Picker selection

I have an observedObject that calls to a viewModel (variable vm). The viewModel contains all the user's information already saved. I want to use the value that the user already saved as the selection of the picker. If no data exists yet for the user (in this case their height, I just leave the selection as an empty string.
I'm getting a bunch of issues with my current approach and can't seem to figure it out. The errors include:
Cannot convert value of type 'String' to expected argument type
'Binding'
Add explicit 'self.' to refer to mutable property of
'PersonalSettingsView'
Chain the optional using '?' to access member 'weight' only for
non-'nil' base values
struct PersonalSettingsView: View {
#ObservedObject var vm = DashboardLogic() //call to viewModel
#State private var height: String = ""
private var heightOptions = ["4'0", "4'1","4'2","4'3", "4'4", "4'5","4'6","4'7","4'8","4'9","4'10","4'11","5'0","5'1", "5'2", "5'3", "5'4", "5'5","5'6","5'7","5'8","5'9","5'10","5'11","6'0","6'1","6'2","6'3","6'4","6'5","6'6","6'7","6'8","6'9","6'10","6'11","7'0","7'1","7'2"]
var body: some View {
NavigationView{
Form{
Section(header: Text("Health Stats")
.foregroundColor(.black)
.font(.body)
.textCase(nil)){
HStack{
Picker(selection: $vm.userModel?.height ?? "", label: Text("Height")){
ForEach(heightOptions, id: \.self){ height in
Text(height)
}
}
}
}
}
}
}
}
you could try this approach, it's a bit convoluted, but it works for me:
HStack{
if vm.userModel != nil {
Picker(selection: Binding<String> (
get: { vm.userModel!.height },
set: { vm.userModel!.height = $0 }),
label: Text("Height")) {
ForEach(heightOptions, id: \.self){ height in
Text(height)
}
}
}
}
Also add this to the Form:
.onAppear {
if vm.userModel == nil { vm.userModel = User(height: "") }
}
If vm.userModel is nil, there is no user to set the height of,
no matter what you select.
So you need to create a user when vm.userModel is nil.
Then you can make a selection for that empty user. You can do this in
.onAppear{ ... } for example.

Swift - Problem with ForEach statement while structure is empty

I have a forEach statement that illiterates through a struct containing meal entries. If the struct is empty, I would like to display alternative Text.
My issue is when the array is empty, I receive the following error when compiling
Cannot convert value of type 'String' to expected argument type 'Meal'
let mealEntrysDinner: [String] = [
]
Section(header:Text("Dinner")){
ForEach(mealEntrysDinner, id: \.self){ meal in
if(mealEntrysDinner.isEmpty == true){
Text("Arr Empty")
}
else{
EntryRow(meal:meal)
}
}
}
Meal Structure below:
struct Meal: Identifiable, Hashable{
var id = UUID()
var mealName: String
var calories: String
var quantity: Int
var amount: String
var protein: String
}
EntryRow
struct EntryRow: View {
var meal: Meal
var body: some View {
HStack{
VStack(alignment: .leading){
Text(meal.mealName)
Text(meal.amount)
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
Text(meal.calories)
}
}
}
There are two issues.
First, it looks like you want mealEntrysDinner to be [Meal], not [String] -- otherwise, as you've discovered, the types don't match and you can't use EntryRow.
Secondly, you have something out-of-order with your if clause -- it should be outside the ForEach:
Section(header:Text("Dinner")){
if mealEntrysDinner.isEmpty {
Text("Arr Empty")
} else {
ForEach(mealEntrysDinner) { meal in
EntryRow(meal:meal)
}
}
}
This way, the ForEach only gets displayed if mealEntrysDinner has elements.

SwiftUI memory leak

I'm getting a weird memory leak in SwiftUI when using List and id: \.self, where only some of the items are destroyed. I'm using macOS Monterey Beta 5.
Here is how to reproduce:
Create a new blank SwiftUI macOS project
Paste the following code:
class Model: ObservableObject {
#Published var objs = (1..<100).map { TestObj(text: "\($0)")}
}
class TestObj: Hashable {
let text: String
static var numDestroyed = 0
init(text: String) {
self.text = text
}
static func == (lhs: TestObj, rhs: TestObj) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
deinit {
TestObj.numDestroyed += 1
print("Deinit: \(TestObj.numDestroyed)")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
List(model.objs, id: \.self) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
}
}
Run the app, and press the "Remove half" button. Keep pressing it until all the items are gone. However, if you look at the console, you'll see that only 85 items have been destroyed, while there were 99 items. The Xcode memory graph also supports this.
This seems to be caused by the id: \.self line. Removing it and switching it out for id: \.text fixes the problem.
However the reason I use id: \.self is because I want to support multiple selection, and I want the selection to be of type Set<TestObj>, instead of Set<UUID>.
Is there any way to solve this issue?
If you didn't have to use selection in your List, you could use any unique & constant id, for example:
class TestObj: Hashable, Identifiable {
let id = UUID()
/* ... */
}
And then your List with the implicit id: \.id:
List(model.objs) { obj in
Text(obj.text)
}
This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.
But you need selection in List, so see more below about how to achieve that.
To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.
Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:
extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}
Change your Model object to this:
class Model: ObservableObject {
#Published var objs: OrderedDictionary<UUID, TestObj>
init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}
And rather than model.objs you'll use model.objs.values, but that's it!
See full demo code below to test the selection:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selection: Set<UUID> = []
var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}
print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}
Result:

Simple SwiftUI CRUD using structs rather than classes?

I have a complex data structure which uses value types (structs and enums), and I'm facing major issues getting basic CRUD to work. Specifically:
How best to "Re-bind" a value in a ForEach for editing by a child view
How to remove/delete a value
Rebinding
If I have an array of items as #State or #Binding, why isn't there a simple way to bind each element to a view? For example:
import SwiftUI
struct Item: Identifiable {
var id = UUID()
var name: String
}
struct ContentView: View {
#State var items: [Item]
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $item) // 🛑 Cannot find '$item' in scope
}
}
}
}
Workaround
I've been able to work around this by introducing a helper function to find the correct index for the item within a loop:
struct ContentView: View {
#State var items: [Item]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $items[index(of: item)].name)
}
}
}
}
However, that feels clunky and possibly dangerous.
Deletion
A far bigger issue: how are you supposed to correctly delete an element? This sounds like such a basic question, but consider the following:
struct ContentView: View {
#State var items: [Item]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $items[index(of: item)].name)
Button( action: {
items.remove(at: index(of: item))
}) {
Text("Delete")
}
}
}
}
}
Clicking the "Delete" button on the first few items works as expected, but trying to Delete the last item results in Fatal error: Index out of range...
My particular use case doesn't map to a List, so I can't use the deletion helper there.
Reference types
I know that reference types make much of this easier, especially if they can conform to #ObservableObject. However, I have a massive, nested, pre-existing value type which is not easily converted to classes.
Any help would be most appreciated!
Update: Suggested solutions
Deleting List Elements from SwiftUI's list: The accepted answer proposes a complex custom binding wrapper. Swift is powerful, so it's possible to solve many problems with elaborate workarounds, but I don't feel like an elaborate workaround should be necessary to have a list of editable items.
Mark Views as "deleted" using State or a private variable, then conditionally hide them, to avoid out-of-bounds errors. This can work, but feels like a hack, and something that should be handled by the framework.
I confirm that more appropriate approach for CRUD is to use ObservableObject class based view model. And an answer provided by #NewDev in comments is a good demo for that approach.
However if you already have a massive, nested, pre-existing value type which is not easily converted to classes., it can be solved by #State/#Binding, but you should think about what/when/and how update each view and in each order - that is the origin of all such index out of bounds on delete issues (and some more).
Here is demo of approach of how to break this update dependency to avoid crash and still use value types.
Tested based on your code with Xcode 11.4 / iOS 13.4 (SwiftUI 1.0+)
struct ContentView: View {
#State var items: [Item] = [Item(name: "Name1"), Item(name: "Name2"), Item(name: "Name3")]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
// separate dependent views as much as possible to make them as
// smaller/lighter as possible
ItemRowView(items: self.$items, index: self.index(of: item))
}
}
}
}
struct ItemRowView: View {
#Binding var items: [Item]
let index: Int
#State private var destroyed = false // internal state to validate self
var body: some View {
// proxy binding to have possibility for validation
let binding = Binding(
get: { self.destroyed ? "" : self.items[self.index].name },
set: { self.items[self.index].name = $0 }
)
return HStack {
if !destroyed { // safety check against extra update
TextField("name", text: binding)
Button( action: {
self.destroyed = true
self.$items.wrappedValue.remove(at: self.index)
}) {
Text("Delete")
}
}
}
}
}
Yes, it is not easy solution, but sometimes there are situations we need it.

Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'

I am getting the above error and couldn't figure out how to solve it. I have an array of objects that contain a boolean value, and need to show a toggle for each of these boolean.
Below is the code.
class Item: Identifiable {
var id: String
var label: String
var isOn: Bool
}
class Service: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var items: [Item] {
didSet {
didChange.send(())
}
}
}
struct MyView: View {
#ObservedObject var service: Service
var body: some View {
List {
ForEach(service.items, id: \.self) { (item: Binding<Item>) in
Section(header: Text(item.label)) { // Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'
Toggle(isOn: item.isOn) {
Text("isOn")
}
}
}
}
.listStyle(GroupedListStyle())
}
}
Use the #Published property wrapper in your Service class, rather than didChange, and iterate over the indices of service.items like so:
struct Item: Identifiable {
var id: String
var label: String
var isOn: Bool {
didSet {
// Added to show that state is being modified
print("\(label) just toggled")
}
}
}
class Service: ObservableObject {
#Published var items: [Item]
init() {
self.items = [
Item(id: "0", label: "Zero", isOn: false),
Item(id: "1", label: "One", isOn: true),
Item(id: "2", label: "Two", isOn: false)
]
}
}
struct MyView: View {
#ObservedObject var service: Service
var body: some View {
List {
ForEach(service.items.indices, id: \.self) { index in
Section(header: Text(self.service.items[index].label)) {
Toggle(isOn: self.$service.items[index].isOn) {
Text("isOn")
}
}
}
}
.listStyle(GroupedListStyle())
}
}
Update: Why use indices?
In this example, we need to get two things from each Item in the model:
The String value of the label property, to use in a Text view.
A Binding<Bool> from the isOn property, to use in a Toggle view.
(See this answer where I explain Binding.)
We could get the label value by iterating over the items directly:
ForEach(service.items) { (item: Item) in
Section(header: Text(item.label)) {
...
}
But the Item struct does not contain a binding. If you tried to reference Toggle(isOn: item.$isOn), you'd get an error: "Value of type 'Item' has no member '$isOn'."
Instead, the Binding is provided at the top level by the #ObservedObject property wrapper, meaning the $ has to come before service. But if we're starting from service, we'll need an index (and we cannot declare intermediate variables inside the ForEach struct, so we'll have to compute it inline):
ForEach(service.items) { (item: Item) in
Section(header: Text(item.label)) {
Toggle(isOn: self.$service.items[self.service.items.firstIndex(of: item)!].isOn) {
// This computes the index ^--------------------------------------^
Text("isOn")
}
}
}
Oh, and that comparison to find the index would mean Item has to conform to Equatable. And, most importantly, because we are looping over all items in the ForEach, and then again in the .firstIndex(of:), we have transformed our code from O(n) complexity to O(n^2), meaning it will run much more slowly when we have a large number of Items in the array.
So we just use the indices. Just for good measure,
ForEach(service.items.indices, id: \.self) { index in
is equivalent to
ForEach(0..<service.items.count, id: \.self) { index in