Best method to return multiple views in SwiftUI - swift

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.

Related

Updating property wrapper like #StateObject, affects other view rendering that does not use that property

When using different property wrappers associated with view updates, changes in one place affect rendering of views that do not use that property.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
Text(thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
class MyViewModel: ObservableObject {
#Published var firstTitle = "firstTitle"
#Published var secondTitle = "secondTitle"
func chageFirstTitle() {
firstTitle = "hello world"
}
}
I understand that the reason why the Text exposing the viewModel.secondTitle is re-rendered is because the #StateObject varviewModel = MyViewModel() dependency changed when the `viewModel.firstTitle changed.
However, I don't know why Text using #State var thirdTitle = "thirdTitle" is re-rendered too. In WWDC21 session Demystify SwiftUI, I saw that the view is re-rendered only when the related dependency is updated according to the dependency graph. But, even though the thirdTitle is irrelevant to the change of the viewModel, third Text using that dependency is re-rendered and the background color is changed.
What's even more confusing is that if I seperate the third Text into a separate view ( ThirdView ) and receive the thirdTitle using #Binding, the background color does not change because it is not re-rendering at that time.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
ThirdView(text: $thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
Regarding the situation I explained, could you help me to understand the rendering conditions of the view?
To render SwiftUI calls body property of a view (it is computable, i.e. executes completely on call). This call is performed whenever any view dependency, i.e. dynamic property, is changed.
So, viewModel.chageFirstTitle() changes dependency for ContentView and ContentView.body is called and every primitive in it is rendered. ThirdView also created but as far as its dependency is not changed, its body is not called, so content is not re-rendered.
A few things wrong here. We don't use view model objects in SwiftUI for view data, it's quite inefficient/buggy to do so. Instead, use a struct with mutating funcs with an #State. Pass in params to sub-Views as lets for read access, #Binding is only when you need write access. In terms of rendering, first of all body is only called if the let property is different from the last time the sub-View is init, then it diffs the body from the last time it was called, if there are any differences then SwiftUI adds/removes/updates actual UIKit UIViews on your behalf, then actual rendering of those UIViews, e.g. drawRect, is done by CoreGraphics.
struct ContentViewConfig {
var firstTitle = "firstTitle"
var secondTitle = "secondTitle"
mutating func changeFirstTitle() {
firstTitle = "hello world"
}
}
struct ContentView: View {
#State var config = Config()
...
struct ThirdView: View {
let text: String
...
Combine's ObservableObject is usually only used when needing to use Combine, e.g. using combineLatest with multiple publishers or for a Store object to hold the model struct arrays in #Published properties that are not tied to a View's lifetime like #State. Your use case doesn't look like a valid use of ObservableObject.

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.

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

Hug subviews in SwiftUI

Dave Abrahams explained some of the mechanics of SwiftUI layouts in his WWDC19 talk about Custom Views, but he left out some bits and I have trouble getting my views properly sized.
Is there a way for a View to tell its container that it is not making any space demands, but it will use all the space it is given? Another way to say it is that the container should hug its subviews.
Concrete example, I want something like c:
If you have some Texts inside a VStack like in a), the VStack will adopt it's width to the widest subview.
If you add a Rectangle though as in b), it will expand as much as it can, until the VStack fills its container.
This indicates that Texts and Rectangles are in different categories when it comes to layout, Text has a fixed size and a Rectangle is greedy. But how can I communicate this to my container if I'm making my own View?
The result I actually want to achieve is c). VStack should ignore Rectangle (or my custom view) when it determines its size, and then once it has done that, then it should tell Rectangle, or my custom view, how much space it can have.
Given that SwiftUI seems to layout bottom-up, maybe this is impossible, but it seems that there should be some way to achieve this.
There is no modifier (AFAIK) to accomplish this, so here's my approach. If this is something you are going to use too often, it could be worth creating your own modifier.
Also note that here I am using standard preferences, but anchor preferences are even better. It is a heavy topic to explain here. I've written an article that you can check here: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
You can use the code below to accomplish what you are looking for.
import SwiftUI
struct MyRectPreference: PreferenceKey {
typealias Value = [CGRect]
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
struct ContentView : View {
#State private var widestText: CGFloat = 0
var body: some View {
VStack {
Text("Hello").background(RectGetter())
Text("Wonderful World!").background(RectGetter())
Rectangle().fill(Color.blue).frame(width: widestText, height: 30)
}.onPreferenceChange(MyRectPreference.self, perform: { prefs in
for p in prefs {
self.widestText = max(self.widestText, p.size.width)
}
})
}
}
struct RectGetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyRectPreference.self, value: [geometry.frame(in: .global)])
}
}
}
So I actually found a way to do this. First I tried putting Spacers around the views in various configurations, to try to push it together, but that didn't work. Then I realised I could perhaps use the .background modifier, and that actually did work. It seems to let the owning view calculate its size first, and then just takes that as its frame, which is exactly what I want.
This is just an example with some hacks to get the right height, but that is a small detail, and in my particular use case it is not needed. Probably not here either if you're clever enough.
var body: some View {
VStack(spacing: 10) {
Text("Short").background(Color.green)
Text("A longer text").background(Color.green)
Text("Dummy").opacity(0)
}
.background(backgroundView)
.background(Color.red)
.padding()
.background(Color.blue)
}
var backgroundView: some View {
VStack(spacing: 10) {
Spacer()
Spacer()
Rectangle().fill(Color.yellow)
}
}
The blue view and all the color backgrounds are of course just to make it easier to see.
This code produces this: