fermoya / SwiftUIPager

Native Pager in SwiftUI
MIT License
1.27k stars 166 forks source link

Reusability breaks view updates #301

Open Samigos opened 1 year ago

Samigos commented 1 year ago

Hi everyone! I've recently started using this pager (2.5.0) on a production app and I've been facing a recycling issue I’ve not been able to solve.

Each page has 1 post, that fills the whole screen. The first batch of posts is fetched and displayed with no issues. After that, I dynamically add new posts at the top of the list; 1 post at a time. That’s when the issue arises! It seems that the new post view copies the state of the next and shows a subview that shouldn’t show.

Here's the pager:

Pager(page: viewModel.feedPage,
                      data: viewModel.feedItems,
                      id: \.self,
                      content: { index in
                        if index < viewModel.posts.count {
                            PostView(post: viewModel.posts[index])
                        } else {
                            Text("Loading...")
                                    .foregroundColor(.white)
                                    .font(.default(size: 17))
                        }
                })
                .vertical()
                .sensitivity(.custom(0.1))
                .onPageWillChange { index in
                    viewModel.willChangeVerticalPage(index: index)
                }
struct PostView: View {
    @StateObject private var viewModel = ViewModel()
    let post: Post

    var body: some View {
        VStack {
            SendFriendRequestView(userId: post.user.id) // the subview inside UserViews is the issue
            PostImageView(imageURL: post.imageURL, text: post.text)
        }
        .onAppear {
            viewModel.setUp(post: post)
        }
        .environmentObject(viewModel)
    }
}
fileprivate struct SendFriendRequestView: View {
    @EnvironmentObject private var viewModel: PostView.ViewModel
    let userId: String

    var body: some View {
        Button {
            viewModel.sendFriendRequest(userId: userId)
        } label: {
            HStack {
                switch viewModel.sendFriendRequestViewState {
                case .send: sendView
                case .loading: loadingView
                case .waiting: waitingView
                case .hidden: EmptyView()
                }
            }
        }
        .disabled(viewModel.sendFriendRequestViewState != .send)
    }

    var sendView: some View {
        HStack {
            Image(systemName: "person.2.fill")
                .resizable()
                .scaledToFit()
                .foregroundColor(.white)
                .frame(width: 10, height: 10)

            Text("Add+")
                .foregroundColor(.white)
                .font(.default(size: 13))
        }
        .padding(8)
        .background(
            Capsule().fill(.white.opacity(0.2))
        )
    }

    var loadingView: some View {
        ProgressView()
            .tint(.white)
            .padding(8)
            .background(
                Capsule().fill(.white.opacity(0.2))
            )
    }

    var waitingView: some View {
        HStack {
            Text("Sent")
                .foregroundColor(.white)
                .font(.default(size: 13))

            Image(systemName: "checkmark")
                .resizable()
                .scaledToFit()
                .foregroundColor(.white)
                .frame(width: 10, height: 10)
        }
        .padding(8)
        .background(
            Capsule().fill(.white.opacity(0.2))
        )
    }
}

And here are the involved pieces of the view model:

    class ViewModel: ObservableObject {
        @Published private(set) var sendFriendRequestViewState = SendFriendRequestViewState.hidden

        func setUp(post: Post) {
            self.post = post
            determineSendFriendRequestViewState()
        }

        func determineSendFriendRequestViewState() {
            func isStateSend() -> Bool {
                post.user.isCurrentUser == false &&
                currentUserService.user?.friendIds.contains(post.user.id) == false &&
                currentUserService.user?.sentFriendRequestUserIds.contains(post.user.id) == false
            }

            func isStateWaiting() -> Bool {
                currentUserService.user?.friendIds.contains(post.user.id) == false &&
                currentUserService.user?.sentFriendRequestUserIds.contains(post.user.id) == true
            }

            if isStateSend() {
                sendFriendRequestViewState = .send
            } else if isStateWaiting() {
                sendFriendRequestViewState = .waiting
            } else {
                sendFriendRequestViewState = .hidden
            }
        }
    }

I realized that if I add .contentLoadingPolicy(.lazy(recyclingRatio: 0)) fixes my issue, but I don’t think it’s a good practice. Any ideas?