Recommended way to refactor/combine multiple near-identical classes using Swift? - swift

In Swift, how would I combine the following into one class?
class FirstClass: Codable, ObservableObject {
#Published var int: Int
#Published var bool: Bool
#Published var firstObject: FirstType
// plus inits and custom decoding/encoding stuff
}
class SecondClass: Codable, ObservableObject {
#Published var int: Int
#Published var bool: Bool
#Published var secondObject: SecondType
// plus inits and custom decoding/encoding stuff
}
class ThirdClass: Codable, ObservableObject {
#Published var int: Int
#Published var bool: Bool
#Published var thirdObject: ThirdType
// plus inits and custom decoding/encoding stuff
}
(with FirstType, SecondType and ThirdType also being class models that conform to Codable and ObservableObject)
in order to end up with something like this:
class CommonClass: Codable, ObservableObject {
#Published var int: Int
#Published var bool: Bool
#Published var object: CommonType // which could accept FirstType, SecondType or ThirdType
// plus inits and custom decoding/encoding stuff
}
How should I go about doing this? And is there a better way that still works the way I intend?
I'm basically trying to achieve 2 things: 1-avoid repeating code (as in my real-life scenario there are a lot more variables in common than just an int and a bool) and 2-Make downstream code more straightforward by hopefully also minimising the need for separate views.

It is not clear about other code, but for that provided I would use generics, like
class CommonClass<T>: Codable, ObservableObject {
//class CommonClass<T: CommonType>: Codable, ObservableObject { // << as variant
#Published var int: Int
#Published var bool: Bool
#Published var object: T
// plus inits and custom decoding/encoding stuff
}

Related

How to make `ObservableObject` with `#Published` properties `Codable`?

My codable observable Thing compiles:
class Thing: Codable, ObservableObject {
var feature: String
}
Wrapping feature in #Published though doesn’t:
class Thing: Codable, ObservableObject {
#Published var feature: String
}
🛑 Class 'Thing' has no initializers
🛑 Type 'Thing' does not conform to protocol 'Decodable'
🛑 Type 'Thing' does not conform to protocol 'Encodable'
Apparently Codable conformance can’t be synthesized anymore because #Published doesn’t know how to encode/decode its wrappedValue (because it doesn’t conform to Codable even if its wrapped value does)?
Okay, so I’ll let it know how to do it!
extension Published: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue) // 🛑 'wrappedValue' is unavailable: #Published is only available on properties of classes
}
public init(from decoder: Decoder) throws {
var container = try decoder.singleValueContainer()
wrappedValue = try container.decode(Value.self) // 🛑 'wrappedValue' is unavailable: #Published is only available on properties of classes
}
}
So sorrow, much misery, many sadness! 😭
How can I easily add back Codable synthesis (or similar) without defining encode(to:) and init(from:)?
I don't know why you would want to do it, I think you should code the struct, not the published class. Something like this:
struct CodableStruct: Codable {
var feature1: String = ""
var feature2: Int = 0
}
class Thing: ObservableObject {
#Published var features: CodableStruct = .init()
}

Type 'any ProtocolName' cannot conform to '(ProtocolName's Inherited Protocol)' [duplicate]

This question already has answers here:
How to define a protocol as a type for a #ObservedObject property?
(6 answers)
Closed 5 months ago.
I have a ProtocolName that conforms to ObservableObject. I then have ClassName that conforms to ProtocolName.
However, when I try to use #ObservedObject serviceName: ProtocolName = ClassName, it fails to build with the following message: "Type 'any ProtocolName' cannot conform to 'ObservableObject'"
What am I doing wrong and how can I fix this?
Example:
protocol ProtocolName: ObservableObject {
let random: String
}
class ClassName: ProtocolName {
let random: String
}
#ObservedObject serviceName: ProtocolName = ClassName()
When you write a line like this:
#ObservedObject var observeMe: ProtocolName
You are saying that "observeMe is a box that can contain any kind of thing that conforms to the ProtocolName protocol"
The type of that box is any ProtocolName it's called an existential.
But the box itself does NOT conform to ProtocolName(and by extension does not conform to ObservableObject). The thing inside the box does, but the box itself does not.
So the compiler complains that the existential whose type is any ProtocolName is not an ObservableObject
You can make the box even more obvious and explicit using the any ProtocolName syntax:
import SwiftUI
import Combine
protocol ProtocolName: ObservableObject {
var someValue: Int { get }
}
class MyClass: ProtocolName {
#Published var someValue: Int = 42
}
struct SomeStruct : View {
#ObservedObject var observeMe: any ProtocolName = MyClass()
var body: some View {
Text("Hello.")
}
}
And you'll still see the error.
To solve the problem your #ObservedObject has to be a concrete type that conforms to ProtocolName:
import SwiftUI
import Combine
protocol ProtocolName: ObservableObject {
var someValue: Int { get }
}
class MyClass: ProtocolName {
#Published var someValue: Int = 42
}
struct SomeStruct : View {
#ObservedObject var observeMe: MyClass = MyClass()
var body: some View {
Text("Hello.")
}
}
let myView = SomeStruct()
Or you can add a type parameter to your view so that when the view is created there is a specific type that conforms to the protocol that is used for the view:
import SwiftUI
import Combine
protocol ProtocolName: ObservableObject {
var someValue: Int { get set }
}
class MyClass: ProtocolName {
#Published var someValue: Int = 42
}
struct SomeStruct<T : ProtocolName> : View {
#ObservedObject var observeMe:T
var body: some View {
Text("Hello.")
Button(action: {observeMe.someValue = 3}) {
Text("Button")
}
}
}
let myView = SomeStruct(observeMe: MyClass())

