24

Another SwiftUI struggle!

I have a view that contains a list. When user taps on a row, I want to first save the selected item in my VM then push another view. The only way I can think of to solve that issue is to first save the selected row and have another button to push the next view. It seems impossible to do this with only one tap.

Anyone have a clue?

Here's the code by the way:

struct AnotherView : View {
    @State var viewModel = AnotherViewModel()

    var body: some View {
        NavigationView {
            VStack {
                    List(viewModel.items.identified(by: \.id)) { item in
                        NavigationLink(destination: DestinationView()) {
                            Text(item)
                        }
                        // Before the new view is open, I want to save the selected item in my VM, which will write to a global store.
                        self.viewModel.selectedItem = item
                    }
                }
        }
    }
}

Thank you!

7
  • Have you tried setting up item in your DestinationView initializer? While it won't immediately update anotherViewModel, it can in the initializer. Also, If you have a view model, please look into using @BindableObject instead of @State, and then either @ObjectBinding or @EnvironmentObject. The sooner the better, it'll give you a better route to work through these things. :-) Otherwise,you'll find yourself with... spaghetti state! Commented Jul 16, 2019 at 14:19
  • @dfd I don't want to go that route. Basically, I have a global object that every VM (1 VM per view) will write into. Those VMs (which are @BindableObject btw) will write into this global store. At the end of the flow, I will use that store to gather all data. That way, all views are independent and I don't need to pass an object over and over. Commented Jul 16, 2019 at 15:00
  • 2
    Sounds good, but some may say that you've just defined what an @EnvironmentObject is. Commented Jul 16, 2019 at 15:25
  • @dfd That's a good point. However, it does not solve my initial problem. As of right now, what is the good way of storing data across a NavigationFlow without having to pass the selected object to the next view and storing it in the global store? The real issue here is that List infer that the only thing we need to do when we select an item is open a view with more detail about that item. Commented Jul 16, 2019 at 19:07
  • I'm probably being simplistic, but consider - (1) everything in SwiftUI and/or Combine is "by reference" not "by value", and (2) a List based on a dynamic array requires a struct that conforms to Identifiable. You don't need to "pass the object and then update the model", you just need to "pass a pointer to the object id already in the model". The memory footprint is the same. (Part of what I call "the paradigm change" - wish I could copyright that - from UIKit.) Commented Jul 16, 2019 at 20:55

4 Answers 4

29

Alright, I found a not too shady solution. I used this article https://ryanashcraft.me/swiftui-programmatic-navigation shout out to him! Instead of using a NavigationLink button, I use a regular button, save the selected item when the user tap then use NavigationDestinationLink to push the new view as is self.link.presented?.value = true.

Works like a charm as of beta 3! I'll update my post if something change in the next betas.

Here's how it could look like:

struct AnotherView : View {
    private let link: NavigationDestinationLink<AnotherView2>
    @State var viewModel = AnotherViewModel()

    init() {
        self.link = NavigationDestinationLink(
            AnotherView2(),
            isDetail: true
        )
    }

    var body: some View {
        NavigationView {
            VStack {
                List(viewModel.items.identified(by: \.id)) { item in
                    Button(action: {
                        // Save the object into a global store to be used later on
                        self.viewModel.selectedItem = item
                        // Present new view
                        self.link.presented?.value = true
                    }) {
                        Text(value: item)
                    }
                }
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

If you could possibly update this now that NavigationDestinationLink has been deprecated in the first SwiftUI release, that would be tremendously helpful.
7

You can add simple TapGesture

                NavigationLink(destination: ContentView() ) {
                    Text("Row")
                        .gesture(TapGesture()
                            .onEnded({ _ in
                                //your action here
                    }))
                }

7 Comments

You can just use .onTapGesture { /* actions here */ } instead of .gesture(TapGesture()).onEnded({ ... })
Using this approach, isn't the "action" only executed if the Text itself is tapped and not the entire row? That is, if the tap is outside of the Text's bounds, I believe the navigation will occur but the "action" will not execute.
Thanks. I think if you use Button(action: with your Text embedded in the button, it does take a tap from the whole row. Actually it was a lot of trouble to get both the action AND destination to work in unison, but was possible by setting a @State Boolean in the action, then testing for that Boolean in a NavigationLink(destination:isActive:) outside of the List. It was easier with what Apple deprecated!
Hah agreed, after some experiment with this, I made this designer other way, I just make some action at .onAppearAction ( detailed VC). Btw you can see my implementation here github.com/titkov5/OhTronald at file TagsList -> QuotesList.onAppear()
@IvanTitkov that doesnt work for a NavigationView with a List of items
|
0

This can also be accomplished by using a Publisher and the OnReceive hook in any View inheritor.

3 Comments

can onReceive be used with an @State?
@malhal the entire view is invalidated when a @State changes, not sure what kind of publisher you would be expecting in that case. I suggest checking out: [stackoverflow.com/a/56462423/1086306]
When an @State changes I wanted to change another @State if the new value is not nil and is used on another view. I've achieved it by using an ObservableObject implementing class and checking inside the @Published didSet to set another @Published but was wondering if there was a simpler way to hook into an @State e.g. like if we had didSetValue.
-6

Swift 5.1

In case you would like to apply it to a static List. You may try this.

NavigationView{
        List{
            NavigationLink("YourView1Description", destination: YourView1())

            NavigationLink("YourView2Description", destination: YourView2())

           NavigationLink("YourView3Description", destination: YourView3())

           NavigationLink("YourView4Description", destination: YourView4())

        }
        .navigationBarTitle(Text("Details"))
    }

1 Comment

this doesn't appear to answer the question!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.