├── .gitignore ├── .travis.yml ├── Assets └── logo.svg ├── CHANGELOG.md ├── CODEOWNERS ├── Example ├── Assets │ ├── carousel.gif │ ├── horizontal.gif │ └── vertical.gif ├── Example.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Example.xcscheme └── Source │ ├── AppDelegate.swift │ ├── Base.lproj │ └── LaunchScreen.xib │ ├── Controllers │ ├── ActionsController │ │ ├── ActionsController.swift │ │ └── ActionsView.swift │ ├── CarouselController │ │ ├── CarouselController.swift │ │ ├── CarouselView.swift │ │ └── Menu │ │ │ ├── CircularTitleItem.swift │ │ │ └── CircularTitleScrollView.swift │ ├── ColorController │ │ ├── ColorController.swift │ │ └── ColorView.swift │ ├── HorizontalController │ │ ├── HorizontalController.swift │ │ ├── HorizontalView.swift │ │ └── Menu │ │ │ ├── HorizontalTitleItem.swift │ │ │ └── HorizontalTitleScrollView.swift │ ├── ImageController │ │ ├── ImageController.swift │ │ └── ImageView.swift │ ├── OptionsController │ │ ├── OptionsController.swift │ │ └── OptionsView.swift │ ├── PageContent │ │ ├── ColorPageLifeCycleObject.swift │ │ └── ImagePageLifeCycleObject.swift │ └── VerticalController │ │ ├── Menu │ │ ├── VerticalTitleItem.swift │ │ └── VerticalTitleScrollView.swift │ │ ├── VerticalController.swift │ │ └── VerticalView.swift │ ├── Core │ ├── Buttons │ │ ├── ClosureButton.swift │ │ └── FilledButton.swift │ └── UIViewControllers │ │ ├── ContentUIViewController.swift │ │ ├── LifecycleContentUIViewController.swift │ │ └── RootUINavigationController.swift │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-24@2x.png │ │ ├── Icon-27.5@2x.png │ │ ├── Icon-29@2x.png │ │ ├── Icon-29@3x.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-44@2x.png │ │ ├── Icon-86@2x.png │ │ ├── Icon-98@2x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── ItunesArtwork@2x.png │ ├── Contents.json │ ├── image1.imageset │ │ ├── Contents.json │ │ └── dog1.png │ ├── image2.imageset │ │ ├── Contents.json │ │ └── dog2.png │ ├── image3.imageset │ │ ├── Contents.json │ │ └── dog3.png │ ├── image4.imageset │ │ ├── Contents.json │ │ └── dog4.png │ ├── image5.imageset │ │ ├── Contents.json │ │ └── dog5.png │ ├── logo_splash.imageset │ │ ├── Contents.json │ │ └── logo_splash.png │ └── main_logo.imageset │ │ ├── Contents.json │ │ └── logo.pdf │ ├── Launch Screen.storyboard │ ├── Protocols │ ├── ClosureButtonDesignable.swift │ ├── ContentActionable.swift │ ├── StatusBarAccessible.swift │ ├── TitleDesignable.swift │ ├── ViewAccessible.swift │ └── ViewLifeCycleDependable.swift │ ├── Routres │ └── RootRouter.swift │ └── Supporting Files │ ├── Info.plist │ └── Localizable.strings ├── LICENSE ├── README.md ├── SlideController.podspec ├── SlideController.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── SlideController.xcscheme ├── SlideController.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Source ├── SlideContainerController.swift ├── SlideContainerView.swift ├── SlideContentController.swift ├── SlideContentView.swift ├── SlideController.swift ├── SlideLifeCycleObjectBuilder.swift ├── SlideView.swift ├── Supporting Files │ ├── Info.plist │ └── SlideController.h ├── TitleItemController.swift ├── TitleScrollView.swift └── TitleSlidableController.swift ├── Tests ├── AppendTests.swift ├── Core │ ├── BaseTestCase.swift │ ├── TestTitleItem.swift │ ├── TestTitleScrollView.swift │ └── TestableLifeCycleObject.swift ├── InsertTests.swift ├── LoadTests.swift ├── RemoveTests.swift ├── ShiftTests.swift └── Supporting Files │ └── Info.plist └── scripts └── deploy.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 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 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | Pods/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode13.1 3 | language: objective-c 4 | 5 | before_deploy: 6 | - gem install cocoapods 7 | - pod repo add-cdn trunk 'https://cdn.cocoapods.org/' 8 | 9 | deploy: 10 | provider: script 11 | script: ./scripts/deploy.sh 12 | on: 13 | tags: true 14 | 15 | script: 16 | - set -o pipefail && xcodebuild -scheme SlideController -workspace SlideController.xcworkspace -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.0' build test | xcpretty --color 17 | - pod lib lint 18 | after_success: 19 | - bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for SlideController 1.5.1 2 | ### Changed 3 | * Updates for Swift 5.0 4 | 5 | # Changelog for SlideController 1.5.0 6 | ### Fixed 7 | * Improved performance by replacing autolayout with frame-based layout 8 | * Fixed title view jumping while scrolling content 9 | 10 | # Changelog for SlideController 1.4.0 11 | ### Added 12 | * Updates for Swift 4.2 13 | 14 | # Changelog for SlideController 1.3.5 15 | ### Fixed 16 | * Reset of `isScrollEnabled` property 17 | 18 | # Changelog for SlideController 1.3.4 19 | ### Fixed 20 | * Sliding indicator animation when title jumps not animated. 21 | 22 | # Changelog for SlideController 1.3.3 23 | ### Added 24 | * `shouldAnimateIndicatorOnSelection(index: Int) -> Bool` in `TitleConfigurable` allows to manage animation of sliding indicator 25 | 26 | # Changelog for SlideController 1.3.2 27 | ### Fixed 28 | * Do not allow multiple `shift` calls at the same time to prevent titleView freeze. 29 | 30 | # Changelog for SlideController 1.3.1 31 | ### Example changes 32 | * `insertAction` now inserts a page before currently selected page for both vertical and horizontal samples. 33 | * `removeAction` now deletes current page for vertical sample as well as for horizontal. 34 | 35 | # Changelog for SlideController 1.3.0 36 | ### Added 37 | * New example look 🎉 . 38 | ### **Breaking Change** 39 | * ```isCircular``` renamed to ```isCarousel```. 40 | ### Fixed 41 | * Select title item after ```insert(object: SlideLifeCycleObjectProvidable, index: Int)``` 42 | 43 | # Changelog for SlideController 1.2.2 44 | ### Fixed 45 | * ```isScrollEnabled``` exposed to public api as intended. 46 | * ```currentIndex``` calculation for not layouted views. 47 | 48 | # Changelog for SlideController 1.2.1 49 | ### Fixed 50 | * Title item selection follow up. [#35](https://github.com/touchlane/SlideController/issues/35) 51 | * Title view sometimes not responding after app enters foreground. 52 | 53 | # Changelog for SlideController 1.2.0 54 | ### Added 55 | * `isCircular` setting that enables infinite scroll between pages. 56 | * `TitleViewAlignment` enum extended with `bottom` option. 57 | * Carousel sample added to example project. 58 | 59 | ### Fixed 60 | * Views unloading on manual `shift(pageIndex:, animated:)` call 61 | 62 | # Changelog for SlideController 1.1.1 63 | ### Added 64 | * Disabled animation on item selection. 65 | 66 | ### Fixed 67 | * Sync LifeCycle calls with animation. [#44](https://github.com/touchlane/SlideController/issues/44) 68 | 69 | # Changelog for SlideController 1.1.0 70 | ### Added 71 | * **Breaking Change** `SlidePageModel` renamed to `SlideLifeCycleObjectBuilder`. 72 | * Callback method `func indicator(position: CGFloat, size: CGFloat, animated: Bool)` in TitleScrollView to implement sliding indicator. 73 | * Sliding indicator HorizontalTitleScrollView sample. 74 | 75 | # Changelog for SlideController 1.0.4 76 | ### Fixed 77 | * Transition between tabs performance. 78 | 79 | # Changelog for SlideController 1.0.3 80 | ### Added 81 | * ``isContentUnloadingEnabled`` setting that allows disable pages unloading. 82 | ### Fixed 83 | * ``SlidePageLifeCycle`` calls on ``insert(object:, index:)`` . 84 | * ``SlidePageLifeCycle`` calls on ``shift(pageIndex:, animated:)``. 85 | 86 | # Changelog for SlideController 1.0.2 87 | ### Added 88 | * Unit tests 89 | ### Fixed 90 | * ``SlidePageLifeCycle`` calls on ``removeViewAtIndex(index:)`` 91 | * ``SlidePageLifeCycle`` calls when appended pages to empty ``SlideController`` 92 | * Duplicated ``didStartSliding`` calls 93 | 94 | # Changelog for SlideController 1.0.1 95 | ### Fixed 96 | * Inappropriate lifecycle calls when ``SlideController`` appears. 97 | * View loading on ``slideController.shift(pageIndex: Int, animated: Bool)``. 98 | * Lifecycle ``didStartSliding`` calls on page transition. 99 | * Layouting ``SlideContentView`` in ``changeContentLayoutAction`` when changing device orientation. 100 | * Crash calculating ``currentIndex`` when ``contentSize`` of a page is 0. 101 | 102 | # Changelog for SlideController 1.0.0 103 | ### Added 104 | * Vertical ``SlideController`` implementation. 105 | * Smart transition - skipping intermediate pages. 106 | * ``SlideContentView`` lazy loading. 107 | * ``SlideContentView`` unloading. 108 | * ``FeatureManager`` for feature toggling. 109 | * ``ActionsView`` for both vertical and horizontal example. 110 | * Device orientation support. 111 | * ``TitleItemObject`` auto selection when it is out of the screen while sliding. 112 | * Lock ``TitleView`` for scrolling and selection while ``SlideController's`` is sliding. 113 | ### Fixed 114 | * ScrollView automatically adjusted ``contentInsets``. 115 | 116 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @VadzimMarozau -------------------------------------------------------------------------------- /Example/Assets/carousel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Assets/carousel.gif -------------------------------------------------------------------------------- /Example/Assets/horizontal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Assets/horizontal.gif -------------------------------------------------------------------------------- /Example/Assets/vertical.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Assets/vertical.gif -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Example/Source/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ScrollController 4 | // 5 | // Created by pknd on 08/16/2017. 6 | // Copyright (c) 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | let rootVC = RootUINavigationController() 16 | var router: RootRouter? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | window = UIWindow(frame: UIScreen.main.bounds) 20 | router = RootRouter(presenter: rootVC) 21 | window?.rootViewController = rootVC 22 | router?.openMainScreen(animated: true) 23 | window?.makeKeyAndVisible() 24 | return true 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Example/Source/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Example/Source/Controllers/ActionsController/ActionsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionsController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ActionsController: ContentActionable { 12 | private let internalView = ActionsView() 13 | 14 | // MARK: ContentActionableImplementation 15 | 16 | var isShowAdvancedActions: Bool { 17 | get { 18 | return internalView.isShowAdvancedActions 19 | } 20 | set { 21 | internalView.isShowAdvancedActions = newValue 22 | } 23 | } 24 | 25 | var removeDidTapAction: Action? { 26 | didSet { 27 | internalView.removeButton.didTouchUpInside = removeDidTapAction 28 | } 29 | } 30 | 31 | var insertDidTapAction: Action? { 32 | didSet { 33 | internalView.insertButton.didTouchUpInside = insertDidTapAction 34 | } 35 | } 36 | 37 | var appendDidTapAction: Action? { 38 | didSet { 39 | internalView.appendButton.didTouchUpInside = appendDidTapAction 40 | } 41 | } 42 | 43 | var changePositionAction: ((Int) -> ())? { 44 | didSet { 45 | internalView.changePositionAction = changePositionAction 46 | } 47 | } 48 | } 49 | 50 | private typealias ViewAccessibleImplementation = ActionsController 51 | extension ViewAccessibleImplementation: ViewAccessible { 52 | var view: UIView { 53 | return internalView 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/Source/Controllers/ActionsController/ActionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionsView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ActionsView: UIView { 12 | let positionControl = UISegmentedControl(items: [ 13 | NSLocalizedString("BesideSegmentTitle", comment: ""), 14 | NSLocalizedString("AboveSegmentTitle", comment: "")]) 15 | let removeButton = FilledButton() 16 | let insertButton = FilledButton() 17 | let appendButton = FilledButton() 18 | let menuButton = FilledButton() 19 | var changePositionAction: ((Int) -> ())? 20 | 21 | private let buttonWidth: CGFloat = 120 22 | private let buttonHeight: CGFloat = 32 23 | private let stackViewSpacing: CGFloat = 20 24 | private let positionControlWidth: CGFloat = 200 25 | private let positionControlBackgroundColor = UIColor.purple 26 | private let positionControlTintColor = UIColor.white 27 | private let stackView = UIStackView() 28 | 29 | init() { 30 | super.init(frame: CGRect.zero) 31 | 32 | stackView.axis = .vertical 33 | stackView.alignment = .center 34 | stackView.distribution = .fill 35 | stackView.spacing = stackViewSpacing 36 | stackView.translatesAutoresizingMaskIntoConstraints = false 37 | addSubview(stackView) 38 | activateStackViewConstraints(view: stackView) 39 | 40 | positionControl.backgroundColor = positionControlBackgroundColor 41 | positionControl.tintColor = positionControlTintColor 42 | positionControl.layer.cornerRadius = 5 // don't let background bleed 43 | positionControl.selectedSegmentIndex = 0 44 | positionControl.addTarget(self, action: #selector(positionControlValueChanged(sender:)), for: .valueChanged) 45 | positionControl.translatesAutoresizingMaskIntoConstraints = false 46 | stackView.addArrangedSubview(positionControl) 47 | activatePositionControlConstraints(view: positionControl) 48 | 49 | removeButton.setTitle("Remove", for: .normal) 50 | removeButton.clipsToBounds = true 51 | removeButton.layer.cornerRadius = buttonHeight / 2 52 | removeButton.translatesAutoresizingMaskIntoConstraints = false 53 | stackView.addArrangedSubview(removeButton) 54 | activateButtonConstraints(view: removeButton) 55 | 56 | insertButton.setTitle("Insert", for: .normal) 57 | insertButton.clipsToBounds = true 58 | insertButton.layer.cornerRadius = buttonHeight / 2 59 | insertButton.translatesAutoresizingMaskIntoConstraints = false 60 | stackView.addArrangedSubview(insertButton) 61 | activateButtonConstraints(view: insertButton) 62 | 63 | appendButton.setTitle("Append", for: .normal) 64 | appendButton.clipsToBounds = true 65 | appendButton.layer.cornerRadius = buttonHeight / 2 66 | appendButton.translatesAutoresizingMaskIntoConstraints = false 67 | stackView.addArrangedSubview(appendButton) 68 | activateButtonConstraints(view: appendButton) 69 | } 70 | 71 | required init?(coder aDecoder: NSCoder) { 72 | fatalError("init(coder:) has not been implemented") 73 | } 74 | 75 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 76 | let stackViewPoint = stackView.convert(point, from: self) 77 | return [removeButton, insertButton, appendButton, menuButton, positionControl] 78 | .contains { $0.frame.contains(stackViewPoint) } 79 | } 80 | 81 | var isShowAdvancedActions: Bool = true { 82 | didSet { 83 | guard oldValue != isShowAdvancedActions else { 84 | return 85 | } 86 | if isShowAdvancedActions { 87 | stackView.insertArrangedSubview(positionControl, at: 0) 88 | activatePositionControlConstraints(view: positionControl) 89 | stackView.insertArrangedSubview(removeButton, at: 1) 90 | activateButtonConstraints(view: removeButton) 91 | stackView.insertArrangedSubview(insertButton, at: 2) 92 | activateButtonConstraints(view: insertButton) 93 | stackView.insertArrangedSubview(appendButton, at: 3) 94 | activateButtonConstraints(view: appendButton) 95 | } 96 | else { 97 | positionControl.removeFromSuperview() 98 | removeButton.removeFromSuperview() 99 | insertButton.removeFromSuperview() 100 | appendButton.removeFromSuperview() 101 | } 102 | } 103 | } 104 | } 105 | 106 | private typealias PrivateActionsView = ActionsView 107 | private extension PrivateActionsView { 108 | func activateStackViewConstraints(view: UIView) { 109 | guard let superview = view.superview else { 110 | return 111 | } 112 | NSLayoutConstraint.activate([ 113 | view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), 114 | view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), 115 | view.centerYAnchor.constraint(equalTo: superview.centerYAnchor)]) 116 | 117 | } 118 | 119 | func activateButtonConstraints(view: UIView) { 120 | guard let superview = view.superview else { 121 | return 122 | } 123 | NSLayoutConstraint.activate([ 124 | view.centerXAnchor.constraint(equalTo: superview.centerXAnchor), 125 | view.widthAnchor.constraint(equalToConstant: self.buttonWidth), 126 | view.heightAnchor.constraint(equalToConstant: self.buttonHeight)]) 127 | } 128 | 129 | func activatePositionControlConstraints(view: UIView) { 130 | guard let superview = view.superview else { 131 | return 132 | } 133 | NSLayoutConstraint.activate([ 134 | view.centerXAnchor.constraint(equalTo: superview.centerXAnchor), 135 | view.widthAnchor.constraint(equalToConstant: self.positionControlWidth), 136 | view.heightAnchor.constraint(equalToConstant: self.buttonHeight)]) 137 | } 138 | 139 | @objc func positionControlValueChanged(sender: UISegmentedControl) { 140 | changePositionAction?(sender.selectedSegmentIndex) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Example/Source/Controllers/CarouselController/CarouselController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselController.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/27/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class CarouselController { 13 | private let internalView = CarouselView() 14 | private let slideController: SlideController! 15 | 16 | init() { 17 | var pagesContent: [SlideLifeCycleObjectBuilder] = [] 18 | for index in 1...5 { 19 | let page = ImagePageLifeCycleObject() 20 | page.controller.image = UIImage(named: "image\(index)") 21 | pagesContent.append(SlideLifeCycleObjectBuilder(object: page)) 22 | } 23 | slideController = SlideController( 24 | pagesContent: pagesContent, 25 | startPageIndex: 0, 26 | slideDirection: SlideDirection.horizontal) 27 | slideController.titleView.alignment = .bottom 28 | slideController.titleView.titleSize = 40 29 | slideController.isCarousel = true 30 | slideController.titleView.position = .above 31 | slideController.titleView.isTransparent = true 32 | internalView.contentView = slideController.view 33 | } 34 | 35 | var optionsController: (ViewAccessible & ContentActionable)? { 36 | didSet { 37 | internalView.optionsView = optionsController?.view 38 | } 39 | } 40 | } 41 | 42 | private typealias ViewLifeCycleDependableImplementation = CarouselController 43 | extension ViewLifeCycleDependableImplementation: ViewLifeCycleDependable { 44 | func viewDidAppear() { 45 | slideController.viewDidAppear() 46 | } 47 | 48 | func viewDidDisappear() { 49 | slideController.viewDidDisappear() 50 | } 51 | } 52 | 53 | private typealias ViewAccessibleImplementation = CarouselController 54 | extension ViewAccessibleImplementation: ViewAccessible { 55 | var view: UIView { 56 | return internalView 57 | } 58 | } 59 | 60 | private typealias StatusBarAccessibleImplementation = CarouselController 61 | extension StatusBarAccessibleImplementation: StatusBarAccessible { 62 | var statusBarStyle: UIStatusBarStyle { 63 | return .lightContent 64 | } 65 | } 66 | 67 | private typealias TitleAccessibleImplementation = CarouselController 68 | extension TitleAccessibleImplementation: TitleAccessible { 69 | var title: String { 70 | return "Carousel" 71 | } 72 | } 73 | 74 | private typealias TitleColorableImplementation = CarouselController 75 | extension TitleColorableImplementation: TitleColorable { 76 | var titleColor: UIColor { 77 | return .white 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Example/Source/Controllers/CarouselController/CarouselView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselView.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/27/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CarouselView: UIView { 12 | var contentView: UIView? { 13 | didSet { 14 | oldValue?.removeFromSuperview() 15 | if let view = contentView { 16 | view.translatesAutoresizingMaskIntoConstraints = false 17 | self.addSubview(view) 18 | activateContentViewConstraints(view: view) 19 | } 20 | } 21 | } 22 | 23 | var optionsView: UIView? { 24 | didSet { 25 | oldValue?.removeFromSuperview() 26 | if let view = optionsView { 27 | view.translatesAutoresizingMaskIntoConstraints = false 28 | self.addSubview(view) 29 | activateOptionsViewConstraints(view: view) 30 | } 31 | } 32 | } 33 | 34 | init() { 35 | super.init(frame: .zero) 36 | backgroundColor = UIColor.black 37 | } 38 | 39 | required init?(coder aDecoder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | } 43 | 44 | private typealias PrivateCarouselView = CarouselView 45 | private extension PrivateCarouselView { 46 | func activateContentViewConstraints(view: UIView) { 47 | var constraints = [NSLayoutConstraint]() 48 | constraints.append(view.bottomAnchor.constraint(equalTo: self.bottomAnchor)) 49 | constraints.append(view.leadingAnchor.constraint(equalTo: self.leadingAnchor)) 50 | constraints.append(view.trailingAnchor.constraint(equalTo: self.trailingAnchor)) 51 | constraints.append(view.topAnchor.constraint(equalTo: self.topAnchor)) 52 | NSLayoutConstraint.activate(constraints) 53 | } 54 | 55 | func activateOptionsViewConstraints(view: UIView) { 56 | var constraints = [NSLayoutConstraint]() 57 | constraints.append(view.leadingAnchor.constraint(equalTo: self.leadingAnchor)) 58 | constraints.append(view.trailingAnchor.constraint(equalTo: self.trailingAnchor)) 59 | constraints.append(view.topAnchor.constraint(equalTo: self.topAnchor, constant: 84)) 60 | NSLayoutConstraint.activate(constraints) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Example/Source/Controllers/CarouselController/Menu/CircularTitleItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselTitleItem.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/27/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class CarouselTitleItem: UIView, Initializable, ItemViewable, Selectable { 13 | let dotView = UIView() 14 | 15 | private let dotViewOffsetX: CGFloat = 5 16 | private let dotViewSizeValue: CGFloat = 10 17 | private let dotDefaultColor = UIColor(white: 0, alpha: 0.3) 18 | private let dotSelectedColor = UIColor(white: 0, alpha: 1) 19 | private var internalIsSelected: Bool = false 20 | private var internalIndex: Int = 0 21 | private var internalDidSelectAction: ((Int) -> Void)? 22 | 23 | required init() { 24 | super.init(frame: CGRect.zero) 25 | dotView.layer.cornerRadius = dotViewSizeValue / 2 26 | dotView.translatesAutoresizingMaskIntoConstraints = false 27 | addSubview(dotView) 28 | activateDotViewConstraints(view: dotView) 29 | isSelected = false 30 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapDetected(_:))) 31 | addGestureRecognizer(tapRecognizer) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | // MARK: - ItemViewableImplementation 39 | 40 | typealias Item = CarouselTitleItem 41 | 42 | var view: Item { 43 | return self 44 | } 45 | 46 | // MARK: - SelectableImplementation 47 | 48 | var didSelectAction: ((Int) -> ())? { 49 | get { 50 | return internalDidSelectAction 51 | } 52 | set { 53 | internalDidSelectAction = newValue 54 | } 55 | } 56 | 57 | var isSelected: Bool { 58 | get { 59 | return internalIsSelected 60 | } 61 | set { 62 | if newValue { 63 | dotView.backgroundColor = dotSelectedColor 64 | } else { 65 | dotView.backgroundColor = dotDefaultColor 66 | } 67 | internalIsSelected = newValue 68 | } 69 | } 70 | 71 | var index: Int { 72 | get { 73 | return internalIndex 74 | } 75 | set { 76 | internalIndex = newValue 77 | } 78 | } 79 | } 80 | 81 | private typealias PrivateCarouselTitleItem = CarouselTitleItem 82 | private extension PrivateCarouselTitleItem { 83 | func activateDotViewConstraints(view: UIView) { 84 | var constraints = [NSLayoutConstraint]() 85 | constraints.append(view.centerYAnchor.constraint(equalTo: centerYAnchor)) 86 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -dotViewOffsetX)) 87 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: dotViewOffsetX)) 88 | constraints.append(view.widthAnchor.constraint(equalToConstant: dotViewSizeValue)) 89 | constraints.append(view.heightAnchor.constraint(equalToConstant: dotViewSizeValue)) 90 | NSLayoutConstraint.activate(constraints) 91 | } 92 | 93 | @objc func tapDetected(_ recognizer: UIGestureRecognizer) { 94 | if !internalIsSelected { 95 | internalDidSelectAction?(internalIndex) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Source/Controllers/CarouselController/Menu/CircularTitleScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarouselTitleScrollView.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/27/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class CarouselTitleScrollView: TitleScrollView { 13 | private var internalItems: [View] = [] 14 | 15 | private let internalItemOffsetX: CGFloat = 0 16 | private let itemOffsetTop: CGFloat = 10 17 | private let itemHeight: CGFloat = 20 18 | private let internalBackgroundColor = UIColor.purple 19 | 20 | override required init() { 21 | super.init() 22 | backgroundColor = internalBackgroundColor 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override var items: [TitleItem] { 30 | return internalItems 31 | } 32 | 33 | override func appendViews(views: [View]) { 34 | var prevView: View? = internalItems.last 35 | let prevPrevView: UIView? = internalItems.count > 1 ? internalItems[items.count - 2] : nil 36 | if let prevItem = prevView { 37 | updateConstraints(view: prevItem, prevView: prevPrevView, isLast: false) 38 | } 39 | for i in 0...views.count - 1 { 40 | let view = views[i] 41 | view.translatesAutoresizingMaskIntoConstraints = false 42 | internalItems.append(view) 43 | addSubview(view) 44 | activateConstraints(view: view, prevView: prevView, isLast: i == views.count - 1) 45 | prevView = view 46 | } 47 | } 48 | 49 | override func insertView(view: View, index: Int) { 50 | guard index < internalItems.count else { 51 | return 52 | } 53 | view.translatesAutoresizingMaskIntoConstraints = false 54 | internalItems.insert(view, at: index) 55 | addSubview(view) 56 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 57 | let nextView: View = internalItems[index + 1] 58 | activateConstraints(view: view, prevView: prevView, isLast: false) 59 | updateConstraints(view: nextView, prevView: view, isLast: index == internalItems.count - 2) 60 | } 61 | 62 | override func removeViewAtIndex(index: Int) { 63 | guard index < internalItems.count else { 64 | return 65 | } 66 | let view: View = internalItems[index] 67 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 68 | let nextView: View? = index < internalItems.count - 1 ? internalItems[index + 1] : nil 69 | internalItems.remove(at: index) 70 | view.removeFromSuperview() 71 | if let nextView = nextView { 72 | updateConstraints(view: nextView, prevView: prevView, isLast: index == internalItems.count - 1) 73 | } else if let prevView = prevView { 74 | let prevPrevView: View? = internalItems.count > 1 ? internalItems[internalItems.count - 2] : nil 75 | updateConstraints(view: prevView, prevView: prevPrevView, isLast: true) 76 | } 77 | } 78 | 79 | var isTransparent = false { 80 | didSet { 81 | backgroundColor = isTransparent ? UIColor.clear : internalBackgroundColor 82 | } 83 | } 84 | } 85 | 86 | private typealias PrivateCarouselTitleScrollView = CarouselTitleScrollView 87 | private extension PrivateCarouselTitleScrollView { 88 | func activateConstraints(view: UIView, prevView: UIView?, isLast: Bool) { 89 | var constraints: [NSLayoutConstraint] = [] 90 | constraints.append(view.topAnchor.constraint(equalTo: topAnchor, constant: itemOffsetTop)) 91 | constraints.append(view.heightAnchor.constraint(equalToConstant: itemHeight)) 92 | if let prevView = prevView { 93 | constraints.append(view.leadingAnchor.constraint(equalTo: prevView.trailingAnchor, constant: 2 * internalItemOffsetX)) 94 | } else { 95 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: internalItemOffsetX)) 96 | } 97 | if isLast { 98 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -internalItemOffsetX)) 99 | } 100 | NSLayoutConstraint.activate(constraints) 101 | } 102 | 103 | func removeConstraints(view: UIView) { 104 | let viewConstraints = constraints.filter({ $0.firstItem === view }) 105 | let heigthConstraints = view.constraints.filter({ $0.firstAttribute == .height }) 106 | NSLayoutConstraint.deactivate(viewConstraints + heigthConstraints) 107 | } 108 | 109 | func updateConstraints(view: UIView, prevView: UIView?, isLast: Bool) { 110 | self.removeConstraints(view: view) 111 | self.activateConstraints(view: view, prevView: prevView, isLast: isLast) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Example/Source/Controllers/ColorController/ColorController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/10/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class ColorController { 13 | private let internalView = ColorView() 14 | } 15 | 16 | private typealias ViewableImplementation = ColorController 17 | extension ViewableImplementation : Viewable { 18 | var view: UIView { 19 | return internalView 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Source/Controllers/ColorController/ColorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/10/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ColorView : UIView { 12 | init() { 13 | super.init(frame: CGRect.zero) 14 | backgroundColor = randomColor() 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | } 21 | 22 | private typealias PrivateColorView = ColorView 23 | private extension PrivateColorView { 24 | func randomColor() -> UIColor{ 25 | let randomRed: CGFloat = CGFloat(drand48()) 26 | let randomGreen: CGFloat = CGFloat(drand48()) 27 | let randomBlue: CGFloat = CGFloat(drand48()) 28 | return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/Source/Controllers/HorizontalController/HorizontalController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/10/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class HorizontalController { 13 | private let internalView = HorizontalView() 14 | private let slideController: SlideController! 15 | 16 | private var addedPagesCount: Int 17 | 18 | lazy var removeCurrentPageAction: (() -> Void)? = { [weak self] in 19 | guard let strongSelf = self else { return } 20 | guard let currentPageIndex = strongSelf.slideController.content 21 | .firstIndex(where: { strongSelf.slideController.currentModel === $0 }) else { 22 | return 23 | } 24 | strongSelf.slideController.removeAtIndex(index: currentPageIndex) 25 | } 26 | 27 | lazy var insertAction: (() -> Void)? = { [weak self] in 28 | guard let strongSelf = self else { return } 29 | let page = SlideLifeCycleObjectBuilder(object: ColorPageLifeCycleObject()) 30 | guard let index = strongSelf.slideController.content 31 | .firstIndex(where: { strongSelf.slideController.currentModel === $0 }) else { 32 | return 33 | } 34 | strongSelf.slideController.insert(object: page, index: index) 35 | strongSelf.addedPagesCount += 1 36 | 37 | let titleItems = strongSelf.slideController.titleView.items 38 | guard titleItems.indices.contains(index) else { return } 39 | titleItems[index].titleLabel.text = strongSelf.title(for: strongSelf.addedPagesCount) 40 | } 41 | 42 | lazy var appendAction: (() -> Void)? = { [weak self] in 43 | guard let strongSelf = self else { return } 44 | let page = SlideLifeCycleObjectBuilder(object: ColorPageLifeCycleObject()) 45 | strongSelf.slideController.append(object: [page]) 46 | strongSelf.addedPagesCount += 1 47 | 48 | let titleItems = strongSelf.slideController.titleView.items 49 | let lastItemIndex = titleItems.count - 1 50 | titleItems[lastItemIndex].titleLabel.text = strongSelf.title(for: strongSelf.addedPagesCount) 51 | } 52 | 53 | private lazy var changePositionAction: ((Int) -> Void)? = { [weak self] position in 54 | guard let strongSelf = self else { return } 55 | switch position { 56 | case 0: 57 | strongSelf.slideController.titleView.position = TitleViewPosition.beside 58 | strongSelf.slideController.titleView.isTransparent = false 59 | case 1: 60 | strongSelf.slideController.titleView.position = TitleViewPosition.above 61 | strongSelf.slideController.titleView.isTransparent = true 62 | default: 63 | break 64 | } 65 | } 66 | 67 | init() { 68 | let pagesContent = [ 69 | SlideLifeCycleObjectBuilder(object: ColorPageLifeCycleObject()), 70 | SlideLifeCycleObjectBuilder(), 71 | SlideLifeCycleObjectBuilder()] 72 | slideController = SlideController( 73 | pagesContent: pagesContent, 74 | startPageIndex: 0, 75 | slideDirection: SlideDirection.horizontal) 76 | 77 | addedPagesCount = pagesContent.count 78 | for index in 0.. String { 100 | return "page \(index)" 101 | } 102 | } 103 | 104 | private typealias ViewLifeCycleDependableImplementation = HorizontalController 105 | extension ViewLifeCycleDependableImplementation: ViewLifeCycleDependable { 106 | func viewDidAppear() { 107 | slideController.viewDidAppear() 108 | } 109 | 110 | func viewDidDisappear() { 111 | slideController.viewDidDisappear() 112 | } 113 | } 114 | 115 | private typealias ViewAccessibleImplementation = HorizontalController 116 | extension ViewAccessibleImplementation: ViewAccessible { 117 | var view: UIView { 118 | return internalView 119 | } 120 | } 121 | 122 | private typealias StatusBarAccessibleImplementation = HorizontalController 123 | extension StatusBarAccessibleImplementation: StatusBarAccessible { 124 | var statusBarStyle: UIStatusBarStyle { 125 | return .lightContent 126 | } 127 | } 128 | 129 | private typealias TitleAccessibleImplementation = HorizontalController 130 | extension TitleAccessibleImplementation: TitleAccessible { 131 | var title: String { 132 | return "Horizontal" 133 | } 134 | } 135 | 136 | private typealias TitleColorableImplementation = HorizontalController 137 | extension TitleColorableImplementation: TitleColorable { 138 | var titleColor: UIColor { 139 | return .white 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Example/Source/Controllers/HorizontalController/HorizontalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/10/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class HorizontalView : UIView { 12 | var contentView: UIView? { 13 | didSet { 14 | oldValue?.removeFromSuperview() 15 | if let view = contentView { 16 | view.translatesAutoresizingMaskIntoConstraints = false 17 | self.addSubview(view) 18 | activateContentViewConstraints(view: view) 19 | } 20 | } 21 | } 22 | 23 | var optionsView: UIView? { 24 | didSet { 25 | oldValue?.removeFromSuperview() 26 | if let view = optionsView { 27 | view.translatesAutoresizingMaskIntoConstraints = false 28 | self.addSubview(view) 29 | activateOptionsViewConstraints(view: view) 30 | } 31 | } 32 | } 33 | 34 | init() { 35 | super.init(frame: .zero) 36 | backgroundColor = UIColor.black 37 | } 38 | 39 | required init?(coder aDecoder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | } 43 | 44 | private typealias PrivateHorizontalView = HorizontalView 45 | private extension PrivateHorizontalView { 46 | func activateContentViewConstraints(view: UIView) { 47 | var constraints = [NSLayoutConstraint]() 48 | constraints.append(view.bottomAnchor.constraint(equalTo: self.bottomAnchor)) 49 | constraints.append(view.leadingAnchor.constraint(equalTo: self.leadingAnchor)) 50 | constraints.append(view.trailingAnchor.constraint(equalTo: self.trailingAnchor)) 51 | constraints.append(view.topAnchor.constraint(equalTo: self.topAnchor)) 52 | NSLayoutConstraint.activate(constraints) 53 | } 54 | 55 | func activateOptionsViewConstraints(view: UIView) { 56 | var constraints = [NSLayoutConstraint]() 57 | constraints.append(view.leadingAnchor.constraint(equalTo: self.leadingAnchor)) 58 | constraints.append(view.trailingAnchor.constraint(equalTo: self.trailingAnchor)) 59 | constraints.append(view.bottomAnchor.constraint(equalTo: self.bottomAnchor)) 60 | constraints.append(view.topAnchor.constraint(equalTo: self.topAnchor, constant: 44)) 61 | NSLayoutConstraint.activate(constraints) 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /Example/Source/Controllers/HorizontalController/Menu/HorizontalTitleItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleItem.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 4/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class HorizontalTitleItem: UIView, Initializable, ItemViewable, Selectable { 13 | let titleLabel = UILabel() 14 | 15 | private var titleLabelOffsetX: CGFloat = 21 16 | private var internalIsSelected: Bool = false 17 | private var internalIndex: Int = 0 18 | private var internalDidSelectAction: ((Int) -> Void)? 19 | 20 | private let titleLabelFont = UIFont.systemFont(ofSize: 16.5) 21 | private let internalBackgroundColor = UIColor.clear 22 | private let titleFontDefaultColor = UIColor(white: 1, alpha: 0.7) 23 | private let titleFontSelectedColor = UIColor(white: 1, alpha: 1) 24 | 25 | required init() { 26 | super.init(frame: CGRect.zero) 27 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 28 | titleLabel.font = titleLabelFont 29 | addSubview(titleLabel) 30 | activateTitleLabelConstraints(view: titleLabel) 31 | isSelected = false 32 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapDetected(_:))) 33 | addGestureRecognizer(tapRecognizer) 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | // MARK: - ItemViewableImplementation 41 | 42 | typealias Item = HorizontalTitleItem 43 | 44 | var view: Item { 45 | return self 46 | } 47 | 48 | // MARK: - SelectableImplementation 49 | 50 | var didSelectAction: ((Int) -> Void)? { 51 | get { 52 | return internalDidSelectAction 53 | } 54 | set { 55 | internalDidSelectAction = newValue 56 | } 57 | } 58 | 59 | var isSelected: Bool { 60 | get { 61 | return internalIsSelected 62 | } 63 | set { 64 | if newValue { 65 | titleLabel.textColor = titleFontSelectedColor 66 | } else { 67 | titleLabel.textColor = titleFontDefaultColor 68 | } 69 | internalIsSelected = newValue 70 | } 71 | } 72 | 73 | var index: Int { 74 | get { 75 | return internalIndex 76 | } 77 | set { 78 | internalIndex = newValue 79 | } 80 | } 81 | } 82 | 83 | private typealias PrivateHorizontalTitleItem = HorizontalTitleItem 84 | private extension PrivateHorizontalTitleItem { 85 | func activateTitleLabelConstraints(view: UIView) { 86 | var constraints = [NSLayoutConstraint]() 87 | constraints.append(view.centerYAnchor.constraint(equalTo: centerYAnchor)) 88 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -titleLabelOffsetX)) 89 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: titleLabelOffsetX)) 90 | NSLayoutConstraint.activate(constraints) 91 | } 92 | 93 | @objc func tapDetected(_ recognizer: UIGestureRecognizer) { 94 | if !internalIsSelected { 95 | internalDidSelectAction?(internalIndex) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/Source/Controllers/HorizontalController/Menu/HorizontalTitleScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HorizontalTitleScrollView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Vadim Morozov on 4/20/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class HorizontalTitleScrollView: TitleScrollView { 13 | private var internalItems: [View] = [] { 14 | didSet { 15 | indicatorView.isHidden = internalItems.isEmpty 16 | } 17 | } 18 | private let internalItemOffsetX: CGFloat = 15 19 | private let itemOffsetTop: CGFloat = 0 20 | private let itemHeight: CGFloat = 36 21 | private let internalBackgroundColor = UIColor.purple 22 | 23 | private var indicatorLeadingAnchor: NSLayoutConstraint? 24 | private var indicatorWidthAnchor: NSLayoutConstraint? 25 | private var indicatorHeight: CGFloat = 2 26 | private var indicatorColor: UIColor = .white 27 | private let indicatorView = UIView() 28 | 29 | override required init() { 30 | super.init() 31 | backgroundColor = internalBackgroundColor 32 | 33 | indicatorView.translatesAutoresizingMaskIntoConstraints = false 34 | indicatorView.backgroundColor = indicatorColor 35 | addSubview(indicatorView) 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override var items: [TitleItem] { 43 | return internalItems 44 | } 45 | 46 | override func appendViews(views: [View]) { 47 | var prevView: View? = internalItems.last 48 | let prevPrevView: UIView? = internalItems.count > 1 ? internalItems[items.count - 2] : nil 49 | if let prevItem = prevView { 50 | updateConstraints(view: prevItem, prevView: prevPrevView, isLast: false) 51 | } 52 | for i in 0...views.count - 1 { 53 | let view = views[i] 54 | view.translatesAutoresizingMaskIntoConstraints = false 55 | internalItems.append(view) 56 | addSubview(view) 57 | activateConstraints(view: view, prevView: prevView, isLast: i == views.count - 1) 58 | prevView = view 59 | } 60 | } 61 | 62 | override func insertView(view: View, index: Int) { 63 | guard index < internalItems.count else { 64 | return 65 | } 66 | view.translatesAutoresizingMaskIntoConstraints = false 67 | internalItems.insert(view, at: index) 68 | addSubview(view) 69 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 70 | let nextView: View = internalItems[index + 1] 71 | activateConstraints(view: view, prevView: prevView, isLast: false) 72 | updateConstraints(view: nextView, prevView: view, isLast: index == internalItems.count - 2) 73 | } 74 | 75 | override func removeViewAtIndex(index: Int) { 76 | guard index < internalItems.count else { 77 | return 78 | } 79 | let view: View = internalItems[index] 80 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 81 | let nextView: View? = index < internalItems.count - 1 ? internalItems[index + 1] : nil 82 | internalItems.remove(at: index) 83 | view.removeFromSuperview() 84 | if let nextView = nextView { 85 | updateConstraints(view: nextView, prevView: prevView, isLast: index == internalItems.count - 1) 86 | } else if let prevView = prevView { 87 | let prevPrevView: View? = internalItems.count > 1 ? internalItems[internalItems.count - 2] : nil 88 | updateConstraints(view: prevView, prevView: prevPrevView, isLast: true) 89 | } 90 | } 91 | 92 | override func indicator(position: CGFloat, size: CGFloat, animated: Bool) { 93 | if let indicatorLeadingAnchor = indicatorLeadingAnchor, 94 | let indicatorWidthAnchor = indicatorWidthAnchor { 95 | indicatorLeadingAnchor.constant = position 96 | indicatorWidthAnchor.constant = size 97 | } else { 98 | activateBackgroundViewConstraints(view: indicatorView, position: position, width: size) 99 | } 100 | if animated { 101 | UIView.animate(withDuration: 0.3, animations: { 102 | self.layoutIfNeeded() 103 | }) 104 | } 105 | } 106 | 107 | var isTransparent = false { 108 | didSet { 109 | backgroundColor = isTransparent ? UIColor.clear : internalBackgroundColor 110 | } 111 | } 112 | } 113 | 114 | private typealias PrivateHorizontalTitleScrollView = HorizontalTitleScrollView 115 | private extension PrivateHorizontalTitleScrollView { 116 | func activateBackgroundViewConstraints(view: UIView, position: CGFloat, width: CGFloat) { 117 | var constraints: [NSLayoutConstraint] = [] 118 | constraints.append(view.topAnchor.constraint(equalTo: topAnchor, constant: itemOffsetTop + itemHeight)) 119 | let leading = view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: position) 120 | indicatorLeadingAnchor = leading 121 | constraints.append(leading) 122 | let width = view.widthAnchor.constraint(equalToConstant: width) 123 | indicatorWidthAnchor = width 124 | constraints.append(width) 125 | constraints.append(view.heightAnchor.constraint(equalToConstant: indicatorHeight)) 126 | NSLayoutConstraint.activate(constraints) 127 | } 128 | 129 | func activateConstraints(view: UIView, prevView: UIView?, isLast: Bool) { 130 | var constraints: [NSLayoutConstraint] = [] 131 | constraints.append(view.topAnchor.constraint(equalTo: topAnchor, constant: itemOffsetTop)) 132 | constraints.append(view.heightAnchor.constraint(equalToConstant: itemHeight)) 133 | if let prevView = prevView { 134 | constraints.append(view.leadingAnchor.constraint(equalTo: prevView.trailingAnchor, constant: 2 * internalItemOffsetX)) 135 | } else { 136 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: internalItemOffsetX)) 137 | } 138 | if isLast { 139 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -internalItemOffsetX)) 140 | } 141 | NSLayoutConstraint.activate(constraints) 142 | } 143 | 144 | func removeConstraints(view: UIView) { 145 | let viewConstraints = constraints.filter({ $0.firstItem === view }) 146 | let heigthConstraints = view.constraints.filter({ $0.firstAttribute == .height }) 147 | NSLayoutConstraint.deactivate(viewConstraints + heigthConstraints) 148 | } 149 | 150 | func updateConstraints(view: UIView, prevView: UIView?, isLast: Bool) { 151 | self.removeConstraints(view: view) 152 | self.activateConstraints(view: view, prevView: prevView, isLast: isLast) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Example/Source/Controllers/ImageController/ImageController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageController.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/28/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class ImageController { 13 | private let internalView = ImageView() 14 | var image: UIImage? { 15 | get { 16 | return internalView.image 17 | } 18 | set { 19 | internalView.image = newValue 20 | } 21 | } 22 | } 23 | 24 | private typealias ViewableImplementation = ImageController 25 | extension ViewableImplementation: Viewable { 26 | var view: UIView { 27 | return internalView 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/Source/Controllers/ImageController/ImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageView.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/28/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageView: UIView { 12 | var image: UIImage? { 13 | didSet { 14 | imageView.image = image 15 | } 16 | } 17 | 18 | private let imageView = UIImageView() 19 | 20 | init() { 21 | super.init(frame: CGRect.zero) 22 | backgroundColor = UIColor.white 23 | imageView.contentMode = .center 24 | imageView.translatesAutoresizingMaskIntoConstraints = false 25 | addSubview(imageView) 26 | activateImageViewConstraints(view: imageView) 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | } 33 | 34 | private typealias PrivateImageView = ImageView 35 | private extension PrivateImageView { 36 | func activateImageViewConstraints (view: UIView) { 37 | NSLayoutConstraint.activate([ 38 | view.centerXAnchor.constraint(equalTo: self.centerXAnchor), 39 | view.centerYAnchor.constraint(equalTo: self.centerYAnchor) 40 | ]) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Example/Source/Controllers/OptionsController/OptionsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 9/6/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol OptionsControllerProtocol: AnyObject { 12 | var openHorizontalDemoAction: (() -> Void)? { get set } 13 | var openVerticalDemoAction: (() -> Void)? { get set } 14 | var openCarouselDemoAction: (() -> Void)? { get set } 15 | } 16 | 17 | class OptionsController { 18 | private let internalView = OptionsView() 19 | } 20 | 21 | private typealias OptionsControllerProtocolImplementation = OptionsController 22 | extension OptionsControllerProtocolImplementation : OptionsControllerProtocol { 23 | var openHorizontalDemoAction: (() -> Void)? { 24 | get { 25 | return internalView.horizontalDemoButton.didTouchUpInside 26 | } 27 | set { 28 | internalView.horizontalDemoButton.didTouchUpInside = newValue 29 | } 30 | } 31 | 32 | var openVerticalDemoAction: (() -> Void)? { 33 | get { 34 | return internalView.verticalDemoButton.didTouchUpInside 35 | } 36 | set { 37 | internalView.verticalDemoButton.didTouchUpInside = newValue 38 | } 39 | } 40 | 41 | var openCarouselDemoAction: (() -> Void)? { 42 | get { 43 | return internalView.carouselDemoButton.didTouchUpInside 44 | } 45 | set { 46 | internalView.carouselDemoButton.didTouchUpInside = newValue 47 | } 48 | } 49 | } 50 | 51 | private typealias ViewAccessibleImplementation = OptionsController 52 | extension ViewAccessibleImplementation: ViewAccessible { 53 | var view: UIView { 54 | get { 55 | return internalView 56 | } 57 | } 58 | } 59 | 60 | private typealias StatusBarAccessibleImplementation = OptionsController 61 | extension StatusBarAccessibleImplementation: StatusBarAccessible { 62 | var statusBarStyle: UIStatusBarStyle { 63 | return .lightContent 64 | } 65 | } 66 | 67 | private typealias TitleAccessibleImplementation = OptionsController 68 | extension TitleAccessibleImplementation: TitleAccessible { 69 | var title: String { 70 | return "SlideController" 71 | } 72 | } 73 | 74 | private typealias TitleColorableImplementation = OptionsController 75 | extension TitleColorableImplementation: TitleColorable { 76 | var titleColor: UIColor { 77 | return .white 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Example/Source/Controllers/OptionsController/OptionsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionsView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 9/6/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol OptionsViewProtocol: AnyObject { 12 | var horizontalDemoButton: Actionable { get } 13 | var verticalDemoButton: Actionable { get } 14 | var carouselDemoButton: Actionable { get } 15 | } 16 | 17 | class OptionsView: UIView { 18 | private let optionButtonWidth: CGFloat = 220 19 | private let optionButtonHeigh: CGFloat = 32 20 | private let horizontalDemoButtonCenterYOffset: CGFloat = -32 21 | private let verticalDemoButtonCenterYOffset: CGFloat = 0 22 | private let carouselDemoButtonCenterYOffset: CGFloat = 32 23 | private let internalHorizontalDemoButton = FilledButton() 24 | private let internalVerticalDemoButton = FilledButton() 25 | private let internalCarouselDemoButton = FilledButton() 26 | private let logoImageView = UIImageView() 27 | private let label = UILabel() 28 | 29 | init() { 30 | super.init(frame: CGRect.zero) 31 | backgroundColor = UIColor.white 32 | 33 | internalHorizontalDemoButton.setTitle(NSLocalizedString("HorizontalSampleButtonTitle", comment: ""), for: .normal) 34 | internalHorizontalDemoButton.clipsToBounds = true 35 | internalHorizontalDemoButton.layer.cornerRadius = optionButtonHeigh / 2 36 | internalHorizontalDemoButton.translatesAutoresizingMaskIntoConstraints = false 37 | addSubview(internalHorizontalDemoButton) 38 | activateOptionButtonConstraints(view: internalHorizontalDemoButton, centerYOffset: horizontalDemoButtonCenterYOffset) 39 | 40 | internalVerticalDemoButton.setTitle(NSLocalizedString("VerticalSampleButtonTitle", comment: ""), for: .normal) 41 | internalVerticalDemoButton.clipsToBounds = true 42 | internalVerticalDemoButton.layer.cornerRadius = optionButtonHeigh / 2 43 | internalVerticalDemoButton.translatesAutoresizingMaskIntoConstraints = false 44 | addSubview(internalVerticalDemoButton) 45 | activateOptionButtonConstraints(view: internalVerticalDemoButton, centerYOffset: verticalDemoButtonCenterYOffset) 46 | 47 | internalCarouselDemoButton.setTitle(NSLocalizedString("CarouselSampleButtonTitle", comment: ""), for: .normal) 48 | internalCarouselDemoButton.clipsToBounds = true 49 | internalCarouselDemoButton.layer.cornerRadius = optionButtonHeigh / 2 50 | internalCarouselDemoButton.translatesAutoresizingMaskIntoConstraints = false 51 | addSubview(internalCarouselDemoButton) 52 | activateOptionButtonConstraints(view: internalCarouselDemoButton, centerYOffset: carouselDemoButtonCenterYOffset) 53 | 54 | logoImageView.image = UIImage(named: "main_logo") 55 | logoImageView.translatesAutoresizingMaskIntoConstraints = false 56 | addSubview(logoImageView) 57 | activateLogoImageConstraints(view: logoImageView, anchorView: internalCarouselDemoButton) 58 | 59 | label.text = "SlideController" 60 | label.font = UIFont.boldSystemFont(ofSize: 24) 61 | label.translatesAutoresizingMaskIntoConstraints = false 62 | label.textColor = UIColor(red: 61 / 255, green: 86 / 255, blue: 166 / 255, alpha: 1) 63 | addSubview(label) 64 | activateLabelConstraints(view: label) 65 | } 66 | 67 | required init?(coder aDecoder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | } 71 | 72 | private typealias PrivateOptionsView = OptionsView 73 | private extension PrivateOptionsView { 74 | func activateOptionButtonConstraints (view: UIView, centerYOffset: CGFloat) { 75 | guard let superview = view.superview else { 76 | return 77 | } 78 | NSLayoutConstraint.activate([ 79 | view.centerXAnchor.constraint(equalTo: superview.centerXAnchor), 80 | view.centerYAnchor.constraint(equalTo: superview.centerYAnchor, constant: centerYOffset * 2), 81 | view.heightAnchor.constraint(equalToConstant: optionButtonHeigh), 82 | view.widthAnchor.constraint(equalToConstant: optionButtonWidth) 83 | ]) 84 | } 85 | 86 | func activateLogoImageConstraints(view: UIView, anchorView: UIView) { 87 | guard let superview = view.superview else { 88 | return 89 | } 90 | NSLayoutConstraint.activate([ 91 | view.centerXAnchor.constraint(equalTo: superview.centerXAnchor), 92 | view.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -20), 93 | view.heightAnchor.constraint(equalToConstant: 60) 94 | ]) 95 | } 96 | 97 | func activateLabelConstraints(view: UIView) { 98 | guard let superview = view.superview else { 99 | return 100 | } 101 | NSLayoutConstraint.activate([ 102 | view.centerXAnchor.constraint(equalTo: superview.centerXAnchor), 103 | view.topAnchor.constraint(equalTo: superview.topAnchor, constant: 20) 104 | ]) 105 | } 106 | } 107 | 108 | private typealias OptionsViewProtocolImplementation = OptionsView 109 | extension OptionsViewProtocolImplementation: OptionsViewProtocol { 110 | var horizontalDemoButton: Actionable { 111 | return internalHorizontalDemoButton 112 | } 113 | 114 | var verticalDemoButton: Actionable { 115 | return internalVerticalDemoButton 116 | } 117 | 118 | var carouselDemoButton: Actionable { 119 | return internalCarouselDemoButton 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Example/Source/Controllers/PageContent/ColorPageLifeCycleObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPageLifeCycleObject.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/10/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class ColorPageLifeCycleObject: Initializable { 13 | var controller = ColorController() 14 | 15 | // MARK: - InitialazableImplementation 16 | 17 | required init() { 18 | 19 | } 20 | } 21 | 22 | private typealias SlideColorPageLifeCycleImplementation = ColorPageLifeCycleObject 23 | extension SlideColorPageLifeCycleImplementation: SlidePageLifeCycle { 24 | var isKeyboardResponsive: Bool { 25 | return false 26 | } 27 | 28 | func didAppear() { } 29 | 30 | func didDissapear() { } 31 | 32 | func viewDidLoad() { } 33 | 34 | func viewDidUnload() { } 35 | 36 | func didStartSliding() { } 37 | 38 | func didCancelSliding() { } 39 | } 40 | 41 | private typealias ViewableImplementation = ColorPageLifeCycleObject 42 | extension ViewableImplementation: Viewable { 43 | var view: UIView { 44 | get { 45 | return controller.view 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/Source/Controllers/PageContent/ImagePageLifeCycleObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePageLifeCycleObject.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/28/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class ImagePageLifeCycleObject: Initializable { 13 | let controller = ImageController() 14 | 15 | // MARK: - InitialazableImplementation 16 | 17 | required init() { 18 | 19 | } 20 | } 21 | 22 | private typealias SlideImagePageLifeCycleImplementation = ImagePageLifeCycleObject 23 | extension SlideImagePageLifeCycleImplementation: SlidePageLifeCycle { 24 | var isKeyboardResponsive: Bool { 25 | return false 26 | } 27 | 28 | func didAppear() { } 29 | 30 | func didDissapear() { } 31 | 32 | func viewDidLoad() { } 33 | 34 | func viewDidUnload() { } 35 | 36 | func didStartSliding() { } 37 | 38 | func didCancelSliding() { } 39 | } 40 | 41 | private typealias ViewableImplementation = ImagePageLifeCycleObject 42 | extension ViewableImplementation: Viewable { 43 | var view: UIView { 44 | get { 45 | return controller.view 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/Source/Controllers/VerticalController/Menu/VerticalTitleItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalTitleItem.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class VerticalTitleItem: UIView, Initializable, Selectable, ItemViewable { 13 | private let backgroundSelectedColor = UIColor.white 14 | private let backgroundViewColor = UIColor.white.withAlphaComponent(0.3) 15 | private let backgoundViewHeightMultiplier: CGFloat = 0.67 16 | private let backgroundView = UIView() 17 | 18 | required init() { 19 | isSelected = false 20 | super.init(frame: CGRect.zero) 21 | backgroundView.translatesAutoresizingMaskIntoConstraints = false 22 | backgroundView.backgroundColor = backgroundViewColor 23 | addSubview(backgroundView) 24 | activateBackgroundViewConstraints(view: backgroundView) 25 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(VerticalTitleItem.tapHandler)) 26 | addGestureRecognizer(tapRecognizer) 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | var index: Int = 0 34 | 35 | // MARK: - SelectableImplementation 36 | 37 | @objc var didSelectAction: ((Int) -> Void)? 38 | 39 | var isSelected: Bool { 40 | didSet { 41 | backgroundView.backgroundColor = isSelected ? backgroundSelectedColor : backgroundViewColor 42 | } 43 | } 44 | 45 | // MARK: - ItemViewableImplementation 46 | 47 | typealias Item = VerticalTitleItem 48 | 49 | var view: Item { 50 | return self 51 | } 52 | } 53 | 54 | private typealias PrivateVerticalTitleItem = VerticalTitleItem 55 | private extension PrivateVerticalTitleItem { 56 | func activateBackgroundViewConstraints(view: UIView) { 57 | guard let superview = view.superview else { 58 | return 59 | } 60 | view.translatesAutoresizingMaskIntoConstraints = false 61 | NSLayoutConstraint.activate([ 62 | view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), 63 | view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), 64 | view.topAnchor.constraint(equalTo: superview.topAnchor), 65 | view.heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: backgoundViewHeightMultiplier) 66 | ]) 67 | } 68 | 69 | @objc func tapHandler(_ recognizer: UITapGestureRecognizer) { 70 | if !isSelected { 71 | didSelectAction?(index) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/Source/Controllers/VerticalController/Menu/VerticalTitleScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalTitleScrollView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class VerticalTitleScrollView: TitleScrollView { 13 | private let itemsViewTopOffset: CGFloat = 20 14 | private let itemsViewBottomOffset: CGFloat = 20 15 | private let itemsViewWidth: CGFloat = 22 16 | private let itemWidth: CGFloat = 2.5 17 | private let itemHeight: CGFloat = 120 18 | private let itemHeightMultiplier: CGFloat = 0.67 19 | private let internalBackgroundColor = UIColor(red: 61 / 255, green: 86 / 255, blue: 166 / 255, alpha: 1) 20 | private var internalItems: [View] = [] 21 | private let itemsView = UIView() 22 | 23 | override required init() { 24 | super.init() 25 | backgroundColor = internalBackgroundColor 26 | isScrollEnabled = false 27 | itemsView.translatesAutoresizingMaskIntoConstraints = false 28 | addSubview(itemsView) 29 | activateItemsViewConstraints(view: itemsView) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override var items: [TitleItem] { 37 | return internalItems 38 | } 39 | 40 | override func appendViews(views: [View]) { 41 | var prevView: View? = internalItems.last 42 | internalItems.append(contentsOf: views) 43 | let heightMultiplier = 1 / CGFloat(internalItems.count) 44 | for view in views { 45 | view.translatesAutoresizingMaskIntoConstraints = false 46 | itemsView.addSubview(view) 47 | activateConstraints(view: view, previousView: prevView, heightMultiplier: heightMultiplier) 48 | prevView = view 49 | } 50 | // Update all view heights 51 | internalItems.forEach { updateConstraints(view: $0, heightMultiplier: heightMultiplier) } 52 | } 53 | 54 | override func insertView(view: View, index: Int) { 55 | guard index < internalItems.count else { 56 | return 57 | } 58 | internalItems.insert(view, at: index) 59 | let prevView: TitleItem? = index > 0 ? internalItems[index - 1] : nil 60 | let nextView: TitleItem = internalItems[index + 1] 61 | let heightMultiplier = 1 / CGFloat(internalItems.count) 62 | view.translatesAutoresizingMaskIntoConstraints = false 63 | itemsView.addSubview(view) 64 | // Activate inserted view constraints 65 | activateConstraints(view: view, previousView: prevView, heightMultiplier: heightMultiplier) 66 | if let constraints = nextView.superview?.constraints.filter({ $0.firstItem === nextView}) { 67 | nextView.superview?.removeConstraints(constraints) 68 | } 69 | // Activate next view constraints 70 | activateConstraints(view: nextView, previousView: view, heightMultiplier: heightMultiplier) 71 | // Update all view heights 72 | internalItems.forEach { updateConstraints(view: $0, heightMultiplier: heightMultiplier) } 73 | } 74 | 75 | override func removeViewAtIndex(index: Int) { 76 | guard index < internalItems.count else { 77 | return 78 | } 79 | let view: View = internalItems[index] 80 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 81 | let nextView: View? = index < internalItems.count - 1 ? internalItems[index + 1] : nil 82 | internalItems.remove(at: index) 83 | view.removeFromSuperview() 84 | let heightMultiplier = 1 / CGFloat(internalItems.count) 85 | if let nextView = nextView { 86 | if let constraints = nextView.superview?.constraints.filter({ $0.firstItem === nextView}) { 87 | nextView.superview?.removeConstraints(constraints) 88 | } 89 | activateConstraints(view: nextView, previousView: prevView, heightMultiplier: heightMultiplier) 90 | } 91 | internalItems.forEach { updateConstraints(view: $0, heightMultiplier: heightMultiplier) } 92 | } 93 | 94 | var isTransparent = false { 95 | didSet { 96 | backgroundColor = isTransparent ? UIColor.clear : internalBackgroundColor 97 | } 98 | } 99 | } 100 | 101 | private typealias PrivateVerticalTitleScrollView = VerticalTitleScrollView 102 | private extension PrivateVerticalTitleScrollView { 103 | func activateItemsViewConstraints(view: UIView) { 104 | guard let superview = view.superview else { 105 | return 106 | } 107 | NSLayoutConstraint.activate([ 108 | view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), 109 | view.topAnchor.constraint(equalTo: superview.topAnchor, constant: itemsViewWidth), 110 | view.widthAnchor.constraint(equalToConstant: itemsViewWidth), 111 | view.heightAnchor.constraint(equalTo: superview.heightAnchor, constant: -(itemsViewBottomOffset + itemsViewTopOffset)) 112 | ]) 113 | } 114 | 115 | func activateConstraints(view: UIView, previousView: UIView?, heightMultiplier: CGFloat) { 116 | guard let superview = view.superview else { 117 | return 118 | } 119 | 120 | var constraints: [NSLayoutConstraint] = [] 121 | constraints.append(view.centerXAnchor.constraint(equalTo: superview.centerXAnchor)) 122 | constraints.append(view.widthAnchor.constraint(equalToConstant: itemWidth)) 123 | 124 | let heightSuperview = view.heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: heightMultiplier) 125 | heightSuperview.priority = UILayoutPriority(rawValue: 750) 126 | constraints.append(heightSuperview) 127 | 128 | let heightItem = view.heightAnchor.constraint(lessThanOrEqualToConstant: itemHeight) 129 | heightItem.priority = UILayoutPriority(rawValue: 1000) 130 | constraints.append(heightItem) 131 | 132 | if let previousView = previousView { 133 | constraints.append(view.topAnchor.constraint(equalTo: previousView.bottomAnchor)) 134 | } else { 135 | constraints.append(view.topAnchor.constraint(equalTo: superview.topAnchor)) 136 | } 137 | 138 | NSLayoutConstraint.activate(constraints) 139 | } 140 | 141 | func updateConstraints(view: UIView, heightMultiplier: CGFloat) { 142 | guard let superview = view.superview else { 143 | return 144 | } 145 | 146 | let filterHeightConstraints = itemsView.constraints.filter { constraint in 147 | let areHeightAttributes = constraint.firstAttribute == .height && constraint.secondAttribute == .height 148 | let areAppropriateViews = constraint.firstItem === view && constraint.secondItem === itemsView 149 | return areHeightAttributes && areAppropriateViews 150 | } 151 | itemsView.removeConstraints(filterHeightConstraints) 152 | 153 | let heightConstraint = view.heightAnchor.constraint(equalTo: superview.heightAnchor, multiplier: heightMultiplier) 154 | heightConstraint.priority = UILayoutPriority(rawValue: 750) 155 | heightConstraint.isActive = true 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Example/Source/Controllers/VerticalController/VerticalController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class VerticalController { 13 | private let titleSize: CGFloat = 22.5 14 | private let internalView = VerticalView() 15 | private let slideController: SlideController 16 | 17 | private lazy var removeAction: (() -> Void)? = { [weak self] in 18 | guard let strongSelf = self else { return } 19 | guard let index = strongSelf.slideController.content 20 | .firstIndex(where: { strongSelf.slideController.currentModel === $0 }) else { 21 | return 22 | } 23 | strongSelf.slideController.removeAtIndex(index: index) 24 | } 25 | 26 | private lazy var insertAction: (() -> Void)? = { [weak self] in 27 | guard let strongSelf = self else { return } 28 | let page = SlideLifeCycleObjectBuilder() 29 | guard let index = strongSelf.slideController.content 30 | .firstIndex(where: { strongSelf.slideController.currentModel === $0 }) else { 31 | return 32 | } 33 | strongSelf.slideController.insert(object: page, index: index) 34 | } 35 | 36 | private lazy var appendAction: (() -> Void)? = { [weak self] in 37 | guard let strongSelf = self else { return } 38 | let page = SlideLifeCycleObjectBuilder() 39 | strongSelf.slideController.append(object: [page]) 40 | } 41 | 42 | private lazy var changePositionAction: ((Int) -> ())? = { [weak self] position in 43 | guard let strongSelf = self else { return } 44 | switch position { 45 | case 0: 46 | strongSelf.slideController.titleView.position = TitleViewPosition.beside 47 | strongSelf.slideController.titleView.isTransparent = false 48 | case 1: 49 | strongSelf.slideController.titleView.position = TitleViewPosition.above 50 | strongSelf.slideController.titleView.isTransparent = true 51 | default: 52 | break 53 | } 54 | } 55 | 56 | var optionsController: (ViewAccessible & ContentActionable)? { 57 | didSet { 58 | internalView.optionsView = optionsController?.view 59 | optionsController?.removeDidTapAction = removeAction 60 | optionsController?.insertDidTapAction = insertAction 61 | optionsController?.appendDidTapAction = appendAction 62 | optionsController?.changePositionAction = changePositionAction 63 | } 64 | } 65 | 66 | init() { 67 | let pagesContent = [ 68 | SlideLifeCycleObjectBuilder(), 69 | SlideLifeCycleObjectBuilder(), 70 | SlideLifeCycleObjectBuilder() 71 | ] 72 | slideController = SlideController(pagesContent: pagesContent, startPageIndex: 0, slideDirection: .vertical) 73 | slideController.titleView.position = .above 74 | slideController.titleView.alignment = .left 75 | slideController.titleView.titleSize = titleSize 76 | internalView.contentView = slideController.view 77 | } 78 | } 79 | 80 | private typealias ViewLifeCycleDependableImplementation = VerticalController 81 | extension ViewLifeCycleDependableImplementation: ViewLifeCycleDependable { 82 | func viewDidAppear() { 83 | slideController.viewDidAppear() 84 | } 85 | 86 | func viewDidDisappear() { 87 | slideController.viewDidDisappear() 88 | } 89 | } 90 | 91 | private typealias ViewAccessibleImplementation = VerticalController 92 | extension ViewAccessibleImplementation: ViewAccessible { 93 | var view: UIView { 94 | return internalView 95 | } 96 | } 97 | 98 | private typealias StatusBarAccessibleImplementation = VerticalController 99 | extension StatusBarAccessibleImplementation: StatusBarAccessible { 100 | var statusBarStyle: UIStatusBarStyle { 101 | return .lightContent 102 | } 103 | } 104 | 105 | private typealias TitleAccessibleImplementation = VerticalController 106 | extension TitleAccessibleImplementation: TitleAccessible { 107 | var title: String { 108 | return "Vertical" 109 | } 110 | } 111 | 112 | private typealias TitleColorableImplementation = VerticalController 113 | extension TitleColorableImplementation: TitleColorable { 114 | var titleColor: UIColor { 115 | return .white 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Example/Source/Controllers/VerticalController/VerticalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalView.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class VerticalView: UIView { 13 | var contentView: UIView? { 14 | didSet { 15 | oldValue?.removeFromSuperview() 16 | if let view = contentView { 17 | view.translatesAutoresizingMaskIntoConstraints = false 18 | addSubview(view) 19 | activateContentViewConstraints(view: view) 20 | } 21 | } 22 | } 23 | 24 | var optionsView: UIView? { 25 | didSet { 26 | oldValue?.removeFromSuperview() 27 | if let view = optionsView { 28 | view.translatesAutoresizingMaskIntoConstraints = false 29 | addSubview(view) 30 | activateOptionsViewConstraints(view: view) 31 | } 32 | } 33 | } 34 | } 35 | 36 | private typealias PrivateVerticalView = VerticalView 37 | private extension PrivateVerticalView { 38 | func activateContentViewConstraints(view: UIView) { 39 | guard let superview = view.superview else { 40 | return 41 | } 42 | NSLayoutConstraint.activate([ 43 | view.bottomAnchor.constraint(equalTo: superview.bottomAnchor), 44 | view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), 45 | view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), 46 | view.topAnchor.constraint(equalTo: superview.topAnchor) 47 | ]) 48 | } 49 | 50 | func activateOptionsViewConstraints(view: UIView) { 51 | guard let superview = view.superview else { 52 | return 53 | } 54 | NSLayoutConstraint.activate([ 55 | view.leadingAnchor.constraint(equalTo: superview.leadingAnchor), 56 | view.trailingAnchor.constraint(equalTo: superview.trailingAnchor), 57 | view.bottomAnchor.constraint(equalTo: superview.bottomAnchor), 58 | view.topAnchor.constraint(equalTo: superview.topAnchor) 59 | ]) 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Example/Source/Core/Buttons/ClosureButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureButton.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/9/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Actionable: AnyObject { 12 | var didTouchUpInside: (() -> Void)? { get set } 13 | } 14 | 15 | class ClosureButton: UIButton { 16 | private var internalDidTouchUpInside: (() -> Void)? { 17 | didSet { 18 | if internalDidTouchUpInside != nil { 19 | addTarget(self, action: #selector(didTouchUpInside(_:)), for: .touchUpInside) 20 | } else { 21 | removeTarget(self, action: #selector(didTouchUpInside(_:)), for: .touchUpInside) 22 | } 23 | } 24 | } 25 | } 26 | 27 | private typealias PrivateClosureButton = ClosureButton 28 | private extension PrivateClosureButton { 29 | @objc func didTouchUpInside(_ sender: UIButton) { 30 | didTouchUpInside?() 31 | } 32 | } 33 | 34 | private typealias ActionableImplementation = ClosureButton 35 | extension ActionableImplementation : Actionable { 36 | var didTouchUpInside: (() -> Void)? { 37 | get { 38 | return internalDidTouchUpInside 39 | } 40 | set { 41 | internalDidTouchUpInside = newValue 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/Source/Core/Buttons/FilledButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilledButton.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/9/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import Foundation 11 | 12 | class FilledButton: ClosureButton, ButtonDesignable { 13 | init() { 14 | super.init(frame: CGRect.zero) 15 | backgroundColor = bgColor 16 | titleLabel?.textColor = textColor 17 | titleLabel?.font = textFont 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/Source/Core/UIViewControllers/ContentUIViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentUIViewController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 9/20/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ContentUIViewController: UIViewController where T: ViewAccessible & StatusBarAccessible & TitleDesignable { 12 | var controller: T? { 13 | didSet { 14 | guard let controller = controller else { 15 | return 16 | } 17 | //Bad design, but this is just a demo :) 18 | view = controller.view 19 | automaticallyAdjustsScrollViewInsets = false 20 | } 21 | } 22 | 23 | override var preferredStatusBarStyle: UIStatusBarStyle { 24 | return controller?.statusBarStyle ?? .default 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Example/Source/Core/UIViewControllers/LifecycleContentUIViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifecycleContentUIViewController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/10/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LifecycleContentUIViewController: UIViewController where T: ViewAccessible & StatusBarAccessible & ViewLifeCycleDependable & TitleDesignable { 12 | var controller: T? { 13 | didSet { 14 | guard let controller = controller else { 15 | return 16 | } 17 | //Bad design, but this is just a demo :) 18 | view = controller.view 19 | title = controller.title 20 | automaticallyAdjustsScrollViewInsets = false 21 | } 22 | } 23 | 24 | override func viewDidAppear(_ animated: Bool) { 25 | super.viewDidAppear(animated) 26 | controller?.viewDidAppear() 27 | } 28 | 29 | override func viewDidDisappear(_ animated: Bool) { 30 | super.viewDidDisappear(animated) 31 | controller?.viewDidDisappear() 32 | } 33 | 34 | override var preferredStatusBarStyle: UIStatusBarStyle { 35 | return controller?.statusBarStyle ?? .default 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/Source/Core/UIViewControllers/RootUINavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootUINavigationController.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/9/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RootUINavigationController: UINavigationController { 12 | 13 | init() { 14 | super.init(nibName: nil, bundle: nil) 15 | navigationBar.barTintColor = .purple 16 | navigationBar.setBackgroundImage(UIImage(), for: .default) 17 | navigationBar.shadowImage = UIImage() 18 | navigationBar.isTranslucent = false 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | override var preferredStatusBarStyle: UIStatusBarStyle { 26 | return topViewController?.preferredStatusBarStyle ?? .default 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-60x60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-App-20x20@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-29x29@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-40x40@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-76x76@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-83.5x83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "ItunesArtwork@2x.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "24x24", 113 | "idiom" : "watch", 114 | "filename" : "Icon-24@2x.png", 115 | "scale" : "2x", 116 | "role" : "notificationCenter", 117 | "subtype" : "38mm" 118 | }, 119 | { 120 | "size" : "27.5x27.5", 121 | "idiom" : "watch", 122 | "filename" : "Icon-27.5@2x.png", 123 | "scale" : "2x", 124 | "role" : "notificationCenter", 125 | "subtype" : "42mm" 126 | }, 127 | { 128 | "size" : "29x29", 129 | "idiom" : "watch", 130 | "filename" : "Icon-29@2x.png", 131 | "role" : "companionSettings", 132 | "scale" : "2x" 133 | }, 134 | { 135 | "size" : "29x29", 136 | "idiom" : "watch", 137 | "filename" : "Icon-29@3x.png", 138 | "role" : "companionSettings", 139 | "scale" : "3x" 140 | }, 141 | { 142 | "size" : "40x40", 143 | "idiom" : "watch", 144 | "filename" : "Icon-40@2x.png", 145 | "scale" : "2x", 146 | "role" : "appLauncher", 147 | "subtype" : "38mm" 148 | }, 149 | { 150 | "size" : "44x44", 151 | "idiom" : "watch", 152 | "scale" : "2x", 153 | "role" : "appLauncher", 154 | "subtype" : "40mm" 155 | }, 156 | { 157 | "size" : "50x50", 158 | "idiom" : "watch", 159 | "scale" : "2x", 160 | "role" : "appLauncher", 161 | "subtype" : "44mm" 162 | }, 163 | { 164 | "size" : "86x86", 165 | "idiom" : "watch", 166 | "filename" : "Icon-86@2x.png", 167 | "scale" : "2x", 168 | "role" : "quickLook", 169 | "subtype" : "38mm" 170 | }, 171 | { 172 | "size" : "98x98", 173 | "idiom" : "watch", 174 | "filename" : "Icon-98@2x.png", 175 | "scale" : "2x", 176 | "role" : "quickLook", 177 | "subtype" : "42mm" 178 | }, 179 | { 180 | "size" : "108x108", 181 | "idiom" : "watch", 182 | "scale" : "2x", 183 | "role" : "quickLook", 184 | "subtype" : "44mm" 185 | }, 186 | { 187 | "idiom" : "watch-marketing", 188 | "size" : "1024x1024", 189 | "scale" : "1x" 190 | }, 191 | { 192 | "size" : "44x44", 193 | "idiom" : "watch", 194 | "filename" : "Icon-44@2x.png", 195 | "scale" : "2x", 196 | "role" : "longLook", 197 | "subtype" : "42mm" 198 | } 199 | ], 200 | "info" : { 201 | "version" : 1, 202 | "author" : "xcode" 203 | }, 204 | "properties" : { 205 | "pre-rendered" : true 206 | } 207 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-24@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-24@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-27.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-27.5@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-29@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-29@3x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-44@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-44@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-86@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-86@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-98@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-98@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dog1.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image1.imageset/dog1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/image1.imageset/dog1.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dog2.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image2.imageset/dog2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/image2.imageset/dog2.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dog3.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image3.imageset/dog3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/image3.imageset/dog3.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dog4.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image4.imageset/dog4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/image4.imageset/dog4.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dog5.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/image5.imageset/dog5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/image5.imageset/dog5.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/logo_splash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo_splash.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/logo_splash.imageset/logo_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/logo_splash.imageset/logo_splash.png -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/main_logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Source/Images.xcassets/main_logo.imageset/logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlane/SlideController/c82cac8fd288d1fa9afed1234a5168d4a790dcef/Example/Source/Images.xcassets/main_logo.imageset/logo.pdf -------------------------------------------------------------------------------- /Example/Source/Launch Screen.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 | 46 | 47 | -------------------------------------------------------------------------------- /Example/Source/Protocols/ClosureButtonDesignable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClosureButtonDesignable.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/9/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ButtonDesignable: AnyObject { 12 | var textFont: UIFont { get } 13 | var textColor: UIColor { get } 14 | var bgColor: UIColor { get } 15 | } 16 | 17 | extension ButtonDesignable { 18 | var textFont: UIFont { 19 | return UIFont.systemFont(ofSize: 15) 20 | } 21 | 22 | var textColor: UIColor { 23 | return UIColor.white 24 | } 25 | 26 | var bgColor: UIColor { 27 | return UIColor.purple 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/Source/Protocols/ContentActionable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentActionable.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 10/24/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | protocol ContentActionable { 10 | typealias Action = () -> Void 11 | 12 | var isShowAdvancedActions: Bool { get set } 13 | var removeDidTapAction: Action? { get set } 14 | var insertDidTapAction: Action? { get set } 15 | var appendDidTapAction: Action? { get set } 16 | var changePositionAction: ((Int) -> ())? { get set } 17 | } 18 | -------------------------------------------------------------------------------- /Example/Source/Protocols/StatusBarAccessible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarAccessible.swift 3 | // Example 4 | // 5 | // Created by Vadim Morozov on 12/29/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol StatusBarAccessible: AnyObject { 12 | var statusBarStyle: UIStatusBarStyle { get } 13 | } 14 | -------------------------------------------------------------------------------- /Example/Source/Protocols/TitleDesignable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleDesignable.swift 3 | // Example 4 | // 5 | // Created by Pavel Kondrashkov on 2/19/18. 6 | // Copyright © 2018 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | typealias TitleDesignable = TitleAccessible & TitleColorable 13 | 14 | protocol TitleAccessible: AnyObject { 15 | var title: String { get } 16 | } 17 | 18 | protocol TitleColorable: AnyObject { 19 | var titleColor: UIColor { get } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Source/Protocols/ViewAccessible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Viewable.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 9/20/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ViewAccessible: AnyObject { 12 | var view: UIView { get } 13 | } 14 | -------------------------------------------------------------------------------- /Example/Source/Protocols/ViewLifeCycleDependable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewLifeCycleDependable.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 9/20/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | protocol ViewLifeCycleDependable: AnyObject { 10 | func viewDidAppear() 11 | func viewDidDisappear() 12 | } 13 | -------------------------------------------------------------------------------- /Example/Source/Routres/RootRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootRouter.swift 3 | // SlideController_Example 4 | // 5 | // Created by Evgeny Dedovets on 8/9/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RootRouter { 12 | private let presenter: UINavigationController 13 | 14 | init(presenter: UINavigationController) { 15 | self.presenter = presenter 16 | } 17 | 18 | func openMainScreen(animated: Bool) { 19 | let optionsControler = OptionsController() 20 | optionsControler.openHorizontalDemoAction = openHorizontalDemoAction 21 | optionsControler.openVerticalDemoAction = openVerticalDemoAction 22 | optionsControler.openCarouselDemoAction = openCarouselDemoAction 23 | let vc = ContentUIViewController() 24 | vc.controller = optionsControler 25 | setupNavigationBar(presenter: presenter, controller: optionsControler) 26 | presenter.setViewControllers([vc], animated: animated) 27 | } 28 | 29 | func showHorizontalPage(animated: Bool) { 30 | let actionsController = ActionsController() 31 | let horizontalController = HorizontalController() 32 | horizontalController.optionsController = actionsController 33 | let vc = LifecycleContentUIViewController() 34 | vc.controller = horizontalController 35 | setupNavigationBar(presenter: presenter, controller: horizontalController) 36 | presenter.pushViewController(vc, animated: animated) 37 | } 38 | 39 | func showVerticalPage(animated: Bool) { 40 | let actionsController = ActionsController() 41 | let verticalController = VerticalController() 42 | verticalController.optionsController = actionsController 43 | let lifecycleController = LifecycleContentUIViewController() 44 | lifecycleController.controller = verticalController 45 | setupNavigationBar(presenter: presenter, controller: verticalController) 46 | presenter.pushViewController(lifecycleController, animated: animated) 47 | } 48 | 49 | func showCarouselPage(animated: Bool) { 50 | let actionsController = ActionsController() 51 | actionsController.isShowAdvancedActions = false 52 | let carouselController = CarouselController() 53 | carouselController.optionsController = actionsController 54 | let lifecycleController = LifecycleContentUIViewController() 55 | lifecycleController.controller = carouselController 56 | setupNavigationBar(presenter: presenter, controller: carouselController) 57 | presenter.pushViewController(lifecycleController, animated: animated) 58 | } 59 | 60 | private lazy var openHorizontalDemoAction: (() -> Void)? = { [weak self] in 61 | guard let strongSelf = self else { return } 62 | strongSelf.showHorizontalPage(animated: true) 63 | } 64 | 65 | private lazy var openVerticalDemoAction: (() -> Void)? = { [weak self] in 66 | guard let strongSelf = self else { 67 | return 68 | } 69 | strongSelf.showVerticalPage(animated: true) 70 | } 71 | 72 | private lazy var openCarouselDemoAction: (() -> Void)? = { [weak self] in 73 | guard let strongSelf = self else { 74 | return 75 | } 76 | strongSelf.showCarouselPage(animated: true) 77 | } 78 | 79 | private func setupNavigationBar(presenter: UINavigationController, controller: TitleDesignable) { 80 | let shadow = NSShadow() 81 | shadow.shadowBlurRadius = 2 82 | shadow.shadowOffset = CGSize(width: 0, height: 1) 83 | shadow.shadowColor = UIColor(white: 0.5, alpha: 0.5) 84 | 85 | presenter.navigationBar.tintColor = controller.titleColor 86 | presenter.navigationBar.titleTextAttributes = [ 87 | .foregroundColor: controller.titleColor, 88 | .shadow: shadow 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Example/Source/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | SlideController 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | Launch Screen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UIStatusBarStyle 34 | UIStatusBarStyleLightContent 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/Source/Supporting Files/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | SlideController 4 | 5 | Created by Vadim Morozov on 10/23/17. 6 | Copyright © 2017 Touchlane LLC. All rights reserved. 7 | */ 8 | 9 | "MenuButtonTitle" = "Menu"; 10 | "HorizontalSampleButtonTitle" = "Horizontal Sample"; 11 | "VerticalSampleButtonTitle" = "Vertical Sample"; 12 | "CarouselSampleButtonTitle" = "Carousel Sample"; 13 | "BesideSegmentTitle" = "Beside"; 14 | "AboveSegmentTitle" = "Above"; 15 | "InsertButtonTitle" = "Insert"; 16 | "RemoveButtonTitle" = "Remove"; 17 | "AppendButtonTitle" = "Append"; 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Touchlane LLC tech@touchlane.com 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 | ![LOGO](https://github.com/touchlane/SlideController/blob/master/Assets/logo.svg) 2 | 3 | ![Language](https://img.shields.io/badge/swift-5.0-orange.svg) 4 | [![Build Status](https://travis-ci.org/touchlane/SlideController.svg?branch=master)](https://travis-ci.org/touchlane/SlideController) 5 | [![codecov.io](https://codecov.io/gh/touchlane/SlideController/branch/master/graphs/badge.svg)](https://codecov.io/gh/codecov/SlideController/branch/master) 6 | [![Version](https://img.shields.io/cocoapods/v/SlideController.svg?style=flat)](http://cocoapods.org/pods/SlideController) 7 | [![License](https://img.shields.io/cocoapods/l/SlideController.svg?style=flat)](http://cocoapods.org/pods/SlideController) 8 | [![Platform](https://img.shields.io/cocoapods/p/SlideController.svg?style=flat)](http://cocoapods.org/pods/SlideController) 9 | 10 | SlideController is a simple and flexible UI component fully written in Swift. Built using power of generic types, it is a nice alternative to UIPageViewController. 11 | 12 | ![Horizontal](Example/Assets/horizontal.gif) 13 | ![Vertical](Example/Assets/vertical.gif) 14 | ![Carousel](Example/Assets/carousel.gif) 15 | 16 | # Requirements 17 | 18 | * iOS 9.0+ 19 | * Xcode 10.2+ 20 | * Swift 5.0+ 21 | 22 | # Installation 23 | 24 | ## CocoaPods 25 | 26 | [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: 27 | 28 | ```$ gem install cocoapods``` 29 | 30 | To integrate SlideController into your Xcode project using CocoaPods, specify it in your ```Podfile```: 31 | 32 | ```ruby 33 | source 'https://github.com/CocoaPods/Specs.git' 34 | platform :ios, '9.0' 35 | use_frameworks! 36 | 37 | target '' do 38 | pod 'SlideController' 39 | end 40 | ``` 41 | 42 | Then, run the following command: 43 | 44 | ```$ pod install``` 45 | 46 | # Usage 47 | 48 | ```swift 49 | import SlideController 50 | ``` 51 | 52 | 1) Create content 53 | ```swift 54 | let content = [ 55 | SlideLifeCycleObjectBuilder(), 56 | SlideLifeCycleObjectBuilder(), 57 | SlideLifeCycleObjectBuilder() 58 | ] 59 | ``` 60 | 61 | * ``PageLifeCycleObject`` is any object conforms to ``Initializable, Viewable, SlidePageLifeCycle `` protocols 62 | 63 | 2) Initialize SlideController 64 | ```swift 65 | slideController = SlideController( 66 | pagesContent: content, 67 | startPageIndex: 0, 68 | slideDirection: .horizontal) 69 | ``` 70 | 71 | * ``CustomTitleView`` is subclass of ``TitleScrollView`` 72 | * ``CustomTitleItem`` is subclass of ``UIView`` and conforms to ``Initializable, ItemViewable, Selectable`` protocols 73 | 74 | 3) Add ``slideController.view`` to view hierarchy 75 | 76 | 4) Call ``slideController.viewDidAppear()`` and ``slideController.viewDidDisappear()`` in appropriate UIViewController methods: 77 | 78 | ```swift 79 | override func viewDidAppear(_ animated: Bool) { 80 | super.viewDidAppear(animated) 81 | slideController.viewDidAppear() 82 | } 83 | ``` 84 | 85 | ```swift 86 | override func viewDidDisappear(_ animated: Bool) { 87 | super.viewDidDisappear(animated) 88 | slideController.viewDidDisappear() 89 | } 90 | ``` 91 | 92 | # Documentation 93 | 94 | ### SlideController 95 | 96 | Default initializer of `SlideController`. 97 | `pagesContent` - initial content of controller, can be empty. 98 | `startPageIndex` - page index that should be displayed initially. 99 | `slideDirection` - slide direction. `.horizontal` or `.vertical`. 100 | ```swift 101 | public init(pagesContent: [SlideLifeCycleObjectProvidable], 102 | startPageIndex: Int = 0, 103 | slideDirection: SlideDirection) 104 | ``` 105 | 106 | Returns `titleView` instanсe of `TitleScrollView`. 107 | ```swift 108 | public var titleView: T { get } 109 | ``` 110 | 111 | Returns `LifeCycleObject` for currently displayed page. 112 | ```swift 113 | public var currentModel: SlideLifeCycleObjectProvidable? { get } 114 | ``` 115 | 116 | Returns array of `LifeCycleObject` that corresponds to `SlideController`'s content. 117 | ```swift 118 | public private(set) var content: [SlideLifeCycleObjectProvidable] 119 | ``` 120 | When set to `true` unloads content when it is out of screen bounds. The default value is `true`. 121 | ```swift 122 | public var isContentUnloadingEnabled: Bool { get set } 123 | ``` 124 | 125 | When set to `true` scrolling in the direction of last item will result jumping to the first item. Makes scrolling infinite. The default value is `false`. 126 | ```swift 127 | public var isCarousel: Bool { get set } 128 | ``` 129 | 130 | If the value of this property is `true`, content scrolling is enabled, and if it is `false`, content scrolling is disabled. The default is `true`. 131 | ```swift 132 | public var isScrollEnabled: Bool { get set } 133 | ``` 134 | 135 | Appends pages array of `SlideLifeCycleObjectProvidable` to the end of sliding content. 136 | ```swift 137 | public func append(object objects: [SlideLifeCycleObjectProvidable]) 138 | ``` 139 | 140 | Inserts `SlideLifeCycleObjectProvidable` page object at `index` in sliding content. 141 | ```swift 142 | public func insert(object: SlideLifeCycleObjectProvidable, index: Int) 143 | ``` 144 | Removes a page at `index`. 145 | ```swift 146 | public func removeAtIndex(index: Int) 147 | ``` 148 | 149 | Slides content to page at `pageIndex` with sliding animation if `animated` is set to `true`. Using `forced` is not recommended, it will perform shift even if other shift animation in progress or `pageIndex` equals current page. The default value of `animated` is `true`. The default value of `forced` is `false`. 150 | ```swift 151 | public func shift(pageIndex: Int, animated: Bool = default, forced: Bool = default) 152 | ``` 153 | 154 | Slides content the next page with sliding animation if `animated` is set to `true`. The default value of `animated` is `true`. 155 | ```swift 156 | public func showNext(animated: Bool = default) 157 | ``` 158 | 159 | Lets the `SlideController` know when it is displayed on the screen. Used for correctly triggering `LifeCycle` events. 160 | ```swift 161 | public func viewDidAppear() 162 | ``` 163 | 164 | Lets the `SlideController` know when it is not displayed on the screen. Used for correctly triggering `LifeCycle` events. 165 | ```swift 166 | public func viewDidDisappear() 167 | ``` 168 | ___ 169 | ### TitleScrollView 170 | 171 | Alignment of title view. Supports `.top`, `.bottom`, `.left`, `.right`. The default value of `alignment` is `.top`. 172 | ```swift 173 | public var alignment: SlideController.TitleViewAlignment { get set } 174 | ``` 175 | 176 | The size of `TitleScrollView`. For `.horizontal` slide direction of `SlideController` the `titleSize` corresponds to `height`. For `.vertical` slide direction of `SlideController` the `titleSize` corresponds to `width`. The default value of `titleSize` is `84`. 177 | ```swift 178 | open var titleSize: CGFloat { get set } 179 | ``` 180 | 181 | Array of title items that displayed in `TitleScrollView`. 182 | ```swift 183 | open var items: [TitleItem] { get } 184 | ``` 185 | ___ 186 | -------------------------------------------------------------------------------- /SlideController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'SlideController' 3 | s.version = '1.5.1' 4 | s.summary = 'SlideController is replacement for Apple\'s UIPageControl completely written in Swift using power of generic types.' 5 | s.description = <<-DESC 6 | Swipe between pages with an interactive title navigation control. Configure horizontal or vertical chains for unlimited pages amount. 7 | DESC 8 | s.homepage = 'https://github.com/touchlane/SlideController' 9 | s.license = { :type => 'MIT', :file => 'LICENSE' } 10 | s.author = { 'Touchlane LLC' => 'tech@touchlane.com' } 11 | s.source = { :git => 'https://github.com/touchlane/SlideController.git', :tag => s.version.to_s } 12 | s.ios.deployment_target = '9.0' 13 | s.swift_version = '5.0' 14 | s.source_files = 'Source/*.swift' 15 | end 16 | -------------------------------------------------------------------------------- /SlideController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SlideController.xcodeproj/xcshareddata/xcschemes/SlideController.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 67 | 68 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /SlideController.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SlideController.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/SlideContainerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlideContainerController.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 4/24/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | ///SlideContainerController do control for specific container view 12 | final class SlideContainerController { 13 | private var internalView = InternalView() 14 | private var isViewLoaded = false 15 | 16 | ///Property to indicate if target view mounted to container 17 | var hasContent: Bool { 18 | return self.isViewLoaded 19 | } 20 | 21 | ///Implements lazy load, add target view as subview to container view when needed and set hasContent = true 22 | func load(view: UIView) { 23 | guard !self.isViewLoaded else { 24 | return 25 | } 26 | self.isViewLoaded = true 27 | self.internalView.addSubview(view) 28 | view.frame = self.internalView.bounds 29 | } 30 | 31 | ///Removes view from container and sets hasContent = false 32 | func unloadView() { 33 | guard self.isViewLoaded else { 34 | return 35 | } 36 | self.isViewLoaded = false 37 | self.internalView.subviews.forEach({ $0.removeFromSuperview() }) 38 | } 39 | } 40 | 41 | ///Viewable protocol implementation 42 | private typealias ViewableImplementation = SlideContainerController 43 | extension ViewableImplementation: Viewable { 44 | var view: UIView { 45 | return self.internalView 46 | } 47 | } 48 | 49 | ///Internal view for SlideContainerController 50 | private final class InternalView: UIView { 51 | private var oldSize: CGSize = .zero 52 | 53 | override func layoutSubviews() { 54 | guard self.oldSize != self.bounds.size else { 55 | return 56 | } 57 | super.layoutSubviews() 58 | 59 | self.subviews.first?.frame = self.bounds 60 | self.oldSize = self.bounds.size 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/SlideContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlideContainerView.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 4/25/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | ///Represents container view for one page content 12 | final class SlideContainerView: UIView { 13 | private var internalView: UIView 14 | 15 | private var oldSize: CGSize = .zero 16 | 17 | /// - Parameter view: The view to show as content. 18 | init(view: UIView) { 19 | self.internalView = view 20 | super.init(frame: .zero) 21 | self.clipsToBounds = true 22 | self.addSubview(view) 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override func layoutSubviews() { 30 | guard self.oldSize != self.bounds.size else { 31 | return 32 | } 33 | 34 | super.layoutSubviews() 35 | 36 | guard !self.isHidden else { 37 | return 38 | } 39 | 40 | self.internalView.frame = self.bounds 41 | self.oldSize = self.bounds.size 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/SlideContentController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlideContentController.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 4/24/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal typealias EdgeContainers = (left: SlideContainerController, right: SlideContainerController) 12 | 13 | ///SlideContentController do control for SlideContentView, 14 | ///manage container controllers and mount target content when needed 15 | final class SlideContentController { 16 | private var slideDirection: SlideDirection! 17 | 18 | ///Depend on set SlideDirection contentSize indicate width or height of the SlideContentView 19 | var contentSize: CGFloat = 0 20 | 21 | ///Container controllers 22 | internal private(set) var containers = [SlideContainerController]() 23 | 24 | ///Superview for container views 25 | internal private(set) var slideContentView: SlideContentView 26 | 27 | ///Left and right temp containers to support infinite scrolling 28 | internal private(set) var edgeContainers: EdgeContainers? 29 | 30 | ///Enables infinite circular scrolling 31 | internal var isCarousel = false { 32 | didSet { 33 | if isCarousel { 34 | addEdgeContainersIfNeeded() 35 | } else { 36 | removeEdgeContainersIfNeeded() 37 | } 38 | } 39 | } 40 | 41 | /// - Parameter pagesCount: number of pages 42 | /// - Parameter slideDirection: indicates the target slide direction 43 | init(pagesCount: Int, slideDirection: SlideDirection) { 44 | self.slideDirection = slideDirection 45 | slideContentView = SlideContentView(slideDirection: slideDirection) 46 | if pagesCount > 0 { 47 | append(pagesCount: pagesCount) 48 | } 49 | } 50 | 51 | ///Append specified number of containers 52 | ///- Parameter pagesCount: number of containers to be added 53 | func append(pagesCount: Int) { 54 | var newControllers = [SlideContainerController]() 55 | for _ in 0.. (() -> Void)? { 105 | guard containers.indices.contains(index) else { 106 | return nil 107 | } 108 | let offsetCorrection = edgeContainers == nil ? 0 : contentSize 109 | var offsetPoint: CGPoint 110 | var startOffsetPoint = slideContentView.contentOffset 111 | var endOffsetPoint: CGPoint 112 | if slideDirection == .horizontal { 113 | if index < currentIndex { 114 | offsetPoint = CGPoint(x: contentSize * CGFloat(integerLiteral: index) + offsetCorrection, y: 0) 115 | startOffsetPoint = CGPoint(x: contentSize * CGFloat(integerLiteral: index + 1) + offsetCorrection, y: 0) 116 | endOffsetPoint = offsetPoint 117 | } else if index == currentIndex{ 118 | offsetPoint = CGPoint(x: contentSize * CGFloat(integerLiteral: currentIndex) + offsetCorrection, y: 0) 119 | endOffsetPoint = CGPoint(x: contentSize * CGFloat(integerLiteral: index) + offsetCorrection, y: 0) 120 | } else { 121 | offsetPoint = CGPoint(x: contentSize * CGFloat(integerLiteral: currentIndex + 1) + offsetCorrection, y: 0) 122 | endOffsetPoint = CGPoint(x: contentSize * CGFloat(integerLiteral: index) + offsetCorrection, y: 0) 123 | } 124 | } else { 125 | if index < currentIndex { 126 | offsetPoint = CGPoint(x: 0, y: contentSize * CGFloat(integerLiteral: index) + offsetCorrection) 127 | startOffsetPoint = CGPoint(x: 0, y: contentSize * CGFloat(integerLiteral: index + 1) + offsetCorrection) 128 | endOffsetPoint = offsetPoint 129 | } else if index == currentIndex{ 130 | offsetPoint = CGPoint(x: 0, y: contentSize * CGFloat(integerLiteral: currentIndex) + offsetCorrection) 131 | endOffsetPoint = CGPoint(x: 0, y: contentSize * CGFloat(integerLiteral: index) + offsetCorrection) 132 | } else { 133 | offsetPoint = CGPoint(x: 0, y: contentSize * CGFloat(integerLiteral: currentIndex + 1) + offsetCorrection) 134 | endOffsetPoint = CGPoint(x: 0, y: contentSize * CGFloat(integerLiteral: index) + offsetCorrection) 135 | } 136 | } 137 | let indexCorrection = edgeContainers == nil ? 0 : 1 138 | var viewIndices: [Int] = [] 139 | if currentIndex - index > 1 { 140 | for i in index + 1...currentIndex - 1 { 141 | viewIndices.append(i + indexCorrection) 142 | } 143 | } else if index - currentIndex > 1 { 144 | for i in currentIndex + 1...index - 1 { 145 | viewIndices.append(i + indexCorrection) 146 | } 147 | } 148 | // Before animation 149 | slideContentView.hideContainers(at: viewIndices) 150 | slideContentView.setContentOffset(startOffsetPoint, animated: false) 151 | // Animation 152 | slideContentView.setContentOffset(offsetPoint, animated: animated) 153 | 154 | let afterAnimation = { [weak self] in 155 | guard let strongSelf = self else { 156 | return 157 | } 158 | /// Disable scrollView delegate so we won't get scrollViewDidScroll calls 159 | /// when transition through multiple pages is finished 160 | let delegate = strongSelf.slideContentView.delegate 161 | strongSelf.slideContentView.delegate = nil 162 | strongSelf.slideContentView.showContainers(at: viewIndices) 163 | strongSelf.slideContentView.setContentOffset(endOffsetPoint, animated: false) 164 | strongSelf.slideContentView.delegate = delegate 165 | } 166 | if animated { 167 | return afterAnimation 168 | } else { 169 | afterAnimation() 170 | } 171 | return nil 172 | } 173 | 174 | private func addEdgeContainersIfNeeded() { 175 | guard edgeContainers == nil && containers.count > 1 else { 176 | return 177 | } 178 | edgeContainers = (left: SlideContainerController(), right: SlideContainerController()) 179 | slideContentView.insertView(view: edgeContainers!.left.view, index: 0) 180 | slideContentView.appendViews(views: [edgeContainers!.right.view]) 181 | } 182 | 183 | private func removeEdgeContainersIfNeeded() { 184 | guard edgeContainers != nil && containers.count <= 1 else { 185 | return 186 | } 187 | edgeContainers = nil 188 | slideContentView.removeViewAtIndex(index: 0) 189 | slideContentView.removeViewAtIndex(index: containers.count) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Source/SlideContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlideContentView.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 3/13/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class SlideContentView: UIScrollView { 12 | private let slideDirection: SlideDirection 13 | private var containers: [SlideContainerView] = [] 14 | 15 | ///Simple hack to be notified when layout completed 16 | var firstLayoutAction: (() -> Void)? 17 | internal private(set) var isLayouted = false 18 | 19 | ///Notifies on each size or content size update 20 | var changeLayoutAction: (() -> ())? 21 | private var previousSize: CGSize = .zero 22 | private var previousContentSize: CGSize = .zero 23 | 24 | /// - Parameter slideDirection: indicates the target slide direction 25 | init(slideDirection: SlideDirection) { 26 | self.slideDirection = slideDirection 27 | super.init(frame: .zero) 28 | self.isPagingEnabled = true 29 | self.bounces = false 30 | self.showsVerticalScrollIndicator = false 31 | self.showsHorizontalScrollIndicator = false 32 | self.isDirectionalLockEnabled = true 33 | if #available(iOS 11.0, *) { 34 | self.contentInsetAdjustmentBehavior = .never 35 | } 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override func layoutSubviews() { 43 | guard self.bounds.size != self.previousSize || self.contentSize != self.previousContentSize else { 44 | return 45 | } 46 | 47 | super.layoutSubviews() 48 | if !self.isLayouted { 49 | self.isLayouted = true 50 | self.firstLayoutAction?() 51 | } 52 | 53 | self.layoutContainers(direction: self.slideDirection) 54 | 55 | self.previousSize = self.bounds.size 56 | self.previousContentSize = self.contentSize 57 | self.changeLayoutAction?() 58 | } 59 | 60 | func hideContainers(at indices: [Int]) { 61 | let pages = containers 62 | .enumerated() 63 | .filter({ indices.contains($0.offset) }) 64 | .map({ $0.element }) 65 | for page in pages { 66 | page.isHidden = true 67 | } 68 | self.layoutContainers(direction: self.slideDirection) 69 | } 70 | 71 | func showContainers(at indices: [Int]) { 72 | let pages = containers 73 | .enumerated() 74 | .filter({ indices.contains($0.offset) }) 75 | .map({ $0.element }) 76 | for page in pages { 77 | page.isHidden = false 78 | } 79 | self.layoutContainers(direction: self.slideDirection) 80 | } 81 | 82 | private func layoutContainers(direction: SlideDirection) { 83 | let size = self.bounds.size 84 | 85 | var scrollAxisOffset: CGFloat = 0 86 | for container in self.containers { 87 | guard !container.isHidden else { 88 | continue 89 | } 90 | 91 | let origin: CGPoint 92 | switch direction { 93 | case .horizontal: 94 | origin = CGPoint(x: scrollAxisOffset, y: 0) 95 | scrollAxisOffset += size.width 96 | case .vertical: 97 | origin = CGPoint(x: 0, y: scrollAxisOffset) 98 | scrollAxisOffset += size.height 99 | } 100 | 101 | container.frame = CGRect(origin: origin, size: size) 102 | } 103 | 104 | switch direction { 105 | case .horizontal: 106 | self.contentSize = CGSize(width: scrollAxisOffset, height: size.height) 107 | case .vertical: 108 | self.contentSize = CGSize(width: size.width, height: scrollAxisOffset) 109 | } 110 | } 111 | } 112 | 113 | private typealias ViewSlidableImplementation = SlideContentView 114 | extension ViewSlidableImplementation: ViewSlidable { 115 | typealias View = UIView 116 | 117 | func appendViews(views: [View]) { 118 | for view in views { 119 | view.backgroundColor = .clear 120 | let container = SlideContainerView(view: view) 121 | self.containers.append(container) 122 | self.addSubview(container) 123 | } 124 | 125 | self.layoutContainers(direction: self.slideDirection) 126 | } 127 | 128 | func insertView(view: View, index: Int) { 129 | guard index < self.containers.count else { 130 | return 131 | } 132 | 133 | view.backgroundColor = .clear 134 | let container = SlideContainerView(view: view) 135 | self.containers.insert(container, at: index) 136 | self.addSubview(container) 137 | 138 | self.layoutContainers(direction: self.slideDirection) 139 | } 140 | 141 | func removeViewAtIndex(index: Int) { 142 | guard index < self.containers.count else { 143 | return 144 | } 145 | 146 | let container = self.containers[index] 147 | self.containers.remove(at: index) 148 | container.removeFromSuperview() 149 | 150 | self.layoutContainers(direction: self.slideDirection) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Source/SlideLifeCycleObjectBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PageScrollViewModel.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 4/16/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | public protocol SlideLifeCycleObjectProvidable: AnyObject { 10 | var lifeCycleObject: SlideLifeCycleObject { get } 11 | } 12 | 13 | open class SlideLifeCycleObjectBuilder: SlideLifeCycleObjectProvidable { 14 | ///Internal LifeCycle Object 15 | private var object: T? 16 | 17 | ///Use to create model with prebuilt LifeCycle object 18 | public init(object: T) { 19 | self.object = object 20 | } 21 | 22 | public init() { } 23 | 24 | // MARK: - SlideLifeCycleObjectProvidableImplementation 25 | open var lifeCycleObject: SlideLifeCycleObject { 26 | return buildObjectIfNeeded() 27 | } 28 | } 29 | 30 | private typealias PrivateSlidePageModel = SlideLifeCycleObjectBuilder 31 | extension PrivateSlidePageModel { 32 | ///Genarate LifeCycle object of specified type when needed 33 | func buildObjectIfNeeded() -> SlideLifeCycleObject { 34 | if let object = object { 35 | return object 36 | } 37 | object = T() 38 | return object! 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Source/SlideView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollContainerView.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 5/6/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SlideView: UIView, TitleViewConfigurationDelegate where T: ViewSlidable, T: UIScrollView, T: TitleConfigurable { 12 | private var oldSize: CGSize = .zero 13 | 14 | var contentView: UIView? { 15 | didSet { 16 | oldValue?.removeFromSuperview() 17 | if let view = self.contentView { 18 | self.addSubview(view) 19 | } 20 | self.layoutContainers() 21 | } 22 | } 23 | 24 | var titleView: T? { 25 | didSet { 26 | oldValue?.removeFromSuperview() 27 | if let view = self.titleView { 28 | view.titleViewConfigurationDelegate = self 29 | self.addSubview(view) 30 | } 31 | self.layoutContainers() 32 | } 33 | } 34 | 35 | override func layoutSubviews() { 36 | guard self.bounds.size != self.oldSize else { 37 | return 38 | } 39 | 40 | super.layoutSubviews() 41 | self.layoutContainers() 42 | self.oldSize = self.bounds.size 43 | } 44 | 45 | private func layoutContainers() { 46 | if let titleView = self.titleView { 47 | let alignment = titleView.alignment 48 | let size = titleView.titleSize 49 | titleView.frame = self.titleFrame(in: self.bounds, alignment: alignment, size: size) 50 | 51 | if titleView.position == TitleViewPosition.beside { 52 | self.contentView?.frame = self.contentFrame(in: self.bounds, alignment: alignment, size: size) 53 | } else { 54 | self.contentView?.frame = self.bounds 55 | } 56 | } else { 57 | self.contentView?.frame = self.bounds 58 | } 59 | } 60 | 61 | private func titleFrame(in bounds: CGRect, alignment: TitleViewAlignment, size: CGFloat) -> CGRect { 62 | switch alignment { 63 | case .top: 64 | return CGRect(x: 0, y: 0, width: bounds.width, height: size) 65 | case .bottom: 66 | return CGRect(x: 0, y: bounds.height - size, width: bounds.width, height: size) 67 | case .left: 68 | return CGRect(x: 0, y: 0, width: size, height: bounds.height) 69 | case .right: 70 | return CGRect(x: bounds.width - size, y: 0, width: size, height: bounds.height) 71 | } 72 | } 73 | private func contentFrame(in bounds: CGRect, alignment: TitleViewAlignment, size: CGFloat) -> CGRect { 74 | switch alignment { 75 | case .top: 76 | return CGRect(x: 0, y: size, width: bounds.width, height: bounds.height - size) 77 | case .bottom: 78 | return CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height - size) 79 | case .left: 80 | return CGRect(x: size, y: 0, width: bounds.width - size, height: bounds.height) 81 | case .right: 82 | return CGRect(x: 0, y: 0, width: bounds.width - size, height: bounds.height) 83 | } 84 | } 85 | 86 | // MARK: - TitleViewConfigurationDelegateImplementation 87 | func didChangeAlignment(alignment: TitleViewAlignment) { 88 | self.layoutContainers() 89 | } 90 | 91 | func didChangeTitleSize(size: CGFloat) { 92 | if self.titleView != nil { 93 | self.layoutContainers() 94 | } 95 | } 96 | 97 | func didChangePosition(position: TitleViewPosition) { 98 | self.layoutContainers() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Source/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.5.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/Supporting Files/SlideController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SlideController.h 3 | // SlideController 4 | // 5 | // Created by Pavel Kondrashkov on 11/14/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for SlideController. 12 | FOUNDATION_EXPORT double SlideControllerVersionNumber; 13 | 14 | //! Project version string for SlideController. 15 | FOUNDATION_EXPORT const unsigned char SlideControllerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Source/TitleItemController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleItemController.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 4/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TitleItemController: TitleItemControllableObject where T: TitleItemObject, T: UIView { 12 | private var item = T() 13 | typealias Item = T.Item 14 | 15 | // MARK: - InitializableImplementation 16 | required init() { 17 | 18 | } 19 | 20 | // MARK: - ItemViewableImplementation 21 | var view: Item { 22 | return item.view 23 | } 24 | 25 | // MARK: - SelectableImplementation 26 | var isSelected: Bool { 27 | get { 28 | return item.isSelected 29 | } 30 | set { 31 | item.isSelected = newValue 32 | } 33 | } 34 | 35 | var didSelectAction: ((Int) -> ())? { 36 | get { 37 | return item.didSelectAction 38 | } 39 | set { 40 | item.didSelectAction = newValue 41 | } 42 | } 43 | 44 | var index: Int { 45 | get { 46 | return item.index 47 | } 48 | set { 49 | item.index = newValue 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Source/TitleScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleScrollView.swift 3 | // SlideController 4 | // 5 | // Created by Evgeny Dedovets on 4/17/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol TitleConfigurable: AnyObject { 12 | associatedtype TitleItem: UIView 13 | var items: [TitleItem] { get } 14 | var alignment: TitleViewAlignment { get set } 15 | var position: TitleViewPosition { get set } 16 | var titleSize: CGFloat { get set } 17 | 18 | /// Called when user slides or shifts content, 19 | /// Use this method to implement sliding indicator 20 | /// - Parameters: 21 | /// - position: updated position for sliding indicator (x or y depends for horizontal and vertical respectively) 22 | /// - size: updated size for indicator (width or height for horizontal and vertical respectively) 23 | /// - animated: should update position and size animated 24 | func indicator(position: CGFloat, size: CGFloat, animated: Bool) 25 | 26 | /// Return `true` if sliding indicator should update position animated 27 | /// - Parameters: 28 | /// - index: target index for sliding indicator 29 | func shouldAnimateIndicatorOnSelection(index: Int) -> Bool 30 | 31 | var titleViewConfigurationDelegate: TitleViewConfigurationDelegate? { get set } 32 | } 33 | 34 | public protocol TitleViewConfigurationDelegate: AnyObject { 35 | func didChangeAlignment(alignment: TitleViewAlignment) 36 | func didChangeTitleSize(size: CGFloat) 37 | func didChangePosition(position: TitleViewPosition) 38 | } 39 | 40 | open class TitleScrollView: UIScrollView, ViewSlidable, TitleConfigurable where T: UIView, T: TitleItemObject { 41 | public typealias View = T 42 | public typealias TitleItem = View 43 | public private(set) var isLayouted = false 44 | private var previousSize: CGSize = .zero 45 | private var previousContentSize: CGSize = .zero 46 | 47 | public init() { 48 | super.init(frame: .zero) 49 | self.showsVerticalScrollIndicator = false 50 | self.showsHorizontalScrollIndicator = false 51 | if #available(iOS 11.0, *) { 52 | self.contentInsetAdjustmentBehavior = .never 53 | } 54 | } 55 | 56 | required public init?(coder aDecoder: NSCoder) { 57 | fatalError("init(coder:) has not been implemented") 58 | } 59 | 60 | override open func layoutSubviews() { 61 | super.layoutSubviews() 62 | 63 | if !self.isLayouted { 64 | self.isLayouted = true 65 | self.firstLayoutAction?() 66 | } 67 | 68 | guard self.bounds.size != self.previousSize || self.contentSize != self.previousContentSize else { 69 | return 70 | } 71 | 72 | self.previousSize = self.bounds.size 73 | self.previousContentSize = self.contentSize 74 | self.changeLayoutAction?() 75 | } 76 | 77 | // MARK: - ViewSlidableImplementation 78 | open func appendViews(views: [View]) { } 79 | 80 | open func insertView(view: View, index: Int) { } 81 | 82 | open func removeViewAtIndex(index: Int) { } 83 | 84 | ///Simple hack to be notified when layout completed 85 | open var firstLayoutAction: (() -> Void)? 86 | 87 | ///Notifies on each size or content size update 88 | open var changeLayoutAction: (() -> Void)? 89 | 90 | // MARK: - TitleConfigurableImplementation 91 | 92 | 93 | /// Alignment of title view. Supports `.top`, `.bottom`, `.left`, `.right`. The default value of `alignment` is `.top`. 94 | public var alignment: TitleViewAlignment = .top { 95 | didSet { 96 | if self.alignment != oldValue { 97 | self.titleViewConfigurationDelegate?.didChangeAlignment(alignment: self.alignment) 98 | } 99 | } 100 | } 101 | 102 | /// The size of `TitleScrollView`. For `.horizontal` slide direction of `SlideController` the `titleSize` corresponds to `height`. For `.vertical` slide direction of `SlideController` the `titleSize` corresponds to `width`. The default value of `titleSize` is `84`. 103 | open var titleSize: CGFloat = 84 { 104 | didSet { 105 | if self.titleSize != oldValue { 106 | self.titleViewConfigurationDelegate?.didChangeTitleSize(size: self.titleSize) 107 | } 108 | } 109 | } 110 | 111 | open var position: TitleViewPosition = .beside { 112 | didSet { 113 | if self.position != oldValue { 114 | self.titleViewConfigurationDelegate?.didChangePosition(position: self.position) 115 | } 116 | } 117 | } 118 | 119 | open func indicator(position: CGFloat, size: CGFloat, animated: Bool) { } 120 | 121 | open func shouldAnimateIndicatorOnSelection(index: Int) -> Bool { 122 | return false 123 | } 124 | 125 | weak public var titleViewConfigurationDelegate: TitleViewConfigurationDelegate? 126 | 127 | /// Array of title items that displayed in `TitleScrollView`. 128 | open var items: [TitleItem] { 129 | return [] 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Tests/AppendTests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import SlideController 4 | 5 | class AppendTests: BaseTestCase { 6 | 7 | func testAppended() { 8 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 9 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 10 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 11 | let givenContent = [page1, page2, page3] 12 | slideController.append(object: givenContent) 13 | 14 | let contentCount = slideController.content.count 15 | let currentIndex = slideController.content.firstIndex(where: { 16 | $0 === slideController.currentModel 17 | }) 18 | 19 | XCTAssertEqual(contentCount, givenContent.count) 20 | XCTAssertEqual(currentIndex, 0) 21 | } 22 | 23 | func testAppendedLifeCycle() { 24 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 25 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 26 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 27 | let page4 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 28 | let givenContent = [page1, page2, page3, page4] 29 | slideController.append(object: givenContent) 30 | 31 | guard let currentPage = slideController.currentModel?.lifeCycleObject as? TestableLifeCycleObject, 32 | let secondPage = page2.lifeCycleObject as? TestableLifeCycleObject, 33 | let thirdPage = page3.lifeCycleObject as? TestableLifeCycleObject, 34 | let fourthPage = page4.lifeCycleObject as? TestableLifeCycleObject else { 35 | XCTFail("page is not TestableLifeCycleObject") 36 | return 37 | } 38 | 39 | /// Presented page 40 | XCTAssert(currentPage.didAppearTriggered) 41 | XCTAssert(!currentPage.didDissapearTriggered) 42 | XCTAssert(currentPage.viewDidLoadTriggered) 43 | XCTAssert(!currentPage.viewDidUnloadTriggered) 44 | XCTAssert(!currentPage.didStartSlidingTriggered) 45 | XCTAssert(!currentPage.didCancelSlidingTriggered) 46 | 47 | XCTAssert(!secondPage.didAppearTriggered) 48 | XCTAssert(!secondPage.didDissapearTriggered) 49 | XCTAssert(secondPage.viewDidLoadTriggered) 50 | XCTAssert(!secondPage.viewDidUnloadTriggered) 51 | XCTAssert(!secondPage.didStartSlidingTriggered) 52 | XCTAssert(!secondPage.didCancelSlidingTriggered) 53 | 54 | XCTAssert(!thirdPage.didAppearTriggered) 55 | XCTAssert(!thirdPage.didDissapearTriggered) 56 | XCTAssert(!thirdPage.viewDidLoadTriggered) 57 | XCTAssert(!thirdPage.viewDidUnloadTriggered) 58 | XCTAssert(!thirdPage.didStartSlidingTriggered) 59 | XCTAssert(!thirdPage.didCancelSlidingTriggered) 60 | 61 | XCTAssert(!fourthPage.didAppearTriggered) 62 | XCTAssert(!fourthPage.didDissapearTriggered) 63 | XCTAssert(!fourthPage.viewDidLoadTriggered) 64 | XCTAssert(!fourthPage.viewDidUnloadTriggered) 65 | XCTAssert(!fourthPage.didStartSlidingTriggered) 66 | XCTAssert(!fourthPage.didCancelSlidingTriggered) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/Core/BaseTestCase.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SlideController 3 | 4 | class BaseTestCase: XCTestCase { 5 | var slideController: SlideController! 6 | 7 | override func setUp() { 8 | super.setUp() 9 | slideController = SlideController( 10 | pagesContent: [], 11 | startPageIndex: 0, 12 | slideDirection: SlideDirection.horizontal) 13 | 14 | slideController.viewDidAppear() 15 | } 16 | 17 | override func tearDown() { 18 | super.tearDown() 19 | slideController = nil 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Core/TestTitleItem.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SlideController 3 | 4 | class TestTitleItem: UIView, Initializable, ItemViewable, Selectable { 5 | let titleLabel = UILabel() 6 | 7 | private var backgroundViewHeight: CGFloat = 2 8 | private var titleLabelOffsetX: CGFloat = 21 9 | private var newIndicatorRadius: CGFloat = 9 10 | private var internalIsSelected: Bool = false 11 | private var internalIndex: Int = 0 12 | private var internalDidSelectAction: ((Int) -> Void)? 13 | private let backgroundView = UIView() 14 | private let backgroundSelectedColor = UIColor.white 15 | private let titleLabelFont = UIFont.systemFont(ofSize: 16.5) 16 | private let internalBackgroundColor = UIColor.clear 17 | private let titleFontDefaultColor = UIColor(white: 1, alpha: 0.7) 18 | private let titleFontSelectedColor = UIColor(white: 1, alpha: 1) 19 | 20 | required init() { 21 | super.init(frame: CGRect.zero) 22 | backgroundView.translatesAutoresizingMaskIntoConstraints = false 23 | addSubview(backgroundView) 24 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 25 | titleLabel.font = titleLabelFont 26 | addSubview(titleLabel) 27 | activateBackgroundViewConstraints(view: backgroundView) 28 | activateTitleLabelConstraints(view: titleLabel) 29 | isSelected = false 30 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapDetected(_:))) 31 | addGestureRecognizer(tapRecognizer) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | // MARK: - ItemViewableImplementation 39 | 40 | typealias Item = TestTitleItem 41 | 42 | var view: Item { 43 | return self 44 | } 45 | 46 | // MARK: - SelectableImplementation 47 | 48 | var didSelectAction: ((Int) -> ())? { 49 | get { 50 | return internalDidSelectAction 51 | } 52 | set { 53 | internalDidSelectAction = newValue 54 | } 55 | } 56 | 57 | var isSelected: Bool { 58 | get { 59 | return internalIsSelected 60 | } 61 | set { 62 | if newValue { 63 | backgroundView.backgroundColor = backgroundSelectedColor 64 | titleLabel.textColor = titleFontSelectedColor 65 | } else { 66 | backgroundView.backgroundColor = internalBackgroundColor 67 | titleLabel.textColor = titleFontDefaultColor 68 | } 69 | internalIsSelected = newValue 70 | } 71 | } 72 | 73 | var index: Int { 74 | get { 75 | return internalIndex 76 | } 77 | set { 78 | internalIndex = newValue 79 | } 80 | } 81 | } 82 | 83 | private typealias PrivateTestTitleItem = TestTitleItem 84 | private extension PrivateTestTitleItem { 85 | func activateBackgroundViewConstraints(view: UIView) { 86 | var constraints = [NSLayoutConstraint]() 87 | constraints.append(view.bottomAnchor.constraint(equalTo: bottomAnchor)) 88 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor)) 89 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor)) 90 | constraints.append(view.heightAnchor.constraint(equalToConstant: backgroundViewHeight)) 91 | NSLayoutConstraint.activate(constraints) 92 | } 93 | 94 | func activateTitleLabelConstraints(view: UIView) { 95 | var constraints = [NSLayoutConstraint]() 96 | constraints.append(view.centerYAnchor.constraint(equalTo: centerYAnchor)) 97 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -titleLabelOffsetX)) 98 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: titleLabelOffsetX)) 99 | NSLayoutConstraint.activate(constraints) 100 | } 101 | 102 | @objc func tapDetected(_ recognizer: UIGestureRecognizer) { 103 | if !internalIsSelected { 104 | internalDidSelectAction?(internalIndex) 105 | } 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /Tests/Core/TestTitleScrollView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SlideController 3 | 4 | class TestTitleScrollView: TitleScrollView { 5 | private var internalItems: [View] = [] 6 | private let internalItemOffsetX: CGFloat = 15 7 | private let itemOffsetTop: CGFloat = 36 8 | private let itemHeight: CGFloat = 36 9 | private let shadowOpacity: Float = 0.16 10 | private let internalBackgroundColor = UIColor.purple 11 | 12 | override required init() { 13 | super.init() 14 | clipsToBounds = false 15 | layer.shadowColor = UIColor.black.cgColor 16 | layer.shadowOffset = CGSize(width: 0, height: 1) 17 | layer.shadowOpacity = shadowOpacity 18 | backgroundColor = internalBackgroundColor 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | fatalError("init(coder:) has not been implemented") 23 | } 24 | 25 | override var items: [TitleItem] { 26 | return internalItems 27 | } 28 | 29 | override func appendViews(views: [View]) { 30 | var prevView: View? = internalItems.last 31 | let prevPrevView: UIView? = internalItems.count > 1 ? internalItems[items.count - 2] : nil 32 | if let prevItem = prevView { 33 | updateConstraints(prevItem, prevView: prevPrevView, isLast: false) 34 | } 35 | for i in 0...views.count - 1 { 36 | let view = views[i] 37 | view.translatesAutoresizingMaskIntoConstraints = false 38 | internalItems.append(view) 39 | addSubview(view) 40 | activateConstraints(view, prevView: prevView, isLast: i == views.count - 1) 41 | prevView = view 42 | } 43 | } 44 | 45 | override func insertView(view: View, index: Int) { 46 | guard index < internalItems.count else { 47 | return 48 | } 49 | view.translatesAutoresizingMaskIntoConstraints = false 50 | internalItems.insert(view, at: index) 51 | addSubview(view) 52 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 53 | let nextView: View = internalItems[index + 1] 54 | activateConstraints(view, prevView: prevView, isLast: false) 55 | updateConstraints(nextView, prevView: view, isLast: index == internalItems.count - 2) 56 | } 57 | 58 | override func removeViewAtIndex(index: Int) { 59 | guard index < internalItems.count else { 60 | return 61 | } 62 | let view: View = internalItems[index] 63 | let prevView: View? = index > 0 ? internalItems[index - 1] : nil 64 | let nextView: View? = index < internalItems.count - 1 ? internalItems[index + 1] : nil 65 | internalItems.remove(at: index) 66 | view.removeFromSuperview() 67 | if let nextView = nextView { 68 | updateConstraints(nextView, prevView: prevView, isLast: index == internalItems.count - 1) 69 | } else if let prevView = prevView { 70 | let prevPrevView: View? = internalItems.count > 1 ? internalItems[internalItems.count - 2] : nil 71 | updateConstraints(prevView, prevView: prevPrevView, isLast: true) 72 | } 73 | } 74 | 75 | var isTransparent = false { 76 | didSet { 77 | backgroundColor = isTransparent ? UIColor.clear : internalBackgroundColor 78 | } 79 | } 80 | } 81 | 82 | private typealias PrivateTestTitleScrollView = TestTitleScrollView 83 | private extension PrivateTestTitleScrollView { 84 | func activateConstraints(_ view: UIView, prevView: UIView?, isLast: Bool) { 85 | var constraints: [NSLayoutConstraint] = [] 86 | constraints.append(view.topAnchor.constraint(equalTo: topAnchor, constant: itemOffsetTop)) 87 | constraints.append(view.heightAnchor.constraint(equalToConstant: itemHeight)) 88 | if let prevView = prevView { 89 | constraints.append(view.leadingAnchor.constraint(equalTo: prevView.trailingAnchor, constant: 2 * itemOffsetX())) 90 | } else { 91 | constraints.append(view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: itemOffsetX())) 92 | } 93 | if isLast { 94 | constraints.append(view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -itemOffsetX())) 95 | } 96 | NSLayoutConstraint.activate(constraints) 97 | } 98 | 99 | func removeConstraints(view: UIView) { 100 | let viewConstraints = constraints.filter({ $0.firstItem === view }) 101 | let heigthConstraints = view.constraints.filter({ $0.firstAttribute == .height }) 102 | NSLayoutConstraint.deactivate(viewConstraints + heigthConstraints) 103 | } 104 | 105 | func updateConstraints(_ view: UIView, prevView: UIView?, isLast: Bool) { 106 | self.removeConstraints(view: view) 107 | self.activateConstraints(view, prevView: prevView, isLast: isLast) 108 | } 109 | 110 | func itemOffsetX() -> CGFloat { 111 | return internalItemOffsetX 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Tests/Core/TestableLifeCycleObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestableLifeCycleObject.swift 3 | // SlideController_Example 4 | // 5 | // Created by Pavel Kondrashkov on 11/13/17. 6 | // Copyright © 2017 Touchlane LLC. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SlideController 11 | 12 | class TestableLifeCycleObject: Initializable { 13 | // MARK: - InitialazableImplementation 14 | 15 | 16 | var didAppearTriggered: Bool = false 17 | var didDissapearTriggered: Bool = false 18 | var viewDidLoadTriggered: Bool = false 19 | var viewDidUnloadTriggered: Bool = false 20 | var didStartSlidingTriggered: Bool = false 21 | var didCancelSlidingTriggered: Bool = false 22 | 23 | required init() { } 24 | } 25 | 26 | private typealias SlidePageLifeCycleImplementation = TestableLifeCycleObject 27 | extension SlidePageLifeCycleImplementation: SlidePageLifeCycle { 28 | var isKeyboardResponsive: Bool { 29 | return false 30 | } 31 | 32 | func didAppear() { 33 | didAppearTriggered = true 34 | } 35 | 36 | func didDissapear() { 37 | didDissapearTriggered = true 38 | } 39 | 40 | func viewDidLoad() { 41 | viewDidLoadTriggered = true 42 | } 43 | 44 | func viewDidUnload() { 45 | viewDidUnloadTriggered = true 46 | } 47 | 48 | func didStartSliding() { 49 | didStartSlidingTriggered = true 50 | } 51 | 52 | func didCancelSliding() { 53 | didCancelSlidingTriggered = true 54 | } 55 | } 56 | 57 | private typealias ViewableImplementation = TestableLifeCycleObject 58 | extension ViewableImplementation: Viewable { 59 | var view: UIView { 60 | get { 61 | return UIView() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/InsertTests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import SlideController 4 | 5 | class InsertTests: BaseTestCase { 6 | 7 | func testInserted() { 8 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 9 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 10 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 11 | let givenContent = [page1, page2, page3] 12 | slideController.append(object: givenContent) 13 | 14 | let insertingPage = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 15 | slideController.insert(object: insertingPage, index: 0) 16 | 17 | let contentCount = slideController.content.count 18 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 19 | 20 | XCTAssertEqual(contentCount, givenContent.count + 1) 21 | XCTAssertEqual(currentIndex, 1) 22 | } 23 | 24 | func testInsertedAtFirstLifeCycle() { 25 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 26 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 27 | let givenContent = [page1, page2] 28 | slideController.append(object: givenContent) 29 | 30 | let insertingPage = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 31 | slideController.insert(object: insertingPage, index: 0) 32 | 33 | guard let insertingObject = insertingPage.lifeCycleObject as? TestableLifeCycleObject, 34 | let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 35 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject else { 36 | XCTFail("page is not TestableLifeCycleObject") 37 | return 38 | } 39 | 40 | XCTAssert(!insertingObject.didAppearTriggered) 41 | XCTAssert(!insertingObject.didDissapearTriggered) 42 | XCTAssert(insertingObject.viewDidLoadTriggered) 43 | XCTAssert(!insertingObject.viewDidUnloadTriggered) 44 | XCTAssert(!insertingObject.didStartSlidingTriggered) 45 | XCTAssert(!insertingObject.didCancelSlidingTriggered) 46 | 47 | XCTAssert(object1.didAppearTriggered) 48 | XCTAssert(!object1.didDissapearTriggered) 49 | XCTAssert(object1.viewDidLoadTriggered) 50 | XCTAssert(!object1.viewDidUnloadTriggered) 51 | XCTAssert(!object1.didStartSlidingTriggered) 52 | XCTAssert(!object1.didCancelSlidingTriggered) 53 | 54 | XCTAssert(!object2.didAppearTriggered) 55 | XCTAssert(!object2.didDissapearTriggered) 56 | XCTAssert(object2.viewDidLoadTriggered) 57 | XCTAssert(!object2.viewDidUnloadTriggered) 58 | XCTAssert(!object2.didStartSlidingTriggered) 59 | XCTAssert(!object2.didCancelSlidingTriggered) 60 | } 61 | 62 | func testInsertedAtLastLifeCycle() { 63 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 64 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 65 | let givenContent = [page1, page2] 66 | slideController.append(object: givenContent) 67 | 68 | let insertingPage = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 69 | slideController.insert(object: insertingPage, index: slideController.content.count - 1) 70 | 71 | guard let insertingObject = insertingPage.lifeCycleObject as? TestableLifeCycleObject, 72 | let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 73 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject else { 74 | XCTFail("page is not TestableLifeCycleObject") 75 | return 76 | } 77 | 78 | XCTAssert(!insertingObject.didAppearTriggered) 79 | XCTAssert(!insertingObject.didDissapearTriggered) 80 | XCTAssert(insertingObject.viewDidLoadTriggered) 81 | XCTAssert(!insertingObject.viewDidUnloadTriggered) 82 | XCTAssert(!insertingObject.didStartSlidingTriggered) 83 | XCTAssert(!insertingObject.didCancelSlidingTriggered) 84 | 85 | XCTAssert(object1.didAppearTriggered) 86 | XCTAssert(!object1.didDissapearTriggered) 87 | XCTAssert(object1.viewDidLoadTriggered) 88 | XCTAssert(!object1.viewDidUnloadTriggered) 89 | XCTAssert(!object1.didStartSlidingTriggered) 90 | XCTAssert(!object1.didCancelSlidingTriggered) 91 | 92 | XCTAssert(!object2.didAppearTriggered) 93 | XCTAssert(!object2.didDissapearTriggered) 94 | XCTAssert(object2.viewDidLoadTriggered) 95 | XCTAssert(object2.viewDidUnloadTriggered) 96 | XCTAssert(!object2.didStartSlidingTriggered) 97 | XCTAssert(!object2.didCancelSlidingTriggered) 98 | } 99 | 100 | func testInsertedBeforeCurrentLifeCycle() { 101 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 102 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 103 | let givenContent = [page1, page2] 104 | slideController.append(object: givenContent) 105 | slideController.shift(pageIndex: 1, animated: false) 106 | 107 | let insertingPage = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 108 | slideController.insert(object: insertingPage, index: 0) 109 | 110 | guard let insertingObject = insertingPage.lifeCycleObject as? TestableLifeCycleObject, 111 | let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 112 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject else { 113 | XCTFail("page is not TestableLifeCycleObject") 114 | return 115 | } 116 | 117 | XCTAssert(!insertingObject.didAppearTriggered) 118 | XCTAssert(!insertingObject.didDissapearTriggered) 119 | XCTAssert(!insertingObject.viewDidLoadTriggered) 120 | XCTAssert(!insertingObject.viewDidUnloadTriggered) 121 | XCTAssert(!insertingObject.didStartSlidingTriggered) 122 | XCTAssert(!insertingObject.didCancelSlidingTriggered) 123 | 124 | XCTAssert(object1.didAppearTriggered) 125 | XCTAssert(object1.didDissapearTriggered) 126 | XCTAssert(object1.viewDidLoadTriggered) 127 | XCTAssert(!object1.viewDidUnloadTriggered) 128 | XCTAssert(!object1.didStartSlidingTriggered) 129 | XCTAssert(!object1.didCancelSlidingTriggered) 130 | 131 | XCTAssert(object2.didAppearTriggered) 132 | XCTAssert(!object2.didDissapearTriggered) 133 | XCTAssert(object2.viewDidLoadTriggered) 134 | XCTAssert(!object2.viewDidUnloadTriggered) 135 | XCTAssert(!object2.didStartSlidingTriggered) 136 | XCTAssert(!object2.didCancelSlidingTriggered) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/LoadTests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import SlideController 4 | 5 | class LoadTests: BaseTestCase { 6 | 7 | func testLoadOnContentUnloadingEnabled() { 8 | slideController.isContentUnloadingEnabled = true 9 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 10 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 11 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 12 | let givenContent = [page1, page2, page3] 13 | slideController.append(object: givenContent) 14 | 15 | guard let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 16 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 17 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 18 | XCTFail("page is not TestableLifeCycleObject") 19 | return 20 | } 21 | 22 | XCTAssert(object1.viewDidLoadTriggered) 23 | XCTAssert(object2.viewDidLoadTriggered) 24 | XCTAssert(!object3.viewDidLoadTriggered) 25 | } 26 | 27 | func testLoadOnContentUnloadingDisabled() { 28 | slideController.isContentUnloadingEnabled = false 29 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 30 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 31 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 32 | let givenContent = [page1, page2, page3] 33 | slideController.append(object: givenContent) 34 | 35 | guard let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 36 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 37 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 38 | XCTFail("page is not TestableLifeCycleObject") 39 | return 40 | } 41 | 42 | XCTAssert(object1.viewDidLoadTriggered) 43 | XCTAssert(object2.viewDidLoadTriggered) 44 | XCTAssert(object3.viewDidLoadTriggered) 45 | } 46 | 47 | func testContentUnloadingModeChangeToDisabled() { 48 | slideController.isContentUnloadingEnabled = true 49 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 50 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 51 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 52 | let givenContent = [page1, page2, page3] 53 | slideController.append(object: givenContent) 54 | slideController.isContentUnloadingEnabled = false 55 | 56 | guard let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 57 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 58 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 59 | XCTFail("page is not TestableLifeCycleObject") 60 | return 61 | } 62 | 63 | XCTAssert(object1.viewDidLoadTriggered) 64 | XCTAssert(object2.viewDidLoadTriggered) 65 | XCTAssert(object3.viewDidLoadTriggered) 66 | } 67 | 68 | func testContentUnloadingModeChangeToEnabled() { 69 | slideController.isContentUnloadingEnabled = false 70 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 71 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 72 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 73 | let givenContent = [page1, page2, page3] 74 | slideController.append(object: givenContent) 75 | slideController.isContentUnloadingEnabled = true 76 | 77 | guard let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 78 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 79 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 80 | XCTFail("page is not TestableLifeCycleObject") 81 | return 82 | } 83 | 84 | XCTAssert(!object1.viewDidUnloadTriggered) 85 | XCTAssert(!object2.viewDidUnloadTriggered) 86 | XCTAssert(object3.viewDidUnloadTriggered) 87 | } 88 | 89 | func testInsertOnContentLoadingModeDisabled() { 90 | slideController.isContentUnloadingEnabled = false 91 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 92 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 93 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 94 | let givenContent = [page1, page2, page3] 95 | slideController.append(object: givenContent) 96 | 97 | let insertingPage = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 98 | slideController.insert(object: insertingPage, index: 0) 99 | 100 | guard let insertingObject = insertingPage.lifeCycleObject as? TestableLifeCycleObject, 101 | let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 102 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 103 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 104 | XCTFail("page is not TestableLifeCycleObject") 105 | return 106 | } 107 | 108 | XCTAssert(insertingObject.viewDidLoadTriggered) 109 | XCTAssert(!insertingObject.viewDidUnloadTriggered) 110 | XCTAssert(!object1.viewDidUnloadTriggered) 111 | XCTAssert(!object2.viewDidUnloadTriggered) 112 | XCTAssert(!object3.viewDidUnloadTriggered) 113 | } 114 | 115 | func testAppendOnContentLoadingModeDisabled() { 116 | slideController.isContentUnloadingEnabled = false 117 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 118 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 119 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 120 | let givenContent = [page1, page2, page3] 121 | slideController.append(object: givenContent) 122 | 123 | let appendingPage = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 124 | slideController.insert(object: appendingPage, index: 2) 125 | 126 | guard let appendingObject = appendingPage.lifeCycleObject as? TestableLifeCycleObject, 127 | let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 128 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 129 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 130 | XCTFail("page is not TestableLifeCycleObject") 131 | return 132 | } 133 | 134 | XCTAssert(appendingObject.viewDidLoadTriggered) 135 | XCTAssert(!appendingObject.viewDidUnloadTriggered) 136 | XCTAssert(!object1.viewDidUnloadTriggered) 137 | XCTAssert(!object2.viewDidUnloadTriggered) 138 | XCTAssert(!object3.viewDidUnloadTriggered) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/RemoveTests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import SlideController 4 | 5 | class RemoveTests: BaseTestCase { 6 | 7 | func testRemovedCurrent() { 8 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 9 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 10 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 11 | let givenContent = [page1, page2, page3] 12 | slideController.append(object: givenContent) 13 | 14 | slideController.removeAtIndex(index: 0) 15 | 16 | let contentCount = slideController.content.count 17 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 18 | 19 | guard let currentPage = page2.lifeCycleObject as? TestableLifeCycleObject else { 20 | XCTFail("page is not TestableLifeCycleObject") 21 | return 22 | } 23 | 24 | XCTAssertEqual(contentCount, givenContent.count - 1) 25 | XCTAssertEqual(currentIndex, 0) 26 | XCTAssert(currentPage === slideController.currentModel?.lifeCycleObject) 27 | } 28 | 29 | func testRemovedAll() { 30 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 31 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 32 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 33 | let givenContent = [page1, page2, page3] 34 | slideController.append(object: givenContent) 35 | 36 | slideController.removeAtIndex(index: 0) 37 | slideController.removeAtIndex(index: 0) 38 | slideController.removeAtIndex(index: 0) 39 | 40 | let contentCount = slideController.content.count 41 | 42 | XCTAssertEqual(contentCount, 0) 43 | XCTAssertNil(slideController.currentModel) 44 | } 45 | 46 | func testRemovedVisibleLifeCycle() { 47 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 48 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 49 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 50 | let givenContent = [page1, page2, page3] 51 | slideController.append(object: givenContent) 52 | 53 | slideController.removeAtIndex(index: 0) 54 | 55 | guard let removedObject = page1.lifeCycleObject as? TestableLifeCycleObject else { 56 | XCTFail("page is not TestableLifeCycleObject") 57 | return 58 | } 59 | 60 | XCTAssert(removedObject.didAppearTriggered) 61 | XCTAssert(removedObject.didDissapearTriggered) 62 | XCTAssert(removedObject.viewDidLoadTriggered) 63 | XCTAssert(removedObject.viewDidUnloadTriggered) 64 | XCTAssert(!removedObject.didStartSlidingTriggered) 65 | XCTAssert(!removedObject.didCancelSlidingTriggered) 66 | } 67 | 68 | func testRemovedNearVisibleLifeCycle() { 69 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 70 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 71 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 72 | let givenContent = [page1, page2, page3] 73 | slideController.append(object: givenContent) 74 | 75 | slideController.removeAtIndex(index: 1) 76 | 77 | guard let removedObject = page2.lifeCycleObject as? TestableLifeCycleObject else { 78 | XCTFail("page is not TestableLifeCycleObject") 79 | return 80 | } 81 | 82 | XCTAssert(!removedObject.didAppearTriggered) 83 | XCTAssert(!removedObject.didDissapearTriggered) 84 | XCTAssert(removedObject.viewDidLoadTriggered) 85 | XCTAssert(removedObject.viewDidUnloadTriggered) 86 | XCTAssert(!removedObject.didStartSlidingTriggered) 87 | XCTAssert(!removedObject.didCancelSlidingTriggered) 88 | } 89 | 90 | func testRemovedFarVisibleLifeCycle() { 91 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 92 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 93 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 94 | let givenContent = [page1, page2, page3] 95 | slideController.append(object: givenContent) 96 | 97 | slideController.removeAtIndex(index: 2) 98 | 99 | guard let removedObject = page3.lifeCycleObject as? TestableLifeCycleObject else { 100 | XCTFail("page is not TestableLifeCycleObject") 101 | return 102 | } 103 | 104 | XCTAssert(!removedObject.didAppearTriggered) 105 | XCTAssert(!removedObject.didDissapearTriggered) 106 | XCTAssert(!removedObject.viewDidLoadTriggered) 107 | XCTAssert(!removedObject.viewDidUnloadTriggered) 108 | XCTAssert(!removedObject.didStartSlidingTriggered) 109 | XCTAssert(!removedObject.didCancelSlidingTriggered) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/ShiftTests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import SlideController 4 | 5 | class ShiftTests: BaseTestCase { 6 | func testContentShift() { 7 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 8 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 9 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 10 | let givenContent = [page1, page2, page3] 11 | slideController.append(object: givenContent) 12 | 13 | slideController.shift(pageIndex: 1, animated: false) 14 | 15 | let contentCount = slideController.content.count 16 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 17 | 18 | XCTAssertEqual(contentCount, givenContent.count) 19 | XCTAssertEqual(currentIndex, 1) 20 | } 21 | 22 | func testShiftAtCurrent() { 23 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 24 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 25 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 26 | let givenContent = [page1, page2, page3] 27 | slideController.append(object: givenContent) 28 | 29 | slideController.shift(pageIndex: 0, animated: false) 30 | 31 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 32 | XCTAssertEqual(currentIndex, 0) 33 | } 34 | 35 | func testShiftAtLast() { 36 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 37 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 38 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 39 | let givenContent = [page1, page2, page3] 40 | slideController.append(object: givenContent) 41 | 42 | slideController.shift(pageIndex: givenContent.count - 1, animated: true) 43 | 44 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 45 | XCTAssertEqual(currentIndex, givenContent.count - 1) 46 | } 47 | 48 | func testShiftedAtCurrentLifeCycle() { 49 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 50 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 51 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 52 | let givenContent = [page1, page2, page3] 53 | slideController.append(object: givenContent) 54 | 55 | slideController.shift(pageIndex: 0, animated: false) 56 | 57 | guard let currentPage = slideController.currentModel?.lifeCycleObject as? TestableLifeCycleObject else { 58 | XCTFail("page is not TestableLifeCycleObject") 59 | return 60 | } 61 | 62 | XCTAssert(currentPage.didAppearTriggered) 63 | XCTAssert(!currentPage.didDissapearTriggered) 64 | XCTAssert(currentPage.viewDidLoadTriggered) 65 | XCTAssert(!currentPage.viewDidUnloadTriggered) 66 | XCTAssert(!currentPage.didStartSlidingTriggered) 67 | XCTAssert(!currentPage.didCancelSlidingTriggered) 68 | } 69 | 70 | func testShiftedAtSecond() { 71 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 72 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 73 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 74 | let givenContent = [page1, page2, page3] 75 | slideController.append(object: givenContent) 76 | slideController.shift(pageIndex: 1) 77 | 78 | guard let object1 = page1.lifeCycleObject as? TestableLifeCycleObject, 79 | let object2 = page2.lifeCycleObject as? TestableLifeCycleObject, 80 | let object3 = page3.lifeCycleObject as? TestableLifeCycleObject else { 81 | XCTFail("page is not TestableLifeCycleObject") 82 | return 83 | } 84 | 85 | XCTAssert(object1.viewDidLoadTriggered) 86 | XCTAssert(object1.didDissapearTriggered) 87 | 88 | XCTAssert(object2.viewDidLoadTriggered) 89 | XCTAssert(object2.didAppearTriggered) 90 | 91 | XCTAssert(object3.viewDidLoadTriggered) 92 | } 93 | 94 | func testShiftedAtFarLifeCycle() { 95 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 96 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 97 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 98 | let givenContent = [page1, page2, page3] 99 | slideController.append(object: givenContent) 100 | 101 | self.slideController.shift(pageIndex: 2, animated: false) 102 | 103 | guard let finishPage = page3.lifeCycleObject as? TestableLifeCycleObject, 104 | let middlePage = page2.lifeCycleObject as? TestableLifeCycleObject, 105 | let initialPage = page1.lifeCycleObject as? TestableLifeCycleObject else { 106 | XCTFail("page is not TestableLifeCycleObject") 107 | return 108 | } 109 | 110 | XCTAssert(initialPage.didAppearTriggered) 111 | XCTAssert(initialPage.didDissapearTriggered) 112 | XCTAssert(initialPage.viewDidLoadTriggered) 113 | XCTAssert(initialPage.viewDidUnloadTriggered) 114 | 115 | XCTAssert(!middlePage.didAppearTriggered) 116 | XCTAssert(!middlePage.didDissapearTriggered) 117 | XCTAssert(middlePage.viewDidLoadTriggered) 118 | XCTAssert(!middlePage.viewDidUnloadTriggered) 119 | XCTAssert(!middlePage.didStartSlidingTriggered) 120 | XCTAssert(!middlePage.didCancelSlidingTriggered) 121 | 122 | XCTAssert(finishPage.didAppearTriggered) 123 | XCTAssert(!finishPage.didDissapearTriggered) 124 | XCTAssert(finishPage.viewDidLoadTriggered) 125 | XCTAssert(!finishPage.viewDidUnloadTriggered) 126 | } 127 | 128 | func testShowNext() { 129 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 130 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 131 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 132 | let givenContent = [page1, page2, page3] 133 | slideController.append(object: givenContent) 134 | slideController.showNext() 135 | 136 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 137 | XCTAssertEqual(currentIndex, 1) 138 | } 139 | 140 | func testShowNextCarousel() { 141 | slideController.isCarousel = true 142 | let page1 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 143 | let page2 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 144 | let page3 = SlideLifeCycleObjectBuilder(object: TestableLifeCycleObject()) 145 | let givenContent = [page1, page2, page3] 146 | slideController.append(object: givenContent) 147 | slideController.shift(pageIndex: 2) 148 | slideController.showNext() 149 | 150 | let currentIndex = slideController.content.firstIndex(where: { $0 === slideController.currentModel }) 151 | XCTAssertEqual(currentIndex, 0) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source ~/.rvm/scripts/rvm 4 | rvm use default 5 | pod trunk push --------------------------------------------------------------------------------