├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── WavePageControl
│ ├── DelegateProtocols.swift
│ ├── Reshuffler.swift
│ ├── Utils.swift
│ └── WavePageControl.swift
├── Tests
└── WavePageControlTests
│ ├── ReshufflerTests.swift
│ └── WavePageControlTests.swift
├── WavePageControl.podspec
└── WavePageControlDemo-iOS
├── WavePageControlDemo-iOS.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ └── WavePageControlDemo-iOS.xcscheme
└── WavePageControlDemo-iOS
├── AppDelegate.swift
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ └── Contents.json
├── Contents.json
├── Page Dot Border.colorset
│ └── Contents.json
└── Page Dot.colorset
│ └── Contents.json
├── Base.lproj
└── LaunchScreen.storyboard
├── Examples
├── CustomPageControl.swift
├── DefaultPageControl.swift
├── PageControlShowcase.swift
└── SimplePageControl.swift
├── Info.plist
├── MainViewController.swift
└── SceneDelegate.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 Bogdan
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "WavePageControl",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "WavePageControl",
15 | targets: ["WavePageControl"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
19 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
20 | .target(
21 | name: "WavePageControl",
22 | dependencies: []),
23 | .testTarget(
24 | name: "WavePageControlTests",
25 | dependencies: ["WavePageControl"]),
26 | ],
27 | swiftLanguageVersions: [.v5]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WavePageControl
2 |
3 | 
4 | 
5 | 
6 | 
7 |
8 | > WavePageControl allows you to effectively organise the Page Control indicators all at once.
9 |
10 | ## How it works
11 |
12 | Your app has large but limited amount of pages, more than such indicators amounts could fit into most of Page Controls?\
13 | You still want to have all indicators visible and have ability to navigate using Page Control?
14 |
15 | ## WavePageControl will help you
16 |
17 | * Show all page indicators visible for a large amounts of pages (let's say `40`)
18 | * Easily navigate through all the pages with swipe
19 | * Quickly move to any page with just a single tap
20 | * Clearly see the current selected page indicator among all (`40`) other
21 | * Customise indicators!
22 | * Change default indicator style
23 | * Create your own **custom indicators**
24 |
25 |
26 |
27 | ## Swift Package Manager Install
28 |
29 | Swift Package Manager
30 |
31 | ```swift
32 | dependencies: [
33 | .package(url: "https://github.com/mrbodich/WavePageControl.git")
34 | ]
35 | ```
36 |
37 |
38 | ## CocoaPods Install
39 |
40 | Add `pod 'WavePageControl'` to your Podfile. "WavePageControl" is the name of the library.
41 |
42 | ## Help
43 |
44 | If you like what you see here, and want to support the work being done in this repository, you could:
45 | * Contribute code, issues and pull requests
46 | * Let people know this library exists (:rocket: spread the word :rocket:)
47 |
48 |
49 | ## Questions & Issues
50 |
51 | If you are having questions or problems, you should:
52 |
53 | - Make sure you are using the latest version of the library. Check the [**release-section**](https://github.com/mrbodich/WavePageControl/releases).
54 | - Search [**known issues**](https://github.com/mrbodich/WavePageControl/issues) for your problem (open and closed)
55 | - Create new issues (please :fire: **search known issues before** :fire:, do not create duplicate issues)
56 |
57 |
58 | # How to use
59 |
60 | ## Auto Layout
61 |
62 | Subject | Description
63 | --- | ---
64 | Position | Satisfy position layout by creating 1 horizontal and 1 vertical constraint
65 | Size | WavePageControl **controls size by itself**, both vertical and horizontal. So keep it in mind and avoid conflicting/ambiguous constraints
66 |
67 | ## Customising WavePageControl instance
68 |
69 | Parameter | Description
70 | --- | ---
71 | maxNavigationWidth | Maximum width of WavePageControl frame. After reaching this width, all indicators sizes will be recalculated to fit max width.
72 | defaultButtonHeight | Permanent height of active indicator. Other indicators will have this size until they fit into the maxNavigationWidth. Minimum size of inactive indicators is not limited.
73 | defaultSpacing | Spacing between indicators until they fit into the maxNavigationWidth.
74 | minSpacing | Once indicators start getting smaller, spacing is getting smaller too and limited with this value.
75 | updateLayout() | Apply changed parameters and rebuild layout. Animated by default. You can use UIView.performWithoutAnimation(_:) to eliminate animation.
76 |
77 |
78 | ## Using WavePageControl
79 |
80 | WavePageControl is utilising array of **ID**s to build page indicators.\
81 | WavePageControl is generic, thus `ID` can be any type until it is `Comparable`.
82 |
83 |
84 | Property | Description
85 | --- | ---
86 | pageIDs | Array of ids. Can be changed in any way, you don't need to think about correct arrangement. WavePageControl is smart enough to calculate all movings/insertions/removals between the previous and new state and **animate** them properly. Actually you can even put another random array here and it will just work!
87 | currentPage | ID of an active indicator, optional. You can put any ID here or nil. If pageIDs does not contain such ID, no any error here, WavePageControl will just indicate no selection. Once such ID will appear in pageIDs, corresponding indicator will be automatically set as active.
88 | delegate | Set instance conforming to WavePageControlDelegate here. `See next section for more info`
89 |
90 |
91 | ## Advanced Control
92 |
93 | You can have advanced control over the WavePageControl using the WavePageControlDelegate.
94 | WavePageControlDelegate has 3 methods with existing default implementation, so you can optionally implement the ones you need.
95 |
96 | Method | Description
97 | --- | ---
98 | createCustomPageView(id:) | Requires to return WavePageButtonView (that is UIView itself). Default implementation returns DefaultPageButtonView. `See examples in Demo Project`
99 | didSwipeScroll(_:, id:, isGestureCompleted:) | Called on swipe. It gives you a reference to WavePageControl, ID of indicator to select during all swipe, and a boolean state if gesture is completed. currentPage should be set explicitly here if needed. Default implementation will select it directly.
100 | didTap(_:, id:) | Called on tap. It gives you a reference to WavePageControl and ID of indicator to select. currentPage should be set explicitly here if needed. Default implementation will select it directly.
101 |
102 |
103 | # Visual Examples
104 |
105 | ##### Basic usage without injecting `WavePageControlDelegate`
106 |
107 | 
108 |
109 | ##### Customising `DefaultPageButtonView` in `WavePageControlDelegate`, handling `didSwipeScroll`
110 |
111 | 
112 |
113 | ##### Using fully custom `WavePageButtonView`, handling `didSwipeScroll`
114 |
115 | 
116 |
117 | ### Live usage example
118 | You can download the app here in the [**:point_right::iphone: App Store**](https://apps.apple.com/us/app/to-the-shop/id1542572914)
119 |
120 | https://user-images.githubusercontent.com/23237473/193412590-70e9b364-d60d-4cdb-9306-1e66436b722d.mp4
121 |
--------------------------------------------------------------------------------
/Sources/WavePageControl/DelegateProtocols.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DelegateProtocols.swift
3 | //
4 | //
5 | // Created by Bogdan Chornobryvets on 21.08.2022.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol WavePageControlDelegate: AnyObject {
11 | associatedtype ID: Comparable
12 | func createCustomPageView(for id: ID) -> WavePageButtonView
13 | func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: ID, isGestureCompleted: Bool)
14 | func didTap(_ wavePageControl: UIWavePageControl, onPageWithId id: ID)
15 | }
16 |
17 | public extension WavePageControlDelegate {
18 | func createCustomPageView(for id: ID) -> WavePageButtonView {
19 | DefaultPageButtonView()
20 | }
21 |
22 | func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: ID, isGestureCompleted: Bool) {
23 | wavePageControl.currentPage = id
24 | }
25 |
26 | func didTap(_ wavePageControl: UIWavePageControl, onPageWithId id: ID) {
27 | wavePageControl.currentPage = id
28 | }
29 | }
30 |
31 | final internal class AnyWavePageControlDelegate: WavePageControlDelegate {
32 | let _createCustomPageView: (_ id: ID) -> WavePageButtonView
33 | let _didSwipeScroll: (_ wavePageControl: UIWavePageControl, _ id: ID, _ isGestureCompleted: Bool) -> ()
34 | let _didTap: (_ wavePageControl: UIWavePageControl, _ id: ID) -> ()
35 |
36 | init(from delegate: Delegate) where Delegate.ID == ID {
37 | _createCustomPageView = { [weak delegate] id in
38 | delegate?.createCustomPageView(for: id) ?? DefaultPageButtonView()
39 | }
40 | _didSwipeScroll = { [weak delegate] pageControl, id, isGestureCompleted in
41 | delegate?.didSwipeScroll(pageControl, toPageWithId: id, isGestureCompleted: isGestureCompleted)
42 | }
43 | _didTap = { [weak delegate] pageControl, id in
44 | delegate?.didTap(pageControl, onPageWithId: id)
45 | }
46 | }
47 |
48 | public func createCustomPageView(for id: ID) -> WavePageButtonView {
49 | _createCustomPageView(id)
50 | }
51 |
52 | public func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: ID, isGestureCompleted: Bool) {
53 | _didSwipeScroll(wavePageControl, id, isGestureCompleted)
54 | }
55 |
56 | func didTap(_ wavePageControl: UIWavePageControl, onPageWithId id: ID) {
57 | _didTap(wavePageControl, id)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/WavePageControl/Reshuffler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reshuffler.swift
3 | //
4 | //
5 | // Created by Bogdan Chornobryvets on 23.07.2022.
6 | //
7 |
8 | import Foundation
9 |
10 | internal struct Reshuffler {
11 | let initialSequence: [Element?]
12 |
13 | func shuffleStrategy(for newItems: [Element?]) -> [Operation] {
14 |
15 | var newItemsMap = newItems.map { (isMarked: false, item: $0) }
16 | let removed = initialSequence.enumerated()
17 | .filter { oldItem in
18 | if let index = newItemsMap.firstIndex(where: { $0 == (false, oldItem.element) }) {
19 | newItemsMap[index].isMarked = true
20 | return false
21 | }
22 | return true
23 | }
24 |
25 | var oldItemsMap = initialSequence.map { (isMarked: false, item: $0) }
26 | var noNewItems = newItems
27 | .filter { newItem in
28 | if let index = oldItemsMap.firstIndex(where: { $0 == (false, newItem) }) {
29 | oldItemsMap[index].isMarked = true
30 | return true
31 | }
32 | return false
33 | }
34 |
35 | for ghost in removed {
36 | noNewItems.insert(ghost.element, at: ghost.offset)
37 | }
38 |
39 | var mutableOld = initialSequence
40 | var replaced: [(from: Int, to: Int)] = []
41 |
42 | for n in 0..= n
47 | }!
48 | if o != n {
49 | replaced.append((from: o, to: n))
50 | let element = mutableOld.remove(at: o)
51 | mutableOld.insert(element, at: n)
52 | }
53 | }
54 |
55 | var mutableRemoved = removed
56 | var insertions: [(offset: Int, element: Element?)] = []
57 | for n in 0..= mutableOld.count || mutableOld[compareIndex] != newItems[n] {
62 | insertions.append((offset: insertIndex, element: newItems[n]))
63 | mutableOld.insert(newItems[n], at: insertIndex)
64 | for r in 0.. Int {
83 | var shiftedIndex = index
84 | var baseShift = 0
85 | var shift = 0
86 | if isInserting {
87 | repeat {
88 | baseShift = shift
89 | shift = pushingIndices.filter { $0 < (shiftedIndex + baseShift) }.count
90 | } while shift != baseShift
91 | } else {
92 | repeat {
93 | baseShift = shift
94 | shift = pushingIndices.filter { $0 <= (shiftedIndex + baseShift) }.count
95 | } while shift != baseShift
96 | }
97 | shiftedIndex += baseShift
98 |
99 | return shiftedIndex
100 | }
101 |
102 | enum Operation {
103 | case reordering(map: [(from: Int, to: Int)])
104 | case insertion(items: [(offset: Int, element: Element?)])
105 | case deletion(reversedIndices: [Int])
106 | }
107 | }
108 |
109 |
--------------------------------------------------------------------------------
/Sources/WavePageControl/Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utils.swift
3 | //
4 | //
5 | // Created by Bogdan Chornobryvets on 21.08.2022.
6 | //
7 |
8 | import UIKit
9 |
10 | internal struct UIConstants {
11 | static func shortAnimation(animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil){
12 | UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: animations, completion: { (completed) in
13 | completion?(completed)
14 | })
15 | }
16 | }
17 |
18 | internal extension Collection {
19 | subscript(optional i: Index) -> Iterator.Element? {
20 | return self.indices.contains(i) ? self[i] : nil
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/WavePageControl/WavePageControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WavePageControl.swift
3 | //
4 | //
5 | // Created by Bogdan Chornobryvets on 18.06.2022.
6 | // Copyright © 2022 Bogdan Chornobryvets. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public final class UIWavePageControl: UIStackView {
12 |
13 | private var heightConstraint: NSLayoutConstraint!
14 | public var defaultButtonHeight: CGFloat
15 | public var defaultSpacing: CGFloat
16 | public var minSpacing: CGFloat
17 | public var maxNavigationWidth: CGFloat
18 |
19 | private var delegate: AnyWavePageControlDelegate?
20 | private let defaultDelegate = DefaultWavePageViewDelegate()
21 | private let defaultAnyDelagate: AnyWavePageControlDelegate
22 | private var currentDelegate: AnyWavePageControlDelegate {
23 | delegate ?? defaultAnyDelagate
24 | }
25 |
26 | public init(maxNavigationWidth: CGFloat = 200, defaultButtonHeight: CGFloat = 16, defaultSpacing: CGFloat = 16, minSpacing: CGFloat = 4) {
27 | defaultAnyDelagate = AnyWavePageControlDelegate(from: defaultDelegate)
28 | self.maxNavigationWidth = maxNavigationWidth
29 | self.defaultButtonHeight = defaultButtonHeight
30 | self.defaultSpacing = defaultSpacing
31 | self.minSpacing = minSpacing
32 |
33 | super.init(frame: .zero)
34 | heightConstraint = heightAnchor.constraint(equalToConstant: defaultButtonHeight)
35 | spacing = defaultSpacing
36 | alignment = .center
37 | distribution = .equalSpacing
38 | contentMode = .scaleToFill
39 |
40 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(pagesNavigationDidScroll(recognizer:)))
41 | self.addGestureRecognizer(panGesture)
42 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(pagesNavigationDidTap(recognizer:)))
43 | self.addGestureRecognizer(tapGesture)
44 | }
45 |
46 | public func setDelegate(_ delegate: Delegate?) where Delegate.ID == ID {
47 | if let delegate = delegate {
48 | self.delegate = AnyWavePageControlDelegate(from: delegate)
49 | } else {
50 | self.delegate = nil
51 | }
52 | }
53 |
54 | public var pageIDs = [ID]() {
55 | didSet {
56 | guard pageIDs != oldValue, superview != nil else { return }
57 | buildButtons()
58 | }
59 | }
60 |
61 | public var currentPage: ID? {
62 | didSet {
63 | guard currentPage != oldValue, superview != nil else { return }
64 | updateCurrentPage()
65 | }
66 | }
67 |
68 | private func buildButtons() {
69 | let initialSequence: [ID?] = allPages.map { $0.isRemoved ? nil : $0.getID() }
70 | var targetSequence: [ID?] = pageIDs
71 | for item in initialSequence.enumerated() where item.element == nil {
72 | let offset = item.offset < targetSequence.count ? item.offset : targetSequence.count
73 | targetSequence.insert(nil, at: offset)
74 | }
75 |
76 | let reshuffler = Reshuffler(initialSequence: initialSequence)
77 | let strategy = reshuffler.shuffleStrategy(for: targetSequence)
78 |
79 | var newButtons: [WavePageButtonView] = []
80 |
81 | for operation in strategy {
82 | switch operation {
83 | case let .reordering(map):
84 | for (from, to) in map {
85 | self.movePage(from: from, to: to)
86 | }
87 | //Animate items reordering
88 | UIConstants.shortAnimation {
89 | self.setNeedsLayout()
90 | self.layoutIfNeeded()
91 | }
92 | case let .insertion(items):
93 | for (offset, element) in items {
94 | if let element = element {
95 | newButtons.append(addPage(id: element, at: offset))
96 | }
97 | }
98 | //Layout still hidden items to the correct position in UIStackView
99 | self.setNeedsLayout()
100 | self.layoutIfNeeded()
101 | case let .deletion(reversedIndices):
102 | for index in reversedIndices {
103 | deletePage(at: index)
104 | }
105 | UIConstants.shortAnimation {
106 | self.setNeedsLayout()
107 | self.layoutIfNeeded()
108 | }
109 | }
110 | }
111 |
112 | newButtons.forEach {
113 | $0.isHidden = false
114 | }
115 | //Will animate later
116 | updateCurrentPage()
117 | }
118 |
119 | private func updateCurrentPage() {
120 | for button in pages {
121 | switch button.getID() == currentPage {
122 | case true:
123 | button.isActive = true
124 | bringSubviewToFront(button)
125 | case false:
126 | button.isActive = false
127 | }
128 | }
129 | updateLayout()
130 | }
131 |
132 | public func updateLayout() {
133 | heightConstraint.constant = defaultButtonHeight
134 | let pagesCount = pages.count
135 | var targetSpacing = ( maxNavigationWidth - defaultButtonHeight * CGFloat(pagesCount) ) / CGFloat( pagesCount - 1 )
136 | let dif = (defaultSpacing - targetSpacing) * 0.67
137 | targetSpacing = defaultSpacing - dif
138 | targetSpacing = targetSpacing > minSpacing ? targetSpacing : minSpacing
139 | switch targetSpacing {
140 | case let spacing where spacing >= defaultSpacing:
141 | self.spacing = defaultSpacing
142 | for button in pages { button.height = defaultButtonHeight } //Set default buttons height
143 | default:
144 | spacing = targetSpacing
145 | switch pages.firstIndex(where: { $0.isActive }) {
146 | case let.some(activeIndex):
147 | rebuildButtonsSizes(with: activeIndex)
148 | case .none:
149 | distributeBuuttonsEvenly()
150 | }
151 | }
152 | UIConstants.shortAnimation {
153 | self.setNeedsLayout()
154 | self.layoutIfNeeded()
155 | }
156 | }
157 |
158 | private func rebuildButtonsSizes(with activeIndex: Int) {
159 | let halfWaveCount = 5
160 | let pagesCount = pages.count
161 | let maxWidth = maxNavigationWidth - spacing * CGFloat( pagesCount - 1 )
162 | let angleSegment = CGFloat.pi / CGFloat( halfWaveCount - 1 )
163 | var alphaFactors: [CGFloat] = []
164 | for index in 0.. WavePageButtonView {
215 | let button = currentDelegate.createCustomPageView(for: id)
216 | button.setup(id: id, withHeight: 0)
217 | self.insertArrangedSubview(button, at: index)
218 | return button
219 | }
220 |
221 | private func deletePage(at index: Int) {
222 | let page = allPages[index]
223 | page.isRemoved = true
224 | UIConstants.shortAnimation {
225 | page.transform = CGAffineTransform(scaleX: 0.05, y: 0.05)
226 | page.isHidden = true
227 | } completion: { _ in
228 | page.removeFromSuperview()
229 | }
230 | }
231 |
232 | private func deletePage(withId id: ID) {
233 | guard let page = pages.first(where: { $0.getID() == id }) else { return }
234 | page.isRemoved = true
235 | UIConstants.shortAnimation {
236 | page.transform = CGAffineTransform(scaleX: 0.05, y: 0.05)
237 | page.isHidden = true
238 | } completion: { _ in
239 | page.removeFromSuperview()
240 | }
241 | }
242 |
243 | public override func didMoveToSuperview() {
244 | translatesAutoresizingMaskIntoConstraints = false
245 | heightConstraint.constant = defaultButtonHeight
246 | heightConstraint.isActive = true
247 | UIView.performWithoutAnimation {
248 | spacing = defaultSpacing
249 | buildButtons()
250 | setNeedsLayout()
251 | layoutIfNeeded()
252 | }
253 | }
254 |
255 | public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
256 | return self.bounds.insetBy(dx: -bounds.width, dy: -bounds.height).contains(point)
257 | }
258 |
259 | @objc func pagesNavigationDidScroll(recognizer: UIPanGestureRecognizer) {
260 | let isGestureCompleted = ![.began, .changed].contains(recognizer.state)
261 |
262 | let count = pages.count
263 | let location = recognizer.location(in: self).x
264 | var index = Int( location / ( self.bounds.width / CGFloat(count) ) )
265 | index = index < 0 ? 0 : ( index >= count ? count - 1 : index )
266 | if let id = pageIDs[optional: index] {
267 | currentDelegate.didSwipeScroll(self, toPageWithId: id, isGestureCompleted: isGestureCompleted)
268 | }
269 | }
270 |
271 | @objc func pagesNavigationDidTap(recognizer: UIPanGestureRecognizer) {
272 | let midLocation = { (view: UIView) -> CGFloat in recognizer.location(in: view).x - view.frame.width / 2 }
273 | let selectedPage = pages.min { midLocation($0).magnitude < midLocation($1).magnitude }
274 | if let selectedID: ID = selectedPage?.getID() {
275 | currentDelegate.didTap(self, onPageWithId: selectedID)
276 | }
277 | }
278 |
279 | @available(*, unavailable)
280 | required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
281 | }
282 |
283 | fileprivate final class DefaultWavePageViewDelegate: WavePageControlDelegate {
284 | func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: ID, isGestureCompleted: Bool) {
285 | wavePageControl.currentPage = id
286 | }
287 | }
288 |
289 | open class WavePageButtonView: UIView {
290 | private var heightConstraint: NSLayoutConstraint!
291 | private var id: Any!
292 | fileprivate var isRemoved: Bool = false
293 |
294 | private override init(frame: CGRect) {
295 | super.init(frame: frame)
296 | translatesAutoresizingMaskIntoConstraints = false
297 | isHidden = true
298 | alpha = 0
299 | }
300 |
301 | public init() {
302 | super.init(frame: .zero)
303 | translatesAutoresizingMaskIntoConstraints = false
304 | isHidden = true
305 | alpha = 0
306 | }
307 |
308 | fileprivate func setup(id: ID, withHeight height: CGFloat) {
309 | self.id = id
310 | heightConstraint = heightAnchor.constraint(equalToConstant: height)
311 | }
312 |
313 | fileprivate func getID() -> ID {
314 | return id as! ID
315 | }
316 |
317 | fileprivate var height: CGFloat = 0 {
318 | didSet {
319 | UIConstants.shortAnimation { [self] in
320 | heightConstraint.constant = height
321 | didChangeHeight(to: height)
322 | layoutIfNeeded()
323 | }
324 | }
325 | }
326 |
327 | open func didChangeHeight(to height: CGFloat) {
328 |
329 | }
330 |
331 | open func didChangeState(_ state: WavePageButtonState) {
332 |
333 | }
334 |
335 | public override var isHidden: Bool {
336 | didSet {
337 | switch isHidden {
338 | case false:
339 | alpha = 1
340 | updateState()
341 | case true:
342 | alpha = 0
343 | }
344 | }
345 | }
346 |
347 | fileprivate var isActive: Bool! = nil {
348 | didSet {
349 | guard let isActive = isActive, isActive != oldValue else { return }
350 | UIConstants.shortAnimation { [weak self] in
351 | self?.updateState()
352 | }
353 | }
354 | }
355 |
356 | private func updateState() {
357 | guard let isActive = isActive else { return }
358 | let state: WavePageButtonState = isActive ? .active : .default
359 | didChangeState(state)
360 | }
361 |
362 | open override func didMoveToSuperview() {
363 | NSLayoutConstraint.activate([
364 | heightConstraint,
365 | widthAnchor.constraint(equalTo: heightAnchor)
366 | ])
367 | isActive = false
368 | }
369 |
370 | public enum WavePageButtonState {
371 | case `default`, active
372 | }
373 |
374 | @available(*, unavailable)
375 | required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
376 | }
377 |
378 | public final class DefaultPageButtonView: WavePageButtonView {
379 | private let accentColor: UIColor
380 | private let defaultColor: UIColor
381 |
382 | public init(accentColor: UIColor = .yellow, defaultColor: UIColor = .white.withAlphaComponent(0.5), dotBorderColor: UIColor = .black, borderWidth: CGFloat = 1) {
383 | self.accentColor = accentColor
384 | self.defaultColor = defaultColor
385 |
386 | super.init()
387 | layer.borderColor = dotBorderColor.cgColor
388 | layer.borderWidth = borderWidth
389 | }
390 |
391 | public override func didChangeHeight(to height: CGFloat) {
392 | layer.cornerRadius = height / 2
393 | }
394 |
395 | public override func didChangeState(_ state: WavePageButtonState) {
396 | switch state {
397 | case .default:
398 | self.backgroundColor = defaultColor
399 | alpha = 0.5
400 | case .active:
401 | self.backgroundColor = accentColor
402 | alpha = 1
403 | }
404 | }
405 | }
406 |
--------------------------------------------------------------------------------
/Tests/WavePageControlTests/ReshufflerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import WavePageControl
3 |
4 | final class ReshufflerTests: XCTestCase {
5 |
6 | func testReshuffler() throws {
7 | var optionalInitial: [String?] = randomChars(length: 1200)
8 | var optionaDesired: [String?] = randomChars(length: 800)
9 | let nilInsertions = (0.. [String] {
75 | let allowedChars = "abcdefghijklmnopqrstuvwxyz"
76 | return (0..(maxNavigationWidth: 300,
15 | defaultButtonHeight: 16,
16 | defaultSpacing: 16,
17 | minSpacing: 2)
18 |
19 | //when
20 | var strongCustomPageProvider: TestCustomWavePageViewProvider? = TestCustomWavePageViewProvider()
21 | weak var weakCustomPageProvider: TestCustomWavePageViewProvider? = strongCustomPageProvider
22 |
23 | //then
24 | pageControl.setDelegate(weakCustomPageProvider)
25 | XCTAssertNotNil(weakCustomPageProvider, "Error: UIWavePageControl did not save CustomWavePageViewProvider reference")
26 | strongCustomPageProvider = nil
27 | XCTAssertNil(weakCustomPageProvider, "Error: UIWavePageControl holds strong reference to the CustomWavePageViewProvider")
28 | }
29 |
30 | private class TestCustomWavePageViewProvider: WavePageControlDelegate {
31 | func createCustomPageView(for id: String) -> WavePageButtonView {
32 | DefaultPageButtonView()
33 | }
34 | func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: String, isGestureCompleted: Bool) {
35 |
36 | }
37 | func didTap(_ wavePageControl: UIWavePageControl, onPageWithId id: String) {
38 |
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/WavePageControl.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'WavePageControl'
3 | s.version = '1.0.0'
4 | s.summary = 'WavePageControl is a powerful & elegant way to organise pages indicators or thumbnails.'
5 |
6 | s.description = <<-DESC
7 | WavePageControl is a Page Control that can organise and show all pages on a single screen.
8 | DESC
9 |
10 | s.homepage = 'https://github.com/mrbodich/WavePageControl'
11 | s.license = { :type => 'MIT', :file => 'LICENSE' }
12 | s.author = { 'Bogdan Chornobryvets' => 'bogdan.chornobryvets@gmail.com' }
13 | s.source = { :git => 'https://github.com/mrbodich/WavePageControl.git', :tag => s.version.to_s }
14 |
15 | s.ios.deployment_target = '13.0'
16 | s.swift_version = '5.0'
17 | s.source_files = 'Sources/WavePageControl/**/*'
18 | end
19 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E677A5F8285F05B900D1ED48 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E677A5F7285F05B900D1ED48 /* MainViewController.swift */; };
11 | E6855D09285F274700DF64C4 /* WavePageControl in Frameworks */ = {isa = PBXBuildFile; productRef = E6855D08285F274700DF64C4 /* WavePageControl */; };
12 | E6C946CB28B679A300F0EE93 /* CustomPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C946CA28B679A300F0EE93 /* CustomPageControl.swift */; };
13 | E6C946CD28B6806500F0EE93 /* SimplePageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C946CC28B6806500F0EE93 /* SimplePageControl.swift */; };
14 | E6C946CF28B6809E00F0EE93 /* PageControlShowcase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C946CE28B6809E00F0EE93 /* PageControlShowcase.swift */; };
15 | E6C946D228B6839A00F0EE93 /* DefaultPageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C946D128B6839A00F0EE93 /* DefaultPageControl.swift */; };
16 | E6D6AF8C285E013E006894EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D6AF8B285E013E006894EE /* AppDelegate.swift */; };
17 | E6D6AF8E285E013E006894EE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D6AF8D285E013E006894EE /* SceneDelegate.swift */; };
18 | E6D6AF95285E013F006894EE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E6D6AF94285E013F006894EE /* Assets.xcassets */; };
19 | E6D6AF98285E013F006894EE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E6D6AF96285E013F006894EE /* LaunchScreen.storyboard */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXFileReference section */
23 | E677A5F7285F05B900D1ED48 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; };
24 | E6855D03285F272500DF64C4 /* WavePageControl */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = WavePageControl; path = ..; sourceTree = ""; };
25 | E6C946CA28B679A300F0EE93 /* CustomPageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPageControl.swift; sourceTree = ""; };
26 | E6C946CC28B6806500F0EE93 /* SimplePageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePageControl.swift; sourceTree = ""; };
27 | E6C946CE28B6809E00F0EE93 /* PageControlShowcase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControlShowcase.swift; sourceTree = ""; };
28 | E6C946D128B6839A00F0EE93 /* DefaultPageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultPageControl.swift; sourceTree = ""; };
29 | E6D6AF88285E013E006894EE /* WavePageControlDemo-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WavePageControlDemo-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
30 | E6D6AF8B285E013E006894EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
31 | E6D6AF8D285E013E006894EE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
32 | E6D6AF94285E013F006894EE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
33 | E6D6AF97285E013F006894EE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
34 | E6D6AF99285E013F006894EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
35 | /* End PBXFileReference section */
36 |
37 | /* Begin PBXFrameworksBuildPhase section */
38 | E6D6AF85285E013E006894EE /* Frameworks */ = {
39 | isa = PBXFrameworksBuildPhase;
40 | buildActionMask = 2147483647;
41 | files = (
42 | E6855D09285F274700DF64C4 /* WavePageControl in Frameworks */,
43 | );
44 | runOnlyForDeploymentPostprocessing = 0;
45 | };
46 | /* End PBXFrameworksBuildPhase section */
47 |
48 | /* Begin PBXGroup section */
49 | E6855D02285F272500DF64C4 /* Packages */ = {
50 | isa = PBXGroup;
51 | children = (
52 | E6855D03285F272500DF64C4 /* WavePageControl */,
53 | );
54 | name = Packages;
55 | sourceTree = "";
56 | };
57 | E6855D07285F274700DF64C4 /* Frameworks */ = {
58 | isa = PBXGroup;
59 | children = (
60 | );
61 | name = Frameworks;
62 | sourceTree = "";
63 | };
64 | E6C946D028B680A700F0EE93 /* Examples */ = {
65 | isa = PBXGroup;
66 | children = (
67 | E6C946CE28B6809E00F0EE93 /* PageControlShowcase.swift */,
68 | E6C946D128B6839A00F0EE93 /* DefaultPageControl.swift */,
69 | E6C946CC28B6806500F0EE93 /* SimplePageControl.swift */,
70 | E6C946CA28B679A300F0EE93 /* CustomPageControl.swift */,
71 | );
72 | path = Examples;
73 | sourceTree = "";
74 | };
75 | E6D6AF7F285E013E006894EE = {
76 | isa = PBXGroup;
77 | children = (
78 | E6855D02285F272500DF64C4 /* Packages */,
79 | E6D6AF8A285E013E006894EE /* WavePageControlDemo-iOS */,
80 | E6D6AF89285E013E006894EE /* Products */,
81 | E6855D07285F274700DF64C4 /* Frameworks */,
82 | );
83 | sourceTree = "";
84 | };
85 | E6D6AF89285E013E006894EE /* Products */ = {
86 | isa = PBXGroup;
87 | children = (
88 | E6D6AF88285E013E006894EE /* WavePageControlDemo-iOS.app */,
89 | );
90 | name = Products;
91 | sourceTree = "";
92 | };
93 | E6D6AF8A285E013E006894EE /* WavePageControlDemo-iOS */ = {
94 | isa = PBXGroup;
95 | children = (
96 | E6D6AF8B285E013E006894EE /* AppDelegate.swift */,
97 | E6D6AF8D285E013E006894EE /* SceneDelegate.swift */,
98 | E677A5F7285F05B900D1ED48 /* MainViewController.swift */,
99 | E6C946D028B680A700F0EE93 /* Examples */,
100 | E6D6AF94285E013F006894EE /* Assets.xcassets */,
101 | E6D6AF96285E013F006894EE /* LaunchScreen.storyboard */,
102 | E6D6AF99285E013F006894EE /* Info.plist */,
103 | );
104 | path = "WavePageControlDemo-iOS";
105 | sourceTree = "";
106 | };
107 | /* End PBXGroup section */
108 |
109 | /* Begin PBXNativeTarget section */
110 | E6D6AF87285E013E006894EE /* WavePageControlDemo-iOS */ = {
111 | isa = PBXNativeTarget;
112 | buildConfigurationList = E6D6AF9C285E013F006894EE /* Build configuration list for PBXNativeTarget "WavePageControlDemo-iOS" */;
113 | buildPhases = (
114 | E6D6AF84285E013E006894EE /* Sources */,
115 | E6D6AF85285E013E006894EE /* Frameworks */,
116 | E6D6AF86285E013E006894EE /* Resources */,
117 | );
118 | buildRules = (
119 | );
120 | dependencies = (
121 | );
122 | name = "WavePageControlDemo-iOS";
123 | packageProductDependencies = (
124 | E6855D08285F274700DF64C4 /* WavePageControl */,
125 | );
126 | productName = "WavePageControlDemo-iOS";
127 | productReference = E6D6AF88285E013E006894EE /* WavePageControlDemo-iOS.app */;
128 | productType = "com.apple.product-type.application";
129 | };
130 | /* End PBXNativeTarget section */
131 |
132 | /* Begin PBXProject section */
133 | E6D6AF80285E013E006894EE /* Project object */ = {
134 | isa = PBXProject;
135 | attributes = {
136 | BuildIndependentTargetsInParallel = 1;
137 | LastSwiftUpdateCheck = 1340;
138 | LastUpgradeCheck = 1340;
139 | TargetAttributes = {
140 | E6D6AF87285E013E006894EE = {
141 | CreatedOnToolsVersion = 13.4;
142 | };
143 | };
144 | };
145 | buildConfigurationList = E6D6AF83285E013E006894EE /* Build configuration list for PBXProject "WavePageControlDemo-iOS" */;
146 | compatibilityVersion = "Xcode 13.0";
147 | developmentRegion = en;
148 | hasScannedForEncodings = 0;
149 | knownRegions = (
150 | en,
151 | Base,
152 | );
153 | mainGroup = E6D6AF7F285E013E006894EE;
154 | productRefGroup = E6D6AF89285E013E006894EE /* Products */;
155 | projectDirPath = "";
156 | projectRoot = "";
157 | targets = (
158 | E6D6AF87285E013E006894EE /* WavePageControlDemo-iOS */,
159 | );
160 | };
161 | /* End PBXProject section */
162 |
163 | /* Begin PBXResourcesBuildPhase section */
164 | E6D6AF86285E013E006894EE /* Resources */ = {
165 | isa = PBXResourcesBuildPhase;
166 | buildActionMask = 2147483647;
167 | files = (
168 | E6D6AF98285E013F006894EE /* LaunchScreen.storyboard in Resources */,
169 | E6D6AF95285E013F006894EE /* Assets.xcassets in Resources */,
170 | );
171 | runOnlyForDeploymentPostprocessing = 0;
172 | };
173 | /* End PBXResourcesBuildPhase section */
174 |
175 | /* Begin PBXSourcesBuildPhase section */
176 | E6D6AF84285E013E006894EE /* Sources */ = {
177 | isa = PBXSourcesBuildPhase;
178 | buildActionMask = 2147483647;
179 | files = (
180 | E6D6AF8C285E013E006894EE /* AppDelegate.swift in Sources */,
181 | E6D6AF8E285E013E006894EE /* SceneDelegate.swift in Sources */,
182 | E6C946CB28B679A300F0EE93 /* CustomPageControl.swift in Sources */,
183 | E6C946D228B6839A00F0EE93 /* DefaultPageControl.swift in Sources */,
184 | E6C946CD28B6806500F0EE93 /* SimplePageControl.swift in Sources */,
185 | E677A5F8285F05B900D1ED48 /* MainViewController.swift in Sources */,
186 | E6C946CF28B6809E00F0EE93 /* PageControlShowcase.swift in Sources */,
187 | );
188 | runOnlyForDeploymentPostprocessing = 0;
189 | };
190 | /* End PBXSourcesBuildPhase section */
191 |
192 | /* Begin PBXVariantGroup section */
193 | E6D6AF96285E013F006894EE /* LaunchScreen.storyboard */ = {
194 | isa = PBXVariantGroup;
195 | children = (
196 | E6D6AF97285E013F006894EE /* Base */,
197 | );
198 | name = LaunchScreen.storyboard;
199 | sourceTree = "";
200 | };
201 | /* End PBXVariantGroup section */
202 |
203 | /* Begin XCBuildConfiguration section */
204 | E6D6AF9A285E013F006894EE /* Debug */ = {
205 | isa = XCBuildConfiguration;
206 | buildSettings = {
207 | ALWAYS_SEARCH_USER_PATHS = NO;
208 | CLANG_ANALYZER_NONNULL = YES;
209 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
210 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
211 | CLANG_ENABLE_MODULES = YES;
212 | CLANG_ENABLE_OBJC_ARC = YES;
213 | CLANG_ENABLE_OBJC_WEAK = YES;
214 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
215 | CLANG_WARN_BOOL_CONVERSION = YES;
216 | CLANG_WARN_COMMA = YES;
217 | CLANG_WARN_CONSTANT_CONVERSION = YES;
218 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
219 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
220 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
221 | CLANG_WARN_EMPTY_BODY = YES;
222 | CLANG_WARN_ENUM_CONVERSION = YES;
223 | CLANG_WARN_INFINITE_RECURSION = YES;
224 | CLANG_WARN_INT_CONVERSION = YES;
225 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
226 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
227 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
228 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
229 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
230 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
231 | CLANG_WARN_STRICT_PROTOTYPES = YES;
232 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
233 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
234 | CLANG_WARN_UNREACHABLE_CODE = YES;
235 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
236 | COPY_PHASE_STRIP = NO;
237 | DEBUG_INFORMATION_FORMAT = dwarf;
238 | ENABLE_STRICT_OBJC_MSGSEND = YES;
239 | ENABLE_TESTABILITY = YES;
240 | GCC_C_LANGUAGE_STANDARD = gnu11;
241 | GCC_DYNAMIC_NO_PIC = NO;
242 | GCC_NO_COMMON_BLOCKS = YES;
243 | GCC_OPTIMIZATION_LEVEL = 0;
244 | GCC_PREPROCESSOR_DEFINITIONS = (
245 | "DEBUG=1",
246 | "$(inherited)",
247 | );
248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
249 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
250 | GCC_WARN_UNDECLARED_SELECTOR = YES;
251 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
252 | GCC_WARN_UNUSED_FUNCTION = YES;
253 | GCC_WARN_UNUSED_VARIABLE = YES;
254 | IPHONEOS_DEPLOYMENT_TARGET = 15.5;
255 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
256 | MTL_FAST_MATH = YES;
257 | ONLY_ACTIVE_ARCH = YES;
258 | SDKROOT = iphoneos;
259 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
260 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
261 | };
262 | name = Debug;
263 | };
264 | E6D6AF9B285E013F006894EE /* Release */ = {
265 | isa = XCBuildConfiguration;
266 | buildSettings = {
267 | ALWAYS_SEARCH_USER_PATHS = NO;
268 | CLANG_ANALYZER_NONNULL = YES;
269 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
270 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
271 | CLANG_ENABLE_MODULES = YES;
272 | CLANG_ENABLE_OBJC_ARC = YES;
273 | CLANG_ENABLE_OBJC_WEAK = YES;
274 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
275 | CLANG_WARN_BOOL_CONVERSION = YES;
276 | CLANG_WARN_COMMA = YES;
277 | CLANG_WARN_CONSTANT_CONVERSION = YES;
278 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
279 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
280 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
281 | CLANG_WARN_EMPTY_BODY = YES;
282 | CLANG_WARN_ENUM_CONVERSION = YES;
283 | CLANG_WARN_INFINITE_RECURSION = YES;
284 | CLANG_WARN_INT_CONVERSION = YES;
285 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
286 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
287 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
288 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
289 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
290 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
291 | CLANG_WARN_STRICT_PROTOTYPES = YES;
292 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
293 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
294 | CLANG_WARN_UNREACHABLE_CODE = YES;
295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
296 | COPY_PHASE_STRIP = NO;
297 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
298 | ENABLE_NS_ASSERTIONS = NO;
299 | ENABLE_STRICT_OBJC_MSGSEND = YES;
300 | GCC_C_LANGUAGE_STANDARD = gnu11;
301 | GCC_NO_COMMON_BLOCKS = YES;
302 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
303 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
304 | GCC_WARN_UNDECLARED_SELECTOR = YES;
305 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
306 | GCC_WARN_UNUSED_FUNCTION = YES;
307 | GCC_WARN_UNUSED_VARIABLE = YES;
308 | IPHONEOS_DEPLOYMENT_TARGET = 15.5;
309 | MTL_ENABLE_DEBUG_INFO = NO;
310 | MTL_FAST_MATH = YES;
311 | SDKROOT = iphoneos;
312 | SWIFT_COMPILATION_MODE = wholemodule;
313 | SWIFT_OPTIMIZATION_LEVEL = "-O";
314 | VALIDATE_PRODUCT = YES;
315 | };
316 | name = Release;
317 | };
318 | E6D6AF9D285E013F006894EE /* Debug */ = {
319 | isa = XCBuildConfiguration;
320 | buildSettings = {
321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
322 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
323 | CODE_SIGN_STYLE = Automatic;
324 | CURRENT_PROJECT_VERSION = 1;
325 | DEVELOPMENT_TEAM = "";
326 | GENERATE_INFOPLIST_FILE = YES;
327 | INFOPLIST_FILE = "WavePageControlDemo-iOS/Info.plist";
328 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
329 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
330 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
331 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
333 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
334 | LD_RUNPATH_SEARCH_PATHS = (
335 | "$(inherited)",
336 | "@executable_path/Frameworks",
337 | );
338 | MARKETING_VERSION = 1.0;
339 | PRODUCT_BUNDLE_IDENTIFIER = "bogdan.chornobryvets.WavePageControlDemo-iOS";
340 | PRODUCT_NAME = "$(TARGET_NAME)";
341 | SWIFT_EMIT_LOC_STRINGS = YES;
342 | SWIFT_VERSION = 5.0;
343 | TARGETED_DEVICE_FAMILY = 1;
344 | };
345 | name = Debug;
346 | };
347 | E6D6AF9E285E013F006894EE /* Release */ = {
348 | isa = XCBuildConfiguration;
349 | buildSettings = {
350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
351 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
352 | CODE_SIGN_STYLE = Automatic;
353 | CURRENT_PROJECT_VERSION = 1;
354 | DEVELOPMENT_TEAM = "";
355 | GENERATE_INFOPLIST_FILE = YES;
356 | INFOPLIST_FILE = "WavePageControlDemo-iOS/Info.plist";
357 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
358 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
359 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
360 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
361 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
362 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
363 | LD_RUNPATH_SEARCH_PATHS = (
364 | "$(inherited)",
365 | "@executable_path/Frameworks",
366 | );
367 | MARKETING_VERSION = 1.0;
368 | PRODUCT_BUNDLE_IDENTIFIER = "bogdan.chornobryvets.WavePageControlDemo-iOS";
369 | PRODUCT_NAME = "$(TARGET_NAME)";
370 | SWIFT_EMIT_LOC_STRINGS = YES;
371 | SWIFT_VERSION = 5.0;
372 | TARGETED_DEVICE_FAMILY = 1;
373 | };
374 | name = Release;
375 | };
376 | /* End XCBuildConfiguration section */
377 |
378 | /* Begin XCConfigurationList section */
379 | E6D6AF83285E013E006894EE /* Build configuration list for PBXProject "WavePageControlDemo-iOS" */ = {
380 | isa = XCConfigurationList;
381 | buildConfigurations = (
382 | E6D6AF9A285E013F006894EE /* Debug */,
383 | E6D6AF9B285E013F006894EE /* Release */,
384 | );
385 | defaultConfigurationIsVisible = 0;
386 | defaultConfigurationName = Release;
387 | };
388 | E6D6AF9C285E013F006894EE /* Build configuration list for PBXNativeTarget "WavePageControlDemo-iOS" */ = {
389 | isa = XCConfigurationList;
390 | buildConfigurations = (
391 | E6D6AF9D285E013F006894EE /* Debug */,
392 | E6D6AF9E285E013F006894EE /* Release */,
393 | );
394 | defaultConfigurationIsVisible = 0;
395 | defaultConfigurationName = Release;
396 | };
397 | /* End XCConfigurationList section */
398 |
399 | /* Begin XCSwiftPackageProductDependency section */
400 | E6855D08285F274700DF64C4 /* WavePageControl */ = {
401 | isa = XCSwiftPackageProductDependency;
402 | productName = WavePageControl;
403 | };
404 | /* End XCSwiftPackageProductDependency section */
405 | };
406 | rootObject = E6D6AF80285E013E006894EE /* Project object */;
407 | }
408 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS.xcodeproj/xcshareddata/xcschemes/WavePageControlDemo-iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 18.06.2022.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Assets.xcassets/Page Dot Border.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.600",
9 | "green" : "0.600",
10 | "red" : "0.600"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.300",
27 | "green" : "0.237",
28 | "red" : "0.162"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Assets.xcassets/Page Dot.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.500",
8 | "blue" : "1.000",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "0.500",
26 | "blue" : "0.600",
27 | "green" : "0.476",
28 | "red" : "0.325"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Examples/CustomPageControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomPageControl.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 24.08.2022.
6 | //
7 |
8 | import SwiftUI
9 | import WavePageControl
10 |
11 | final class CustomPageControl: PageControlShowcase {
12 | override func viewDidLoad() {
13 | super.viewDidLoad()
14 | view.backgroundColor = .black
15 | }
16 |
17 | override func setupPageControl(_ pageControl: UIWavePageControl) {
18 | pageControl.setDelegate(self)
19 | }
20 |
21 | //WavePageControl setup example
22 | override func updatePageControl(_ pageControl: UIWavePageControl, screenWidth: CGFloat) {
23 | //MARK: You can change UIWavePageControl parameters
24 | let screenWidth = screenWidth * 0.8
25 | let itemHeight = min(screenWidth / 10, 50)
26 |
27 | pageControl.maxNavigationWidth = screenWidth
28 | pageControl.defaultButtonHeight = itemHeight
29 | pageControl.defaultSpacing = itemHeight
30 | pageControl.minSpacing = screenWidth / 140
31 | pageControl.updateLayout() //Will animate to the new layout
32 | }
33 | }
34 |
35 | extension CustomPageControl: WavePageControlDelegate {
36 | func createCustomPageView(for id: String) -> WavePageButtonView {
37 |
38 | //Default implementation of this delegate method is DefaultPageButtonView()
39 |
40 | //Here we are creating fully custom view for the page. Must inherit WavePageButtonView.
41 | CustomPageButtonView(with: id)
42 | }
43 |
44 | func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: String, isGestureCompleted: Bool) {
45 |
46 | //You can simply set current page.
47 | //Next row is a default implementation of this delegate method.
48 | //wavePageControl.currentPage = id
49 |
50 | if currentPage != id {
51 | currentPage = id
52 | vibrate()
53 | }
54 | //Animate Page Control size
55 | switch isGestureCompleted {
56 | case false:
57 | if wavePageControl.transform == .identity {
58 | UIView.animate(withDuration: 0.3) {
59 | wavePageControl.transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
60 | }
61 | }
62 | case true:
63 | UIView.animate(withDuration: 0.3) {
64 | wavePageControl.transform = .identity
65 | }
66 | }
67 | }
68 |
69 | func didTap(_ wavePageControl: UIWavePageControl, onPageWithId id: String) {
70 |
71 | //You can simply set current page.
72 | //Next row is a default implementation of this delegate method.
73 | //wavePageControl.currentPage = id
74 |
75 | if currentPage != id {
76 | currentPage = id
77 | vibrate()
78 | }
79 | }
80 | }
81 |
82 | //Custom page view must inherit WavePageButtonView
83 | final class CustomPageButtonView: WavePageButtonView {
84 | private let accentColor: UIColor = .white
85 | private let transparentColor: UIColor = .black
86 | private var currentHeight: CGFloat? = nil
87 | private var isZoomed: Bool = false
88 |
89 | let label = UILabel()
90 | let infoView = UIView()
91 | var offsetConstraint: NSLayoutConstraint!
92 | var widthConstraint: NSLayoutConstraint!
93 | var heightConstraint: NSLayoutConstraint!
94 |
95 | init(with text: String) {
96 | super.init()
97 | infoView.layer.borderColor = accentColor.cgColor
98 | infoView.backgroundColor = accentColor
99 | infoView.layer.borderWidth = 1
100 | infoView.translatesAutoresizingMaskIntoConstraints = false
101 | offsetConstraint = infoView.centerYAnchor.constraint(equalTo: centerYAnchor)
102 | widthConstraint = infoView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1)
103 | heightConstraint = infoView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1)
104 |
105 | label.translatesAutoresizingMaskIntoConstraints = false
106 | label.text = text.uppercased()
107 | label.font = .systemFont(ofSize: 30, weight: .bold)
108 | label.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
109 | label.textColor = .red
110 | label.alpha = 0
111 | infoView.addSubview(label)
112 | label.centerXAnchor.constraint(equalTo: infoView.centerXAnchor).isActive = true
113 | label.centerYAnchor.constraint(equalTo: infoView.centerYAnchor).isActive = true
114 | }
115 |
116 | override func didMoveToSuperview() {
117 | super.didMoveToSuperview()
118 | addSubview(infoView)
119 | NSLayoutConstraint.activate([
120 | infoView.centerXAnchor.constraint(equalTo: centerXAnchor),
121 | offsetConstraint,
122 | widthConstraint,
123 | heightConstraint
124 | ])
125 | zoomConstraints()
126 | }
127 |
128 | public override func didChangeHeight(to height: CGFloat) {
129 | let newHeight = isZoomed ? height * 1.6 / 2 : height / 4
130 | infoView.layer.cornerRadius = newHeight
131 | }
132 |
133 | public override func didChangeState(_ state: WavePageButtonState) {
134 | switch state {
135 | case .default:
136 | infoView.backgroundColor = transparentColor
137 | label.alpha = 0
138 | label.transform = CGAffineTransform(scaleX: 0.3, y: 0.3)
139 | isZoomed = false
140 | case .active:
141 | infoView.backgroundColor = accentColor
142 | label.alpha = 1
143 | label.transform = CGAffineTransform(scaleX: 1, y: 1)
144 | isZoomed = true
145 | }
146 | zoomConstraints()
147 | }
148 |
149 | private func zoomConstraints() {
150 | offsetConstraint.constant = isZoomed ? -50 : 0
151 | widthConstraint = widthConstraint.setMultiplier(multiplier: isZoomed ? 1.6 : 1)
152 | heightConstraint = heightConstraint.setMultiplier(multiplier: isZoomed ? 2.5 : 1)
153 | }
154 |
155 | }
156 |
157 | fileprivate extension NSLayoutConstraint {
158 | func setMultiplier(multiplier: CGFloat) -> NSLayoutConstraint {
159 | let isActiveState = isActive
160 | isActive = false
161 | let newConstraint = NSLayoutConstraint(
162 | item: firstItem!,
163 | attribute: firstAttribute,
164 | relatedBy: relation,
165 | toItem: secondItem,
166 | attribute: secondAttribute,
167 | multiplier: multiplier,
168 | constant: constant)
169 |
170 | newConstraint.priority = priority
171 | newConstraint.shouldBeArchived = self.shouldBeArchived
172 | newConstraint.identifier = self.identifier
173 |
174 | if isActiveState {
175 | NSLayoutConstraint.activate([newConstraint])
176 | }
177 | return newConstraint
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Examples/DefaultPageControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultPageControl.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 24.08.2022.
6 | //
7 |
8 | import SwiftUI
9 | import WavePageControl
10 |
11 | final class DefaultPageControl: PageControlShowcase {
12 | //WavePageControl setup example
13 | override func updatePageControl(_ pageControl: UIWavePageControl, screenWidth: CGFloat) {
14 | //MARK: You can change UIWavePageControl parameters
15 | let screenWidth = screenWidth * 0.8
16 | let itemHeight = min(screenWidth / 10, 50)
17 |
18 | pageControl.maxNavigationWidth = screenWidth
19 | pageControl.defaultButtonHeight = itemHeight
20 | pageControl.defaultSpacing = itemHeight
21 | pageControl.minSpacing = screenWidth / 140
22 | pageControl.updateLayout() //Will animate to the new layout
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Examples/PageControlShowcase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageControlShowcase.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 24.08.2022.
6 | //
7 |
8 | import UIKit
9 | import WavePageControl
10 | import Combine
11 |
12 | class PageControlShowcase: UIViewController {
13 | private let allowedChars: String = "abcdefghijklmnopqrstuvwxyz"
14 | @Published var pageIDs: [String] = []
15 | @Published var currentPage: String = ""
16 | private let pageControl = UIWavePageControl()
17 | private var bucket = Set()
18 | private let screenWidth = UIScreen.main.bounds.width * 0.8
19 | private let showExtraControls: Bool
20 |
21 | init(showExtraControls: Bool = true) {
22 | self.showExtraControls = showExtraControls
23 | super.init(nibName: nil, bundle: nil)
24 | }
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 | view.backgroundColor = .white
29 | let vStack = UIStackView()
30 | vStack.translatesAutoresizingMaskIntoConstraints = false
31 | vStack.axis = .vertical
32 | vStack.alignment = .center
33 | vStack.spacing = 20
34 | view.addSubview(vStack)
35 |
36 | NSLayoutConstraint.activate([
37 | vStack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
38 | vStack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
39 | vStack.widthAnchor.constraint(equalTo: view.widthAnchor)
40 | ])
41 |
42 | let itemsLabel = UILabel()
43 | itemsLabel.translatesAutoresizingMaskIntoConstraints = false
44 | itemsLabel.heightAnchor.constraint(equalToConstant: 30).isActive = true
45 | itemsLabel.alpha = showExtraControls ? 1 : 0
46 | vStack.addArrangedSubview(itemsLabel)
47 |
48 | let separator = UIView()
49 | separator.translatesAutoresizingMaskIntoConstraints = false
50 | separator.heightAnchor.constraint(equalToConstant: 100).isActive = true
51 | vStack.addArrangedSubview(separator)
52 |
53 | vStack.addArrangedSubview(pageControl)
54 | UIView.performWithoutAnimation {
55 | setupPageControl(pageControl)
56 | updatePageControl(pageControl, screenWidth: screenWidth)
57 | pageControl.layoutIfNeeded()
58 | }
59 |
60 | addButtons(to: vStack)
61 |
62 | let startPageIDs = randomChars()
63 | pageIDs = startPageIDs
64 | UIView.performWithoutAnimation {
65 | pageControl.pageIDs = startPageIDs
66 | itemsLabel.attributedText = self.attributedTitle(for: startPageIDs, selectedItem: "")
67 | pageControl.layoutIfNeeded()
68 | itemsLabel.layoutIfNeeded()
69 | }
70 |
71 | $pageIDs
72 | .receive(on: DispatchQueue.main)
73 | .sink { [weak self] newPageIDs in
74 | guard let self = self else { return }
75 | itemsLabel.attributedText = self.attributedTitle(for: newPageIDs, selectedItem: self.pageControl.currentPage)
76 | itemsLabel.layoutIfNeeded()
77 | self.pageControl.pageIDs = newPageIDs
78 | }
79 | .store(in: &bucket)
80 |
81 | $currentPage
82 | .receive(on: DispatchQueue.main)
83 | .sink { [weak self] newCurrentPage in
84 | guard let self = self else { return }
85 | itemsLabel.attributedText = self.attributedTitle(for: self.pageControl.pageIDs, selectedItem: newCurrentPage)
86 | itemsLabel.layoutIfNeeded()
87 | self.pageControl.currentPage = newCurrentPage
88 | }
89 | .store(in: &bucket)
90 | }
91 |
92 | func setupPageControl(_ pageControl: UIWavePageControl) {}
93 | func updatePageControl(_ pageControl: UIWavePageControl, screenWidth: CGFloat) { }
94 |
95 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
96 | updatePageControl(pageControl, screenWidth: size.width)
97 | }
98 |
99 | private func addButtons(to stack: UIStackView) {
100 | let buttonsStack = UIStackView()
101 | buttonsStack.translatesAutoresizingMaskIntoConstraints = false
102 | buttonsStack.axis = .horizontal
103 | buttonsStack.spacing = 30
104 | let addButton = getButton(withTitle: "Add", selector: #selector(addPagePressed))
105 | let removeButton = getButton(withTitle: "Remove", selector: #selector(removePagePressed))
106 | let customButton = getButton(withTitle: "Custom", selector: #selector(randomisePages))
107 | let throwActiveButton = getButton(withTitle: "< Move >", selector: #selector(moveActivePage))
108 |
109 | buttonsStack.addArrangedSubview(addButton)
110 | buttonsStack.addArrangedSubview(removeButton)
111 | buttonsStack.addArrangedSubview(customButton)
112 | if showExtraControls {
113 | buttonsStack.addArrangedSubview(throwActiveButton)
114 | }
115 | stack.addArrangedSubview(buttonsStack)
116 | }
117 |
118 | private func getButton(withTitle title: String, selector: Selector) -> UIButton {
119 | let button = UIButton(type: .system)
120 | button.translatesAutoresizingMaskIntoConstraints = false
121 | button.setTitle(title, for: .normal)
122 | button.titleLabel?.font = .systemFont(ofSize: 18)
123 | button.addTarget(self, action: selector, for: .touchUpInside)
124 | return button
125 | }
126 |
127 | private func attributedTitle(for stringArray: [String], selectedItem: String?) -> NSAttributedString {
128 | let font = UIFont.monospacedSystemFont(ofSize: 20, weight: .bold)
129 | let attributedString = NSMutableAttributedString(string: stringArray.joined())
130 | attributedString.setAttributes([.font: font, .foregroundColor: UIColor.gray],
131 | range: NSRange(location: 0, length: attributedString.length))
132 | if let index = stringArray.firstIndex(of: selectedItem ?? "") {
133 | let range = NSRange(location: index,length: 1)
134 | attributedString.setAttributes([.font: font.withSize(25), .foregroundColor: UIColor.systemPink],
135 | range: range)
136 | }
137 | return attributedString
138 | }
139 |
140 | private func randomChars(length: Int? = nil) -> [String] {
141 | let length = length ?? Int.random(in: 5..<20)
142 | let slice = allowedChars.shuffled().map(String.init).prefix(length)
143 | return Array(slice)
144 | }
145 |
146 | private func removePage(at index: Int) {
147 | pageIDs.remove(at: index < pageIDs.count ? index : pageIDs.count - 1)
148 | }
149 |
150 | private func addPage(at index: Int) {
151 | guard let item = Set(allowedChars.map(String.init))
152 | .subtracting(Set(pageIDs))
153 | .randomElement() else { return }
154 | pageIDs.insert(item, at: index)
155 | }
156 |
157 | func vibrate() {
158 | let generator = UISelectionFeedbackGenerator()
159 | generator.selectionChanged()
160 | }
161 |
162 | @available(*, unavailable)
163 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
164 | }
165 |
166 | extension PageControlShowcase {
167 | @objc private func addPagePressed() {
168 | vibrate()
169 | let index = pageIDs.count > 0 ? Int.random(in: 0.. 0 else { return }
176 | removePage(at: Int.random(in: 0..= pageIDs.count / 2 ? 1 : 0)) - index
187 | pageIDs.move(fromOffsets: .init(integer: index), toOffset: .init(destinationIndex))
188 | }
189 | }
190 |
191 | fileprivate extension RandomAccessCollection {
192 | func unique(by id: KeyPath) -> [Element] {
193 | var seen: [ID: Bool] = [:]
194 | return self.filter {
195 | seen.updateValue(true, forKey: $0[keyPath: id]) == nil
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Examples/SimplePageControl.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SimplePageControl.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 24.08.2022.
6 | //
7 |
8 | import SwiftUI
9 | import WavePageControl
10 |
11 | final class SimplePageControl: PageControlShowcase {
12 | override func setupPageControl(_ pageControl: UIWavePageControl) {
13 | pageControl.setDelegate(self)
14 | }
15 |
16 | //WavePageControl setup example
17 | override func updatePageControl(_ pageControl: UIWavePageControl, screenWidth: CGFloat) {
18 | //MARK: You can change UIWavePageControl parameters
19 | let screenWidth = screenWidth * 0.8
20 | let itemHeight = min(screenWidth / 9, 55)
21 |
22 | pageControl.maxNavigationWidth = screenWidth
23 | pageControl.defaultButtonHeight = itemHeight
24 | pageControl.defaultSpacing = itemHeight
25 | pageControl.minSpacing = screenWidth / 140
26 | pageControl.updateLayout() //Will animate to the new layout
27 | }
28 | }
29 |
30 | extension SimplePageControl: WavePageControlDelegate {
31 | func createCustomPageView(for id: String) -> WavePageButtonView {
32 |
33 | //Default implementation of this delegate method is DefaultPageButtonView()
34 |
35 | DefaultPageButtonView(accentColor: .green,
36 | defaultColor: .green.withAlphaComponent(0.2),
37 | dotBorderColor: .gray,
38 | borderWidth: 2)
39 | }
40 |
41 | func didSwipeScroll(_ wavePageControl: UIWavePageControl, toPageWithId id: String, isGestureCompleted: Bool) {
42 |
43 | //You can simply set current page.
44 | //Next row is a default implementation of this delegate method.
45 | //wavePageControl.currentPage = id
46 |
47 | if currentPage != id {
48 | currentPage = id
49 | vibrate()
50 | }
51 | //Animate Page Control size
52 | switch isGestureCompleted {
53 | case false:
54 | if wavePageControl.transform == .identity {
55 | UIView.animate(withDuration: 0.3) {
56 | wavePageControl.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
57 | }
58 | }
59 | case true:
60 | UIView.animate(withDuration: 0.3) {
61 | wavePageControl.transform = .identity
62 | }
63 | }
64 | }
65 |
66 | func didTap(_ wavePageControl: UIWavePageControl, onPageWithId id: String) {
67 |
68 | //You can simply set current page.
69 | //Next row is a default implementation of this delegate method.
70 | //wavePageControl.currentPage = id
71 |
72 | if currentPage != id {
73 | currentPage = id
74 | vibrate()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/MainViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 18.06.2022.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 | import Combine
11 | import WavePageControl
12 |
13 | class MainViewController: UINavigationController {
14 | var bucket = Set()
15 | let pageControls: [(description: String, vc: PageControlShowcase)] = [
16 | ("Default", DefaultPageControl(showExtraControls: false)),
17 | ("Simple", SimplePageControl()),
18 | ("Custom", CustomPageControl())
19 | ]
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 | view.backgroundColor = .white
24 |
25 | let baseVC = UIViewController()
26 | baseVC.view.backgroundColor = .white
27 | pushViewController(baseVC, animated: false)
28 |
29 | let vStack = UIStackView()
30 | vStack.translatesAutoresizingMaskIntoConstraints = false
31 | vStack.axis = .vertical
32 | baseVC.view.addSubview(vStack)
33 | NSLayoutConstraint.activate([
34 | vStack.centerXAnchor.constraint(equalTo: baseVC.view.centerXAnchor),
35 | vStack.centerYAnchor.constraint(equalTo: baseVC.view.centerYAnchor)
36 | ])
37 |
38 | for destination in pageControls.enumerated() {
39 | vStack.addArrangedSubview(presentingButton("Show \(destination.element.description)", vcIndex: destination.offset))
40 | }
41 | }
42 |
43 | private func presentingButton(_ title: String, vcIndex: Int) -> UIButton {
44 | let button = UIButton(type: .system)
45 | button.tag = vcIndex
46 | button.translatesAutoresizingMaskIntoConstraints = false
47 | button.setTitle(title, for: .normal)
48 | button.titleLabel?.font = .systemFont(ofSize: 20)
49 | button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
50 | return button
51 | }
52 |
53 | @objc private func didTapButton(_ sender: UIButton) {
54 | self.pushViewController(pageControls[sender.tag].vc, animated: true)
55 | }
56 |
57 | }
58 |
59 | //MARK: - SwiftUI
60 |
61 | struct MainViewControllerRepresentable: UIViewControllerRepresentable {
62 | func makeUIViewController(context: Context) -> MainViewController {
63 | MainViewController()
64 | }
65 |
66 | func updateUIViewController(_ uiViewController: MainViewController, context: Context) {
67 |
68 | }
69 | }
70 |
71 | #if DEBUG
72 | struct MainViewControllerRepresentable_Preview: PreviewProvider {
73 | static var previews: some View {
74 | MainViewControllerRepresentable()
75 | .edgesIgnoringSafeArea(.all)
76 | }
77 | }
78 | #endif
79 |
--------------------------------------------------------------------------------
/WavePageControlDemo-iOS/WavePageControlDemo-iOS/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // WavePageControlDemo-iOS
4 | //
5 | // Created by Bogdan Chornobryvets on 18.06.2022.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | guard let windowScene = (scene as? UIWindowScene) else { return }
16 |
17 | let window = UIWindow(windowScene: windowScene)
18 | window.rootViewController = MainViewController()
19 | self.window = window
20 | window.makeKeyAndVisible()
21 | }
22 |
23 | }
24 |
25 |
--------------------------------------------------------------------------------