Call view modifying function on child view - swift

I have a view which has a function fadeInOut() that fades a square in and out.
struct FadingSquare: View {
#State var fading = false
var body: some View {
Rectangle()
.frame(width: 20, height: 20)
.foregroundColor(.primary)
.opacity(fading ? 1 : 0)
.onAppear {
fadeInOut()
}
}
func fadeInOut() {
withAnimation(.linear(duration: 1)) {
self.fading = true
}
withAnimation(.linear(duration: 1).delay(1)) {
self.fading = false
}
}
}
That works fine, the square fades in, then fades out.
But I want to place FadingSquare in other places, and call fadingSquare.fadeInOut() from a parent view. This is what I have attempted:
struct ContentView: View {
var fadingSquare = FadingSquare()
var body: some View {
VStack {
fadingSquare
Button {
fadingSquare.fadeInOut()
} label: {
Text("Fade")
}
}
}
}
This does not work, and making fadingSquare a #State doesn't work either. I'd rather not use Bindings, I want the FadingSquare to be responsible for its own values.

So in SwiftUI, parents can modify their children in really just two ways.
Within the parent view, (ContentView) add a #State variable to track the fade. Within the child view, (FadingSquare) change the #State variable to #Binding and remove the default value. Now, within ContentView, you can have the square update when the parent's fade variable changes by writing it in the body as FadingSquare(fading: $ourFadingState) where ourFadingState is that #State variable we created.
The parent and the child each hold a reference to the same ObservableObject. This is the most performant way to do it when dealing with complex UIs. Within the parent's button action, you'd modify the object's fading property. Within the FadingSquare, you'd have an #ObservedObject variable which would cause the square to update itself upon any changes, regardless of if they were made within the view itself, a parent, or on the moon.
By the way, for SwiftUI to work properly, you don't keep a fadingView property, you instantiate a brand new one within the body code. This is because when it's a property, it can't update until the parent gets trashed, which will never happen because ContentView is typically the root view of a scene and will never get destroyed. Look at the system SwiftUI views and notice you don't need to hold them within variables either. We do it the same way.
So instead of:
var fadingSquare = FadingSquare()
var body: some View {
fadingSquare
}
It should be:
var body: some View {
FadingSquare()
}
Remember that unlike UIKit, a SwiftUI view struct is not the view itself. It is a collection of data describing to the system what the view should contain during a given refresh, like HTML. So initializing a new FadingSquare() within body code is correct and it will be the same view, even if it is "a new" struct in code.

Related

SwiftUI: How do I share an ObservedObject between child views without affecting the parent view

TL;DR: If I have a view containing a NavigationSplitView(sidebar:detail:), with a property (such as a State or StateObject) tracking user selection, how should I make it so that the sidebar and detail views observe the user selection, but the parent view does not?
Using SwiftUI's new NavigationSplitView (or the deprecated NavigationView), a common paradigm is to have a list of selectable items in the sidebar view, with details of the selected item in the detail view. The selection, of course, needs to be observed, usually from within an ObservedObject.
struct ExampleView: View {
#StateObject private var viewModel = ExampleViewModel()
var body: some View {
NavigationSplitView {
SidebarView(selection: $viewModel.selection)
} detail: {
DetailView(item: viewModel.selection)
}
}
}
struct SidebarView: View {
let selectableItems: [Item] = []
#Binding var selection: Item?
var body: some View {
List(selectableItems, selection: $viewModel.selected) { item in
NavigationLink(value: item) { Text(item.name) }
}
}
}
struct DetailView: View {
let item: Item?
var body: some View {
// Details of the selected item
}
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
}
This, however, poses a problem: the ExampleView owns the ExampleViewModel used for tracking the selection, which means that it gets recalculated whenever the selection changes. This, in turn, causes its children SidebarView and DetailView to be redrawn.
Since we want those children to be recalculated, one might be forgiven for thinking that everything is as intended. However, the ExampleView itself should not be recalculated in my opinion, because doing so will not only update the child views (intended), but also everything in the parent view (not intended). This is especially true if its body is composed of other views, modifiers, or setup work. Case in point: in this example, the NavigationSplitView itself will also be recalculated, which I don't think is what we want.
Almost all tutorials, guides and examples I see online use a version of the above example - sometimes the viewModel is passed as an ObservedObject, or as an EnvironmentObject, but they all share the same trait in that the parent view containing the NavigationSplitView is observing the property that should only be observed by the children of NavigationSplitView.
My current solution is to initiate the viewmodel in the parent view, but not observe it:
struct ExampleView: View {
let viewModel = ExampleViewModel()
...
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
nonisolated init() { }
}
This way, the parent view will remain intact (at least in regards to user selection); however, this will cause the ExampleViewModel to be recreated if anything else would cause the ExampleView to be redrawn - effectively resetting our user selection. Additionally, we are unable to pass any of the viewModel's properties as bindings. So while it works for my current use-case, I don't consider this an effective solution.

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.

