SwiftUI = ObservableObject as Choice of Class - swift

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.

Related

Modify #Published variable from another class that is not declared in | SwiftUI

I want to put the logic of all my #Published in a model class, however when I try to separate it, it doesn't update. I recreated a little example:
The code below works, it increases every time the button is clicked:
struct ContentView: View {
#StateObject var myClass = MyClass()
var body: some View {
Button(action: {
myClass.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
func doStuff(numb: Int) {
people += numb
}
}
However, once I split the logic and try to have my #Published in a separate class to have it more clean, it doesn't update, see below:
struct ContentView: View {
#StateObject var myClass = MyClass()
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass()
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
}
I think it's because there are two different instances in the view right? Anyway, how can I separate the #Publish correctly have it updated?
Thanks
Your first form is absolutely fine! You may, though, consider your ContentView using a #ObservedObject instead a #StateObject.
Your second form is flawed, for several reasons:
don't move logic into a view
don't use class variables to keep "state".
The first statement is due to a sane design that keeps your models and views nicely separated.
The second statement is due to how SwiftUI works. If you need to have some "state" in your views, use #State where the value is a struct.
Using #State ensures, that it's value is bound to the actual life time of the "conceptual view", i.e. the thing we human perceive as the view. And this "conceptual view" (managed as some data object by SwiftUI) is distinct from the struct View, which is merely a means to describe how to create and modify this conceptual view - that is, struct view is rather a function that will be used to initially create the "conceptual view" and modify it. Once this is done, it gets destroyed, and gets recreated when the conceptual view needs to be modified. That also means, the life time of this struct is not bound to the life time of its "state". The state's life time is bound to the conceptual view, and thus has usually longer life time than the struct view, where the struct view can be created and destroyed several times.
Now, imagine what happens when you always execute let modify = Modify() whenever the (conceptual) view or its content view is modified and needs to be recalculated and rendered by creating a struct view and then - after it has been rendered - destroying it again.
Also, this "state" is considered private for the view, means it is considered an implementation detail for the view. If you want to exchange data from "outside" and "inside" use a #ObservedObject or a Binding.
The problem is that you have 2 separate instances of MyClass:
#StateObject var myClass = MyClass()
var myClass = MyClass()
You are updating the myClass in Modify, which you aren't receiving updates from. A way to fix this is by having one instance of MyClass, passed into Modify during initialization:
struct ContentView: View {
#StateObject var myClass: MyClass
let modify: Modify
init() {
let temp = MyClass()
_myClass = StateObject(wrappedValue: temp)
modify = Modify(myClass: temp)
}
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
let myClass: MyClass
init(myClass: MyClass) {
self.myClass = myClass
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
Another method is to have a #Published property in Modify to observe the changes of MyClass:
struct ContentView: View {
#StateObject var modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(modify.myClass.people)")
}
}
}
class Modify: ObservableObject {
#Published var myClass = MyClass()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = myClass.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
you could try this approach using a singleton. Works well for me:
struct ContentView: View {
#StateObject var myClass = MyClass.shared // <--- here
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass.shared // <--- here
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
static let shared = MyClass() // <--- here
}

SwiftUI: Why does the changed property of my Data not refresh the UI?

I have the following data model:
class MyImage: : Identifiable, Equatable, ObservableObject {
let id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D?
}
class MyImageCollection : ObservableObject {
#Published var images: [MyImage] = []
#Published var selection: Set<UUID> = []
}
extension Array where Element == MyImage {
func haveCoordinates() -> Array<MyImage> {
return filter { (image) -> Bool in
return image.coordinate != nil
}
}
}
I use the collection in the views as follows:
# Top View
#StateObject var imageModel: MyImageCollection = MyImageCollection()
# Dependend Views
#ObservedObject var imageModel: MyImageCollection
So in my SwiftUI, whenever I add a new instance of MyImage via imageCollection.images.append(anImage) everything works perfectly and any View is updated accordingly, also any View using imageCollection.haveCoordinates() is updated. But I also want to have any views updated, when I change a property of an image like imageCollection.images[0].coordinate = someCoordinate. That does not work currently.
Any hints?
Your subviews need to directly observe your 'MyImage' class to be updated accordingly. Pass in your 'MyImage' instances directly into an observed object variable. Here's what that may look like...
ForEach(collection.images) { myImage in
YourSubView(image: myImage)
}
Where the image parameter is passed to an observed object property in your subview.
Switch the #ObservedObject to #EnvironmentObject in the DependantView and initialize DependentView().environmentObject(imageModel). Apple Documentation.This connects the two instances.
Also, If you want to Observe each MyImage you have to create an MyImageView that observes it directly
struct MyImage: View{
#ObservedObject var myImage: MyImage
//Rest of code
}
Thanks for your replies, very appreciated. I was trying to create a Subview with a reference to the object as ObservableObject too, but failed because I was using MapAnnotation and that is initialized with the coordinate property. Need to figure that out in detail what the difference is here, the documentation from Apple does not help very much to see any difference at a glance. I'm using some kind of workaround now by "invalidating" the MyImage instance with setting a new UUID:
class MyImage: : Identifiable, Equatable, ObservableObject {
var id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D? {
didSet {
id = UUID()
}
}
}

Computed property from #Published array of objects not updating SwiftUI view

I have a StateController class:
import Foundation
import Combine
class StateController: ObservableObject {
// Array of subjects loaded in init() with StorageController
#Published var subjects: [Subject]
private let storageController = StorageController()
init() {
self.subjects = storageController.fetchData()
}
// MARK: - Computed properties
// Array with all tasks from subjects, computed property
var allTasks: [Task] {
var all: [Task] = []
for subject in subjects {
all += subject.tasks
}
print("Computed property updated!")
return all
}
var numberofCompletedTasks: Int {
return subjects.map({$0.tasks.map({$0.isCompleted == true})}).count
}
var numberOfHighPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 1})}).count
}
var numberOfMediumPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 2})}).count
}
var numberOfLowPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 3})}).count
}
}
And a SwiftUI view:
import SwiftUI
struct SmartList: View {
// MARK: - Properties
let title: String
#EnvironmentObject private var stateController: StateController
// MARK: - View body
var body: some View {
List(stateController.allTasks, id: \.taskID) { task in
TaskView(task: task)
.environmentObject(self.stateController)
}.listStyle(InsetGroupedListStyle())
.navigationTitle(LocalizedStringKey(title))
}
}
When I update "Task" objects inside "subjects" #Published array, for example checking them as complete, SwiftUI should automatically update the view because computed properties are derived from #Published property of an ObservableObject (declared as #EnvironmentObject inside view) but it doesn't work.
How can I bind my SwiftUI view to computed properties derived from a #Published property??
Sadly, SwiftUI automatically updating views that are displaying computed properties when an #EnvironmentObject/#ObservedObject changes only works in very limited circumstances. Namely, the #Published property itself cannot be a reference type, it needs to be a value type (or if it's a reference type, the whole reference needs to be replaced, simply updating a property of said reference type won't trigger an objectWillChange emission and hence a View reload).
Because of this, you cannot rely on computed properties with SwiftUI. Instead, you need to make all properties that your view needs stored properties and mark them as #Published too. Then you need to set up a subscription on the #Published property, whose value you need for your computed properties and hence update the value of your stored properties each time the value they depend on changes.
class StateController: ObservableObject {
// Array of subjects loaded in init() with StorageController
#Published var subjects: [Subject]
#Published var allTasks: [Task] = []
private let storageController = StorageController()
private var subscriptions = Set<AnyCancellable>()
init() {
self.subjects = storageController.fetchData()
// Closure for calculating the value of `allTasks`
let calculateAllTasks: (([Subject]) -> [Task]) = { subjects in subjects.flatMap { $0.tasks } }
// Subscribe to `$subjects`, so that each time it is updated, pass the new value to `calculateAllTasks` and then assign its output to `allTasks`
self.$subjects.map(calculateAllTasks).assign(to: \.allTasks, on: self).store(in: &subscriptions)
}
}

Can you use a Publisher directly as an #ObjectBinding property in SwiftUI?

In SwiftUI, can you use an instance of a Publisher directly as an #ObjectBinding property or do you have to wrap it in a class that implements BindableObject?
let subject = PassthroughSubject<Void, Never>()
let view = ContentView(data:subject)
struct ContentView : View {
#ObjectBinding var data:AnyPublisher<Void, Never>
}
// When I want to refresh the view, I can just call:
subject.send(())
This doesn't compile for me and just hangs Xcode 11 Beta 2. But should you even be allowed to do this?
In your View body use .onReceive passing in the publisher like the example below, taken from Data Flow Through SwiftUI - WWDC 2019 # 21:23. Inside the closure you update an #State var, which in turn is referenced somewhere else in the body which causes body to be called when it is changed.
You can implement a BindableObject wich takes a publisher as initializer parameter.
And extend Publisher with a convenience function to create this BindableObject.
class BindableObjectPublisher<PublisherType: Publisher>: BindableObject where PublisherType.Failure == Never {
typealias Data = PublisherType.Output
var didChange: PublisherType
var data: Data?
init(didChange: PublisherType) {
self.didChange = didChange
_ = didChange.sink { (value) in
self.data = value
}
}
}
extension Publisher where Failure == Never {
func bindableObject() -> BindableObjectPublisher<Self> {
return BindableObjectPublisher(didChange: self)
}
}
struct ContentView : View {
#ObjectBinding var binding = Publishers.Just("test").bindableObject()
var body: some View {
Text(binding.data ?? "Empty")
}
}

