Model objects that are not developer-defined? - swift

Every SwiftUI tutorial/example uses model objects that are defined by me, the guy writing the app. However, what is the best practice when the model objects are not under my direct control? For example, the HomeKit framework has an API to get all the rooms in a given home. It returns an array of HMRoom objects:
open class HMRoom: NSObject
{
open var name: String { get }
open var accessories: [HMAccessory] { get }
open var uniqueIdentifier: UUID { get }
// Update the room's name in a HomeKit-compliant way that notifies all other HomeKit apps
open func updateName(_ name: String) async throws
}
When I receive an array of HMRoom objects from the HomeKit API, what should I do with them to power SwiftUI? Should I create my own class that looks like this:
final class Room: ObservableObject
{
#Published var name: String
#Published var accessories: [Accessory]
#Published var uniqueIdentifier: UUID
private var representedRoom: HMRoom
init(homekitRoom: HMRoom)
{
// Copy properties from 'homekitRoom' to self, then set `representedRoom` to `homekitRoom` so we can use it to call the updateName(:) function
}
}
Is there, instead, a way for me to extend the HMRoom class directly to inform SwiftUI that name, accessories, and uniqueIdentifier are the properties we must watch for changes in order to reload views appropriately?
It's unclear to me what the best approach is to integrate #Published/#ObservableObject when I don't write the model classes/structs myself.

