I have an array called items
that is stored in the Clients
class, which conforms the ObservableObject
protocol. Like this:
class Clients: ObservableObject {
@Published var items = [ClientItem]() {
didSet {
if let encoded = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(encoded, forKey: "Clients")
}
}
}
init() {
if let savedClients = UserDefaults.standard.data(forKey: "Clients") {
if let decodedClients = try? JSONDecoder().decode([ClientItem].self, from: savedClients) {
items = decodedClients
return
}
}
items = []
}
}
Then, in the ContentView
struct, I have the following:
struct ContentView: View {
@StateObject var clients = Clients()
@State private var showAddClient = false
@State private var showFilterSheet = false
var body: some View {
NavigationView {
List {
ForEach(???) { item in
HStack {
VStack(alignment: .leading) {
Text(item.name).font(.headline)
Text(item.id)
Text(item.isVisited ? "Visited" : "Not visited")
}
Spacer()
VStack(alignment: .trailing) {
Link(item.phone, destination: URL(string: "tel:\(item.phone)")!)
Text(item.email)
}
}
}
.onDelete(perform: removeItems)
}
.navigationTitle("Dummy")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Filter") {
showFilterSheet = true
}
.confirmationDialog("Select filter", isPresented: $showFilterSheet) {
Button("ID") {
??? = clients.items.sorted { (client1, client2) -> Bool in
let clientId1 = client1.id
let clientId2 = client2.id
return (clientId1.localizedCaseInsensitiveCompare(clientId2) == .orderedAscending)
}
print(clients.items)
}
Button("Name") {
??? = clients.items.sorted { (client1, client2) -> Bool in
let clientName1 = client1.name
let clientName2 = client2.name
return (clientName1.localizedCaseInsensitiveCompare(clientName2) == .orderedAscending)
}
}
Button("Visited") {
??? = clients.items.filter { item in
return item.isVisited == true
}
}
Button("No visitados") {
??? = clients.items.filter { item in
return item.isVisited == false
}
}
Button("Cancel", role: .cancel) { }
} message: {
Text("Filter by")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showAddClient = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAddClient) {
AddView(clients: clients)
}
}
}
func removeItems(at offsets: IndexSet) {
clients.items.remove(atOffsets: offsets)
}
}
As you can see, I would like to filter the array of clients but, if I do so, then I would be modifying the stored clients, which is something that I want to avoid. Notice the ???
fragments in the code, which make reference to the possible variable that would fix the problem. Such variable would observe the clients.items
property to be able to show the new items created, as well as filtering them without overriding the existing data.
NOTE: I know that storing this kind of data in UserDefaults is not a good practice, but I'm doing so for the sake of simplicity. My main focus here is to know how to handle the observables.
Edit: ClientItem
struct ClientItem: Identifiable, Codable { let id: String let name: String let phone: String let email: String var isVisited = false }
I have taken a different approach with the filtering. I have added the following to the Clients
class:
var itemsOrderedByName: [ClientItem] {
items.sorted { (client1, client2) -> Bool in
let client1 = client1.name
let client2 = client2.name
return (client1.localizedCaseInsensitiveCompare(client2) == .orderedAscending)
}
}
var itemsOrderedById: [ClientItem] {
items.sorted { (client1, client2) -> Bool in
let client1 = client1.id
let client2 = client2.id
return (client1.localizedCaseInsensitiveCompare(client2) == .orderedAscending)
}
}
var itemsVisited: [ClientItem] {
items.filter { item in
return item.isVisited == true
}
}
var itemsNotVisited: [ClientItem] {
items.filter { item in
return item.isVisited == false
}
}
I still don't know what to put in the ForEach(???)
line though.
CodePudding user response:
As mentioned in the comments, you can make your array a computed property. That resulting array will be what you display in ForEach
.
See inline comments for explanations.
class Clients: ObservableObject {
@Published var items : [ClientItem] = [
.init(id: "1", name: "Z", phone: "1", email: "c"),
.init(id: "2", name: "Y", phone: "2", email: "b"),
.init(id: "3", name: "X", phone: "3", email: "a", isVisited: true)
]
enum FilterType {
case none, id, name, visited, notVisited
}
@Published var filterType : FilterType = .none //what type of filter is active
var itemsOrderedByName: [ClientItem] {
items.sorted { (client1, client2) -> Bool in
let client1 = client1.name
let client2 = client2.name
return (client1.localizedCaseInsensitiveCompare(client2) == .orderedAscending)
}
}
var itemsOrderedById: [ClientItem] {
items.sorted { (client1, client2) -> Bool in
let client1 = client1.id
let client2 = client2.id
return (client1.localizedCaseInsensitiveCompare(client2) == .orderedAscending)
}
}
var itemsVisited: [ClientItem] {
items.filter { item in
return item.isVisited == true
}
}
var itemsNotVisited: [ClientItem] {
items.filter { item in
return item.isVisited == false
}
}
var filteredItems : [ClientItem] { //return an array based on the current filter type
switch filterType {
case .none:
return items
case .id:
return itemsOrderedById
case .name:
return itemsOrderedByName
case .visited:
return itemsVisited
case .notVisited:
return itemsNotVisited
}
}
}
struct ContentView: View {
@StateObject var clients = Clients()
@State private var showAddClient = false
@State private var showFilterSheet = false
var body: some View {
NavigationView {
List {
ForEach(clients.filteredItems) { item in //iterate over the filtered elements
HStack {
VStack(alignment: .leading) {
Text(item.name).font(.headline)
Text(item.id)
Text(item.isVisited ? "Visited" : "Not visited")
}
Spacer()
VStack(alignment: .trailing) {
Link(item.phone, destination: URL(string: "tel:\(item.phone)")!)
Text(item.email)
}
}
}
.onDelete(perform: removeItems)
}
.navigationTitle("Dummy")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Filter") {
showFilterSheet = true
}
.confirmationDialog("Select filter", isPresented: $showFilterSheet) {
Button("ID") {
clients.filterType = .id //set the filter type (same for the other types of buttons)
}
Button("Name") {
clients.filterType = .name
}
Button("Visited") {
clients.filterType = .visited
}
Button("No visitados") {
clients.filterType = .notVisited
}
Button("Cancel", role: .cancel) {
clients.filterType = .none
}
} message: {
Text("Filter by")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showAddClient = true
} label: {
Image(systemName: "plus")
}
}
}
}
}
func removeItems(at offsets: IndexSet) {
//we can't just use the index any more, because of the filter. So, find the corresponding item based on the current filter and then delete the item from the original array with a matching id
if let offset = offsets.first {
let item = clients.filteredItems[offset]
clients.items.removeAll { $0.id == item.id }
}
}
}