Specify Key Path in a List Swift UI - xcode

I have below structure , where GroceryData has details about the section as [GrocerySection] , this in turn has items to be displayed in the section as [Grocery].
struct GroceryData {
var showFavorites:Bool = false
var sections:[GrocerySection] = [GrocerySection(sectionName: "Common Items")]
}
struct GrocerySection {
var sectionName:String
var items:[Grocery] = [Grocery(id:1, name: "Milk", isFavorite: true, price: 1.99)]
}
struct Grocery: Identifiable,Hashable, Codable {
var id:Int
var name:String
var isFavorite:Bool
var price:Float
}
What should be the key path for the identifiable property.
struct ContentView: View {
var data:GroceryData
var body: some View {
List(data.sections, id: \GrocerySection.items.id) { (item) -> Text in
Text("Hello")
}
}
}

since you are dealing with sections, this may work:
List(data.sections, id: \.self.sectionName) { section in
Text("hello section \(section.sectionName)")
}
as long as the sectionName is unique, otherwise you can always add and id field.
If you want to loop over items you can try this:
List(data.sections, id: \.self.sectionName) { section in
ForEach(section.items) { item in
Text("\(item.name)")
}
}

You iterate list of sections so GrocerySection must be identifiable, like
struct GrocerySection: Identifiable {
var id = UUID() // << this
// var id: String { sectionName } // << or even this
var sectionName:String
var items:[Grocery] = [Grocery(id:1, name: "Milk", isFavorite: true, price: 1.99)]
}
then you can write
List(data.sections) { (section) -> Text in
Text("Hello")
}
or using keypath if every section name is unique, as
List(data.sections, id: \.sectionName) { (section) -> Text in
Text("Hello")
}

Related

Populating SwiftUI List with array elements that can be editied in TextEditor

I have a SwiftUI app that produces a List made from elements of an array of columns held in a struct.
I need the items in the row to be editable so I'm trying to use TextEditor but the bindings are proving difficult. I have a working prototype however the TextEditors are uneditable - I get the warning:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Here's a much shortened version of my code which produces the same problem:
import SwiftUI
struct Item: Identifiable {
#State var stringValue: String
var id: UUID = UUID()
}
struct ArrayContainer {
var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
}
struct ContentView: View {
#State var wrapperArray: ArrayContainer = ArrayContainer()
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach (Array(wrapperArray.items.enumerated()), id: \.offset) { index, item in
TextEditor(text: item.$stringValue)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I bind the TextEditor to the items stringValues within the items array?
TIA.
#State should only be used as a property wrapper on your View -- not on your model.
You can use a binding within ForEach using the $ syntax to get an editable version of the item.
struct Item: Identifiable {
var stringValue: String
var id: UUID = UUID()
}
struct ArrayContainer {
var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
}
struct ContentView: View {
#State var wrapperArray: ArrayContainer = ArrayContainer()
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach ($wrapperArray.items, id: \.id) { $item in
TextEditor(text: $item.stringValue)
}
}
}
}
}
This could be simplified further to avoid the ArrayContainer if you want:
struct ContentView: View {
#State var items: [Item] = [Item(stringValue: "one"), Item(stringValue: "two")]
var body: some View {
List {
Section(header: Text("Test List")) {
ForEach ($items, id: \.id) { $item in
TextEditor(text: $item.stringValue)
}
}
}
}
}

In SwiftUI, how do you delete a list row that is nested within two ForEach statements?

