├── .gitignore ├── .swift-version ├── HoverConversion.podspec ├── HoverConversion ├── HCContentViewController.swift ├── HCDefaultAnimatedTransitioning.swift ├── HCNavigationController.swift ├── HCNavigationView.swift ├── HCNextHeaderView.swift ├── HCPagingViewController.swift ├── HCRootAnimatedTransitioning.swift ├── HCRootViewController.swift ├── HCViewContentable.swift ├── NSIndexPath+Row.swift ├── UIScrollView+BottomBounceSize.swift └── UITableViewCell+Screenshot.swift ├── HoverConversionSample ├── HoverConversionSample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── HoverConversionSample.xcworkspace │ └── contents.xcworkspacedata ├── HoverConversionSample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Cell │ │ ├── HomeTableViewCell.swift │ │ └── HomeTableViewCell.xib │ ├── Info.plist │ ├── Manager │ │ └── TwitterManager.swift │ ├── Request │ │ ├── StatusesUserTimelineRequest.swift │ │ ├── TWTRAPIClient+Extra.swift │ │ ├── TWTRRequestable.swift │ │ └── UsersLookUpRequest.swift │ ├── View │ │ └── NextHeaderView.swift │ └── ViewController │ │ ├── HomeViewController.swift │ │ └── UserTimelineViewController.swift ├── Podfile └── Podfile.lock ├── Images ├── next_header.png ├── sample1.gif ├── sample2.gif └── storyboard.png ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | # OS X 6 | .DS_Store 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /HoverConversion.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint HoverConversion.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'HoverConversion' 11 | s.version = '0.3.1' 12 | s.summary = 'HoverConversion realized vertical paging. UIViewController will be paging when reaching top or bottom of UITableView content.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | # s.description = <<-DESC 21 | # TODO: Add long description of the pod here. 22 | # DESC 23 | 24 | s.homepage = 'https://github.com/marty-suzuki/HoverConversion' 25 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'marty-suzuki' => 's1180183@gmail.com' } 28 | s.source = { :git => 'https://github.com/marty-suzuki/HoverConversion.git', :tag => s.version.to_s } 29 | s.social_media_url = 'https://twitter.com/marty_suzuki' 30 | 31 | s.ios.deployment_target = '8.0' 32 | 33 | s.source_files = 'HoverConversion/*.{swift}' 34 | 35 | # s.resource_bundles = { 36 | # 'HoverConversion' => ['HoverConversion/Assets/*.png'] 37 | # } 38 | 39 | # s.public_header_files = 'Pod/Classes/**/*.h' 40 | s.frameworks = 'UIKit' 41 | s.dependency 'MisterFusion', '~> 2.0.0' 42 | # s.dependency 'AFNetworking', '~> 2.3' 43 | end 44 | -------------------------------------------------------------------------------- /HoverConversion/HCContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCContentViewController.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol HCContentViewControllerScrollDelegate: class { 12 | func contentViewController(_ viewController: HCContentViewController, scrollViewWillBeginDragging scrollView: UIScrollView) 13 | func contentViewController(_ viewController: HCContentViewController, scrollViewDidScroll scrollView: UIScrollView) 14 | func contentViewController(_ viewController: HCContentViewController, scrollViewDidEndDragging scrollView: UIScrollView, willDecelerate decelerate: Bool) 15 | func contentViewController(_ viewController: HCContentViewController, handlePanGesture gesture: UIPanGestureRecognizer) 16 | } 17 | 18 | public struct PagableHandler { 19 | public enum Direction { 20 | case prev, next 21 | } 22 | 23 | private var values: [Direction : Bool] = [ 24 | .prev : true, 25 | .next : true 26 | ] 27 | 28 | public subscript(direction: Direction) -> Bool { 29 | get { 30 | return values[direction] ?? false 31 | } 32 | set { 33 | values[direction] = newValue 34 | } 35 | } 36 | } 37 | 38 | open class HCContentViewController: UIViewController, HCViewContentable { 39 | 40 | open var tableView: UITableView! = UITableView() 41 | open var navigatoinContainerView: UIView! = UIView() 42 | open var navigationView: HCNavigationView! = HCNavigationView(buttonPosition: .left) 43 | let cellImageView = UIImageView(frame: .zero) 44 | 45 | open weak var scrollDelegate: HCContentViewControllerScrollDelegate? 46 | open var canPaging: PagableHandler = PagableHandler() 47 | 48 | open override var title: String? { 49 | didSet { 50 | navigationView?.titleLabel.text = title 51 | } 52 | } 53 | 54 | override open func viewDidLoad() { 55 | super.viewDidLoad() 56 | 57 | // Do any additional setup after loading the view. 58 | automaticallyAdjustsScrollViewInsets = false 59 | addViews() 60 | tableView.delegate = self 61 | view.addSubview(cellImageView) 62 | 63 | let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(HCContentViewController.handleNavigatoinContainerViewPanGesture(_:))) 64 | navigatoinContainerView.addGestureRecognizer(panGestureRecognizer) 65 | } 66 | 67 | override open func didReceiveMemoryWarning() { 68 | super.didReceiveMemoryWarning() 69 | // Dispose of any resources that can be recreated. 70 | } 71 | 72 | open func navigationView(_ navigationView: HCNavigationView, didTapLeftButton button: UIButton) { 73 | _ = navigationController?.popViewController(animated: true) 74 | } 75 | 76 | func handleNavigatoinContainerViewPanGesture(_ gesture: UIPanGestureRecognizer) { 77 | scrollDelegate?.contentViewController(self, handlePanGesture: gesture) 78 | } 79 | } 80 | 81 | extension HCContentViewController: UITableViewDelegate { 82 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 83 | scrollDelegate?.contentViewController(self, scrollViewWillBeginDragging: scrollView) 84 | } 85 | 86 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 87 | scrollDelegate?.contentViewController(self, scrollViewDidScroll: scrollView) 88 | } 89 | 90 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 91 | scrollDelegate?.contentViewController(self, scrollViewDidEndDragging: scrollView, willDecelerate: decelerate) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /HoverConversion/HCDefaultAnimatedTransitioning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCDefaultAnimatedTransitioning.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/09/11. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | // 9 | 10 | import UIKit 11 | 12 | class HCDefaultAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning { 13 | fileprivate struct Const { 14 | static let defaultDuration: TimeInterval = 0.25 15 | static let rootDuration: TimeInterval = 0.4 16 | static let scaling: CGFloat = 0.95 17 | } 18 | 19 | let operation: UINavigationControllerOperation 20 | fileprivate let alphaView = UIView() 21 | 22 | init(operation: UINavigationControllerOperation) { 23 | self.operation = operation 24 | super.init() 25 | } 26 | 27 | @objc func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 28 | guard 29 | let toVC = transitionContext?.viewController(forKey: UITransitionContextViewControllerKey.to), 30 | let fromVC = transitionContext?.viewController(forKey: UITransitionContextViewControllerKey.from) 31 | else { 32 | return 0 33 | } 34 | switch (fromVC, toVC) { 35 | case (_ as HCPagingViewController, _ as HCRootViewController): return Const.rootDuration 36 | case (_ as HCRootViewController, _ as HCPagingViewController): return Const.rootDuration 37 | default: return Const.defaultDuration 38 | } 39 | } 40 | 41 | // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. 42 | @objc func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 43 | guard 44 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), 45 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) 46 | else { 47 | transitionContext.completeTransition(true) 48 | return 49 | } 50 | 51 | let containerView = transitionContext.containerView 52 | containerView.backgroundColor = .black 53 | alphaView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5) 54 | alphaView.frame = containerView.bounds 55 | 56 | switch operation { 57 | case .pop: popAnimation(transitionContext, toVC: toVC, fromVC: fromVC, containerView: containerView) 58 | case .push: pushAnimation(transitionContext, toVC: toVC, fromVC: fromVC, containerView: containerView) 59 | case .none: transitionContext.completeTransition(true) 60 | } 61 | } 62 | 63 | fileprivate func popAnimation(_ transitionContext: UIViewControllerContextTransitioning, toVC: UIViewController, fromVC: UIViewController, containerView: UIView) { 64 | containerView.insertSubview(toVC.view, belowSubview: fromVC.view) 65 | containerView.insertSubview(alphaView, belowSubview: fromVC.view) 66 | 67 | if let pagingVC = fromVC as? HCPagingViewController, let rootVC = toVC as? HCRootViewController { 68 | let indexPath = pagingVC.currentIndexPath 69 | //pagingVC.homeViewTalkContainerView.backgroundColor = .whiteColor() 70 | if rootVC.tableView?.cellForRow(at: indexPath as IndexPath) == nil { 71 | //rootVC.tableView?.scrollToRowAtIndexPath(indexPath, atScrollPosition: pagingVC.scrollDirection, animated: false) 72 | } 73 | rootVC.tableView?.selectRow(at: indexPath as IndexPath, animated: false, scrollPosition: .none) 74 | } 75 | 76 | alphaView.alpha = 1 77 | toVC.view.frame = containerView.bounds 78 | toVC.view.transform = CGAffineTransform(scaleX: Const.scaling, y: Const.scaling) 79 | 80 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: { 81 | toVC.view.transform = CGAffineTransform.identity 82 | fromVC.view.frame.origin.x = containerView.bounds.size.width 83 | self.alphaView.alpha = 0 84 | }) { finished in 85 | let canceled = transitionContext.transitionWasCancelled 86 | if canceled { 87 | toVC.view.removeFromSuperview() 88 | } else { 89 | fromVC.view.removeFromSuperview() 90 | } 91 | 92 | toVC.view.transform = CGAffineTransform.identity 93 | self.alphaView.removeFromSuperview() 94 | 95 | if let pagingVC = fromVC as? HCPagingViewController, let rootVC = toVC as? HCRootViewController { 96 | let indexPath = pagingVC.currentIndexPath 97 | rootVC.tableView?.deselectRow(at: indexPath as IndexPath, animated: true) 98 | } 99 | transitionContext.completeTransition(!canceled) 100 | } 101 | } 102 | 103 | fileprivate func pushAnimation(_ transitionContext: UIViewControllerContextTransitioning, toVC: UIViewController, fromVC: UIViewController, containerView: UIView) { 104 | containerView.addSubview(alphaView) 105 | containerView.addSubview(toVC.view) 106 | 107 | toVC.view.frame = containerView.bounds 108 | toVC.view.frame.origin.x = containerView.bounds.size.width 109 | alphaView.alpha = 0 110 | 111 | UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: { 112 | fromVC.view.transform = CGAffineTransform(scaleX: Const.scaling, y: Const.scaling) 113 | self.alphaView.alpha = 1 114 | toVC.view.frame.origin.x = 0 115 | }) { finished in 116 | let canceled = transitionContext.transitionWasCancelled 117 | if canceled { 118 | toVC.view.removeFromSuperview() 119 | } 120 | 121 | self.alphaView.removeFromSuperview() 122 | fromVC.view.transform = CGAffineTransform.identity 123 | 124 | transitionContext.completeTransition(!canceled) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /HoverConversion/HCNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCNavigationController.swift 3 | // 4 | // Created by Taiki Suzuki on 2016/09/11. 5 | // Copyright © 2016年 marty-suzuki. All rights reserved. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | open class HCNavigationController: UINavigationController { 12 | fileprivate struct Const { 13 | fileprivate static let queueLabel = "jp.marty-suzuki.HoverConversion.SynchronizationQueue" 14 | static let synchronizationQueue = DispatchQueue(label: queueLabel, attributes: []) 15 | static func performBlock(_ block: @escaping () -> ()) { 16 | synchronizationQueue.async { 17 | DispatchQueue.main.sync(execute: block) 18 | } 19 | } 20 | } 21 | 22 | enum SwipeType { 23 | case edge, pan, none 24 | var threshold: CGFloat { 25 | switch self { 26 | case .edge: return 0.3 27 | case .pan: return 0.01 28 | case .none: return 0 29 | } 30 | } 31 | } 32 | 33 | fileprivate let interactiveTransition = UIPercentDrivenInteractiveTransition() 34 | let interactiveEdgePanGestureRecognizer = UIScreenEdgePanGestureRecognizer() 35 | let interactivePanGestureRecognizer = UIPanGestureRecognizer() 36 | 37 | fileprivate var isPaning = false 38 | 39 | 40 | 41 | override open func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | delegate = self 45 | interactivePopGestureRecognizer?.isEnabled = false 46 | 47 | interactiveEdgePanGestureRecognizer.edges = .left 48 | interactiveEdgePanGestureRecognizer.addTarget(self, action: #selector(HCNavigationController.handleInteractiveEdgePanGesture(_:))) 49 | interactiveEdgePanGestureRecognizer.delegate = self 50 | view.addGestureRecognizer(interactiveEdgePanGestureRecognizer) 51 | 52 | interactivePanGestureRecognizer.addTarget(self, action: #selector(HCNavigationController.handleInteractivePanGesture(_:))) 53 | interactivePanGestureRecognizer.delegate = self 54 | view.addGestureRecognizer(interactivePanGestureRecognizer) 55 | } 56 | 57 | func interactiveEdgePanGestureRecognizerMakesToFail(gesture: UIGestureRecognizer) { 58 | gesture.require(toFail: interactiveEdgePanGestureRecognizer) 59 | } 60 | 61 | func interactivePanGestureRecognizerMakesToFail(gesture: UIGestureRecognizer) { 62 | gesture.require(toFail: interactivePanGestureRecognizer) 63 | } 64 | 65 | func handleInteractiveEdgePanGesture(_ edgePanGestureRecognizer: UIScreenEdgePanGestureRecognizer) { 66 | handlePanGesture(edgePanGestureRecognizer, swipeType: .edge) 67 | } 68 | 69 | func handleInteractivePanGesture(_ panGestureRecognizer: UIPanGestureRecognizer) { 70 | handlePanGesture(panGestureRecognizer, swipeType: .pan) 71 | } 72 | 73 | fileprivate func handlePanGesture(_ gesture: UIPanGestureRecognizer, swipeType: SwipeType) { 74 | let translation = gesture.translation(in: view) 75 | let velocity = gesture.velocity(in: view) 76 | let percentage = min(1, max(0, translation.x / view.bounds.size.width)) 77 | 78 | switch gesture.state { 79 | case .began: 80 | Const.performBlock { 81 | if self.viewControllers.count < 2 { return } 82 | self.isPaning = true 83 | self.popViewController(animated: true) 84 | } 85 | case .changed: 86 | Const.performBlock { 87 | self.interactiveTransition.update(percentage) 88 | } 89 | case .ended, .failed, .possible, .cancelled: 90 | Const.performBlock { 91 | self.isPaning = false 92 | if 0 < velocity.x && swipeType.threshold < percentage { 93 | self.interactiveTransition.finish() 94 | } else { 95 | self.interactiveTransition.cancel() 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | extension HCNavigationController: UINavigationControllerDelegate { 103 | public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 104 | return isPaning ? interactiveTransition : nil 105 | } 106 | 107 | public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 108 | //TODO: initial frame 109 | switch (fromVC, toVC, isPaning) { 110 | case (_ as HCRootViewController, _ as HCPagingViewController, _): 111 | return HCRootAnimatedTransitioning(operation: operation) 112 | case (_ as HCPagingViewController, _ as HCRootViewController, false): 113 | return HCRootAnimatedTransitioning(operation: operation) 114 | default: 115 | return HCDefaultAnimatedTransitioning(operation: operation) 116 | } 117 | } 118 | } 119 | 120 | extension HCNavigationController: UIGestureRecognizerDelegate { 121 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 122 | if let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer , 123 | gestureRecognizer === interactivePanGestureRecognizer && 124 | gestureRecognizer.velocity(in: navigationController?.view).x < 0 { 125 | return false 126 | } 127 | return true 128 | } 129 | 130 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { 131 | if gestureRecognizer === interactivePanGestureRecognizer && 132 | otherGestureRecognizer === interactiveEdgePanGestureRecognizer { 133 | return true 134 | } 135 | return false 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /HoverConversion/HCNavigationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCNavigationView.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MisterFusion 11 | 12 | public protocol HCNavigationViewDelegate: class { 13 | func navigationView(_ navigationView: HCNavigationView, didTapLeftButton button: UIButton) 14 | func navigationView(_ navigationView: HCNavigationView, didTapRightButton button: UIButton) 15 | } 16 | 17 | open class HCNavigationView: UIView { 18 | public struct ButtonPosition: OptionSet { 19 | static let right = ButtonPosition(rawValue: 1 << 0) 20 | static let left = ButtonPosition(rawValue: 1 << 1) 21 | 22 | public let rawValue: UInt 23 | public init(rawValue: UInt) { 24 | self.rawValue = rawValue 25 | } 26 | } 27 | 28 | open static let height: CGFloat = 64 29 | 30 | open var leftButton: UIButton? 31 | open let titleLabel: UILabel = { 32 | let label = UILabel(frame: .zero) 33 | label.textAlignment = .center 34 | label.font = UIFont.boldSystemFont(ofSize: 16) 35 | return label 36 | }() 37 | open var rightButton: UIButton? 38 | 39 | weak var delegate: HCNavigationViewDelegate? 40 | 41 | public convenience init() { 42 | self.init(buttonPosition: []) 43 | } 44 | 45 | public init(buttonPosition: ButtonPosition) { 46 | super.init(frame: .zero) 47 | if buttonPosition.contains(.left) { 48 | let leftButton = UIButton(type: .custom) 49 | addLayoutSubview(leftButton, andConstraints: 50 | leftButton.left, 51 | leftButton.bottom, 52 | leftButton.width |==| leftButton.height, 53 | leftButton.height |==| 44 54 | ) 55 | leftButton.setTitle("‹", for: UIControlState()) 56 | leftButton.titleLabel?.font = .systemFont(ofSize: 40) 57 | leftButton.addTarget(self, action: #selector(HCNavigationView.didTouchDown(_:)), for: .touchDown) 58 | leftButton.addTarget(self, action: #selector(HCNavigationView.didTouchDragExit(_:)), for: .touchDragExit) 59 | leftButton.addTarget(self, action: #selector(HCNavigationView.didTouchDragEnter(_:)), for: .touchDragEnter) 60 | leftButton.addTarget(self, action: #selector(HCNavigationView.didTouchUpInside(_:)), for: .touchUpInside) 61 | self.leftButton = leftButton 62 | } 63 | if buttonPosition.contains(.right) { 64 | let rightButton = UIButton(type: .custom) 65 | addLayoutSubview(rightButton, andConstraints: 66 | rightButton.right, 67 | rightButton.bottom, 68 | rightButton.width |==| rightButton.height, 69 | rightButton.height |==| 44 70 | ) 71 | rightButton.addTarget(self, action: #selector(HCNavigationView.didTouchDown(_:)), for: .touchDown) 72 | rightButton.addTarget(self, action: #selector(HCNavigationView.didTouchDragExit(_:)), for: .touchDragExit) 73 | rightButton.addTarget(self, action: #selector(HCNavigationView.didTouchDragEnter(_:)), for: .touchDragEnter) 74 | rightButton.addTarget(self, action: #selector(HCNavigationView.didTouchUpInside(_:)), for: .touchUpInside) 75 | self.rightButton = rightButton 76 | } 77 | 78 | var constraints: [MisterFusion] = [] 79 | if let leftButton = leftButton { 80 | constraints += [leftButton.right |==| titleLabel.left] 81 | } else { 82 | constraints += [titleLabel.left |+| 44] 83 | } 84 | 85 | if let rightButton = rightButton { 86 | constraints += [rightButton.left |==| titleLabel.right] 87 | } else { 88 | constraints += [titleLabel.right |-| 44] 89 | } 90 | constraints += [ 91 | titleLabel.height |==| 44, 92 | titleLabel.bottom 93 | ] 94 | addLayoutSubview(titleLabel, andConstraints: constraints) 95 | } 96 | 97 | required public init?(coder aDecoder: NSCoder) { 98 | fatalError("init(coder:) has not been implemented") 99 | } 100 | 101 | func didTouchUpInside(_ sender: UIButton) { 102 | sender.alpha = 1 103 | if sender == leftButton { 104 | delegate?.navigationView(self, didTapLeftButton: sender) 105 | } else if sender == rightButton { 106 | delegate?.navigationView(self, didTapRightButton: sender) 107 | } 108 | } 109 | 110 | func didTouchDown(_ sender: UIButton) { 111 | sender.alpha = 0.5 112 | } 113 | 114 | func didTouchDragExit(_ sender: UIButton) { 115 | sender.alpha = 0.5 116 | } 117 | 118 | func didTouchDragEnter(_ sender: UIButton) { 119 | sender.alpha = 1 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /HoverConversion/HCNextHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCNextHeaderView.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/09/13. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class HCNextHeaderView: UIView {} 12 | -------------------------------------------------------------------------------- /HoverConversion/HCPagingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCPagingViewController.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MisterFusion 11 | 12 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 13 | switch (lhs, rhs) { 14 | case let (l?, r?): 15 | return l < r 16 | case (nil, _?): 17 | return true 18 | default: 19 | return false 20 | } 21 | } 22 | 23 | fileprivate func >= (lhs: T?, rhs: T?) -> Bool { 24 | switch (lhs, rhs) { 25 | case let (l?, r?): 26 | return l >= r 27 | default: 28 | return !(lhs < rhs) 29 | } 30 | } 31 | 32 | public enum HCPagingPosition: Int { 33 | case upper = 1, center = 0, lower = 2 34 | } 35 | 36 | public protocol HCPagingViewControllerDataSource : class { 37 | func pagingViewController(_ viewController: HCPagingViewController, viewControllerFor indexPath: IndexPath) -> HCContentViewController? 38 | func pagingViewController(_ viewController: HCPagingViewController, nextHeaderViewFor indexPath: IndexPath) -> HCNextHeaderView? 39 | } 40 | 41 | open class HCPagingViewController: UIViewController { 42 | fileprivate struct Const { 43 | static let fireDistance: CGFloat = 180 44 | static let bottomTotalSpace = HCNavigationView.height 45 | static let nextAnimationDuration: TimeInterval = 0.4 46 | static let previousAnimationDuration: TimeInterval = 0.3 47 | static fileprivate func calculateRudderBanding(_ distance: CGFloat, constant: CGFloat, dimension: CGFloat) -> CGFloat { 48 | return (1 - (1 / ((distance * constant / dimension) + 1))) * dimension 49 | } 50 | } 51 | 52 | open fileprivate(set) var viewControllers: [HCPagingPosition : HCContentViewController?] = [ 53 | .upper : nil, 54 | .center : nil, 55 | .lower : nil 56 | ] 57 | 58 | let containerViews: [HCPagingPosition : UIView] = [ 59 | .upper : UIView(), 60 | .center : UIView(), 61 | .lower : UIView() 62 | ] 63 | 64 | fileprivate var containerViewsAdded: Bool = false 65 | var currentIndexPath: IndexPath 66 | open fileprivate(set) var isPaging: Bool = false 67 | fileprivate var isDragging: Bool = false 68 | fileprivate var isPanning = false 69 | fileprivate var beginningContentOffset: CGPoint = .zero 70 | fileprivate(set) var scrollDirection: UITableViewScrollPosition = .none 71 | 72 | fileprivate var _alphaView: UIView? 73 | fileprivate var alphaView: UIView { 74 | let alphaView: UIView 75 | if let _alphaView = _alphaView { 76 | alphaView = _alphaView 77 | } else { 78 | alphaView = createAlphaViewAndAddSubview(containerViews[.center]) 79 | _alphaView = alphaView 80 | } 81 | return alphaView 82 | } 83 | 84 | fileprivate var nextHeaderView: HCNextHeaderView? 85 | 86 | open weak var dataSource: HCPagingViewControllerDataSource? { 87 | didSet { 88 | addContainerViews() 89 | setupViewControllers() 90 | } 91 | } 92 | 93 | public init(indexPath: IndexPath) { 94 | self.currentIndexPath = indexPath 95 | super.init(nibName: nil, bundle: nil) 96 | } 97 | 98 | required public init?(coder aDecoder: NSCoder) { 99 | fatalError("init(coder:) has not been implemented") 100 | } 101 | 102 | override open func viewDidLoad() { 103 | super.viewDidLoad() 104 | 105 | // Do any additional setup after loading the view. 106 | automaticallyAdjustsScrollViewInsets = false 107 | addContainerViews() 108 | } 109 | 110 | open override var preferredStatusBarStyle : UIStatusBarStyle { 111 | return viewController(.center)?.preferredStatusBarStyle ?? .default 112 | } 113 | 114 | fileprivate func createAlphaViewAndAddSubview(_ view: UIView?) -> UIView { 115 | let alphaView = UIView() 116 | alphaView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6) 117 | view?.addLayoutSubview(alphaView, andConstraints: 118 | alphaView.top, alphaView.left, alphaView.bottom, alphaView.right 119 | ) 120 | alphaView.isUserInteractionEnabled = false 121 | alphaView.alpha = 0 122 | return alphaView 123 | } 124 | 125 | fileprivate func clearAlphaView() { 126 | _alphaView?.removeFromSuperview() 127 | _alphaView = nil 128 | } 129 | 130 | fileprivate func clearNextHeaderView() { 131 | nextHeaderView?.removeFromSuperview() 132 | nextHeaderView = nil 133 | } 134 | 135 | fileprivate func setScrollsTop() { 136 | viewControllers.forEach { $0.1?.tableView?.scrollsToTop = $0.0 == .center } 137 | } 138 | 139 | fileprivate func viewController(_ position: HCPagingPosition) -> HCContentViewController? { 140 | guard let nullableViewController = viewControllers[position] else { return nil } 141 | return nullableViewController 142 | } 143 | 144 | fileprivate func setupViewControllers() { 145 | setupViewController(indexPath: currentIndexPath.rowPlus(-1), position: .upper) 146 | setupViewController(indexPath: currentIndexPath, position: .center) 147 | setupViewController(indexPath: currentIndexPath.rowPlus(1), position: .lower) 148 | setScrollsTop() 149 | let tableView = viewController(.center)?.tableView 150 | if let _ = viewController(.lower) { 151 | tableView?.contentInset.bottom = Const.bottomTotalSpace 152 | tableView?.scrollIndicatorInsets.bottom = Const.bottomTotalSpace 153 | } else { 154 | tableView?.contentInset.bottom = 0 155 | tableView?.scrollIndicatorInsets.bottom = 0 156 | } 157 | setNeedsStatusBarAppearanceUpdate() 158 | } 159 | 160 | fileprivate func setupViewController(indexPath: IndexPath, position: HCPagingPosition) { 161 | if (indexPath as NSIndexPath).row < 0 || (indexPath as NSIndexPath).section < 0 { return } 162 | guard 163 | let vc = dataSource?.pagingViewController(self, viewControllerFor: indexPath) 164 | else { return } 165 | addViewController(vc, to: position) 166 | } 167 | 168 | fileprivate func addViewController(_ viewController: HCContentViewController, to position: HCPagingPosition) { 169 | viewController.scrollDelegate = self 170 | addView(viewController.view, to: position) 171 | addChildViewController(viewController) 172 | viewController.didMove(toParentViewController: self) 173 | viewControllers[position] = viewController 174 | } 175 | 176 | fileprivate func addView(_ view: UIView, to position: HCPagingPosition) { 177 | guard let containerView = containerViews[position] else { return } 178 | containerView.addLayoutSubview(view, andConstraints: 179 | view.top, view.right, view.left, view.bottom 180 | ) 181 | } 182 | 183 | fileprivate func addContainerViews() { 184 | if containerViewsAdded { return } 185 | containerViewsAdded = true 186 | containerViews.sorted { $0.0.rawValue < $1.0.rawValue }.forEach { 187 | let misterFusion: MisterFusion 188 | switch $0.0 { 189 | case .upper: misterFusion = $0.1.bottom |==| view.top 190 | case .center: misterFusion = $0.1.top 191 | case .lower: misterFusion = $0.1.top |==| view.bottom 192 | } 193 | view.addLayoutSubview($0.1, andConstraints: 194 | misterFusion, $0.1.height, $0.1.left, $0.1.right 195 | ) 196 | } 197 | } 198 | 199 | override open func didReceiveMemoryWarning() { 200 | super.didReceiveMemoryWarning() 201 | // Dispose of any resources that can be recreated. 202 | } 203 | 204 | func moveToNext(_ scrollView: UIScrollView, offset: CGPoint) { 205 | guard let _ = viewController(.lower) , viewController(.center)?.canPaging[.next] == true else { return } 206 | 207 | scrollDirection = .bottom 208 | let value = offset.y - (scrollView.contentSize.height - scrollView.bounds.size.height) 209 | let headerHeight = HCNavigationView.height 210 | 211 | isPaging = true 212 | scrollView.setContentOffset(scrollView.contentOffset, animated: false) 213 | 214 | let relativeDuration = TimeInterval(0.25) 215 | let lowerViewController = viewController(.lower) 216 | let centerViewController = viewController(.center) 217 | UIView.animateKeyframes(withDuration: Const.nextAnimationDuration, delay: 0, options: UIViewKeyframeAnimationOptions(), animations: { 218 | 219 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 - relativeDuration) { 220 | lowerViewController?.view.frame.origin.y = -self.view.bounds.size.height + headerHeight 221 | centerViewController?.view.frame.origin.y = -self.view.bounds.size.height + value + headerHeight 222 | centerViewController?.navigationView?.frame.origin.y = self.view.bounds.size.height - value - headerHeight 223 | self.nextHeaderView?.alpha = 0 224 | } 225 | 226 | UIView.addKeyframe(withRelativeStartTime: 1.0 - relativeDuration, relativeDuration: relativeDuration) { 227 | lowerViewController?.view.frame.origin.y = -self.view.bounds.size.height 228 | centerViewController?.view.frame.origin.y = -self.view.bounds.size.height + value 229 | } 230 | }) { _ in 231 | let upperViewController = self.viewController(.upper) 232 | upperViewController?.view.removeFromSuperview() 233 | centerViewController?.view.removeFromSuperview() 234 | lowerViewController?.view.removeFromSuperview() 235 | 236 | if let lowerView = lowerViewController?.view { 237 | self.addView(lowerView, to: .center) 238 | } 239 | 240 | if let centerView = centerViewController?.view { 241 | self.addView(centerView, to: .upper) 242 | } 243 | 244 | centerViewController?.scrollDelegate = nil 245 | 246 | upperViewController?.willMove(toParentViewController: self) 247 | upperViewController?.removeFromParentViewController() 248 | 249 | let nextCenterVC = lowerViewController 250 | let nextUpperVC = centerViewController 251 | self.viewControllers[.center] = nextCenterVC 252 | self.viewControllers[.upper] = nextUpperVC 253 | self.viewControllers[.lower] = nil 254 | 255 | self.currentIndexPath = self.currentIndexPath.rowPlus(1) 256 | if let newViewController = self.dataSource?.pagingViewController(self, viewControllerFor: self.currentIndexPath.rowPlus(1)) { 257 | self.addViewController(newViewController, to: .lower) 258 | self.viewControllers[.lower] = newViewController 259 | nextCenterVC?.tableView.contentInset.bottom = Const.bottomTotalSpace 260 | nextCenterVC?.tableView.scrollIndicatorInsets.bottom = Const.bottomTotalSpace 261 | } else { 262 | nextCenterVC?.tableView.contentInset.bottom = 0 263 | nextCenterVC?.tableView.scrollIndicatorInsets.bottom = 0 264 | } 265 | 266 | nextCenterVC?.scrollDelegate = self 267 | 268 | if nextUpperVC?.tableView?.contentOffset.y >= scrollView.contentSize.height - scrollView.bounds.size.height { 269 | if scrollView.contentSize.height > scrollView.bounds.size.height { 270 | nextUpperVC?.tableView?.setContentOffset(CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height), animated: false) 271 | } else { 272 | nextUpperVC?.tableView?.setContentOffset(.zero, animated: false) 273 | } 274 | } 275 | 276 | nextUpperVC?.tableView?.reloadData() 277 | self.setNeedsStatusBarAppearanceUpdate() 278 | 279 | self.setScrollsTop() 280 | self.clearAlphaView() 281 | self.clearNextHeaderView() 282 | 283 | self.isPaging = false 284 | } 285 | } 286 | 287 | func moveToPrevious(_ scrollView: UIScrollView, offset: CGPoint) { 288 | guard let _ = viewController(.upper) , viewController(.center)?.canPaging[.prev] == true else { return } 289 | 290 | scrollDirection = .top 291 | isPaging = true 292 | 293 | scrollView.setContentOffset(scrollView.contentOffset, animated: false) 294 | 295 | let upperViewController = viewController(.upper) 296 | let centerViewController = viewController(.center) 297 | UIView.animate(withDuration: Const.previousAnimationDuration, delay: 0, options: .curveLinear, animations: { 298 | upperViewController?.view.frame.origin.y = self.view.bounds.size.height 299 | centerViewController?.view.frame.origin.y = self.view.bounds.size.height + offset.y 300 | }) { finished in 301 | let lowerViewController = self.viewController(.lower) 302 | upperViewController?.view.removeFromSuperview() 303 | centerViewController?.view.removeFromSuperview() 304 | lowerViewController?.view.removeFromSuperview() 305 | 306 | if let upperView = upperViewController?.view { 307 | self.addView(upperView, to: .center) 308 | } 309 | 310 | if let centerView = centerViewController?.view { 311 | self.addView(centerView, to: .lower) 312 | } 313 | 314 | centerViewController?.scrollDelegate = nil 315 | 316 | lowerViewController?.willMove(toParentViewController: self) 317 | lowerViewController?.removeFromParentViewController() 318 | 319 | let nextCenterVC = upperViewController 320 | let nextLowerVC = centerViewController 321 | self.viewControllers[.center] = nextCenterVC 322 | self.viewControllers[.lower] = nextLowerVC 323 | self.viewControllers[.upper] = nil 324 | 325 | self.currentIndexPath = self.currentIndexPath.rowPlus(-1) 326 | if let newViewController = self.dataSource?.pagingViewController(self, viewControllerFor: self.currentIndexPath.rowPlus(-1)) { 327 | self.addViewController(newViewController, to: .upper) 328 | self.viewControllers[.upper] = newViewController 329 | } 330 | if let _ = nextLowerVC { 331 | nextCenterVC?.tableView.contentInset.bottom = Const.bottomTotalSpace 332 | nextCenterVC?.tableView.scrollIndicatorInsets.bottom = Const.bottomTotalSpace 333 | } else { 334 | nextCenterVC?.tableView.contentInset.bottom = 0 335 | nextCenterVC?.tableView.scrollIndicatorInsets.bottom = 0 336 | } 337 | 338 | nextCenterVC?.scrollDelegate = self 339 | nextLowerVC?.tableView?.reloadData() 340 | self.setNeedsStatusBarAppearanceUpdate() 341 | 342 | self.setScrollsTop() 343 | self.clearAlphaView() 344 | self.clearNextHeaderView() 345 | 346 | self.isPaging = false 347 | } 348 | } 349 | } 350 | 351 | extension HCPagingViewController: HCContentViewControllerScrollDelegate { 352 | public func contentViewController(_ viewController: HCContentViewController, scrollViewDidScroll scrollView: UIScrollView) { 353 | if isPanning { return } 354 | 355 | guard viewController == self.viewController(.center) else { return } 356 | 357 | let offset = scrollView.contentOffset 358 | let contentSize = scrollView.contentSize 359 | let scrollViewSize = scrollView.bounds.size 360 | if contentSize.height - scrollViewSize.height <= offset.y { 361 | guard let lowerViewController = self.viewController(.lower) else { return } 362 | let delta = offset.y - (contentSize.height - scrollViewSize.height) 363 | lowerViewController.view.frame.origin.y = min(0, -delta) 364 | let value: CGFloat = scrollView.bottomBounceSize 365 | if value > Const.bottomTotalSpace { 366 | let alpha = min(1, max(0, (value - Const.bottomTotalSpace) / Const.fireDistance)) 367 | alphaView.alpha = alpha 368 | } 369 | 370 | if let _ = self.nextHeaderView { 371 | } else if let view = self.viewController(.lower)?.view, 372 | let nhv = dataSource?.pagingViewController(self, nextHeaderViewFor: currentIndexPath.rowPlus(1)) { 373 | view.addLayoutSubview(nhv, andConstraints: 374 | nhv.top, nhv.right, nhv.left, nhv.height |==| HCNavigationView.height 375 | ) 376 | self.nextHeaderView = nhv 377 | } 378 | } else if offset.y < 0 { 379 | guard 380 | let upperViewController = self.viewController(.upper), 381 | let centerViewController = self.viewController(.center) 382 | else { return } 383 | let delta = max(0, -offset.y) 384 | if (currentIndexPath as NSIndexPath).row > 0 { 385 | let alpha = min(1, max(0, -offset.y / Const.fireDistance)) 386 | alphaView.alpha = alpha 387 | } 388 | clearNextHeaderView() 389 | upperViewController.view.frame.origin.y = delta 390 | centerViewController.navigationView.frame.origin.y = delta 391 | } else { 392 | viewControllers[.lower]??.view.frame.origin.y = 0 393 | viewControllers[.upper]??.view.frame.origin.y = 0 394 | viewControllers[.center]??.navigationView.frame.origin.y = 0 395 | 396 | clearAlphaView() 397 | clearNextHeaderView() 398 | } 399 | 400 | if isDragging { return } 401 | if scrollView.contentSize.height > scrollView.bounds.size.height { 402 | if offset.y < -Const.fireDistance { 403 | moveToPrevious(scrollView, offset: offset) 404 | } else if offset.y > (scrollView.contentSize.height + Const.fireDistance) - scrollView.bounds.size.height { 405 | moveToNext(scrollView, offset: offset) 406 | } 407 | } else { 408 | if offset.y < -Const.fireDistance { 409 | moveToPrevious(scrollView, offset: offset) 410 | } else if offset.y > Const.fireDistance { 411 | moveToNext(scrollView, offset: CGPoint(x: offset.x, y: offset.y + (scrollView.contentSize.height - scrollView.bounds.size.height))) 412 | } 413 | } 414 | } 415 | 416 | public func contentViewController(_ viewController: HCContentViewController, scrollViewWillBeginDragging scrollView: UIScrollView) { 417 | guard viewController == self.viewController(.center) else { return } 418 | isDragging = true 419 | } 420 | 421 | public func contentViewController(_ viewController: HCContentViewController, scrollViewDidEndDragging scrollView: UIScrollView, willDecelerate decelerate: Bool) { 422 | guard viewController == self.viewController(.center) else { return } 423 | isDragging = false 424 | } 425 | 426 | public func contentViewController(_ viewController: HCContentViewController, handlePanGesture gesture: UIPanGestureRecognizer) { 427 | guard let centerViewController = self.viewController(.center) 428 | , centerViewController == viewController && (currentIndexPath as NSIndexPath).row > 0 && viewController.canPaging[.prev] 429 | else { return } 430 | 431 | let translation = gesture.translation(in: view) 432 | let velocity = gesture.velocity(in: view) 433 | let tableView = viewController.tableView 434 | 435 | switch gesture.state { 436 | case .began: 437 | isPanning = true 438 | beginningContentOffset = (tableView?.contentOffset)! 439 | 440 | case .changed: 441 | let position = max(0, translation.y) 442 | let rudderBanding = Const.calculateRudderBanding(position, constant: 0.55, dimension: view.frame.size.height) 443 | 444 | let headerPosition = max(0, rudderBanding) 445 | if viewController.navigationView.frame.origin.y != headerPosition { 446 | viewController.navigationView.frame.origin.y = headerPosition 447 | } 448 | 449 | let tableViewOffset = beginningContentOffset.y - max(0, rudderBanding) 450 | tableView?.setContentOffset(CGPoint(x: 0, y: tableViewOffset), animated: false) 451 | 452 | self.viewController(.upper)?.view.frame.origin.y = headerPosition 453 | 454 | alphaView.alpha = min(1, (rudderBanding / Const.fireDistance)) 455 | 456 | case .cancelled, .ended: 457 | if velocity.y > 0 && translation.y > Const.fireDistance && (currentIndexPath as NSIndexPath).row > 0 { 458 | isPanning = false 459 | let rudderBanding = Const.calculateRudderBanding(max(0, translation.y), constant: 0.55, dimension: view.frame.size.height) 460 | moveToPrevious(tableView!, offset: CGPoint(x: 0, y: -rudderBanding)) 461 | } else { 462 | UIView.animate(withDuration: 0.25, animations: { 463 | viewController.navigationView.frame.origin.y = 0 464 | self.viewController(.upper)?.view.frame.origin.y = 0 465 | tableView?.setContentOffset(self.beginningContentOffset, animated: false) 466 | self.alphaView.alpha = 0 467 | }, completion: { finished in 468 | self.beginningContentOffset = .zero 469 | self.isPanning = false 470 | }) 471 | } 472 | 473 | case .failed, .possible: 474 | break 475 | } 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /HoverConversion/HCRootAnimatedTransitioning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCRootAnimatedTransitioning.swift 3 | // 4 | // Created by Taiki Suzuki on 2016/09/11. 5 | // Copyright © 2016年 marty-suzuki. All rights reserved. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | class HCRootAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning { 12 | fileprivate struct Const { 13 | static let duration: TimeInterval = 0.3 14 | static let scaling: CGFloat = 0.95 15 | } 16 | 17 | let operation: UINavigationControllerOperation 18 | fileprivate let alphaView = UIView() 19 | 20 | init(operation: UINavigationControllerOperation) { 21 | self.operation = operation 22 | super.init() 23 | } 24 | 25 | @objc func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 26 | return Const.duration 27 | } 28 | // This method can only be a nop if the transition is interactive and not a percentDriven interactive transition. 29 | @objc func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 30 | guard 31 | let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), 32 | let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) 33 | else { 34 | transitionContext.completeTransition(true) 35 | return 36 | } 37 | 38 | let containerView = transitionContext.containerView 39 | containerView.backgroundColor = .black 40 | alphaView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5) 41 | alphaView.frame = containerView.bounds 42 | 43 | switch operation { 44 | case .pop: popAnimation(transitionContext, toVC: toVC, fromVC: fromVC, containerView: containerView) 45 | case .push: pushAnimation(transitionContext, toVC: toVC, fromVC: fromVC, containerView: containerView) 46 | case .none: 47 | transitionContext.completeTransition(true) 48 | break 49 | } 50 | } 51 | 52 | fileprivate func popAnimation(_ transitionContext: UIViewControllerContextTransitioning, toVC: UIViewController, fromVC: UIViewController, containerView: UIView) { 53 | containerView.insertSubview(toVC.view, belowSubview: fromVC.view) 54 | containerView.insertSubview(alphaView, belowSubview: fromVC.view) 55 | 56 | var initialFrame: CGRect? 57 | if let rootVC = toVC as? HCRootViewController, let pagingVC = fromVC as? HCPagingViewController { 58 | let indexPath = pagingVC.currentIndexPath 59 | if rootVC.tableView?.cellForRow(at: indexPath as IndexPath) == nil { 60 | rootVC.tableView?.scrollToRow(at: indexPath as IndexPath, at: pagingVC.scrollDirection, animated: false) 61 | } 62 | 63 | if let cell = rootVC.tableView?.cellForRow(at: indexPath as IndexPath) { 64 | if let nullableVC = pagingVC.viewControllers[.center], let centeVC = nullableVC { 65 | centeVC.cellImageView.frame = cell.bounds 66 | centeVC.cellImageView.image = cell.screenshot() 67 | } 68 | 69 | if let superview = rootVC.view, 70 | let point = cell.superview?.convert(cell.frame.origin, to: superview) { 71 | var selectedCellFrame: CGRect = .zero 72 | selectedCellFrame.origin = point 73 | selectedCellFrame.size = cell.bounds.size 74 | initialFrame = selectedCellFrame 75 | } 76 | } 77 | 78 | rootVC.tableView?.selectRow(at: indexPath as IndexPath, animated: true, scrollPosition: .none) 79 | } 80 | 81 | fromVC.view.clipsToBounds = true 82 | alphaView.alpha = 1 83 | toVC.view.transform = CGAffineTransform(scaleX: Const.scaling, y: Const.scaling) 84 | 85 | UIView.animateKeyframes(withDuration: transitionDuration(using: transitionContext) , delay: 0, options: UIViewKeyframeAnimationOptions(), animations: { 86 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25) { 87 | (fromVC as? HCPagingViewController)?.viewControllers[.center]??.cellImageView.alpha = 1 88 | } 89 | UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.25) { 90 | (fromVC as? HCPagingViewController)?.containerViews[.center]?.alpha = 0 91 | } 92 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1) { 93 | if let initialFrame = initialFrame { 94 | fromVC.view.frame = initialFrame 95 | } 96 | fromVC.view.layoutIfNeeded() 97 | toVC.view.transform = CGAffineTransform.identity 98 | self.alphaView.alpha = 0 99 | } 100 | }) { finished in 101 | let canceled = transitionContext.transitionWasCancelled 102 | if canceled { 103 | toVC.view.removeFromSuperview() 104 | } else { 105 | fromVC.view.removeFromSuperview() 106 | fromVC.view.clipsToBounds = false 107 | } 108 | 109 | toVC.view.transform = CGAffineTransform.identity 110 | self.alphaView.removeFromSuperview() 111 | 112 | if let pagingVC = fromVC as? HCPagingViewController, let rootVC = toVC as? HCRootViewController { 113 | let indexPath = pagingVC.currentIndexPath 114 | rootVC.tableView?.deselectRow(at: indexPath as IndexPath, animated: true) 115 | } 116 | transitionContext.completeTransition(!canceled) 117 | } 118 | } 119 | 120 | fileprivate func pushAnimation(_ transitionContext: UIViewControllerContextTransitioning, toVC: UIViewController, fromVC: UIViewController, containerView: UIView) { 121 | containerView.addSubview(alphaView) 122 | containerView.addSubview(toVC.view) 123 | 124 | toVC.view.clipsToBounds = true 125 | 126 | var earlyAlphaAnimation = false 127 | if let rootVC = fromVC as? HCRootViewController, let pagingVC = toVC as? HCPagingViewController { 128 | let indexPath = pagingVC.currentIndexPath 129 | if let cell = rootVC.tableView?.cellForRow(at: indexPath as IndexPath) { 130 | if let nullableVC = pagingVC.viewControllers[.center], let centeVC = nullableVC { 131 | centeVC.cellImageView.frame = cell.bounds 132 | centeVC.cellImageView.image = cell.screenshot() 133 | } 134 | if let superview = rootVC.view, 135 | let point = cell.superview?.convert(cell.frame.origin, to: superview) { 136 | var selectedCellFrame: CGRect = .zero 137 | selectedCellFrame.origin = point 138 | selectedCellFrame.size = cell.bounds.size 139 | toVC.view.frame = selectedCellFrame 140 | } 141 | if let superview = rootVC.view, 142 | let point = cell.superview?.convert(cell.frame.origin, to: superview) , point.y < containerView.bounds.size.height / 3 { 143 | earlyAlphaAnimation = true 144 | } 145 | } 146 | } 147 | alphaView.alpha = 0 148 | 149 | let relativeStartTime = earlyAlphaAnimation ? 0 : 0.25 150 | UIView.animateKeyframes(withDuration: transitionDuration(using: transitionContext), delay: 0, options: UIViewKeyframeAnimationOptions(), animations: { 151 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1) { 152 | toVC.view.frame = containerView.bounds 153 | fromVC.view.transform = CGAffineTransform(scaleX: Const.scaling, y: Const.scaling) 154 | self.alphaView.alpha = 1 155 | } 156 | UIView.addKeyframe(withRelativeStartTime: relativeStartTime, relativeDuration: 0.5) { 157 | (toVC as? HCPagingViewController)?.viewControllers[.center]??.cellImageView.alpha = 0 158 | } 159 | }) { finished in 160 | let canceled = transitionContext.transitionWasCancelled 161 | if canceled { 162 | toVC.view.removeFromSuperview() 163 | toVC.view.clipsToBounds = false 164 | } 165 | 166 | self.alphaView.removeFromSuperview() 167 | fromVC.view.transform = CGAffineTransform.identity 168 | 169 | transitionContext.completeTransition(!canceled) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /HoverConversion/HCRootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCRootViewController.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class HCRootViewController: UIViewController, HCViewControllable { 12 | 13 | open var tableView: UITableView! = UITableView() 14 | open var navigatoinContainerView: UIView! = UIView() 15 | open var navigationView: HCNavigationView! = HCNavigationView() 16 | 17 | open override var title: String? { 18 | didSet { 19 | navigationView?.titleLabel.text = title 20 | } 21 | } 22 | 23 | override open func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | // Do any additional setup after loading the view. 27 | addViews() 28 | automaticallyAdjustsScrollViewInsets = false 29 | navigationController?.setNavigationBarHidden(true, animated: false) 30 | } 31 | 32 | override open func didReceiveMemoryWarning() { 33 | super.didReceiveMemoryWarning() 34 | // Dispose of any resources that can be recreated. 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /HoverConversion/HCViewContentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HCViewContentable.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MisterFusion 11 | 12 | public protocol HCViewControllable: HCNavigationViewDelegate { 13 | var navigationView: HCNavigationView! { get set } 14 | var navigatoinContainerView: UIView! { get set } 15 | var tableView: UITableView! { get set } 16 | func addViews() 17 | } 18 | 19 | extension HCViewControllable where Self: UIViewController { 20 | public func addViews() { 21 | navigationView.delegate = self 22 | view.addLayoutSubview(navigatoinContainerView, andConstraints: 23 | navigatoinContainerView.top, 24 | navigatoinContainerView.right, 25 | navigatoinContainerView.left, 26 | navigatoinContainerView.height |==| HCNavigationView.height 27 | ) 28 | 29 | navigatoinContainerView.addLayoutSubview(navigationView, andConstraints: 30 | navigationView.top, 31 | navigationView.right, 32 | navigationView.left, 33 | navigationView.bottom 34 | ) 35 | 36 | view.addLayoutSubview(tableView, andConstraints: 37 | tableView.top |==| navigatoinContainerView.bottom, 38 | tableView.right, 39 | tableView.left, 40 | tableView.bottom 41 | ) 42 | view.bringSubview(toFront: navigatoinContainerView) 43 | } 44 | 45 | public func navigationView(_ navigationView: HCNavigationView, didTapLeftButton button: UIButton) {} 46 | public func navigationView(_ navigationView: HCNavigationView, didTapRightButton button: UIButton) {} 47 | } 48 | 49 | public protocol HCViewContentable: HCViewControllable { 50 | weak var scrollDelegate: HCContentViewControllerScrollDelegate? { get set } 51 | } 52 | -------------------------------------------------------------------------------- /HoverConversion/NSIndexPath+Row.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSIndexPath+Row.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/09/11. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | // 9 | 10 | import Foundation 11 | 12 | extension IndexPath { 13 | func rowPlus(_ value: Int) -> IndexPath { 14 | return IndexPath(row: row + value, section: section) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /HoverConversion/UIScrollView+BottomBounceSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+BottomBounceSize.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/09/12. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScrollView { 12 | var bottomBounceSize: CGFloat { 13 | if bounds.size.height < contentSize.height { 14 | return contentOffset.y - (contentSize.height - bounds.size.height) 15 | } else { 16 | return contentOffset.y 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /HoverConversion/UITableViewCell+Screenshot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewCell+Screenshot.swift 3 | // HoverConversion 4 | // 5 | // Created by Taiki Suzuki on 2016/09/08. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITableViewCell { 12 | func screenshot(_ scale: CGFloat = UIScreen.main.scale) -> UIImage? { 13 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, scale) 14 | drawHierarchy(in: self.bounds, afterScreenUpdates: true) 15 | let image = UIGraphicsGetImageFromCurrentImageContext() 16 | UIGraphicsEndImageContext() 17 | return image 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 376AB1C31D8705C8001A8CD7 /* NextHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376AB1C21D8705C8001A8CD7 /* NextHeaderView.swift */; }; 11 | 376B9FC61D7C72B90009CC07 /* UserTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B9FC51D7C72B90009CC07 /* UserTimelineViewController.swift */; }; 12 | 376B9FC91D80752B0009CC07 /* TWTRAPIClient+Extra.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B9FC81D80752B0009CC07 /* TWTRAPIClient+Extra.swift */; }; 13 | 376B9FCB1D80755A0009CC07 /* UsersLookUpRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B9FCA1D80755A0009CC07 /* UsersLookUpRequest.swift */; }; 14 | 376B9FCD1D8076A10009CC07 /* TWTRRequestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B9FCC1D8076A10009CC07 /* TWTRRequestable.swift */; }; 15 | 376B9FCF1D808B250009CC07 /* StatusesUserTimelineRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376B9FCE1D808B250009CC07 /* StatusesUserTimelineRequest.swift */; }; 16 | 377008E71D3BDAC7007606E8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377008E61D3BDAC7007606E8 /* AppDelegate.swift */; }; 17 | 377008EC1D3BDAC7007606E8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 377008EA1D3BDAC7007606E8 /* Main.storyboard */; }; 18 | 377008EE1D3BDAC7007606E8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 377008ED1D3BDAC7007606E8 /* Assets.xcassets */; }; 19 | 377008F11D3BDAC7007606E8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 377008EF1D3BDAC7007606E8 /* LaunchScreen.storyboard */; }; 20 | 3799D0D81D7B462400CD339B /* TwitterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3799D0D31D7B462400CD339B /* TwitterManager.swift */; }; 21 | 3799D0DA1D7B462400CD339B /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3799D0D71D7B462400CD339B /* HomeViewController.swift */; }; 22 | 3799D0DD1D7B468600CD339B /* HomeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3799D0DB1D7B468600CD339B /* HomeTableViewCell.swift */; }; 23 | 3799D0DE1D7B468600CD339B /* HomeTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3799D0DC1D7B468600CD339B /* HomeTableViewCell.xib */; }; 24 | A19CAA499B1B6C5C98BAF960 /* Pods_HoverConversionSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 989C93E9FFF664FC7F5DAC59 /* Pods_HoverConversionSample.framework */; }; 25 | /* End PBXBuildFile section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | 178E9201DDD007E86236711A /* Pods-HoverConversionSample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HoverConversionSample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-HoverConversionSample/Pods-HoverConversionSample.debug.xcconfig"; sourceTree = ""; }; 29 | 376AB1C21D8705C8001A8CD7 /* NextHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NextHeaderView.swift; sourceTree = ""; }; 30 | 376B9FC51D7C72B90009CC07 /* UserTimelineViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserTimelineViewController.swift; sourceTree = ""; }; 31 | 376B9FC81D80752B0009CC07 /* TWTRAPIClient+Extra.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TWTRAPIClient+Extra.swift"; sourceTree = ""; }; 32 | 376B9FCA1D80755A0009CC07 /* UsersLookUpRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersLookUpRequest.swift; sourceTree = ""; }; 33 | 376B9FCC1D8076A10009CC07 /* TWTRRequestable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TWTRRequestable.swift; sourceTree = ""; }; 34 | 376B9FCE1D808B250009CC07 /* StatusesUserTimelineRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusesUserTimelineRequest.swift; sourceTree = ""; }; 35 | 377008E31D3BDAC6007606E8 /* HoverConversionSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HoverConversionSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 377008E61D3BDAC7007606E8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 377008EB1D3BDAC7007606E8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | 377008ED1D3BDAC7007606E8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | 377008F01D3BDAC7007606E8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | 377008F21D3BDAC7007606E8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 3799D0D31D7B462400CD339B /* TwitterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwitterManager.swift; sourceTree = ""; }; 42 | 3799D0D71D7B462400CD339B /* HomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 43 | 3799D0DB1D7B468600CD339B /* HomeTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeTableViewCell.swift; sourceTree = ""; }; 44 | 3799D0DC1D7B468600CD339B /* HomeTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = HomeTableViewCell.xib; sourceTree = ""; }; 45 | 989C93E9FFF664FC7F5DAC59 /* Pods_HoverConversionSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HoverConversionSample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | BCBB46B066D4D58B7568C17F /* Pods-HoverConversionSample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HoverConversionSample.release.xcconfig"; path = "Pods/Target Support Files/Pods-HoverConversionSample/Pods-HoverConversionSample.release.xcconfig"; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 377008E01D3BDAC6007606E8 /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | A19CAA499B1B6C5C98BAF960 /* Pods_HoverConversionSample.framework in Frameworks */, 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXFrameworksBuildPhase section */ 59 | 60 | /* Begin PBXGroup section */ 61 | 376B9FC71D80752B0009CC07 /* Request */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 376B9FC81D80752B0009CC07 /* TWTRAPIClient+Extra.swift */, 65 | 376B9FCC1D8076A10009CC07 /* TWTRRequestable.swift */, 66 | 376B9FCA1D80755A0009CC07 /* UsersLookUpRequest.swift */, 67 | 376B9FCE1D808B250009CC07 /* StatusesUserTimelineRequest.swift */, 68 | ); 69 | path = Request; 70 | sourceTree = ""; 71 | }; 72 | 377008DA1D3BDAC6007606E8 = { 73 | isa = PBXGroup; 74 | children = ( 75 | 377008E51D3BDAC7007606E8 /* HoverConversionSample */, 76 | 377008E41D3BDAC6007606E8 /* Products */, 77 | 77C3C9ACBF70123F6F82AA62 /* Pods */, 78 | 3BDC8B7EA0C8D8C60EA01922 /* Frameworks */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 377008E41D3BDAC6007606E8 /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 377008E31D3BDAC6007606E8 /* HoverConversionSample.app */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 377008E51D3BDAC7007606E8 /* HoverConversionSample */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 377008E61D3BDAC7007606E8 /* AppDelegate.swift */, 94 | 377008EA1D3BDAC7007606E8 /* Main.storyboard */, 95 | 3799D0D11D7B462400CD339B /* Cell */, 96 | 3799D0D21D7B462400CD339B /* Manager */, 97 | 376B9FC71D80752B0009CC07 /* Request */, 98 | 3799D0D51D7B462400CD339B /* View */, 99 | 3799D0D61D7B462400CD339B /* ViewController */, 100 | 377008ED1D3BDAC7007606E8 /* Assets.xcassets */, 101 | 377008EF1D3BDAC7007606E8 /* LaunchScreen.storyboard */, 102 | 377008F21D3BDAC7007606E8 /* Info.plist */, 103 | ); 104 | path = HoverConversionSample; 105 | sourceTree = ""; 106 | }; 107 | 3799D0D11D7B462400CD339B /* Cell */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 3799D0DB1D7B468600CD339B /* HomeTableViewCell.swift */, 111 | 3799D0DC1D7B468600CD339B /* HomeTableViewCell.xib */, 112 | ); 113 | path = Cell; 114 | sourceTree = ""; 115 | }; 116 | 3799D0D21D7B462400CD339B /* Manager */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 3799D0D31D7B462400CD339B /* TwitterManager.swift */, 120 | ); 121 | path = Manager; 122 | sourceTree = ""; 123 | }; 124 | 3799D0D51D7B462400CD339B /* View */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 376AB1C21D8705C8001A8CD7 /* NextHeaderView.swift */, 128 | ); 129 | path = View; 130 | sourceTree = ""; 131 | }; 132 | 3799D0D61D7B462400CD339B /* ViewController */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 3799D0D71D7B462400CD339B /* HomeViewController.swift */, 136 | 376B9FC51D7C72B90009CC07 /* UserTimelineViewController.swift */, 137 | ); 138 | path = ViewController; 139 | sourceTree = ""; 140 | }; 141 | 3BDC8B7EA0C8D8C60EA01922 /* Frameworks */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 989C93E9FFF664FC7F5DAC59 /* Pods_HoverConversionSample.framework */, 145 | ); 146 | name = Frameworks; 147 | sourceTree = ""; 148 | }; 149 | 77C3C9ACBF70123F6F82AA62 /* Pods */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 178E9201DDD007E86236711A /* Pods-HoverConversionSample.debug.xcconfig */, 153 | BCBB46B066D4D58B7568C17F /* Pods-HoverConversionSample.release.xcconfig */, 154 | ); 155 | name = Pods; 156 | sourceTree = ""; 157 | }; 158 | /* End PBXGroup section */ 159 | 160 | /* Begin PBXNativeTarget section */ 161 | 377008E21D3BDAC6007606E8 /* HoverConversionSample */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = 377008F51D3BDAC7007606E8 /* Build configuration list for PBXNativeTarget "HoverConversionSample" */; 164 | buildPhases = ( 165 | 60CE7C4347D44804863A8C57 /* [CP] Check Pods Manifest.lock */, 166 | 377008DF1D3BDAC6007606E8 /* Sources */, 167 | 377008E01D3BDAC6007606E8 /* Frameworks */, 168 | 377008E11D3BDAC6007606E8 /* Resources */, 169 | 3799D0CC1D7AB6B300CD339B /* ShellScript */, 170 | E091ECB7B854A4215F480250 /* [CP] Embed Pods Frameworks */, 171 | 90CB03264FD0C6DD77F59633 /* [CP] Copy Pods Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | ); 177 | name = HoverConversionSample; 178 | productName = HoverConversionSample; 179 | productReference = 377008E31D3BDAC6007606E8 /* HoverConversionSample.app */; 180 | productType = "com.apple.product-type.application"; 181 | }; 182 | /* End PBXNativeTarget section */ 183 | 184 | /* Begin PBXProject section */ 185 | 377008DB1D3BDAC6007606E8 /* Project object */ = { 186 | isa = PBXProject; 187 | attributes = { 188 | LastSwiftUpdateCheck = 0730; 189 | LastUpgradeCheck = 0800; 190 | ORGANIZATIONNAME = "szk-atmosphere"; 191 | TargetAttributes = { 192 | 377008E21D3BDAC6007606E8 = { 193 | CreatedOnToolsVersion = 7.3.1; 194 | LastSwiftMigration = 0800; 195 | }; 196 | }; 197 | }; 198 | buildConfigurationList = 377008DE1D3BDAC6007606E8 /* Build configuration list for PBXProject "HoverConversionSample" */; 199 | compatibilityVersion = "Xcode 3.2"; 200 | developmentRegion = English; 201 | hasScannedForEncodings = 0; 202 | knownRegions = ( 203 | en, 204 | Base, 205 | ); 206 | mainGroup = 377008DA1D3BDAC6007606E8; 207 | productRefGroup = 377008E41D3BDAC6007606E8 /* Products */; 208 | projectDirPath = ""; 209 | projectRoot = ""; 210 | targets = ( 211 | 377008E21D3BDAC6007606E8 /* HoverConversionSample */, 212 | ); 213 | }; 214 | /* End PBXProject section */ 215 | 216 | /* Begin PBXResourcesBuildPhase section */ 217 | 377008E11D3BDAC6007606E8 /* Resources */ = { 218 | isa = PBXResourcesBuildPhase; 219 | buildActionMask = 2147483647; 220 | files = ( 221 | 377008F11D3BDAC7007606E8 /* LaunchScreen.storyboard in Resources */, 222 | 377008EE1D3BDAC7007606E8 /* Assets.xcassets in Resources */, 223 | 3799D0DE1D7B468600CD339B /* HomeTableViewCell.xib in Resources */, 224 | 377008EC1D3BDAC7007606E8 /* Main.storyboard in Resources */, 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | }; 228 | /* End PBXResourcesBuildPhase section */ 229 | 230 | /* Begin PBXShellScriptBuildPhase section */ 231 | 3799D0CC1D7AB6B300CD339B /* ShellScript */ = { 232 | isa = PBXShellScriptBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | inputPaths = ( 237 | ); 238 | outputPaths = ( 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | shellPath = /bin/sh; 242 | shellScript = "\"${PODS_ROOT}/Fabric/run\" 17f9b693790022f676a0812ed5b69bf3f93d01ff e63261495fad4e59db4746e04a9bedd867c4ef814a71a5d3fd4d6e5478288a20"; 243 | }; 244 | 60CE7C4347D44804863A8C57 /* [CP] Check Pods Manifest.lock */ = { 245 | isa = PBXShellScriptBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | inputPaths = ( 250 | ); 251 | name = "[CP] Check Pods Manifest.lock"; 252 | outputPaths = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | shellPath = /bin/sh; 256 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; 257 | showEnvVarsInLog = 0; 258 | }; 259 | 90CB03264FD0C6DD77F59633 /* [CP] Copy Pods Resources */ = { 260 | isa = PBXShellScriptBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | ); 264 | inputPaths = ( 265 | ); 266 | name = "[CP] Copy Pods Resources"; 267 | outputPaths = ( 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | shellPath = /bin/sh; 271 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-HoverConversionSample/Pods-HoverConversionSample-resources.sh\"\n"; 272 | showEnvVarsInLog = 0; 273 | }; 274 | E091ECB7B854A4215F480250 /* [CP] Embed Pods Frameworks */ = { 275 | isa = PBXShellScriptBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | ); 279 | inputPaths = ( 280 | ); 281 | name = "[CP] Embed Pods Frameworks"; 282 | outputPaths = ( 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | shellPath = /bin/sh; 286 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-HoverConversionSample/Pods-HoverConversionSample-frameworks.sh\"\n"; 287 | showEnvVarsInLog = 0; 288 | }; 289 | /* End PBXShellScriptBuildPhase section */ 290 | 291 | /* Begin PBXSourcesBuildPhase section */ 292 | 377008DF1D3BDAC6007606E8 /* Sources */ = { 293 | isa = PBXSourcesBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | 376B9FCF1D808B250009CC07 /* StatusesUserTimelineRequest.swift in Sources */, 297 | 376B9FC61D7C72B90009CC07 /* UserTimelineViewController.swift in Sources */, 298 | 376AB1C31D8705C8001A8CD7 /* NextHeaderView.swift in Sources */, 299 | 376B9FC91D80752B0009CC07 /* TWTRAPIClient+Extra.swift in Sources */, 300 | 3799D0DD1D7B468600CD339B /* HomeTableViewCell.swift in Sources */, 301 | 3799D0DA1D7B462400CD339B /* HomeViewController.swift in Sources */, 302 | 376B9FCB1D80755A0009CC07 /* UsersLookUpRequest.swift in Sources */, 303 | 377008E71D3BDAC7007606E8 /* AppDelegate.swift in Sources */, 304 | 376B9FCD1D8076A10009CC07 /* TWTRRequestable.swift in Sources */, 305 | 3799D0D81D7B462400CD339B /* TwitterManager.swift in Sources */, 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | }; 309 | /* End PBXSourcesBuildPhase section */ 310 | 311 | /* Begin PBXVariantGroup section */ 312 | 377008EA1D3BDAC7007606E8 /* Main.storyboard */ = { 313 | isa = PBXVariantGroup; 314 | children = ( 315 | 377008EB1D3BDAC7007606E8 /* Base */, 316 | ); 317 | name = Main.storyboard; 318 | sourceTree = ""; 319 | }; 320 | 377008EF1D3BDAC7007606E8 /* LaunchScreen.storyboard */ = { 321 | isa = PBXVariantGroup; 322 | children = ( 323 | 377008F01D3BDAC7007606E8 /* Base */, 324 | ); 325 | name = LaunchScreen.storyboard; 326 | sourceTree = ""; 327 | }; 328 | /* End PBXVariantGroup section */ 329 | 330 | /* Begin XCBuildConfiguration section */ 331 | 377008F31D3BDAC7007606E8 /* Debug */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ALWAYS_SEARCH_USER_PATHS = NO; 335 | CLANG_ANALYZER_NONNULL = YES; 336 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 337 | CLANG_CXX_LIBRARY = "libc++"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_WARN_BOOL_CONVERSION = YES; 341 | CLANG_WARN_CONSTANT_CONVERSION = YES; 342 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 343 | CLANG_WARN_EMPTY_BODY = YES; 344 | CLANG_WARN_ENUM_CONVERSION = YES; 345 | CLANG_WARN_INFINITE_RECURSION = YES; 346 | CLANG_WARN_INT_CONVERSION = YES; 347 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 348 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 352 | COPY_PHASE_STRIP = NO; 353 | DEBUG_INFORMATION_FORMAT = dwarf; 354 | ENABLE_STRICT_OBJC_MSGSEND = YES; 355 | ENABLE_TESTABILITY = YES; 356 | GCC_C_LANGUAGE_STANDARD = gnu99; 357 | GCC_DYNAMIC_NO_PIC = NO; 358 | GCC_NO_COMMON_BLOCKS = YES; 359 | GCC_OPTIMIZATION_LEVEL = 0; 360 | GCC_PREPROCESSOR_DEFINITIONS = ( 361 | "DEBUG=1", 362 | "$(inherited)", 363 | ); 364 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 365 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 366 | GCC_WARN_UNDECLARED_SELECTOR = YES; 367 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 368 | GCC_WARN_UNUSED_FUNCTION = YES; 369 | GCC_WARN_UNUSED_VARIABLE = YES; 370 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 371 | MTL_ENABLE_DEBUG_INFO = YES; 372 | ONLY_ACTIVE_ARCH = YES; 373 | SDKROOT = iphoneos; 374 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 375 | TARGETED_DEVICE_FAMILY = "1,2"; 376 | }; 377 | name = Debug; 378 | }; 379 | 377008F41D3BDAC7007606E8 /* Release */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ALWAYS_SEARCH_USER_PATHS = NO; 383 | CLANG_ANALYZER_NONNULL = YES; 384 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 385 | CLANG_CXX_LIBRARY = "libc++"; 386 | CLANG_ENABLE_MODULES = YES; 387 | CLANG_ENABLE_OBJC_ARC = YES; 388 | CLANG_WARN_BOOL_CONVERSION = YES; 389 | CLANG_WARN_CONSTANT_CONVERSION = YES; 390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 396 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 397 | CLANG_WARN_UNREACHABLE_CODE = YES; 398 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 399 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 400 | COPY_PHASE_STRIP = NO; 401 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 402 | ENABLE_NS_ASSERTIONS = NO; 403 | ENABLE_STRICT_OBJC_MSGSEND = YES; 404 | GCC_C_LANGUAGE_STANDARD = gnu99; 405 | GCC_NO_COMMON_BLOCKS = YES; 406 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 407 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 408 | GCC_WARN_UNDECLARED_SELECTOR = YES; 409 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 410 | GCC_WARN_UNUSED_FUNCTION = YES; 411 | GCC_WARN_UNUSED_VARIABLE = YES; 412 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 413 | MTL_ENABLE_DEBUG_INFO = NO; 414 | SDKROOT = iphoneos; 415 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 416 | TARGETED_DEVICE_FAMILY = "1,2"; 417 | VALIDATE_PRODUCT = YES; 418 | }; 419 | name = Release; 420 | }; 421 | 377008F61D3BDAC7007606E8 /* Debug */ = { 422 | isa = XCBuildConfiguration; 423 | baseConfigurationReference = 178E9201DDD007E86236711A /* Pods-HoverConversionSample.debug.xcconfig */; 424 | buildSettings = { 425 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; 426 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 427 | INFOPLIST_FILE = HoverConversionSample/Info.plist; 428 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 429 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 430 | ONLY_ACTIVE_ARCH = YES; 431 | PRODUCT_BUNDLE_IDENTIFIER = "szk-atmosphere.HoverConversionSample"; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | SWIFT_VERSION = 3.0; 434 | }; 435 | name = Debug; 436 | }; 437 | 377008F71D3BDAC7007606E8 /* Release */ = { 438 | isa = XCBuildConfiguration; 439 | baseConfigurationReference = BCBB46B066D4D58B7568C17F /* Pods-HoverConversionSample.release.xcconfig */; 440 | buildSettings = { 441 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | INFOPLIST_FILE = HoverConversionSample/Info.plist; 444 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 445 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 446 | ONLY_ACTIVE_ARCH = YES; 447 | PRODUCT_BUNDLE_IDENTIFIER = "szk-atmosphere.HoverConversionSample"; 448 | PRODUCT_NAME = "$(TARGET_NAME)"; 449 | SWIFT_VERSION = 3.0; 450 | }; 451 | name = Release; 452 | }; 453 | /* End XCBuildConfiguration section */ 454 | 455 | /* Begin XCConfigurationList section */ 456 | 377008DE1D3BDAC6007606E8 /* Build configuration list for PBXProject "HoverConversionSample" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | 377008F31D3BDAC7007606E8 /* Debug */, 460 | 377008F41D3BDAC7007606E8 /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | 377008F51D3BDAC7007606E8 /* Build configuration list for PBXNativeTarget "HoverConversionSample" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | 377008F61D3BDAC7007606E8 /* Debug */, 469 | 377008F71D3BDAC7007606E8 /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | /* End XCConfigurationList section */ 475 | }; 476 | rootObject = 377008DB1D3BDAC6007606E8 /* Project object */; 477 | } 478 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // HoverConversionSample 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Fabric 11 | import TwitterKit 12 | import TouchVisualizer 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | var window: UIWindow? 18 | 19 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 20 | // Override point for customization after application launch. 21 | Fabric.with([Twitter.self]) 22 | Visualizer.start() 23 | return true 24 | } 25 | 26 | func applicationWillResignActive(_ application: UIApplication) { 27 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 28 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 29 | } 30 | 31 | func applicationDidEnterBackground(_ application: UIApplication) { 32 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 33 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 34 | Visualizer.stop() 35 | } 36 | 37 | func applicationWillEnterForeground(_ application: UIApplication) { 38 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 39 | Visualizer.start() 40 | } 41 | 42 | func applicationDidBecomeActive(_ application: UIApplication) { 43 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 44 | Visualizer.start() 45 | } 46 | 47 | func applicationWillTerminate(_ application: UIApplication) { 48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 49 | Visualizer.stop() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Cell/HomeTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeTableViewCell.swift 3 | // HoverConversionSample 4 | // 5 | // Created by Taiki Suzuki on 2016/09/04. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import TwitterKit 11 | 12 | class HomeTableViewCell: UITableViewCell, IconImageViewLoadable { 13 | static let Height: CGFloat = 80 14 | 15 | @IBOutlet weak var iconImageView: UIImageView! 16 | @IBOutlet weak var userNameLabel: UILabel! 17 | @IBOutlet weak var screenNameLabel: UILabel! 18 | @IBOutlet weak var latestTweetLabel: UILabel! 19 | @IBOutlet weak var timestampLabel: UILabel! 20 | 21 | var userValue: (TWTRUser, TWTRTweet)? { 22 | didSet { 23 | guard let value = userValue else { return } 24 | let user = value.0 25 | if let url = URL(string: user.profileImageLargeURL) { 26 | loadImage(url) 27 | } 28 | userNameLabel.text = user.name 29 | screenNameLabel.text = "@" + user.screenName 30 | let tweet = value.1 31 | latestTweetLabel.text = tweet.text 32 | timestampLabel.text = tweet.createdAt.description 33 | } 34 | } 35 | 36 | override func awakeFromNib() { 37 | super.awakeFromNib() 38 | // Initialization code 39 | iconImageView.layer.cornerRadius = iconImageView.bounds.size.height / 2 40 | iconImageView.layer.borderWidth = 1 41 | iconImageView.layer.borderColor = UIColor.lightGray.cgColor 42 | iconImageView.layer.masksToBounds = true 43 | } 44 | 45 | override func setSelected(_ selected: Bool, animated: Bool) { 46 | super.setSelected(selected, animated: animated) 47 | 48 | // Configure the view for the selected state 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Cell/HomeTableViewCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 36 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | Fabric 24 | 25 | APIKey 26 | 17f9b693790022f676a0812ed5b69bf3f93d01ff 27 | Kits 28 | 29 | 30 | KitInfo 31 | 32 | consumerKey 33 | Tqg8V7zKdLsQyEl5V01o80kLj 34 | consumerSecret 35 | cMRzcgG08gsYahpLf6RAltA91WvFAQJvGRJ5wi2OK9U5JkYmKi 36 | 37 | KitName 38 | Twitter 39 | 40 | 41 | 42 | LSRequiresIPhoneOS 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Manager/TwitterManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterManager.swift 3 | // HoverConversionSample 4 | // 5 | // Created by Taiki Suzuki on 2016/09/03. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | class TwitterManager { 13 | fileprivate let screenNames: [String] = [ 14 | "tim_cook", 15 | "SwiftLang", 16 | "BacktotheFuture", 17 | "realmikefox", 18 | "marty_suzuki" 19 | ] 20 | 21 | fileprivate(set) var tweets: [String : [TWTRTweet]] = [:] 22 | fileprivate(set) var users: [TWTRUser] = [] 23 | 24 | fileprivate lazy var client = TWTRAPIClient() 25 | 26 | func sortUsers() { 27 | let result = users.flatMap { user -> (TWTRUser, TWTRTweet)? in 28 | guard let tweet = tweets[user.screenName]?.first else { 29 | return nil 30 | } 31 | return (user, tweet) 32 | } 33 | let sortedResult = result.sorted { $0.0.1.createdAt.timeIntervalSince1970 > $0.1.1.createdAt.timeIntervalSince1970 } 34 | users = sortedResult.flatMap { $0.0 } 35 | } 36 | 37 | func fetchUsersTimeline(_ completion: @escaping (() -> ())) { 38 | let group = DispatchGroup() 39 | screenNames.forEach { 40 | group.enter() 41 | fetchUserTimeline(screenName: $0) { 42 | group.leave() 43 | } 44 | } 45 | group.notify(queue: DispatchQueue.main) { 46 | completion() 47 | } 48 | } 49 | 50 | func fetchUserTimeline(screenName: String, completion: @escaping (() -> ())) { 51 | let request = StatusesUserTimelineRequest(screenName: screenName, maxId: nil, count: 1) 52 | client.sendTwitterRequest(request) { [weak self] in 53 | switch $0.result { 54 | case .success(let tweets): 55 | guard let userTweets = self?.tweets[screenName] else { 56 | self?.tweets[screenName] = tweets 57 | completion() 58 | return 59 | } 60 | self?.tweets[screenName] = Array([userTweets, tweets].joined()) 61 | case .failure(let error): 62 | print(error) 63 | } 64 | completion() 65 | } 66 | } 67 | 68 | func fetchUsers(_ completion: @escaping (() -> ())) { 69 | let request = UsersLookUpRequest(screenNames: screenNames) 70 | client.sendTwitterRequest(request) { [weak self] in 71 | switch $0.result { 72 | case .success(let users): 73 | self?.users = users 74 | case .failure(let error): 75 | print(error) 76 | } 77 | completion() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Request/StatusesUserTimelineRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusesUserTimelineRequest.swift 3 | // HoverConversionSample 4 | // 5 | // Created by 鈴木大貴 on 2016/09/08. 6 | // Copyright © 2016年 szk-atmosphere. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | struct StatusesUserTimelineRequest: TWTRGetRequestable { 13 | typealias ResponseType = [TWTRTweet] 14 | typealias ParseResultType = [[String : NSObject]] 15 | 16 | let path: String = "/1.1/statuses/user_timeline.json" 17 | 18 | let screenName: String 19 | let maxId: String? 20 | let count: Int? 21 | 22 | var parameters: [AnyHashable: Any]? { 23 | var parameters: [AnyHashable: Any] = [ 24 | "screen_name" : screenName 25 | ] 26 | if let maxId = maxId { 27 | parameters["max_id"] = maxId 28 | } 29 | if let count = count { 30 | parameters["count"] = String(count) 31 | } 32 | return parameters 33 | } 34 | 35 | static func decode(_ data: Data) -> TWTRResult { 36 | switch UsersLookUpRequest.parseData(data) { 37 | case .success(let parsedData): 38 | return .success(parsedData.flatMap { TWTRTweet(jsonDictionary: $0) }) 39 | case .failure(let error): 40 | return .failure(error) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Request/TWTRAPIClient+Extra.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TWTRAPIClient+Extra.swift 3 | // HoverConversionSample 4 | // 5 | // Created by Taiki Suzuki on 2016/09/04. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | enum TWTRResult { 13 | case success(T) 14 | case failure(NSError) 15 | } 16 | 17 | struct TWTRResponse { 18 | let request: URLRequest? 19 | let response: HTTPURLResponse? 20 | let data: Data? 21 | let result: TWTRResult 22 | } 23 | 24 | enum TWTRHTTPMethod: String { 25 | case GET = "GET" 26 | } 27 | 28 | extension TWTRAPIClient { 29 | func sendTwitterRequest(_ request: T, completion: @escaping (TWTRResponse) -> ()) { 30 | guard let URL = request.URL else { 31 | let error = NSError(domain: TWTRAPIErrorDomain, code: -9999, userInfo: nil) 32 | completion(TWTRResponse(request: nil, response: nil, data: nil, result: .failure(error))) 33 | return 34 | } 35 | let absoluteString = URL.absoluteString 36 | var error: NSError? 37 | let request = urlRequest(withMethod: request.method.rawValue, url: absoluteString, parameters: request.parameters, error: &error) 38 | if let error = error { 39 | completion(TWTRResponse(request: request, response: nil, data: nil, result: .failure(error))) 40 | return 41 | } 42 | sendTwitterRequest(request) { 43 | let result: TWTRResult 44 | if let error = $0.2 { 45 | result = .failure(error as NSError) 46 | } else if let data = $0.1 { 47 | switch T.decode(data) { 48 | case .success(let decodeData): 49 | result = .success(decodeData) 50 | case .failure(let error): 51 | result = .failure(error) 52 | } 53 | } else { 54 | result = .failure(NSError(domain: TWTRAPIErrorDomain, code: -9999, userInfo: nil)) 55 | } 56 | completion(TWTRResponse(request: request, response: $0.0 as? HTTPURLResponse, data: $0.1, result: result)) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Request/TWTRRequestable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TWTRRequestable.swift 3 | // HoverConversionSample 4 | // 5 | // Created by 鈴木大貴 on 2016/09/08. 6 | // Copyright © 2016年 szk-atmosphere. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | protocol TWTRRequestable { 13 | associatedtype ResponseType 14 | associatedtype ParseResultType 15 | var method: TWTRHTTPMethod { get } 16 | var baseURL: Foundation.URL? { get } 17 | var path: String { get } 18 | var URL: Foundation.URL? { get } 19 | var parameters: [AnyHashable: Any]? { get } 20 | static func parseData(_ data: Data) -> TWTRResult 21 | static func decode(_ data: Data) -> TWTRResult 22 | } 23 | 24 | extension TWTRRequestable { 25 | var baseURL: Foundation.URL? { 26 | return Foundation.URL(string: "https://api.twitter.com") 27 | } 28 | 29 | var URL: Foundation.URL? { 30 | return Foundation.URL(string: path, relativeTo: baseURL) 31 | } 32 | 33 | static func parseData(_ data: Data) -> TWTRResult { 34 | do { 35 | let anyObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) 36 | guard let object = anyObject as? ParseResultType else { 37 | return .failure(NSError(domain: TWTRAPIErrorDomain, code: -9999, userInfo: nil)) 38 | } 39 | return .success(object) 40 | } catch let error as NSError { 41 | return .failure(error) 42 | } 43 | } 44 | } 45 | 46 | protocol TWTRGetRequestable: TWTRRequestable {} 47 | extension TWTRGetRequestable { 48 | var method: TWTRHTTPMethod { 49 | return .GET 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/Request/UsersLookUpRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersLookUpRequest.swift 3 | // HoverConversionSample 4 | // 5 | // Created by 鈴木大貴 on 2016/09/08. 6 | // Copyright © 2016年 szk-atmosphere. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TwitterKit 11 | 12 | struct UsersLookUpRequest: TWTRGetRequestable { 13 | typealias ResponseType = [TWTRUser] 14 | typealias ParseResultType = [[String : NSObject]] 15 | 16 | let path: String = "/1.1/users/lookup.json" 17 | 18 | let screenNames: [String] 19 | 20 | var parameters: [AnyHashable: Any]? { 21 | let screenNameValue: String = screenNames.joined(separator: ",") 22 | let parameters: [AnyHashable: Any] = [ 23 | "screen_name" : screenNameValue, 24 | "include_entities" : "true" 25 | ] 26 | return parameters 27 | } 28 | 29 | static func decode(_ data: Data) -> TWTRResult { 30 | switch UsersLookUpRequest.parseData(data) { 31 | case .success(let parsedData): 32 | return .success(parsedData.flatMap { TWTRUser(jsonDictionary: $0) }) 33 | case .failure(let error): 34 | return .failure(error) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/View/NextHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NextHeaderView.swift 3 | // HoverConversionSample 4 | // 5 | // Created by 鈴木大貴 on 2016/09/13. 6 | // Copyright © 2016年 szk-atmosphere. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import HoverConversion 11 | import TwitterKit 12 | import MisterFusion 13 | 14 | protocol IconImageViewLoadable { 15 | var iconImageView: UIImageView! { get } 16 | func loadImage(_ url: URL) 17 | } 18 | 19 | extension IconImageViewLoadable { 20 | func loadImage(_ url: URL) { 21 | DispatchQueue.global().async { 22 | guard let data = try? Data(contentsOf: url) else { return } 23 | DispatchQueue.main.async { 24 | guard let image = UIImage(data: data) else { return } 25 | self.iconImageView.image = image 26 | } 27 | } 28 | } 29 | } 30 | 31 | class NextHeaderView: HCNextHeaderView, IconImageViewLoadable { 32 | var user: TWTRUser? { 33 | didSet { 34 | guard let user = user else { return } 35 | setupViews() 36 | 37 | titleLabel.numberOfLines = 2 38 | let attributedText = NSMutableAttributedString() 39 | attributedText.append(NSAttributedString(string: user.name + "\n", attributes: [ 40 | NSFontAttributeName : UIFont.boldSystemFont(ofSize: 18), 41 | NSForegroundColorAttributeName : UIColor.white 42 | ])) 43 | attributedText.append(NSAttributedString(string: "@" + user.screenName, attributes: [ 44 | NSFontAttributeName : UIFont.systemFont(ofSize: 16), 45 | NSForegroundColorAttributeName : UIColor(white: 1, alpha: 0.6) 46 | ])) 47 | titleLabel.attributedText = attributedText 48 | 49 | guard let url = URL(string: user.profileImageLargeURL) else { return } 50 | loadImage(url) 51 | } 52 | } 53 | 54 | let iconImageView: UIImageView! = UIImageView(frame: .zero) 55 | let titleLabel: UILabel = UILabel(frame: .zero) 56 | 57 | init() { 58 | super.init(frame: .zero) 59 | } 60 | 61 | required init?(coder aDecoder: NSCoder) { 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | override func layoutSubviews() { 66 | super.layoutSubviews() 67 | iconImageView.layer.cornerRadius = iconImageView.bounds.size.height / 2 68 | } 69 | 70 | fileprivate func setupViews() { 71 | addLayoutSubview(iconImageView, andConstraints: 72 | iconImageView.top |+| 8, 73 | iconImageView.left |+| 8, 74 | iconImageView.bottom |-| 8, 75 | iconImageView.width |==| iconImageView.height 76 | ) 77 | 78 | iconImageView.layer.borderWidth = 1 79 | iconImageView.layer.borderColor = UIColor.lightGray.cgColor 80 | iconImageView.layer.masksToBounds = true 81 | 82 | addLayoutSubview(titleLabel, andConstraints: 83 | titleLabel.top |+| 4, 84 | titleLabel.right |-| 4, 85 | titleLabel.left |==| iconImageView.right |+| 16, 86 | titleLabel.bottom |-| 4 87 | ) 88 | 89 | backgroundColor = UIColor(red: 0.25, green: 0.25, blue: 0.25, alpha: 1) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/ViewController/HomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewController.swift 3 | // HoverConversionSample 4 | // 5 | // Created by Taiki Suzuki on 2016/07/18. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import HoverConversion 11 | import TwitterKit 12 | 13 | class HomeViewController: HCRootViewController { 14 | 15 | let twitterManager = TwitterManager() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | // Do any additional setup after loading the view, typically from a nib. 20 | navigationView.backgroundColor = UIColor(red: 85 / 255, green: 172 / 255, blue: 238 / 255, alpha: 1) 21 | navigationView.titleLabel.textColor = .white 22 | tableView.delegate = self 23 | tableView.dataSource = self 24 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell") 25 | tableView.register(UINib(nibName: "HomeTableViewCell", bundle: nil), forCellReuseIdentifier: "HomeTableViewCell") 26 | title = "Following List" 27 | } 28 | 29 | override var preferredStatusBarStyle: UIStatusBarStyle { 30 | return .lightContent 31 | } 32 | 33 | override func viewDidAppear(_ animated: Bool) { 34 | super.viewDidAppear(animated) 35 | 36 | let group = DispatchGroup() 37 | group.enter() 38 | twitterManager.fetchUsersTimeline { 39 | group.leave() 40 | } 41 | group.enter() 42 | twitterManager.fetchUsers { 43 | group.leave() 44 | } 45 | group.notify(queue: DispatchQueue.main) { 46 | self.twitterManager.sortUsers() 47 | self.tableView.reloadData() 48 | } 49 | } 50 | override func didReceiveMemoryWarning() { 51 | super.didReceiveMemoryWarning() 52 | // Dispose of any resources that can be recreated. 53 | } 54 | 55 | fileprivate func showPagingViewContoller(indexPath: IndexPath) { 56 | let vc = HCPagingViewController(indexPath: indexPath) 57 | vc.dataSource = self 58 | navigationController?.pushViewController(vc, animated: true) 59 | } 60 | } 61 | 62 | extension HomeViewController: UITableViewDataSource { 63 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 64 | if twitterManager.tweets.count == twitterManager.users.count { 65 | return twitterManager.users.count 66 | } 67 | return 0 68 | } 69 | 70 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 71 | let user = twitterManager.users[(indexPath as NSIndexPath).row] 72 | guard let tweet = twitterManager.tweets[user.screenName]?.first else { 73 | return tableView.dequeueReusableCell(withIdentifier: "UITableViewCell")! 74 | } 75 | let cell = tableView.dequeueReusableCell(withIdentifier: "HomeTableViewCell") as! HomeTableViewCell 76 | cell.userValue = (user, tweet) 77 | return cell 78 | } 79 | } 80 | 81 | extension HomeViewController: UITableViewDelegate { 82 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 83 | return HomeTableViewCell.Height 84 | } 85 | 86 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 87 | tableView.deselectRow(at: indexPath, animated: false) 88 | showPagingViewContoller(indexPath: indexPath) 89 | } 90 | } 91 | 92 | extension HomeViewController: HCPagingViewControllerDataSource { 93 | func pagingViewController(_ viewController: HCPagingViewController, viewControllerFor indexPath: IndexPath) -> HCContentViewController? { 94 | guard 0 <= indexPath.row && indexPath.row < twitterManager.users.count else { return nil } 95 | let vc = UserTimelineViewController() 96 | vc.user = twitterManager.users[indexPath.row] 97 | return vc 98 | } 99 | 100 | func pagingViewController(_ viewController: HCPagingViewController, nextHeaderViewFor indexPath: IndexPath) -> HCNextHeaderView? { 101 | guard 0 <= indexPath.row && indexPath.row < twitterManager.users.count else { return nil } 102 | let view = NextHeaderView() 103 | view.user = twitterManager.users[indexPath.row] 104 | return view 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /HoverConversionSample/HoverConversionSample/ViewController/UserTimelineViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserTimelineViewController.swift 3 | // HoverConversionSample 4 | // 5 | // Created by Taiki Suzuki on 2016/09/05. 6 | // Copyright © 2016年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import HoverConversion 11 | import TwitterKit 12 | 13 | class UserTimelineViewController: HCContentViewController { 14 | var user: TWTRUser? 15 | 16 | fileprivate var tweets: [TWTRTweet] = [] 17 | fileprivate var hasNext = true 18 | fileprivate let client = TWTRAPIClient() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | // Do any additional setup after loading the view. 23 | navigationView.backgroundColor = UIColor(red: 85 / 255, green: 172 / 255, blue: 238 / 255, alpha: 1) 24 | 25 | if let user = user { 26 | navigationView.titleLabel.numberOfLines = 2 27 | let attributedText = NSMutableAttributedString() 28 | attributedText.append(NSAttributedString(string: user.name + "\n", attributes: [ 29 | NSFontAttributeName : UIFont.boldSystemFont(ofSize: 14), 30 | NSForegroundColorAttributeName : UIColor.white 31 | ])) 32 | attributedText.append(NSAttributedString(string: "@" + user.screenName, attributes: [ 33 | NSFontAttributeName : UIFont.systemFont(ofSize: 12), 34 | NSForegroundColorAttributeName : UIColor(white: 1, alpha: 0.6) 35 | ])) 36 | navigationView.titleLabel.attributedText = attributedText 37 | } 38 | 39 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell") 40 | tableView.register(TWTRTweetTableViewCell.self, forCellReuseIdentifier: "TWTRTweetTableViewCell") 41 | tableView.dataSource = self 42 | loadTweets() 43 | } 44 | 45 | override var preferredStatusBarStyle: UIStatusBarStyle { 46 | return .lightContent 47 | } 48 | 49 | override func didReceiveMemoryWarning() { 50 | super.didReceiveMemoryWarning() 51 | // Dispose of any resources that can be recreated. 52 | } 53 | 54 | fileprivate func loadTweets() { 55 | guard let user = user , hasNext else { return } 56 | let oldestTweetId = tweets.first?.tweetID 57 | let request = StatusesUserTimelineRequest(screenName: user.screenName, maxId: oldestTweetId, count: nil) 58 | client.sendTwitterRequest(request) { [weak self] in 59 | switch $0.result { 60 | case .success(let tweets): 61 | if tweets.count < 1 { 62 | self?.hasNext = false 63 | return 64 | } 65 | let filterdTweets = tweets.filter { $0.tweetID != oldestTweetId } 66 | let sortedTweets = filterdTweets.sorted { $0.0.createdAt.timeIntervalSince1970 < $0.1.createdAt.timeIntervalSince1970 } 67 | guard let storedTweets = self?.tweets else { return } 68 | self?.tweets = sortedTweets + storedTweets 69 | self?.tableView.reloadData() 70 | if let tweets = self?.tweets { 71 | let indexPath = IndexPath(row: tweets.count - 2, section: 0) 72 | self?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false) 73 | } 74 | case .failure(let error): 75 | print(error) 76 | self?.hasNext = false 77 | } 78 | } 79 | } 80 | } 81 | 82 | extension UserTimelineViewController { 83 | func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: IndexPath) -> CGFloat { 84 | guard (indexPath as NSIndexPath).row < tweets.count else { return 0 } 85 | let tweet = tweets[(indexPath as NSIndexPath).row] 86 | let width = UIScreen.main.bounds.size.width 87 | return TWTRTweetTableViewCell.height(for: tweet, style: .compact, width: width, showingActions: false) 88 | } 89 | 90 | func tableView(_ tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: IndexPath) { 91 | if (indexPath as NSIndexPath).row < 1 { 92 | //loadTweets() 93 | } 94 | } 95 | } 96 | 97 | extension UserTimelineViewController: UITableViewDataSource { 98 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 99 | return tweets.count 100 | } 101 | 102 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 103 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "TWTRTweetTableViewCell") as? TWTRTweetTableViewCell else { 104 | return tableView.dequeueReusableCell(withIdentifier: "UITableViewCell")! 105 | } 106 | cell.configure(with: tweets[(indexPath as NSIndexPath).row]) 107 | return cell 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /HoverConversionSample/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '8.0' 3 | # Uncomment this line if you're using Swift 4 | use_frameworks! 5 | 6 | target 'HoverConversionSample' do 7 | pod 'HoverConversion', :path => '../' 8 | pod 'Fabric' 9 | pod 'TwitterKit' 10 | pod 'TwitterCore' 11 | pod 'TouchVisualizer', '~>2.0.1' 12 | end 13 | 14 | -------------------------------------------------------------------------------- /HoverConversionSample/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Fabric (1.6.8) 3 | - HoverConversion (0.3.1): 4 | - MisterFusion (~> 2.0.0) 5 | - MisterFusion (2.0.1) 6 | - TouchVisualizer (2.0.1) 7 | - TwitterCore (2.4.0): 8 | - Fabric 9 | - TwitterKit (2.4.0): 10 | - TwitterCore (= 2.4.0) 11 | 12 | DEPENDENCIES: 13 | - Fabric 14 | - HoverConversion (from `../`) 15 | - TouchVisualizer (~> 2.0.1) 16 | - TwitterCore 17 | - TwitterKit 18 | 19 | EXTERNAL SOURCES: 20 | HoverConversion: 21 | :path: "../" 22 | 23 | SPEC CHECKSUMS: 24 | Fabric: 5755268d0171435ab167e3d0878a28a777deaf10 25 | HoverConversion: d0ecc0c9a5bb17bfefa32c744dfa605cd66a39a9 26 | MisterFusion: d42cac7afe8318c282bcda93396eba0aef45d30e 27 | TouchVisualizer: d1bd1b438d084245332c222d9e14416c271c5f25 28 | TwitterCore: a139904c80805ad3dde9eef1100704f96f646d1d 29 | TwitterKit: 1cf9742edf9d182c1518cfdd6aaac0dfe313b38b 30 | 31 | PODFILE CHECKSUM: 13ce710cc2a6620a1d2e5917681efbec3e30ebe5 32 | 33 | COCOAPODS: 1.1.0.rc.2 34 | -------------------------------------------------------------------------------- /Images/next_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/HoverConversion/2dc49fd6a002e35e4a71bd2542d95d89058b8f08/Images/next_header.png -------------------------------------------------------------------------------- /Images/sample1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/HoverConversion/2dc49fd6a002e35e4a71bd2542d95d89058b8f08/Images/sample1.gif -------------------------------------------------------------------------------- /Images/sample2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/HoverConversion/2dc49fd6a002e35e4a71bd2542d95d89058b8f08/Images/sample2.gif -------------------------------------------------------------------------------- /Images/storyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/HoverConversion/2dc49fd6a002e35e4a71bd2542d95d89058b8f08/Images/storyboard.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 szk-atmosphere 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HoverConversion 2 | 3 | [![Platform](http://img.shields.io/badge/platform-ios-blue.svg?style=flat 4 | )](https://developer.apple.com/iphone/index.action) 5 | [![Language](http://img.shields.io/badge/language-swift-brightgreen.svg?style=flat 6 | )](https://developer.apple.com/swift) 7 | [![Version](https://img.shields.io/cocoapods/v/HoverConversion.svg?style=flat)](http://cocoapods.org/pods/HoverConversion) 8 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 9 | [![License](https://img.shields.io/cocoapods/l/HoverConversion.svg?style=flat)](http://cocoapods.org/pods/HoverConversion) 10 | 11 | [ManiacDev.com](https://maniacdev.com/) referred. 12 | [https://maniacdev.com/2016/09/hoverconversion-a-swift-ui-component-for-navigating-between-multiple-table-views](https://maniacdev.com/2016/09/hoverconversion-a-swift-ui-component-for-navigating-between-multiple-table-views) 13 | 14 | ![](./Images/sample1.gif) ![](./Images/sample2.gif) 15 | 16 | HoverConversion realized vertical paging with UITableView. UIViewController will be paging when reaching top or bottom of UITableView's contentOffset. 17 | 18 | ## Featrue 19 | 20 | - [x] Vertical paging with UITableView 21 | - [x] Seamless transitioning 22 | - [x] Transitioning with navigationView pan gesture 23 | - [x] Selected cell that related to UIViewController is highlighting 24 | - [x] Support Swift2.3 25 | - [x] Support Swift3 26 | 27 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 28 | 29 | ## Installation 30 | 31 | HoverConversion is available through [CocoaPods](http://cocoapods.org). To install 32 | it, simply add the following line to your Podfile: 33 | 34 | ```ruby 35 | pod "HoverConversion" 36 | ``` 37 | 38 | ## Usage 39 | 40 | If you install from cocoapods, You have to write `import HoverConversion`. 41 | 42 | #### Storyboard or Xib 43 | 44 | ![](./Images/storyboard.png) 45 | 46 | Set custom class of `UINavigationController` to `HCNavigationController`. In addition, set module to `HoverConversion`. 47 | And set `HCRootViewController` as `navigationController`'s first viewController. 48 | 49 | #### Code 50 | 51 | Set `HCNavigationController` as `self.window.rootViewController`. 52 | And set `HCRootViewController` as `navigationController`'s first viewController. 53 | 54 | #### HCPagingViewController 55 | 56 | If you want to show vertical contents, please use `HCPagingViewController`. 57 | 58 | ```swift 59 | let vc = HCPagingViewController(indexPath: indexPath) 60 | vc.dataSource = self 61 | navigationController?.pushViewController(vc, animated: true) 62 | ``` 63 | 64 | #### HCContentViewController 65 | 66 | A content included in `HCPagingViewController` is `HCContentViewController`. 67 | Return `HCContentViewController` (or subclass) with this delegate method. 68 | 69 | ```swift 70 | extension ViewController: HCPagingViewControllerDataSource { 71 | func pagingViewController(viewController: HCPagingViewController, viewControllerFor indexPath: NSIndexPath) -> HCContentViewController? { 72 | guard 0 <= indexPath.row && indexPath.row < twitterManager.users.count else { return nil } 73 | let vc = UserTimelineViewController() 74 | vc.user = twitterManager.users[indexPath.row] 75 | return vc 76 | } 77 | } 78 | ``` 79 | 80 | #### HCNextHeaderView 81 | 82 | ![](./Images/next_header.png) 83 | 84 | Return `HCNextHeaderView` (or subclass) with this delegate method. 85 | 86 | ```swift 87 | extension ViewController: HCPagingViewControllerDataSource { 88 | func pagingViewController(viewController: HCPagingViewController, nextHeaderViewFor indexPath: NSIndexPath) -> HCNextHeaderView? { 89 | guard 0 <= indexPath.row && indexPath.row < twitterManager.users.count else { return nil } 90 | let view = NextHeaderView() 91 | view.user = twitterManager.users[indexPath.row] 92 | return view 93 | } 94 | } 95 | ``` 96 | 97 | #### Stop transitioning 98 | 99 | If you want to load more contents from server and want to stop transitioning, you can use `canPaging` in `HCContentViewController`. 100 | 101 | ```swift 102 | //Stop transitioning to previous ViewController 103 | canPaging[.prev] = false //Default true 104 | 105 | //Stop transitioning to next ViewController 106 | canPaging[.next] = false //Default true 107 | ``` 108 | 109 | ## Requirements 110 | 111 | - Xcode 7.3 or greater 112 | - iOS 8.0 or greater 113 | - [MisterFusion](https://github.com/marty-suzuki/MisterFusion) - Swift DSL for AutoLayout 114 | 115 | ## Special Thanks 116 | 117 | Those OSS are used in sample project! 118 | 119 | - [TouchVisualizer](https://github.com/morizotter/TouchVisualizer) (Created by [@morizotter](https://github.com/morizotter)) 120 | - [TwitterKit](https://docs.fabric.io/apple/twitter/overview.html#) 121 | 122 | ## Author 123 | 124 | marty-suzuki, s1180183@gmail.com 125 | 126 | ## License 127 | 128 | HoverConversion is available under the MIT license. See the LICENSE file for more info. 129 | --------------------------------------------------------------------------------