initial commit
This commit is contained in:
commit
06886ac07b
|
@ -0,0 +1,5 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
|
@ -0,0 +1,29 @@
|
|||
// swift-tools-version:5.2
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "ScrollViewProxy",
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "ScrollViewProxy",
|
||||
targets: ["ScrollViewProxy"]),
|
||||
],
|
||||
dependencies: [
|
||||
// Dependencies declare other packages that this package depends on.
|
||||
// .package(url: /* package url */, from: "1.0.0"),
|
||||
.package(name: "Introspect", url: "https://github.com/timbersoftware/SwiftUI-Introspect.git", from: "0.1.0")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(
|
||||
name: "ScrollViewProxy",
|
||||
dependencies: ["Introspect"]),
|
||||
// .testTarget(
|
||||
// name: "ScrollViewProxyTests",
|
||||
// dependencies: ["ScrollViewProxy"]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,90 @@
|
|||
# ScrollViewProxy
|
||||
|
||||
Adds `ScrollViewReader` and `ScrollViewProxy` that help you scroll to locations in a ScrollView
|
||||
|
||||
|
||||
To get a ScrollViewProxy you can either use the conveinience init on ScrollView
|
||||
|
||||
```swift
|
||||
ScrollView { proxy in
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
or add a ScrollViewReader to any View that creates a UIScrollView under the hood
|
||||
|
||||
```swift
|
||||
List {
|
||||
ScrollViewReader { proxy in
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The ScrollViewProxy currently has two functions you can call
|
||||
|
||||
```swift
|
||||
/// Scrolls to an edge or corner
|
||||
public func scrollTo(_ alignment: Alignment, animated: Bool = true)
|
||||
|
||||
/// Scrolls the view with ID to an edge or corner
|
||||
public func scrollTo(_ id: ID, alignment: Alignment = .top, animated: Bool = true)
|
||||
```
|
||||
|
||||
To use the scroll to ID function you have to add a view with that ID to the ScrollViewProxy
|
||||
|
||||
```swift
|
||||
ScrollView { proxy in
|
||||
HStack { ... }
|
||||
.id("someId", scrollView: proxy)
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
Everything put together in an example
|
||||
|
||||
```swift
|
||||
struct ScrollViewProxySimpleExample: View {
|
||||
|
||||
@State var randomInt = Int.random(in: 0..<200)
|
||||
@State var proxy: ScrollViewProxy<Int>? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollView { proxy in
|
||||
ForEach(0..<200) { index in
|
||||
VStack {
|
||||
Text("\(index)").font(.title)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.id(index, scrollView: proxy)
|
||||
}.onAppear {
|
||||
self.proxy = proxy
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.proxy?.scrollTo(self.randomInt, alignment: .center)
|
||||
self.randomInt = Int.random(in: 0..<200)
|
||||
}, label: {
|
||||
Text("Go to \(self.randomInt)")
|
||||
})
|
||||
Spacer()
|
||||
Button(action: { self.proxy?.scrollTo(.top) }, label: {
|
||||
Text("Top")
|
||||
})
|
||||
Spacer()
|
||||
Button(action: { self.proxy?.scrollTo(.center) }, label: {
|
||||
Text("Center")
|
||||
})
|
||||
Spacer()
|
||||
Button(action: { self.proxy?.scrollTo(.bottom) }, label: {
|
||||
Text("Bottom")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,133 @@
|
|||
// Created by Casper Zandbergen on 01/06/2020.
|
||||
// https://twitter.com/amzdme
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
extension ScrollView {
|
||||
/// Creates a ScrollView with a ScrollViewReader
|
||||
public init<ID: Hashable, ProxyContent: View>(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, @ViewBuilder content: @escaping (ScrollViewProxy<ID>) -> ProxyContent) where Content == ScrollViewReader<ID, ProxyContent> {
|
||||
self.init(axes, showsIndicators: showsIndicators, content: {
|
||||
ScrollViewReader { content($0) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Adds an ID to this view so you can scroll to it with `ScrollViewProxy.scrollTo(_:alignment:animated:)`
|
||||
public func id<ID: Hashable>(_ id: ID, scrollView proxy: ScrollViewProxy<ID>) -> some View {
|
||||
func save(geometry: GeometryProxy) -> some View {
|
||||
proxy.save(geometry: geometry, for: id)
|
||||
return Color.clear
|
||||
}
|
||||
|
||||
return self.background(GeometryReader(content: save(geometry:)))
|
||||
}
|
||||
}
|
||||
|
||||
public struct ScrollViewReader<ID: Hashable, Content: View>: View {
|
||||
private var content: (ScrollViewProxy<ID>) -> Content
|
||||
|
||||
@State private var proxy = ScrollViewProxy<ID>()
|
||||
|
||||
public init(@ViewBuilder content: @escaping (ScrollViewProxy<ID>) -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
content(proxy)
|
||||
.coordinateSpace(name: proxy.space)
|
||||
.introspectScrollView { self.proxy.coordinator.scrollView = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
public struct ScrollViewProxy<ID: Hashable> {
|
||||
fileprivate class Coordinator<ID: Hashable> {
|
||||
var frames = [ID: CGRect]()
|
||||
weak var scrollView: UIScrollView?
|
||||
}
|
||||
fileprivate var coordinator = Coordinator<ID>()
|
||||
fileprivate var space: UUID = UUID()
|
||||
|
||||
fileprivate init() { }
|
||||
|
||||
/// Scrolls to an edge or corner
|
||||
public func scrollTo(_ alignment: Alignment, animated: Bool = true) {
|
||||
guard let scrollView = coordinator.scrollView else { return }
|
||||
|
||||
let contentRect = CGRect(origin: .zero, size: scrollView.contentSize)
|
||||
let visibleFrame = frame(contentRect, with: alignment)
|
||||
scrollView.scrollRectToVisible(visibleFrame, animated: animated)
|
||||
}
|
||||
|
||||
/// Scrolls the view with ID to an edge or corner
|
||||
public func scrollTo(_ id: ID, alignment: Alignment = .top, animated: Bool = true) {
|
||||
guard let scrollView = coordinator.scrollView else { return }
|
||||
guard let cellFrame = coordinator.frames[id] else {
|
||||
return print("ID not found, make sure to add views with `.id(_:scrollView:)`")
|
||||
}
|
||||
|
||||
let visibleFrame = frame(cellFrame, with: alignment)
|
||||
scrollView.scrollRectToVisible(visibleFrame, animated: animated)
|
||||
}
|
||||
|
||||
private func frame(_ frame: CGRect, with alignment: Alignment) -> CGRect {
|
||||
guard let scrollView = coordinator.scrollView else { return frame }
|
||||
|
||||
var visibleSize = scrollView.visibleSize
|
||||
visibleSize.width -= scrollView.adjustedContentInset.horizontal
|
||||
visibleSize.height -= scrollView.adjustedContentInset.vertical
|
||||
|
||||
var origin = CGPoint.zero
|
||||
switch alignment {
|
||||
case .center:
|
||||
origin.x = frame.midX - visibleSize.width / 2
|
||||
origin.y = frame.midY - visibleSize.height / 2
|
||||
case .leading:
|
||||
origin.x = frame.minX
|
||||
origin.y = frame.midY - visibleSize.height / 2
|
||||
case .trailing:
|
||||
origin.x = frame.maxX - visibleSize.width
|
||||
origin.y = frame.midY - visibleSize.height / 2
|
||||
case .top:
|
||||
origin.x = frame.midX - visibleSize.width / 2
|
||||
origin.y = frame.minY
|
||||
case .bottom:
|
||||
origin.x = frame.midX - visibleSize.width / 2
|
||||
origin.y = frame.maxY - visibleSize.height
|
||||
case .topLeading:
|
||||
origin.x = frame.minX
|
||||
origin.y = frame.minY
|
||||
case .topTrailing:
|
||||
origin.x = frame.maxX - visibleSize.width
|
||||
origin.y = frame.minY
|
||||
case .bottomLeading:
|
||||
origin.x = frame.minX
|
||||
origin.y = frame.maxY - visibleSize.height
|
||||
case .bottomTrailing:
|
||||
origin.x = frame.maxX - visibleSize.width
|
||||
origin.y = frame.maxY - visibleSize.height
|
||||
default:
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
origin.x = max(0, min(origin.x, scrollView.contentSize.width - visibleSize.width))
|
||||
origin.y = max(0, min(origin.y, scrollView.contentSize.height - visibleSize.height))
|
||||
return CGRect(origin: origin, size: visibleSize)
|
||||
}
|
||||
|
||||
fileprivate func save(geometry: GeometryProxy, for id: ID) {
|
||||
coordinator.frames[id] = geometry.frame(in: .named(space))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIEdgeInsets {
|
||||
/// top + bottom
|
||||
var vertical: CGFloat {
|
||||
return top + bottom
|
||||
}
|
||||
/// left + right
|
||||
var horizontal: CGFloat {
|
||||
return left + right
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue