How can I dynamically add SwiftUI Views to a parent View? - ios

Is it possible to add multiple SwiftUI Views to a parent View dynamically & programmatically?
For example suppose we have a basic View such as:
struct MyRectView: View {
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
}
}
And a Button defined as:
struct MyButtonThatMakesRects: View {
var body: some View {
Button(
action: {
// Create & add Views to parent here?
// ...
},
label: {
Text("tap to create a Rect")
}
)
}
}
Is there any way I can create multiple instances of MyRectView in a parent View when MyButtonThatMakesRects is tapped?
My initial thinking was in line with how I would do this in UIKit. That being on button tap, create a new UIView(), and then use .addSubview(...) to add it to a parent. Not sure if SwiftUI has similar functionality. Or maybe there is a simpler way to do this that I'm not seeing?

SwiftUI is functional and reactive, so its output is entirely a reflection of state. You'll have to store and manipulate state that results in a SwiftUI view with your desired outcome. The view is reconstructed from scratch every time its state changes. (Not really, as there's some efficient diffing under the hood, but it's a good mental model to use.)
The simplest way that SwiftUI provides is the #State property wrapper, so a version of what you're asking for would look something like this:
struct RootView: View {
#State private var numberOfRects = 0
var body: some View {
VStack {
Button(action: {
self.numberOfRects += 1
}) {
Text("Tap to create")
}
ForEach(0 ..< numberOfRects, id: \.self) { _ in
MyRectView()
}
}
}
}
I'm guessing your desired end result is more complicated than that, but you can use #State or use a property pointing to a separate class that handles your state/model, marked with the #ObservedObject wrapper, to get to whatever you need.

Related

SwiftUI View is reinitialized when #Binding Property has not been modified

