SwiftUI - Accessing a property of a generic observed class - swift

I am new to SwiftUI.
I have a view declared like this:
struct MyScrollView: View {
#ObservedObject var model:MyModel
var body: some View {
ScrollView {
HStack {
ForEach(model.items!, id: \.self) { title in
Text(title)
}
}
}
Now I want to create a new kind of scrollView, that inherits all this defined for MyScrollView. In fact I would love to create an "instance" of MyScrollView and extend its methods and properties.
The problem is that structs cannot be instanced.
If MyScrollView was a class what I would like is this:
class MySuperScrollView: MyScrollView {
var anotherProperty:Bool
func anotherFunction() -> Bool {
return anotherProperty
}
}
^ this in terms of struct.

You cannot inherit struct views, but you can aggregate one into another, like
struct MySuperScrollView: View {
#ObservedObject var model: MyModel
var anotherProperty:Bool
func anotherFunction() -> Bool {
return anotherProperty
}
var body: some View {
// ... other elements around
MyScrollView(model: self.model)
// ... other elements around
}
}

Inheritance is not the way to create reusable SwiftUI views that you can extend with extra properties.
Instead of using inheritance, you should use composition, which means that you break up your views into small reusable components and then add these views to the body of your other view to compose a more complex view.
However, without seeing exactly what changes you want to achieve, I cannot give you an exact code example.

Related

Is there a way to pass mutating closures as arguments to a SwiftUI View?

The Idea
In one of the views of my application I need to mutate some data. To make the code clear, testable and just to test how far I can get without logic in ViewModels, I've moved the mutating logic to the Model layer.
Say this is my Model
struct Model {
var examples: [Example] = []
/* lots of other irrelevant properties and a constructor here */
}
struct Example: Identifiable {
var id = UUID()
var isEnabled: Bool = true
/* other irrelevant properties and a constructor here */
}
And the function that mutates stuff is
// MARK: mutating methods
extension Model {
mutating func disableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
mutating func enableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
}
Now, let's display it in views, shall we?
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples)
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples) {
// TODO: do stuff
}
}
}
}
The attempt
Since I don't want to put the whole Model into the ExampleMutatingView, both because I don't need the whole thing and due to performance reasons, I tried to supply the view with necessary methods.
I've also added the ability to select examples by providing a State variable.
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples,
operationsOnExamples: (enable: model.enableExamples, disable: model.disableExamples))
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
let operationsOnExamples: (enable: ((Set<UUID>) -> Void, disable: (Set<UUID>) -> Void)
#State private var multiSelection = Set<UUID>()
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples, selection: $multiSelection) { example in
Text("\(example.id)")
}
HStack {
Button { operationsOnExamples.enable(with: multiSelection) } label: { Text("Enable selected") }
Button { operationsOnExamples.disable(with: multiSelection) } label: { Text("Disable selected") }
}
}
}
}
The problem
The thing is, with such setup the ContentView greets me with Cannot reference 'mutating' method as function value error. Not good, but mysterious for me for the very reason that fixes it: supplying the actual Model into the view.
The (non ideal) solution
Showing only the parts that changed
// ContentView
1. ExampleMutatingView(model: $model)
// ExampleMutatingView
1. #Binding var model: Model
2. List($model.examples/*...*/)
3. Button { model.enableExamples(with: multiSelection) } /*...*/
4. Button { model.disableExamples(with: multiSelection) } /*...*/
The discussion
Why is it the case? The only difference I see and cannot explain accurately between these two is that supplying the model might give the method access to its self, which is, otherwise, not available. If that's the case, maybe wrapping the methods in some kind of closure with an [unowned self] would help?
I'm fresh to the topic of self in Swift, so I honestly have no idea.
TL;DR: why does it work when I supply the object defining the methods, but does not when I supply only the methods?

SwiftUI - Best pattern to simplify a view init() that's the same across different views

