ChildView in FullScreenCover is recreated every time if ContentView is updated - swift

I have a question related to the next behavior.
I have ContentView with a list of views where the corresponding view models are passed. The user can click by some view. At the moment full-screen modal dialog will be shown according to the passed type. It's fine.
At some time my view models are being updated and the whole ContentView will be reloaded. The problem is: fullScreenCover is called and ChildEventView is recreated. How to prevent recreating ChildEventView?
struct ContentView: View {
#State private var fullScreenType: FullScreenType?
// some stuff
var body: some View {
ScrollView {
LazyVStack {
ForEach(eventListViewModel.cardStates.indices, id: \.self) { index in
let eventVM = eventListViewModel.eventVMs[index]
EventCardView(eventViewModel: eventVM, eventId: $selectedEvent.eventId) {
self.fullScreenType = .type1
}
// some other views
}
}
}
.fullScreenCover(item: $fullScreenType, onDismiss: {
self.fullScreenType = nil
}, content: { fullScreenType in
switch fullScreenType {
case .type1:
return ChildEventView(selectedEvent.eventId).eraseToAnyView()
// some other cases
}
})
}
}

Related

Use a Full Screen Cover within a View with a Full Screen Cover

I have a parent view covering the entire screen that has a .fullScreenCover. That parent view contains several child views with all the details of the 'page'. One of those child views contains an element that also has a .fullScreenCover control.
struct ParentView: View {
#State var isPresent = false
var body: some View {
VStack {
// Other children
ChildView()
}.fullScreenCover(isPresent: self.$isPresent) {
CoverView()
}
}
}
struct ChildView: View {
#State var isPresent = false
var body: some View {
Button("Hello", action: { self.isPresent = true })
.fullScreenView(isPresent: self.$isPresent) {
ChildCoverView()
}
}
}
The screen cover for the parent view opens as expected, but when I try to open the screen cover from the child view, nothing happens. If I remove the .fullScreenCover control from the parent view, then the child screen view does work, so it must have something to do with the nesting and all.
Is there a way to get a child .fullScreenCover to work inside a parent view that also has a .fullScreenCover?
There was/is a bug preventing this from working until iOS 14.5 beta 3.
From the release notes (https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-14_5-beta-release-notes):
You can now apply multiple sheet(isPresented:onDismiss:content:) and fullScreenCover(item:onDismiss:content:) modifiers in the same view hierarchy. (74246633)
The workaround until then is to present from a common ancestor or present the parent fullScreenCover on a different branch of the view tree (ie one that doesn't contain the child view):
struct ContentView: View {
#State var isPresent = false
var body: some View {
VStack {
// Other children
ChildView() //branch 1
Button("Present Parent") { //branch 2
isPresent.toggle()
}.fullScreenCover(isPresented: self.$isPresent) {
Text("hello")
}
}
}
}
struct ChildView: View {
#State var isPresent = false
var body: some View {
Button("Present child", action: { self.isPresent = true })
.fullScreenCover(isPresented: self.$isPresent) {
Text("world")
}
}
}

SwiftUI - Conditional based on ObservedObject doesn't work in subsequent Views

I have a very simple app which contains two views that are tied together with a NavigationLink. In the first view, ContentView, I can see updates to my ObservedObject as expected. However, when I go to the next View, it seems that the code based on the ObservedObject does not recognize changes.
ContentView.swift (The working view):
import SwiftUI
struct ContentView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
NavigationView {
VStack(spacing: 15) {
Toggle(isOn: self.$toggleObject.isToggled) {
Text("Toggle:")
}
if self.toggleObject.isToggled == true {
Text("ON")
}
else {
Text("OFF")
}
NavigationLink(destination: ShowToggleView()) {
Text("Show Toggle Status")
}
}
}
}
}
ShowToggleView.swift (The view that does not behave as I expect it to):
import SwiftUI
struct ShowToggleView: View {
#ObservedObject var toggleObject = ToggleObject()
var body: some View {
Form {
Section {
if self.toggleObject.isToggled {
Text("Toggled on")
}
else {
Text("Toggled off")
}
}
}
}
}
All of this data is stored in a simple file, ToggleObject.swift:
import SwiftUI
class ToggleObject: ObservableObject {
#Published var isToggled = false
}
When I toggle it on in the first View I see the text "ON" which is expected, but when I go into the next view it shows "Toggled off" no matter what I do in the first View... Why is that?
Using Xcode 11.5 and Swift 5
You are almost doing everything correct. However, you are creating another instance of ToggleObject() in your second view, which overrides the data. You basically only create one ObservedObject and then pass it to your subview, so they both access the same data.
Change it to this:
struct ShowToggleView: View {
#ObservedObject var toggleObject : ToggleObject
And then pass the object to that view in your navigation link...
NavigationLink(destination: ShowToggleView(toggleObject: self.toggleObject)) {
Text("Show Toggle Status")
}

Value from #State variable does not change

I have created a View that provides a convinient save button and a save method. Both can then be used inside a parent view.
The idea is to provide these so that the navigation bar items can be customized, but keep the original implementation.
Inside the view there is one Textfield which is bound to a #State variable. If the save method is called from within the same view everthing works as expected. If the parent view calls the save method on the child view, the changes to the #State variable are not applied.
Is this a bug in SwiftUI, or am I am missing something? I've created a simple playbook implementation that demonstrates the issue.
Thank you for your help.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
// Create the child view to make the save button available inside this view
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
// Set the trailing button to the one from the child view.
// This is required as this view might be inside a modal
// sheet, and we need to add the cancel button as a leading
// button:
// leading: self.cancelButton
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
I think a more SwiftUi way of doing it:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
return NavigationView {
// tell the child view where to render it's navigation item
// Instead of configuring navigation items.
NavigationLink(destination: Child(navigationSide: .left)) {
Text("Open")
}
}
}
}
struct Child: View {
enum NavigationSide { case left, right }
// If you really want to encapsulate all state in this view then #State
// is a good choice.
// If the parent view needs to read it, too, #Binding would be your friend here
#State private var value: String = "default"
// no need for #State as it's never changed from here.
var navigationSide = NavigationSide.right
// wrap in AnyView here to make ternary in ui code easier readable.
var saveButton: AnyView {
AnyView(Button(action: save) {
Text("Save")
})
}
var emptyAnyView: AnyView { AnyView(EmptyView()) }
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(leading: navigationSide == .left ? saveButton : emptyAnyView,
trailing: navigationSide == .right ? saveButton : emptyAnyView)
}
func save() {
print("\(value)")
}
}
TextField will only update your value binding when the return button is pressed. To get text changes that occur during editing, set up an observed object on Child with didSet. This was the playground I altered used from your example.
struct ContentView: View {
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
class TextChanges: ObservableObject {
var completion: (() -> ())?
#Published var text = "default" {
didSet {
print(text)
}
}
}
struct Child: View {
#ObservedObject var textChanges = TextChanges()
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $textChanges.text).multilineTextAlignment(.center)
Button(action: {
print(self.textChanges.text)
}) {
Text("Update")
}
}
}
func save() {
print("\(textChanges.text)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Inside Child: value is mutable because it's wrapped with #State.
Inside ContentView: child is immutable because it's not wrapped with #State.
Your issue can be fixed with this line: #State var child = Child()
Good luck.
Child view needs to keep its state as a #Binding. This works:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var v = "default"
var body: some View {
let child = Child(value: $v)
return NavigationView {
NavigationLink(
destination: child.navigationBarItems(trailing: child.saveButton)
) {
Text("Open")
}
}
}
}
struct Child: View {
#Binding var value: String
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Based on this commend from #nine-stones (thank you!) I implemented a more SwiftUI way so solve my problem. It does not allow the customization of the navigation items as I planned, but that was not the problem that needed to be solved. I wanted to use the Child view in a navigation link, as well as inside a modal sheet. The problem was how to perform custom cancel actions. This is why I removed the button implementation and replaced it with a cancelAction closure. Now I can display the child view wherever and however I want.
One thing I still do not know why SwiftUI is not applying the child context to the button inside the saveButton method.
Still, here is the code, maybe it helps someone in the future.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
NavigationLink(
destination: Child(
// Instead of defining the buttons here, I send an optional
// cancel action to the child. This will make it possible
// to use the child view on navigation links, as well as in
// modal dialogs.
cancelAction: {
self.presentationMode.wrappedValue.dismiss()
}
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
#Environment(\.presentationMode) var presentationMode
var cancelAction: (() -> Void)?
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(
leading: self.cancelAction != nil ? Button(action: self.cancelAction!, label: {
Text("Cancel")
}) : nil,
trailing: self.saveButton
)
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())

How to access content view's elements later in SwiftUI?

Let's say that I have a content view like this one:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
CustomClass()
.tabItem {
VStack {
Image("First")
Text("First")
}
}
.tag(0)
Button(action: { EmployeeStorage.sharedInstance.reload() }) {
Text("Reload")
}
.tabItem {
VStack {
Image("Second")
Text("Second")
}
}
.tag(1)
}
}
}
// MARK: - SomeDelegateThatUpdatesMeLater
extension ContentView: SomeDelegateThatUpdatesMeLater {
func callback() {
// Here I want to update my content view's subviews
// The ContentClass instance needs to be updated
}
}
Let's say that I want to listen to a callback method and then update the content view's tab number 1 (CustomClass) later on. How to access the content view's subviews? I'd need something like UIKit's subviewWithTag(_:). Is there any equivalent in SwiftUI?
You should probably rethink your approach. What exactly is it you want to happen? You have some external data model type that fetches (or updates) data and you want your views to react to that? If that's the case, create an ObservableObject and pass that to your CustomClass.
struct CustomClass: View {
#ObservedObject var model: Model
var body: some View {
// base your view on your observed-object
}
}
Perhaps you want to be notified of events that originate from CustomClass?
struct CustomClass: View {
var onButtonPress: () -> Void = { }
var body: some View {
Button("Press me") { self.onButtonPress() }
}
}
struct ParentView: View {
var body: some View {
CustomClass(onButtonPress: { /* react to the press here */ })
}
}
Lastly, if you truly want some kind of tag on your views, you can leverage the Preferences system in SwiftUI. This is a more complicated topic so I will just point out what I have found to be a great resource here:
https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

