├── .gitignore ├── .travis.yml ├── Framework ├── Info.plist └── TabView.h ├── LICENSE ├── README.md ├── Resources └── Screenshot.png ├── Sample ├── AppDelegate.swift ├── Assets.xcassets │ ├── Add.imageset │ │ ├── Add.pdf │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist └── ViewController.swift ├── Sources ├── Internal │ ├── Extension │ │ ├── NSLayoutConstraint+Custom.swift │ │ └── UIBarButtonItem+View.swift │ ├── TabCollectionView │ │ ├── TabViewTabCollectionView.swift │ │ └── TabViewTabCollectionViewLayout.swift │ ├── TabViewBar.swift │ └── Util │ │ └── NavigationItemObserver.swift ├── TabViewContainerViewController.swift ├── TabViewController.swift └── Theme.swift ├── TabView.podspec ├── TabView.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── Sample.xcscheme │ │ ├── TabView.xcscheme │ │ └── libTabView.xcscheme └── xcuserdata │ └── ian.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── build.plist /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.xcworkspacedata 3 | *.xcuserdatad 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode9.2 3 | env: 4 | global: 5 | - LC_CTYPE=en_US.UTF-8 6 | - LANG=en_US.UTF-8 7 | - PROJECT=TabView.xcodeproj 8 | - IOS_FRAMEWORK_SCHEME="TabView" 9 | - IOS_SIM_SDK=iphonesimulator11.2 10 | - IOS_DEV_SDK=iphoneos11.2 11 | matrix: 12 | - SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_SIM_SDK" 13 | - SCHEME="$IOS_FRAMEWORK_SCHEME" SDK="$IOS_DEV_SDK" 14 | script: 15 | - set -o pipefail 16 | - xcodebuild -version 17 | - xcodebuild -showsdks 18 | - xcodebuild -project "$PROJECT" -scheme "$SCHEME" -sdk "$SDK" -configuration Release ONLY_ACTIVE_ARCH=NO ENABLE_TESTABILITY=YES build; 19 | 20 | branches: 21 | only: 22 | - master 23 | -------------------------------------------------------------------------------- /Framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Framework/TabView.h: -------------------------------------------------------------------------------- 1 | // 2 | // TabView.h 3 | // TabView 4 | // 5 | // Created by Ian McDowell on 2/2/18. 6 | // Copyright © 2018 Ian McDowell. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TabView. 12 | FOUNDATION_EXPORT double TabViewVersionNumber; 13 | 14 | //! Project version string for TabView. 15 | FOUNDATION_EXPORT const unsigned char TabViewVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 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 | [![Build Status](http://img.shields.io/travis/IMcD23/TabView.svg)](https://travis-ci.org/IMcD23/TabView) 15 | [![Version](https://img.shields.io/github/release/IMcD23/TabView.svg)](https://github.com/IMcD23/TabView/releases/latest) 16 | ![Package Managers](https://img.shields.io/badge/supports-Carthage-orange.svg) 17 | [![Contact](https://img.shields.io/badge/contact-%40ian__mcdowell-3a8fc1.svg)](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 | --------------------------------------------------------------------------------