Setting a StateObject value from child view causes NavigationView to pop all views
In mocking up a Store where dispatches mutates a root @Published state that directly fires ObjectWillChange, I can replicate your popping behavior. If I separate state mutation from ObjectWillChange by using CurrentValueSubject or nesting an ObservableObject, NavigationLink performs exactly as desired.
I'd suggest decoupling a global root state firing from mandatorily marking all views as dirty. That way you can use a diffing mechanism to trigger that yourself.
That said, maybe there's a workaround re: @loremipsum comment. I am unfamiliar with that as I work with NavigationView in macOS multi-pane only, where I find it very reliable.
Edit: Example
Given your Q in the comment, here is a rough demo that keeps your root state on an @Published property.
That lets you pass your old store into the environment and use it as normal for views that you're OK with diffing for every possible state mutation.
Mock store & container
class Store: ObservableObject {
@Published var state = RootState()
}
struct RootState {
var nav: NavTag = .page1
var other: Int = 1
}
class Container: ObservableObject {
let store = Store()
func makeNavVM() -> NavVM {
NavVM(store)
}
}
struct RootView: View {
@StateObject private var container = Container()
var body: some View {
ContentView(vm: container.makeNavVM())
.environmentObject(container.store)
.environmentObject(container)
}
}
Navigation
class NavVM: ObservableObject {
init(_ root: Store) {
self.root = root
root.$state
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
guard let self = self,
state.nav != self.tag else { return }
self.tag = state.nav
}
.store(in: &subs)
}
@Published private(set) var tag: NavTag = .page1
private var subs = Set<AnyCancellable>()
private weak var root: Store?
func navigate(to tag: NavTag?) {
guard let tag = tag else { return }
root?.state.nav = tag
}
}
enum NavTag: String {
case page1
case page2
var color: Color {
switch self {
case .page1: return .pink
case .page2: return .yellow
}
}
var label: String {
switch self {
case .page1: return "Page 1"
case .page2: return "Page 2"
}
}
}
Usage
struct ContentView: View {
@StateObject var vm: NavVM
var body: some View {
NavigationView {
VStack(alignment: .center) {
Text("Your navigation view")
NavigationButton(tag: .page1)
NavigationButton(tag: .page2)
}
}
.environmentObject(vm)
}
}
struct Child: View {
@EnvironmentObject var store: Store
var color: Color
var body: some View {
VStack {
Button { store.state.other += 1 } label: {
ZStack {
color
Text("Press to mutate state \(store.state.other)")
}
}
NavigationLink("Go to page not in NavTag or state", destination: Child(color: .green))
}
}
}
/// On its own the Link-in-own-view-encapsulation
/// worked on first screen w/o navigation,
/// but once navigated the view hierarchy will pop
/// unless you stop state updates at the top of the hierarchy
struct NavigationButton: View {
@EnvironmentObject var navVM: NavVM
var tag: NavTag
var neveractuallynil: Binding<NavTag?> {
Binding<NavTag?> { navVM.tag }
set: { navVM.navigate(to: $0) }
}
var body: some View {
NavigationLink(destination: Child(color: tag.color),
tag: tag,
selection: neveractuallynil )
{ Text(tag.label) }
}
}
The demo uses a view model object to partition updates, but you can use a Combine pipeline in a view, too. You may want to do other variations on this general theme, from the publisher you use for root state and a UI factory object.
SwiftUI List data update from destination view causes unexpected behaviour
You're controlling lifetimes and objects in a way that's not a pattern in this UI framework. The VieModel is not going to magically republish a singleton value type; it's going to instantiate with that value and then mutate its state without ever checking in with the shared instance again, unless it is rebuilt.
class Store: ObservableObject {
static var shared: Store = .init()
struct ContentView: View {
@ObservedObject var store = Store.shared
struct ItemDetailView: View {
@ObservedObject var viewModel = ItemDetailViewModel()
class ViewModel: ObservableObject {
@Published var items: [String] = Store.shared.items
There are lots of potential viable patterns. For example:
Create
class RootStore: ObservableObject
and house it as an @StateObject in App. The lifetime of a StateObject is the lifetime of that view hierarchy's lifetime. You can expose it (a) directly to child views by .environmentObject(store) or (b) create a container object, such as Factory that vends ViewModels and pass that via the environment without exposing the store to views, or (c) do both a and b.If you reference the store in another class, hold a weak reference
weak var store: Store?
. If you keep state in that RootStore on @Published, then you can subscribe directly to that publisher or to the RootStore's own ObjectWillChangePublisher. You could also use other Combine publishers like CurrentValueSubject to achieve the same as @Published.
For code examples, see
Setting a StateObject value from child view causes NavigationView to pop all views
SwiftUI @Published Property Being Updated In DetailView
SwiftUI - ObservableObject created multiple times
Latest SwiftUI updates have brought solution to this problem. (iOS 14 onwards)
@StateObject
is what we should use instead of @ObservedObject
, but only where that object is created and not everywhere in the sub-views where we are passing the same object.
For eg-
class User: ObservableObject {
var name = "mohit"
}
struct ContentView: View {
@StateObject var user = User()
var body: some View {
VStack {
Text("name: \(user.name)")
NameCount(user: self.user)
}
}
}
struct NameCount: View {
@ObservedObject var user
var body: some View {
Text("count: \(user.name.count)")
}
}
In the above example, only the view responsible (ContentView) for creating that object is annotating the User
object with @StateObject
and all other views (NameCount) that share the object is using @ObservedObject
.
By this approach whenever your parent view(ContentView) is re-created, the User
object will not be re-created and it will persist its @State, while your child views just observing
to the same User
object doesn't have to care about its re-creation.
How can you switch views without having a navigationView or an popover?
If you just want to hide the navigation bar it's easy:
import SwiftUI
struct View2: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("POP")
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("PUSH")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you, instead, want to get rid of the NavigationView
and NavigationLink
views you have to implement your own custom navigation. It's a little more complicated. The following is just a simple example of a push/pop transition between two views.
import SwiftUI
struct View2: View {
@Binding var push: Bool
var body: some View {
ZStack {
Color.yellow
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("POP")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct View1: View {
@Binding var push: Bool
var body: some View {
ZStack {
Color.green
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("PUSH")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView: View {
@State private var push = false
var body: some View {
ZStack {
if !push {
View1(push: $push)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
}
if push {
View2(push: $push)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing)))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
How can I pop to the Root view using SwiftUI?
Setting the view modifier isDetailLink
to false
on a NavigationLink
is the key to getting pop-to-root to work. isDetailLink
is true
by default and is adaptive to the containing View. On iPad landscape for example, a Split view is separated and isDetailLink
ensures the destination view will be shown on the right-hand side. Setting isDetailLink
to false
consequently means that the destination view will always be pushed onto the navigation stack; thus can always be popped off.
Along with setting isDetailLink
to false
on NavigationLink
, pass the isActive
binding to each subsequent destination view. At last when you want to pop to the root view, set the value to false
and it will automatically pop everything off:
import SwiftUI
struct ContentView: View {
@State var isActive : Bool = false
var body: some View {
NavigationView {
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive
) {
Text("Hello, World!")
}
.isDetailLink(false)
.navigationBarTitle("Root")
}
}
}
struct ContentView2: View {
@Binding var rootIsActive : Bool
var body: some View {
NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
Text("Hello, World #2!")
}
.isDetailLink(false)
.navigationBarTitle("Two")
}
}
struct ContentView3: View {
@Binding var shouldPopToRootView : Bool
var body: some View {
VStack {
Text("Hello, World #3!")
Button (action: { self.shouldPopToRootView = false } ){
Text("Pop to root")
}
}.navigationBarTitle("Three")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Swift UI how to retrieve state from child view
In SwiftUI, you want a single source of truth for your data, and then all views read and write to that data.
@State
is a bit like private, in that it creates a new source of truth that can only be used by that view (and its subviews).
What you want is to have the data in the Parent (ContentView in this case) and then pass the data that you want the children to update to them.
You can do this using @Binding
:
public struct ChildPickerView : View {
var selectedText: Binding<String>
var body: some View { }
}
And then passing that binding from the parent using $
:
public struct ContentView : View {
@State var selectedText = ""
var body: some View {
ChildViewItem($selectedText)
}
}
You can also use EnvironmentObject
to avoid passing data from view to view.
SwiftUI Navigation immediately dismisses views? iOS 15
No need for .onTapGesture
or .background
...
It should look something like this:
struct ProfileView: View {
var body: some View {
NavigationView {
List {
Text("other content ...")
//other content {
NavigationLink("Show settings menu") {
Settings()
}
}
.listStyle(PlainListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct Settings: View {
var body: some View {
VStack(spacing: 30) {
NavigationLink("Account Menu") {
AccountTab()
}
NavigationLink("Security Menu") {
// SecuritySettings()
}
NavigationLink("Notification Menu") {
// UserNotificationsView()
}
}
}
}
struct AccountTab: View {
var body: some View {
VStack {
NavigationLink("Email") {
// EditEmail()
Text("Edit Mail")
}
NavigationLink("Phone") {
// EditGender()
Text("Edit Phone")
}
NavigationLink("Gender") {
// EditPhone()
Text("Edit Gender")
}
}
}
}
Related Topics
Swift: Memory Not Clearing When I Segue to Another View Controller, Recieving Memory Warning
Uisearchbar Out of Screen Bounds When Navigation Bar Translucent = False
My Uiviews Muck-Up When I Combine Uipangesturerecognizer and Autolayout
Customise Uitabbar Height in Xcode11/Ios13 or 13.1
Cropping Avasset Video with Avfoundation Not Working iOS 8
Exc_Bad_Access in Parent Class Init() with Xcode 10.2
Fft Calculating Incorrectly - Swift
Swift Select All Photos from Specific Photos Album
Rotate a Imageview Around a Pivot Point in iOS
How to Programmatically Check and Open an Existing App in iOS 8
How to Create Gmsmarker with Combined Images in Swift
Drawing an Infinite Grid in iOS