I have two nested ForEach statements that together create a list with section headers. The section headers are collected from a property of the items in the list. (In my example below, the section headers are the categories.)
I'm looking to enable the user to delete an item from the array underlying the list. The complexity here is that .onDelete() returns the index of the row within the section, so to correctly identify the item to delete, I also need to use the section/category, as explained in this StackOverflow posting. The code in this posting isn't working for me, though - for some reason, the category/territorie variable is not available for use in the onDelete() command. I did try converting the category to an index when iterating through the categories for the first ForEach (i.e., ForEach(0..< appData.sortedByCategories.count) { i in ), and that didn't work either. An added wrinkle is that I'd ideally like to use the appData.deleteItem function to perform the deletion (see below), but I couldn't get the code from the StackOverflow post on this issue to work even when I wasn't doing that.
What am I overlooking? The example below illustrates the problem. Thank you so much for your time and any insight you can provide!
#main
struct NestedForEachDeleteApp: App {
var appData = AppData()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(appData)
}
}
}
import Foundation
import SwiftUI
class AppData: ObservableObject {
static let appData = AppData()
#Published var items = [Item]()
// Create a dictionary based upon the category
var dictGroupedByCategory: [String: [Item]] {
Dictionary(grouping: items.sorted(), by: {$0.category})
}
// Sort the category-based dictionary alphabetically
var sortedByCategories: [String] {
dictGroupedByCategory.map({ $0.key }).sorted(by: {$0 < $1})
}
func deleteItem(index: Int) {
items.remove(at: index)
}
init() {
items = [
Item(id: UUID(), name: "Item 1", category: "Category A"),
Item(id: UUID(), name: "Item 2", category: "Category A"),
Item(id: UUID(), name: "Item 3", category: "Category B"),
Item(id: UUID(), name: "Item 4", category: "Category B"),
Item(id: UUID(), name: "Item 5", category: "Category C"),
]
} // End of init()
}
class Item: ObservableObject, Identifiable, Comparable, Equatable {
var id = UUID()
#Published var name: String
#Published var category: String
// Implement Comparable and Equable conformance
static func <(lhs: Item, rhs: Item) -> Bool {
return lhs.name < rhs.name
}
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.category < rhs.category
}
// MARK: - Initialize
init(id: UUID, name: String, category: String) {
// Initialize stored properties.
self.id = id
self.name = name
self.category = category
}
}
struct ContentView: View {
#EnvironmentObject var appData: AppData
var body: some View {
List {
ForEach(appData.sortedByCategories, id: \.self) { category in
Section(header: Text(category)) {
ForEach(appData.dictGroupedByCategory[category] ?? []) { item in
Text(item.name)
} // End of inner ForEach (items within category)
.onDelete(perform: self.deleteItem)
} // End of Section
} // End of outer ForEach (for categories themselves)
} // End of List
} // End of body View
func deleteItem(at offsets: IndexSet) {
for offset in offsets {
print(offset)
// print(category) // error = "Cannot find 'category' in scope
// let indexToDelete = appData.items.firstIndex(where: {$0.id == item.id }) // Error = "Cannot find 'item' in scope
appData.deleteItem(index: offset) // this code will run but it removes the wrong item because the offset value is the offset *within the category*
}
}
} // End of ContentView
I've figured out a solution to my question above, and am posting it here in case anyone else ever struggles with this same issue. The solution involved using .swipeActions() rather than .onDelete(). For reasons I don't understand, I could attach .swipeActions() (but not .onDelete()) to the Text(item.name) line of code. This made the "item" for each ForEach iteration available to the .swipeAction code, and everything else was very straightforward. The revised ContentView code now looks like this:
struct ContentView: View {
#EnvironmentObject var appData: AppData
var body: some View {
List {
ForEach(appData.sortedByCategories, id: \.self) { category in
Section(header: Text(category)) {
ForEach(appData.dictGroupedByCategory[category] ?? []) { item in
Text(item.name)
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
print(category)
print(item.name)
if let indexToDelete = appData.items.firstIndex(where: {$0.id == item.id }) {
appData.deleteItem(index: indexToDelete)
} // End of action to perform if there is an indexToDelete
} label: {
Label("Delete", systemImage: "trash.fill")
}
} // End of .swipeActions()
} // End of inner ForEach (items within category)
} // End of Section
} // End of outer ForEach (for categories themselves)
} // End of List
} // End of body View
} // End of ContentView

Save the selection state of the current selected item on a list

