18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Ian McDowell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tab View
2 |
3 |
4 | TiltedTabView •
5 | TabView •
6 | InputAssistant •
7 | Git
8 |
9 |
10 | --------
11 |
12 | A replacement for UITabViewController, which mimics Safari tabs on iOS
13 |
14 | [](https://travis-ci.org/IMcD23/TabView)
15 | [](https://github.com/IMcD23/TabView/releases/latest)
16 | 
17 | [](https://twitter.com/ian_mcdowell)
18 |
19 |
20 |
21 | # Requirements
22 |
23 | * Xcode 9 or later
24 | * iOS 11.0 or later
25 |
26 | # Usage
27 |
28 | There are two primary view controllers in this library: `TabViewController` and `TabViewContainerViewController`.
29 | A `TabViewController` contains an array of tabs, a visible tab, and some methods to add and remove tabs. A `TabViewContainerViewController` contains `TabViewController`s.
30 |
31 | It's not necessary to use a `TabViewContainerViewController`, but it's suggested, as it allows for split screen on iPad.
32 |
33 | To get started, take a look at the public API for both classes, and look at the sample app for an example of how to use both.
34 | At a minimum, you must subclass or instantiate a `TabViewController`, and add and remove tabs from it using its `activateTab(_:)` and `closeTab(_:)` methods.
35 |
36 | # Installation
37 |
38 | ## Carthage
39 | To install TabView using [Carthage](https://github.com/Carthage/Carthage), add the following line to your Cartfile:
40 |
41 | ```
42 | github "IMcD23/TabView" "master"
43 | ```
44 |
45 | ## Submodule
46 | To install TabView as a submodule into your git repository, run the following command:
47 |
48 | ```
49 | git submodule add -b master https://github.com/IMcD23/TabView.git Path/To/TabView
50 | git submodule update --init --recursive
51 | ```
52 |
53 | Then, add the `.xcodeproj` in the root of the repository into your Xcode project, and add it as a build dependency.
54 |
55 | ## ibuild
56 | A Swift static library of this project is also available for the ibuild build system. Learn more about ibuild [here](https://github.com/IMcD23/ibuild)
57 |
58 | # Author
59 | Created by [Ian McDowell](https://ianmcdowell.net)
60 |
61 | # License
62 | All code in this project is available under the license specified in the LICENSE file. However, since this project also bundles code from other projects, you are subject to those projects' licenses as well.
63 |
--------------------------------------------------------------------------------
/Resources/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-mcdowell/TabView/1f18d29f836a64b5bdc185f05838c3b6c39803ea/Resources/Screenshot.png
--------------------------------------------------------------------------------
/Sample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Sample
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import TabView
11 |
12 | @UIApplicationMain
13 | class AppDelegate: UIResponder, UIApplicationDelegate {
14 |
15 | var window: UIWindow?
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 |
19 | window = UIWindow(frame: UIScreen.main.bounds)
20 | let tabRoot = TabViewContainerViewController.init(theme: TabViewThemeLight())
21 | window?.rootViewController = tabRoot
22 | window?.makeKeyAndVisible()
23 |
24 | return true
25 | }
26 |
27 | func applicationWillResignActive(_ application: UIApplication) { }
28 | func applicationDidEnterBackground(_ application: UIApplication) { }
29 | func applicationWillEnterForeground(_ application: UIApplication) { }
30 | func applicationDidBecomeActive(_ application: UIApplication) { }
31 | func applicationWillTerminate(_ application: UIApplication) { }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/Add.imageset/Add.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ian-mcdowell/TabView/1f18d29f836a64b5bdc185f05838c3b6c39803ea/Sample/Assets.xcassets/Add.imageset/Add.pdf
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/Add.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Add.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/Sample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Sample/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 |
--------------------------------------------------------------------------------
/Sample/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 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIRequiredDeviceCapabilities
26 |
27 | armv7
28 |
29 | UISupportedInterfaceOrientations
30 |
31 | UIInterfaceOrientationPortrait
32 | UIInterfaceOrientationLandscapeLeft
33 | UIInterfaceOrientationLandscapeRight
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Sample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Sample
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import TabView
11 |
12 | class ViewController: TabViewController {
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 |
17 | self.title = "Tab View"
18 |
19 | // Set the tabs in this tab view
20 | self.viewControllers = [
21 | Tab(title: "White", color: .white),
22 | Tab(title: "Black", color: .black),
23 | Tab(title: "Red", color: .red),
24 | Tab(title: "Green", color: .green),
25 | Tab(title: "Blue", color: .blue),
26 | Tab(title: "A really long title", color: .blue)
27 | ]
28 |
29 | // Add a "Add tab" button to the right
30 | self.navigationItem.rightBarButtonItem = UIBarButtonItem.init(image: #imageLiteral(resourceName: "Add"), style: .plain, target: self, action: #selector(addTab))
31 | // Add a "Theme Toggle" button to the left
32 | self.navigationItem.leftBarButtonItem = UIBarButtonItem.init(title: "Theme Toggle", style: .plain, target: self, action: #selector(toggleTheme))
33 | }
34 |
35 | @objc private func addTab() {
36 | // Add a new tab and switch to it
37 | self.activateTab(Tab(title: "New Tab With Title", color: .white))
38 | }
39 |
40 | @objc private func toggleTheme() {
41 | // The theme can be changed at any time by setting the `theme` property.
42 | if type(of: self.theme) == TabViewThemeLight.self {
43 | self.theme = TabViewThemeDark()
44 | } else {
45 | self.theme = TabViewThemeLight()
46 | }
47 | }
48 |
49 | }
50 |
51 | private class Tab: UITableViewController {
52 |
53 | init(title: String, color: UIColor) {
54 | super.init(nibName: nil, bundle: nil)
55 | self.title = title
56 | view.backgroundColor = color
57 |
58 | // This view controller's navigation items will be visible next to the tab bar's navigation items.
59 | self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "\(title) Action", style: .done, target: nil, action: nil)
60 | }
61 |
62 | required init?(coder aDecoder: NSCoder) {
63 | fatalError("init(coder:) has not been implemented")
64 | }
65 |
66 | override func viewDidLoad() {
67 | super.viewDidLoad()
68 |
69 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
70 | tableView.tableFooterView = UIView()
71 | }
72 |
73 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
74 | return 4
75 | }
76 |
77 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
78 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
79 |
80 | cell.textLabel?.text = "\(indexPath.row)"
81 | return cell
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/Internal/Extension/NSLayoutConstraint+Custom.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSLayoutConstraint+Custom.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | public extension NSLayoutConstraint {
12 |
13 | /// Add a priority to an existing constraint.
14 | /// Useful when creating and setting at the same time:
15 | /// self.constraint = self.widthAnchor.constraint(equalToConstant: 0).withPriority(.defaultHigh)
16 | func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
17 | self.priority = priority
18 | return self
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Internal/Extension/UIBarButtonItem+View.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIBarButtonItem+View.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIBarButtonItem {
12 |
13 | /// Takes a UIBarButtonItem and converts it to a UIBarButtonItemView, or instead returns its custom view if it has one.
14 | func toView() -> UIView {
15 | if let customView = self.customView {
16 | return customView
17 | }
18 |
19 | return UIBarButtonItemView(item: self)
20 | }
21 | }
22 |
23 | /// Class that attempts to properly render a UIBarButtonItem in a UIButton.
24 | /// Supports:
25 | /// - title
26 | /// - style
27 | /// - image
28 | /// Doesn't support:
29 | /// - system icons
30 | private class UIBarButtonItemView: UIButton {
31 | var item: UIBarButtonItem?
32 | private var itemObservation: NSKeyValueObservation?
33 |
34 | convenience init(item: UIBarButtonItem) {
35 | self.init(type: .system)
36 | self.item = item
37 | self.imageView?.contentMode = .scaleAspectFit
38 | setTitle(item.title, for: .normal)
39 | setImage(item.image, for: .normal)
40 | self.tintColor = item.tintColor
41 | if let target = item.target, let action = item.action {
42 | addTarget(target, action: action, for: .touchUpInside)
43 | }
44 | self.titleLabel?.font = item.style == .done ? UIFont.boldSystemFont(ofSize: 17) : UIFont.systemFont(ofSize: 17)
45 | itemObservation = item.observe(\.title) { [weak self] item, _ in
46 | self?.setTitle(item.title, for: .normal)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/Internal/TabCollectionView/TabViewTabCollectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabViewTabCollectionView.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | private let closeButtonSize: CGFloat = 28
12 | private let closeButtonImageSize: CGFloat = 15
13 | private let closeButtonImagePadding: CGFloat = 4
14 | private let closeButtonImageThickness: CGFloat = 1
15 | private let titleLabelPadding: CGFloat = 12
16 |
17 | /// Collection view to display a horizontal list of tabs.
18 | class TabViewTabCollectionView: UICollectionView {
19 |
20 | /// The bar that the collection view is inside.
21 | weak var bar: TabViewBar?
22 |
23 | private var barDataSource: TabViewBarDataSource? { return bar?.barDataSource }
24 | private var barDelegate: TabViewBarDelegate? { return bar?.barDelegate }
25 |
26 | var theme: TabViewTheme {
27 | didSet { applyTheme(theme) }
28 | }
29 |
30 | init(theme: TabViewTheme) {
31 | self.theme = theme
32 |
33 | super.init(frame: .zero, collectionViewLayout: TabViewTabCollectionViewLayout())
34 |
35 | self.backgroundColor = nil
36 | self.showsHorizontalScrollIndicator = false
37 | self.showsVerticalScrollIndicator = false
38 | self.decelerationRate = UIScrollViewDecelerationRateFast
39 | self.allowsMultipleSelection = false
40 |
41 | // Enable drag and drop
42 | self.dragInteractionEnabled = true
43 |
44 | self.register(TabViewTab.self, forCellWithReuseIdentifier: "Tab")
45 |
46 | self.delegate = self
47 | self.dataSource = self
48 | self.dragDelegate = self
49 | self.dropDelegate = self
50 |
51 | applyTheme(theme)
52 | }
53 |
54 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
55 |
56 | /// Calls `update` for each visible cell.
57 | /// Useful to update title and such without affecting selection state
58 | func updateVisibleTabs() {
59 | for cell in self.visibleCells.flatMap({ $0 as? TabViewTab }) {
60 | cell.update()
61 | }
62 | }
63 |
64 | /// Apply the given theme to the view
65 | private func applyTheme(_ theme: TabViewTheme) {
66 | (self.collectionViewLayout as? TabViewTabCollectionViewLayout)?.separatorColor = theme.separatorColor
67 | updateVisibleTabs()
68 | }
69 |
70 | private var viewControllers: [UIViewController] {
71 | return barDataSource?.viewControllers ?? []
72 | }
73 |
74 | }
75 | extension TabViewTabCollectionView: UICollectionViewDataSource {
76 |
77 | public func numberOfSections(in collectionView: UICollectionView) -> Int {
78 | return 1
79 | }
80 |
81 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
82 | return viewControllers.count
83 | }
84 |
85 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
86 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Tab", for: indexPath) as! TabViewTab
87 | let tab = viewControllers[indexPath.row]
88 |
89 | cell.collectionView = self
90 | cell.setTab(tab)
91 |
92 | return cell
93 | }
94 | }
95 | extension TabViewTabCollectionView: UICollectionViewDelegate {
96 |
97 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
98 | let viewController = viewControllers[indexPath.row]
99 |
100 | barDelegate?.activateTab(viewController)
101 | }
102 |
103 | }
104 |
105 | extension TabViewTabCollectionView: UICollectionViewDragDelegate {
106 |
107 | func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {
108 | barDelegate?.dragInProgress = true
109 | session.localContext = self.barDelegate
110 | }
111 |
112 | func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
113 | barDelegate?.dragInProgress = false
114 | }
115 |
116 | func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
117 | let dragItem = UIDragItem.init(itemProvider: NSItemProvider.init())
118 | dragItem.localObject = viewControllers[indexPath.item]
119 |
120 | // Render the cell in the given size, so even if it is shrunk (on iPad), it will be a reasonable size.
121 | let size = CGSize.init(width: 120, height: collectionView.bounds.height)
122 | let snapshot = self.snapshotCell(at: indexPath, withSize: size)
123 |
124 | // Put the snapshot in an image view and give it to the drag item for previewing
125 | let imageView = UIImageView(image: snapshot)
126 | let parameters = self.collectionView(collectionView, dragPreviewParametersForItemAt: indexPath)!
127 | dragItem.previewProvider = { return UIDragPreview.init(view: imageView, parameters: parameters) }
128 | return [
129 | dragItem
130 | ]
131 | }
132 |
133 | private func snapshotCell(at indexPath: IndexPath, withSize size: CGSize) -> UIImage {
134 | guard let view = cellForItem(at: indexPath) else { return UIImage() }
135 | let frame = view.frame
136 | view.frame = CGRect.init(origin: .zero, size: size)
137 | let image = UIGraphicsImageRenderer.init(size: size).image { context in
138 | view.drawHierarchy(in: view.frame, afterScreenUpdates: true)
139 | }
140 | view.frame = frame
141 | return image
142 | }
143 |
144 | func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? {
145 | let parameters = UIDragPreviewParameters()
146 | // Since the cell may not have a background color (if it's selected), set one to the background color of the bar
147 | parameters.backgroundColor = theme.barTintColor
148 | return parameters
149 | }
150 |
151 | func collectionView(_ collectionView: UICollectionView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool {
152 | // Don't let tabs escape the current app.
153 | return true
154 | }
155 | }
156 |
157 | extension TabViewTabCollectionView: UICollectionViewDropDelegate {
158 |
159 | func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
160 | guard let localSession = session.localDragSession, let localObject = localSession.items.first?.localObject else { return false }
161 | let canHandle = localObject is UIViewController
162 | return canHandle
163 | }
164 |
165 | func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
166 | return UICollectionViewDropProposal.init(operation: .move, intent: .insertAtDestinationIndexPath)
167 | }
168 |
169 | func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
170 | guard
171 | let dragItem = coordinator.session.localDragSession?.items.first,
172 | let destinationIndexPath = coordinator.destinationIndexPath,
173 | let viewController = dragItem.localObject as? UIViewController,
174 | let oldDelegate = coordinator.session.localDragSession?.localContext as? TabViewBarDelegate
175 | else { return }
176 | oldDelegate.closeTab(viewController)
177 | barDelegate?.insertTab(viewController, atIndex: destinationIndexPath.item)
178 | self.barDelegate?.activateTab(viewController)
179 | }
180 | }
181 |
182 | private class TabViewTab: UICollectionViewCell {
183 |
184 | private let titleView: UILabel
185 | private let closeButton: UIButton
186 |
187 | private var titleViewLeadingConstraint: NSLayoutConstraint?
188 | private var titleViewWidthConstraint: NSLayoutConstraint?
189 |
190 | private weak var currentTab: UIViewController?
191 | weak var collectionView: TabViewTabCollectionView?
192 |
193 | override var isSelected: Bool {
194 | didSet { update() }
195 | }
196 |
197 | override init(frame: CGRect) {
198 | closeButton = UIButton()
199 | titleView = UILabel()
200 |
201 | super.init(frame: frame)
202 |
203 | self.clipsToBounds = true
204 |
205 | let buttonSize = CGSize(width: closeButtonSize, height: closeButtonSize)
206 | let buttonImageSize = CGSize(width: closeButtonImageSize, height: closeButtonImageSize)
207 | let buttonSizeDiff = CGSize(width: buttonSize.width - buttonImageSize.width, height: buttonSize.height - buttonImageSize.height)
208 | let buttonInsets = UIEdgeInsets(top: buttonSizeDiff.height / 2, left: buttonSizeDiff.width / 2, bottom: buttonSizeDiff.height / 2, right: buttonSizeDiff.width / 2)
209 |
210 | closeButton.setImage(TabViewTab.closeImage, for: .normal)
211 | closeButton.imageView?.layer.cornerRadius = buttonImageSize.width / 2
212 | closeButton.imageEdgeInsets = buttonInsets
213 |
214 | closeButton.addTarget(self, action: #selector(TabViewTab.closeButtonTapped), for: .touchUpInside)
215 |
216 | titleView.textAlignment = .center
217 | titleView.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
218 | titleView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
219 |
220 | closeButton.translatesAutoresizingMaskIntoConstraints = false
221 | titleView.translatesAutoresizingMaskIntoConstraints = false
222 | contentView.addSubview(closeButton)
223 | contentView.addSubview(titleView)
224 |
225 | NSLayoutConstraint.activate([
226 | closeButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
227 | closeButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
228 | closeButton.widthAnchor.constraint(equalToConstant: buttonSize.width).withPriority(.defaultHigh),
229 | closeButton.heightAnchor.constraint(equalToConstant: buttonSize.height).withPriority(.defaultHigh)
230 | ])
231 |
232 | let titleViewLeadingConstraint = titleView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: titleLabelPadding)
233 | let titleViewWidthConstraint = titleView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)
234 | self.titleViewLeadingConstraint = titleViewLeadingConstraint
235 | self.titleViewWidthConstraint = titleViewWidthConstraint
236 | NSLayoutConstraint.activate([
237 | titleViewLeadingConstraint,
238 | titleView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -titleLabelPadding).withPriority(.defaultHigh),
239 | titleViewWidthConstraint,
240 | titleView.topAnchor.constraint(equalTo: contentView.topAnchor),
241 | titleView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
242 | ])
243 | }
244 |
245 | public required init?(coder aDecoder: NSCoder) {
246 | fatalError("init(coder:) has not been implemented")
247 | }
248 |
249 | override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
250 | // Don't provide our own attributes. The default implementation calls systemLayoutSizeFittingSize, which is expensive.
251 | return layoutAttributes
252 | }
253 |
254 | private var isActive: Bool {
255 | return collectionView?.bar?.barDataSource?.visibleViewController == currentTab
256 | }
257 |
258 | func applyTheme(_ theme: TabViewTheme) {
259 |
260 | closeButton.imageView?.tintColor = theme.tabCloseButtonColor
261 | closeButton.imageView?.backgroundColor = theme.tabCloseButtonBackgroundColor
262 |
263 | if isActive {
264 | self.backgroundColor = nil
265 | titleView.textColor = theme.tabSelectedTextColor
266 | } else {
267 | self.backgroundColor = theme.tabBackgroundDeselectedColor
268 | titleView.textColor = theme.tabTextColor
269 | }
270 | }
271 |
272 | func setTab(_ tab: UIViewController) {
273 | currentTab = tab
274 |
275 | update()
276 | }
277 |
278 | override func prepareForReuse() {
279 | super.prepareForReuse()
280 |
281 | titleView.text = nil
282 | }
283 |
284 | func update() {
285 | if let theme = collectionView?.theme {
286 | applyTheme(theme)
287 | }
288 |
289 | self.closeButton.isHidden = !self.isActive || self.bounds.size.width < closeButtonSize
290 |
291 | titleView.text = self.currentTab?.title
292 | if !closeButton.isHidden && self.bounds.width - titleView.intrinsicContentSize.width - titleLabelPadding * 2 < closeButtonSize {
293 | self.titleViewLeadingConstraint?.constant = closeButtonSize
294 | self.titleViewWidthConstraint?.constant = 120 - (closeButtonSize + titleLabelPadding)
295 | } else {
296 | self.titleViewLeadingConstraint?.constant = titleLabelPadding
297 | self.titleViewWidthConstraint?.constant = 120 - titleLabelPadding * 2
298 | }
299 | }
300 |
301 | @objc func closeButtonTapped() {
302 | if let currentTab = currentTab {
303 | collectionView?.bar?.barDelegate?.closeTab(currentTab)
304 | }
305 | }
306 |
307 | private static var closeImage: UIImage = {
308 | let size = CGSize(width: closeButtonImageSize, height: closeButtonImageSize)
309 | let start = closeButtonImagePadding
310 | let finish = size.width - closeButtonImagePadding
311 | let thickness = closeButtonImageThickness
312 | return UIGraphicsImageRenderer(size: size).image(actions: { context in
313 | let downwards = UIBezierPath()
314 | downwards.move(to: CGPoint(x: start, y: start))
315 | downwards.addLine(to: CGPoint(x: finish, y: finish))
316 | UIColor.white.setStroke()
317 | downwards.lineWidth = thickness
318 | downwards.stroke()
319 |
320 | let upwards = UIBezierPath()
321 | upwards.move(to: CGPoint(x: start, y: finish))
322 | upwards.addLine(to: CGPoint(x: finish, y: start))
323 | UIColor.white.setStroke()
324 | upwards.lineWidth = thickness
325 | upwards.stroke()
326 |
327 | context.cgContext.addPath(downwards.cgPath)
328 | context.cgContext.addPath(upwards.cgPath)
329 | }).withRenderingMode(.alwaysTemplate)
330 | }()
331 | }
332 |
--------------------------------------------------------------------------------
/Sources/Internal/TabCollectionView/TabViewTabCollectionViewLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabViewTabCollectionViewLayout.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/5/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Custom layout attributes, which pass necessary data between layout and its decoration views.
12 | private class LayoutAttributes: UICollectionViewLayoutAttributes {
13 | var separatorColor: UIColor?
14 |
15 | override func isEqual(_ object: Any?) -> Bool {
16 | return separatorColor == (object as? LayoutAttributes)?.separatorColor && super.isEqual(object)
17 | }
18 | }
19 |
20 | /// Custom flow-layout-like collection view layout.
21 | /// In regular horizontal size classes, it collapses tabs into each other, similar to Safari for iPad.
22 | class TabViewTabCollectionViewLayout: UICollectionViewLayout {
23 |
24 | // Provide our own custom layout class.
25 | override class var layoutAttributesClass: AnyClass { return LayoutAttributes.self }
26 |
27 | /// Color of the separator decoration views. Set by the collectionView.
28 | var separatorColor: UIColor = .white {
29 | didSet { self.invalidateLayout() }
30 | }
31 |
32 | /// A minimum width that an item can be. Items may be squished further when they are going off screen (Regular size class)
33 | private let minimumItemWidth: CGFloat = 120
34 |
35 | /// Layout attributes for each cell, indexed by the cell index in section 0
36 | private var cellLayoutAttributes: [UICollectionViewLayoutAttributes] = []
37 |
38 | /// Layout attributes for a separator per cell.
39 | private var separatorLayoutAttributes: [UICollectionViewLayoutAttributes] = []
40 |
41 | /// The total width, determined in `prepare`
42 | private var totalWidth: CGFloat = 0
43 |
44 | /// Construct the layout attributes for each item, as well as attributes for decorators.
45 | override func prepare() {
46 | super.prepare()
47 |
48 | self.register(SeparatorView.self, forDecorationViewOfKind: SeparatorView.elementKind)
49 |
50 | guard let collectionView = collectionView, collectionView.numberOfSections > 0 else {
51 | return
52 | }
53 |
54 | let collectionViewWidth = collectionView.bounds.size.width
55 |
56 | // Only a single section is supported.
57 | let numberOfItems = collectionView.numberOfItems(inSection: 0)
58 |
59 | // Reset old values
60 | cellLayoutAttributes = []
61 | separatorLayoutAttributes = []
62 |
63 | // Calculate a target item width, constrained to minimum width
64 | let itemWidth = max(minimumItemWidth, collectionViewWidth / CGFloat(numberOfItems))
65 | totalWidth = itemWidth * CGFloat(numberOfItems)
66 | for itemIndex in 0.. 0 {
81 | // Get the offset of the item to the right of this one, and wherever its leftmost point is (position + offset),
82 | // our width is the difference between that position and ours.
83 | let adjacentOffset = self.itemOffset(in: collectionView, itemIndex: itemIndex + 1, itemWidth: itemWidth, numberOfItems: numberOfItems)
84 | let adjacentPosition = itemWidth * CGFloat(itemIndex + 1)
85 | width = (adjacentPosition + adjacentOffset) - itemPosition
86 | } else if itemOffset < 0 && itemIndex > 0 {
87 | // This item is on the right side of the screen. We want to move our position past the end of the previous item.
88 | // We have already calculated attributes for the item on the left (since we do this in order), so retrieve those.
89 | let previousAttributes = cellLayoutAttributes[itemIndex - 1]
90 | let positionDiff = (previousAttributes.frame.origin.x + previousAttributes.frame.size.width) - itemPosition
91 | itemPosition += positionDiff
92 | width = itemWidth - positionDiff
93 | } else {
94 | // If the item isn't being offset, then it can have the standard width.
95 | width = itemWidth
96 | }
97 |
98 | attributes.frame = CGRect(
99 | x: itemPosition,
100 | y: 0,
101 | width: width,
102 | height: collectionView.bounds.size.height
103 | )
104 |
105 | // The first item should be on top, so the z-index is the opposite of the index of the item.
106 | attributes.zIndex = numberOfItems - itemIndex
107 |
108 | cellLayoutAttributes.append(attributes)
109 |
110 | // Create a separator, right off the left side of the cell. It's 0.5px wide, and offset by -0.5px
111 | let separatorAttributes = LayoutAttributes.init(forDecorationViewOfKind: SeparatorView.elementKind, with: indexPath)
112 | separatorAttributes.separatorColor = self.separatorColor
113 | separatorAttributes.frame = CGRect(
114 | x: itemPosition - 0.5,
115 | y: 0,
116 | width: 0.5,
117 | height: collectionView.bounds.size.height
118 | )
119 | separatorAttributes.zIndex = numberOfItems + 1
120 |
121 | separatorLayoutAttributes.append(separatorAttributes)
122 | }
123 | }
124 |
125 | /// Calculate an offset for an item relative to its original position.
126 | /// This offset will create a parallax-y overlay effect, similar to that in Safari for iPad.
127 | private func itemOffset(in collectionView: UICollectionView, itemIndex: Int, itemWidth: CGFloat, numberOfItems: Int) -> CGFloat {
128 | // Disable this offset logic on iPhone
129 | if collectionView.traitCollection.horizontalSizeClass != .regular { return 0 }
130 |
131 | let collectionViewWidth = collectionView.bounds.size.width
132 |
133 | // Define a portion of the width of the collection view that will be squished. Applies to both sides.
134 | let overlapDistance = collectionViewWidth / 8
135 |
136 | // Get current scroll position. (will be from 0...contentSize.width - collectionViewWidth)
137 | let currentOffset = collectionView.contentOffset.x
138 | // Get current scroll position on the right side (will be from collectionViewWidth...contentSize.width)
139 | let currentRightOffset = currentOffset + collectionViewWidth
140 |
141 | /// Where (in pixels) is the item that we're calculating the offset for?
142 | let itemPosition = itemWidth * CGFloat(itemIndex)
143 | /// Where (in pixels) is the right border of the item?
144 | let itemRightPosition = itemPosition + itemWidth
145 |
146 | let itemOffset: CGFloat
147 |
148 | // Core logic:
149 | // Calculate the number of squished items that are in this area, then calculate the item's position in that area.
150 | // The offset will be between the zero position and the overlap distance, based on the item's position
151 |
152 | // Is the item to the left of the left overlap point?
153 | if currentOffset > itemPosition - overlapDistance {
154 | // Position for the item to be stuck all the way to the left of the collection view
155 | let zeroPosition = currentOffset - itemPosition
156 |
157 | let numberOfSquishedItems = ((currentOffset + overlapDistance) / itemWidth)
158 | if numberOfSquishedItems == 0 || itemIndex == 0 {
159 | itemOffset = max(zeroPosition, 0)
160 | } else {
161 | let multiplier = (CGFloat(itemIndex) / numberOfSquishedItems)
162 | itemOffset = zeroPosition + (overlapDistance * multiplier)
163 | }
164 | }
165 | // Is the item to the right of the right overlap point?
166 | else if itemRightPosition + overlapDistance > currentRightOffset {
167 | // Position for the item to be stuck all the way to the right of the collection view
168 | let rightPosition = currentRightOffset - itemRightPosition
169 |
170 | let numberOfSquishedItems = (totalWidth - (currentRightOffset - overlapDistance)) / itemWidth
171 | if numberOfSquishedItems == 0 || itemIndex == numberOfItems - 1 {
172 | itemOffset = min(rightPosition, 0)
173 | } else {
174 | let multiplier = (CGFloat((numberOfItems - 1) - itemIndex) / numberOfSquishedItems)
175 | itemOffset = rightPosition - (overlapDistance * multiplier)
176 | }
177 | } else {
178 | itemOffset = 0
179 | }
180 | return itemOffset
181 | }
182 |
183 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
184 | return true
185 | }
186 |
187 | override var collectionViewContentSize: CGSize {
188 | return CGSize.init(width: totalWidth, height: collectionView?.frame.size.height ?? 0)
189 | }
190 |
191 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
192 | return cellLayoutAttributes[indexPath.item]
193 | }
194 |
195 | override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
196 | switch elementKind {
197 | case SeparatorView.elementKind:
198 | if indexPath.item < separatorLayoutAttributes.count {
199 | return separatorLayoutAttributes[indexPath.item]
200 | }
201 | return UICollectionViewLayoutAttributes.init(forDecorationViewOfKind: elementKind, with: indexPath)
202 | default:
203 | return nil
204 | }
205 | }
206 |
207 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
208 | return (cellLayoutAttributes + separatorLayoutAttributes).filter { rect.intersects($0.frame) }
209 | }
210 |
211 | override func layoutAttributesForInteractivelyMovingItem(at indexPath: IndexPath, withTargetPosition position: CGPoint) -> UICollectionViewLayoutAttributes {
212 | let attributes = super.layoutAttributesForInteractivelyMovingItem(at: indexPath, withTargetPosition: position)
213 | attributes.frame.size.width = minimumItemWidth
214 | return attributes
215 | }
216 |
217 | override func indexPathsToDeleteForDecorationView(ofKind elementKind: String) -> [IndexPath] {
218 | switch elementKind {
219 | case SeparatorView.elementKind:
220 | return separatorLayoutAttributes.map { $0.indexPath }
221 | default:
222 | return []
223 | }
224 | }
225 |
226 | }
227 |
228 | extension TabViewTabCollectionViewLayout {
229 |
230 | class SeparatorView: UICollectionReusableView {
231 |
232 | static let elementKind: String = "SeparatorView"
233 |
234 | override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
235 | if let attributes = layoutAttributes as? LayoutAttributes {
236 | self.backgroundColor = attributes.separatorColor
237 | }
238 | }
239 | }
240 | }
241 |
242 |
--------------------------------------------------------------------------------
/Sources/Internal/TabViewBar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabViewBar.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | private let barHeight: CGFloat = 48
12 | private let tabHeight: CGFloat = 33
13 |
14 | protocol TabViewBarDataSource: class {
15 | var title: String? { get }
16 | var viewControllers: [UIViewController] { get }
17 | var visibleViewController: UIViewController? { get }
18 | var hidesSingleTab: Bool { get }
19 | }
20 |
21 | protocol TabViewBarDelegate: class {
22 | func activateTab(_ tab: UIViewController)
23 | func closeTab(_ tab: UIViewController)
24 | func insertTab(_ tab: UIViewController, atIndex index: Int)
25 | var dragInProgress: Bool { get set }
26 | }
27 |
28 | /// Replacement for UINavigationBar, contains a TabCollectionView at the bottom.
29 | class TabViewBar: UIView {
30 |
31 | /// Object that provides tabs & a title to the bar.
32 | weak var barDataSource: TabViewBarDataSource?
33 |
34 | /// Object that reacts to tabs being moved, activated, or closed by the user.
35 | weak var barDelegate: TabViewBarDelegate?
36 |
37 | var theme: TabViewTheme {
38 | didSet { self.applyTheme(theme) }
39 | }
40 |
41 | /// The bar has a visual effect view with a blur effect determined by the current theme.
42 | /// This tries to match UINavigationBar's blur effect as best as it can.
43 | private let visualEffectView: UIVisualEffectView
44 |
45 | /// Bold title label in the top center.
46 | private let titleLabel: UILabel
47 |
48 | /// Stack view containing views for the leading bar button items.
49 | private let leadingBarButtonStackView: UIStackView
50 |
51 | /// Stack view containing views for the trailing bar button items.
52 | private let trailingBarButtonStackView: UIStackView
53 |
54 | /// Collection view containing the tabs from the data source
55 | private let tabCollectionView: TabViewTabCollectionView
56 |
57 | /// View below the tabCollectionView that is a 1px separator
58 | private let separator: UIView
59 |
60 | /// Constraint that places the top of the tabCollectionView.
61 | /// Constant is adjusted when the view should be hidden, which causes the bar to resize.
62 | private var tabTopConstraint: NSLayoutConstraint?
63 |
64 | /// Create a new tab view bar with the given theme.
65 | init(theme: TabViewTheme) {
66 | self.theme = theme
67 |
68 | // Start with no effect, this is set in applyTheme
69 | self.visualEffectView = UIVisualEffectView(effect: nil)
70 |
71 | self.titleLabel = UILabel()
72 | self.leadingBarButtonStackView = UIStackView()
73 | self.trailingBarButtonStackView = UIStackView()
74 |
75 | self.tabCollectionView = TabViewTabCollectionView(theme: theme)
76 | self.separator = UIView()
77 |
78 | super.init(frame: .zero)
79 |
80 | tabCollectionView.bar = self
81 |
82 | addSubview(visualEffectView)
83 | visualEffectView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
84 |
85 | // Match UINavigationBar's title font
86 | titleLabel.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
87 | // Should shrink before bar button items, but should move on X axis (centerXAnchor is .defaultLow) before it shrinks.
88 | titleLabel.setContentCompressionResistancePriority(.init(500), for: .horizontal)
89 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
90 | addSubview(titleLabel)
91 |
92 | for stackView in [leadingBarButtonStackView, trailingBarButtonStackView] {
93 | stackView.alignment = .fill
94 | stackView.axis = .horizontal
95 | stackView.distribution = .fill
96 | stackView.spacing = 15
97 | stackView.translatesAutoresizingMaskIntoConstraints = false
98 | stackView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
99 | stackView.setContentHuggingPriority(.required, for: .horizontal)
100 | addSubview(stackView)
101 | }
102 | // Lay out titleLabel
103 | NSLayoutConstraint.activate([
104 | titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor).withPriority(.defaultLow),
105 | titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingBarButtonStackView.trailingAnchor, constant: 5),
106 | titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingBarButtonStackView.leadingAnchor, constant: -5),
107 | titleLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
108 | titleLabel.heightAnchor.constraint(equalToConstant: barHeight).withPriority(.defaultHigh)
109 | ])
110 |
111 | // Lay out stack views
112 | NSLayoutConstraint.activate([
113 | leadingBarButtonStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 15),
114 | leadingBarButtonStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
115 | leadingBarButtonStackView.heightAnchor.constraint(equalToConstant: barHeight).withPriority(.defaultHigh),
116 | trailingBarButtonStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -15),
117 | trailingBarButtonStackView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
118 | trailingBarButtonStackView.heightAnchor.constraint(equalToConstant: barHeight).withPriority(.defaultHigh)
119 | ])
120 |
121 | // Lay out tab collection view
122 | tabCollectionView.translatesAutoresizingMaskIntoConstraints = false
123 | addSubview(tabCollectionView)
124 | let tabTopConstraint = tabCollectionView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor)
125 | self.tabTopConstraint = tabTopConstraint
126 | NSLayoutConstraint.activate([
127 | tabCollectionView.heightAnchor.constraint(equalToConstant: tabHeight),
128 | tabTopConstraint,
129 | tabCollectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
130 | tabCollectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
131 | tabCollectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
132 | ])
133 |
134 | // Add separator below tab collection view
135 | separator.translatesAutoresizingMaskIntoConstraints = false
136 | addSubview(separator)
137 | NSLayoutConstraint.activate([
138 | separator.heightAnchor.constraint(equalToConstant: 0.5).withPriority(.defaultHigh),
139 | separator.leadingAnchor.constraint(equalTo: leadingAnchor),
140 | separator.trailingAnchor.constraint(equalTo: trailingAnchor),
141 | separator.bottomAnchor.constraint(equalTo: bottomAnchor)
142 | ])
143 |
144 | applyTheme(theme)
145 | }
146 |
147 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
148 |
149 | private func applyTheme(_ theme: TabViewTheme) {
150 | self.backgroundColor = theme.barTintColor.withAlphaComponent(0.7)
151 | self.visualEffectView.effect = UIBlurEffect.init(style: theme.barBlurStyle)
152 | self.titleLabel.textColor = theme.barTitleColor
153 | self.separator.backgroundColor = theme.separatorColor
154 | self.tabCollectionView.theme = theme
155 | }
156 |
157 | /// Reset the leading items.
158 | func setLeadingBarButtonItems(_ barButtonItems: [UIBarButtonItem]) {
159 | let views = barButtonItems.map { $0.toView() }
160 |
161 | for view in leadingBarButtonStackView.arrangedSubviews {
162 | view.removeFromSuperview()
163 | }
164 | for view in views {
165 | leadingBarButtonStackView.addArrangedSubview(view)
166 | }
167 | }
168 |
169 | /// Reset the trailing items.
170 | func setTrailingBarButtonItems(_ barButtonItems: [UIBarButtonItem]) {
171 | let views = barButtonItems.map { $0.toView() }
172 |
173 | for view in trailingBarButtonStackView.arrangedSubviews {
174 | view.removeFromSuperview()
175 | }
176 | for view in views {
177 | trailingBarButtonStackView.addArrangedSubview(view)
178 | }
179 | }
180 |
181 | /// Add a new tab at the given index. Animates.
182 | func addTab(atIndex index: Int) {
183 | tabCollectionView.performBatchUpdates({
184 | tabCollectionView.insertItems(at: [IndexPath.init(item: index, section: 0)])
185 | }, completion: nil)
186 | self.hideTabsIfNeeded()
187 | }
188 |
189 | /// Remove the view for the tab at the given index. Animates.
190 | func removeTab(atIndex index: Int) {
191 | tabCollectionView.performBatchUpdates({
192 | tabCollectionView.deleteItems(at: [IndexPath.init(item: index, section: 0)])
193 | }, completion: nil)
194 | self.hideTabsIfNeeded()
195 | }
196 |
197 | /// Deselects other selected tabs, then selects the given tab and scrolls to it. Animates.
198 | func selectTab(atIndex index: Int) {
199 | if let indexPaths = tabCollectionView.indexPathsForSelectedItems {
200 | for indexPath in indexPaths where indexPath.item != index {
201 | tabCollectionView.deselectItem(at: indexPath, animated: true)
202 | }
203 | }
204 | tabCollectionView.selectItem(at: IndexPath.init(item: index, section: 0), animated: true, scrollPosition: .centeredHorizontally)
205 | }
206 |
207 | /// If there are less than the required number of tabs to keep the bar visible, hide it.
208 | /// Otherwise, un-hide it.
209 | func hideTabsIfNeeded() {
210 | // To hide, the bar is moved up by its height, then set to isHidden.
211 | let minimum = (barDataSource?.hidesSingleTab ?? true) ? 1 : 0
212 | let shouldHide = tabCollectionView.numberOfItems(inSection: 0) <= minimum
213 | if shouldHide && !tabCollectionView.isHidden {
214 | tabCollectionView.isHidden = true
215 | tabTopConstraint?.constant = -tabHeight
216 | } else if !shouldHide && tabCollectionView.isHidden {
217 | tabCollectionView.isHidden = false
218 | tabTopConstraint?.constant = 0
219 | }
220 | }
221 |
222 | /// Update the title in the bar, and all visible tabs' titles.
223 | func updateTitles() {
224 | self.titleLabel.text = barDataSource?.title
225 | tabCollectionView.updateVisibleTabs()
226 | }
227 |
228 | /// Force a reload of all visible data. Maintains current tab if possible.
229 | func refresh() {
230 | tabCollectionView.reloadData()
231 | updateTitles()
232 | hideTabsIfNeeded()
233 |
234 | if let visibleVC = barDataSource?.visibleViewController, let index = barDataSource?.viewControllers.index(of: visibleVC) {
235 | self.selectTab(atIndex: index)
236 | }
237 | }
238 | }
239 |
240 |
--------------------------------------------------------------------------------
/Sources/Internal/Util/NavigationItemObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationItemObserver.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Monitor class for bar button items and title of a navigation item.
12 | /// Calls its provided block when a change is detected.
13 | class NavigationItemObserver {
14 | weak var navigationItem: UINavigationItem?
15 | private var observations: [NSKeyValueObservation] = []
16 |
17 | init(navigationItem: UINavigationItem, _ changeHandler: @escaping () -> Void) {
18 | self.navigationItem = navigationItem
19 |
20 | observations = [
21 | navigationItem.observe(\UINavigationItem.leftBarButtonItem, changeHandler: { _, _ in changeHandler() }),
22 | navigationItem.observe(\UINavigationItem.rightBarButtonItem, changeHandler: { _, _ in changeHandler() }),
23 | navigationItem.observe(\UINavigationItem.leftBarButtonItems, changeHandler: { _, _ in changeHandler() }),
24 | navigationItem.observe(\UINavigationItem.rightBarButtonItems, changeHandler: { _, _ in changeHandler() }),
25 | navigationItem.observe(\UINavigationItem.title, changeHandler: { _, _ in changeHandler() })
26 | ]
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/TabViewContainerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabViewRootController.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/6/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Represents the state of a tab view container.
12 | /// Access the value of this using the `state` property.
13 | public enum TabViewContainerState {
14 | /// There is a single tab view controller visible
15 | case single
16 |
17 | /// The container is split horizontally, with a secondary tab view controller on the right.
18 | case split
19 | }
20 |
21 | /// Internal protocol that the TabViewContainerViewController conforms to,
22 | /// so other objects and reference it without knowing its generic type.
23 | internal protocol TabViewContainer: class {
24 | /// Get the current state of the container
25 | var state: TabViewContainerState { get set }
26 |
27 | /// Get the primary tab view controller
28 | var primary: TabViewController { get }
29 |
30 | /// Get the secondary tab view controller, if there is one
31 | var secondary: TabViewController? { get }
32 |
33 | /// When a tab collection view starts dragging in either side, the container is alerted.
34 | /// This is done so the container can potentially enable a drop area to enter split view.
35 | func dragStateChanged(in tabViewController: TabViewController, to newDragState: Bool)
36 |
37 | /// Sets the inset of the stack view. This is done by the drop target when the user is hovering
38 | /// over the right side.
39 | var contentViewRightInset: CGFloat { get set }
40 | }
41 |
42 | /// A tab view container view controller manages the display of tab view controllers.
43 | /// It can be in various states, as noted by its `state` property.
44 | /// It's not required that you embed a tab view controller in a container, but if you want
45 | /// the ability to go into split view, this is the suggested class to use.
46 | open class TabViewContainerViewController: UIViewController {
47 |
48 | /// The current state of the container. Set this to manually change states.
49 | public var state: TabViewContainerState {
50 | didSet {
51 | switch state {
52 | case .single:
53 | secondaryTabViewController = nil
54 | setOverrideTraitCollection(nil, forChildViewController: primaryTabViewController)
55 | case .split:
56 | let secondaryVC = TabViewType.init(theme: self.theme)
57 | // Override trait collection to be always compact horizontally, while in split mode
58 | let overriddenTraitCollection = UITraitCollection.init(traitsFrom: [
59 | self.traitCollection,
60 | UITraitCollection.init(horizontalSizeClass: .compact)
61 | ])
62 | setOverrideTraitCollection(overriddenTraitCollection, forChildViewController: primaryTabViewController)
63 | setOverrideTraitCollection(overriddenTraitCollection, forChildViewController: secondaryVC)
64 | self.secondaryTabViewController = secondaryVC
65 | }
66 | }
67 | }
68 |
69 | /// Current theme. When set, will propagate to current tab view controllers.
70 | public var theme: TabViewTheme {
71 | didSet { applyTheme(theme) }
72 | }
73 |
74 | /// A view displayed underneath the stack view, which has a background color set to the theme's border color.
75 | /// This is a relatively hacky way to display a separator when in split state.
76 | private let backgroundView: UIView
77 |
78 | /// Stack view containing visible tab view controllers.
79 | private let stackView: UIStackView
80 |
81 | /// The primary tab view controller in the container. This view controller will always be visible,
82 | /// no matter the state.
83 | public let primaryTabViewController: TabViewType
84 |
85 | /// The secondary tab view controller in the container. Is visible if the container is in split view.
86 | public private(set) var secondaryTabViewController: TabViewType? {
87 | didSet {
88 | oldValue?.view.removeFromSuperview()
89 | oldValue?.removeFromParentViewController()
90 |
91 | if let newValue = secondaryTabViewController {
92 | newValue.container = self
93 | addChildViewController(newValue)
94 | stackView.addArrangedSubview(newValue.view)
95 | newValue.didMove(toParentViewController: self)
96 | }
97 | }
98 | }
99 |
100 | /// Constraint governing the trailing position of the stack view.
101 | /// This is adjusted when using drag and drop, to make a drop area
102 | /// visible to enter split mode.
103 | private var stackViewRightConstraint: NSLayoutConstraint?
104 |
105 | /// A UIView that is used for drag and drop.
106 | private let dropView = TabViewContainerDropView()
107 |
108 | /// Create a new tab view container view controller with the given theme
109 | /// This creates a tab view controller of the given type.
110 | /// The container starts in the `single` style.
111 | public init(theme: TabViewTheme) {
112 | self.state = .single
113 | self.theme = theme
114 | self.primaryTabViewController = TabViewType.init(theme: theme)
115 | self.secondaryTabViewController = nil
116 | self.stackView = UIStackView()
117 | self.backgroundView = UIView()
118 | super.init(nibName: nil, bundle: nil)
119 |
120 | dropView.container = self
121 | primaryTabViewController.container = self
122 | addChildViewController(primaryTabViewController)
123 | }
124 |
125 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
126 |
127 | open override func viewDidLoad() {
128 | super.viewDidLoad()
129 |
130 | backgroundView.frame = stackView.bounds
131 | backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
132 | stackView.addSubview(backgroundView)
133 |
134 | // Stack view fills frame
135 | stackView.translatesAutoresizingMaskIntoConstraints = false
136 | view.addSubview(stackView)
137 | let trailingConstraint = stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)
138 | self.stackViewRightConstraint = trailingConstraint
139 | NSLayoutConstraint.activate([
140 | trailingConstraint,
141 | stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
142 | stackView.topAnchor.constraint(equalTo: view.topAnchor),
143 | stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
144 | ])
145 |
146 | stackView.distribution = .fillEqually
147 | stackView.axis = .horizontal
148 | stackView.alignment = .fill
149 | stackView.spacing = 0.5
150 | stackView.insertArrangedSubview(primaryTabViewController.view, at: 0)
151 | primaryTabViewController.didMove(toParentViewController: self)
152 |
153 | applyTheme(theme)
154 | }
155 |
156 | private func applyTheme(_ theme: TabViewTheme) {
157 | view.backgroundColor = theme.barTintColor
158 |
159 | backgroundView.backgroundColor = theme.separatorColor
160 | setNeedsStatusBarAppearanceUpdate()
161 |
162 | primaryTabViewController.theme = theme
163 | secondaryTabViewController?.theme = theme
164 | }
165 |
166 | open override var preferredStatusBarStyle: UIStatusBarStyle {
167 | return theme.statusBarStyle
168 | }
169 |
170 | }
171 |
172 | /// This transparent view is displayed on the trailing side of the container, only when a drag and drop session is active.
173 | /// It is the droppable region that a tab can be dropped into.
174 | class TabViewContainerDropView: UIView, UIDropInteractionDelegate {
175 |
176 | /// Reference to the container view controller
177 | weak var container: TabViewContainer?
178 |
179 | init() {
180 | super.init(frame: .zero)
181 |
182 | let dropInteraction = UIDropInteraction(delegate: self)
183 | addInteraction(dropInteraction)
184 | }
185 |
186 | required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
187 |
188 | // Only handle tabs
189 | public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
190 | guard let localSession = session.localDragSession, let localObject = localSession.items.first?.localObject else { return false }
191 | let canHandle = localObject is UIViewController
192 | return canHandle
193 | }
194 |
195 | // When the finger enters our view, move the stack view away
196 | func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnter session: UIDropSession) {
197 | container?.contentViewRightInset = 140
198 | }
199 |
200 | // When finger leaves, reset stack view.
201 | func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) {
202 | container?.contentViewRightInset = 0
203 | }
204 | func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) {
205 | container?.contentViewRightInset = 0
206 | }
207 |
208 | func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
209 | return UIDropProposal.init(operation: .move)
210 | }
211 |
212 | func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
213 | guard
214 | let container = self.container,
215 | let dragItem = session.localDragSession?.items.first,
216 | let viewController = dragItem.localObject as? UIViewController
217 | else { return }
218 |
219 | // Move the dropped view controller into a new secondary tab view controller.
220 | container.contentViewRightInset = 0
221 | container.state = .split
222 | container.primary.closeTab(viewController)
223 | container.secondary?.viewControllers = [viewController]
224 | }
225 | }
226 |
227 | /// Conform to the TabViewContainer protocol, which other objects (such as TabViewContainerDropView and TabViewController) talk to.
228 | extension TabViewContainerViewController: TabViewContainer {
229 |
230 | var contentViewRightInset: CGFloat {
231 | get { return -(stackViewRightConstraint?.constant ?? 0) }
232 | set {
233 | stackViewRightConstraint?.constant = -newValue
234 | UIView.animate(withDuration: 0.2) {
235 | self.view.layoutIfNeeded()
236 | }
237 | }
238 | }
239 |
240 | var primary: TabViewController {
241 | return primaryTabViewController
242 | }
243 |
244 | var secondary: TabViewController? {
245 | return secondaryTabViewController
246 | }
247 |
248 | func dragStateChanged(in tabViewController: TabViewController, to newDragState: Bool) {
249 | // If the given tab is the primary, there is no secondary, and started dragging, then show the drop view.
250 | // Otherwise, remove the drop view.
251 | if shouldEnableDropView && newDragState == true && state == .single && tabViewController == primaryTabViewController {
252 | dropView.translatesAutoresizingMaskIntoConstraints = false
253 | view.addSubview(dropView)
254 | NSLayoutConstraint.activate([
255 | dropView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
256 | dropView.topAnchor.constraint(equalTo: view.topAnchor),
257 | dropView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
258 | dropView.widthAnchor.constraint(equalToConstant: 100)
259 | ])
260 | } else {
261 | dropView.removeFromSuperview()
262 | }
263 | }
264 |
265 | private var shouldEnableDropView: Bool {
266 | return self.traitCollection.horizontalSizeClass == .regular
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/Sources/TabViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabViewController.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | open class TabViewController: UIViewController {
12 |
13 | /// The container that this tab view resides in.
14 | internal weak var container: TabViewContainer?
15 |
16 | /// Current theme
17 | public var theme: TabViewTheme {
18 | didSet { self.applyTheme(theme) }
19 | }
20 |
21 | open override var title: String? {
22 | get { return super.title ?? visibleViewController?.title }
23 | set { super.title = newValue }
24 | }
25 |
26 | /// The current tab shown in the tab view controller's content view
27 | public var visibleViewController: UIViewController? {
28 | didSet {
29 | oldValue?.removeFromParentViewController()
30 | oldValue?.view.removeFromSuperview()
31 |
32 | if let visibleViewController = visibleViewController {
33 | addChildViewController(visibleViewController)
34 | visibleViewController.view.frame = contentView.bounds
35 | contentView.addSubview(visibleViewController.view)
36 | visibleViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
37 | visibleViewController.didMove(toParentViewController: self)
38 | }
39 | updateVisibleViewControllerInsets()
40 |
41 | if let visibleViewController = visibleViewController {
42 | visibleNavigationItemObserver = NavigationItemObserver.init(navigationItem: visibleViewController.navigationItem, { [weak self] in
43 | self?.refreshTabBar()
44 | })
45 | } else {
46 | visibleNavigationItemObserver = nil
47 | }
48 | if let newValue = visibleViewController, let index = viewControllers.index(of: newValue) {
49 | tabViewBar.selectTab(atIndex: index)
50 | }
51 | refreshTabBar()
52 | }
53 | }
54 | private var _viewControllers: [UIViewController] = [] {
55 | didSet {
56 | displayEmptyViewIfNeeded()
57 | }
58 | }
59 | /// All of the tabs, in order.
60 | public var viewControllers: [UIViewController] {
61 | get { return _viewControllers }
62 | set {
63 | _viewControllers = newValue;
64 | tabViewBar.refresh()
65 | if visibleViewController == nil || !viewControllers.contains(visibleViewController!) {
66 | visibleViewController = viewControllers.first
67 | }
68 | }
69 | }
70 |
71 | /// If you want to display a view when there are no tabs, set this to some value
72 | public var emptyView: UIView? = nil {
73 | didSet {
74 | oldValue?.removeFromSuperview()
75 | displayEmptyViewIfNeeded()
76 | }
77 | }
78 |
79 | /// Store the value of the below property.
80 | private var _hidesSingleTab: Bool = true
81 | /// Should the tab bar hide when only a single tab is visible? Default: YES
82 | /// If in the right side of a split container, then always NO
83 | public var hidesSingleTab: Bool {
84 | get {
85 | if let container = container, container.state == .split { return false }
86 | return _hidesSingleTab
87 | }
88 | set { _hidesSingleTab = newValue }
89 | }
90 |
91 | /// Tab bar shown above the content view
92 | private let tabViewBar: TabViewBar
93 |
94 | /// View containing the current tab's view
95 | private let contentView: UIView
96 |
97 | private var ownNavigationItemObserver: NavigationItemObserver?
98 | private var visibleNavigationItemObserver: NavigationItemObserver?
99 |
100 | internal var dragInProgress: Bool = false {
101 | didSet { container?.dragStateChanged(in: self, to: dragInProgress) }
102 | }
103 |
104 | /// Create a new tab view controller, with a theme.
105 | public required init(theme: TabViewTheme) {
106 | self.theme = theme
107 | self.tabViewBar = TabViewBar(theme: theme)
108 | self.contentView = UIView()
109 |
110 | super.init(nibName: nil, bundle: nil)
111 |
112 | tabViewBar.barDataSource = self
113 | tabViewBar.barDelegate = self
114 |
115 | self.ownNavigationItemObserver = NavigationItemObserver.init(navigationItem: self.navigationItem, self.refreshTabBar)
116 | }
117 |
118 | public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
119 |
120 | open override func viewDidLoad() {
121 | super.viewDidLoad()
122 |
123 | // Content view fills frame
124 | contentView.frame = view.bounds
125 | contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
126 | view.addSubview(contentView)
127 |
128 | // Tab bar is on top of content view, with automatic height.
129 | tabViewBar.translatesAutoresizingMaskIntoConstraints = false
130 | view.addSubview(tabViewBar)
131 | NSLayoutConstraint.activate([
132 | tabViewBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
133 | tabViewBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
134 | tabViewBar.topAnchor.constraint(equalTo: view.topAnchor)
135 | ])
136 |
137 | self.edgesForExtendedLayout = []
138 |
139 | applyTheme(theme)
140 | }
141 |
142 | open override func viewDidLayoutSubviews() {
143 | super.viewDidLayoutSubviews()
144 |
145 | updateVisibleViewControllerInsets()
146 | }
147 |
148 | open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
149 | super.traitCollectionDidChange(previousTraitCollection)
150 |
151 | // Trait collection may change because of change in container states.
152 | // A change in state may invalidate the tab hiding behavior.
153 | tabViewBar.hideTabsIfNeeded()
154 | }
155 |
156 | /// Activates the given tab and saves the new state
157 | ///
158 | /// - Parameters:
159 | /// - viewController: the tab to activate
160 | /// - saveState: if the new state should be saved
161 | open func activateTab(_ tab: UIViewController) {
162 | if !_viewControllers.contains(tab) {
163 | tabViewBar.layoutIfNeeded()
164 | _viewControllers.append(tab)
165 | tabViewBar.addTab(atIndex: _viewControllers.count - 1)
166 | }
167 | visibleViewController = tab
168 | }
169 |
170 | /// Closes the provided tab and selects another tab to be active.
171 | ///
172 | /// - Parameter tab: the tab to close
173 | open func closeTab(_ tab: UIViewController) {
174 | if let index = _viewControllers.index(of: tab) {
175 | tabViewBar.layoutIfNeeded()
176 | _viewControllers.remove(at: index)
177 | tabViewBar.removeTab(atIndex: index)
178 |
179 | if index == 0 {
180 | visibleViewController = _viewControllers.first
181 | } else {
182 | visibleViewController = _viewControllers[index - 1]
183 | }
184 | }
185 |
186 | // If this is the secondary vc in a container, and there are none left,
187 | // close this vc by setting the state to single
188 | if _viewControllers.isEmpty, let container = container {
189 | if container.state == .split && container.secondary == self {
190 | container.state = .single
191 | }
192 | }
193 | }
194 |
195 | func insertTab(_ tab: UIViewController, atIndex index: Int) {
196 | if let oldIndex = _viewControllers.index(of: tab) {
197 | _viewControllers.remove(at: oldIndex)
198 | }
199 | _viewControllers.insert(tab, at: index)
200 | tabViewBar.addTab(atIndex: index)
201 | }
202 |
203 | open override var preferredStatusBarStyle: UIStatusBarStyle {
204 | return theme.statusBarStyle
205 | }
206 |
207 | /// Apply the current theme to the view controller and its views.
208 | private func applyTheme(_ theme: TabViewTheme) {
209 | self.view.backgroundColor = theme.backgroundColor
210 | self.setNeedsStatusBarAppearanceUpdate()
211 | tabViewBar.theme = theme
212 | }
213 |
214 | /// The safe area of the visible view controller is inset on top by the height of the bar.
215 | /// Tries to replicate behavior by UINavigationViewController.
216 | private func updateVisibleViewControllerInsets() {
217 | if let visibleViewController = visibleViewController {
218 | visibleViewController.additionalSafeAreaInsets = UIEdgeInsets(top: tabViewBar.frame.size.height - contentView.safeAreaInsets.top, left: 0, bottom: 0, right: 0)
219 | }
220 | }
221 |
222 | /// When a navigation changes, it's important to update all of the views that we display from that item.
223 | private func refreshTabBar() {
224 | tabViewBar.updateTitles()
225 | tabViewBar.setLeadingBarButtonItems((navigationItem.leftBarButtonItems ?? []) + (visibleViewController?.navigationItem.leftBarButtonItems ?? []))
226 | tabViewBar.setTrailingBarButtonItems((visibleViewController?.navigationItem.rightBarButtonItems ?? []) + (navigationItem.rightBarButtonItems ?? []))
227 | }
228 |
229 | /// Show an empty view if there is one, and there are no view controllers
230 | private func displayEmptyViewIfNeeded() {
231 | if let emptyView = self.emptyView {
232 | if viewControllers.isEmpty {
233 | emptyView.frame = contentView.bounds
234 | contentView.addSubview(emptyView)
235 | emptyView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
236 | } else {
237 | emptyView.removeFromSuperview()
238 | }
239 | }
240 | }
241 | }
242 |
243 | // Define these conformances, to make sure we expose the proper methods to the tab view bar.
244 | extension TabViewController: TabViewBarDataSource, TabViewBarDelegate {
245 | }
246 |
--------------------------------------------------------------------------------
/Sources/Theme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Theme.swift
3 | // TabView
4 | //
5 | // Created by Ian McDowell on 2/2/18.
6 | // Copyright © 2018 Ian McDowell. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Tab view controller can be displayed in various themes.
12 | /// This governs what colors things are.
13 | /// You can create your own themes, by adopting this protocol,
14 | /// or subclassing an existing theme.
15 | public protocol TabViewTheme {
16 | /// Color of the content view of the tab view controller.
17 | /// Displayed when there are no tabs, or a tab's view controller is transparent.
18 | var backgroundColor: UIColor { get }
19 |
20 | /// Color of the active tab's title shown in the bar.
21 | var barTitleColor: UIColor { get }
22 |
23 | /// A color to apply to the blur effect of the tab view.
24 | /// Has a minimal effect, similar to the UINavigationBar barTintColor property does.
25 | var barTintColor: UIColor { get }
26 |
27 | /// The style of blur to apply to the tab bar.
28 | var barBlurStyle: UIBlurEffectStyle { get }
29 |
30 | /// Color for separator lines that appear between tabs and underneath tabs.
31 | var separatorColor: UIColor { get }
32 |
33 | /// The color of the "X" in the close button.
34 | var tabCloseButtonColor: UIColor { get }
35 |
36 | /// The color of the close button's circle.
37 | var tabCloseButtonBackgroundColor: UIColor { get }
38 |
39 | /// The background to display in a deselected tab.
40 | var tabBackgroundDeselectedColor: UIColor { get }
41 |
42 | /// The color of a tab's title when deselected.
43 | var tabTextColor: UIColor { get }
44 |
45 | /// The color of a tab's title when it is active.
46 | var tabSelectedTextColor: UIColor { get }
47 |
48 | /// The status bar style (dark or light).
49 | /// Only matters if UIViewControllerBasedStatusBarAppearance is turned on.
50 | var statusBarStyle: UIStatusBarStyle { get }
51 | }
52 |
53 | /// Light tab view theme.
54 | /// Attempts to mimic UIBarStyleDefault
55 | open class TabViewThemeLight: TabViewTheme {
56 | public init() {}
57 | public var backgroundColor: UIColor = .lightGray
58 | public var barTitleColor: UIColor = .black
59 | public var barTintColor: UIColor = .init(white: 1, alpha: 1)
60 | public var barBlurStyle: UIBlurEffectStyle = .light
61 | public var separatorColor: UIColor = .init(white: 0.7, alpha: 1)
62 | public var tabCloseButtonColor: UIColor = .white
63 | public var tabCloseButtonBackgroundColor: UIColor = .init(white: 175/255, alpha: 1)
64 | public var tabBackgroundDeselectedColor: UIColor = .init(white: 0.6, alpha: 0.3)
65 | public var tabTextColor: UIColor = .init(white: 0.1, alpha: 1)
66 | public var tabSelectedTextColor: UIColor = .black
67 | public var statusBarStyle: UIStatusBarStyle = .default
68 | }
69 |
70 | /// Dark tab view theme.
71 | /// Attempts to mimic UIBarStyleBlack
72 | open class TabViewThemeDark: TabViewTheme {
73 | public init() {}
74 | public var backgroundColor: UIColor = .darkGray
75 | public var barTitleColor: UIColor = .white
76 | public var barTintColor: UIColor = .init(white: 0.2, alpha: 1)
77 | public var barBlurStyle: UIBlurEffectStyle = .dark
78 | public var separatorColor: UIColor = .init(white: 0.15, alpha: 1)
79 | public var tabCloseButtonColor: UIColor = .init(white: 50/255, alpha: 1)
80 | public var tabCloseButtonBackgroundColor: UIColor = .init(white: 0.8, alpha: 1)
81 | public var tabBackgroundDeselectedColor: UIColor = .init(white: 0.4, alpha: 0.3)
82 | public var tabTextColor: UIColor = .init(white: 0.9, alpha: 1)
83 | public var tabSelectedTextColor: UIColor = .white
84 | public var statusBarStyle: UIStatusBarStyle = .lightContent
85 | }
86 |
--------------------------------------------------------------------------------
/TabView.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod spec lint InputAssistant.podspec' to ensure this is a
3 | # valid spec and to remove all comments including this before submitting the spec.
4 | #
5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
7 | #
8 |
9 | Pod::Spec.new do |s|
10 |
11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
12 | #
13 | # These will help people to find your library, and whilst it
14 | # can feel like a chore to fill in it's definitely to your advantage. The
15 | # summary should be tweet-length, and the description more in depth.
16 | #
17 |
18 | s.name = "TabView"
19 | s.version = "1.0.2"
20 | s.summary = "A replacement for UITabViewController, which mimics Safari tabs on iOS"
21 |
22 | # This description is used to generate tags and improve search results.
23 | # * Think: What does it do? Why did you write it? What is the focus?
24 | # * Try to keep it short, snappy and to the point.
25 | # * Write the description between the DESC delimiters below.
26 | # * Finally, don't worry about the indent, CocoaPods strips it!
27 | s.description = <<-DESC
28 | TabView 1.0.1 - A replacement for UITabViewController, which mimics Safari tabs on iOS
29 | DESC
30 |
31 | s.homepage = "https://github.com/IMcD23/TabView"
32 | s.screenshots = "https://github.com/IMcD23/TabView/raw/master/Resources/Screenshot.png"
33 |
34 |
35 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
36 | #
37 | # Licensing your code is important. See http://choosealicense.com for more info.
38 | # CocoaPods will detect a license file if there is a named LICENSE*
39 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
40 | #
41 |
42 | s.license = { :type => "MIT", :file => "LICENSE" }
43 |
44 |
45 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
46 | #
47 | # Specify the authors of the library, with email addresses. Email addresses
48 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
49 | # accepts just a name if you'd rather not provide an email address.
50 | #
51 | # Specify a social_media_url where others can refer to, for example a twitter
52 | # profile URL.
53 | #
54 |
55 | s.author = { "Ian McDowell" => "me@ianmcdowell.net" }
56 | # Or just: s.author = "Ian McDowell"
57 | # s.authors = { "Ian McDowell" => "me@ianmcdowell.net" }
58 | s.social_media_url = "http://twitter.com/ian_mcdowell"
59 |
60 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
61 | #
62 | # If this Pod runs only on iOS or OS X, then specify the platform and
63 | # the deployment target. You can optionally include the target after the platform.
64 | #
65 |
66 | s.platform = :ios, "11.0"
67 | # s.platform = :ios, "5.0"
68 |
69 | # When using multiple platforms
70 | # s.ios.deployment_target = "5.0"
71 | # s.osx.deployment_target = "10.7"
72 | # s.watchos.deployment_target = "2.0"
73 | # s.tvos.deployment_target = "9.0"
74 |
75 |
76 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
77 | #
78 | # Specify the location from where the source should be retrieved.
79 | # Supports git, hg, bzr, svn and HTTP.
80 | #
81 |
82 | s.source = { :git => "https://github.com/IMcD23/TabView.git", :tag => "#{s.version}" }
83 |
84 |
85 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
86 | #
87 | # CocoaPods is smart about how it includes source code. For source files
88 | # giving a folder will include any swift, h, m, mm, c & cpp files.
89 | # For header files it will include any header in the folder.
90 | # Not including the public_header_files will make all headers public.
91 | #
92 |
93 | s.source_files = "Sources/**/*.swift"
94 |
95 |
96 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
97 | #
98 | # A list of resources included with the Pod. These are copied into the
99 | # target bundle with a build phase script. Anything else will be cleaned.
100 | # You can preserve files from being cleaned, please don't preserve
101 | # non-essential files like tests, examples and documentation.
102 | #
103 |
104 | # s.resource = "icon.png"
105 | # s.resources = "Resources/*.png"
106 |
107 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave"
108 |
109 |
110 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
111 | #
112 | # Link your library with frameworks, or libraries. Libraries do not include
113 | # the lib prefix of their name.
114 | #
115 |
116 | # s.framework = "SomeFramework"
117 | # s.frameworks = "SomeFramework", "AnotherFramework"
118 |
119 | # s.library = "iconv"
120 | # s.libraries = "iconv", "xml2"
121 |
122 |
123 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
124 | #
125 | # If your library depends on compiler flags you can set them in the xcconfig hash
126 | # where they will only apply to your library. If you depend on other Podspecs
127 | # you can include multiple dependencies to ensure it works.
128 |
129 | s.requires_arc = true
130 | s.swift_version = "4.0"
131 |
132 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
133 | # s.dependency "JSONKit", "~> 1.4"
134 |
135 | end
136 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 48;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 3C17680E20257F630031FEA9 /* TabView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C17680C20257F630031FEA9 /* TabView.h */; settings = {ATTRIBUTES = (Public, ); }; };
11 | 3C17681C20257F910031FEA9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17681B20257F910031FEA9 /* AppDelegate.swift */; };
12 | 3C17681E20257F910031FEA9 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17681D20257F910031FEA9 /* ViewController.swift */; };
13 | 3C17682320257F910031FEA9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C17682220257F910031FEA9 /* Assets.xcassets */; };
14 | 3C17682620257F910031FEA9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3C17682420257F910031FEA9 /* LaunchScreen.storyboard */; };
15 | 3C17682E20257FA90031FEA9 /* TabView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C17680920257F630031FEA9 /* TabView.framework */; };
16 | 3C17683020257FB80031FEA9 /* TabView.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 3C17680920257F630031FEA9 /* TabView.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
17 | 3C1768322025801E0031FEA9 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1768312025801E0031FEA9 /* TabViewController.swift */; };
18 | 3C176834202581BE0031FEA9 /* NSLayoutConstraint+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C176833202581BE0031FEA9 /* NSLayoutConstraint+Custom.swift */; };
19 | 3C176836202582F60031FEA9 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C176835202582F60031FEA9 /* Theme.swift */; };
20 | 3C17683B2025855C0031FEA9 /* TabViewBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17683A2025855C0031FEA9 /* TabViewBar.swift */; };
21 | 3C17683D202585920031FEA9 /* TabViewTabCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17683C202585920031FEA9 /* TabViewTabCollectionView.swift */; };
22 | 3C17683F202585D80031FEA9 /* UIBarButtonItem+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17683E202585D80031FEA9 /* UIBarButtonItem+View.swift */; };
23 | 3C1768422025A33C0031FEA9 /* NavigationItemObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1768412025A33C0031FEA9 /* NavigationItemObserver.swift */; };
24 | 3C857EB82029626A0082D945 /* TabViewTabCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C857EB72029626A0082D945 /* TabViewTabCollectionViewLayout.swift */; };
25 | 3C905FBA2025BEB90084BA63 /* NavigationItemObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1768412025A33C0031FEA9 /* NavigationItemObserver.swift */; };
26 | 3C905FBC2025BEB90084BA63 /* TabViewBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17683A2025855C0031FEA9 /* TabViewBar.swift */; };
27 | 3C905FBD2025BEB90084BA63 /* TabViewTabCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17683C202585920031FEA9 /* TabViewTabCollectionView.swift */; };
28 | 3C905FBE2025BEBB0084BA63 /* NSLayoutConstraint+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C176833202581BE0031FEA9 /* NSLayoutConstraint+Custom.swift */; };
29 | 3C905FBF2025BEBB0084BA63 /* UIBarButtonItem+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C17683E202585D80031FEA9 /* UIBarButtonItem+View.swift */; };
30 | 3C905FC02025BEBB0084BA63 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C176835202582F60031FEA9 /* Theme.swift */; };
31 | 3C905FC12025BEBB0084BA63 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1768312025801E0031FEA9 /* TabViewController.swift */; };
32 | 3CD4457A202ACFC500DD3CCB /* TabViewContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CD44579202ACFC500DD3CCB /* TabViewContainerViewController.swift */; };
33 | 3CD4457B202ACFC700DD3CCB /* TabViewContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CD44579202ACFC500DD3CCB /* TabViewContainerViewController.swift */; };
34 | /* End PBXBuildFile section */
35 |
36 | /* Begin PBXContainerItemProxy section */
37 | 3C17682B20257FA50031FEA9 /* PBXContainerItemProxy */ = {
38 | isa = PBXContainerItemProxy;
39 | containerPortal = 3C17680020257F630031FEA9 /* Project object */;
40 | proxyType = 1;
41 | remoteGlobalIDString = 3C17680820257F630031FEA9;
42 | remoteInfo = TabView;
43 | };
44 | /* End PBXContainerItemProxy section */
45 |
46 | /* Begin PBXCopyFilesBuildPhase section */
47 | 3C17682F20257FAC0031FEA9 /* Copy Frameworks */ = {
48 | isa = PBXCopyFilesBuildPhase;
49 | buildActionMask = 2147483647;
50 | dstPath = "";
51 | dstSubfolderSpec = 10;
52 | files = (
53 | 3C17683020257FB80031FEA9 /* TabView.framework in Copy Frameworks */,
54 | );
55 | name = "Copy Frameworks";
56 | runOnlyForDeploymentPostprocessing = 0;
57 | };
58 | 3C905FAF2025BEAF0084BA63 /* CopyFiles */ = {
59 | isa = PBXCopyFilesBuildPhase;
60 | buildActionMask = 2147483647;
61 | dstPath = "include/$(PRODUCT_NAME)";
62 | dstSubfolderSpec = 16;
63 | files = (
64 | );
65 | runOnlyForDeploymentPostprocessing = 0;
66 | };
67 | /* End PBXCopyFilesBuildPhase section */
68 |
69 | /* Begin PBXFileReference section */
70 | 3C17680920257F630031FEA9 /* TabView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TabView.framework; sourceTree = BUILT_PRODUCTS_DIR; };
71 | 3C17680C20257F630031FEA9 /* TabView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TabView.h; sourceTree = ""; };
72 | 3C17680D20257F630031FEA9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
73 | 3C17681920257F910031FEA9 /* TabView Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "TabView Sample.app"; sourceTree = BUILT_PRODUCTS_DIR; };
74 | 3C17681B20257F910031FEA9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
75 | 3C17681D20257F910031FEA9 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
76 | 3C17682220257F910031FEA9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
77 | 3C17682520257F910031FEA9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
78 | 3C17682720257F910031FEA9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
79 | 3C1768312025801E0031FEA9 /* TabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewController.swift; sourceTree = ""; };
80 | 3C176833202581BE0031FEA9 /* NSLayoutConstraint+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Custom.swift"; sourceTree = ""; };
81 | 3C176835202582F60031FEA9 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
82 | 3C17683A2025855C0031FEA9 /* TabViewBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewBar.swift; sourceTree = ""; };
83 | 3C17683C202585920031FEA9 /* TabViewTabCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewTabCollectionView.swift; sourceTree = ""; };
84 | 3C17683E202585D80031FEA9 /* UIBarButtonItem+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem+View.swift"; sourceTree = ""; };
85 | 3C1768412025A33C0031FEA9 /* NavigationItemObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemObserver.swift; sourceTree = ""; };
86 | 3C857EB72029626A0082D945 /* TabViewTabCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewTabCollectionViewLayout.swift; sourceTree = ""; };
87 | 3C905FB12025BEAF0084BA63 /* liblibTabView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibTabView.a; sourceTree = BUILT_PRODUCTS_DIR; };
88 | 3CD44579202ACFC500DD3CCB /* TabViewContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabViewContainerViewController.swift; sourceTree = ""; };
89 | /* End PBXFileReference section */
90 |
91 | /* Begin PBXFrameworksBuildPhase section */
92 | 3C17680520257F630031FEA9 /* Frameworks */ = {
93 | isa = PBXFrameworksBuildPhase;
94 | buildActionMask = 2147483647;
95 | files = (
96 | );
97 | runOnlyForDeploymentPostprocessing = 0;
98 | };
99 | 3C17681620257F910031FEA9 /* Frameworks */ = {
100 | isa = PBXFrameworksBuildPhase;
101 | buildActionMask = 2147483647;
102 | files = (
103 | 3C17682E20257FA90031FEA9 /* TabView.framework in Frameworks */,
104 | );
105 | runOnlyForDeploymentPostprocessing = 0;
106 | };
107 | 3C905FAE2025BEAF0084BA63 /* Frameworks */ = {
108 | isa = PBXFrameworksBuildPhase;
109 | buildActionMask = 2147483647;
110 | files = (
111 | );
112 | runOnlyForDeploymentPostprocessing = 0;
113 | };
114 | /* End PBXFrameworksBuildPhase section */
115 |
116 | /* Begin PBXGroup section */
117 | 3C1767FF20257F630031FEA9 = {
118 | isa = PBXGroup;
119 | children = (
120 | 3C17681420257F7F0031FEA9 /* Sources */,
121 | 3C17680B20257F630031FEA9 /* Framework */,
122 | 3C17681A20257F910031FEA9 /* Sample */,
123 | 3C17680A20257F630031FEA9 /* Products */,
124 | );
125 | sourceTree = "";
126 | usesTabs = 0;
127 | };
128 | 3C17680A20257F630031FEA9 /* Products */ = {
129 | isa = PBXGroup;
130 | children = (
131 | 3C17680920257F630031FEA9 /* TabView.framework */,
132 | 3C17681920257F910031FEA9 /* TabView Sample.app */,
133 | 3C905FB12025BEAF0084BA63 /* liblibTabView.a */,
134 | );
135 | name = Products;
136 | sourceTree = "";
137 | };
138 | 3C17680B20257F630031FEA9 /* Framework */ = {
139 | isa = PBXGroup;
140 | children = (
141 | 3C17680C20257F630031FEA9 /* TabView.h */,
142 | 3C17680D20257F630031FEA9 /* Info.plist */,
143 | );
144 | path = Framework;
145 | sourceTree = "";
146 | };
147 | 3C17681420257F7F0031FEA9 /* Sources */ = {
148 | isa = PBXGroup;
149 | children = (
150 | 3C176840202589440031FEA9 /* Internal */,
151 | 3C176835202582F60031FEA9 /* Theme.swift */,
152 | 3C1768312025801E0031FEA9 /* TabViewController.swift */,
153 | 3CD44579202ACFC500DD3CCB /* TabViewContainerViewController.swift */,
154 | );
155 | path = Sources;
156 | sourceTree = "";
157 | };
158 | 3C17681A20257F910031FEA9 /* Sample */ = {
159 | isa = PBXGroup;
160 | children = (
161 | 3C17681B20257F910031FEA9 /* AppDelegate.swift */,
162 | 3C17681D20257F910031FEA9 /* ViewController.swift */,
163 | 3C17682220257F910031FEA9 /* Assets.xcassets */,
164 | 3C17682420257F910031FEA9 /* LaunchScreen.storyboard */,
165 | 3C17682720257F910031FEA9 /* Info.plist */,
166 | );
167 | path = Sample;
168 | sourceTree = "";
169 | };
170 | 3C1768372025852A0031FEA9 /* Extension */ = {
171 | isa = PBXGroup;
172 | children = (
173 | 3C176833202581BE0031FEA9 /* NSLayoutConstraint+Custom.swift */,
174 | 3C17683E202585D80031FEA9 /* UIBarButtonItem+View.swift */,
175 | );
176 | path = Extension;
177 | sourceTree = "";
178 | };
179 | 3C176840202589440031FEA9 /* Internal */ = {
180 | isa = PBXGroup;
181 | children = (
182 | 3C1768432025A3DB0031FEA9 /* Util */,
183 | 3C17683A2025855C0031FEA9 /* TabViewBar.swift */,
184 | 3C857EB6202962440082D945 /* TabCollectionView */,
185 | 3C1768372025852A0031FEA9 /* Extension */,
186 | );
187 | path = Internal;
188 | sourceTree = "";
189 | };
190 | 3C1768432025A3DB0031FEA9 /* Util */ = {
191 | isa = PBXGroup;
192 | children = (
193 | 3C1768412025A33C0031FEA9 /* NavigationItemObserver.swift */,
194 | );
195 | path = Util;
196 | sourceTree = "";
197 | };
198 | 3C857EB6202962440082D945 /* TabCollectionView */ = {
199 | isa = PBXGroup;
200 | children = (
201 | 3C17683C202585920031FEA9 /* TabViewTabCollectionView.swift */,
202 | 3C857EB72029626A0082D945 /* TabViewTabCollectionViewLayout.swift */,
203 | );
204 | path = TabCollectionView;
205 | sourceTree = "";
206 | };
207 | /* End PBXGroup section */
208 |
209 | /* Begin PBXHeadersBuildPhase section */
210 | 3C17680620257F630031FEA9 /* Headers */ = {
211 | isa = PBXHeadersBuildPhase;
212 | buildActionMask = 2147483647;
213 | files = (
214 | 3C17680E20257F630031FEA9 /* TabView.h in Headers */,
215 | );
216 | runOnlyForDeploymentPostprocessing = 0;
217 | };
218 | /* End PBXHeadersBuildPhase section */
219 |
220 | /* Begin PBXNativeTarget section */
221 | 3C17680820257F630031FEA9 /* TabView */ = {
222 | isa = PBXNativeTarget;
223 | buildConfigurationList = 3C17681120257F630031FEA9 /* Build configuration list for PBXNativeTarget "TabView" */;
224 | buildPhases = (
225 | 3C17680420257F630031FEA9 /* Sources */,
226 | 3C17680520257F630031FEA9 /* Frameworks */,
227 | 3C17680620257F630031FEA9 /* Headers */,
228 | 3C17680720257F630031FEA9 /* Resources */,
229 | );
230 | buildRules = (
231 | );
232 | dependencies = (
233 | );
234 | name = TabView;
235 | productName = TabView;
236 | productReference = 3C17680920257F630031FEA9 /* TabView.framework */;
237 | productType = "com.apple.product-type.framework";
238 | };
239 | 3C17681820257F910031FEA9 /* TabView Sample */ = {
240 | isa = PBXNativeTarget;
241 | buildConfigurationList = 3C17682820257F910031FEA9 /* Build configuration list for PBXNativeTarget "TabView Sample" */;
242 | buildPhases = (
243 | 3C17681520257F910031FEA9 /* Sources */,
244 | 3C17681620257F910031FEA9 /* Frameworks */,
245 | 3C17681720257F910031FEA9 /* Resources */,
246 | 3C17682F20257FAC0031FEA9 /* Copy Frameworks */,
247 | );
248 | buildRules = (
249 | );
250 | dependencies = (
251 | 3C17682C20257FA50031FEA9 /* PBXTargetDependency */,
252 | );
253 | name = "TabView Sample";
254 | productName = Sample;
255 | productReference = 3C17681920257F910031FEA9 /* TabView Sample.app */;
256 | productType = "com.apple.product-type.application";
257 | };
258 | 3C905FB02025BEAF0084BA63 /* libTabView */ = {
259 | isa = PBXNativeTarget;
260 | buildConfigurationList = 3C905FB92025BEAF0084BA63 /* Build configuration list for PBXNativeTarget "libTabView" */;
261 | buildPhases = (
262 | 3C905FAD2025BEAF0084BA63 /* Sources */,
263 | 3C905FAE2025BEAF0084BA63 /* Frameworks */,
264 | 3C905FAF2025BEAF0084BA63 /* CopyFiles */,
265 | );
266 | buildRules = (
267 | );
268 | dependencies = (
269 | );
270 | name = libTabView;
271 | productName = libTabView;
272 | productReference = 3C905FB12025BEAF0084BA63 /* liblibTabView.a */;
273 | productType = "com.apple.product-type.library.static";
274 | };
275 | /* End PBXNativeTarget section */
276 |
277 | /* Begin PBXProject section */
278 | 3C17680020257F630031FEA9 /* Project object */ = {
279 | isa = PBXProject;
280 | attributes = {
281 | LastSwiftUpdateCheck = 0920;
282 | LastUpgradeCheck = 1000;
283 | ORGANIZATIONNAME = "Ian McDowell";
284 | TargetAttributes = {
285 | 3C17680820257F630031FEA9 = {
286 | CreatedOnToolsVersion = 9.2;
287 | LastSwiftMigration = 0920;
288 | ProvisioningStyle = Automatic;
289 | };
290 | 3C17681820257F910031FEA9 = {
291 | CreatedOnToolsVersion = 9.2;
292 | ProvisioningStyle = Automatic;
293 | };
294 | 3C905FB02025BEAF0084BA63 = {
295 | CreatedOnToolsVersion = 9.2;
296 | };
297 | };
298 | };
299 | buildConfigurationList = 3C17680320257F630031FEA9 /* Build configuration list for PBXProject "TabView" */;
300 | compatibilityVersion = "Xcode 8.0";
301 | developmentRegion = en;
302 | hasScannedForEncodings = 0;
303 | knownRegions = (
304 | en,
305 | Base,
306 | );
307 | mainGroup = 3C1767FF20257F630031FEA9;
308 | productRefGroup = 3C17680A20257F630031FEA9 /* Products */;
309 | projectDirPath = "";
310 | projectRoot = "";
311 | targets = (
312 | 3C17680820257F630031FEA9 /* TabView */,
313 | 3C905FB02025BEAF0084BA63 /* libTabView */,
314 | 3C17681820257F910031FEA9 /* TabView Sample */,
315 | );
316 | };
317 | /* End PBXProject section */
318 |
319 | /* Begin PBXResourcesBuildPhase section */
320 | 3C17680720257F630031FEA9 /* Resources */ = {
321 | isa = PBXResourcesBuildPhase;
322 | buildActionMask = 2147483647;
323 | files = (
324 | );
325 | runOnlyForDeploymentPostprocessing = 0;
326 | };
327 | 3C17681720257F910031FEA9 /* Resources */ = {
328 | isa = PBXResourcesBuildPhase;
329 | buildActionMask = 2147483647;
330 | files = (
331 | 3C17682620257F910031FEA9 /* LaunchScreen.storyboard in Resources */,
332 | 3C17682320257F910031FEA9 /* Assets.xcassets in Resources */,
333 | );
334 | runOnlyForDeploymentPostprocessing = 0;
335 | };
336 | /* End PBXResourcesBuildPhase section */
337 |
338 | /* Begin PBXSourcesBuildPhase section */
339 | 3C17680420257F630031FEA9 /* Sources */ = {
340 | isa = PBXSourcesBuildPhase;
341 | buildActionMask = 2147483647;
342 | files = (
343 | 3C17683F202585D80031FEA9 /* UIBarButtonItem+View.swift in Sources */,
344 | 3C176836202582F60031FEA9 /* Theme.swift in Sources */,
345 | 3C176834202581BE0031FEA9 /* NSLayoutConstraint+Custom.swift in Sources */,
346 | 3C17683D202585920031FEA9 /* TabViewTabCollectionView.swift in Sources */,
347 | 3C17683B2025855C0031FEA9 /* TabViewBar.swift in Sources */,
348 | 3C857EB82029626A0082D945 /* TabViewTabCollectionViewLayout.swift in Sources */,
349 | 3C1768422025A33C0031FEA9 /* NavigationItemObserver.swift in Sources */,
350 | 3C1768322025801E0031FEA9 /* TabViewController.swift in Sources */,
351 | 3CD4457A202ACFC500DD3CCB /* TabViewContainerViewController.swift in Sources */,
352 | );
353 | runOnlyForDeploymentPostprocessing = 0;
354 | };
355 | 3C17681520257F910031FEA9 /* Sources */ = {
356 | isa = PBXSourcesBuildPhase;
357 | buildActionMask = 2147483647;
358 | files = (
359 | 3C17681E20257F910031FEA9 /* ViewController.swift in Sources */,
360 | 3C17681C20257F910031FEA9 /* AppDelegate.swift in Sources */,
361 | );
362 | runOnlyForDeploymentPostprocessing = 0;
363 | };
364 | 3C905FAD2025BEAF0084BA63 /* Sources */ = {
365 | isa = PBXSourcesBuildPhase;
366 | buildActionMask = 2147483647;
367 | files = (
368 | 3CD4457B202ACFC700DD3CCB /* TabViewContainerViewController.swift in Sources */,
369 | 3C905FC12025BEBB0084BA63 /* TabViewController.swift in Sources */,
370 | 3C905FBD2025BEB90084BA63 /* TabViewTabCollectionView.swift in Sources */,
371 | 3C905FBA2025BEB90084BA63 /* NavigationItemObserver.swift in Sources */,
372 | 3C905FBF2025BEBB0084BA63 /* UIBarButtonItem+View.swift in Sources */,
373 | 3C905FBC2025BEB90084BA63 /* TabViewBar.swift in Sources */,
374 | 3C905FBE2025BEBB0084BA63 /* NSLayoutConstraint+Custom.swift in Sources */,
375 | 3C905FC02025BEBB0084BA63 /* Theme.swift in Sources */,
376 | );
377 | runOnlyForDeploymentPostprocessing = 0;
378 | };
379 | /* End PBXSourcesBuildPhase section */
380 |
381 | /* Begin PBXTargetDependency section */
382 | 3C17682C20257FA50031FEA9 /* PBXTargetDependency */ = {
383 | isa = PBXTargetDependency;
384 | target = 3C17680820257F630031FEA9 /* TabView */;
385 | targetProxy = 3C17682B20257FA50031FEA9 /* PBXContainerItemProxy */;
386 | };
387 | /* End PBXTargetDependency section */
388 |
389 | /* Begin PBXVariantGroup section */
390 | 3C17682420257F910031FEA9 /* LaunchScreen.storyboard */ = {
391 | isa = PBXVariantGroup;
392 | children = (
393 | 3C17682520257F910031FEA9 /* Base */,
394 | );
395 | name = LaunchScreen.storyboard;
396 | sourceTree = "";
397 | };
398 | /* End PBXVariantGroup section */
399 |
400 | /* Begin XCBuildConfiguration section */
401 | 3C17680F20257F630031FEA9 /* Debug */ = {
402 | isa = XCBuildConfiguration;
403 | buildSettings = {
404 | ALWAYS_SEARCH_USER_PATHS = NO;
405 | CLANG_ANALYZER_NONNULL = YES;
406 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
407 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
408 | CLANG_CXX_LIBRARY = "libc++";
409 | CLANG_ENABLE_MODULES = YES;
410 | CLANG_ENABLE_OBJC_ARC = YES;
411 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
412 | CLANG_WARN_BOOL_CONVERSION = YES;
413 | CLANG_WARN_COMMA = YES;
414 | CLANG_WARN_CONSTANT_CONVERSION = YES;
415 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
416 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
417 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
418 | CLANG_WARN_EMPTY_BODY = YES;
419 | CLANG_WARN_ENUM_CONVERSION = YES;
420 | CLANG_WARN_INFINITE_RECURSION = YES;
421 | CLANG_WARN_INT_CONVERSION = YES;
422 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
423 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
424 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
426 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
427 | CLANG_WARN_STRICT_PROTOTYPES = YES;
428 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
429 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
430 | CLANG_WARN_UNREACHABLE_CODE = YES;
431 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
432 | CODE_SIGN_IDENTITY = "";
433 | COPY_PHASE_STRIP = NO;
434 | CURRENT_PROJECT_VERSION = 1;
435 | DEBUG_INFORMATION_FORMAT = dwarf;
436 | ENABLE_STRICT_OBJC_MSGSEND = YES;
437 | ENABLE_TESTABILITY = YES;
438 | GCC_C_LANGUAGE_STANDARD = gnu11;
439 | GCC_DYNAMIC_NO_PIC = NO;
440 | GCC_NO_COMMON_BLOCKS = YES;
441 | GCC_OPTIMIZATION_LEVEL = 0;
442 | GCC_PREPROCESSOR_DEFINITIONS = (
443 | "DEBUG=1",
444 | "$(inherited)",
445 | );
446 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
447 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
448 | GCC_WARN_UNDECLARED_SELECTOR = YES;
449 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
450 | GCC_WARN_UNUSED_FUNCTION = YES;
451 | GCC_WARN_UNUSED_VARIABLE = YES;
452 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
453 | MTL_ENABLE_DEBUG_INFO = YES;
454 | ONLY_ACTIVE_ARCH = YES;
455 | SDKROOT = iphoneos;
456 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
457 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
458 | SWIFT_VERSION = 4.0;
459 | TARGETED_DEVICE_FAMILY = "1,2";
460 | VERSIONING_SYSTEM = "apple-generic";
461 | VERSION_INFO_PREFIX = "";
462 | };
463 | name = Debug;
464 | };
465 | 3C17681020257F630031FEA9 /* Release */ = {
466 | isa = XCBuildConfiguration;
467 | buildSettings = {
468 | ALWAYS_SEARCH_USER_PATHS = NO;
469 | CLANG_ANALYZER_NONNULL = YES;
470 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
471 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
472 | CLANG_CXX_LIBRARY = "libc++";
473 | CLANG_ENABLE_MODULES = YES;
474 | CLANG_ENABLE_OBJC_ARC = YES;
475 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
476 | CLANG_WARN_BOOL_CONVERSION = YES;
477 | CLANG_WARN_COMMA = YES;
478 | CLANG_WARN_CONSTANT_CONVERSION = YES;
479 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
480 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
481 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
482 | CLANG_WARN_EMPTY_BODY = YES;
483 | CLANG_WARN_ENUM_CONVERSION = YES;
484 | CLANG_WARN_INFINITE_RECURSION = YES;
485 | CLANG_WARN_INT_CONVERSION = YES;
486 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
487 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
488 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
489 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
490 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
491 | CLANG_WARN_STRICT_PROTOTYPES = YES;
492 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
493 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
494 | CLANG_WARN_UNREACHABLE_CODE = YES;
495 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
496 | CODE_SIGN_IDENTITY = "";
497 | COPY_PHASE_STRIP = NO;
498 | CURRENT_PROJECT_VERSION = 1;
499 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
500 | ENABLE_NS_ASSERTIONS = NO;
501 | ENABLE_STRICT_OBJC_MSGSEND = YES;
502 | GCC_C_LANGUAGE_STANDARD = gnu11;
503 | GCC_NO_COMMON_BLOCKS = YES;
504 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
505 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
506 | GCC_WARN_UNDECLARED_SELECTOR = YES;
507 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
508 | GCC_WARN_UNUSED_FUNCTION = YES;
509 | GCC_WARN_UNUSED_VARIABLE = YES;
510 | IPHONEOS_DEPLOYMENT_TARGET = 11.0;
511 | MTL_ENABLE_DEBUG_INFO = NO;
512 | SDKROOT = iphoneos;
513 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
514 | SWIFT_VERSION = 4.0;
515 | TARGETED_DEVICE_FAMILY = "1,2";
516 | VALIDATE_PRODUCT = YES;
517 | VERSIONING_SYSTEM = "apple-generic";
518 | VERSION_INFO_PREFIX = "";
519 | };
520 | name = Release;
521 | };
522 | 3C17681220257F630031FEA9 /* Debug */ = {
523 | isa = XCBuildConfiguration;
524 | buildSettings = {
525 | DEFINES_MODULE = YES;
526 | DYLIB_COMPATIBILITY_VERSION = 1;
527 | DYLIB_CURRENT_VERSION = 1;
528 | DYLIB_INSTALL_NAME_BASE = "@rpath";
529 | INFOPLIST_FILE = Framework/Info.plist;
530 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
531 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
532 | PRODUCT_BUNDLE_IDENTIFIER = net.ianmcdowell.TabView;
533 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
534 | SKIP_INSTALL = YES;
535 | };
536 | name = Debug;
537 | };
538 | 3C17681320257F630031FEA9 /* Release */ = {
539 | isa = XCBuildConfiguration;
540 | buildSettings = {
541 | DEFINES_MODULE = YES;
542 | DYLIB_COMPATIBILITY_VERSION = 1;
543 | DYLIB_CURRENT_VERSION = 1;
544 | DYLIB_INSTALL_NAME_BASE = "@rpath";
545 | INFOPLIST_FILE = Framework/Info.plist;
546 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
547 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
548 | PRODUCT_BUNDLE_IDENTIFIER = net.ianmcdowell.TabView;
549 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
550 | SKIP_INSTALL = YES;
551 | };
552 | name = Release;
553 | };
554 | 3C17682920257F910031FEA9 /* Debug */ = {
555 | isa = XCBuildConfiguration;
556 | buildSettings = {
557 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
558 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
559 | CODE_SIGN_IDENTITY = "iPhone Developer";
560 | DEVELOPMENT_TEAM = "";
561 | INFOPLIST_FILE = Sample/Info.plist;
562 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
563 | PRODUCT_BUNDLE_IDENTIFIER = "net.ianmcdowell.TabView-Sample";
564 | PRODUCT_NAME = "$(TARGET_NAME)";
565 | SWIFT_VERSION = 4.0;
566 | TARGETED_DEVICE_FAMILY = "1,2";
567 | };
568 | name = Debug;
569 | };
570 | 3C17682A20257F910031FEA9 /* Release */ = {
571 | isa = XCBuildConfiguration;
572 | buildSettings = {
573 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
574 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
575 | CODE_SIGN_IDENTITY = "iPhone Developer";
576 | DEVELOPMENT_TEAM = "";
577 | INFOPLIST_FILE = Sample/Info.plist;
578 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
579 | PRODUCT_BUNDLE_IDENTIFIER = "net.ianmcdowell.TabView-Sample";
580 | PRODUCT_NAME = "$(TARGET_NAME)";
581 | SWIFT_VERSION = 4.0;
582 | TARGETED_DEVICE_FAMILY = "1,2";
583 | };
584 | name = Release;
585 | };
586 | 3C905FB72025BEAF0084BA63 /* Debug */ = {
587 | isa = XCBuildConfiguration;
588 | buildSettings = {
589 | DEFINES_MODULE = YES;
590 | OTHER_LDFLAGS = "-ObjC";
591 | PRODUCT_NAME = "$(TARGET_NAME)";
592 | SKIP_INSTALL = YES;
593 | };
594 | name = Debug;
595 | };
596 | 3C905FB82025BEAF0084BA63 /* Release */ = {
597 | isa = XCBuildConfiguration;
598 | buildSettings = {
599 | DEFINES_MODULE = YES;
600 | OTHER_LDFLAGS = "-ObjC";
601 | PRODUCT_NAME = "$(TARGET_NAME)";
602 | SKIP_INSTALL = YES;
603 | };
604 | name = Release;
605 | };
606 | /* End XCBuildConfiguration section */
607 |
608 | /* Begin XCConfigurationList section */
609 | 3C17680320257F630031FEA9 /* Build configuration list for PBXProject "TabView" */ = {
610 | isa = XCConfigurationList;
611 | buildConfigurations = (
612 | 3C17680F20257F630031FEA9 /* Debug */,
613 | 3C17681020257F630031FEA9 /* Release */,
614 | );
615 | defaultConfigurationIsVisible = 0;
616 | defaultConfigurationName = Release;
617 | };
618 | 3C17681120257F630031FEA9 /* Build configuration list for PBXNativeTarget "TabView" */ = {
619 | isa = XCConfigurationList;
620 | buildConfigurations = (
621 | 3C17681220257F630031FEA9 /* Debug */,
622 | 3C17681320257F630031FEA9 /* Release */,
623 | );
624 | defaultConfigurationIsVisible = 0;
625 | defaultConfigurationName = Release;
626 | };
627 | 3C17682820257F910031FEA9 /* Build configuration list for PBXNativeTarget "TabView Sample" */ = {
628 | isa = XCConfigurationList;
629 | buildConfigurations = (
630 | 3C17682920257F910031FEA9 /* Debug */,
631 | 3C17682A20257F910031FEA9 /* Release */,
632 | );
633 | defaultConfigurationIsVisible = 0;
634 | defaultConfigurationName = Release;
635 | };
636 | 3C905FB92025BEAF0084BA63 /* Build configuration list for PBXNativeTarget "libTabView" */ = {
637 | isa = XCConfigurationList;
638 | buildConfigurations = (
639 | 3C905FB72025BEAF0084BA63 /* Debug */,
640 | 3C905FB82025BEAF0084BA63 /* Release */,
641 | );
642 | defaultConfigurationIsVisible = 0;
643 | defaultConfigurationName = Release;
644 | };
645 | /* End XCConfigurationList section */
646 | };
647 | rootObject = 3C17680020257F630031FEA9 /* Project object */;
648 | }
649 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
78 |
80 |
86 |
87 |
88 |
89 |
91 |
92 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/xcshareddata/xcschemes/TabView.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/xcshareddata/xcschemes/libTabView.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/TabView.xcodeproj/xcuserdata/ian.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Sample.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 2
11 |
12 | TabView.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 | libTabView.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 1
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | 3C17681820257F910031FEA9
26 |
27 | primary
28 |
29 |
30 | 3C905FB02025BEAF0084BA63
31 |
32 | primary
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/build.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | name
6 | TabView
7 | build
8 |
9 | buildSystem
10 | xcode
11 | buildArgs
12 |
13 | -project
14 | TabView.xcodeproj
15 | -target
16 | libTabView
17 |
18 | outputs
19 |
20 | libTabView.a
21 |
22 |
23 | dependencies
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------