3

I am trying to make a SwiftUI ScrollView scroll to a certain point in an abstracted view when a button is pressed in a view which is calling the abstracted view programmatically. Here is my code:

struct AbstractedView: View {
    @Namespace var view2ID

    var body: some View {
        ScrollView {
            VStack {
                View1()
                View2()
                .id(view2ID)
                View3()
            }
        }
    }
    func scrollToView2(_ proxy: ScrollViewProxy) {
        proxy.scrollTo(view2ID, anchor: .topTrailing)
    }
}

As you can see, when scrollToView2() is called (in a ScrollViewReader), the AbstractedView scrolls to view2ID. I am creating a number of AbstractedView's programmatically in a different View:

struct HigherView: View {
    var numAbstractedViewsToMake: Int

    var body: some View {
        VStack {
            HStack {
                ForEach (0..<numAbstractedViewsToMake, id: \.self) { _ in
                    AbstractedView()
                }
            }
            Text("button")
            .onTapGesture {
                /* call each AbstractedView.scrollToView2()
            }
        }
    }
}

If I stored these views in an array in a struct inside my HigherView with a ScrollViewReader for each AbstractedView would that work? I feel as though there has to be a nicer way to achieve this, I just have no clue how to do it. I am new to Swift so thank you for any help.

P.S. I have heard about UIKit but I don't know anything about it, is this the right time to be using that?

Randy
  • 87
  • 9
  • 2
    Trying to "call a function" on different `View` is somewhat antithetical to the principals of SwiftUI. If you need `AbstractedView` to react to some state change, store the state in the parent view (`HigherView`) and pass it down through props. – jnpdx Jul 20 '22 at 16:17
  • I tried to do that first, but I could not find a way to make the ```ScrollView``` call the ```scrollToView2()``` on state change. – Randy Jul 20 '22 at 16:19
  • I am aware of the ```.OnChange(of:)``` modifier but I couldn't figure out how to use it in this case – Randy Jul 20 '22 at 16:22
  • 3
    It will not work that way, scrollTo needs one ID to be scrolled to (per-event). See here for example https://stackoverflow.com/a/60855853/12299030. – Asperi Jul 20 '22 at 16:24

1 Answers1

0

Using the comments from @Asperi and @jnpdx, I was able to come up with a more powerful solution than I needed:

class ScrollToModel: ObservableObject {
    enum Action {
        case end
        case top
    }
    @Published var direction: Action? = nil
}

struct HigherView: View {
    @StateObject var vm = ScrollToModel()
    var numAbstractedViewsToMake: Int

    var body: some View {
        VStack {
            HStack {
                Button(action: { vm.direction = .top }) { // < here
                    Image(systemName: "arrow.up.to.line")
                      .padding(.horizontal)
                }
                Button(action: { vm.direction = .end }) { // << here
                    Image(systemName: "arrow.down.to.line")
                      .padding(.horizontal)
                }
            }
            Divider()
            HStack {
                ForEach(0..<numAbstractedViewsToMake, id: \.self) { _ in
                    ScrollToModelView(vm: vm)
                }
            }
        }
    }
}

struct AbstractedView: View {
    @ObservedObject var vm: ScrollToModel

    let items = (0..<200).map { $0 } // this is his demo
    var body: some View {
        VStack {
            
            ScrollViewReader { sp in
                ScrollView {
               
                    LazyVStack { // this bit can be changed accordingly
                        ForEach(items, id: \.self) { item in
                            VStack(alignment: .leading) {
                                Text("Item \(item)").id(item)
                                Divider()
                            }.frame(maxWidth: .infinity).padding(.horizontal)
                        }
                    }.onReceive(vm.$direction) { action in
                        guard !items.isEmpty else { return }
                        withAnimation {
                            switch action {
                                case .top:
                                    sp.scrollTo(items.first!, anchor: .top)
                                case .end:
                                    sp.scrollTo(items.last!, anchor: .bottom)
                                default:
                                    return
                            }
                        }
                    }
                }
            }
        }
    }
}

Thank you both!

Randy
  • 87
  • 9