Was watching Paul Hudson – Hacking with Swift 2 hour plus video that he did in December on birthday and followed everything fine. I have an OK understanding of SwiftUI but learning. The ContentView drives the creation and searching of People. PeopleView let us refactor most of the body so that we could change the searching / sorting dynamically.
So I tried to upgrade the app he built by putting in the built-in ContentUnavailableView.search
“No Search Results” view and accomplished that OK with (one-liner) in the “else” in PeopleView. I then just look at if !people.isEmpty
to see whether it creates result lines or no results.
I wanted to also add a custom “ContentUnavailableView” when there are no physical records/objects in SwiftData to start. I added this to ContentView in the “else” which seems to work by checking if personCount > 0
.
For both views, I set up private var allPeople: Int
so that I could compute the total records/objects in Person to see if I needed to display the custom “ContentUnavailableView” in ContentView. The .onAppear
in ContentView then sets personCount which takes care of that calculation. In PeopleView, I use func deletePeople
to perform the calculation and then send it back through the @Binding
.
This current setup seems to work. Where I had the problem (before adding the computed property fix) is that the “delete” function in PeopleView when I deleted someone, the onAppear in ContentView didn’t fire thus the count would be wrong in ContentView.
To Summarize…
I need to come into ContentView for first time with no records/objects and it should show my custom “ContentUnavailableView” with its own “add” button. Toolbar will be gone. Once I have at least 1 record that matches the search text, it should show that resulting Person’s. If the search text returns no results, it should show the built-in ContentUnavailableView.search view. If I then delete all the records/objects in Person, I should again get my custom “ContentUnavailableView” that has an “add” button on it.
I guess my real questions are…
-
My computed property
private var allPeople: Int
is in both views but is there a better way to set this up? -
Also, is this computed property the best way to find total records/objects in Person class?
-
If my functionality is OK, is there a better way to refactor this repetitive code out to another location and just call it?
-
Could this be simplified so as to not use
@State private var personCount
ANDprivate var allPeople: Int
in order to get the overall count of Person in SwiftData? How would it be triggered so that the branching in ContentView occurs when needed? -
Is there a better way to compute the records/objects in the View body with one let/var?
-
I built my own “back” button because if a user added a Person and we navigate to it but leave the name blank, I need to delete that record from SwiftData before returning to ContentView. Is there a better way to delete this “empty” record from the modelContext than the following from EditPersonView:
.navigationBarBackButtonHidden(true) .navigationBarItems(leading: Button(action : { emptyObject() self.mode.wrappedValue.dismiss() }){ Image(systemName: "arrowshape.backward.fill") })
Using This…
// This will check to make sure that we have data filled in "name" field. If not, will delete that object.
func emptyObject() {
if person.name.isEmpty {
modelContext.delete(person)
}
}
Thanks,
Scott
import Foundation
import SwiftData`
@Model
final class Person {
var name: String
var emailAddress: String
var details: String
var metAt: Event? // What Event the Person was first met at. Nothing set to start with.
@Attribute(.externalStorage) var photo: Data?
init(name: String, emailAddress: String, details: String, metAt: Event? = nil) {
self.name = name
self.emailAddress = emailAddress
self.details = details
self.metAt = metAt
}
}
Main ContentView…
struct ContentView: View {
@Environment(.modelContext) var modelContext
@State private var destPath = NavigationPath()
@State private var sortOrder = [SortDescriptor(Person.name)]
@State private var searchText = ""
// Current "Person" count
@State private var personCount = 0
// TEST - Finding the total object count for Person and see if I could keep it updated in this view
private var allPeople: Int {
let decriptor = FetchDescriptor<Person>(predicate: #Predicate { !$0.name.isEmpty })
return (try? modelContext.fetchCount(decriptor)) ?? 0
}
var body: some View {
NavigationStack(path: $destPath) {
Group {
if personCount > 0 {
PeopleView(searchString: searchText, weHavePeople: $personCount, sortOrder: sortOrder)
.navigationTitle("Face Facts Man: (personCount)")
.toolbar {
Menu("Sort", systemImage: "arrow.up.arrow.down") {
Picker("Sort", selection: $sortOrder) {
Text("Names (A-Z)")
.tag([SortDescriptor(Person.name)])
Text("Names (Z-A)")
.tag([SortDescriptor(Person.name, order: .reverse)])
}
}
Button("Add Person", systemImage: "plus", action: addPerson)
}
.searchable(text: $searchText, prompt: "Find Some People...")
} else {
ContentUnavailableView(label: {
Label("You Must Know Someone", systemImage: "list.bullet.rectangle.portrait")
}, description: {
Text("ContentView: Go ahead, add some people! (personCount)")
}, actions: {
Button("Add A Person", systemImage: "person.badge.plus", action: addPerson)
.buttonStyle(.borderedProminent)
})
}
}
.navigationDestination(for: Person.self) { chosenPerson in
EditPersonView(person: chosenPerson, navPath: $destPath)
}
// TEST: Put in to see if the count was updating and assign so that we can branch above in Group
.onAppear {
personCount = allPeople
}
}
}
/*
Creates the new person and inserts it as "blank" into context. Then change to the destPath
to the correct Person we are going to edit. It's now binded and auto-saving is now done as
fields are changed on edit screen.
*/
func addPerson() {
let newPerson = Person(name: "", emailAddress: "", details: "")
modelContext.insert(newPerson)
destPath.append(newPerson)
}
}
PeopleView…
struct PeopleView: View {
// TEST
@Binding var peopleExist: Int
// Basically our work area. We add / Edit data to the context and let SwiftData do saving work
@Environment(.modelContext) var modelContext
// Added here to implement seaching...
// This will create and array of Person's and will keep it up-to-date
@Query var people: [Person]
// TEST
// This will calculate the number of "Person's" that exist so it can be returned
private var allPeople: Int {
let decriptor = FetchDescriptor<Person>(predicate: #Predicate { !$0.name.isEmpty })
return (try? modelContext.fetchCount(decriptor)) ?? 0
}
var body: some View {
// After custom init runs, we will know if "people" is empty
if !people.isEmpty {
List {
ForEach(people) { eachPerson in
NavigationLink(value: eachPerson) {
/*
Paul creates this Add/Edit sort of as the same thing like Apple Notes.
For the Add, it will first create a NEW blank Person object and then
navigate right to it. There is NO saving needed since object is created.
You can also Edit then since the linkage is always there. Not sure what
happens if you do an Add and then go oops and navigate back. I assume
that since object is created I would have to deal with that "blank" entry.
*/
Text(eachPerson.name)
}
}
.onDelete(perform: deletePeople)
}
} else {
ContentUnavailableView.search
}
}
/*
This custom init will take in what is being typed in the searchString and for each letter change
rebuild the query to match. When empty (default), all people will match. We check to see if the
serchString is empty and if so, that person is included (obviously wanting everyone in result).
If not empty, we get "one" person in and check the specific property against the searchString.
If it matches the "else" returns true. By using _people we are telling SwiftData to change the
query itself which creates the "people" array.
*/
init(searchString: String = "", weHavePeople: Binding<Int>, sortOrder: [SortDescriptor<Person>] = []) {
_people = Query(filter: #Predicate { personToCheck in
if searchString.isEmpty {
true
} else {
// We now check all fields
// These are checked in order so think about efficiency
personToCheck.name.localizedStandardContains(searchString) ||
personToCheck.emailAddress.localizedStandardContains(searchString) ||
personToCheck.details.localizedStandardContains(searchString)
}
}, sort: sortOrder)
self._peopleExist = weHavePeople
}
/* Removed from ContentView and added here to implement seaching...
Given an index set of People to delete, look through them and delete from context. SwiftData
will decide how / when to save the data and update the model. If you wanted to clear the model
you could do: try? modelContext.delete(model: Person.self)
*/
func deletePeople(at offsets: IndexSet) {
for eachOffset in offsets {
let personToDelete = people[eachOffset]
modelContext.delete(personToDelete)
}
// Grab the new updated count from SwiftData
peopleExist = allPeople
}
}