SwiftUI: How to switch to a new navigation stack with NavigationViews

I am currently using SwiftUI Beta 5. I have a workflow which involves navigating through a series of views. The last view involves an operation which populates a load of data into the app and ends that particular workflow.
Once the data has been downloaded, the user should be able to start new workflow(s). I would like to "forget" about the old NavigationView, since there is no use in going back through the navigation stack once the workflow has completed. Instead, I would like to then navigate to a "launch" view which effectively becomes the root of a new navigation view.
How can one view within a navigation stack be used to navigate to another view with a different NavigationView (and hence becomes a root for a new navigation stack) using SwiftUI NavigagationViews?
First, sorry I wanted to post a simple comment but not enough reputation point :(
I just updated my way to go back to the root you at stackoverflow.com/a/57513566/7786555
You actually gave me the idea with your comment for new way to go back to the root. Having a new root view. If you force the refresh of your struct view managing the root view then it will automatically do what you want. Here under is only going back to the root (without animation). You can adapt the example to change the root view (instead of using the same) to suit your need.
struct DetailViewB: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var fullDissmiss:Bool = false
var body: some View {
SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
VStack {
Text("This is Detail View B.")
Button(action: { self.presentationMode.wrappedValue.dismiss() } )
{ Text("Pop to Detail View A.") }
Button(action: {
self.fullDissmiss = true
} )
{ Text("Pop two levels to Master View with SGGoToRoot.") }
}
}
}
}
struct DetailViewA: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State var fullDissmiss:Bool = false
var body: some View {
SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
VStack {
Text("This is Detail View A.")
NavigationLink(destination: DetailViewB() )
{ Text("Push to Detail View B.") }
Button(action: { self.presentationMode.wrappedValue.dismiss() } )
{ Text("Pop one level to Master.") }
Button(action: { self.fullDissmiss = true } )
{ Text("Pop one level to Master with SGGoToRoot.") }
}
}
}
}
struct MasterView: View {
var body: some View {
VStack {
Text("This is Master View.")
NavigationLink(destination: DetailViewA() )
{ Text("Push to Detail View A.") }
}
}
}
struct ContentView: View {
var body: some View {
SGRootNavigationView{
MasterView()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
struct SGRootNavigationView<Content>: View where Content: View {
let cancellable = NotificationCenter.default.publisher(for: Notification.Name("SGGoToRoot"), object: nil)
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
#State var goToRoot:Bool = false
var body: some View {
return
Group{
if goToRoot == false{
NavigationView {
content()
}
}else{
NavigationView {
content()
}
}
}.onReceive(cancellable, perform: {_ in
DispatchQueue.main.async {
self.goToRoot.toggle()
}
})
}
}
struct SGNavigationChildsView<Content>: View where Content: View {
let notification = Notification(name: Notification.Name("SGGoToRoot"))
var fullDissmiss:Bool{
get{ return false }
set{ if newValue {self.goToRoot()} }
}
let content: () -> Content
init(fullDissmiss:Bool, #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.fullDissmiss = fullDissmiss
}
var body: some View {
return Group{
content()
}
}
func goToRoot(){
NotificationCenter.default.post(self.notification)
}
}
This is how we solved this question: We had a main/root/launch view, from which the user could tap a button to start a business workflow of some kind. This would open a sheet, which would display a modal pop-up view. (The width and height of sheets can be customised, in order to take up most/all of the screen.)
The sheet will have a NavigationView. This would allow the user to step through a series of views as part of their workflow. The "presented" flag gets passed as a binding from the main view to each navigated view.
When the user reaches the last view and taps a Submit/Done/Finish button to end that particular workflow, the "presented" binding can be set to false, which closes the modal pop-up, and returns the user back to the main view.