I am executing a SwiftUI playground that contains 2 labels and 2 buttons that modified the value of these labels.
I've stored the value of these labels in a #ObservableObject. Whene I modify the value of any of these properties, both views CustomText2 and CustomText3 are reinitialized, even the one that his values has not changed.
Code:
final class ViewModel: ObservableObject {
#Published var title: Int
#Published var title2: Int
init(title: Int = 0, title2: Int = 0) {
self.title = title
self.title2 = title2
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(
action: {
viewModel.title += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText1(
title: $viewModel.title
)
Button(
action: {
viewModel.title2 += 1
}, label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
)
CustomText2(
title: $viewModel.title2
)
}
.padding()
}
}
struct CustomText1: View {
#Binding var title: Int
init(
title: Binding<Int>
) {
self._title = title
}
var body: some View {
Text("\(title)")
.foregroundColor(.black)
}
}
However if I store both properties as #State in the view and I modify them, the CustomTexts are not reinitialized, they just update their value in the body without executing an init.
Why are they getting reinitialized when I store both properties in the ViewModel?
I've tried to make the views conforming Equatable but they're reinitialized.
Can be a performance problem if the views are initialized many times?
I am interested in not having the subviews reinitialized because I want to perform custom stuff in the init of some subviews.
When you have one StateObject that encompasses multiple State variables, change in one will redraw the entire view. In your case, any change in any variable in viewModel will trigger the publisher of viewModel and reload ContentView
Also we are not supposed to make any assumptions on when a View will be redrawn, as this might change with different versions of SwiftUI. Its better to move this custom stuff you are doing in the init of views to some other place(if it can be). Init should only do work needed to redraw the view with the new state parameters and nothing else.
#ObservableObject is for model data, not view data.
The reason is when using lets or #State vars, SwiftUI uses dependency tracking to decide if body needs to be called and in your case body doesn't use the values anywhere so there is no need to call it.
It can't track objects in the same way, if there is a #StateObject declared then body is called regardless if any properties are accessed, so it's best to start with #State value types and only change to #StateObject when you really need features of a reference type. Not very often now we have .task which is the place to put your custom async work.

Use one #StateObject between all view or one per tab-parent view when passing a common view model object - MVVM App

In the following example, I have an app with two Tab Views, Parent View 1 and Parent View 2, and each parent view has child views. All of the views share a view model (cDViewModel) that handles all of the Core Data related stuff. I have read that when passing data around views you should instantiate your object with #StateObject and then pass it around to other child views using #ObservedObject, clear enough. My confusion is that in my case almost all views in the app will be using the cDViewModel. In my app, I'm currently using Option 1 but I for some reason would like to adopt Option 2 if possible.
Does it make a difference which of the two methods below you use when sharing a common object within an MVVM app?
Option 1
This is how I'm currently using it in my app. Please note that I'm declaring the #StateObject inside the TabView section and start sharing it from there, in other words in this scenario only one instance of the cDViewModel is created.
struct MainView: View {
#StateObject var cDViewModel = CoreDataViewModel()
#State var selectedView = 0
var body: some View {
TabView(selection: $selectedView){
ParentView1(cDViewModel: cDViewModel)
.tabItem {
Text("Parent View 1")
}.tag(0)
ParentView2(cDViewModel: cDViewModel)
.tabItem {
Text("Parent View 2")
}.tag(1)
}
}
}
First Tab View
struct ParentView1: View {
#ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: ChildView1(cDViewModel: cDViewModel)){ }
}
struct ChildView1: View {
#ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Second Tab View
struct ParentView2: View {
#ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: ChildView2(cDViewModel: cDViewModel)){ }
}
struct ChildView2: View {
#ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Option 2
Please note that here I'm declaring the #StateObject in each of the parent views and not in the TabView section, for some reason I tend to like this option better but I'm not sure if this could create refreshing issues by having multiple #StateObject declarations.
struct MainView: View {
#State var selectedView = 0
var body: some View {
TabView(selection: $selectedView){
ParentView1()
.tabItem {
Text("Parent View 1")
}.tag(0)
ParentView2()
.tabItem {
Text("Parent View 2")
}.tag(1)
}
}
}
First Tab View
struct ParentView1: View {
#StateObject var cDViewModel = CoreDataViewModel()
NavigationLink(destination: ChildView1(cDViewModel: cDViewModel)){ }
}
struct ChildView1: View {
#ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
Second Tab View
struct ParentView2: View {
#StateObject var cDViewModel = CoreDataViewModel()
NavigationLink(destination: ChildView2(cDViewModel: cDViewModel)){ }
}
struct ChildView2: View {
#ObservedObject var cDViewModel:CoreDataViewModel
NavigationLink(destination: OtherChildView(cDViewModel: cDViewModel)){ }
}
// Other Child views...
I think this implementation is a slightly misguided attempt at achieving an MVVM-like architecture (trust me, I've been there).
In MVVM the so-called 'view model' is just as it sounds - a model for each view. It is the object that is responsible for keeping track of state for its view. If a user taps a button, the view model would be notified of the interaction, it does some internal recalculating of state, and then publishes that new state for the view to consume and implement. An example of this might be...
struct MyView: View {
#StateObject private var viewModel = MyViewViewModel()
var body: some View {
VStack(spacing: 20) {
Button("Hello") {
viewModel.helloTapped()
}
if viewModel.helloIsVisible {
Text("Why hello friend.")
}
}
}
}
class MyViewViewModel: ObservableObject {
#Published private(set) var helloIsVisible = false
func helloTapped() {
helloIsVisible = true
}
}
In this example, the view model holds the state of the view (helloIsVisible) and publishes it for the view to consume. When the user interacts with the button, the view model recalculates state.
The flaws I see in your implementation are as follows:
You seem to be combining the view model with what should be another object in the service layer of your app. Your view and view model should be completely dumb about how the data that drives your view (hopefully in the form of a concise model) were fetched from CoreData. You can delegate the task of fetching the data and packaging it into a consumable model for the view model into another object (another class) in the service layer of your app. This could be passed around the app in a number of ways, preferably as a dependency, but also perhaps instantiated as a public singleton. It would be wise to read up on the different techniques of making this service available. The view model would either ask this 'CoreData fetching' class for the data that it needs, or the data would be injected as a dependency. Read about singletons here and dependency injection here.
Each view should have its own view model. Presumably the views are different in some way - otherwise, why have more than one? Create a separate view model for each, that each uniquely manage the state of its corresponding view. They should each have a similar mechanism for retrieving data from the 'CoreData fetching' class.

Change Text color on tap in SwiftUI

I have defined a List of Text inside SwiftUI in the following way.
struct SampleView: View {
private let list: [String] = ["Tapping each line should change its color from grey to black", "First Line", "Second Line", "Third Line."]
var body: some View {
ScrollViewReader { value in
List {
ForEach(Array(zip(list, list.indices)), id: \.0) { eachString, index in
Text(eachString)
.bold().font(.largeTitle)
.foregroundColor(.gray)
.onTapGesture {
self.foregroundColor(.black)
}
}
}.listStyle(.plain)
}.frame(maxHeight: .infinity)
}
}
struct SampleView_Preview: PreviewProvider {
static var previews: some View {
SampleView()
}
}
I need to be able to change the foregroundColor of a particular TextView whenever the user taps on it.
CustomString is a class that I defined that conforms to Hashable. I can't get it to conform to ObservableObject at the same time which means I can't change its value inside the onTapGesture block.
I also can't remove the index from the loop as I'll need that too.
TIA.
Don't take it personal at all, just keep in mind that Stackoverflow is a great way to share between developers, but all the basics are much more clear in the documentation and samples from Apple. They are incredibly well done by the way.
https://developer.apple.com/tutorials/swiftui
And for your problem, you must declare a state variable with your color, so when the color changes, the view is regenerated.
I have not tested your code, because you don't provide a copy/paste ready to run playground or view class, but you see the principle.
One sure thing is that if you need each text cell to update independently, you should make a "TextCell" subview or whatever, that has it's own color. Not at the highest level. In your code, I presume all cells will get the same color, since it is defined at top level of your List.
Happy new year :)
...
#State var color: Color = .gray
var myString: [CustomString]
...
ScrollViewReader { value in
List {
ForEach(Array(zip(myString, myString.indices)), id: \.0) { eachString, index in
Text(eachString.text)
.foregroundColor(color)
.onTapGesture {
self.color = .black
}
}
}

Why does NavigationView have to be a top-level view?

I am new to iOS and SwiftUI - why does NavigationView even exist? Why isn't there just NavigationLinks whenever we need to link somewhere?
And why does NavigationView have to be the top level view? Semantically it just doesn't make sense as the top level View should be VStack, Zstack etc
Currently:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
Text("Hello World")
NavigationLink(destination: DetailView()) {
Text("Do Something")
}
}
}
}
}
Should be:
struct ContentView : View {
var body: some View {
VStack {
Text("Hello World")
NavigationView {
NavigationLink(destination: DetailView()) {
Text("Do Something")
}
}
}
}
}
Any idea?
SwiftUI gives you a whole bunch of conveniences practically for free.
One of those is an interface building block called NavigationView.
That interface building block has some convenient features and abilities, such as giving you automatic navigation interface (like back buttons to the original View in the navigation bar of the view you followed via a NavigationLink, etc).
Most of those conveniences simply wouldn’t make sense if it wasn’t the top level of your view.
Is there something you wanted from NavigationView at a lower level? I can’t imagine what, but I suspect there’s another SwiftUI building block that will achieve it, made specifically for the context.

Lifecycling in SwifUI: Running code when leaving a child view of a NavigationView hierarchy

With SwiftUI's NavigationView we benefit from simplicity with code construction. However, not exposed, at least in these early stages are the overrides. Further, the reduced focus on managing the LifeCycle of views makes it difficult to find out what and when to call something based on the state of the view.
I would like to run some code when a user chooses to go back up a NavigationView hierarchy (i.e. click the back button supplied by NavigationView).
I've tried onDisappear() {} and it's other variants and I cannot get it to work. It seems like it is not called. The onAppear() {} does work so I'm stumped.
Any help would be SUPER APPRECIATED!
I'm quite certain that at this stage, there is no method that can be override to catch the 'back' action of the navigation view.
However, I did figure out that I can hide the NavigationView's back button and add a custom one myself where I can call the code before dismissing the child.
import SwiftUI
struct SecondView: View {
var body: some View {
Text("Second View")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: NavigationViewCustomBack())
}
}
==========================
import SwiftUI
struct NavigationViewCustomBack: View {
var body: some View {
HStack{
Image(systemName: "chevron.left").foregroundColor(.blue).font(Font.title.weight(.medium))
Text("Home").padding(.leading, -5)
}
}
}
struct NavigationViewCustomBack_Previews: PreviewProvider {
static var previews: some View {
NavigationViewCustomBack()
}
}

Resources