SwiftUI creating a dictionary to hold different views - swift

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()
}
}
}
}

Related

Best method to return multiple views in SwiftUI

var myView: some View {
Group {
Text("Hello")
Text("Bye")
}
}
#ViewBuilder var myView: some View {
Text("Hello")
Text("Bye")
}
Should I use a Group or #ViewBuilder, are there any advantages in using one over the other in terms of performance and customizability? If not, is there a convention in place to use one rather than the other?
Personally I think that you shouldn't do both, if you need two vertically aligned Text views then just wrap them in VStack (or whatever you need) and make a component for flexibility. It will help with debug and will make your entire codebase more readable. Just write something like that and properly name it.
struct TwoTextViews: some View {
#Binding var firstLabelText: String
#Binding var secondLabelText: String
var body: some View {
VStack {
Text(firstLabelText)
Text(secondLabelText)
}
}
}
Don't really get the situation when you should return a list of separate views instead of them wrapped into some container.

SwiftUI - Accessing a property of a generic observed class

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.

Pass view to struct in SwiftUI

I'm trying to pass a view into a struct for creating a tab which is the following code:
struct TabItem: TabView {
var tabView: View
var tabText: String
var tabIcon: String
var body: some View {
self.tabView.tabItem {
Text(self.tabText)
Image(systemName: self.tabIcon)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
TabItem(tabView: SimpleCalculatorView(), tabText: "Home", tabIcon: "house")
}
}
}
}
The error I'm getting is the following:
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
The error says here is that the View protocol has associatedtype Body inside it and thus you can't use the View as a type. This means we have to provide more information about the expected type of the tabView. One of the solutions would be to use AnyView as a type, but that would require additional content wrapping into AnyView.
What I would suggest doing here instead of using AnyView is to let the compiler figure out the actual tabView type for you.
Let's tell the compiler that we expect some type Content that confirms to the View protocol
Additionally I don't really see the necessity of using TabView as part of the TabItem declaration. Try going with just View unless you have a strong reason for not doing so.
struct TabItem<Content: View>: View { // Content is a type that we expect. `View` is used instead of `TabView` as it is in original code
var tabView: Content // Using Content instead of a View as it is an actual type now
...
}
The rest of the code can stay unmodified.

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 - #Binding to a computed property which accesses value inside ObservableObject property duplicates the variable?

In the code below (a stripped-down version of some code in a project) I'm using a MVVM pattern with two views:
ViewA - displays a value stored in an ObservableObject ViewModel;
ViewB - displays the same value and has a Slider that changes that value, which is passed to the view using Binding.
Inside of ViewModelA I have a computed property which serves both to avoid the View from accessing the Model directly and to perform some other operations when the value inside the model (the one being displayed) is changed.
I'm also passing that computed value to a ViewModelB, using Binding, which acts as a StateObject for ViewB. However, when dragging the Slider to change that value, the value changes on ViewA but doesn't change on ViewB and the slider itself doesn't slide. As expected, when debugging, the wrappedValue inside the Binding is not changing. But how is the change propagated upwards (through the Binding's setters, I imagine) but not downwards back to ViewB?? I imagine this can only happen if the variable is being duplicated somewhere and changed only in one place, but I can't seem to understand where or if that's what's actually happening.
Thanks in advance!
Views:
import SwiftUI
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.value)
}
}
}
struct ViewA: View {
let value: Double
var body: some View {
Text("\(value)").padding()
}
}
struct ViewB: View {
#StateObject var viewModelB: ViewModelB
init(value: Binding<Double>){
_viewModelB = StateObject(wrappedValue: ViewModelB(value: value))
}
var body: some View {
VStack{
Text("\(viewModelB.value)")
Slider(value: $viewModelB.value, in: 0...1)
}
}
}
ViewModels:
class ViewModelA: ObservableObject {
#Published var model = Model()
var value: Double {
get {
model.value
}
set {
model.value = newValue
// perform other checks and operations
}
}
}
class ViewModelB: ObservableObject {
#Binding var value: Double
init(value: Binding<Double>){
self._value = value
}
}
Model:
struct Model {
var value: Double = 0
}
If you only look where you can't go, you might just miss the riches below
Breaking single source of truth, and breaching local (private) property of #StateObjectby sharing it via Binding are two places where you can't go.
#EnvironmentObject or more generally the concept of "shared object" between views are the riches below.
This is an example of doing it without MVVM nonsense:
import SwiftUI
final class EnvState: ObservableObject {#Published var value: Double = 0 }
struct ContentView: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
ViewA()
ViewB()
}
}
}
struct ViewA: View {
#EnvironmentObject var eos: EnvState
var body: some View {
Text("\(eos.value)").padding()
}
}
struct ViewB: View {
#EnvironmentObject var eos: EnvState
var body: some View {
VStack{
Text("\(eos.value)")
Slider(value: $eos.value, in: 0...1)
}
}
}
Isn't this easier to read, cleaner, less error-prone, with fewer overheads, and without serious violation of fundamental coding principles?
MVVM does not take value type into consideration. And the reason Swift introduces value type is so that you don't pass shared mutable references and create all kinds of bugs.
Yet the first thing MVVM devs do is to introduce shared mutable references for every view and pass references around via binding...
Now to your question:
the only options I see are either using only one ViewModel per Model, or having to pass the Model (or it's properties) between ViewModels through Binding
Another option is to drop MVVM, get rid of all view models, and use #EnvironmentObject instead.
Or if you don't want to drop MVVM, pass #ObservedObject (your view model being a reference type) instead of #Binding.
E.g.;
struct ContentView: View {
#ObservedObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA)
ViewB(value: viewModelA)
}
}
}
On a side note, what's the point of "don't access model directly from view"?
It makes zero sense when your model is value type.
Especially when you pass view model reference around like cookies in a party so everyone can have it.
Really it looks like broken single-source or truth concept. Instead the following just works (ViewModelB might probably be needed for something, but not for this case)
Tested with Xcode 12 / iOS 14
Only modified parts:
struct ContentView: View {
#StateObject var viewModelA = ViewModelA()
var body: some View {
VStack{
ViewA(value: viewModelA.value)
ViewB(value: $viewModelA.model.value)
}
}
}
struct ViewB: View {
#Binding var value: Double
var body: some View {
VStack{
Text("\(value)")
Slider(value: $value, in: 0...1)
}
}
}