├── .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 | 
2 |
3 | 
4 | [](https://travis-ci.org/touchlane/SlideController)
5 | [](https://codecov.io/gh/codecov/SlideController/branch/master)
6 | [](http://cocoapods.org/pods/SlideController)
7 | [](http://cocoapods.org/pods/SlideController)
8 | [](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 | 
13 | 
14 | 
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
--------------------------------------------------------------------------------