A macOS app that works with local files need to reload the files when it gets focused again. This is to load any new files that might have been placed there while the app was in the background.
The issue is that when I clear the list, reload the files (rebuilding the list), the first item is always selected, so if I was working on an item I'm being forced to select it again to continue working on it.
I tried keeping the current UUID of the selected note and passing that back, but I have no clue whether this is correct, or how to programmatically select an item in the list matching the UUID saved.
How do I keep the current selected item, and then go to it when I reload the data?
Code I tried:
struct DataItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
}
struct AllData: View {
#EnvironmentObject private var data: DataModel //array of DataItem's
#State var selectedItemId: UUID?
#State var currentItemId: UUID?
NavigationView {
List(data.prices.filter { searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText) }) { price in
NavigationLink(
destination: PriceView(price: price, text: price.text),
tag: price.id,
selection: $selectedItemId
) {
VStack(alignment: .leading) {
Text(getTitle(titleText: price.text)).font(.body).fontWeight(.bold)
}
.padding(.vertical, 10)
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
self.currentItemId = self.selectedItemId
if getCurrentSaveDirectory(for: "savedDirectory") != "" {
if !isDirectoryEmpty() {
data.price.removeAll()
loadFiles(dataModel: data)
self.selectedItemId = self.currentItemId
}
}
}
}
I think using the current UUID of the selected note as you do is a good idea.
Without all the code it is difficult to test my answer, however you could
try this approach, using a List selection variable and adding the simple code in onReceive,
such as this example code:
struct AllData: View {
#EnvironmentObject private var data: DataModel //array of DataItem's
#State var selectedItemId: UUID?
#State var currentItemId: UUID?
#State var listSelection: DataItem? // <--- here selection
var body: some View {
NavigationView {
List(data.prices.filter { searchText.isEmpty ? true : $0.text.localizedCaseInsensitiveContains(searchText) },
selection: $listSelection) { price in // <--- here selection
NavigationLink(
destination: PriceView(price: price, text: price.text),
tag: price.id,
selection: $selectedItemId
) {
VStack(alignment: .leading) {
Text(getTitle(titleText: price.text)).font(.body).fontWeight(.bold)
}
.padding(.vertical, 10)
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
self.currentItemId = self.selectedItemId
if getCurrentSaveDirectory(for: "savedDirectory") != "" {
if !isDirectoryEmpty() {
data.price.removeAll()
loadFiles(dataModel: data)
self.selectedItemId = self.currentItemId
// --- here ---
if let theItem = data.first(where: {$0.id == selectedItemId}) {
listSelection = theItem
}
}
}
}
}
}

SwiftUI : difficulty with dictionary to define a result list of search

I'll try to do one Search View.
I would like to save the result in a dictionary in order to create a list of result but xCode show me this error :
Cannot assign through subscript: 'self' is immutable
My code :
import SwiftUI
struct SearchListView: View {
#State var search: String = "test"
var stringDictionary: Dictionary = [Int: String]()
var body: some View
{
NavigationView
{
ForEach(chapterData) { chapter in
ForEach(chapter.lines) { line in
HStack {
if (self.search == line.text) {
HStack {
stringDictionary[0] = line.text
}
}
}
}
}
}
}
}
struct SearchListView_Previews: PreviewProvider {
static var previews: some View {
SearchListView(search: "test")
}
}
struct Chapter: Codable, Identifiable {
let id:Int
let lines: [Line]
}
struct Line: Codable, Identifiable {
let id: Int
let text: String
}

SwiftUI Picker with Enum Source Is Not Enabled

I'm trying to understand the new SwiftUI picker style, especially with data from a source other than an array. I have built a picker with an enum. I first made a simple app with only the picker and associated enum. This works as expected.
Strangely, when I copy and paste that code into another app with other controls in the form, the picker seems to be inactive. I see it, but cannot click it.
Here's the first app (the picker works):
struct ContentView: View {
#State private var selectedVegetable = VegetableList.asparagus
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedVegetable, label: Text("My Vegetables")) {
ForEach(VegetableList.allCases) { v in
Text(v.name).tag(v)
//use of tag makes no difference
}
}
}
}
.navigationBarTitle("Picker with Enum")
}
}
}
enum VegetableList: CaseIterable, Hashable, Identifiable {
case asparagus
case celery
case shallots
case cucumbers
var name: String {
return "\(self)".map {
$0.isUppercase ? " \($0)" : "\($0)" }.joined().capitalized
}
var id: VegetableList {self}
}
Here's the app with other controls (picker does not work).
struct Order {
var includeMustard = false
var includeMayo = false
var quantity: Int = 1
var avocadoStyle = PepperoniStyle.sliced
var vegetableType = VegetableType.none
var breadType = BreadType.wheat
}
struct OrderForm: View {
#State private var order = Order()
#State private var comment = "No Comment"
#State private var selectedVegetable = VegetableType.asparagus
#State private var selectedBread = BreadType.rye
func submitOrder() {}
var body: some View {
Form {
Text("Vegetable Ideas")
.font(.title)
.foregroundColor(.green)
Section {
Picker(selection: $selectedVegetable, label: Text("Vegetables")) {
ForEach(VegetableType.allCases) { v in
Text(v.name).tag(v)
}
}
Picker(selection: $selectedBread, label: Text("Bread")) {
ForEach(BreadType.allCases) { b in
Text(b.name).tag(b)
}
}
}
Toggle(isOn: $order.includeMustard) {
Text("Include Mustard")
}
Toggle(isOn: $order.includeMayo) {
Text("Include Mayonaisse")
}
Stepper(value: $order.quantity, in: 1...10) {
Text("Quantity: \(order.quantity)")
}
TextField("Say What?", text: $comment)
Button(action: submitOrder) {
Text("Order")
}
}
.navigationBarTitle("Picker in Form")
.padding()
}
}
enum PepperoniStyle {
case sliced
case crushed
}
enum BreadType: CaseIterable, Hashable, Identifiable {
case wheat, white, rye, sourdough, seedfeast
var name: String { return "\(self)".capitalized }
var id: BreadType {self}
}
enum VegetableType: CaseIterable, Hashable, Identifiable {
case none
case asparagus
case celery
case shallots
case cucumbers
var name: String {
return "\(self)".map {
$0.isUppercase ? " \($0)" : "\($0)" }.joined().capitalized
}
var id: VegetableType {self}
}
Xcode 11 Beta 7, Catalina Beta 7
There is no behavior difference between Preview and Simulator .I must be missing
something simple here. Any guidance would be appreciated.
I wrapped the Form in a NavigationView and the pickers now operate as expected. I need to research that once the documentation is more complete but perhaps this can help someone else.

Resources