From f7b5384c958cbd8fbe1003f9135e4e215b472fe6 Mon Sep 17 00:00:00 2001 From: matvii Date: Wed, 6 Mar 2019 14:04:22 +0200 Subject: [PATCH] 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 --- Example/Tokamak.xcodeproj/project.pbxproj | 4 + Example/Tokamak/Components/Image.swift | 42 ++++++-- Example/Tokamak/Components/Scroll.swift | 30 ++++++ Example/Tokamak/Router.swift | 3 + Sources/Tokamak/AnyNode.swift | 9 ++ .../Tokamak/Components/Host/ScrollView.swift | 97 +++++++++++++++++++ Sources/Tokamak/Hooks/Hooks.swift | 2 +- .../Components/Host/ScrollView.swift | 67 +++++++++++++ .../Protocols/UIViewComponent.swift | 2 + Tokamak.xcodeproj/project.pbxproj | 8 ++ 10 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 Example/Tokamak/Components/Scroll.swift create mode 100644 Sources/Tokamak/Components/Host/ScrollView.swift create mode 100644 Sources/TokamakUIKit/Components/Host/ScrollView.swift diff --git a/Example/Tokamak.xcodeproj/project.pbxproj b/Example/Tokamak.xcodeproj/project.pbxproj index 4de99616..1f24e677 100644 --- a/Example/Tokamak.xcodeproj/project.pbxproj +++ b/Example/Tokamak.xcodeproj/project.pbxproj @@ -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 = ""; }; A67717212226E7FD0028A6F3 /* Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menu.swift; sourceTree = ""; }; A6D5AF86221B131400DBF186 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + A6FEF7942227C1CC008BB292 /* Scroll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Scroll.swift; sourceTree = ""; }; 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 = ""; }; C6DA99382B6892EAB361742F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; D11DB6422219C03000013FC3 /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = ""; }; @@ -200,6 +202,7 @@ A6D5AF86221B131400DBF186 /* Image.swift */, A62AC6532223F5CD009B3B25 /* TextField.swift */, D1BB3D2F2223F6B400C30062 /* Animation.swift */, + A6FEF7942227C1CC008BB292 /* Scroll.swift */, ); path = Components; sourceTree = ""; @@ -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 */, diff --git a/Example/Tokamak/Components/Image.swift b/Example/Tokamak/Components/Image.swift index 1948aac8..009e0d57 100644 --- a/Example/Tokamak/Components/Image.swift +++ b/Example/Tokamak/Components/Image.swift @@ -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 + ), ]) } } diff --git a/Example/Tokamak/Components/Scroll.swift b/Example/Tokamak/Components/Scroll.swift new file mode 100644 index 00000000..5948ba13 --- /dev/null +++ b/Example/Tokamak/Components/Scroll.swift @@ -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)") } + ) + ) + ) + } +} diff --git a/Example/Tokamak/Router.swift b/Example/Tokamak/Router.swift index 90af8780..a39217a1 100644 --- a/Example/Tokamak/Router.swift +++ b/Example/Tokamak/Router.swift @@ -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( diff --git a/Sources/Tokamak/AnyNode.swift b/Sources/Tokamak/AnyNode.swift index 68481f01..9f8368c3 100644 --- a/Sources/Tokamak/AnyNode.swift +++ b/Sources/Tokamak/AnyNode.swift @@ -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 + ) -> AnyNode { + return node(Props.defaultValue, children, ref: ref) + } +} diff --git a/Sources/Tokamak/Components/Host/ScrollView.swift b/Sources/Tokamak/Components/Host/ScrollView.swift new file mode 100644 index 00000000..a007bff9 --- /dev/null +++ b/Sources/Tokamak/Components/Host/ScrollView.swift @@ -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 +} diff --git a/Sources/Tokamak/Hooks/Hooks.swift b/Sources/Tokamak/Hooks/Hooks.swift index b674355f..9ce1b326 100644 --- a/Sources/Tokamak/Hooks/Hooks.swift +++ b/Sources/Tokamak/Hooks/Hooks.swift @@ -109,7 +109,7 @@ public final class Hooks { (initialized from `initial` if current was absent) */ func ref(_ initial: Ref) -> Ref { - defer { stateIndex += 1 } + defer { refIndex += 1 } guard let component = component else { assertionFailure("hooks.state should only be called within `render`") diff --git a/Sources/TokamakUIKit/Components/Host/ScrollView.swift b/Sources/TokamakUIKit/Components/Host/ScrollView.swift new file mode 100644 index 00000000..26b47011 --- /dev/null +++ b/Sources/TokamakUIKit/Components/Host/ScrollView.swift @@ -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, + _ 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) + } +} diff --git a/Sources/TokamakUIKit/Components/Protocols/UIViewComponent.swift b/Sources/TokamakUIKit/Components/Protocols/UIViewComponent.swift index 319a5702..1a9666ad 100644 --- a/Sources/TokamakUIKit/Components/Protocols/UIViewComponent.swift +++ b/Sources/TokamakUIKit/Components/Protocols/UIViewComponent.swift @@ -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: + box.view.addSubview(target) case let box as ViewBox: box.view.addSubview(target) case let box as ViewBox: diff --git a/Tokamak.xcodeproj/project.pbxproj b/Tokamak.xcodeproj/project.pbxproj index 7fb055c3..e68bd82d 100644 --- a/Tokamak.xcodeproj/project.pbxproj +++ b/Tokamak.xcodeproj/project.pbxproj @@ -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 = ""; }; A6D188ED2220221B00C3947C /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; A6D188EF2220222F00C3947C /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; + A6FEF7902227BAB5008BB292 /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = ""; }; + A6FEF7922227BAD0008BB292 /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = ""; }; D1B4F8D9221AFB0800C53C42 /* ImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; D1B4F8DC221AFB2B00C53C42 /* ImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; D1CFC81C222546F500B03222 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; @@ -437,6 +441,7 @@ OBJ_29 /* Switch.swift */, OBJ_30 /* View.swift */, A6D188EF2220222F00C3947C /* TextField.swift */, + A6FEF7922227BAD0008BB292 /* ScrollView.swift */, ); path = Host; sourceTree = ""; @@ -575,6 +580,7 @@ OBJ_82 /* Switch.swift */, A6D188ED2220221B00C3947C /* TextField.swift */, OBJ_83 /* View.swift */, + A6FEF7902227BAB5008BB292 /* ScrollView.swift */, ); path = Host; sourceTree = ""; @@ -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 */,