Take this simple view. It has a #StateObject that is used within the view to automatically load and parse some data. I have many of these views with different loaders and parsers.
struct SomeView {
#StateObject var loader: Loader<SomeParser> = Loader<SomeParser>()
var body: some View {
// Some body that uses the above loader
VStack {
// ...
}
}
}
The loaders are set to use #MainActor and since the swift 5.6 update I get the new warning about initiating these with a default value and that it will be an error in swift 6
Expression requiring global actor 'MainActor' cannot appear in
default-value expression of property '_loader'; this is an error in
Swift 6
There's a simple fix, as explained here. We simply set it in the init
struct SomeView {
#StateObject var loader: Loader<SomeParser>
init() {
self._loader = StateObject(wrappedValue: Loader<SomeParser>())
}
var body: some View {
// Some body that uses the above loader
VStack {
// ...
}
}
}
Now the issue I have, is that I have 20+ of these views, with different loaders and parsers and I have to go through each and add this init.
I thought, let's simply create a class that does it and subclass it. But it's a View struct so that's not possible to subclass.
Then I had a go at using a protocol, but I couldn't figure out a way to make it work as overriding the init() in the protocol doesn't let you set self.loader = ...
Is there a better way to do this, or is adding an init to every view the only way?
Well, actually it is possible (I don't know all your 20+ views, but still) to try using generics to separate common parts and generalise them via protocols and dependent views.
Here is a simplified demo of generalisation based on your provided snapshot. Tested with Xcode 13.2 / iOS 15.2
Note: as you will see the result is more generic, but it seems you will need more changes to adapt it than you would just change inits
Separate model into protocol with associated type and required members
protocol LoaderInterface: ObservableObject { // observable
associatedtype Parser // associated parser
init() // needed to be creatable
var isLoading: Bool { get } // just for demo
}
Generalize a view with dependent model and builder based on that model
struct LoadingView<Loader, Content>: View where Loader: LoaderInterface, Content: View {
#StateObject private var loader: Loader
private var content: (Loader) -> Content
init(#ViewBuilder content: #escaping (Loader) -> Content) {
self._loader = StateObject(wrappedValue: Loader())
self.content = content
}
var body: some View {
content(loader) // build content with loader inline
// so observing got worked
}
}
Now try to use above to create concrete view based on concrete model
protocol Creatable { // just helper
init()
}
// another generic loader (as you would probably already has)
class MyLoader<T>: LoaderInterface where T: Creatable {
typealias Parser = T // confirm to LoaderInterface
var isLoading = false
private var parser: T
required init() { // confirm to LoaderInterface
parser = T()
}
}
class MyParser: Creatable {
required init() {} // confirm to Creatable
func parse() {}
}
// demo for specified `LoadingView<MyLoader<MyParser>>`
struct LoaderDemoView: View {
var body: some View {
LoadingView { (loader: MyLoader<MyParser>) in
Text(loader.isLoading ? "Loading..." : "Completed")
}
}
}

MVVM model in SwiftUI

I want to separate view from view model according to MVVM. How would I create a model in SwiftUI? I read that one should use struct rather than class.
As an example I have a model for a park where you can plant trees in:
// View Model
struct Park {
var numberOfTrees = 0
func plantTree() {
numberOfTrees += 1 // Cannot assign to property: 'self' is immutable
}
}
// View
struct ParkView: View {
var park: Park
var body: some View {
// …
}
}
Read things about #State in such things, that make structs somewhat mutable, so I tried:
struct Park {
#State var numberOfTrees = 0 // Enum 'State' cannot be used as an attribute
func plantTree() {
numberOfTrees += 1 // Cannot assign to property: 'self' is immutable
}
}
I did use #State successfully directly in a View. This doesn’t help with separating the view model code though.
I could use class:
class Park: ObservableObject {
var numberOfTrees = 0
func plantTree() {
numberOfTrees += 1
}
}
…but then I would have trouble using this view model nested in another one, say City:
struct City {
#ObservedObject var centerPark: Park
}
Changes in centerPark wouldn’t be published as Park now is reference type (at least not in my tests or here). Also, I would like to know how you solve this using a struct.
as a starting point:
// Model
struct Park {
var numberOfTrees = 0
mutating func plantTree() { // `mutating`gets rid of your error
numberOfTrees += 1
}
}
// View Model
class CityVM: ObservableObject {
#Published var park = Park() // creates a Park and publishes it to the views
// ... other #Published things ...
// Intents:
func plantTree() {
park.plantTree()
}
}
// View
struct ParkView: View {
// create the ViewModel, which creates the model(s)
// usually you would do this in the App struct and make available to all views by .environmentObject
#StateObject var city = CityVM()
var body: some View {
VStack {
Text("My city has \(city.park.numberOfTrees) trees.")
Button("Plant one more") {
city.plantTree()
}
}
}
}
mutating func is the fix but I thought I'd include some other info below:
We don't use MVVM with SwiftUI because we don't use classes for transient view state and we don't control the View in the MVVM/MVC sense. SwiftUI creates and updates the real view objects automatically for us, i.e. UILabels, UITableView etc. The SwiftUI View structs are essentially the view model already, so if you were to recreate that as an object not only will you be needlessly make your code more complex but also would introduce object reference bugs SwiftUI is trying to eliminate by using structs. With property wrappers like #State and #Binding SwiftUI is doing some magic to make the struct behave like an object it is not a good idea to ignore that. To make your View structs more testable you can extract related vars into a struct and use mutating funcs like this:
// View Model
struct ParkConfig {
var numberOfTrees = 0
mutating func plantTree() {
numberOfTrees += 1
}
}
struct ContentView {
#State var parkConfig = ParkConfig()
var body: some View {
ParkView(config: $parkConfig)
}
}
// View
struct ParkView: View {
#Binding var config: ParkConfig
var body: some View {
Button("Click Me") {
config.plantTree()
}
}
}
You can see Apple demonstrate this pattern in Data Essentials in SwiftUI WWDC 2020 at 4:18 where he says "EditorConfig can maintain invariants on its properties and be tested independently. And because EditorConfig is a value type, any change to a property of EditorConfig, like its progress, is visible as a change to EditorConfig itself."

How to reset a subview in SwiftUI?

Below is a simplified version of the code that I'm using. But whenever I resetKeyboard() it still shows the previous keyboard. Is there anyway to make it so when I call resetKeyboard() it replaces the keyboard with a fresh KeyboardView?
struct GameView: View {
#State var myKeyboard = KeyboardView()
var body: some View {
VStack{
Button("Change Keyboard") {
myKeyboard.change()
}
myKeyboard
Button("Reset Keyboard") {
resetKeyboard()
}
}
}
func resetKeyboard(){
self.myKeyboard = KeyboardView()
}
}
SwiftUI constructs a view tree from View objects in body of their parents.
So, what SwiftUI got was the initial copy (remember, it's a value-type struct) of myKeyboard, not the copy you are changing.
Under normal usage, you don't keep instances of various View stored as variables (I mean, you can, but you'd need to understand in depth what's going on).
What you probably want is to change the data that drives the child view. Where does (should) this data live? It depends on what you want to do.
In most cases the parent "owns" the state, i.e. has the source of truth of some data that the child relies on. Then it's trivial to change the state and pass the state to the child via its init:
struct ChildView: View {
let number: Int
var body: some View {
Text("\(number)")
}
}
struct ParentView: View {
#State var random: Int = Int.random(1...100)
var body: some View {
VStack() {
ChildView(number: random)
Button("randomize") {
self.random = Int.random(1...100)
}
}
}
}
But, say, the parent doesn't want to do the randomization - i.e. the child should deal with it.
The proper approach is to create a view model for the child, which the parent (or the parent's own view model) could own and pass via init, and then the view model would deal with nuances of randomization.
class ChildVM: ObservableObject {
#Published var random = Int.random(1...100)
func change() {
random = Int.random(1...100)
}
}
The parent creates an instance of ChildVM and passes it to the child:
struct ParentVuew: View {
let childVM = ChildVM()
var body: some View {
VStack() {
ChildView(vm: childVm)
Button("randomize") {
self.childVM.change()
}
}
}
}
And the child view is simply driven by the view model:
struct ChildView: View {
#ObservedObject let vm: ChildVM
var body: some View {
Text("\(vm.random)")
}
}
Obviously, this is a simplified example that could have been achieved in any number of ways.
And there are different ways for the parent to "message" the child.
But the general takeaway should be that Views should be thought of as declarative structures - not living instances - and the data is what drives the changes in those views. You need to decide who is best to own the source of truth.

SwiftUI creating a dictionary to hold different views

I am experimenting building a pretty simple SwiftUI program to swap views easily. The goal is to make it as simple as possible to add more views without having to change the code that determines which view is how (so a standard if-else isn't going to work).
My current thinking is to keep a dictionary with views stored as part of a key-value pair. A very basic example of the implementation is as follows:
import SwiftUI
struct ViewA: View {
var body: some View {
Text("This is View A")
}
}
struct ViewB: View {
var body: some View {
Text("This is View B")
}
}
struct MainView: View {
var subviews: [String:View] = [
"View-1": ViewA(),
"View-2": ViewB(),
]
var body: some View {
self.subviews["View-1"]
}
}
I am however getting an error on the lines where I am creating the dictionary: Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.
I have tried a number of different types for the value part of the dictionary, including AnyView, Groups, and making a generic type <Content: View> on the MainView struct. These however give more/different errors.
I have looked at the project SwiftUIRouter, as it kind of solves what I a trying to achieve, however my goals are a bit more simplistic.
Thanks in advance.
Swift doesn't have named subscripts, so you have to put it together hackily, but you can use the same calling syntax with otherwise useful/better language features:
struct MainView: View {
var body: some View {
subviews["View-1"]
}
enum subviews {
#ViewBuilder static subscript(string: String) -> some View {
switch string {
case "View-1":
ViewA()
case "View-2":
ViewB()
default:
fatalError()
}
}
}
}