Home > Net >  How to observe a variable from another variable?
How to observe a variable from another variable?

Time:04-05

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 }
        }
    }
}
  • Related