import Foundation import UIKit import ComponentFlow import Display public final class ScrollChildEnvironment: Equatable { public let insets: UIEdgeInsets public init(insets: UIEdgeInsets) { self.insets = insets } public static func ==(lhs: ScrollChildEnvironment, rhs: ScrollChildEnvironment) -> Bool { if lhs.insets != rhs.insets { return false } return true } } public final class ScrollComponent: Component { public typealias EnvironmentType = ChildEnvironment public class ExternalState { public var contentHeight: CGFloat = 0.0 public init() { } } let content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)> let externalState: ExternalState? let contentInsets: UIEdgeInsets let contentOffsetUpdated: (_ top: CGFloat, _ bottom: CGFloat) -> Void let contentOffsetWillCommit: (UnsafeMutablePointer) -> Void let resetScroll: ActionSlot public init( content: AnyComponent<(ChildEnvironment, ScrollChildEnvironment)>, externalState: ExternalState? = nil, contentInsets: UIEdgeInsets, contentOffsetUpdated: @escaping (_ top: CGFloat, _ bottom: CGFloat) -> Void, contentOffsetWillCommit: @escaping (UnsafeMutablePointer) -> Void, resetScroll: ActionSlot = ActionSlot() ) { self.content = content self.externalState = externalState self.contentInsets = contentInsets self.contentOffsetUpdated = contentOffsetUpdated self.contentOffsetWillCommit = contentOffsetWillCommit self.resetScroll = resetScroll } public static func ==(lhs: ScrollComponent, rhs: ScrollComponent) -> Bool { if lhs.content != rhs.content { return false } if lhs.contentInsets != rhs.contentInsets { return false } return true } public final class View: UIScrollView, UIScrollViewDelegate { private var component: ScrollComponent? private let contentView: ComponentHostView<(ChildEnvironment, ScrollChildEnvironment)> override init(frame: CGRect) { self.contentView = ComponentHostView() super.init(frame: frame) if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.contentInsetAdjustmentBehavior = .never } self.delegate = self self.showsVerticalScrollIndicator = false self.showsHorizontalScrollIndicator = false self.canCancelContentTouches = true self.addSubview(self.contentView) } public override func touchesShouldCancel(in view: UIView) -> Bool { return true } private var ignoreDidScroll = false public func scrollViewDidScroll(_ scrollView: UIScrollView) { guard let component = self.component, !self.ignoreDidScroll else { return } let topOffset = scrollView.contentOffset.y let bottomOffset = max(0.0, scrollView.contentSize.height - scrollView.contentOffset.y - scrollView.frame.height) component.contentOffsetUpdated(topOffset, bottomOffset) } public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { guard let component = self.component, !self.ignoreDidScroll else { return } component.contentOffsetWillCommit(targetContentOffset) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: ScrollComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { let contentSize = self.contentView.update( transition: transition, component: component.content, environment: { environment[ChildEnvironment.self] ScrollChildEnvironment(insets: component.contentInsets) }, containerSize: CGSize(width: availableSize.width, height: .greatestFiniteMagnitude) ) transition.setFrame(view: self.contentView, frame: CGRect(origin: .zero, size: contentSize), completion: nil) component.resetScroll.connect { [weak self] point in self?.setContentOffset(point ?? .zero, animated: point != nil) } if self.contentSize != contentSize { self.ignoreDidScroll = true self.contentSize = contentSize self.ignoreDidScroll = false } if self.scrollIndicatorInsets != component.contentInsets { self.scrollIndicatorInsets = component.contentInsets } component.externalState?.contentHeight = contentSize.height self.component = component return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }