I found that the new table component of SwiftUI 3.0 is like a toy, which can be used easily, but it is difficult to expand more functions.
TableRow and TableColumn inherit from the value object. How can I get the view of a row? I want to set a different ContextMenu for each row. In addition, I want to set the ContextMenu for the column header.
How to implement it on the basis of Table component? I don't want to use the List component.
struct Person: Identifiable {
let givenName: String
let familyName: String
let id = UUID()
}
#State private var people = [
Person(givenName: "Juan", familyName: "Chavez"),
Person(givenName: "Mei", familyName: "Chen"),
Person(givenName: "Tom", familyName: "Clark"),
Person(givenName: "Gita", familyName: "Kumar"),
]
#State private var sortOrder = [KeyPathComparator(\Person.givenName)]
var body: some View {
Table(people, sortOrder: $sortOrder) {
TableColumn("Given Name", value: \.givenName)
TableColumn("Family Name", value: \.familyName)
}
.onChange(of: sortOrder) {
people.sort(using: $0)
}
}
In order to have contextMenu working on SwiftUI 3.0 Table it is necessary to add it to every TableColumn item. Plus, if you want to add Double Tap support it is necessary to add it independently too.
Table(documents, selection: $fileSelection) {
TableColumn("File name") { item in
Text(item.filename)
.contextMenu { YOUR_CONTEXT_MENU }
.simultaneousGesture(TapGesture(count: 1).onEnded { fileSelection = item.id })
.simultaneousGesture(TapGesture(count: 2).onEnded { YOUR_DOUBLE_TAP_IMPLEMENTATION })
}
TableColumn("Size (MB)") { item in
Text(item.size)
.contextMenu { YOUR_CONTEXT_MENU }
.simultaneousGesture(TapGesture(count: 1).onEnded { fileSelection = item.id })
.simultaneousGesture(TapGesture(count: 2).onEnded { YOUR_DOUBLE_TAP_IMPLEMENTATION })
}
}
From macOS 13 this will work as expected:
To try it out use the Garden App from apple and replace the row section of the Table as below
} rows: {
ForEach(plants) { plant in
TableRow(plant)
.itemProvider { plant.itemProvider }
.contextMenu {
Button {
} label: {
Text("test")
}
}
}
.onInsert(of: [Plant.draggableType]) { index, providers in
Plant.fromItemProviders(providers) { plants in
garden.plants.insert(contentsOf: plants, at: index)
}
}
}
Related
just this small table. I try to make items selectable. But I can't see, which row is selected and if I select the same row again, the app goes down with error: duplicate entry.
#FetchRequest var contacts: FetchedResults<Contacts>
#State private var selectedContact = Set<Contacts.ID>()
var body: some View {
Table(contacts, selection: $selectedContact, sortOrder: $contacts.sortDescriptors) {
TableColumn("company", value: \.company) { contact in
Text(contact.company!)
}
TableColumn("name", value:\.lastName) { contact in
ContactNameView(contact: contact)
}
TableColumn("street", value: \.street) { contact in
Text(contact.street!)
}
TableColumn("zip", value: \.zip) { contact in
Text(contact.zip!)
}
.width(50)
}
.tableStyle(.bordered(alternatesRowBackgrounds: true))
.toolbar {
Button {
print("goggo")
} label: {
Image(systemName: "plus")
}
}
Text("\(selectedContact.count)")
}
I think, the problem is on declaring selectedContact.
btw. counter on bottom increases on selection, so selecting itself seems to work.
I'm fairly new to Swift and Core Data. I’m having a problem resolving a state issue in a new project of mine.
I have a parent view (CategoryView)that includes a context menu item to allow editing of certain category properties (EditCategoryView). When the EditCategoryView sheet is presented and an edit to a category property is made, the CategoriesView updates correctly when the sheet is dismissed. Works fine.
There is a navigation link off of CategoriesView (ItemsView) that also includes a context menu to allow editing of certain item properties (EditItemView). Unlike the prior example, when the EditItemView sheet is presented and an edit is made to an item property, the ItemsView does not update when the sheet is dismissed. The old item property still displays. If I navigate back to CategoriesView and then return to ItemsView, the updated item property displays correctly.
I’m stumped and clearly don’t understand how state is managed in a CoreData environment. My code for the 2 views seems to be similar, yet they are behaving distinctly different. I wonder if the problem relates to the difference in the structures used in the 2 ForEach lines. That is, in CategoriesView I'm looping on the results of a Fetch and in EventsView I'm looping on the results of a computed value.
Any suggestions? thanks in advance for any guidance.
I created a simple example project that demonstrates the problem. To reproduce:
tap on Load Sample Data
choose a Category
tap and hold an Item to bring up context menu
choose Edit and change the name of the item
you’ll note when sheet dismisses the updated name is not reflected
return to Category list and then select the item again to see the updated name
https://github.com/jayelevy/CoreDataState
edit to include the code for the minimal example referenced in the repo
xcdatamodeld
2 Entities
Category
Attribute: name: String
Relationships: items, destination: Item (many-to-one)
Item
Attribute: name: String
Relationships: category, destination: Category (to one)
#main
struct CoreDataStateApp: App {
#StateObject var dataController: DataController
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
var body: some Scene {
WindowGroup {
CategoriesView()
.environment(\.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), perform: save)
}
}
func save(_ note: Notification) {
dataController.save()
}
}
struct CategoriesView: View {
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
var categories: FetchedResults<Category>
var body: some View {
NavigationView {
VStack {
List {
ForEach(categories) { category in
NavigationLink {
ItemsView(category: category)
} label : {
Text(category.categoryName)
}
}
}
}
.navigationTitle("My Categories")
.toolbar {
ToolbarItem(placement: .automatic) {
Button {
dataController.deleteAll()
try? dataController.createSampleData()
} label: {
Text("Load Sample Data")
}
}
}
}
}
}
problem occurs with the following view. When an item is edited in EditItemView, the updated property (name) does not display when returning to ItemsView from the sheet.
If you return to CategoryView and then return to ItemsView, the correct property name is displayed.
struct ItemsView: View {
#ObservedObject var category: Category
#State private var isEditingItem = false
var body: some View {
VStack {
List {
ForEach(category.categoryItems) { item in
NavigationLink {
//
} label: {
Text(item.itemName)
}
.contextMenu {
Button {
isEditingItem.toggle()
} label: {
Label("Edit Item", systemImage: "pencil")
}
}
.sheet(isPresented: $isEditingItem) {
EditItemView(item: item)
}
}
}
}
.navigationTitle(category.categoryName)
}
}
struct EditItemView: View {
var item: Item
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.dismiss) private var dismiss
#State private var itemName: String
init(item: Item) {
// _item = ObservedObject(initialValue: item)
self.item = item
_itemName = State(initialValue: item.itemName)
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Item Name", text: $itemName)
}
}
}
.navigationTitle("Edit Item")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
// add any needed cancel logic
Button("Cancel") {
dismiss()
}
}
ToolbarItem {
Button {
saveItem()
dismiss()
} label: {
Text("Update")
}
.disabled(itemName.isEmpty)
}
}
}
}
func saveItem() {
item.name = itemName
dataController.save()
}
}
extension Category {
var categoryName: String {
name ?? "New Category"
}
var categoryItems: [Item] {
items?.allObjects as? [Item] ?? []
}
extension Item {
var itemName: String {
name ?? "New Item"
}
}
extension Binding {
func onChange(_ handler: #escaping () -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler()
}
)
}
}
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Fatal error loading store: \(error.localizedDescription)")
}
}
}
static var preview: DataController = {
let dataController = DataController(inMemory: true)
let viewContext = dataController.container.viewContext
do {
try dataController.createSampleData()
} catch {
fatalError("Fatal error creating preview: \(error.localizedDescription)")
}
return dataController
}()
func createSampleData() throws {
let viewContext = container.viewContext
for i in 1...4 {
let category = Category(context: viewContext)
category.name = "Category \(i)"
category.items = []
for j in 1...5 {
let item = Item(context: viewContext)
item.name = "Item \(j)"
item.category = category
}
}
try viewContext.save()
}
func save() {
if container.viewContext.hasChanges {
try? container.viewContext.save()
}
}
func delete(_ object: NSManagedObject) {
container.viewContext.delete(object)
}
func deleteAll() {
let fetchRequest1: NSFetchRequest<NSFetchRequestResult> = Item.fetchRequest()
let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1)
_ = try? container.viewContext.execute(batchDeleteRequest1)
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = Category.fetchRequest()
let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
_ = try? container.viewContext.execute(batchDeleteRequest2)
}
func count<T>(for fetchRequest: NSFetchRequest<T>) -> Int {
(try? container.viewContext.count(for: fetchRequest)) ?? 0
}
}
ItemsView needs its own #FetchRequest for CategoryItem with a predicate where category = %#.
Also, instead of passing your DataController object around just put your helper methods in an extension of NSManagedObjectContext. Then you can change DataController back to the struct it should be.
I imagine there are other opportunities to improve my code (obviously, still learning), per other posts. However, the resolution was quite simple.
Modified saveItem in EditItemView to include objectWillChange.send()
func saveItem() {
item.name = itemName
item.category = itemCategory
item.category?.objectWillChange.send()
dataController.save()
}
So let's say I have a list component in SwiftUI:
struct MyListView: View {
var body: some View {
List(...) { rec in
Row(rec)
}
}
}
Now let's say I want to make this reusable, and I want the "caller" of this view to determine what happens when I tap on each row view. What would be the correct way to insert that behavior?
Here is some other Buttons in ListView example that you can run and play with it yourself
import SwiftUI
struct TestTableView: View {
#State private var item: MyItem?
var body: some View {
NavigationView {
List {
// Cell as Button that display Sheet
ForEach(1...3, id:\.self) { i in
Button(action: { item = MyItem(number: i) }) {
TestTableViewCell(number: i)
}
}
// Cell as NavigationLink
ForEach(4...6, id:\.self) { i in
NavigationLink(destination: TestTableViewCell(number: i)) {
TestTableViewCell(number: i)
}
}
// If you want a button inside cell which doesn't trigger the whole cell when being touched
HStack {
TestTableViewCell(number: 7)
Spacer()
Button(action: { item = MyItem(number: 7) }) {
Text("Button").foregroundColor(.accentColor)
}.buttonStyle(PlainButtonStyle())
}
}
}.sheet(item: $item) { myItem in
TestTableViewCell(number: myItem.number)
}
}
struct MyItem: Identifiable {
var number: Int
var id: Int { number }
}
}
struct TestTableViewCell: View {
var number: Int
var body: some View {
Text("View Number \(number)")
}
}
Make it like Button and takes an action param that is a closure.
From my understanding you're looking for a reusable generic List view with tap on delete functionality. If I'm guessing right my approach then would be like this:
struct MyArray: Identifiable {
let id = UUID()
var title = ""
}
struct ContentView: View {
#State private var myArray = [
MyArray(title: "One"),
MyArray(title: "Two"),
MyArray(title: "Three"),
MyArray(title: "Four"),
MyArray(title: "Five"),
]
var body: some View {
MyListView(array: myArray) { item in
Text(item.title) // row view
} onDelete: { item in
myArray.removeAll(where: {$0.id == item.id}) // delete func
}
}
}
struct MyListView<Items, Label>: View
where Items: RandomAccessCollection, Items.Element: Identifiable, Label: View {
var array: Items
var row: (Items.Element) -> Label
var onDelete: (Items.Element) -> ()
var body : some View {
List(array) { item in
Button {
onDelete(item)
} label: {
row(item)
}
}
}
}
In a form, I'd like a user to be able to dynamically maintain a list of phone numbers, including adding/removing numbers as they wish.
I'm currently maintaining the list of numbers in a published array property of an ObservableObject class, such that when a new number is added to the array, the SwiftUI form will rebuild the list through its ForEach loop. (Each phone number is represented as a PhoneDetails struct, with properties for the number itself and the type of phone [work, cell, etc].)
Adding/removing works perfectly fine, but when I attempt to edit a phone number within a TextField, as soon as I type a character, the TextField loses focus.
My instinct is that, since the TextField is bound to the phoneNumber property of one of the array items, as soon as I modify it, the entire array within the class publishes the fact that it's been changed, hence SwiftUI dutifully rebuilds the ForEach loop, thus losing focus. This behavior is not ideal when trying to enter a new phone number!
I've also tried looping over an array of the PhoneDetails objects directly, without using an ObservedObject class as an in-between repository, and the same behavior persists.
Below is the minimum reproducible example code; as mentioned, adding/removing items works great, but attempting to type into any TextField immediately loses focus.
Can someone please help point me in the right direction as to what I'm doing wrong?
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
func deletePhoneNumber(at index: Int) {
if allPhones.indices.contains(index) {
allPhones.remove(at: index)
}
}
}
struct PhoneDetails: Equatable, Hashable {
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach(userPhonesManager.allPhones, id: \.self) { phoneDetails in
let index = userPhonesManager.allPhones.firstIndex(of: phoneDetails)!
HStack {
Button(action: { userPhonesManager.deletePhoneNumber(at: index) }) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
try this:
ForEach(userPhonesManager.allPhones.indices, id: \.self) { index in
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(at: index)
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
EDIT-1:
Reviewing my comment and in light of renewed interest, here is a version without using indices.
It uses the ForEach with binding feature of SwiftUI 3 for ios 15+:
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
// -- here --
func deletePhoneNumber(of phone: PhoneDetails) {
allPhones.removeAll(where: { $0.id == phone.id })
}
}
struct PhoneDetails: Identifiable, Equatable, Hashable {
let id = UUID() // <--- here
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach($userPhonesManager.allPhones) { $phone in // <--- here
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(of: phone) // <--- here
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $phone.phoneNumber) // <--- here
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.