Skip to content

Instantly share code, notes, and snippets.

@masuidrive
Last active January 1, 2024 16:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save masuidrive/4cedd3977ecf21425ec0c80d834cfcc4 to your computer and use it in GitHub Desktop.
Save masuidrive/4cedd3977ecf21425ec0c80d834cfcc4 to your computer and use it in GitHub Desktop.
DelegateEnabledScrollView with iOS 17 or lower.

DelegateEnabledScrollView

DelegateEnabledScrollView is a custom SwiftUI view that facilitates the use of UIScrollViewDelegate's event handlers within a SwiftUI context. This enables you to easily implement custom behaviors based on UIScrollView's scroll events in SwiftUI.

Usage

  1. Content Provision: When initializing DelegateEnabledScrollView, you provide the content that will be displayed. This is done through a closure that takes an optional UIScrollView as an argument and returns a view.

  2. Event Handlers: You can chain methods corresponding to each UIScrollViewDelegate event (like onDidScroll, onWillEndDragging, etc.) to the view. These methods take closures as arguments, allowing you to set custom actions for each event.

  3. Layout Adjustments: Adjust the layout as needed using SwiftUI view modifiers like .frame.

import SwiftUI
@_spi(Advanced) import SwiftUIIntrospect
struct DelegateEnabledScrollView<Content: View>: View {
@State private var scrollViewDelegate: DelegateEnabledScrollViewDelegate?
@State private var scrollView: UIScrollView? = nil
let content: (UIScrollView?) -> Content
var onDidZoom: ((UIScrollView) -> Void)?
var onWillBeginDragging: ((UIScrollView) -> Void)?
var onDidScroll: ((UIScrollView) -> Void)?
var onDidEndDecelerating: ((UIScrollView) -> Void)?
var onWillBeginDecelerating: ((UIScrollView) -> Void)?
var onDidScrollToTop: ((UIScrollView) -> Void)?
var onShouldScrollToTop: ((UIScrollView) -> Bool)?
var onDidEndScrollingAnimation: ((UIScrollView) -> Void)?
var onWillBeginZooming: ((UIScrollView, UIView?) -> Void)?
var onDidChangeAdjustedContentInset: ((UIScrollView) -> Void)?
var onDidEndDragging:((UIScrollView, Bool) -> Void)?
var onDidEndZooming:((UIScrollView, UIView?, CGFloat) -> Void)?
var onWillEndDragging:((UIScrollView, CGPoint, UnsafeMutablePointer<CGPoint>) -> Void)?
init(@ViewBuilder content: @escaping (UIScrollView?) -> Content,
onDidZoom: ((UIScrollView) -> Void)? = nil,
onWillBeginDragging: ((UIScrollView) -> Void)? = nil,
onDidScroll: ((UIScrollView) -> Void)? = nil,
onDidEndDecelerating: ((UIScrollView) -> Void)? = nil,
onWillBeginDecelerating: ((UIScrollView) -> Void)? = nil,
onDidScrollToTop: ((UIScrollView) -> Void)? = nil,
onShouldScrollToTop: ((UIScrollView) -> Bool)? = nil,
onDidEndScrollingAnimation: ((UIScrollView) -> Void)? = nil,
onWillBeginZooming: ((UIScrollView, UIView?) -> Void)? = nil,
onDidChangeAdjustedContentInset: ((UIScrollView) -> Void)? = nil,
onDidEndDragging: ((UIScrollView, Bool) -> Void)? = nil,
onDidEndZooming: ((UIScrollView, UIView?, CGFloat) -> Void)? = nil,
onWillEndDragging: ((UIScrollView, CGPoint, UnsafeMutablePointer<CGPoint>) -> Void)? = nil
) {
self.content = content
self.onDidZoom = onDidZoom
self.onWillBeginDragging = onWillBeginDragging
self.onDidScroll = onDidScroll
self.onDidEndDecelerating = onDidEndDecelerating
self.onWillBeginDecelerating = onWillBeginDecelerating
self.onDidScrollToTop = onDidScrollToTop
self.onShouldScrollToTop = onShouldScrollToTop
self.onDidEndScrollingAnimation = onDidEndScrollingAnimation
self.onWillBeginZooming = onWillBeginZooming
self.onDidChangeAdjustedContentInset = onDidChangeAdjustedContentInset
self.onDidEndDragging = onDidEndDragging
self.onDidEndZooming = onDidEndZooming
self.onWillEndDragging = onWillEndDragging
}
var body: some View {
ScrollViewReader { proxy in
ScrollView {
content(self.scrollView)
}
.onAppear {
self.scrollViewDelegate = DelegateEnabledScrollViewDelegate(
onDidScroll: onDidScroll ?? { _ in },
onWillBeginDragging: onWillBeginDragging ?? { _ in },
onDidEndDecelerating: onDidEndDecelerating ?? { _ in },
onWillBeginDecelerating: onWillBeginDecelerating ?? { _ in },
onDidScrollToTop: onDidScrollToTop ?? { _ in },
onShouldScrollToTop: onShouldScrollToTop ?? { _ in true },
onDidEndScrollingAnimation: onDidEndScrollingAnimation ?? { _ in },
onWillBeginZooming: onWillBeginZooming ?? { _, _ in },
onDidChangeAdjustedContentInset: onDidChangeAdjustedContentInset ?? { _ in },
onDidEndDragging: onDidEndDragging ?? { _, _ in },
onDidEndZooming: onDidEndZooming ?? { _, _, _ in },
onWillEndDragging: onWillEndDragging ?? { _, _, _ in }
)
}
.introspect(.scrollView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { _scrollView in
self.scrollView = _scrollView
self.scrollView!.delegate = self.scrollViewDelegate
}
}
}
func onDidScroll(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidScroll = action
return view
}
func onWillBeginDragging(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onWillBeginDragging = action
return view
}
func onDidEndDecelerating(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidEndDecelerating = action
return view
}
func onWillBeginDecelerating(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onWillBeginDecelerating = action
return view
}
func onDidScrollToTop(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidScrollToTop = action
return view
}
func onShouldScrollToTop(_ action: @escaping (_ scrollView: UIScrollView) -> Bool) -> DelegateEnabledScrollView {
var view = self
view.onShouldScrollToTop = action
return view
}
func onDidEndScrollingAnimation(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidEndScrollingAnimation = action
return view
}
func onWillBeginZooming(_ action: @escaping (_ scrollView: UIScrollView, _ view: UIView?) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onWillBeginZooming = action
return view
}
func onDidChangeAdjustedContentInset(_ action: @escaping (_ scrollView: UIScrollView) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidChangeAdjustedContentInset = action
return view
}
func onDidEndDragging(_ action: @escaping (_ scrollView: UIScrollView, _ decelerate: Bool) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidEndDragging = action
return view
}
func onDidEndZooming(_ action: @escaping (_ scrollView: UIScrollView, _ view: UIView?, _ scale: CGFloat) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onDidEndZooming = action
return view
}
func onWillEndDragging(_ action: @escaping (_ scrollView: UIScrollView, _ velocity: CGPoint, _ targetContentOffset: UnsafeMutablePointer<CGPoint>) -> Void) -> DelegateEnabledScrollView {
var view = self
view.onWillEndDragging = action
return view
}
}
class DelegateEnabledScrollViewDelegate: NSObject, UIScrollViewDelegate {
var onDidScroll: ((UIScrollView) -> Void)
var onWillBeginDragging: ((UIScrollView) -> Void)
var onDidEndDecelerating: ((UIScrollView) -> Void)
var onWillBeginDecelerating: ((UIScrollView) -> Void)
var onDidScrollToTop: ((UIScrollView) -> Void)
var onShouldScrollToTop: ((UIScrollView) -> Bool)
var onDidEndScrollingAnimation: ((UIScrollView) -> Void)
var onWillBeginZooming: ((UIScrollView, UIView?) -> Void)
var onDidChangeAdjustedContentInset: ((UIScrollView) -> Void)
var onDidEndDragging: ((UIScrollView, Bool) -> Void)
var onDidEndZooming: ((UIScrollView, UIView?, CGFloat) -> Void)
var onWillEndDragging: ((UIScrollView, CGPoint, UnsafeMutablePointer<CGPoint>) -> Void)
init(
onDidScroll: @escaping ((UIScrollView) -> Void),
onWillBeginDragging: @escaping ((UIScrollView) -> Void),
onDidEndDecelerating: @escaping ((UIScrollView) -> Void),
onWillBeginDecelerating: @escaping ((UIScrollView) -> Void),
onDidScrollToTop: @escaping ((UIScrollView) -> Void),
onShouldScrollToTop: @escaping ((UIScrollView) -> Bool),
onDidEndScrollingAnimation: @escaping ((UIScrollView) -> Void),
onWillBeginZooming: @escaping ((UIScrollView, UIView?) -> Void),
onDidChangeAdjustedContentInset: @escaping ((UIScrollView) -> Void),
onDidEndDragging: @escaping ((UIScrollView, Bool) -> Void),
onDidEndZooming: @escaping ((UIScrollView, UIView?, CGFloat) -> Void),
onWillEndDragging: @escaping ((UIScrollView, CGPoint, UnsafeMutablePointer<CGPoint>) -> Void)
) {
self.onDidScroll = onDidScroll
self.onWillBeginDragging = onWillBeginDragging
self.onDidEndDecelerating = onDidEndDecelerating
self.onWillBeginDecelerating = onWillBeginDecelerating
self.onDidScrollToTop = onDidScrollToTop
self.onShouldScrollToTop = onShouldScrollToTop
self.onDidEndScrollingAnimation = onDidEndScrollingAnimation
self.onWillBeginZooming = onWillBeginZooming
self.onDidChangeAdjustedContentInset = onDidChangeAdjustedContentInset
self.onDidEndDragging = onDidEndDragging
self.onDidEndZooming = onDidEndZooming
self.onWillEndDragging = onWillEndDragging
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
onDidScroll(scrollView)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
onWillBeginDragging(scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
onDidEndDecelerating(scrollView)
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
onWillBeginDecelerating(scrollView)
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
onDidScrollToTop(scrollView)
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return onShouldScrollToTop(scrollView)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
onDidEndScrollingAnimation(scrollView)
}
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
onWillBeginZooming(scrollView, view)
}
func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
onDidChangeAdjustedContentInset(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
onDidEndDragging(scrollView, decelerate)
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
onDidEndZooming(scrollView, view, scale)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
onWillEndDragging(scrollView, velocity, targetContentOffset)
}
}
struct _Previewer: View {
@State private var lastEvent = ""
var body: some View {
GeometryReader { geometry in
VStack {
ScrollView {
Text(lastEvent)
.frame(maxWidth: .infinity, minHeight: geometry.size.height / 3, alignment: .leading)
.background(Color(.systemGray4))
}
DelegateEnabledScrollView { scrollView in
LazyVStack {
ForEach(0..<100, id: \.self) { index in
Text("Item \(index)")
.id(index)
}
}
}
.onWillEndDragging { scrollView, velocity, targetContentOffset in
lastEvent = "onWillEndDragging: \(velocity), \(targetContentOffset.pointee)"
}
.frame(height: geometry.size.height * 2 / 3)
}
}
}
}
#Preview {
_Previewer()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment