How to make a scroll view of 9 images in a forEach loop open on image 6 if image 6 is clicked on from a grid?

71 Views Asked by At

I have a grid of images in my app which will open a scroll view of the images if one is clicked. But no matter what image I click, the scroll view will always open on image 1 with 2,3,4,5,6,7,8,9 below it. I want to make it so if a user clicks on image 6 it will open the scroll view on image 6 with images 1,2,3,4,5 above it and 7,8,9 below it. Here is my code:

LazyVGrid(columns: threeColumnGrid, alignment: .center) {
    ForEach(viewModel.posts) { post in
        NavigationLink(destination: ScrollPostView(user: user, postids: viewModel.posts, selectedpostid: post.id)){
            KFImage(URL(string: post.imageurl))
                .resizable()
                .aspectRatio(1, contentMode: .fit)
                .cornerRadius(15)
        }
        .overlay(RoundedRectangle(cornerRadius: 14)
            .stroke(Color.black, lineWidth: 2))
     }
}
// scroll view

    @StateObject var viewModel: PostGridViewModel
    let postids: [Post]
    let selectedpostid: Post.ID
    @State private var scrollPosition: Post.ID?

    
    let user: User

    init(user: User, postids: [Post], selectedpostid: Post.ID ) {
        self._viewModel = StateObject(wrappedValue: PostGridViewModel(user: user))
        self.user = user
        self.postids = postids
        self.selectedpostid = selectedpostid
    }


    var body: some View {
        ZStack{
            Color(red: 0.1608, green: 0.4078, blue: 0.3725)
                .ignoresSafeArea()
            
            VStack(spacing: 2){
                ScrollView{
                    
                    VStack{                            
                        ForEach(viewModel.posts) {post in
                            posts_cell(post: post, user: user)
                        }
                        .padding(.horizontal)
                    }
                    .padding(.top, 20)
                    .padding(.bottom,18)
                    .scrollTargetLayout()
                }
                .scrollPosition(id: $scrollPosition, anchor: .center)
                .onAppear {
                    scrollPosition = selectedpostid
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
                .padding(.top, 30)
                .padding(.bottom, 52)
            }
        }
    }
}

//viewModel
class PostGridViewModel: ObservableObject{
    private let user: User
    @Published var posts = [Post]()
    
    
    init(user: User) {
        self.user = user
        
        Task { try await fetchUserPosts() }
    }
    
    @MainActor
    func fetchUserPosts() async throws {
        self.posts = try await PostService.FetchUserPosts(uid: user.id)
        
        for i in 0 ..< posts.count {
            posts[i].user = self.user
            
        }
    }
}

//post service
static func FetchUserPosts(uid: String) async throws -> [Post] {
    let snapshot = try await postsCollection.whereField("ownerUid", isEqualTo: uid).order(by: "timestamp", descending: true).getDocuments()
    return try snapshot.documents.compactMap({ try $0.data(as: Post.self) })
}

struct Post: Identifiable, Hashable, Codable {

    let id: String
    let ownerUid: String
    let caption: String
    var likes: Int
    var postlikes: Array<String>
    var postsaves: Array<String>
    let imageurl: String
    let timestamp: Timestamp
    var user: User?
    
    var didLike: Bool? = false
    var didSave: Bool? = false
}
1

There are 1 best solutions below

6
Benzy Neez On BEST ANSWER

You can scroll to a particular view in a ScrollView by using its id. This can either be done using a ScrollViewReader (as suggested in a comment) or by using a .scrollPosition modifier on the ScrollView (requires iOS 17).

Here is an example of how it can be done using .scrollPosition.

  • The images used here are just system images (symbols). The symbol name is used as its id.
  • The images in the scrolled view all have different heights. This makes it difficult for a LazyVStack to predict the scroll distance correctly, so a VStack is used instead. But if your images all have the same height then you could try using a LazyVStack.
  • Set the target to scroll to in .onAppear.
  • If you use .center as the anchor for scrolling then images in the middle of the list should be centered vertically in the display.
struct ContentView: View {

    private let imageNames = ["hare", "tortoise", "dog", "cat", "lizard", "bird", "ant", "ladybug", "fossil.shell", "fish"]
    private let threeColumnGrid: [GridItem] = [.init(), .init(), .init()]

    var body: some View {
        NavigationStack {
            LazyVGrid(columns: threeColumnGrid, alignment: .center, spacing: 20) {
                ForEach(imageNames, id: \.self) { imageName in
                    NavigationLink(destination: ScrollPostView(imageNames: imageNames, selectedName: imageName)){
                        Image(systemName: imageName)
                            .resizable()
                            .scaledToFit()
                            .padding()
                            .frame(maxHeight: 100)
                            .background(.yellow)
                            .clipShape(RoundedRectangle(cornerRadius: 15))
                    }
                    .overlay(
                        RoundedRectangle(cornerRadius: 15)
                            .stroke(Color.black, lineWidth: 2)
                    )
                }
            }
        }
    }
}

struct ScrollPostView: View {
    let imageNames: [String]
    let selectedName: String
    @State private var scrollPosition: String?

    var body: some View {
        ScrollView {
            VStack {
                ForEach(imageNames, id: \.self) { imageName in
                    Image(systemName: imageName)
                        .resizable()
                        .scaledToFit()
                        .padding()
                        .background(selectedName == imageName ? .orange : .yellow)
                        .clipShape(RoundedRectangle(cornerRadius: 15))
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition(id: $scrollPosition, anchor: .center)
        .onAppear {
            scrollPosition = selectedName
        }
    }
}

Animation


EDIT Thanks for updating your question with more code. It looks like everything is correctly in place, but I am wondering if the posts are being loaded after the view has appeared? This might be why the scroll position isn't working.

You could try setting scrollPosition in an .onAppear handler on the post itself (and remove the .onAppear that you had on the ScrollView). You could also add a print to help debugging:

ForEach(viewModel.posts) {post in
    posts_cell(post: post, user: user)
        .onAppear {
            if post.id == selectedpostid {
                print("setting scroll position to id \(post.id)")
                scrollPosition = selectedpostid
            }
        }
}