Creating a dynamic List with section headers in SwiftUI? - ios

I'm experimenting with SwiftUI and I'm trying to build a workout tracker app that I've already sketched in UIKit. I am trying to build an exercise list for the user to consult, so when the app starts I load some exercises in CoreData, and the Exercise has the following properties.
#NSManaged public var name: String
#NSManaged public var muscleGroup: String
#NSManaged public var exerciseDescription: String
#NSManaged public var type: String
#NSManaged public var id: UUID
When I'm building the list view, I retrieve an array of Exercise from CoreData and load them in a list. This work fine with a basic list, the thing is I would like to create a List with section headers in alphabetical order. In UIKit I did this by building a dictionary of the form [ "Prefix" : [Exercise]], and using the keys as section headers. This is practical because I could give the user sorting option just by changing the dictionary and reloading the data. In SwiftUI, I can't seem to make it work because I can't work on the fetch request object before view creation, and I can't iterate over dictionaries. Here is my code:
import SwiftUI
import CoreData
struct ExerciseListUIView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Exercise.name, ascending: true)
]
) var exerciseList: FetchedResults<Exercise>
#State private var prefixList = [String]()
var body: some View {
return NavigationView {
VStack{
List(exerciseList, id: \.self) { exercise in
Text(exercise.name)
}
}
.navigationBarTitle("Exercises")
}
}
}
I tried a bunch of things but nothing seem to work. The most promising solution seems to store exercises directly in a different data structure, ExercisesByLetter(prefix: "String", exercises: [Exercise]), retrieve an array of [ExercisesByLetter] and iterate on that for building the list, but that would mean changing the way I store data, adding more work in the data storage functions and being forced to add different storages for each sorting option, like ExercisesByMuscleGroup, ExercisesByEquipment, and so on.
Let me know what you think,
Thanks.

For anyone interested in the solution, I came up with the following. I created a ViewModel for my main View to manipulate the data. In that view model I retrieve all the Exercise as an array of [Exercise] from CoreData, and store them in a property of the ViewModel. I created then an helper function to iterate through the list of [Exercise] and create an array of [ExercisesBy]. This type contains a property which stores the sorting criterion (first letter, muscle group, equipment, etc) and another property which stores the array of Exercise which adhere to that criterion.
This array of ExercisesBy is then iterated from my view to construct the sectioned list.

Related

Passing data to subview with Core Data and MVVM

I am using SwiftUI and Core Data with MVVM.
I have a ForEach loop and I want to pass the data to the subview. First I did this using a property like this:
#StateObject var viewModel = ListViewModel()
ForEach(viewModel.items) { item in
NavigationLink {
ItemDetailView() // empty view
} label: {
ItemListRowView(name: item.name!)
}
}
Then in the subview ListRowView would be something like:
let name: String
Text("\(name)")
And the view model where the ForEach is grabbing its data:
#Published var items: [ItemEntity] = []
#Published var name: String = ""
func getItems() {
let request = NSFetchRequest<ItemEntity>(entityName: "ItemEntity")
do {
items = try dataManager.context.fetch(request)
} catch let error {
print("\(error.localizedDescription)")
}
}
That works as expected but now I want to edit the data and pass more properties to the subviews. I think this means I need to use bindings and #ObservedObject in the subviews.
What I see commonly done is one would make a custom Item data type conforming to Identifiable protocol, for example:
struct Item: Identifiable {
let id: UUID
let name: String
}
And then they'd update their ForEach to use the Item type and do something like let items: [Item] = [] but I've already got let items: [ItemEntity] = [] with ItemEntity being the name of the Core Data Item entity.
What I suspect needs to happen is in my getItems method, items needs to be changed to use an Item data type. Is this correct? Or how should I go about this? I'm shiny new to Core Data and MVVM and any input will be super appreciated.
Edit: I've seen this done too but I'm not sure if it's what I'm looking for:
ForEach(viewModel.items.indicies) { index in
SubView(viewModel.items[index])
}
Couple of mistakes:
ForEach is a View, not a loop, if you attempt to use it with indices it will crash when you access an array by index in its closure. In the case of value types you need to supply the ForEach with an id which needs to be a property of the data that is a unique identifier. Or the data can implement Identifiable. However, in the case of objects like in Core Data, it will automatically use the object's pointer as its id, which works because the managed object context ensures there is only one instance of an object that represents a record. So what this all means is you can use ForEach with the array of objects.
We don't need MVVM in SwiftUI because the View struct is already the view model and the property wrappers make it behave like a view model object. Using #StateObject to implement a view model will cause some major issues because state is designed to be a source of truth whereas a traditional view model object is not. #StateObject is designed for when you need a reference type in an #State source of truth, i.e. doing something async with a lifetime you want to associate with something on screen.
The property wrapper for Core Data is #FetchRequest or #SectionedFetchRequest. If you create an app project in Xcode with core data checked the template will demonstrate how to use it.

Is there any way to optimize struct copy in Swift?

