How to setup if-else in SwiftUI Views so can display the proper ContentUnavailableView (Custom or .search)

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…

  1. My computed property private var allPeople: Int is in both views but is there a better way to set this up?

  2. Also, is this computed property the best way to find total records/objects in Person class?

  3. If my functionality is OK, is there a better way to refactor this repetitive code out to another location and just call it?

  4. Could this be simplified so as to not use @State private var personCount AND private 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?

  5. Is there a better way to compute the records/objects in the View body with one let/var?

  6. 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:

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    <code> .navigationBarBackButtonHidden(true)
    .navigationBarItems(leading: Button(action : {
    emptyObject()
    self.mode.wrappedValue.dismiss()
    }){
    Image(systemName: "arrowshape.backward.fill")
    })
    </code>
    <code> .navigationBarBackButtonHidden(true) .navigationBarItems(leading: Button(action : { emptyObject() self.mode.wrappedValue.dismiss() }){ Image(systemName: "arrowshape.backward.fill") }) </code>
     .navigationBarBackButtonHidden(true)
     .navigationBarItems(leading: Button(action : {
         emptyObject()
         self.mode.wrappedValue.dismiss()
     }){
         Image(systemName: "arrowshape.backward.fill")
     })
    

Using This…

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>// 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)
}
}
</code>
<code>// 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) } } </code>
// 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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code> 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
}
}
</code>
<code> 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 } } </code>
    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…

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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)
}
}
</code>
<code>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) } } </code>
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…

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>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
}
}
</code>
<code>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 } } </code>
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
    }
}

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật