How to publish an CoreData entity extension? - swift

I'm using CoreData in my SwiftUI app. To have some better handling I added some wrappers to the CoreData classes like this:
extension Category {
public var wrappedName: String {
name ?? ""
}
}
This is working fine till now. But now I need to observe this wrappers also and I use the Category as an StateObject. Therefor I wanted to Publish it like this:
extension Category {
#Published public var wrappedName: String {
name ?? ""
}
}
This is triggering the error: Non-static property 'wrappedName' declared inside an extension cannot have a wrapper
If I remove the #Published I get an error for the #StateObject:
Property type 'Category?' does not match that of the 'wrappedValue' property of its wrapper type 'StateObject'
How to solve this issue?

Related

Autogeneration of readonly Swift protocol exposing #Published properties

I'm currently working on a refactor where write access to model objects is restricted. Model objects are reference types with #Published properties:
public final class Model {
public enum State { /* Some cases here. */ }
#Published public var state: State
}
Let's say that I want a view model that returns a string to be observed by the UI layer. I'm using a protocol to expose readonly access to the view model, and a concrete implementation to support it:
public protocol ViewModelAPI {
var stringPublisher: AnyPublisher<String, Never> { get }
}
private struct ViewModel: ViewModelAPI {
public let stringPublisher: AnyPublisher<String, Never>
init(model: Model) {
stringPublisher =
model.$state
.map { /* Convert Model.State to string to display. */ }
.eraseToAnyPublisher()
}
}
public final class StateStringView: UIView {
init(viewModel: ViewModelAPI) {
/* Subscribe to `viewModel` to observe new String to display. */
}
}
Generally, this approach works pretty well, in that it prevents the UI layer from having direct Model dependencies. However, the ViewModel implementation has access to the settable Model.state property even though it only ever reads the property and never sets it.
Ideally, I'd like an easy way to create a protocol from an existing model object, such that we convert any #Published property into an AnyPublisher allowing readonly access. I think creating a protocol by hand is appropriate for ViewModel APIs since it's tailored to specifically the data used by a single UI feature. However, creating explicit observable protocols for all Model objects seems like a lot of overhead since it would require updating the protocol whenever the Model is updated.
Does anyone have any ideas on how to generate a protocol such that we could inspect a class and extract all of the #Published properties? Would dynamic member lookup be a possible solution here? Or would I just need to use a script to generate protocols that accomplish this?

How to store properties in generic struct?

I have a Codable struct that is part of my app, RemoteData. I’m building a reusable package that will fetch the data and store it in UserDefaults. The data fetching, DataFetcher class has a Codable generic parameter. I am subclassing DataFetcher to pass in RemoteData as the generic param.
// in my app
struct RemoteData: Codable {
var experimentOne: [Variant<[Page]>]
var experimentTwo: [Variant<Bool>]
var experimentThree: [Variant<String>]
}
All of the properties in RemoteData will be arrays of type Variant<T> where T is Codable:
// in my package
public struct Variant<T: Codable>: Codable, VariantProtocol {
public var experimentName: String
public var variantName: String
public var percent: Int
public var value: T
}
I’d like to be able to save this data in UserDefaults. I’d like to perform some filtering on the Variant array to see if this user should see that configuration. I’d like to save the data so that each experiment name is the key and the single variant the user should see is the value rather than the whole array. Although if the whole array is the only option, I’d be ok with that too.
However, since my DataFetcher doesn’t know what the properties are since it is just taking in a generic I don’t think I can do that. My first thought was to create a protocol that RemoteConfig confirms to and that the DataFetcher generic also conforms to.
// in my package, but subclassing in my app to provide url
open class DataFetcher<T: Decodable> {
var remoteConfig: T?
var url: URL
public init(url: String) {
self.url = url
}
func fetchAndSaveData() { ... }
}
That doesn’t work because I then need to specify T in Variant and I will only be able to have Variant arrays of one type.
I’m stuck here and not sure how to move forward.

Model objects that are not developer-defined?

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.

SwiftUI - Use EnvironmentObject in ObservableObject Class/Dependency Injection

I am having trouble using an #EnvironmentObject in an #ObservableObject class. Based on some research, this is not possible, as EnvironmentObject is for views only.
I have resorted to doing the following, but the value is not updated dynamically.
For example, it is initialized with the value of "A", but when I change the value in a class that is using EnvironmentObject, the value found in my ObservableObject class remains "A". It updates in all other locations that are using the #EnvironmentObject, just not the ObservableObject API class.
Is there a way to have the code in the ObservableObject API class update when the EnvironmentObject updates the published variable?
The class that needs a variable in it that operates like EnvironmentObject is the API class.
class SelectedStation: ObservableObject {
#Published var selectedStation: String = "A"
}
class API: ObservableObject {
var selectedStation: SelectedStation
init(selectedStation: SelectedStation) {
self.selectedStation = selectedStation
print(selectedStation.selectedStation)
}
///some code that will utilize the selectedStation variable
}
What exactly am I doing wrong here?
You are initializing a different version of your class. Try adding public static let shared = SelectedStation() like this:
class SelectedStation: ObservableObject {
#Published var selectedStation: String = "A"
public static let shared = SelectedStation()
}
and then where you need to use it, declare it as:
var selectedStation = SelectedStation.shared
Also, you should rename the #Published var to something other than selectedStation, otherwise you could run into the unfortunate selectedStation.selectedStation as a reference to that variable.
Lastly, remember the #Environment needs to be initialized with the SelectedStation.shared so everything is sharing the one instantiation of the class.

Swift, Core Data, Optional Integer16 and keyPath

I have an entity in CoreData which has an optional property of type Integer 16. It can genuinely be nil and in my application I want to refer to it as an Int? type. As Int? (or for that matter Int16?) isn't a recognised Objective-C type, the compiler throws a bit of a wobbly. I want to avoid using code like NSNumber?.intValue throughout so I've actually set up my ManagedObject type with custom accessors for this property. My question relates to identifying the property through #keyPath rather than a static string. In Core Data the field is named 'pin' on entity 'User'. Here's the code I have:
class User: NSManagedObject {
// MARK: - Properties
static let pinKey = "pin"
#NSManaged internal(set) var name: String
#NSManaged fileprivate var primitivePin: NSNumber?
internal(set) var pin: Int? {
get {
willAccessValue(forKey: #keyPath(pin)) // THIS LINE ERRORS
let value: Int? = primitivePin.map { $0.intValue }
didAccessValue(forKey: User.pinKey)
return value
}
set {
willChangeValue(forKey: User.pinKey)
primitivePin = newValue.map { NSNumber(value: Int16($0)) }
didChangeValue(forKey: User.pinKey)
}
}
}
The line in error is what I 'want' to achieve but of course the var pin isn't an obj-c type and the compiler complains, so I have defined the static constant pinKey as you can see. #keyPath feels like the right way to go about it, and the entity does have a field called pin, but in this scenario is the only option open to me to use a static value?
In #keyPath you have to specify property name. If you don't have defined property called pin, you will receive an error. In your case you have to use #keyPath(User.primitivePin). I believe this should work.
Also, i guess, calling map is redundant here. You can write directly let value = primitivePin?.intValue and so on.
The answer is....with custom properties/accessors #keyPath can't used as there is no defined #NSManaged property for it - as Maksym points out. However, you can't use the defined primitive for it either, instead using the property name as a String as also shown in the code above (User.pinKey)