How to include collections in a users document in Firebase / Swift - ios

I am new to coding and swift and I'm trying out lots of different things and working with firebase. In my app I'm getting step data using HealthKit and using that to practise with data in swift.
 
I have made the app get the users steps and convert them into points. 1,000 steps = 2 points. In the App there is a selection of "boxes" to choose from. The aim is to fill these "boxes" up with points. However you can only work towards filling up two "boxes" at a time (haven't made code that limits user to supporting 2 boxes at a time yet).
 
I have written code so that when you tap a "box" takes you to a detailed view of the "box" with a button labelled "WorkTowards". When this button is tapped, it gets added to "workingTowardsBoxes" which is displayed in the user's profile to indicate which boxes are being worked on.
 
I want to make the "boxes" a user is working towards be stored with their other data like username, bio etc. in their document on firebase but am unsure how to do this.
 
I'm new to firebase too but I imagine there is a way to give a field two separate values and the values being a document in a different collection.
 
The "Boxes" are stored in a collection in firebase so each "Box" has it's own uid. I need a field in the users document that contains two "Boxes" titled "activelyWorkingTowardsBoxes". The values in this field should change when the user changes the "Box" they are working on.
 
At this stage I want to have the "boxes" a user wants to 'workTowards' to be stored in the user's firebase document. Then I will work on adding the points to the boxes.
The code for the UserModel:
struct UserModel {
var username : String
var pic : String
var bio: String
var uid : String
// something like this needs to be included?:
// var activelyWorkingOnBoxes: WorkingOnBoxes
}
The code for the Boxes model is:
struct Box: Identifiable {
var id: String
var box_name: String
var box_type: String
var box_profileImage: String
// To identify whether it is being supported...
var isWorkingOn: Bool = false
}
The Code For The WorkingOnBoxes is:
struct WorkingOnBoxes: Identifiable {
var id = UUID().uuidString
var box: Box
}
Here are my 'Working on' functions:
func isWorkingOn() -> Bool {
return sharedData.workingOnBoxes.contains { boxData in
return self.boxData.id == boxData.id
}
}
func addToWorkingOn() {
if let index = sharedData.workingOnBoxes.firstIndex(where: {
boxData in
return self.boxData.id == boxData.id
}){
// Remove from working on...
sharedData.workingOnBoxes.remove(at: index)
}
else {
// Add to working on...
sharedData.workingOnBoxes.append(boxData)
}
}
Any Help Would Be Much Appreciated!

Related

Best practices for combining #published arrays in real time?

So I'm writing code that can find nearby users using both locally (MultiPeers and Bluetooth Low Energy) and through network (Using a real-time database to find nearby users by their location
class ViewModel: ObservableObject{
#Published nearbyMultipeers: [User] = []
#Published nearbyBluetoothUsers: [User] = []
#Published nearbyGeoUsers: [User] = []
// This gets the nearby users by GeoLocation and updates the nearbyGeoUsers array
func getNearbyUsersByGeoLocation(){ /* ... */ }
// This will loop through all of the nearby users obtained via multipeer and grab their user data from the database and append it to the nearbyMultipeers array
func getUsersFromPeers(nearbyPeers: [Peer])( /* ... */ )
}
Now these lists will constantly update (as multipeers only works when the app is in foreground and naturally you will move in and out of the range of nearby users).
The issue that that there will be duplicate data at times, nearbyBluetoothUsers may contain some nearbyMultipeers, nearbyGeoUsers may contain some nearbyBluetoothUsers etc. I need a way to display a list of all of these users in real-time without displaying duplicate data.
For simplicity let's say I'm displaying them in a list like so
struct NearbyUsersView: View {
// This observable object contains information on the nearby peers //(multipeers)
// This is how I get the nearby peers
#ObservableObject var multipeerDataSource: MultipeerDataSource
var body: some View {
VStack{
// Ideally I would display them in a scrollable list of some sort, this is
// just to illustrate my issue
ForEach(viewModel.$allUsersExcludingDuplicates){. user in
Text(user.name)
}
}
.onAppear{
viewModel.getNearbyUsersByGeoLocation()
}
.onChange(of: multipeerDataSource.$nearbyPeers) { nearbyPeers
// this array contains the nearby peers (users)
// We have to actually convert it to a `User`, or fetch the user data because //the objects here won't be the actual data it may just contain the user Id or some sort // so we have to grab the actual data
viewModel.getUsersFromPeers(nearbyPeers)
}
}
}
I omitted grabbing via bluetooth since it isn't necessary to understand the problem.
Now the only thing I can think of in the NearbyUsersView is to do
ForEach((viewModel.nearByMultipeers + viewModel.nearbyBluetoothUsers + viewModel.nearbyGeoUsers).removeDuplicates()) { user in /* ... */ }
But something tells me I won't have expected results
You could simply use a computed variable in your ViewModel, assuming that User conforms to Equatable like this:
public var nearbyUsers: Set<User> {
Set(nearbyMultipeers).union(Set(nearbyBluetoothUsers).union(Set(nearbyGeoUsers)))
}
This converts your arrays to sets, and creates one set by multiple unions. Sets can't have duplicates. If you need it as an array, you could do this:
public var nearbyUsers: [User] {
Array(Set(nearbyMultipeers).union(Set(nearbyBluetoothUsers).union(Set(nearbyGeoUsers))))
}
Lastly, if User conforms to Comparable, you could return a sorted array like this:
public var nearbyUsers: [User] {
Array(Set(nearbyMultipeers).union(Set(nearbyBluetoothUsers).union(Set(nearbyGeoUsers)))).sorted()
}

Pull unique items & calculations from an array in SwiftUI

I'm trying to create a Metrics view in my SwiftUI app. I'm building this so I can track my poker sessions I play. Each Session model looks like this:
struct PokerSession: Hashable, Codable, Identifiable {
var id = UUID()
let location: String
let game: String
let stakes: String
let date: Date
let profit: Int
let notes: String
let imageName: String
let startTime: Date
let endTime: Date
In my Metrics View I would like to iterate through all of the sessions which are stored in an array of type: [PokerSession] that displays a List of all the unique locations and their corresponding profit totals. This is how my code looks right now, obviously not working because I'm getting duplicate locations:
List {
ForEach(viewModel.sessions) { location in
HStack {
Text(location.location)
Spacer()
Text("$500")
.bold()
.foregroundColor(.green)
}
}
.navigationBarTitle(Text("Profit by Location"))
}
Does anyone know how I can grab unique locations and calculate their total profit? Many thanks!
I'd define a new type to store your totals:
struct LocationProfit: Identifiable {
let id = UUID()
let location: String
let profit: Int
}
Then you can group all your sessions by location in a dictionary, transform the sessions into a sum of profits, then transform the location and profit totals into our LocationProfit structure.
let locationProfits = Dictionary(grouping: sessions) { element in
element.location
}.mapValues { sessionsGroupedByLocation -> Int in
sessionsGroupedByLocation
.map { $0.profit }
.reduce(0, +)
}.map { locationProfitPair in
LocationProfit(location: locationProfitPair.key, profit: locationProfitPair.value)
}
Just stuff the whole conversion into your viewModel and iterate over the locationProfits in your View.
You need to filter which will return a filtered array of your PokerSessions by location then you reduce the filtered array to get the sum of your profit like this:
viewModel.sessions.filter( { $0.location == location}).reduce(0) { $0 + $1.profit})
edit with use case assuming it is in USD:
Text("$\(viewModel.sessions.filter( { $0.location == location}).reduce(0) { $0 + $1.profit}))")
You could also turn it into a string by add .description to the end of it. If you only need to display the data to the user, and don't need it generally available to the app, this is the simplest way of doing it.
You can generate an array of unique locations like this:
Array(Set(viewModel.sessions.map { $0.location }))
Use this in your ForEach to iterate over the location strings.
EDIT
To calculate the total per location, you can simply query your existing data (as described in Yrb's answer):
viewModel.sessions.filter({ $0.location == location }).reduce(0) { $0 + $1.profit }
Since you already use a view model, I suggest to hide both the creation of the unique locations list and the total profit lookup inside the view model, which will make your UI code much cleaner and more readable.
Also, if you have a lot of PokerSession entries, I suggest to generate the data only once and cache it inside the view model, e.g. using a custom data model as described in Rob's answer (or even simpler by generating a dictionary which maps from location string to total profit). The good thing about abstracting the data access away inside the view model is that you can introduce the caching approach later, without changing the UI layer, since you will be able to keep the same access methods and just change their implementation.
My solution was borrowed from most of your responses, thank you for the feedback. Below is the final code that worked, was able to avoid creating a dictionary and instead relied on map and reduce.
List {
ForEach(viewModel.uniqueLocations, id: \.self) { location in
HStack {
Text(location)
Spacer()
Text("$" + "\(viewModel.sessions.filter({$0.location == location}).reduce(0) { $0 + $1.profit})")
.bold()
.foregroundColor(.green)
}
}
.navigationBarTitle(Text("Profit by Location"))
}

How to convert Firebase result to List in SwiftUI

I wanted to make a list in swiftui from firebase data. So here's my UI
I've already make a data for that username "mike_one", and it's perfectly working,
this is my xcode result
{
IC2ol5bimucKu2d89u2YBz0Bqot2 = {
name = mike;
photo = "https://firebasestorage.googleapis.com/v0/b/instagram-ios-1ed09.appspot.com/o/photo%2Fa2.jpg?alt=media&token=9b6c58f1-eedc-4190-bc63-3f3325c84d77";
username = "mike_one";
};}
And this is my database
So what i'm asking right now is, How to make the result of the database to be a model? so I can use it as a list.
Please help me.
I appreciate your answer!
Strictly speaking, it would not be possible to convert textual firebase data to a List object. A SwiftUI List is defined as
A container that presents rows of data arranged in a single column
in other words it's a UI element, not a data element.
That being said a List is often backed by an Array or other data storage element. So you'll read Firebase data and store that in an array. Then that array backs your UI List object.
In this case, I would suggest creating a UserClass to hold the Firebase data
class UserClass {
var name = ""
var photoUrl = ""
var username = ""
}
and then array to store your users in
var userArray = [UserClass]()
then as data is read from Firebase, create the user objects and populate the array. Your Firebase code wasn't included so in brief
firebase.observe.... { snapshot in
let user = UserClass()
...populate user properites from the snapshot
self.userArray.append(user)
}
Then in your view, access the array elements to show them in the List object
struct ContentView: View {
var body: some View {
List(userArray) { user in
//do something with each user object
// like Text(user.username)
}
}
}

SwiftUI Struct for Dictionary entry

I have decided after several years of development to restart my project using SwiftUI to future proof as much as I can.
In my current project I have my data in several .CSV's which I then process into dictionaries and then create a list of entries on screen using an Array of keys which are generated programmatically from user input.
All examples I've seen for SwiftUI use JSON. However the structure of these files are identical to an Array of Dictionaries. My question is; is it possible to create a Struct of a dictionary entry to pass in a forEach watching an Array of Keys (data inside the dictionary will never change and I am not looking to iterate or watch the dictionary).
My main goal is to reuse as much as possible but am willing to change what I have to get full benefit of SwiftUI. Obviously if I change the way I store my data almost everything will be useless. If there's a real benefit to converting my data to JSON or even start using something like CoreData I will.
If I'm understanding correctly, you are looking to
Take some user input
Transform that into keys that correspond to your data dictionary
Extract the data for the matching keys into some struct
Display a list of those structs using SwiftUI
Here is a simple implementation of those steps.
import SwiftUI
// A dictionary containing your data
let data: [String: Int] = [
"apples": 5,
"bananas": 3,
"cherries": 12
]
// A struct representing a match from your data
struct Item {
var name: String
var quantity: Int
}
// A view that displays the contents of your struct
struct RowView: View {
var item: Item
var body: some View {
Text("\(item.quantity) \(item.name)")
}
}
struct ContentView: View {
#State private var searchText: String = ""
func items(matching search: String) -> [Item] {
// 2 - split the user input into individual keys
let split = search.split(separator: " ", omittingEmptySubsequences: true).map { $0.lowercased() }
// 3 - turn any matching keys/values in your dictionary to a view model
return split.compactMap { name in
guard let quantity = data[name] else { return nil }
return Item(name: name, quantity: quantity)
}
}
var body: some View {
VStack {
// 1 - get user input
TextField("Search", text: $searchText)
.padding()
// 4 - display the matching values using ForEach (note that the id: \.name is important)
List {
ForEach(items(matching: searchText), id: \.name) { item in
RowView(item: item)
}
}
}
}
}
You'll see that as you type in the text field, if you enter any of the strings "apples", "bananas", or "cherries", a corresponding row will pop into your list.
Depending on the size of your list, and what kind of validation you are performing on your users search queries, you might need to be a little more careful about doing the filtering/searching in an efficient way (e.g. using Combine to only split and search after the user stops typing).

Alamofire & Swift - Query and return JSON data based on a values

I am trying to create a method to return JSON objects using parameters.
I'll explain further, and how my application works.
Firstly, the user enters a product i.e. "MacBook", the user then presses the search button on the keyboard and all products with the name "MacBook" are returned and populated into a UITableView.
When the user selects a row, the data is passed to the next view controller i.e. "DetailViewController". The DetailedViewController has variables i.e. var productStock, var productPrice, var productBrand etc.
All the information is displayed and the app, so far, is working as planned.
However, I wish to create a method which can take certain parameters i.e. productBrand and productName. And search the whole API and return results which match the criteria, within the DetailsViewController. For example (pseudo-code)
var minimumStockAmount = 1
var productPrice: Int!
var url = "www.testapi.com/products"
func callApiWithParameters(productStock, productPrice) {
Alamofire.request((url), parameters: productBrand, productPrice) {
if productStock >= minimumStockAmount {
return productStock //This will return stock which is at-least 1
}
if price = productPrice - 20 {
return price //This is get items from the API where the price is the current product price minus 20. e.g. productPrice = 30, minus 20 will be 10. Should return any/all items in the API with a price of 10.
}
}
If there is any confusion, please tell me I will help to clarify. In essence, it's similar to when using SQL/SQLite the the SELECT * WHERE command(s).

Resources