Passing data to subview with Core Data and MVVM - ios

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.

Related

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.

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

Creating a dynamic List with section headers in SwiftUI?

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.

Combine two TableViewControllers

I have the following problem:
I have an application, which uses two tableViewControllers. The first TableViewController lists
struct Task{
let type: String
[other attributes]
}
These Tasks are stored in a TaskStore class, with a static shared instance which stores all Tasks in the application.
class TaskStore {
var tasks: [Task] = []
class var sharedInstance: TaskStore {
struct Static {
static let instance = TaskStore()
}
return Static.instance
}
}
Now, for each Task, I need to store a set of Subtasks associated to that instance of Task. Hence, I created a Subtask struct
struct Subtask{
let subtype: String
[other attributes]
}
and created a SubtaskStore class, which I modified to no longer include the static instance, but instead:
class SubtaskStore {
var subtasks: [Subtask] = []
class var sharedInstance: SubtaskStore {
let instance = SubtaskStore()
return instance
}
}
Note: This seems to me to be much more beneficial than simply including an array of Subtasks as a member of the Task struct, because I can create methods that operate of the SubtaskStore.sharedinstance.
Then I amended the Task struct as follows
struct Task{
let type: String
var subtasks: SubtaskStore // as oppose to: var subtasks [Subtask] = []
}
The idea is that now every Task has its own SubtaskStore, which stores all the Subtasks for the Task at hand.
First question: Is there a better way of modeling the hierarchy of Tasks to Subtasks? I am quite new to programming, but the design seemed intuitive.
I have successfully created an interactive TableViewController that displays the elements in TaskStore and updates the Table when a new Task is added/removed. I have now created a link between the Task TableViewController and the SubTask TableViewController; clicking on a TaskTableViewCell opens a new SubTaskTableViewController (currently empty).
Second question: Assume that the TaskTableViewCell selected by the user is in row 5. I want to display the elements in "TaskStore[4].subtasks" in the SubtaskTableViewController. How do I pass the number "4" from the selected TaskTableViewCell in the TaskTableViewController to the SubTaskTableViewController and display on the elements in the TaskStore[4].subtasks.sharedInstace?
I would argue that your architecture isn't a particularly good setup.
The big thing to note is that if you want to change some property of ALL tasks, like adding a color property, you need to implement it in two Structs, then update two separate view controllers to colorize. What happens if you want to allow subtasks to have subtasks - do we have to continuously create deeper structs of type SubSubTask? Even before we make any changes - I'm sure you wont be excited to implement two separate, yet almost identical, table view controllers.
The better method is to have a single struct, Task which allows for a subtasks array. Your TaskStore will only track the top-level tasks, subtasks are "owned" by their parent tasks.
struct Task {
var children = [Task]()
// other properties
}
class TaskStore {
var tasks = [Task]()
// Swift now supports static vars on classes
static var sharedInstance = TaskStore()
}
With this setup, you can also just write a single table view controller which displays a list of tasks provided to it (from some source of your choice). That task list could be a sublist, but it would be irrelevant to the table view controller - which is great - because it makes your code simpler and much easier to maintain.
As for part two, you can pass information to other view controllers if you are using storyboards using the prepare for segue method of the sending view controller. There's lots of information on the topic here on SO.
How do you share data between view controllers and other objects in Swift?

Resources