Skip to content

Instantly share code, notes, and snippets.

@aronbalog
Created March 17, 2020 10:57
Show Gist options
  • Save aronbalog/2fade2ae3f9fa61dff0854aa661d20a6 to your computer and use it in GitHub Desktop.
Save aronbalog/2fade2ae3f9fa61dff0854aa661d20a6 to your computer and use it in GitHub Desktop.
UIScrollView Wrapper for SwiftUI
import SwiftUI
public struct ScrollViewWrapper<Content: View>: UIViewRepresentable {
@Binding var contentOffset: CGPoint
let content: () -> Content
public init(contentOffset: Binding<CGPoint>, @ViewBuilder _ content: @escaping () -> Content) {
self._contentOffset = contentOffset
self.content = content
}
public func makeUIView(context: UIViewRepresentableContext<ScrollViewWrapper>) -> UIScrollView {
let view = UIScrollView()
view.delegate = context.coordinator
let controller = UIHostingController(rootView: content())
controller.view.sizeToFit()
view.addSubview(controller.view)
view.contentSize = controller.view.bounds.size
return view
}
public func updateUIView(_ uiView: UIScrollView, context: UIViewRepresentableContext<ScrollViewWrapper>) {
uiView.contentOffset = self.contentOffset
}
public func makeCoordinator() -> Coordinator {
Coordinator(contentOffset: self._contentOffset)
}
public class Coordinator: NSObject, UIScrollViewDelegate {
let contentOffset: Binding<CGPoint>
init(contentOffset: Binding<CGPoint>) {
self.contentOffset = contentOffset
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
contentOffset.wrappedValue = scrollView.contentOffset
}
}
}
@squinney
Copy link

This was helpful for me, though I had an issue with it updating when content was changed. I added a few lines so that UIHostingController is properly updated on changes:

public struct ScrollViewWrapper<Content: View>: UIViewRepresentable {
    @Binding var contentOffset: CGPoint
    @Binding var scrollViewHeight: CGFloat
    @Binding var visibleHeight: CGFloat
    
    let content: () -> Content
    
    public init(
        contentOffset: Binding<CGPoint>,
        scrollViewHeight: Binding<CGFloat>,
        visibleHeight: Binding<CGFloat>,
        @ViewBuilder _ content: @escaping () -> Content) {
            self._contentOffset = contentOffset
            self._scrollViewHeight = scrollViewHeight
            self._visibleHeight = visibleHeight
            
            self.content = content
        }
    
    public func makeUIView(context: UIViewRepresentableContext<ScrollViewWrapper>) -> UIScrollView {
        let view = UIScrollView()
        view.delegate = context.coordinator
        
        let controller = UIHostingController(rootView: content())
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controller.view)
        
        NSLayoutConstraint.activate([
            controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            controller.view.topAnchor.constraint(equalTo: view.topAnchor),
            controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            controller.view.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
        
        context.coordinator.hostingController = controller
        
        return view
    }
    
    public func updateUIView(_ uiView: UIScrollView, context: UIViewRepresentableContext<ScrollViewWrapper>) {
        uiView.contentOffset = self.contentOffset
        
        if let hostingController = context.coordinator.hostingController {
            hostingController.rootView = content()
        }
        
        DispatchQueue.main.async {
            self.scrollViewHeight = uiView.contentSize.height
            self.visibleHeight = uiView.frame.size.height
            
            if let hostedView = uiView.subviews.first {
                hostedView.frame = CGRect(origin: .zero, size: uiView.contentSize)
            }
        }
    }
    
    public func makeCoordinator() -> Coordinator {
        Coordinator(contentOffset: self._contentOffset, scrollViewHeight: self._scrollViewHeight)
    }
    
    public class Coordinator: NSObject, UIScrollViewDelegate {
        let contentOffset: Binding<CGPoint>
        let scrollViewHeight: Binding<CGFloat>
        var hostingController: UIHostingController<Content>?
        
        init(contentOffset: Binding<CGPoint>, scrollViewHeight: Binding<CGFloat>) {
            self.contentOffset = contentOffset
            self.scrollViewHeight = scrollViewHeight
        }
        
        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            contentOffset.wrappedValue = scrollView.contentOffset
        }
    }
}

@EngOmarElsayed
Copy link

one more thing layoutIfNeeded must be called before returing the view because otherwise the view may freeze when updating the content in the scrollView

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment