Reputation: 93
(Using XCode 13.4 / iOS 15)
I have a @Published
array that is presented to the user in a List
with the ability to make changes to the items. The source of the array is from a file that is updated once the user makes changes to the items.
The sample code below shows how I'm currently doing that.
My question is: I want the List
that is presented to the user to be sorted. How can I do that without changing the underlying @Published
array, while still enabling the user to make changes to the items?
struct Contact: Identifiable {
let id = UUID()
var firstName: String
var lastName: String
var dateOfBirth: Date
}
class ViewModel: ObservableObject {
@Published var contacts: [Contact]
static let shared = ViewModel()
private init() {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MMM-yyyy"
let sampleData = [
Contact(firstName: "James", lastName: "Smith", dateOfBirth: formatter.date(from: "2-Jan-1965")!),
Contact(firstName: "John", lastName: "Johnson", dateOfBirth: formatter.date(from: "7-Feb-1982")!),
Contact(firstName: "David", lastName: "Brown", dateOfBirth: formatter.date(from: "22-Mar-1971")!),
Contact(firstName: "Michael", lastName: "Jones", dateOfBirth: formatter.date(from: "15-Aug-2002")!),
Contact(firstName: "Robert", lastName: "Davis", dateOfBirth: formatter.date(from: "11-Nov-2010")!),
]
contacts = sampleData
}
}
struct ContentView: View {
@ObservedObject var viewModel = ViewModel.shared
var body: some View {
List {
ForEach($viewModel.contacts) {$contact in
HStack {
Text("\(contact.firstName) \(contact.lastName)")
DatePicker("", selection: $contact.dateOfBirth, displayedComponents: [.date])
}
}
}
}
}
Upvotes: 2
Views: 1012
Reputation: 93
After quite a bit of experimenting and investigations I came up with a solution.
The trick was to transform the Binding
Array
of the ObservedObject
(e.g. Binding<[Contact]>
) into Array of Binding
(e.g. [Binding<Contact>]
). This enabled presenting the data in different ways while having the Binding
bounded to the original items.
It boils down to one computed property that transforms and return the data to be presented:
private var filteredContacts: [Binding<Contact>] {
/// `$viewModel.contacts` is of type `Binding<[Contact]>`. We are creating an array
/// of `Binding<Contact>` from the source `viewModel.contact`
let contactBindingsArr = $viewModel.contacts.map {$contact in return $contact}
// Sort/Filter the array of Bindings
let res = contactBindingsArr.filter{contact in ![2,4,6].contains(contact.id) }.sorted{$0.id > $1.id}
return res
}
Here is the code of a fully working example. There are three List
views: first is showing the original data, second is showing filtered data and third is showing grouped data. It also shows behavior when adding a new item.
import SwiftUI
struct Contact: Identifiable {
let id: Int // = UUID()
var firstName: String
var lastName: String
var dateOfBirth: Date
}
class ViewModel: ObservableObject {
@Published var contacts: [Contact]
static let shared = ViewModel()
private init() {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MMM-yyyy"
let sampleData = [
Contact(id: 1, firstName: "James", lastName: "Smith", dateOfBirth: formatter.date(from: "2-Jan-1965")!),
Contact(id: 5, firstName: "John", lastName: "Johnson", dateOfBirth: formatter.date(from: "7-Feb-2022")!),
Contact(id: 2, firstName: "David", lastName: "Brown", dateOfBirth: formatter.date(from: "22-Mar-1965")!),
Contact(id: 4, firstName: "Michael", lastName: "Jones", dateOfBirth: formatter.date(from: "15-Aug-2022")!),
Contact(id: 3, firstName: "Robert", lastName: "Davis", dateOfBirth: formatter.date(from: "11-Nov-1965")!),
]
contacts = sampleData
}
}
struct ContentView: View {
@ObservedObject private var viewModel = ViewModel.shared
private var groupedContacts:[GroupedContacts] {
/// `$viewModel.contacts` is of type `Binding<[Contact]>`. We are creating an array
/// of `Binding<Contact>` from the source `viewModel.contact`
let contactBindingsArr = $viewModel.contacts.map {$contact in return $contact}
// Group contacts by year of the dateOfBirth
let dict = Dictionary(grouping: contactBindingsArr, by: {$item in item.dateOfBirth.formatted(.dateTime.year())})
var arr = dict.keys.map { GroupedContacts(key: $0, contacts: dict[$0]!.sorted{$0.id > $1.id}) }
arr.sort { $0.key > $1.key }
return arr
}
private var filteredContacts: [Binding<Contact>] {
/// `$viewModel.contacts` is of type `Binding<[Contact]>`. We are creating an array
/// of `Binding<Contact>` from the source `viewModel.contact`
let contactBindingsArr = $viewModel.contacts.map {$contact in return $contact}
// Sort/Filter the array of Bindings
let res = contactBindingsArr.filter{contact in ![2,4,6].contains(contact.id) }.sorted{$0.id > $1.id}
return res
}
var body: some View {
NavigationView {
VStack {
// This list shows the original data
List {
ForEach(viewModel.contacts) {contact in
HStack {
Text("\(contact.id). \(contact.firstName) \(contact.lastName)")
Spacer()
Text(contact.dateOfBirth.formatted(.dateTime.year().month().day()))
}
}
}
.border(Color.red)
// This list shows sorted/filtered data
List {
ForEach(filteredContacts) {$contact in
HStack {
Text("\(contact.id). \(contact.firstName) \(contact.lastName)")
DatePicker("", selection: $contact.dateOfBirth, displayedComponents: [.date])
}
}
}
.border(Color.blue)
// This list shows grouped data
List {
ForEach(groupedContacts, id: \.key) {group in
Section(content: {
ForEach(group.contacts) {$contact in
HStack {
Text("\(contact.id). \(contact.firstName) \(contact.lastName)")
DatePicker("", selection: $contact.dateOfBirth, displayedComponents: [.date])
}
}
}, header: {Text("Year Of \(group.key)")})
}
}
.border(Color.orange)
}
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("\(Image(systemName: "plus"))") {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MMM-yyyy"
let maxId = (viewModel.contacts.max {a, b in a.id < b.id})?.id ?? 0
let newContact = Contact(id: maxId + 1, firstName: "John", lastName: "Doe", dateOfBirth: formatter.date(from: "18-May-2022")!)
viewModel.contacts.append(newContact)
}
}
}
}
}
private struct GroupedContacts {
let key:String
let contacts:[Binding<Contact>]
}
}
Upvotes: 2
Reputation: 33
You can do it just as follows:
var filteredList: [ListItems] {
modelData.listItems.filter { entry in
(!showFavoritesOnly || entry.isFavorite)
}
}
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(filteredList) { currentItem in
NavigationLink{
ItemDetail(item: currentItem)
} label: {
ItemRow(landmark: currentItem)
}
}
}
.navigationTitle("Components")
}
}
Here I have a List that is being filtered to "show favorites only," when the user determines to do so. To achieve this, the model of my ListItems has a boolean, isFavorite
, that is flipped when the user chooses to make the item a favorite. I then filter here (if the toggle switch is on) to check which items are favorites.
For you, you should add a boolean to Contact()
that you can look at to specifically filter out anything that is true or false.
Upvotes: 0