Add ScrollView (#58)

* Init ScrollView

* Add ScrollView component

* Update ScollView

* Update Scroll extension

* Fix Ref index

* Add scroll example with ref styling

* Init zoom

* Add contentInset, bounces, scrollsToTop to ScrollView

* Add alwaysBounceVertical to ScrollView

* Add alwaysBounceHorizontal, indicatorStyle to ScrollView

* Add scrollIndicatorInsets, showsHorizontalScrollIndicator, showsVerticalScrollIndicator to ScrollView

* Add isDirectionalLockEnabled, isPagingEnabled, isScrollEnabled to ScrollView

* Add maximumZoomScale, minimumZoomScale, zoomScale to ScrollView

* Add bouncesZoom to ScrollView

* Add extension to RefComponent

* Add zoom to Image example

* Add simple ScrollView example

* Format code
This commit is contained in:
matvii 2019-03-06 14:04:22 +02:00 committed by GitHub
parent a1b5d0388a
commit f7b5384c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 256 additions and 8 deletions

View File

@ -22,6 +22,7 @@
A67717202226DC7C0028A6F3 /* Gameboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A677171F2226DC7C0028A6F3 /* Gameboard.swift */; };
A67717222226E7FD0028A6F3 /* Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A67717212226E7FD0028A6F3 /* Menu.swift */; };
A6D5AF87221B131400DBF186 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D5AF86221B131400DBF186 /* Image.swift */; };
A6FEF7952227C1CC008BB292 /* Scroll.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FEF7942227C1CC008BB292 /* Scroll.swift */; };
C449B806DFEE55B6CEE6478C /* libPods-TokamakDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B96B435A9D67621D318616E /* libPods-TokamakDemo.a */; };
D11DB6432219C03000013FC3 /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11DB6422219C03000013FC3 /* Timer.swift */; };
D1BB3D302223F6B400C30062 /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BB3D2F2223F6B400C30062 /* Animation.swift */; };
@ -59,6 +60,7 @@
A677171F2226DC7C0028A6F3 /* Gameboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gameboard.swift; sourceTree = "<group>"; };
A67717212226E7FD0028A6F3 /* Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = "<group>"; };
A6D5AF86221B131400DBF186 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
A6FEF7942227C1CC008BB292 /* Scroll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scroll.swift; sourceTree = "<group>"; };
A9EEF813955DAEEFE1D52ED4 /* Pods-TokamakDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TokamakDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TokamakDemo/Pods-TokamakDemo.debug.xcconfig"; sourceTree = "<group>"; };
C6DA99382B6892EAB361742F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
D11DB6422219C03000013FC3 /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = "<group>"; };
@ -200,6 +202,7 @@
A6D5AF86221B131400DBF186 /* Image.swift */,
A62AC6532223F5CD009B3B25 /* TextField.swift */,
D1BB3D2F2223F6B400C30062 /* Animation.swift */,
A6FEF7942227C1CC008BB292 /* Scroll.swift */,
);
path = Components;
sourceTree = "<group>";
@ -315,6 +318,7 @@
D1F7185F2215A5D0004E5951 /* Constraints.swift in Sources */,
A62AC65922243DCB009B3B25 /* Cell.swift in Sources */,
D11DB6432219C03000013FC3 /* Timer.swift in Sources */,
A6FEF7952227C1CC008BB292 /* Scroll.swift in Sources */,
A62AC65622243CC3009B3B25 /* SnakeGame.swift in Sources */,
D1F7185322159E09004E5951 /* Controls.swift in Sources */,
D1DEEC2922009E8000C525EE /* ModalRouter.swift in Sources */,

View File

