Skip to content

Commit

Permalink
feat: add new scroll view extension to read content offset
Browse files Browse the repository at this point in the history
  • Loading branch information
gtokman committed May 2, 2021
1 parent 56a1db3 commit 7f0da42
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 0 deletions.
97 changes: 97 additions & 0 deletions Sources/ExtensionKit/SwiftUI/ScrollViewOffSetReader.swift
@@ -0,0 +1,97 @@
import SwiftUI
import UIKit

/// Hosting controller that updates the scroll view offset (x,y)
class ScrollViewOffSetReader<Content>: UIHostingController<Content> where Content: View {

var offset: Binding<CGFloat>
let isOffsetX: Bool
var showed = false
var sv: UIScrollView?

init(offset: Binding<CGFloat>, isOffsetX: Bool, rootView: Content) {
self.offset = offset
self.isOffsetX = isOffsetX
super.init(rootView: rootView)
}

@objc dynamic required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidAppear(_ animated: Bool) {
if showed {
return
}
showed = true

sv = findScrollView(in: view)

sv?.addObserver(self,
forKeyPath: #keyPath(UIScrollView.contentOffset),
options: [.old, .new],
context: nil)

scroll(to: offset.wrappedValue, animated: false)

super.viewDidAppear(animated)
}

func scroll(to position: CGFloat, animated: Bool = true) {
if let s = sv {
if position != (self.isOffsetX ? s.contentOffset.x : s.contentOffset.y) {
let offset = self.isOffsetX ? CGPoint(x: position, y: 0) : CGPoint(x: 0, y: position)
sv?.setContentOffset(offset, animated: animated)
}
}
}

override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey: Any]?,
context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(UIScrollView.contentOffset) {
if let s = self.sv {
DispatchQueue.main.async {
self.offset.wrappedValue = self.isOffsetX ? s.contentOffset.x : s.contentOffset.y
}
}
}
}

func findScrollView(in view: UIView?) -> UIScrollView? {
if view?.isKind(of: UIScrollView.self) ?? false {
return view as? UIScrollView
}

for subview in view?.subviews ?? [] {
if let sv = findScrollView(in: subview) {
return sv
}
}

return nil
}
}

struct ScrollViewOffSetReaderRepresentable<Content>: UIViewControllerRepresentable where Content: View {

var offset: Binding<CGFloat>
let isOffsetX: Bool
let content: Content

init(offset: Binding<CGFloat>, isOffsetX: Bool, @ViewBuilder content: @escaping () -> Content) {

self.offset = offset
self.isOffsetX = isOffsetX
self.content = content()
}

func makeUIViewController(context: Context) -> ScrollViewOffSetReader<Content> {
ScrollViewOffSetReader(offset: offset, isOffsetX: isOffsetX, rootView: content)
}

func updateUIViewController(_ uiViewController: ScrollViewOffSetReader<Content>, context: Context) {
uiViewController.scroll(to: offset.wrappedValue, animated: true)
}
}
14 changes: 14 additions & 0 deletions Sources/ExtensionKit/SwiftUI/View.swift
Expand Up @@ -418,4 +418,18 @@ public extension View {
func onReceive<P: Publisher>(_ publisher: P, assignTo binding: Binding<P.Output?>) -> some View where P.Failure == Never {
onReceive(publisher) { binding.wrappedValue = $0 }
}

/// Get the scroll view content offset X
/// - Parameter offsetX: Binding for offset
/// - Returns: View
func scrollOffsetX(_ offsetX: Binding<CGFloat>) -> some View {
ScrollViewOffSetReaderRepresentable(offset: offsetX, isOffsetX: true) { self }
}

/// Get the scroll view content offset Y
/// - Parameter offsetY: Binding for offset
/// - Returns: View
func scrollOffsetY(_ offsetY: Binding<CGFloat>) -> some View {
ScrollViewOffSetReaderRepresentable(offset: offsetY, isOffsetX: false) { self }
}
}

0 comments on commit 7f0da42

Please sign in to comment.