fermoya / SwiftUIPager

Native Pager in SwiftUI
MIT License
1.3k stars 172 forks source link

[BUG] ZoomableScrollView inside a Pager, can’t swipe on the last page #236

Closed iKK001 closed 2 years ago

iKK001 commented 3 years ago

Not sure if it is your library, however I do have an issue with swiping a custom ZoomableScrollView since updating to iOS 15.0.1.

I explained everyone better here

Did you experience something similar when swiping all the way to the last page of any SwiftUIPager View ?

Thank you for any support on this.

My ZoomableScrollView Code looks like this:

import SwiftUI

struct ZoomableScrollView<Content: View>: UIViewRepresentable {

    @Binding var didZoom: Bool

    private var content: Content

    init(didZoom: Binding<Bool>, @ViewBuilder content: () -> Content) {
        _didZoom = didZoom
        self.content = content()        
    }

    func makeUIView(context: Context) -> UIScrollView {

        // set up the UIScrollView
        let scrollView = UIScrollView()
        scrollView.delegate = context.coordinator  // for viewForZooming(in:)
        scrollView.maximumZoomScale = 20
        scrollView.minimumZoomScale = 1
        scrollView.bouncesZoom = true

        // create a UIHostingController to hold our SwiftUI content
        let hostedView = context.coordinator.hostingController.view!
        hostedView.translatesAutoresizingMaskIntoConstraints = true
        hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hostedView.frame = scrollView.bounds
        hostedView.backgroundColor = .black
        scrollView.addSubview(hostedView)

        return scrollView
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(hostingController: UIHostingController(rootView: self.content), didZoom: $didZoom)
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // update the hosting controller's SwiftUI content
        context.coordinator.hostingController.rootView = self.content
        assert(context.coordinator.hostingController.view.superview == uiView)
    }

    // MARK: - Coordinator

    class Coordinator: NSObject, UIScrollViewDelegate {

        var hostingController: UIHostingController<Content>
        @Binding var didZoom: Bool

        init(hostingController: UIHostingController<Content>, didZoom: Binding<Bool>) {
            self.hostingController = hostingController
            _didZoom = didZoom
        }

        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return hostingController.view
        }

        func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
            didZoom = !(scrollView.zoomScale == scrollView.minimumZoomScale)
        }
    }
}
JCNrick commented 3 years ago

Got the same problem and I also think it must be a bug at Apple with the UIViewRepresentable.

iKK001 commented 3 years ago

I completely switched to Apples native possiblity to achieve a Pager (and no longer use SwiftUIPager).

The reason is that Apple somehow improved their API and now it is at least possible to change the currentIndex. (this was'nt the case on earlier versions of the TabView-Pager)

My implementation looks like this:

struct Post: Identifiable, Hashable {
    var id = UUID().uuidString
    var isVideo: Bool
    var mediaURL: URL
}

TabView(selection: $currentPost) {

    ForEach(posts) { post in

        GeometryReader { proxy in

            let size = proxy.size

            if post.isVideo {
                CustomVideoView(url: .constant(post.mediaURL), controlsVisible: $controlsVisible, isPortrait: $isPortrait, playerMuted: $playerMuted, runningIndex: .constant(currentIdx))
            } else {
                ZoomableScrollView(didZoom: $didZoom) {
                    Image(uiImage: UIImage(contentsOfFile: post.mediaURL.path)!)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: size.width, height: size.height)
                }
                .id(resetZoomByID)
                .opacity(fadeOpacity)
                .gesture(singleGesture)
                .gesture(dragDownFadeGesture)
                .onChange(of: isPortrait) { _ in
                    resetZoomByID = UUID() // make sure the zoom gets reset after rotation
                }
            }
        }
        .tag(post.id)
        .ignoresSafeArea()
    }
}
.tabViewStyle(.page(indexDisplayMode: .always))
.transition(.move(edge: .bottom))

Once you have your full mediaList, you can always fill your Post-Array with something like this 👍

fillAllPostsInRAM() currentPost = posts[idx].id

private func fillAllPostsInRAM() {
    posts.removeAll()
    for path in finalMediaList.paths {
        let url = URL(fileURLWithPath: path)
        if url.containsImage {
            posts.append(Post(isVideo: false, mediaURL: url))
        } else if url.containsVideo {
            posts.append(Post(isVideo: true, mediaURL: url))
        }
    }
}

The ZoomableScrollView still looks like this:

struct ZoomableScrollView<Content: View>: UIViewRepresentable {

    @Binding var didZoom: Bool

    private var content: Content

    init(didZoom: Binding<Bool>, @ViewBuilder content: () -> Content) {
        _didZoom = didZoom
        self.content = content()        
    }

    func makeUIView(context: Context) -> UIScrollView {

        // set up the UIScrollView
        let scrollView = UIScrollView()
        scrollView.delegate = context.coordinator  // for viewForZooming(in:)
        scrollView.maximumZoomScale = 20
        scrollView.minimumZoomScale = 1
        scrollView.bouncesZoom = true

        // create a UIHostingController to hold our SwiftUI content
        let hostedView = context.coordinator.hostingController.view!
        hostedView.translatesAutoresizingMaskIntoConstraints = true
        hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hostedView.frame = scrollView.bounds
        hostedView.backgroundColor = .black
        scrollView.addSubview(hostedView)

        return scrollView
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(hostingController: UIHostingController(rootView: self.content), didZoom: $didZoom)
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // update the hosting controller's SwiftUI content
        context.coordinator.hostingController.rootView = self.content
        assert(context.coordinator.hostingController.view.superview == uiView)
    }

    // MARK: - Coordinator

    class Coordinator: NSObject, UIScrollViewDelegate {

        var hostingController: UIHostingController<Content>
        @Binding var didZoom: Bool

        init(hostingController: UIHostingController<Content>, didZoom: Binding<Bool>) {
            self.hostingController = hostingController
            _didZoom = didZoom
        }

        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return hostingController.view
        }

        func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
            didZoom = !(scrollView.zoomScale == scrollView.minimumZoomScale)
        }
    }
}
fermoya commented 3 years ago

@iKK001 can you share how you’re embedding ZoomableScrollView inside a Pager please?

fermoya commented 2 years ago

Seems like a SwiftUI issue