How can I initialize attributes in SwiftUI? - ios

So this block of code allows me to initialize a data model
struct MockMessage: Identifiable {
let id: Int
let imageName: String
let messageText: String
let isCurrentUser: Bool
}
let MOCK_MESSAGES: [MockMessage] = [
.init(id: 0, imageName: "user", messageText: "Hey, what's up?", isCurrentUser: false),
.init(id: iCouldSetANewOne, imageName: iCouldSetANewOne, messageText: iCouldSetANewOne, isCurrentUser: iCouldSetANewOne)]
My question is
1- How can I replicate/initialize attributes instead of manually writing the same attributes over and over again
2- How can I modify it, let's say, the default rotation effect is set to a degree: 25 and I'd like to change it.
Example of Image attributes
Image(systemName: tabs[index].image)
.frame(height: 20)
.rotationEffect(.degrees(-25))
.foregroundColor(.yellow).scaleEffect(1.25)

If I understand correctly, you want to group a commonly used set of modifiers into 1 simple modifier. You can easily do it like this:
extension View {
func myStyle() -> some View {
self
.frame(...)
.rotationEffect(...)
.foregroundColor(...)
}
}
You can even parametrize this function if the values can be different.
About the "changing the rotation effect that is set to 25 degrees" I believe you can have multiple of the same effects, so if you want to rotate some view that is already rotated, just put another .rotationEffect() view modifier on top.

Related

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"))
}

#State and #Published properties in child views, SwiftUI

In MyController, I have the following property which is updated in other methods that are called:
#Published public var data = [Glucose]()
I also have a function, which limits this Published property by a given limit:
public func latestReadings(limit: Int = 5) -> [Glucose] {
// return latests results
}
In a SwiftUI View, I consume this data by the following, which works fine and updates when MyController's data changes:
#EnvironmentObject var data: MyController
var body: Some View {
ForEach(self.data.latestReadings(limit: 11), id: \.self) {
/// Display Text etc.
}
}
But, I want to call the following here, which converts the Glucose readings into a DataPoint array which the Chart consumes:
Chart(
data: self.data.latestReadings(limit: 37),
formattedBy: { (readings) -> [DataPoint] in
var result = [DataPoint]()
var i = 0
for reading in readings {
result.append(DataPoint(x: Double(i), y: reading.mmol))
i += 1
}
return result
}
)
...Which refers to another SwiftUI View defined as:
struct Chart: View {
// Properties
#State var data: [DataPoint] // I asusme this should be #State
var opt: ChartOptions
// Formatters
private var fmt: Formatting = Formatting.shared
// Init
public init(data: [Glucose], formattedBy:ChartDataFormatter) {
_data = State(wrappedValue: formattedBy(data)) // Again I assume this is probably wrong..
}
...draw views etc.
}
This all works on the first time the Chart is drawn, but the data property on the Chart view doesn't re-draw as the MyController data property changes. I assume I'm doing something wrong with state and observing changes here?
If I understood your workflow correctly you don't need state wrapper in Chart, because it prevents value update... so try without it, like
struct Chart: View {
// Properties
var data: [DataPoint]
// ...
// Init
public init(data: [Glucose], formattedBy:ChartDataFormatter) {
self.data = formattedBy(data)
}
// ...
#State breaks the connection with your Controller. Per the documentation #State should always be private.
Pass the data using #EnvironmentObject and manipulate it within the view or in the Controller.

Comparing two variables breaks building process

I have big problem with comparing strings in SwiftUI. I have declared global observable object called Order:
class Order: ObservableObject {
#Published var stationSymbol: String = ""
}
and I have some repository of locations:
class LocationsRepository: ObservableObject {
#Published collection: [Location] = [
// here is list of 3 Location objects
]
}
Location structure:
struct Location: Identifiable {
let id = UUID()
let name: String
let address: String
let canBeSelectedAsVehicleLocation: Bool
let stands: [Stand]
}
and in my view I'm using it like that:
#ObservedObject var locationsRepository = LocationsRepository()
#EnvironmentObject var order: Order
I'm listing all those locations using this:
ForEach(self.locationsRepository.collection.filter { $0.canBeSelectedAsVehicleLocation }) { location in
Everything works dope. I have inside this loop for every location 1 button which sets Order's stationSymbol to Location's name. The thing is that I want to set different bg color and font color of Location's buttons (those inside ForEach loop) so I have code:
Button(action: {
self.order.stationSymbol = location.name
}) {
HStack {
Image(systemName: "mappin.and.ellipse")
.padding(.trailing, 10)
.font(.system(size: 32))
.foregroundColor(Color("gray-500"))
VStack(alignment: .leading) {
Text(location.name)
.font(.system(size: 25))
.fontWeight(.bold)
.foregroundColor(Color("gray-800"))
.padding(.bottom, 5)
Text(location.address)
.foregroundColor(Color("gray-600"))
}
Spacer()
}
.padding()
.background(self.order.stationSymbol == location.name ? Color("blue-700") : Color("gray-200"))
}
.cornerRadius(7)
but when I'm trying to add one more short if statement (or if statement for selected location so I can list Location's stands Xcode fails at building and I can't run app. I tried to use LocationRepository as EnvironmentObject and comparing id's (I just changed type of stationSymbol and put there id from Location) but still I have same problem. After the failed build I get an error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
I also tried to reinstall Xcode (clearing cache, build folder etc.). I have newest MacOS.

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).

pass multidimensional array in collection view inside the table view

Please find the photo i want this type of view tableview with collection view
here members array is every time different means members are not fix in every cell, please help me out, ... thanks
Although it is based on how your application getting the data, for such a case, -in general- I would suggest to create your own custom Model instead of just declaring arrays/dictionaries of arrays. Example:
struct Family {
var title: String
var thumbnail: UIImage
var members: [Member]
}
struct Member {
var firstName: String
var lastName: String
var thumbnail: UIImage
}
so forth You could declare such an array of families:
let families = [Family(title: "YOGA", thumbnail: UIImage(named: "YOGA.jpg"), members: [Member(firstName: "John", lastName: "Smith", thumbnail: UIImage(named: "John.jpg"))]),
Family(title: "DEMO1", thumbnail: UIImage(named: "DEMO_1.jpg"), members: [Member(firstName: "Foo", lastName: "Bar", thumbnail: UIImage(named: "Foo.jpg"))])]
// and so on...
thus the data source for the table view would be the families, each collection view in the table view cell data source would be the members of the current family.
By following this approach you could easily templating your data to be more expressive.

Resources