@ -8,20 +8,48 @@
import Tokamak
struct ImageExample: PureLeafComponent {
class ScrollDelegate: NSObject, UIScrollViewDelegate {
var view: UIView?
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return view
}
}
struct ImageExample: LeafComponent {
typealias Props = Null
static func render(props _: Null) -> AnyNode {
static func render(props _: Null, hooks: Hooks) -> AnyNode {
let refScroll = hooks.ref(type: UIScrollView.self)
let refImage = hooks.ref(type: UIImageView.self)
let delegate = hooks.ref(ScrollDelegate())
hooks.effect {
guard let image = refImage.value else { return }
guard let scroll = refScroll.value else { return }
delegate.value.view = image
scroll.delegate = delegate.value
}
return StackView.node(.init(
Edges.equal(to: .safeArea),
alignment: .center,
axis: .vertical,
distribution: .fillEqually
distribution: .fill
), [
ImageView.node(.init(
Style(contentMode: .scaleAspectFit),
image: Image(name: "tokamak")
)),
Label.node("You can pan and zoom this image"),
ScrollView.node(
.init(
Style(Width.equal(to: .parent)),
maximumZoomScale: 2.0
),
ImageView.node(.init(
Style(Edges.equal(to: .parent), contentMode: .scaleAspectFit),
image: Image(name: "tokamak")
), ref: refImage),
ref: refScroll
),
])
}
}

View File

@ -0,0 +1,30 @@
//
// Scroll.swift
// TokamakDemo
//
// Created by Matvii Hodovaniuk on 2/28/19.
// Copyright © 2019 Tokamak. All rights reserved.
//
import Tokamak
struct ScrollViewExample: LeafComponent {
typealias Props = Null
static func render(props: Props, hooks: Hooks) -> AnyNode {
return View.node(
.init(Style(Edges.equal(to: .safeArea))),
ScrollView.node(
.init(Style(Edges.equal(to: .parent))),
StackView.node(
.init(
Edges.equal(to: .parent),
axis: .vertical,
distribution: .fill
),
(1..<100).map { Label.node("Text \($0)") }
)
)
)
}
}

View File

@ -21,6 +21,7 @@ enum AppRoute: String, CaseIterable {
case textField = "Text Field"
case animation
case snakeGame = "Snake Game"
case scrollView = "Scroll"
}
extension AppRoute: CustomStringConvertible {
@ -68,6 +69,8 @@ struct Router: NavigationRouter {
result = Animation.node()
case .snakeGame:
result = SnakeGame.node()
case .scrollView:
result = ScrollViewExample.node()
}
return NavigationItem.node(

View File

@ -154,3 +154,12 @@ extension RefComponent where Children == Null {
return node(props, Null(), ref: ref)
}
}
extension RefComponent where Props: Default, Props.DefaultValue == Props {
public static func node(
_ children: Children,
ref: Ref<RefTarget?>
) -> AnyNode {
return node(Props.defaultValue, children, ref: ref)
}
}

View File

@ -0,0 +1,97 @@
//
// ScrollView.swift
// Tokamak
//
// Created by Matvii Hodovaniuk on 2/28/19.
//
public struct ScrollView: HostComponent {
public struct Props: Equatable, StyleProps, Default {
public static var defaultValue: Props {
return Props()
}
public struct EdgeInsets: Equatable {
public let bottom: Float
public let left: Float
public let right: Float
public let top: Float
public init(
top: Float = 0,
left: Float = 0,
bottom: Float = 0,
right: Float = 0
) {
self.top = top
self.left = left
self.bottom = bottom
self.right = right
}
}
public enum IndicatorStyle {
case `default`
case black
case white
}
public let style: Style?
public let alwaysBounceHorizontal: Bool
public let alwaysBounceVertical: Bool
public let bounces: Bool
public let bouncesZoom: Bool
public let contentInset: EdgeInsets
public let indicatorStyle: IndicatorStyle
public let isDirectionalLockEnabled: Bool
public let isPagingEnabled: Bool
public let isScrollEnabled: Bool
public let maximumZoomScale: Float
public let minimumZoomScale: Float
public let scrollIndicatorInsets: EdgeInsets
public let scrollsToTop: Bool
public let showsHorizontalScrollIndicator: Bool
public let showsVerticalScrollIndicator: Bool
public let zoomScale: Float
public init(
_ style: Style? = nil,
alwaysBounceHorizontal: Bool = false,
alwaysBounceVertical: Bool = false,
bounces: Bool = true,
bouncesZoom: Bool = true,
contentInset: EdgeInsets = EdgeInsets(),
indicatorStyle: IndicatorStyle = .default,
isDirectionalLockEnabled: Bool = false,
isPagingEnabled: Bool = false,
isScrollEnabled: Bool = true,
maximumZoomScale: Float = 1.0,
minimumZoomScale: Float = 1.0,
scrollIndicatorInsets: EdgeInsets = EdgeInsets(),
scrollsToTop: Bool = true,
showsHorizontalScrollIndicator: Bool = true,
showsVerticalScrollIndicator: Bool = true,
zoomScale: Float = 1.0
) {
self.style = style
self.alwaysBounceHorizontal = alwaysBounceHorizontal
self.alwaysBounceVertical = alwaysBounceVertical
self.bounces = bounces
self.bouncesZoom = bouncesZoom
self.contentInset = contentInset
self.isDirectionalLockEnabled = isDirectionalLockEnabled
self.isPagingEnabled = isPagingEnabled
self.indicatorStyle = indicatorStyle
self.isScrollEnabled = isScrollEnabled
self.maximumZoomScale = maximumZoomScale
self.minimumZoomScale = minimumZoomScale
self.scrollIndicatorInsets = scrollIndicatorInsets
self.scrollsToTop = scrollsToTop
self.showsVerticalScrollIndicator = showsVerticalScrollIndicator
self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator
self.zoomScale = zoomScale
}
}
public typealias Children = AnyNode
}

View File

@ -109,7 +109,7 @@ public final class Hooks {
(initialized from `initial` if current was absent)
*/
func ref<T>(_ initial: Ref<T>) -> Ref<T> {
defer { stateIndex += 1 }
defer { refIndex += 1 }
guard let component = component else {
assertionFailure("hooks.state should only be called within `render`")

View File

@ -0,0 +1,67 @@
//
// ScrollView.swift
// TokamakUIKit
//
// Created by Matvii Hodovaniuk on 2/28/19.
//
import Tokamak
import UIKit
final class TokamakScrollView: UIScrollView, Default {
static var defaultValue: TokamakScrollView {
return TokamakScrollView()
}
}
extension UIEdgeInsets {
public init(_ edges: ScrollView.Props.EdgeInsets) {
self.init(
top: CGFloat(edges.top),
left: CGFloat(edges.left),
bottom: CGFloat(edges.bottom),
right: CGFloat(edges.right)
)
}
}
extension UIScrollView.IndicatorStyle {
public init(_ type: ScrollView.Props.IndicatorStyle) {
switch type {
case .default:
self = .default
case .black:
self = .black
case .white:
self = .white
}
}
}
extension ScrollView: UIViewComponent {
public typealias RefTarget = UIScrollView
static func update(
view box: ViewBox<TokamakScrollView>,
_ props: ScrollView.Props,
_ children: AnyNode
) {
let view = box.view
view.alwaysBounceHorizontal = props.alwaysBounceHorizontal
view.alwaysBounceVertical = props.alwaysBounceVertical
view.bounces = props.bounces
view.bouncesZoom = props.bouncesZoom
view.contentInset = UIEdgeInsets(props.contentInset)
view.isDirectionalLockEnabled = props.isDirectionalLockEnabled
view.isPagingEnabled = props.isPagingEnabled
view.indicatorStyle = UIScrollView.IndicatorStyle(props.indicatorStyle)
view.isScrollEnabled = props.isScrollEnabled
view.maximumZoomScale = CGFloat(props.maximumZoomScale)
view.minimumZoomScale = CGFloat(props.minimumZoomScale)
view.scrollIndicatorInsets = UIEdgeInsets(props.scrollIndicatorInsets)
view.scrollsToTop = props.scrollsToTop
view.showsVerticalScrollIndicator = props.showsVerticalScrollIndicator
view.showsHorizontalScrollIndicator = props.showsHorizontalScrollIndicator
view.zoomScale = CGFloat(props.zoomScale)
}
}

View File

@ -120,6 +120,8 @@ extension UIViewComponent where Target == Target.DefaultValue,
box.view.addArrangedSubview(target)
// no covariance/contravariance in Swift generics require next
// two cases to be duplicated :(
case let box as ViewBox<TokamakScrollView>:
box.view.addSubview(target)
case let box as ViewBox<UIView>:
box.view.addSubview(target)
case let box as ViewBox<TokamakView>:

View File

@ -25,6 +25,8 @@
A6AB60B5221C70340063F88A /* ContentMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AB60B4221C70340063F88A /* ContentMode.swift */; };
A6D188EC221F1CA300C3947C /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D188EB221F1CA300C3947C /* Accessibility.swift */; };
A6D188EE2220221B00C3947C /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D188ED2220221B00C3947C /* TextField.swift */; };
A6FEF7912227BAB5008BB292 /* ScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FEF7902227BAB5008BB292 /* ScrollView.swift */; };
A6FEF7932227BAD0008BB292 /* ScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FEF7922227BAD0008BB292 /* ScrollView.swift */; };
D1B4F8DB221AFB0E00C53C42 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4F8D9221AFB0800C53C42 /* ImageView.swift */; };
D1B4F8DD221AFB2B00C53C42 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4F8DC221AFB2B00C53C42 /* ImageView.swift */; };
D1CFC81D222546F500B03222 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CFC81C222546F500B03222 /* Image.swift */; };
@ -192,6 +194,8 @@
A6D188EB221F1CA300C3947C /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = "<group>"; };
A6D188ED2220221B00C3947C /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
A6D188EF2220222F00C3947C /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
A6FEF7902227BAB5008BB292 /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = "<group>"; };
A6FEF7922227BAD0008BB292 /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = "<group>"; };
D1B4F8D9221AFB0800C53C42 /* ImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
D1B4F8DC221AFB2B00C53C42 /* ImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
D1CFC81C222546F500B03222 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
@ -437,6 +441,7 @@
OBJ_29 /* Switch.swift */,
OBJ_30 /* View.swift */,
A6D188EF2220222F00C3947C /* TextField.swift */,
A6FEF7922227BAD0008BB292 /* ScrollView.swift */,
);
path = Host;
sourceTree = "<group>";
@ -575,6 +580,7 @@
OBJ_82 /* Switch.swift */,
A6D188ED2220221B00C3947C /* TextField.swift */,
OBJ_83 /* View.swift */,
A6FEF7902227BAB5008BB292 /* ScrollView.swift */,
);
path = Host;
sourceTree = "<group>";
@ -811,6 +817,7 @@
OBJ_179 /* CenterY.swift in Sources */,
OBJ_180 /* Constraint.swift in Sources */,
OBJ_181 /* Edges.swift in Sources */,
A6FEF7912227BAB5008BB292 /* ScrollView.swift in Sources */,
OBJ_182 /* FirstBaseline.swift in Sources */,
OBJ_183 /* Height.swift in Sources */,
OBJ_184 /* LastBaseline.swift in Sources */,
@ -910,6 +917,7 @@
OBJ_281 /* Height.swift in Sources */,
OBJ_282 /* LastBaseline.swift in Sources */,
OBJ_283 /* Leading.swift in Sources */,
A6FEF7932227BAD0008BB292 /* ScrollView.swift in Sources */,
OBJ_284 /* Left.swift in Sources */,
OBJ_285 /* OwnConstraint.swift in Sources */,
OBJ_286 /* Right.swift in Sources */,