Swift : Filter in the dictionary that is grouped by key - swift

My goal is to be able to filter by name in the dictionary that is grouped by key and to see the results filtered by the text that was inputted in the Search Field. What I stopped at is:
var groupedContacts: [String: [CNContact]] {
.init (
grouping: store.contacts,
by: {$0.nameFirstLetter}
)
}
func filterContactsByName(_ textSearch: String) -> [String: [CNContact]] {
let contacts = groupedContacts
if !textSearch.isEmpty {
return contacts.compactMapValues{$0.filter {$0.name.localizedCaseInsensitiveContains(textSearch)}}.filter{!$0.value.isEmpty}
} else {
return contacts
}
}
And then:
List() {
ForEach(self.filterContactsByName(searchText).keys.sorted(), id: \.self) { key in
Section(header: Text(key).modifier(SectionHeader(backgroundColor: Color.white, foregroundColor: Color.black))) {
ForEach(self.groupedContacts[key]!, id: \.self) { contact in
HStack {
self.image(for: contact.imageProfile)
.renderingMode(.original)
.resizable()
.scaledToFill()
.frame(width: 40, height: 40)
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
My result:
screenshot
As I understood, I received all the results in the key-group, that's why we see Danial before David. And it will be the same if we input David - we will see Danial.
TIA for a help.

You need to both filter out the right group and then filter inside that group. In the below code, which is somewhat simplified since I used String instead of CNContact and didn't bother with case insensitive searching, I first find the group by filtering on the first char of the search string and then I used reduce(into:) in combination with filter to filter the array for the found key.
var groupedContacts: [String: [String]] = ["D": ["David", "Daniel"], "A": ["AAA"]]
let filter = "Dav"
let results = groupedContacts.filter { $0.key == filter.prefix(1) }
.reduce(into: [:]) {$0[$1.key] = $1.value.filter { $0.hasPrefix(filter)}}

Related

How to show a different price value and currency based on the country

I have an app aiming to manage a collection (like pokemon cards). It has JSON data with different variables, in which, the price. To display the list of all items I do the following:
struct itemRow: View {
let items: [Item] = Bundle.main.decode("item.json")
var body: some View {
List {
ForEach(items) { item in
HStack {
Text(item.name)
Text("\(String(format: "%.2f", item.price))€")
}
}
}
}
}
Which creates a straightforward list with names and prices stacked one on top of the other. My issue is that the original data are in €, so when the user uses the app whenever he is in the world, he will see Euro prices. I wanted to do something like an 'if, else' statement or whatever works, to make the user see the correct price even if he has USD. I was able only to show different currencies in the text, like '90$' instead of '90€', but the right conversion is like '97.69$'. I know that rates change every day, but due to the simple app, even some static constants are good enough for my purpose. I would like to support GBP and USD, not every currency
I solved in this way, basing the if-else statement on the Locale
struct itemRow: View {
let items: [Item] = Bundle.main.decode("item.json")
let locale = Locale.current
var body: some View {
List {
ForEach(items) { item in
HStack {
Text(item.name)
if (locale.identifier == "en_US") {
Text("\(String(format: "%.2f", minifig.price * 1.10))$")
.foregroundColor(.gray)
.font(.system(size: 12))
} else if (locale.identifier == "en_GB") {
Text("\(String(format: "%.2f", minifig.price * 0.88))£")
.foregroundColor(.gray)
.font(.system(size: 12))
} else {
Text("\(String(format: "%.2f", minifig.price))€")
.foregroundColor(.gray)
.font(.system(size: 12))
}
}
}
}
}
}

updateData overwrites data in collection

I have a collection (userRecipes), which should contain sub collection's containing individual recipes. (I save each sub collection with the recipe's name, in this instance it's cheese)
My issue is whenever I save each sub collection, it overwrites the previous recipe.(ie. if I saved another recipe, it would overwrite cheese) In a perfect world, I would like each sub collection saved without overwriting the existing collections. Can't see why updateData isn't working in this instance, unless I'm misinterpreting firebase's docs.
struct SaveRecipeButton: View {
#EnvironmentObject var recipeClass: Recipe
func saveRecipe (){
//saves from object in RecipeModel to arrays
var ingredientArr:[String:String] = [:]
var directionArr: [String] = []
//goes through both ingredients and directions
for item in recipeClass.ingredients {
ingredientArr[item.sizing] = item.description
}
for item in recipeClass.directions {
directionArr.append(item.description)
}
//sets up firebase w/ recipe as subcollection
let newRecipeInfo: [String: Any] = [
"userRecipes": [
recipeClass.recipeTitle: [
"recipeTitle": recipeClass.recipeTitle,
"recipePrepTime": recipeClass.recipePrepTime,
"createdAt": Date.now,
"ingredientItem": ingredientArr,
"directions": directionArr
]
]
]
//grab current user
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
return
}
//updates data in firebase
do {
try FirebaseManager.shared.firestore.collection("users").document(uid).updateData(newRecipeInfo)
print("successfully save to database")
} catch let error {
print("Error writing recipe to Firestore: \(error)")
}
}
var body: some View {
Button(action: {
}){
HStack{
Image(systemName: "pencil").resizable()
.frame(width:40, height:40)
.foregroundColor(.white)
Button(action: {
saveRecipe()
}){
Text("Save Recipe")
.font(.title)
.frame(width:200)
.foregroundColor(.white)
}
}
.padding(EdgeInsets(top: 12, leading: 100, bottom: 12, trailing: 100))
.background(
RoundedRectangle(cornerRadius: 10)
.fill(
Color("completeGreen")))
}
}
}
Your userRecipes is a field in a document, not a collection. When you call updateData it replaces the existing value in the userRecipes with the values you specify.
If you want to update a value in a nested field, you can do so using dot notation. So:
FirebaseManager.shared.firestore.collection("users").document(uid).updateData([
"userRecipes.Cheese.recipeTitle": "Cheesiest"
])
Also see this similar question for JavaScript which I gave a few hours ago: Adding rating to a movie using Firebase and React

How to Order An Array Of Data and Display It In That Order (Data On Firebase)

I have a project where I want to display badges (an image with a title) in a horizontal scroll view. The data for the badges is stored in a Firestore database. My code can fetch the data and show the badges in a horizontal scroll view. At the moment the badges are in a random order and I want to be able to sort them so they are in the correct order. For example:
Currently I have 4 badges, These include:
10k steps badge, 20k steps badge, 30k steps badge, 40k steps badge
This is the order I want them to be displayed in the horizontal scrollview however they are in a random order.
How can I order this data correctly? I am currently using a ForEach to Loop through the data.
My Model for the badges is as follows:
struct FitnessBadge: Identifiable {
var id: String
var fitness_badge_name: String
var fitness_badge_details: String
var fitness_badge_image: String
}
The View for the Badges is as follows:
struct FitnessBadgeView: View {
var fitnessBadge: FitnessBadge
var body: some View {
VStack(alignment: .center){
// Downloading Image From Web...
WebImage(url: URL(string: fitnessBadge.fitness_badge_image))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 150)
.cornerRadius(15)
HStack(spacing: 8){
Text(fitnessBadge.fitness_badge_name)
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.black)
}
HStack(alignment: .center){
Text(fitnessBadge.fitness_badge_details)
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(2)
Spacer(minLength: 0)
}
}
}
}
How the badges are being displayed in a scroll view is as follows:
ScrollView(.horizontal, showsIndicators: false, content: {
VStack(alignment: .leading) {
Text("Fitness Badges")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.black)
HStack(spacing: 10){
ForEach(AchievementPageModel.filteredFitnessBadges){fitnessBadge in
// Badge View...
ZStack(alignment: Alignment(horizontal: .center, vertical: .top), content: {
FitnessBadgeView(fitnessBadge: fitnessBadge)
})
}
}
}
})
I want it to go: 10k steps, 20k steps, 30k steps etc.
The AchievementPageModel.filteredFitnessBadges array is as follows:
class AchievementPageViewModel: NSObject, ObservableObject {
// Fitness Badges Data...
#Published var fitnessBadges: [FitnessBadge] = []
#Published var filteredFitnessBadges: [FitnessBadge] = []
}
And the fetching data for the badges is as follows:
func fetchFitnessBadgeData(){
let db = Firestore.firestore()
db.collection("FitnessBadges").getDocuments { (snap, err) in
guard let fitnessBadgeData = snap else{return}
self.fitnessBadges = fitnessBadgeData.documents.compactMap({ (doc) -> FitnessBadge? in
let id = doc.documentID
let name = doc.get("fitness_badge_name") as! String
let image = doc.get("fitness_badge_image") as! String
let details = doc.get("fitness_badge_details") as! String
return FitnessBadge(id: id, fitness_badge_name: name, fitness_badge_details: details, fitness_badge_image: image)
})
self.filteredFitnessBadges = self.fitnessBadges
}
}
There is a terrific answer by #HunterLion but it could become impractical and not scaleable because it assumes you're loading ALL of the documents from FitnessBadges into memory (an array) to allow for sorting.
What if there are 1 million documents? That could be a LOT of data and overwhelm the device.
Another option is to let Firestore do the heavy lifting; instead of sorting on your app, let Firestore do it!
With larger amounts of data, you will be way better off allowing Firebase to sort on the server and present that sorted data to your app because then you can read smaller 'chunks' of data through pagination.
A better solution would be to use order(by: like this
db.collection("FitnessBadges").order(by: "fitness_badge_name").getDocuments
That sorts millions of documents in a flash on the server and then you can get just the section you want - records 1-10 for example, and then 11-20 and they will be sorted.
For reference: Firestore Pagination
In the view where badges are being displayed, replace:
ForEach(AchievementPageModel.filteredFitnessBadges)
with:
ForEach(AchievementPageModel.filteredFitnessBadges.sorted { $0.fitness_badge_name < $1.fitness_badge_name })
This will sort by the field fitness_badge_name. I have assumed that's the field that contains "10k", "20k"... if the key is another field, just replace the variable in the .sorted() function.

Update Text Field only when Variable updates from the cloud in IOS app

I am using Firebase to append a String of data for an Array. I would then like to display this data on the screen. The view on the screen is a List and each Post is looking at a different part of the Array (example; postName[0] vs. postName[1].
My problem is that as soon as the screen loads, the data has not completely loaded from the cloud yet and therefore the Array is empty. This causes the app to crash saying that "Index out of range" since there is nothing in an array that a Text box is trying to read from.
The Array receives data from Firebase and if the data arrives fast enough no issue occurs, however, sometimes the data does not come fast enough and crashes saying index not in range.
Is there anything I can set up to not load the Text field until the data has finished loading?
Code provided:
List(fetchPostModel.postsNearby, id: \.self) { post in
ZStack {
if !fetchPostModel.postName.isEmpty { Text(fetchPostModel.postName[Int(post) ?? 0])
.font(.title)
.bold()
.padding()
} else { Text("Loading...").font(.title).bold().padding() }
}
.onAppear {
fetchFromCloud(postNumber: fetchFromCloud.postNumber[Int(post) ?? 0])
}
}
To prevent the "Index out of range" you can unwrap the property first to avoid the Int(post) ?? 0
if !fetchPostModel.postName.isEmpty {
if let postIndex = post {
Text(fetchPostModel.postName[Int(postIndex)])
.font(.title)
.bold()
.padding()
}
} else { Text("Loading...").font(.title).bold().padding() }
you can create one extension for that
extension Collection where Indices.Iterator.Element == Index {
public subscript(safe index: Index) -> Iterator.Element? {
return (startIndex <= index && index < endIndex) ? self[index] : nil
}
}
Now it can be applied universally across different collections
Example:
[1, 2, 3][safe: 4] // Array - prints 'nil'
(0..<3)[safe: 4] // Range - prints 'nil'
In your problem, you can use like that
List(fetchPostModel.postsNearby, id: \.self) { post in
ZStack {
if let currentPostName = fetchPostModel.postName[safe: post] {
Text(currentPostName)
.font(.title)
.bold()
.padding()
} else {
Text("Loading...").font(.title).bold().padding()
}
}
.onAppear {
fetchFromCloud(postNumber: fetchFromCloud.postNumber[Int(post) ?? 0])
}
}

Order a list of Core Data entities alphabetically off one attribute - SwiftUI

I have two entities in my project, DBTeam and DBPlayer. DBTeam has a one to many relationship with DBPlayer allowing multiple players to be associated with one team; DBPlayer then has an attribute called name which is what I want to base the alphabetical order off of. What's the easiest way to list these alphabetically off of name?
Here is the code I am using to list them at the moment. I have tried .sorted but that just produced other errors.
struct PlayersView: View{
var team: DBTeam
var body: some View{
NavigationView{
List{
ForEach(Array(team.player as? Set<DBPlayer> ?? []), id: \.id){ player in //I want to sort this ForEach here
Text(player.name ?? "Player Name")
}
}
.navigationBarTitle("Players")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Try using localizedCaseInsensitiveCompare:
List {
let array = Array(team.player as? Set<DBPlayer> ?? [])
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
ForEach(array, id: \.id) { player in
Text(player.name ?? "Player Name")
}
}