Using environmentObject in watchOS

I am trying to use environmentObject in a watchOS6 app to bind my data model to my view.
I have created a simple, stand-alone Watch app in Xcode 11.
I created a new DataModel class
import Combine
import Foundation
import SwiftUI
final class DataModel: BindableObject {
let didChange = PassthroughSubject<DataModel,Never>()
var aString: String = "" {
didSet {
didChange.send(self)
}
}
}
In my ContentView struct I bind this class using #EnvironmentObject -
struct ContentView : View {
#EnvironmentObject private var dataModel: DataModel
var body: some View {
Text($dataModel.aString.value)
}
}
Finally, I attempt to inject an instance of the DataModel into the environment in the HostingController class -
class HostingController : WKHostingController<ContentView> {
override var body: ContentView {
return ContentView().environmentObject(DataModel())
}
}
But, I get an error:
Cannot convert return expression of type '_ModifiedContent<ContentView, _EnvironmentKeyWritingModifier<DataModel?>>' to return type 'ContentView'
The error is because the WKHostingController is a generic that needs a concrete type - WKHostingController<ContentView> in this case.
A similar approach works perfectly with UIHostingController in an iOS app because UIHostingController isn't a generic class.
Is there some other way to inject the environment to a watchOS view?
You can use type erasure, AnyView in the case of SwiftUI View.
I would refactor WKHostingController to return AnyView.
This seems to compile fine on my end.
class HostingController : WKHostingController<AnyView> {
override var body: AnyView {
return AnyView(ContentView().environmentObject(DataModel()))
}
}
For anyone like Brett (in the comments) who was getting
"Property 'body' with type 'AnyView' cannot override a property with type 'ContentView'"
I got the same error because I hadn't replaced the return value and wrapped the ContentView being returned.
ie. this is what my first attempt looked like.. notice the
WKHostingController<ContentView>
that should be
WKHostingController<AnyView>
class HostingController : WKHostingController<ContentView> {
override var body: AnyView {
return AnyView(ContentView().environmentObject(DataModel()))
}
}
Adding to Matteo's awesome answer,
If you want to use delegate then use like this:
class HostingController : WKHostingController<AnyView> {
override var body: AnyView {
var contentView = ContentView()
contentView.environmentObject(DataModel())
contentView.delegate = self
let contentWrapperView = AnyView(contentView)
return contentWrapperView
}
}