Navigation between SwiftUI Views

I don't know how to navigate between views with buttons.
The only thing I've found online is detail view, but I don't want a back button in the top left corner. I want two independent views connected via two buttons one on the first and one on the second.
In addition, if I were to delete the button on the second view, I should be stuck there, with the only option to going back to the first view being crashing the app.
In storyboard I would just create a button with the action TouchUpInSide() and point to the preferred view controller.
Also do you think getting into SwiftUI is worth it when you are used to storyboard?
One of the solutions is to have a #Statevariable in the main view. This view will display one of the child views depending on the value of the #Statevariable:
struct ContentView: View {
#State var showView1 = false
var body: some View {
VStack {
if showView1 {
SomeView(showView: $showView1)
.background(Color.red)
} else {
SomeView(showView: $showView1)
.background(Color.green)
}
}
}
}
And you pass this variable to its child views where you can modify it:
struct SomeView: View {
#Binding var showView: Bool
var body: some View {
Button(action: {
self.showView.toggle()
}) {
Text("Switch View")
}
}
}
If you want to have more than two views you can make #State var showView1 to be an enum instead of a Bool.

ObervableObject being init multiple time, and not refreshing my view

I have a structure like this:
contentView {
navigationView {
foreach {
NavigationLink(ViewA(id: id))
}
}
}
/// where ViewA contains an request trigger when it appears
struct ViewA: View {
#State var filterString: String = ""
var id: String!
#ObservedObject var model: ListObj = ListObj()
init(id: String) {
self.id = id
}
var body: some View {
VStack {
SearchBarView(searchText: $filterString)
List {
ForEach(model.items.filter({ filterString.isEmpty || $0.id.contains(filterString) || $0.name.contains(filterString) }), id: \.id) { item in
NavigationLink(destination: ViewB(id: item.id)) {
VStack {
Text("\(item.name) ")
}
}
}
}
}
.onAppear {
self.model.getListObj(id: self.id) //api request, fill data and call objectWillChange.send()
}
}
}
ViewB has the same code as ViewA: It receives an id, stores and requests an API to collect data.
But the viewB list is not being refreshed.
I also noticed that viewB's model property
#ObservedObject var model: model = model()
was instantiated multiple times.
Debugging, I found that every navigationLink instantiates its destination even before it is triggered. That's not a problem usually, but in my case i feel like the ViewB model is being instantiated 2 times, and my onAppear call the wrong one, reason why self.objectWillChange.send() not refreshing my view.
There are two issues here:
SwiftUI uses value types that that get initialized over and over again each pass through body.
Related to #1, NavigationLink is not lazy.
#1
A new ListObj gets instantiated every time you call ViewA.init(...). ObservedObject does not work the same as #State where SwiftUI keeps careful track of it for you throughout the onscreen lifecycle. SwiftUI assumes that ultimate ownership of an #ObservedObject exists at some level above the View it's used in.
In other words, you should almost always avoid things like #ObservedObject var myObject = MyObservableObject().
(Note, even if you did #State var model = ListObj() it would be instantiated every time. But because it's #State SwiftUI will replace the new instance with the original before body gets called.)
#2
In addition to this, NavigationLink is not lazy. Each time you instantiate that NavigationLink you pass a newly instantiated ViewA, which instantiates your ListObj.
So for starters, one thing you can do is make a LazyView to delay instantiation until NavigationLink.destination.body actually gets called:
// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
var content: () -> Content
var body: some View {
self.content()
}
}
Now you can do NavigationLink(destination: LazyView { ViewA() }) and instantiation of ViewA will be deferred until the destination is actually shown.
Simply using LazyView will fix your current problem as long as it's the top view in the hierarchy, like it is when you push it in a NavigationView or if you present it.
However, this is where #user3441734's comment comes in. What you really need to do is keep ownership of model somewhere outside of your View because of what was explained in #1.
If your #ObservedObject is being initialized multiple times, it is because the owner of the object is refreshed and recreated every time it has state changes. Try to use #StateObject if your app is iOS 14 and above. It prevents the object from being recreated when the view refreshes.
https://developer.apple.com/documentation/swiftui/stateobject
When a view creates its own #ObservedObject instance it is recreated
every time a view is discarded and redrawn. On the contrary a #State
variable will keep its value when a view is redrawn. A #StateObject is
a combination of #ObservedObject and #State - the instance of the
ViewModel will be kept and reused even after a view is discarded and
redrawn
What is the difference between ObservedObject and StateObject in SwiftUI

