initial commit

This commit is contained in:
Casper Zandbergen 2020-06-03 10:05:52 +02:00
commit 06886ac07b
4 changed files with 257 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

29
Package.swift Normal file
View File

@ -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"]),
]
)

90
README.md Normal file
View File

@ -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")
})
}
}
}
}
```

View File

@ -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
}
}