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"))
}
Related
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()
}
I am trying to figure out a way to have different types of the same struct and cannot figure out whether to "unify" a singular struct with optionals, or split them up into multiple structs. For example, right now I am making a sports application and have a Game struct that differs based on the type of Sport being played: team based (football, basketball) or individual (golf).
The end goal in my application is to pass either struct to a UITableViewCell and setup each cell depending on whether it is a team or individual game.
struct Game {
let sportID: String
let date: Date
let location: String
let type: String //Team or Individual
let teams: [Team]? //Team Game
let players: [Player]? //Individual Game
}
OR
struct TeamGame: Game {
let sportID: String
let date: Date
let location: String
let teams: [Team]
}
struct IndividualGame: Game {
let sportID: String
let date: Date
let location: String
let players: [Player]
}
Is it ultimately based on preference or is there a better/standard manage this?
Will these structures be used outside of the code you are writing (i.e. if you are developing a library that will be used by several independent consumers)?
If not, don't let analysis paralysis get you. Pick one way that feels less awkward, and change it later if you feel it doesn't work out. You don't really risk much as long as the code stays in a single app. You can always rewrite it. Besides, it is really hard to give architecture advice without seeing the whole picture.
Answering your question directly, the most "natural" way (IMO) will be:
enum GameMode {
case individual(players: [Player])
case team(teams: [Team])
}
struct Game {
let sportID: String
let date: Date
let location: String
let mode: GameMode
}
Edit: more idiomatically, as suggested by #Jessy
struct Game {
enum Mode {
case individual(players: [Player])
case team(teams: [Team])
}
let sportID: String
let date: Date
let location: String
let mode: Mode
}
This is my list...
List {
ForEach(viewModel.flight.flightEvents, id: \.id) { flightEvent in
...
}
}
The view model has this...
#Published var flight: Flight
Flight looks like this...
struct Flight: Identifiable {
let flightEvents: [FlightEvent]
let id = UUID()
}
FlightEvent...
struct FlightEvent: Identifiable {
let id = UUID()
let dateComponents: DateComponents
}
This code results in the items in the flightEvents array immediately binding to the list.
If, however, I change the collection bound to the List to this...
viewModel.flight.flightEvents.filter { $0.dateComponents.date! <= Date() }
... then the items never appear in the list - the filter is false for every item to begin with. I have a workaround where I copy the filtered flight events to a different array when the user refreshes and bind this array instead.
Is there a way to have SwiftUI continuously evaluate the filter expression? I think it's not binding because the base collection hasn't changed.
Is there a way to have SwiftUI continuously evaluate the filter expression?
Actually, no. That would cause unstoppable view redrawing, which is very undesirable. But you can set up timer (say for 1 sec, or better more) and in callback force
viewModel.objectWillCange.send()
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).
I have an array of arrays, theMealIngredients = [[]]
I'm creating a newMeal from a current meal and I'm basically copying all checkmarked ingredients into it from the right section and row from a tableview. However, when I use the append, it obviously doesn't know which section to go in as the array is multidimensional. It keeps telling me to cast it as an NSArray but that isn't what I want to do, I don't think.
The current line I'm using is:
newMeal.theMealIngredients.append((selectedMeal!.theMealIngredients[indexPath.section][indexPath.row])
You should re-model your data to match its meaning, then extract your tableview from that. That way you can work much more easily on the data without having to fuss with the special needs of displaying the data. From your description, you have a type, Meal that has [Ingredient]:
struct Meal {
let name: String
let ingredients: [Ingredient]
}
(None of this has been tested; but it should be pretty close to correct. This is all in Swift 3; Swift 2.2 is quite similar.)
Ingredient has a name and a kind (meat, carbs, etc):
struct Ingredient {
enum Kind: String {
case meat
case carbs
var name: String { return self.rawValue }
}
let kind: Kind
let name: String
}
Now we can think about things in terms of Meals and Ingredients rather than sections and rows. But of course we need sections and rows for table views. No problem. Add them.
extension Meal {
// For your headers (these are sorted by name so they have a consistent order)
var ingredientSectionKinds: [Ingredient.Kind] {
return ingredients.map { $0.kind }.sorted(by: {$0.name < $1.name})
}
// For your rows
var ingredientSections: [[Ingredient]] {
return ingredientSectionKinds.map { sectionKind in
ingredients.filter { $0.kind == sectionKind }
}
}
}
Now we can easily grab an ingredient for any given index path, and we can implement your copying requirement based on index paths:
extension Meal {
init(copyingIngredientsFrom meal: Meal, atIndexPaths indexPaths: [IndexPath]) {
let sections = meal.ingredientSections
self.init(name: meal.name, ingredients: indexPaths.map { sections[$0.section][$0.row] })
}
}
Now we can do everything in one line of calling code in the table view controller:
let newMeal = Meal(copyingIngredientsFrom: selectedMeal,
atIndexPaths: indexPathsForSelectedRows)
We don't have to worry about which section to put each ingredient into for the copy. We just throw all the ingredients into the Meal and let them be sorted out later.
Some of this is code is very inefficient (it recomputes some things many times). That would be a problem if ingredient lists could be long (but they probably aren't), and can be optimized if needed by caching the results or redesigning the internal implementation details of Meal. But starting with a clear data model keeps the code simple and straightforward rather than getting lost in nested arrays in the calling code.
Multi-dimentional arrays are very challenging to use well in Swift because they're not really multi-dimentional. They're just arrays of arrays. That means every row can have a different number of columns, which is a common source of crashing bugs when people run off the ends of a given row.