SwiftUI: How do I avoid modifying state during view update?

I want to update a text label after it is being pressed, but I am getting this error while my app runs (there is no compile error): [SwiftUI] Modifying state during view update, this will cause undefined behaviour.
This is my code:
import SwiftUI
var randomNum = Int.random(in: 0 ..< 230)
struct Flashcard : View {
#State var cardText = String()
var body: some View {
randomNum = Int.random(in: 0 ..< 230)
cardText = myArray[randomNum].kana
let stack = VStack {
Text(cardText)
.color(.red)
.bold()
.font(.title)
.tapAction {
self.flipCard()
}
}
return stack
}
func flipCard() {
cardText = myArray[randomNum].romaji
}
}
If you're running into this issue inside a function that isn't returning a View (and therefore can't use onAppear or gestures), another workaround is to wrap the update in an async update:
func updateUIView(_ uiView: ARView, context: Context) {
if fooVariable { do a thing }
DispatchQueue.main.async { fooVariable = nil }
}
I can't speak to whether this is best practices, however.
Edit: I work at Apple now; this is an acceptable method. An alternative is using a view model that conforms to ObservableObject.
struct ContentView: View {
#State var cardText: String = "Hello"
var body: some View {
self.cardText = "Goodbye" // <<< This mutation is no good.
return Text(cardText)
.onTapGesture {
self.cardText = "World"
}
}
}
Here I'm modifying a #State variable within the body of the body view. The problem is that mutations to #State variables cause the view to update, which in turn call the body method on the view. So, already in the middle of a call to body, another call to body initiated. This could go on and on.
On the other hand, a #State variable can be mutated in the onTapGesture block, because it's asynchronous and won't get called until after the update is finished.
For example, let's say I want to change the text every time a user taps the text view. I could have a #State variable isFlipped and when the text view is tapped, the code in the gesture's block toggles the isFlipped variable. Since it's a special #State variable, that change will drive the view to update. Since we're no longer in the middle of a view update, we won't get the "Modifying state during view update" warning.
struct ContentView: View {
#State var isFlipped = false
var body: some View {
return Text(isFlipped ? "World" : "Hello")
.onTapGesture {
self.isFlipped.toggle() // <<< This mutation is ok.
}
}
}
For your FlashcardView, you might want to define the card outside of the view itself and pass it into the view as a parameter to initialization.
struct Card {
let kana: String
let romaji: String
}
let myCard = Card(
kana: "Hello",
romaji: "World"
)
struct FlashcardView: View {
let card: Card
#State var isFlipped = false
var body: some View {
return Text(isFlipped ? card.romaji : card.kana)
.onTapGesture {
self.isFlipped.toggle()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return FlashcardView(card: myCard)
}
}
#endif
However, if you want the view to change when card itself changes (not that you necessarily should do that as a logical next step), then this code is insufficient. You'll need to import Combine and reconfigure the card variable and the Card type itself, in addition to figuring out how and where the mutation going to happen. And that's a different question.
Long story short: modify #State variables within gesture blocks. If you want to modify them outside of the view itself, then you need something else besides a #State annotation. #State is for local/private use only.
(I'm using Xcode 11 beta 5)
On every redraw (in case a state variable changes) var body: some View gets reevaluated. Doing so in your case changes another state variable, which would without mitigation end in a loop since on every reevaluation another state variable change gets made.
How SwiftUI handles this is neither guaranteed to be stable, nor safe. That is why SwiftUI warns you that next time it may crash due to this.
Be it due to an implementation change, suddenly triggering an edge condition, or bad luck when something async changes text while it is being read from the same variable, giving you a garbage string/crash.
In most cases you will probably be fine, but that is less so guaranteed than usual.