If you read Apple's document Choosing Between Structures and Classes the Use Classes When You Need to Control Identity section makes you think use a class, however if you read the Use Structures When You Don't Control Identity section and notice that every HomeKit object contains a uniqueIdentifier it turns out we can use structs yay!
struct Room: Identifiable {
let id: UUID
var name: String
}
class HomeDelegate: NSObject, HMHomeDelegate {
weak var homeManagerDelegate: HomeManagerDelegate?
// HomeManagerDelegate sets the primaryHome.
var home: HMHome? {
didSet {
oldValue?.delegate = nil
home?.delegate = self
reload()
}
}
func reload() {
let rooms: [Room]
if let home = home {
rooms = home.rooms.map { r in
Room(id: r.uniqueIdentifier, name: r.name)
}
}else {
rooms = []
}
homeManagerDelegate?.rooms = rooms
}
func home(_ home: HMHome, didUpdateNameFor room: HMRoom) {
if let index = homeManagerDelegate?.rooms.firstIndex(where: { $0.id == room.uniqueIdentifier } ) {
homeManagerDelegate?.rooms[index].name = room.name
}
}
Note: I'd recommend not using structs that mirror the home classes and instead create a struct that contains what you want to show in your UI. And in your model ObservableObject you could have several #Published arrays of different custom structs for different HomeKit related things. Apple's Fruta and Scrumdinger sample projects can help with that.

Why not a class to hold all of your rooms with a published variable like this:
class Rooms: ObservableObject {
#Published rooms: [HMRoom]
}
Isn't that your view model? You don't need the room parameters to be published; you need to know when anything changes and evaluate it from there.

Related

SwiftUI with complex MVVM (Repository + Nested ObservedObject)

Explanation
I am still in the process of learning to utilize SwiftUI patterns in the most optimal way. But most SwiftUI MVVM implementation examples I find are very simplistic. They usually have one database class and then 1-2 viewmodels that take data from there and then you have views.
In my app, I have a SQLite DB, Firebase and different areas of content. So I have a few separate model-vm-view paths. In the Android equivalent of my app, I used a pattern like this:
View - ViewModel - Repository - Database
This way I can separate DB logic like all SQL queries in the repository classes and have the VM handle only view related logic. So the whole thing looks something like this:
In Android this works fine, because I just pass through the LiveData object to the view. But when trying this pattern in SwiftUI, I kind of hit a wall:
It doesn't work / I don't know how to correctly connect the Published objects of both
The idea of "chaining" or nesting ObservableObjects seems to be frowned upon:
This article about Nested Observable Objects in SwiftUI:
I’ve seen this pattern described as “nested observable objects”, and it’s a subtle quirk of SwiftUI and how the Combine ObservableObject protocol works that can be surprising. You can work around this, and get your view updating with some tweaks to the top level object, but I’m not sure that I’d suggest this as a good practice. When you hit this pattern, it’s a good time to step back and look at the bigger picture.
So it seems like one is being pushed towards using the simpler pattern of:
View - ViewModel - Database Repository
Without the repository in-between. But this seems annoying to me, it would make my viewmodel classes bloated and would mix UI/business code with SQL queries.
My Code
So this is a simplified version of my code to demonstrate the problem:
Repository:
class SA_Repository: ObservableObject {
#Published var selfAffirmations: [SelfAffirmation]?
private var dbQueue: DatabaseQueue?
init() {
do {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
// Etc. other SQL code
} catch {
print(error.localizedDescription)
}
}
private func fetchSelfAffirmations() {
let saObservation = ValueObservation.tracking { db in
try SelfAffirmation.fetchAll(db)
}
if let unwrappedDbQueue = dbQueue {
let _ = saObservation.start(
in: unwrappedDbQueue,
scheduling: .immediate,
onError: {error in print(error.localizedDescription)},
onChange: {selfAffirmations in
print("change in SA table noticed")
self.selfAffirmations = selfAffirmations
})
}
}
public func updateSA() {...}
public func insertSA() {...}
// Etc.
}
ViewModel:
class SA_ViewModel: ObservableObject {
#ObservedObject private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
init() {
selfAffirmations = saRepository.selfAffirmations ?? []
}
public func updateSA() {...}
public func insertSA() {...}
// + all the Firebase stuff later on
}
View:
struct SA_View: View {
#ObservedObject var saViewModel = SA_ViewModel()
var body: some View {
NavigationView {
List(saViewModel.selfAffirmations, id: \.id) { selfAffirmation in
SA_ListitemView(content: selfAffirmation.content,
editedValueCallback: { newString in
saViewModel.updateSA(id: selfAffirmation.id, newContent: newString)
})
}
}
}
}
Attempts
Obviously the way I did it here is wrong, because it clones the data from repo to vm once with selfAffirmations = saRepository.selfAffirmations ?? [] but then it never updates when I edit the entries from the view, only on app restart.
I tried $selfAffirmations = saRepository.$selfAffirmations to just transfer the binding. But the repo one is an optional, so I'd need to make the vm selfAffirmations an optional too, which would then mean handling unnecessary logic in the view code. And not sure if it would even work at all.
I tried to do it manually with Combine but this way seemed to not be recommended and fragile. Plus it also didn't work:
selfAffirmations = saRepository.selfAffirmations ?? []
cancellable = saRepository.$selfAffirmations.sink(
receiveValue: { [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
)
Question
Overall I would just need some way to pass through the data from the repo to the view, but have the vm be in the middle as a separator. I read about the PassthroughSubject in Combine, which sounds like it would be fitting, but I'm not sure if I am just misunderstanding some concepts here.
Now I am not sure if my architecture concepts are wrong/unfitting, or if I just don't understand enough about Combine publishers yet to make this work.
Any advice would be appreciated.
After getting some input from the comments, I figured out a clean way.
The problem for me was understanding how to make a property of a class publish its values. Because the comments suggested that property wrappers like #ObservedObject was a frontend/SwiftUI only thing, making me assume that everything related was limited to that too, like #Published.
So I was looking for something like selfAffirmations.makePublisher {...}, something that would make my property a subscribable value emitter. I found that arrays naturally come with a .publisher property, but this one seems to only emit the values once and never again.
Eventually I figured out that #Published can be used without #ObservableObject and still work properly! It turns any property into a published property.
So now my setup looks like this:
Repository (using GRDB.swift btw):
class SA_Repository {
private var dbQueue: DatabaseQueue?
#Published var selfAffirmations: [SelfAffirmation]?
// Set of cancellables so they live as long as needed and get deinitialiazed with the class end
var subscriptions = Array<DatabaseCancellable>()
init() {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
}
private func fetchSelfAffirmations() {
// DB code....
}
}
And viewmodel:
class SA_ViewModel: ObservableObject {
private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
// Set of cancellables to keep them running
var subscriptions = Set<AnyCancellable>()
init() {
saRepository.$selfAffirmations
.sink{ [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
.store(in: &subscriptions)
}
}

Access and modify a #EnvironmentObject on global functions

I have an ObservableObject declared on my main view (ContentView.swift).
final class DataModel: ObservableObject {
#AppStorage("stuff") public var notes: [NoteItem] = []
}
Then I declare it in the main entry of the app as (removed extra code not needed for this example):
#main struct The_NoteApp: App {
private let dataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
}
In the ContentView.swift, I can use it on the different views I declared there:
struct NoteView: View {
#EnvironmentObject private var data: DataModel
// more code follows...
}
Now, I have a collection of global functions saved on FileFunctions.swift, which essentially are functions that interact with files on disk. One of them is to load those files and their content into my app.
Now, I'm trying to use #EnvironmentObject private var data: DataModel in those functions so at loading time, I can populate the data model with the actual data from the files. And when I declare that either as a global declaration in FileFunctions.swift or inside each function separately, I get two behaviors.
With the first one I get an error:
Global 'var' declaration requires an initializer expression or an explicitly stated getter`,
and
Property wrappers are not yet supported in top-level code
I tried to initialize it in any way, but it goes nowhere. With the second one, adding them to each function, Xcode craps on me with a segfault. Even if I remove the private and try to declare it in different ways, I get nowhere.
I tried the solution in Access environment variable inside global function - SwiftUI + CoreData, but the more I move things around the worse it gets.
So, how would I access this ObservableObject, and how would I be able to modify it within global functions?
Below is an example of a global function and how it's being called.
In FileFunctions.swift I have:
func loadFiles() {
var text: String = ""
var title: String = ""
var date: Date
do {
let directoryURL = try resolveURL(for: "savedDirectory")
if directoryURL.startAccessingSecurityScopedResource() {
let contents = try FileManager.default.contentsOfDirectory(at: directoryURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles])
for file in contents {
text = readFile(filename: file.path)
date = getModifiedDate(filename: file.absoluteURL)
title = text.components(separatedBy: NSCharacterSet.newlines).first!
// I need to save this info to the DataModel here
}
directoryURL.stopAccessingSecurityScopedResource()
} else {
Alert(title: Text("Couldn't load notes"),
message: Text("Make sure the directory where the notes are stored is accessible."),
dismissButton: .default(Text("OK")))
}
} catch let error as ResolveError {
print("Resolve error:", error)
return
} catch {
print(error)
return
}
}
And I call this function from here:
#main struct The_NoteApp: App {
private let dataModel = DataModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
.onAppear {
loadFiles()
}
}
}
You could change the signature of the global functions to allow receiving the model:
func loadFiles(dataModel: DataModel) { ... }
This way, you have access to the model instance within the function, what's left to do is to pass it at the call site:
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
.onAppear {
loadFiles(dataModel: self.dataModel)
}
You can do the same if the global functions calls originate from the views.
I would do something like this :
final class DataModel: ObservableObject {
public static let shared = DataModel()
#AppStorage("stuff") public var notes: [NoteItem] = []
}
#main struct The_NoteApp: App {
private let dataModel = DataModel.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(self.dataModel)
}
}
now in your viewModel you can access it like this
class AnyClass {
init (){
print(DataModel.shared.notes)
}
// or
func printNotes(){
print(DataModel.shared.notes)
}
}
As discussed in the comments, here a basic approach which makes some changes to the structure by defining dedicated "components" which have a certain role and which are decoupled as far as necessary.
I usually define a namespace for a "feature" where I put every "component" which is related to it. This offers a couple of advantages which you might recognise soon later:
enum FilesInfo {}
Using a "DataModel" or a "ViewModel" to separate your "Data" from the View
makes sense. A ViewModel - as opposed to DataModel - just obeys the rules from the MVVM pattern. A ViewModel should expose a "binding". I call this "ViewState", which completely describes what the view should render:
extension FilesInfo {
enum ViewState {
struct FileInfo {
var date: Date
var title: String
}
case undefined
case idle([FileInfo])
init() { self = .undefined } // note that!
}
}
Why ViewState is an enum?
Because you might want to represent also a loading state when your load function is asynchronous (almost always the case!) and an error state later. As you can see, you start with a state that's "undefined". You can name it also "zero" or "start", or however you like. It just means: "no data loaded yet".
A view model basically looks like this:
extension FilesInfo {
final class ViewModel: ObservableObject {
#Published private(set) var viewState: ViewState = .init()
...
}
}
Note, that there is a default initialiser for ViewState.
It also may have public functions where you can send "events" to it, which may originate in the view, or elsewhere:
extension FilesInfo.ViewModel {
// gets the view model started.
func load() -> Void {
...
}
// func someAction(with parameter: Param) -> Void
}
Here, the View Model implements load() - possibly in a similar fashion you implemented your loadFiles.
Almost always, a ViewModel operates (like an Actor) on an internal "State", which is not always the same as the ViewState. But your ViewState is a function of the State:
extension FilesInfo.ViewModel {
private struct State {
...
}
private func view(_ state: State) -> ViewState {
//should be a pure function (only depend on state variable)
// Here, you likely just transform the FilesInfo to
// something which is more appropriate to get rendered.
// You call this function whenever the internal state
// changes, and assign the result to the published
// property.
}
}
Now you can define your FileInfosView:
extension FilesInfo {
struct ContentView: View {
let state: ViewState
let action: () -> Void // an "event" function
let requireData: () -> Void // a "require data" event
var body: some View {
...
.onAppear {
if case .undefined = state {
requireData()
}
}
}
}
}
When you look more closely on the ContentView, it has no knowledge from a ViewModel, neither from loadFiles. It only knows about the "ViewState" and it just renders this. It also has no knowledge when the view model is ready, or provides data. But it knows when it should render data but has none and then calls requireData().
Note, it does not take a ViewModel as parameter. Those kind of setups are better done in some dedicated parent view:
extension FilesInfo {
struct CoordinatorView: View {
#ObservedObject viewModel: ViewModel
var body: some View {
ContentView(
state: viewModel.viewState,
action: {},
requireData: viewModel.load
)
}
}
}
Your "coordinator view" deals with separating ViewModel from your specific content view. This is not strictly necessary, but it increases decoupling and you can reuse your ContentView elsewhere with a different ViewModel.
Your CoordinatorView may also be responsible for creating the ViewModel and creating target views for navigation. This depends on what convention you establish.
IMHO, it may make sense, to restrict the access to environment variables to views with a certain role, because this creates a dependency from the view to the environment. We should avoid such coupling.
Also, I would consider mutating environment variables from within Views a "smell". Environment variables should be kind of a configuration which you setup in a certain place in your app (also called "CompositionRoot"). You may end up with an uncontrollable net of variables if you allow that everyone can change any environment variable at any time. When you have "ViewModels" in your environment, these of course get not "mutated" when they change their state - these are classes - for a reason.
Basically, that's it for a very basic but functional MVVM pattern.

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

SwiftUI bind array of objects and show changes

I'm trying to build a little app in SwiftUI. It is supposed to show a list of items an maybe change those. However, I am not able to figure out, how the data flow works correctly, so that changes will be reflected in my list.
Let's say I have a class of Item like this:
class Item: Identifiable {
let id = UUID()
var name: String
var dateCreated: Date
}
And this class has an initializer, that assigns each member a useful random value.
Now let's say I want to store a list of items in another class like this:
class ItemStore {
var items = [Item]()
}
This item store is part of my SceneDelegate and is handed to the ContextView.
Now what I want to do is hand one element to another view (from the stack of a NavigationView), where it will be changed, but I don't know how to save the changes made so that they will be reflected in the list, that is shown in the ContextView.
My idea is to make the item store an environment object. But what do I have to do within the item class and how do I have to pass the item to the other view, so that this works?
I already tried something with the videos from Apple's WWDC, but the wrappers there are deprecated, so that didn't work.
Any help is appreciated. Thanks a lot!
The possible approach is to use ObservableObject (from Combine) for storage
class ItemStore: ObservableObject {
#Published var items = [Item]()
// ... other code
}
class Item: ObservableObject, Identifiable {
let id = UUID()
#Published var name: String
#Published var dateCreated: Date
// ... other code
}
and in dependent views
struct ItemStoreView: View {
#ObservedObject var store: ItemStore
// ... other code
}
struct ItemView: View {
#ObservedObject var item: Item
// ... other code
}

How to use Core Data Relationship in ForEach SwiftUI

Im facing issues with displaying my Core Data inside of SwiftUI because of relationships being Sets. What is the best way to go about displaying a many relationship set inside of a SwiftUI ForEach loop?
For instance, I have two entities in core date: Entry & Position. Entry can contain many positions, and each position can only belong to one entry.
I have the Entry as a #binding var on the view that is suppose to display the entry's Positions. I would ideally like to access the positions directly from this entry variable, but because positions are a Set, I get the following error:
error 'ForEach' requires that 'Set' conform to 'RandomAccessCollection'
#Binding private var entry: Entry?
ForEach(entry?.positions) { position in
Text("position here")
}
Solution One:
Or, I could do a fetch request for all positions, then filter out all the ones that do not belong to the entity, I do not like this idea as I would be fetching potentially thousands of Positions to only get a few. (or am i thinking of this wrong? Im new to core data, coming from realm because of swiftui)
Although this could work if I could do a fetch ONLY on the #binding entry var, and fetch all its positions as sorted fetched results, but I'm not sure there is a way to do this.
Maybe like this, or would this be a performance issue if there was potentially thousands of entry's each with 10-20+ positions? and could objectID be used this way, and would it still be unique if the entry was moved into another journal?:
#Binding private var entry: Entry?
#FetchRequest(
entity: Position.entity(),
sortDescriptors: [],
predicate: NSPredicate(formate: "entry.objectID == %#", self.entry.objectID)
) var positions: FetchedResults<Position>
Solution Two:
I thought of adding an attribute to positions like 'date', this way positions could be compared and sorted? But not sure how this could be updated with SwiftUI, as it would be done only once in the init().
let list = entry.wrappedValue?.positions?.sorted()
Core Data Models:
public class Entry: NSManagedObject, Identifiable {
// MARK: - ATTRIBUTES
#NSManaged public var date: Date
// MARK: - RELATIONSHIPS
#NSManaged public var journal: Journal?
#NSManaged public var positions: Set<Position>?
}
public class Position: NSManagedObject, Identifiable {
// MARK: - RELATIONSHIPS
#NSManaged public var entry: Entry?
}
How would you go about solving this problem? Keep in mind on the view where the positions are being listed, that this view can add, delete, and modify positions, so the solution should allow SwiftUI to reload the view and update the positions when changes are made.
#JoakimDanielson comment works like a charm, but requires a few tweaks.
Wrapping the set in the array initializer works like this, but requires optional sets to be unwrapped. I was surprised to find force unwrapping does not cause a crash even if the set is nil? Maybe someone could explain this?
ForEach(Array(entry.positions!)) { position in
Text("Position")
}
The next issue was that the array would be randomized everytime the set of positions changed due to sets being unordered. So by conforming Position to the Comparable Protocol solved this. I decided it made the most sense to sort positions by date, so I updated the model like so:
public class Position: NSManagedObject, Identifiable {
// MARK: - ATTRIBUTES
#NSManaged public var date: Date
// MARK: - RELATIONSHIPS
#NSManaged public var entry: Entry?
}
extension Position: Comparable {
static func < (lhs: Position, rhs: Position) -> Bool {
lhs.date < rhs.date
}
}
Then the ForEach could be sorted and looks like this:
ForEach(Array(entry.positions!).sorted()) { position in
Text("\(position.date)")
}
Some other solutions I found but are not ideal for reasons mentioned in original post, but they do work, is to either use a fetch request customized inside the view init like so:
#FetchRequest var positions: FetchedResults<Position>
init(entry: Entry) {
var predicate: NSPredicate?
// Do some kind of control flow for customizing the predicate here.
predicate = NSPredicate(formate: "entry == %#", entry)
self._positions = FetchRequest(
entity: Position.entity(),
sortDescriptors: [],
predicate: predicate
)
}
or create an "middle man" View Model bound to #ObservedObject that converts core data managed objects to useable view data. Which to me makes the least sense because it will basically contain a bunch of redundant information already found inside the core data managed object, and I like the idea of the core data managed object also being the single source of truth.
I found myself using this solution frequently so I added an extension to the CoreData object (see below for an example using Entry/Position types). This also has the added benefit of handling optional relationships and simply returning an empty array in that case.
extension Entry {
func arrayOfPositions() -> [Position] {
if let items = self.positions as? Set<Position> {
return items.sorted(by: {$0.date < $1.date})
} else {
return []
}
}
}
So that instead of unsafely and cumbersomely writing:
ForEach(Array(entry.positions! as! Set<Position>).sorted(by: {$0.date < $1.date})) { item in
Text(item.description)
}
You can safely use:
ForEach(entry.arrayOfPositions()) { item in
Text(item.description)
}
Simply pass a customised FetchRequest param to the View containing the #FetchRequest property wrapper.
struct PositionsView: View {
let entry: Entry
var body: some View {
PositionsList(positions: FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Positions.date, ascending: true)], predicate: NSPredicate(format: "entry = %#", entry)))
}
}
struct PositionsList : View {
#FetchRequest var positions: FetchedResults<Positions>
var body: some View {
ForEach(positions) { position in
Text("\(position.date!, formatter: positionFormatter)")
}
}
}
For more detail, see this answer.