I'm developing an iOS app and have the following data model:
struct Student {
var name: String?
var age: UInt?
var hobbies: String?
...
}
This model is used as the data source in one view controller, where each property value will be filled in an UITextfield instance so that the user can edit a student's information. Every time a user finishes typing an item, e.g. the name, the new value will override the old model's corresponding property.
The problem is, since struct is a value type instead of a reference type, a new model instance is generated every time I assign a new property value to it. There may be above 20 properties in my model and I think so many copies are quite a waste. For some reasons I'm not allowed to use class. Is there any way to optimize this? Will these copies cause any performance issues?
you can create a func with mutating keyword like below
struct Point {
var x = 0.0
mutating func add(_ t: Double){
x += t
}
}
find more here

Preview fails with core data lookup but works with random value in Swift 5 / Core Data / FetchRequest

I am trying to make a temporary preview work but am running into an unusual problem. I have a view that passes along the UUID of the list item that is clicked into a subview. The subview uses this item to populate related items. When I send a randomly generated UUID the code works, but obviously nothing populates since that random UUID doesn't correlate to child objects. I wrote a #FetchRequest to pull Core Data and assign a real UUID, which is called as a static function in the preview provider. When I do this, the preview doesn't compile - no error is ever generated. Here is the code:
struct ViewList_Previews: PreviewProvider {
#FetchRequest(
entity: Item.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Item.userOrder, ascending: true)],
animation: .default)
private static var items: FetchedResults<Item>
static func assignUUID() -> UUID {
var tempID = UUID()
for item in items {
tempID = item.id!
}
print (tempID)
return tempID
}
static var previews: some View {
ViewList(listID: UUID())
}
}
I replaced ViewList(listID: UUID()) with ViewList(listID: assignUUID()), but that is when the preview fails.
There's got to be a better way to do this. I shouldn't have to call this function at all and instead fetch a single record, then navigate to the item.id of that first record, set it as a value, and use that in the ViewList pull. I've searched THE ENTIRE Internet (really, just SO) and have found code for older versions of Swift, but none of it compiles in Swift 5. I'm at a loss for ideas and next steps with this and would appreciate any hints / advice.
This is one of the frustrating parts of working with Core Data and the lack of clear documentation out there. I figured this out using some hints from this SO Q&A: How to fill TextField with data from Core Data and update changes?
My errors started with the main view, where I was using NavigationLink incorrectly to call a detail view. I should have been calling the detail view of a Core Data object using:
ViewList(item: item)
Where item was the individual member of the #FetchRequest object, 'items'. This was iterated through a ForEach loop.
In the detail view, I needed to declare this item as the Core Data entity as follows:
var item: Item
Where Item is the Core Data entity I created.
I haven't been able to get the preview to work like it is supposed to, but the following code prevents it from crashing:
struct ViewList_Previews: PreviewProvider {
#Environment(\.managedObjectContext) private static var viewContext
static var previews: some View {
let sampleItem = Item.init(context: viewContext)
ViewList(item: sampleItem)
.environmentObject(Item())
}
}
I thought that sampleItem = Item.init would bring up the initial item in the Entity, but clearly that doesn't seem to be working. I'm hoping someone will have an answer to this part of the question. I hope the earlier explanations help others with similar problems - it seems to be a common question on SO.

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

How to name an instance of a struct the contents of a variable - Swift

There is almost certainly a better way of doing this and I'd love to know but I can't phrase it in a question so essentially here is my problem:
I am creating an app that presents a list of items(In a table view) which have various bits of data that come along with the item(String Int Date ect). I've decided that the best way to store this data is in a struct because it allows me to store lost of different types of data as well as run processes on it.
The problem is that I want to have theoretically an infinite number of items in the list and so I need to make lost of instances of the Item struct without predetermining the names of each instance.
I would then store these instance names in an array so that they can be listed in the table view.
I'm completely stuck at this point I've spent hours looking and I just can't make sense of it I'm sure its stupidly easy because hundreds of apps must need to do this. I'm Open to anything thanks.
Currently, I have a struct:
struct Item() {
var data1: String
var data2: String // (But Should be Int)
var data3: String
func setDate() {
// code
}
func returnDate() {
// code
}
}
and then in the view controller I have:
#IBAction func SubmitButton(_ sender: UIButton) {
var textField1 = Item(data1: textField1.text!, data2: textFeild2.text!, data3: "Units")
print(textField1.data1)
}
I am not completely sure what your goal is but I guess the final goal is to have an array of your Item Objects, which then can be used to populate an UITableView??? Without setting up a name for this Instances?
The most easiest solution would be to create a array as a Class Property for storing all the Items which is empty like this:
var items : [Item] = []
And then in viewDidLoad(), you can call a function which populates the item array with all the available Items.
This Item Array can then be used to populate the UITableView.
Second Part:
You got this IBAction which creates a new Item. I guess this is supposed to add a new Item to the existing ones.
You can add this new Item to the existing Item Array by using
items.append(Item(<parameters>))
And don't forget to reload the UITableView to include the new data.
tableView.reloadData()

Resources