Class with an Observable Object has been used by is not initializing

I've added an Observable Object to my class DetailViewModel, but I am getting an error "Class 'DetailViewModel' has no initializers". Can anyone explain why?
import Foundation
import UIKit
#MainActor
class DetailViewModel: ObservableObject{
#Published var strMeal: String = ""
#Published var strInstructions: String
#Published var strIngredient: String
#Published var strMeasure: String
#Published var strMealThumb:URL?
private func loadMealDetails(idMeal: String) async {
do {
let mealDetailResponse = try await WebServiceRequest().loadData(url: Constants.Urls.getMealByIdUrl(strMeal)) { data in
return try? JSONDecoder().decode(MealDetailModel.self, from:data )
}
} catch {
print(error)
}
}
You have defined some properties (strInstructions, strIngredient, and strMeasure) that don't have initial values specified. Unlike structs, which get synthesized initializers (eg the compiler makes a initializer for us), with a class, we have to create an initializer ourselves (or give all of the properties default values).
With default values, it may look like:
#MainActor
class DetailViewModel: ObservableObject{
#Published var strMeal: String = ""
#Published var strInstructions: String = ""
#Published var strIngredient: String = ""
#Published var strMeasure: String = ""
#Published var strMealThumb:URL?
}
Or, with an initializer, it could be something like:
#MainActor
class DetailViewModel: ObservableObject{
#Published var strMeal: String = ""
#Published var strInstructions: String
#Published var strIngredient: String
#Published var strMeasure: String
#Published var strMealThumb:URL?
init(strInstructions: String, strIngredient: String, strMeasure: String) {
self.strInstructions = strInstructions
self.strIngredient = strIngredient
self.strMeasure = strMeasure
}
}
You may also want to accept values for strMeal and strMealThumb in your initializer -- it's up to you.

SwiftUI = ObservableObject as Choice of Class

Using SwiftUI I want to press a button and have it switch the class which is used to filter an image.
In SwiftUI, the button would do something like what follows:
#ObservedObject var currentFilter = FilterChoice()
...
var body: some View {..
Button(action:{
print("clicked")
var newFilter = Luminance()
self.currentFilter = newFilter
}) {
Text("Switch to Luminance Filter")
}
}
There is an ObservableObject:
class FilterChoice: ObservableObject {
#Published var filter = Luminance()
}
Which is consumed by a UIViewRepresentable:
struct FilteredPhotoView: UIViewRepresentable {
#ObservedObject var currentFilter = FilterChoice()
func makeUIView(context: Context) -> UIView {
...
// Code works and pulls correct filter but can not be changed
let className = currentFilter.filter
let filteredImage = testImage.filterWithOperation(className)
...
}...
Currently, FilteredPhotoView is properly returning the filtered image.
But how can ObservedObject be used to change a CLASS?
In other words, the ObservedObject sets the class correctly here:
class FilterChoice: ObservableObject {
#Published var filter = Luminance()
}
But how can this ObservableObject be changed so that the class can be changed in SwiftUI? For example, I want to click a button and the filter should be changed to another class (for example:
new filter = ColorInversion()
I think I understand how ObservableObjects work but I can't get it to work as a change of class rather than something simple like a string value.
What you actually need is some generics.
Declare a protocol like this:
protocol ImageFilter {
func apply(to image: UIImage) // for example
}
Declare here any methods or properties that all your filters will share and that will be used by FilterChoice.
Next declare all your filters as conforming to the protocol:
class Luminance: ImageFilter {
// implement all the methods and properties declared in the protocol
// for every filter
}
Next declare your #Published filter to conform to that protocol
class FilterChoice: ObservableObject {
#Published var filter: ImageFilter
public init(filter: ImageFilter) {
self.filter = filter
}
// etc.
}
You will be able to change the filters used by #Published.

Updating a #Published variable based on changes in an observed variable

I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}