I have an app with a number of categories--some the can be changed by the user and some that can't. The categories that can't be changed are stored currently in an array while categories that the user can change are stored in a class. The problem comes when creating expense entries where the picker needs to show both types of categories.
The other side of the coin is to place all the categories (text strings) in the class. Here the expense entry picker and class storage will work ok, but then there is the problem of preventing the user from deleting the default categories.
I'm guessing that latter option is the better route since it will place all the categories in the picker list. Here is the code for storing the dynamic categories. I suppose I could add some init() code to store the categories that don't change. Not sure exactly how to do that.
struct CatItem: Codable {
var catName: String
var catPix: String
}
class Categories: ObservableObject {
#Published var catItem: [CatItem] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(catItem) {
UserDefaults.standard.set(encoded, forKey: "workCat")
}
}
}
init() {
if let catItem = UserDefaults.standard.data(forKey: "workCat") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([CatItem].self, from: catItem) {
self.catItem = decoded
return
}
}
self.catItem = []
}
}
How would you prevent the user from deleting some of the fixed categories? Usually you have a list with an onDelete statement.
Can you use the index to determine if deleting is allowed? For example don't delete entry if indexSet[index] < 8?
.onDelete { indexSet in
for index in indexSet {
remove entry
}
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
I assume you wanted something like
.onDelete { indexSet in
guard let i = indexSet.first, indexSet[i] < 8 else { return }
// .. other code
I resolved this storage quandary by deciding to store both the permanent and changeable categories in the same class catItem. In the module where the categories may be viewed and where new categories may be added I use a filtered list showing only the categories that may be edited.
This is how the categories are initialized at startup:
let item = CatItem(catName: "name1", catPix: "sf symbol", noShow: true)
self.catItem.append(item)
This is how the categories are displayed in a list:
List {
ForEach(categories.catItem, id: \.catName) { item in
if item.noShow == false {
HStack {
Text(item.catName)
.padding(.horizontal, 10)
Spacer()
Image(systemName: item.catPix).resizable()
.frame(width: 30, height: 30)
}
}
}
.onDelete(perform: removeItems)
}
So if the user can't see the permanent categories in List, then they can't delete them. The user can only delete the categories that they add.
When the user adds new categories the noShow parameter is set to false.
In the picker the categories are not filtered so all categories may be viewed.
Related
Let's say I have 2 entities:
GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"
the relationship is "one to many": One session can have many players
Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink)
It sends me to a new view where I can see:
All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session
Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...
I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"
Basically in my first view I have something like that:
struct RamiListePartieUIView: View {#Environment(.managedObjectContext) var moc#FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>
var body: some View {
VStack {
List {
ForEach(gamesessions, id: \.date) { session in
NavigationLink (destination: DetailPartieSelecUIView(session: session)){
Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
}
}
.onDelete(perform: deleteSessions)
.padding()
}
}
}
}
And in my second view I have something like that:
struct DetailPartieSelecUIView: View {
#State var session:GameSession
#Environment(\.managedObjectContext) var moc
var body: some View {
Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
ForEach(session.playersArray, id: \.self) { player in
HStack {
Text(player.wrappedName) // OK it works
TextField("score", text : player.wrappedScore_part1) // it generates an error
TextField("score", text : player.wrappedScore_part2) // it generates an error
TextField("score", text : player.wrappedScore_part3) // it generates an error
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .medium
formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
return formatter
}()
also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file
and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file
it is done like that:
public var wrappedPlace: String {
place ?? "Unknown"
}
// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
let playersSet = players as? Set<Player> ?? []
return playersSet.sorted {
$0.wrappedName< $1.wrappedName
}
}
I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.
Thanks a lot
I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message
I tried also to add the $ symbol, like that:
TextField("score", text : player.$wrappedScore_part1)
But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'"
And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"
Thank you for your help!
Best regards,
JB
Your first problem of how to fetch the players in a session you need to supply a predicate to the #FetchRequest<Player>, e.g.
#FetchRequest
private var players: FetchedResults<Player>
init(session: Session) {
let predicate = NSPredicate(format: "session = %#", session)
let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
}
That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.
The second problem about the bindings can be solved like this:
struct PlayerView: View{
#ObservedObject var player: Player {
var body:some View {
if let score = Binding($player.score) {
TextField("Score", score)
}else{
Text("Player score missing")
}
}
}
This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.
I have a fairly complex document type to work with. It is basically a bundle containing a set of independent documents of the same type, with various pieces of metadata about the documents. The data structure that represents the bundle is an array of structs, similar to this (there are several more fields, but these are representative):
struct DocumentData: Equatable, Identifiable, Hashable {
let id = UUID()
var docData: DocumentDataClass
var docName: String
var docFileWrapper: FileWrapper?
func hash(into hasher: inout Hasher) {
id.hash(into: &hasher)
}
static func ==(lhs: KeyboardLayoutData, rhs: KeyboardLayoutData) -> Bool {
return lhs.id == rhs.id
}
}
The window for the bundle is a master-detail, with a list on the left and, when one is selected, there is an edit pane for the document on the right. The FileWrapper is used to keep track of which files need to be written for saving, so it gets initialised on reading the relevant file, and reset when an undoable change is made. That is largely the only way that the DocumentData structure gets changed (ignoring explicit things like changing the name).
I've reached a point where a lot of things are working, but I'm stuck on one. There's a view inside the edit pane, several levels deep, and when I double-click it, I want a sheet to appear. It does so, but then disappears by itself.
Searching for ways to work this out, I discovered by using print(Self._printChanges()) at various points that the edit pane was being refreshed after showing the sheet, which meant that the parent disappeared. What I found was that the dependency that changed was the DocumentData instance. But, I then added a print of the DocumentData instance before the _printChanges call, and it is identical. I have also put in didSet for each field of DocumentData to print when they get set, and nothing gets printed, so I'm not sure where the change is happening.
So the question comes down to how I can work out what is actually driving the refresh, since what is claimed to be different is identical in every field.
There are some other weird things happening, such as dragging and dropping text into the view causing the whole top-level document array of DocumentData items to change before the drop gets processed and the data structures get updated, so there are things I am not understanding as clearly as I might like. Any guidance is much appreciated.
ADDED:
The view that triggers the sheet is fairly straightforward, especially compared to its enclosing view, which is where most of the interface code is. This is a slightly simplified version of it:
struct MyView: View, DropDelegate {
#EnvironmentObject var keyboardStatus: KeyboardStatus
#Environment(\.displayFont) var displayFont
#Environment(\.undoManager) var undoManager
var keyCode: Int
#State var modifiers: NSEvent.ModifierFlags = []
#State private var dragHighlight = false
#State private var activeSheet: ActiveSheet?
#State private var editPopoverIsPresented = false
// State variables for double click and drop handling
...
static let dropTypes = [UTType.utf8PlainText]
var body: some View {
ZStack {
BackgroundView(...)
Text(...)
}
.onAppear {
modifiers = keyboardStatus.currentModifiers
}
.focusable(false)
.allowsHitTesting(true)
.contentShape(geometry.contentPath)
.onHover { entered in
// updates an inspector view
}
.onTapGesture(count: 2) {
interactionType = .doubleClick
activeSheet = .doubleClick
}
.onTapGesture(count: 1) {
handleItemClick()
}
.sheet(item: $activeSheet, onDismiss: handleSheetReturn) { item in
switch item {
case .doubleClick:
DoubleClickItem(...) ) {
activeSheet = nil
}
case .drop:
DropItem(...) {
activeSheet = nil
}
}
}
.popover(isPresented: $editPopoverIsPresented) {
EditPopup(...)
}
.onDrop(of: KeyCap.dropTypes, delegate: self)
.contextMenu {
ItemContextMenu(...)
}
}
func handleItemClick() {
NotificationCenter.default.post(name: .itemClick, object: nil, userInfo: [...])
}
func handleEvent(event: KeyEvent) {
if event.eventKind == .dropText {
interactionType = .drop
activeSheet = .drop
}
else if event.eventKind == .replaceText {
...
handleItemDoubleClick()
}
}
func handleSheetReturn() {
switch interactionType {
case .doubleClick:
handleItemDoubleClick()
case .drop:
handleItemDrop()
case .none:
break
}
}
func handleItemDoubleClick() {
switch itemAction {
case .state1:
...
case .state2:
...
case .none:
// User cancelled
break
}
interactionType = nil
}
func handleItemDrop() {
switch itemDropAction {
case .action1:
...
case .action2:
...
case .none:
// User cancelled
break
}
interactionType = nil
}
// Drop delegate
func dropEntered(info: DropInfo) {
dragHighlight = true
}
func dropExited(info: DropInfo) {
dragHighlight = false
}
func performDrop(info: DropInfo) -> Bool {
if let item = info.itemProviders(for: MyView.dropTypes).first {
item.loadItem(forTypeIdentifier: UTType.utf8PlainText.identifier, options: nil) { (textData, error) in
if let textData = String(data: textData as! Data, encoding: .utf8) {
let event = ...
handleEvent(event: event)
}
}
return true
}
return false
}
}
Further edit:
I ended up rewiring the code so that the sheet belongs to the higher level view, which makes everything work without solving the question. I still don't understand why I get a notification that a dependency has changed when it is identical to what it was before, and none of the struct's didSet blocks are called.
Try removing the class from the DocumentData. The use of objects in SwiftUI can cause these kind of bugs since it’s all designed for value types.
Try using ReferenceFileDocument to work with your model object instead of FileDocument which is designed for a model of value types.
Try using sheet(item:onDismiss:content:) for editing. I've seen people have the problem you describe when they try to hack the boolean sheet to work with editing an item.
Please see the code below. Pressing the button once (or twice at most) is almost certain to crash the app. The app shows a list containing two sections, each of which have four items. When button is pressed, it inserts a new item into each section and also changes the section order.
I have just submitted FB9952691 to Apple. But I wonder if anyone on SO happens to know 1) Does UIKit has the same issue? I'm just curious (the last time I used UIkit was two years ago). 2) Is it possible to work around the issue in SwiftUI? Thanks.
import SwiftUI
let groupNames = (1...2).map { "\($0)" }
let groupNumber = groupNames.count
let itemValues = (1...4)
let itemNumber = itemValues.count
struct Item: Identifiable {
var value: Int
var id = UUID()
}
struct Group: Identifiable {
var name: String
var items: [Item]
var id = UUID()
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
}
struct Data {
var groups: [Group]
// initial data: 2 sections, each having 4 items.
init() {
groups = groupNames.map { name in
let items = itemValues.map{ Item(value: $0) }
return Group(name: name, items: items)
}
}
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
}
struct ContentView: View {
#State var data = Data()
var body: some View {
VStack {
List {
ForEach(data.groups) { group in
Section {
ForEach(group.items) { item in
Text("\(group.name): \(item.value)")
}
}
header: {
Text("Section \(group.name)")
}
}
}
Button("Press to crash the app!") {
withAnimation {
data.change()
}
}
.padding()
}
}
}
More information:
The error message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=8, oldGlobalRowCount=8)'
The issue isn't caused by animation. Removing withAnimation still has the same issue. I believe the issue is caused by the section order change (though it works fine occasionally).
Update: Thank #Yrb for pointing out an out-of-index bug in insertItem(). That function is a setup utility in the example code and is irrelevant to the issue with change(). So please ignore it.
The problem is here:
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
You are attempting to do too much to the array at once, so in the middle of reversing the order, the array counts are suddenly off, and the List (and it's underlying UITableView) can't handle it. So, you can either reverse the rows, or add an item to the rows, but not both at the same time.
As a bonus, this will be your next crash:
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
though it is not causing the above as I fixed this first. You have set a fixed Int for itemNumber which is the count of the items in the first place. Arrays are 0 indexed, which means the initial array indices will be (0...3). This line let index = (0...itemNumber).randomElement()! gives you an index that is in the range of (0...4), so you have a 20% chance of crashing your app each time this runs. In this sort of situation, always use an index of (0..<Array.count) and make sure the array is not empty.
I got Apple's reply regarding FB9952691. The issue has been fixed in iOS16 (I verified it).
I am trying to change the swipeAction from "Paid" to "UnPaid" based on payment status and somehow seems to be failing. Error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
Appreciate any help
struct ContentView: View {
var data: [Data] = [data1, data2, data3, data4]
#State var swipeLabel = true
var body: some View {
let grouped = groupByDate(data)
List {
ForEach(Array(grouped.keys).sorted(by: >), id: \.self) { date in
let studentsDateWise = grouped[date]!
Section(header:Text(date, style: .date)) {
ForEach(studentsDateWise, id:\.self) { item in
HStack {
Text(item.name)
padding()
Text(item.date, style: .time)
if(item.paymentStatus == false) {
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color.red)
} else {
Image(systemName: "banknote")
.foregroundColor(Color.green)
}
} // HStack ends here
.swipeActions() {
if(item.paymentStatus) {
Button("Paid"){}
} else {
Button("UnPaid"){}
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
} // var ends here
}
The body func shouldn't do any grouping or sorting. You need to prepare your data first into properties and read from those in body, e.g. in an onAppear block. Also if your Data is a struct you can't use id: \.self you need to either specify a unique identifier property on the data id:\.myUniqueID or implement the Indentifiable protocol by either having an id property or an id getter that computes a unique identifier from other properties.
I would suggest separating all this code into small Views with a small body that only uses one or a two properties. Work from bottom up. Then eventually with one View works on an array of dates and another on an array of items that contains the small Views made earlier.
You should probably also learn that if and foreach in body are not like normal code, those are converted into special Views. Worth watching Apple's video Demystify SwiftUI to learn about structural identity.
I'm trying the new Realm 10 wrappers by using the example given in the Realm documentation Integration Guides -> SwiftUI & Combine and I like how simple it is to add and delete records when using the #ObservedResults and the #ObservedRealmObject. The one thing I don't quite understand is why when deleting items from the Group object it only removes the items from the Group but leaves the actual items in the Item Realm object undeleted. See the Realm Browser image below.
Here is what the Realm Browser shows after adding and deleting four (4) items through the app UI, as you can see the four (4) items were deleted from the Group but left all four (4) items in the Item object.
Can someone please explain why the items don't get deleted from the Item object only from the Group object when calling .onDelete(perform: $group.items.remove)? How can I delete them?
I tried deleting them like this...
ItemsView.swift
.onDelete(perform: deleteItems)
func deleteItems(at offsets: IndexSet){
let realm = try? Realm()
try! realm?.write {
// 1. delete items
for item in list.items{
realm?.delete(item)
}
// 2. delete the list
realm?.delete(list)
}
}
but I got the following error:
Thread 1: "Can only delete an object from the Realm it belongs to."
Again, the whole code can be found in the Integration Guides - Without Sync.
EDIT: Added code for, LocalOnlyContentView, ItemsView, Group and Item models.
Item.swift
import Foundation
import RealmSwift
/// Random adjectives for more interesting demo item names
let randomAdjectives = [
"fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden",
"acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen",
"aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet"
]
/// Random noun for more interesting demo item names
let randomNouns = [
"floor", "monitor", "hair tie", "puddle", "hair brush", "bread",
"cinder block", "glass", "ring", "twister", "coasters", "fridge",
"toe ring", "bracelet", "cabinet", "nail file", "plate", "lace",
"cork", "mouse pad"
]
final class Item: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
#Persisted var isFavorite = false
#Persisted(originProperty: "items") var group: LinkingObjects<Group>
}
Group.swift
import Foundation
import RealmSwift
final class Group: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id: ObjectId
#Persisted var items = RealmSwift.List<Item>()
}
ItemsView.swift
struct ItemsView: View {
#ObservedRealmObject var group: Group
var leadingBarButton: AnyView?
var body: some View {
NavigationView {
VStack {
// The list shows the items in the realm.
List {
ForEach(group.items) { item in
ItemRow(item: item)
}
.onDelete(perform: $group.items.remove)
.onMove(perform: $group.items.move)
}.listStyle(GroupedListStyle())
.navigationBarTitle("Items", displayMode: .large)
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: self.leadingBarButton,
trailing: EditButton())
HStack {
Spacer()
Button(action: {
$group.items.append(Item())
}) { Image(systemName: "plus") }
}.padding()
}
}
}
}
LocalOnlyContentView.swift
struct LocalOnlyContentView: View {
#ObservedResults(Group.self) var groups
var body: some View {
if let group = groups.first {
AnyView(ItemsView(group: group))
} else {
AnyView(ProgressView().onAppear {
$groups.append(Group())
})
}
}
}
SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = LocalOnlyContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Keep in mind that what's being shown in that tutorial is how to remove an item from a group's List - not how to totally delete the item.
Going very high level with this answer - objects in a List are not the actual objects - they are a "pointer" to the actual item stored on disk.
Suppose you have three items
Item 0
Item 1
Item 2
and a Group with List of items
MyGroup
List of items
Item 0
Item 1
Item 2
What's actually going on is the List "points" to the items on disk
My Group
List of items
pointer to Item 0
pointer to Item 1
pointer to Item 2
So when this is called $group.items.remove it's removing the pointer to the item from the list, not the item itself.
The solution (well, one of the solutions) is to remove the actual item
let myItemToRemove = List of items[0] //get the item at index 0
realm.remove(myItemToRemove) // removes the item itself from realm, along
// with the pointer stored in the list
The code to actually delete an item is this
try? realm.write {
realm.delete(objectToDelete)
}