23

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!

Benjamin Clanet
  • 1,076
  • 3
  • 10
  • 17
  • 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! –  Jul 16 '19 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. – Benjamin Clanet Jul 16 '19 at 15:00
  • 2
    Sounds good, but some may say that you've just defined what an `@EnvironmentObject` is. –  Jul 16 '19 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. – Benjamin Clanet Jul 16 '19 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`.) –  Jul 16 '19 at 20:55
  • @dfd I'd love to talk more about how I architectured things but not here as it is not really the point of the question. However, I found a satisfying solution to my problem if you want to read my answer :) – Benjamin Clanet Jul 16 '19 at 21:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/196570/discussion-between-benjamin-clanet-and-dfd). – Benjamin Clanet Jul 17 '19 at 12:17

4 Answers4

28

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)
                    }
                }
            }
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
Benjamin Clanet
  • 1,076
  • 3
  • 10
  • 17
  • 10
    If you could possibly update this now that NavigationDestinationLink has been deprecated in the first SwiftUI release, that would be tremendously helpful. – P. Ent Nov 05 '19 at 18:17
5

You can add simple TapGesture

                NavigationLink(destination: ContentView() ) {
                    Text("Row")
                        .gesture(TapGesture()
                            .onEnded({ _ in
                                //your action here
                    }))
                }
Ivan Titkov
  • 359
  • 3
  • 14
  • 8
    You can just use `.onTapGesture { /* actions here */ }` instead of `.gesture(TapGesture()).onEnded({ ... })` – Ben Dec 02 '19 at 10:20
  • 7
    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. – electromaggot Jan 19 '20 at 03:01
  • 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! – electromaggot Jan 20 '20 at 04:43
  • 1
    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 https://github.com/titkov5/OhTronald at file TagsList -> QuotesList.onAppear() – Ivan Titkov Jan 21 '20 at 05:27
  • @IvanTitkov that doesnt work for a NavigationView with a List of items – Learn2Code Mar 01 '20 at 02:56
  • @Learn2Code what exactly? – Ivan Titkov Mar 02 '20 at 11:52
  • @IvanTitkov putting a gesture when its in a NavigationView and part of a List. It works only when its one NavigationLink only. I think Apple should fix the NavigationView when its in a List to execute the gesture as well. – Learn2Code Mar 02 '20 at 13:45
0

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

Hasta Dhana
  • 4,699
  • 7
  • 17
  • 26
Mike W.
  • 69
  • 3
  • can onReceive be used with an @State? – malhal Jan 09 '20 at 01:25
  • @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: [https://stackoverflow.com/a/56462423/1086306] – Mike W. Jan 09 '20 at 04:11
  • 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. – malhal Jan 09 '20 at 12:57
-5

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"))
    }
William Tong
  • 479
  • 7
  • 14