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.
Related
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"))
}
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.
I'm struggling with a SwiftUI app with SwiftUI life cycle where I have created a fairly standard Core Data list and have added a search field to filter the list. Two views - one with a predicate and one without. The unfiltered list generation works as expected including swipe to delete. The filtered list appropriately displays the filtered list, but on swipe to delete I get completely unpredictable results. Sometimes the item disappears, sometimes it pops back on the list, sometimes the wrong item is deleted. I cannot discern any pattern.
Here's the view:
struct MyFilteredListView: View {
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.horizontalSizeClass) var sizeClass
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \InvItem.name, ascending: true)],
animation: .default) private var invItems: FetchedResults<InvItem>
var fetchRequest: FetchRequest<InvItem>
init(filter: String) {
//there are actually a bunch of string fields included - just listed two here
fetchRequest = FetchRequest<InvItem>(entity: InvItem.entity(), sortDescriptors: [], predicate: NSPredicate(format: "category1 CONTAINS[c] %# || name CONTAINS[c] %# ", filter, filter))
}
var body: some View {
let sc = (sizeClass == .compact)
return List {
ForEach(fetchRequest.wrappedValue, id: \.self) { item in
NavigationLink(destination: InvItemDetailView(invItem: item)) {
InvItemRowView(invItem: item)
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: sc ? 100 : 200)
.padding(.leading, 10)
}//link
}
.onDelete(perform: deleteInvItems)
}//list
}
private func deleteInvItems(offsets: IndexSet) {
withAnimation {
offsets.map { invItems[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this - raise an alert
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
Core Data is pretty standard:
class InvItem: NSManagedObject, Identifiable {}
extension InvItem {
#NSManaged var id: UUID
#NSManaged var category1: String?
#NSManaged var name: String
//bunch more attributes
public var wrappedCategory1: String {
category1 ?? "No category1"
}
//other wrapped items
}//extension inv item
extension InvItem {
static func getAllInvItems() -> NSFetchRequest<InvItem> {
let request: NSFetchRequest<InvItem> = InvItem.fetchRequest() as! NSFetchRequest<InvItem>
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [sortDescriptor]
return request
}
}//extension
I'm guessing there is something about the offsets behavior in deleteInvItems(offsets: IndexSet) that I don't understand, but
I have not been able to find anything on this. This same code works as expected in the unfiltered list.
Any guidance would be appreciated. Xcode Version 12.2 beta (12B5018i) iOS 14
First Edit:
I figured out the pattern. Apparently the IndexSet refers to the entire entity, not the filtered items. Say for example that the unfiltered list has 10 items and the filtered list has 3 items. When I delete the third item in the filtered list the result is the deletion of the third item in the unfiltered list, so unless those two are the same, the third item reappears on the filtered list and the third item on the unfiltered list is deleted from Core Data. If I then delete the third item on the filtered list again, the fourth item on the original list is deleted (since the third is already gone).
So the question becomes - how do I get a reference to the object I want
to delete - IndexSet does not work.
It is likely because your ForEach is using a wrappedValue which may or may not be updated.
https://developer.apple.com/documentation/swiftui/binding/wrappedvalue
I suggest you look at the code that is provided when you create a new project (you are using some of it) so you can adjust and have more accurate data.
ForEach(items) { item in
Also, when deleting you are mixing the offset from fetchRequest and items in invItems stick with only one.
If you want to be able to dynamically filter the list I have had better luck using a FetchedResultsController wrapped in an ObservedObject
https://www.youtube.com/watch?v=-U-4Zon6dbE
I'm new to SwiftUI and coding in general, so sorry if this has being covered before. I did search about it but I couldn't find it clear enough.
I'm trying to make an app for storing notes and tasks. (I'm not planning to put it on the store, I'm too newbie for that, but I do want to learn Swift and working on an actual app is more useful to me than reading about it.) I have an entity called "Base" with 8 attributes and automatic Codegen Class Definition selected. I prefer to keep it that way, no manual please.
I have three fetch requests. One gets me all data from Core Data. The other two filter one attribute called campoEstado. For now in campoEstado I only store one of two possible values, as strings: "Activas", and "Pospuestas". In the future I may add more so I can't use a boolean for this.
I get a List working with one fetch request. But I can't change that source when the app is running.
I made a Picker with .pickerStyle(SegmentedPickerStyle()) that shows me: Todo, Activas, Pospuestas. When the user selects one of this tabs in the picker the list should change to:
Tab 1: All tasks
Tab 2: A filtered list containing only tasks with campoEstado = "Activas" (calls the fetch request filtroActivas)
Tab 3: A filtered list containing only tasks with campoEstado = "Pospuestas" (calls the fetch request filtroPospuestas)
How it should look:
My code in ContentView:
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Base.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Base.campoNota, ascending: true),
NSSortDescriptor(keyPath: \Base.campoFechaCreacion, ascending: true)
], predicate: NSPredicate(format: "campoEstado == %#", "Activas")
) var filtroActivas: FetchedResults<Base>
#FetchRequest(entity: Base.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Base.campoNota, ascending: true),
NSSortDescriptor(keyPath: \Base.campoFechaCreacion, ascending: true)
], predicate: NSPredicate(format: "campoEstado == %#", "Pospuestas")
) var filtroPospuestas: FetchedResults<Base>
#FetchRequest(entity: Base.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Base.campoNota, ascending: true),
NSSortDescriptor(keyPath: \Base.campoFechaCreacion, ascending: true)
]
) var bases: FetchedResults<Base>
var body: some View {
NavigationView {
List {
Picker("Solapas", selection: $selectorIndex) {
//This should connect to ForEach source, currently "bases"
}
.pickerStyle(SegmentedPickerStyle())
ForEach(bases, id: \.self) { lista in
NavigationLink(destination: VistaEditar(base: lista)) {
Rectangle()
.foregroundColor(.gray)
.frame(width: 7, height: 50)
VStack(alignment: .leading) {
Text(lista.campoNota ?? "Unknown title")
.font(.headline)
Text(lista.campoEstado ?? "Activas")
}
}
}
I have two problems:
I don't know how to connect the selected tab in the picker with the source of ForEach inside List
I made the list work for one fetch request, the one that brings me every record in Core Data, but I don't know how to change the ForEach source when the app is running. I have tried an array of names of variables for every one of the fetch requests variables names (bases, filtroActivas and filtroPospuestas) putting them in [] but that didn't work.
I know this isn't elegant, I just need it to work first and then go for efficiency and elegance. I'm sure there is some stupid thing I'm not seeing but it's been a week and I'm getting desperate.
I hope I was clear, if you need more information please ask.
If anyone can help me I would very much appreciate it. Thanks in advance!
I worry I may get downvoted for this because it's not an entirely complete answer. But I'm out of time and I wanted to see if I could get you in the right direction.
What I believe you're looking for is an Array of Arrays. A FetchedResult will return an optional array, which you'll want to ensure isn't nil. I used compactMap in my example to generate non-nil array's of my sample optional arrays which are intended to simulate what you are getting back from your #FetchRequest properties. You should be able to load this code into a new project in Xcode to see the results. Again it's not a complete answer but will hopefully help you along in your project. I hope this helps, keep working at it, you'll get it!
struct ContentView: View {
// Sample data to mimic optional FetchedResults coming from CoreData.
// These are your #FetchRequest properties
let fetchedResultsBases: [String?] = ["Red", "Green", "Blue"]
let fetchedResultsFiltroPospuestas: [String?] = ["1", "2", "3"]
let fetchedResultsFiltroActivas: [String?] = ["βΊοΈ", "π", "π"]
// Options for your picker
let types = ["Bases", "Pospuestas", "Activas"]
#State private var groups = [[String]]()
#State private var selection = 0
var body: some View {
NavigationView {
VStack {
Picker("Solapas", selection: $selection) {
ForEach(0..<types.count, id: \.self) {
Text("\(self.types[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
List {
ForEach(0..<groups.count, id: \.self) { group in
Text("\(self.groups[self.selection][group])")
}
}
}
}
.onAppear(perform: {
self.loadFetchedResults()
})
} // This is the end of the View body
func loadFetchedResults() {
// This is where you'll create your array of fetchedResults, then add it to the #State property.
var fetchedResults = [[String]]()
// Use compact map to remove any nil values.
let bases = fetchedResultsBases.compactMap { $0 }
let pospuestas = fetchedResultsFiltroPospuestas.compactMap { $0 }
let filtroActivas = fetchedResultsFiltroActivas.compactMap { $0 }
fetchedResults.append(bases)
fetchedResults.append(pospuestas)
fetchedResults.append(filtroActivas)
groups = fetchedResults
}
} // This is the end of the ContentView struct
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).