├── .gitignore
├── Example
├── Example
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ └── ContentView.swift
└── Example.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── Tests
├── LinuxMain.swift
└── StatefulTabViewTests
│ ├── XCTestManifests.swift
│ └── StatefulTabViewTests.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Sources
└── StatefulTabView
│ ├── Modifiers
│ ├── Tab+Modifiers.swift
│ └── StatefulTabView+Modifiers.swift
│ ├── StatefulTabView.swift
│ └── Helpers
│ ├── Tab.swift
│ ├── TabBarController.swift
│ └── TabBarCoordinator.swift
├── LICENSE
├── Package.swift
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import StatefulTabViewTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += StatefulTabViewTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/StatefulTabViewTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(StatefulTabViewTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/StatefulTabView/Modifiers/Tab+Modifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tab+Modifiers.swift
3 | // Example
4 | //
5 | // Created by Nicholas Bellucci on 5/13/20.
6 | // Copyright © 2020 Nicholas Bellucci. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public extension Tab {
12 | func prefersLargeTitle(_ bool: Bool) -> Tab {
13 | var copy = self
14 | copy.prefersLargeTitle = bool
15 | return copy
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/StatefulTabViewTests/StatefulTabViewTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import SwiftUI
3 | @testable import StatefulTabView
4 |
5 | final class StatefulTabViewTests: XCTestCase {
6 | func testExample() {
7 | // This is an example of a functional test case.
8 | // Use XCTAssert and related functions to verify your tests produce the correct
9 | // results.
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/StatefulTabView/Modifiers/StatefulTabView+Modifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatefulTabView+Modifiers.swift
3 | //
4 | //
5 | // Created by Nicholas Bellucci on 5/10/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public extension StatefulTabView {
11 | func barTintColor(_ color: UIColor) -> StatefulTabView {
12 | var copy = self
13 | copy.barTintColor = color
14 | return copy
15 | }
16 |
17 | func unselectedItemTintColor(_ color: UIColor) -> StatefulTabView {
18 | var copy = self
19 | copy.unselectedItemTintColor = color
20 | return copy
21 | }
22 |
23 | func barBackgroundColor(_ color: UIColor) -> StatefulTabView {
24 | var copy = self
25 | copy.backgroundColor = color
26 | return copy
27 | }
28 |
29 | func barAppearanceConfiguration(_ configuration: TabBarBackgroundConfiguration) -> StatefulTabView {
30 | var copy = self
31 | copy.tabBarConfiguration = configuration
32 | return copy
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Bluecheese LLC
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "StatefulTabView",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
13 | .library(
14 | name: "StatefulTabView",
15 | targets: ["StatefulTabView"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
24 | .target(
25 | name: "StatefulTabView",
26 | dependencies: []),
27 | .testTarget(
28 | name: "StatefulTabViewTests",
29 | dependencies: ["StatefulTabView"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/Example/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by Nicholas Bellucci on 5/10/20.
6 | // Copyright © 2020 Nicholas Bellucci. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/Example/Example/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 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Example/Example/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 | UIInterfaceOrientationLandscapeLeft
50 | UIInterfaceOrientationLandscapeRight
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Sources/StatefulTabView/StatefulTabView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StatefulTabView.swift
3 | //
4 | //
5 | // Created by Nicholas Bellucci on 5/10/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct StatefulTabView: View {
11 | internal var viewControllers: [UIHostingController] = []
12 | internal var tabBarItems: [Tab] = []
13 |
14 | internal var barTintColor: UIColor? = nil
15 | internal var unselectedItemTintColor: UIColor? = nil
16 | internal var backgroundColor: UIColor? = nil
17 | internal var tabBarConfiguration: TabBarBackgroundConfiguration? = nil
18 |
19 | @State private var stateIndex: Int = 0
20 | @Binding private var bindableIndex: Int
21 |
22 | private var useBindableIndex: Bool = false
23 |
24 | public init(selectedIndex: Binding? = nil, @TabBuilder _ content: () -> [Tab]) {
25 | if let selectedIndex = selectedIndex {
26 | _bindableIndex = selectedIndex
27 | useBindableIndex = true
28 | } else {
29 | _bindableIndex = .constant(0)
30 | useBindableIndex = false
31 | }
32 |
33 | configureViewControllers(with: content())
34 | }
35 |
36 | public var body: some View {
37 | TabBarController(controllers: viewControllers,
38 | tabBarItems: tabBarItems,
39 | barTintColor: barTintColor,
40 | unselectedItemTintColor: unselectedItemTintColor,
41 | backgroundColor: backgroundColor,
42 | tabBarConfiguration: tabBarConfiguration,
43 | selectedIndex: useBindableIndex ? $bindableIndex : $stateIndex)
44 | .edgesIgnoringSafeArea(.all)
45 | }
46 | }
47 |
48 | private extension StatefulTabView {
49 | mutating func configureViewControllers(with tabs: [Tab]) {
50 | tabs.forEach {
51 | let tabController = UIHostingController(rootView: $0.view)
52 | tabController.tabBarItem = $0.barItem
53 | tabBarItems.append($0)
54 | viewControllers.append(tabController)
55 | }
56 | }
57 | }
58 |
59 | @resultBuilder
60 | public struct TabBuilder {
61 | public static func buildBlock(_ children: Tab...) -> [Tab] {
62 | children
63 | }
64 |
65 | public static func buildBlock(_ component: Tab) -> [Tab] {
66 | [component]
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/StatefulTabView/Helpers/Tab.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tab.swift
3 | //
4 | //
5 | // Created by Nicholas Bellucci on 5/8/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public struct Tab {
11 | var view: AnyView
12 | var barItem: UITabBarItem? = nil
13 |
14 | internal var prefersLargeTitle: Bool = false
15 |
16 | let badgeValue: String?
17 |
18 | // MARK: Asset Image Names
19 | public init(title: String? = nil,
20 | imageName: String,
21 | selectedImageName: String? = nil,
22 | badgeValue: String? = nil,
23 | @ViewBuilder content: @escaping () -> T) where T: View {
24 |
25 | self.badgeValue = badgeValue
26 |
27 | var selectedImage: UIImage?
28 | if let selectedImageName = selectedImageName {
29 | selectedImage = UIImage(named: selectedImageName)
30 | }
31 |
32 | barItem = UITabBarItem(title: title, image: UIImage(named: imageName), selectedImage: selectedImage)
33 |
34 | self.view = AnyView(content())
35 | }
36 |
37 | // MARK: System Image Names
38 | public init(title: String? = nil,
39 | systemImageName: String,
40 | selectedSystemImageName: String? = nil,
41 | badgeValue: String? = nil,
42 | @ViewBuilder content: @escaping () -> T) where T: View {
43 |
44 | self.badgeValue = badgeValue
45 |
46 | var selectedImage: UIImage?
47 |
48 | if let selectedSystemImageName = selectedSystemImageName {
49 | selectedImage = UIImage(systemName: selectedSystemImageName)
50 | }
51 |
52 | barItem = UITabBarItem(title: title, image: UIImage(systemName: systemImageName), selectedImage: selectedImage)
53 |
54 | self.view = AnyView(content())
55 | }
56 |
57 | // MARK: UIImages
58 | public init(title: String? = nil,
59 | image: UIImage?,
60 | selectedImage: UIImage? = nil,
61 | badgeValue: String? = nil,
62 | @ViewBuilder content: @escaping () -> T) where T: View {
63 |
64 | self.badgeValue = badgeValue
65 |
66 | barItem = UITabBarItem(title: title, image: image, selectedImage: selectedImage)
67 |
68 | self.view = AnyView(content())
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Example/Example/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Example
4 | //
5 | // Created by Nicholas Bellucci on 5/10/20.
6 | // Copyright © 2020 Nicholas Bellucci. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
21 |
22 | // Create the SwiftUI view that provides the window contents.
23 | let contentView = ContentView()
24 |
25 | // Use a UIHostingController as window root view controller.
26 | if let windowScene = scene as? UIWindowScene {
27 | let window = UIWindow(windowScene: windowScene)
28 | window.rootViewController = UIHostingController(rootView: contentView)
29 | self.window = window
30 | window.makeKeyAndVisible()
31 | }
32 | }
33 |
34 | func sceneDidDisconnect(_ scene: UIScene) {
35 | // Called as the scene is being released by the system.
36 | // This occurs shortly after the scene enters the background, or when its session is discarded.
37 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
39 | }
40 |
41 | func sceneDidBecomeActive(_ scene: UIScene) {
42 | // Called when the scene has moved from an inactive state to an active state.
43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
44 | }
45 |
46 | func sceneWillResignActive(_ scene: UIScene) {
47 | // Called when the scene will move from an active state to an inactive state.
48 | // This may occur due to temporary interruptions (ex. an incoming phone call).
49 | }
50 |
51 | func sceneWillEnterForeground(_ scene: UIScene) {
52 | // Called as the scene transitions from the background to the foreground.
53 | // Use this method to undo the changes made on entering the background.
54 | }
55 |
56 | func sceneDidEnterBackground(_ scene: UIScene) {
57 | // Called as the scene transitions from the foreground to the background.
58 | // Use this method to save data, release shared resources, and store enough scene-specific state information
59 | // to restore the scene back to its current state.
60 | }
61 |
62 |
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/Example/Example/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Example
4 | //
5 | // Created by Nicholas Bellucci on 5/10/20.
6 | // Copyright © 2020 Nicholas Bellucci. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContentView: View {
12 | @State var selectedIndex: Int = 1
13 | @State var badgeValue: String? = "1"
14 |
15 | var body: some View {
16 | StatefulTabView(selectedIndex: $selectedIndex) {
17 | Tab(title: "Tab 1", systemImageName: "circle.fill", badgeValue: badgeValue) {
18 | NavigationView {
19 | List {
20 | Section {
21 | ForEach(0..<20, id: \.self) { index in
22 | NavigationLink(destination: PushedView(text: "Pushed number \(index)")) {
23 | Text("\(index)")
24 | }
25 | }
26 | }
27 | }
28 | .navigationBarTitle("Navigation View 1", displayMode: .inline)
29 | }
30 | }
31 |
32 | Tab(title: "Tab 2", systemImageName: "square.fill") {
33 | NavigationView {
34 | List {
35 | Section {
36 | ForEach(0..<20, id: \.self) { index in
37 | NavigationLink(destination: PushedView(text: "Pushed number \(index)")) {
38 | Text("\(index)")
39 | }
40 | }
41 | }
42 | }
43 | .navigationBarTitle("Navigation View 2")
44 | }
45 | }
46 | .prefersLargeTitle(true)
47 |
48 | Tab(title: "Tab 3", image: UIImage(systemName: "triangle.fill")) {
49 | NavigationView{
50 | List {
51 | Section {
52 | ForEach(0..<20, id: \.self) { index in
53 | NavigationLink(destination: PushedView(text: "Pushed number \(index)")) {
54 | Text("\(index)")
55 | }
56 | }
57 | }
58 | }
59 | .navigationBarTitle("Navigation View 3")
60 | }
61 | }
62 | .prefersLargeTitle(true)
63 |
64 | Tab(title: "Tab 4", systemImageName: "shield", selectedSystemImageName: "shield.fill") {
65 | List {
66 | Section {
67 | ForEach(0..<20, id: \.self) { index in
68 | NavigationLink(destination: PushedView(text: "Pushed number \(index)")) {
69 | Text("\(index)")
70 | }
71 | }
72 | }
73 | }
74 | }
75 | }
76 | .barTintColor(.red)
77 | .unselectedItemTintColor(.green)
78 | .barBackgroundColor(.yellow)
79 | .barAppearanceConfiguration(.transparent)
80 | }
81 | }
82 |
83 | struct PushedView: View {
84 | var text: String
85 |
86 | var body: some View {
87 | Text(text)
88 | }
89 | }
90 |
91 | struct ContentView_Previews: PreviewProvider {
92 | static var previews: some View {
93 | ContentView()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/Sources/StatefulTabView/Helpers/TabBarController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarController.swift
3 | //
4 | //
5 | // Created by Nicholas Bellucci on 5/8/20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public enum TabBarBackgroundConfiguration {
11 | case `default`
12 | case opaque
13 | case transparent
14 | }
15 |
16 | struct TabBarController: UIViewControllerRepresentable {
17 | var controllers: [UIViewController]
18 | var tabBarItems: [Tab]
19 |
20 | var barTintColor: UIColor?
21 | var unselectedItemTintColor: UIColor?
22 | var backgroundColor: UIColor?
23 | var tabBarConfiguration: TabBarBackgroundConfiguration?
24 |
25 | @Binding var selectedIndex: Int
26 |
27 | func makeUIViewController(context: Context) -> UITabBarController {
28 | let tabBarController = UITabBarController()
29 | tabBarController.viewControllers = controllers
30 | tabBarController.delegate = context.coordinator
31 | tabBarController.selectedIndex = selectedIndex
32 |
33 | configure(tabBarController.tabBar)
34 | return tabBarController
35 | }
36 |
37 | func updateUIViewController(_ tabBarController: UITabBarController, context: Context) {
38 | tabBarController.selectedIndex = selectedIndex
39 |
40 | tabBarItems.forEach { tab in
41 | guard let index = tabBarItems.firstIndex(where: { $0.barItem == tab.barItem }), let controllers = tabBarController.viewControllers else { return }
42 |
43 | if controllers.indices.contains(index) {
44 | controllers[index].tabBarItem.badgeValue = tab.badgeValue
45 | }
46 | }
47 | }
48 |
49 | func makeCoordinator() -> TabBarCoordinator {
50 | TabBarCoordinator(self)
51 | }
52 | }
53 |
54 | private extension TabBarController {
55 | func configure(_ tabBar: UITabBar) {
56 | let appearance = tabBar.standardAppearance.copy()
57 |
58 | if let config = tabBarConfiguration {
59 | switch config {
60 | case .default:
61 | appearance.configureWithDefaultBackground()
62 | case .opaque:
63 | appearance.configureWithOpaqueBackground()
64 | case .transparent:
65 | appearance.configureWithTransparentBackground()
66 | }
67 | }
68 |
69 | if let barTintColor = barTintColor {
70 | tabBar.tintColor = barTintColor
71 | }
72 |
73 | if let unselectedItemTintColor = unselectedItemTintColor {
74 | if #available(iOS 13.0, *) {
75 | appearance.stackedLayoutAppearance.normal.titleTextAttributes = [NSAttributedString.Key.foregroundColor: unselectedItemTintColor]
76 | appearance.stackedLayoutAppearance.normal.iconColor = unselectedItemTintColor
77 | } else {
78 | tabBar.unselectedItemTintColor = unselectedItemTintColor
79 | }
80 | }
81 |
82 | if let backgroundColor = backgroundColor {
83 | tabBar.backgroundColor = backgroundColor
84 | }
85 |
86 | tabBar.standardAppearance = appearance
87 | }
88 |
89 | func navigationController(in viewController: UIViewController) -> UINavigationController? {
90 | var controller: UINavigationController?
91 |
92 | if let navigationController = viewController as? UINavigationController {
93 | return navigationController
94 | }
95 |
96 | viewController.children.forEach {
97 | if let navigationController = $0 as? UINavigationController {
98 | controller = navigationController
99 | } else {
100 | controller = navigationController(in: $0)
101 | }
102 | }
103 |
104 | return controller
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/StatefulTabView/Helpers/TabBarCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TabBarCoordinator.swift
3 | // Example
4 | //
5 | // Created by Nicholas Bellucci on 5/13/20.
6 | // Copyright © 2020 Nicholas Bellucci. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | class TabBarCoordinator: NSObject, UITabBarControllerDelegate {
12 | private enum Constants {
13 | static let boundsContainsPoint = CGPoint(x: 0, y: -89)
14 | static let largeTitleRect = CGRect(x: 0, y: -52, width: 1, height: 1)
15 | static let inlineTitleRect = CGRect(x: 0, y: 0, width: 1, height: 1)
16 | }
17 |
18 | var parent: TabBarController
19 |
20 | init(_ tabBarController: TabBarController) {
21 | self.parent = tabBarController
22 | }
23 |
24 | func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
25 | if parent.selectedIndex == tabBarController.selectedIndex {
26 | guard let navigationController = navigationController(in: viewController) else {
27 | scrollToTop(in: viewController)
28 | return
29 | }
30 |
31 | if navigationController.visibleViewController == navigationController.viewControllers.first {
32 | scrollToTop(in: navigationController, selectedIndex: tabBarController.selectedIndex)
33 | } else {
34 | navigationController.popToRootViewController(animated: true)
35 | }
36 | }
37 |
38 | parent.selectedIndex = tabBarController.selectedIndex
39 | }
40 | }
41 |
42 | private extension TabBarCoordinator {
43 | func scrollToTop(in navigationController: UINavigationController, selectedIndex: Int) {
44 | let views = navigationController.viewControllers
45 | .map { $0.view.subviews }
46 | .reduce([], +)
47 |
48 | if let scrollView = scrollView(in: views) {
49 | if parent.tabBarItems[selectedIndex].prefersLargeTitle {
50 | if !scrollView.bounds.contains(Constants.boundsContainsPoint) {
51 | scrollView.scrollRectToVisible(Constants.largeTitleRect, animated: true)
52 | }
53 | } else {
54 | scrollView.scrollRectToVisible(Constants.inlineTitleRect, animated: true)
55 | }
56 | }
57 | }
58 |
59 | func scrollToTop(in viewController: UIViewController) {
60 | let views = viewController.view.subviews
61 |
62 | if let scrollView = scrollView(in: views) {
63 | scrollView.scrollRectToVisible(Constants.inlineTitleRect, animated: true)
64 | }
65 | }
66 | }
67 |
68 | private extension TabBarCoordinator {
69 | func navigationController(in viewController: UIViewController) -> UINavigationController? {
70 | var controller: UINavigationController?
71 |
72 | if let navigationController = viewController as? UINavigationController {
73 | return navigationController
74 | }
75 |
76 | viewController.children.forEach {
77 | if let navigationController = $0 as? UINavigationController {
78 | controller = navigationController
79 | } else {
80 | controller = navigationController(in: $0)
81 | }
82 | }
83 |
84 | return controller
85 | }
86 |
87 | func scrollView(in views: [UIView]) -> UIScrollView? {
88 | var view: UIScrollView?
89 |
90 | views.forEach {
91 | guard view == nil else {
92 | return
93 | }
94 |
95 | if let scrollView = $0 as? UIScrollView {
96 | view = scrollView
97 | } else {
98 | view = scrollView(in: $0.subviews)
99 | }
100 | }
101 |
102 | return view
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # StatefulTabView
2 |
3 | A SwiftUI UITabBarController implementation that retains state between tab changes. Big thanks to [Amzd](https://gist.github.com/Amzd) and everyone who helped to refine this [gist](https://gist.github.com/Amzd/2eb5b941865e8c5cccf149e6e07c8810) as it was a major jumping off point for setting up this project.
4 |
5 | #### Requirements
6 | - iOS 13.0+
7 | - Xcode 11.2+
8 | - Swift 5+
9 |
10 | ## Installation
11 |
12 | ### Swift Package Manager
13 | In Xcode 11 or greater, navigate to `File > Swift Packages > Add Package Dependency...`. From there just simply add `https://github.com/NicholasBellucci/StatefulTabView` as the package repository url and use the master branch or the most recent version. Master will always be inline with the newest release.
14 |
15 | ## Table of Contents
16 | * [Features](#features)
17 | * [Usage](#usage)
18 | * [Basic](#basic)
19 | * [Appearance Modifications](#appearance-modifications)
20 | * [Selected Index](#selected-index)
21 | * [Badge Value](#badge-value)
22 | * [Scroll to Top with Large Titles](#scroll-to-top-with-large-titles)
23 | * [License](#license)
24 |
25 | ## Features
26 | - [x] State driven selected index
27 | - [x] TabBar appearance configuration
28 | - [x] TabBar custom tint color
29 | - [x] TabBar custom background color
30 | - [x] TabBarItem custom title and image
31 | - [x] TabBarItem badge value
32 | - [x] State retention from tab to tab
33 | - [x] Pop to root functionality when selecting the already selected tab
34 | - [x] Scroll to top functionality when selecting the already selected tab at the root view
35 |
36 | ## Usage
37 |
38 | Setting up StatefulTabView is relatively simple and works similar to the native TabView. The main difference is that the content of the tabs is wrapped in a Tab struct. There is no limitation on how many tabs can be used. Once more than 5 are used, the Apple standard more tab will become available. Feel free to check out the example project for the exact usage.
39 |
40 | ### Basic
41 | ```Swift
42 | StatefulTabView {
43 | Tab(title: "Tab 1", systemImageName: "circle.fill") {
44 | NavigationView {
45 | List {
46 | Section {
47 | ForEach(0..<20, id: \.self) { index in
48 | NavigationLink(destination: PushedView(text: "Pushed number \(index)")) {
49 | Text("\(index)")
50 | }
51 | }
52 | }
53 | }
54 | .navigationBarTitle("Navigation View 1")
55 | }
56 | }
57 | }
58 | ```
59 |
60 | ### Appearance Modifications
61 |
62 | All appearance modifications can be made by using extensions for the StatefulTabView.
63 |
64 | ```Swift
65 | StatefulTabView {
66 | ...
67 | }
68 | .barTintColor(.red)
69 | .unselectedItemTintColor(.green)
70 | .barBackgroundColor(.yellow)
71 | .barAppearanceConfiguration(.transparent)
72 | ```
73 |
74 | ### Selected Index
75 |
76 | The selected index of the StatefulTabView can be set within the initializer. The passed value is a binding.
77 |
78 | ```Swift
79 | @State var selectedIndex: Int = 2
80 |
81 | StatefulTabView(selectedIndex: $selectedIndex) {
82 | ...
83 | }
84 | ```
85 |
86 | ### Badge Value
87 |
88 | The TabBarItem badge value can be set in the initializer of a Tab.
89 |
90 | ```Swift
91 | @State var badgeValue: String = "1"
92 |
93 | Tab(title: "Tab 1", systemImageName: "circle.fill", badgeValue: badgeValue) {
94 | ...
95 | }
96 | ```
97 |
98 | ### Scroll to Top with Large Titles
99 |
100 | Scroll to top is handled when selecting the already selected tab that contains a scrollView in the heirarchy. The only issue is that large titles in navigation bars are not factored in when calling `scrollRectToVisible(CGRect(x: 0, y: 0, width: 1, height: 1), animated: true)` for obvious reasons. Due to this limitation adding `.prefersLargeTitle(true)` to a `Tab` will fix this issue. For root navigation views that do not use a large title no change to a `Tab` is needed.
101 |
102 | ```Swift
103 | Tab(title: "Tab 1", systemImageName: "circle.fill") {
104 | NavigationView {
105 | List {
106 | ...
107 | }
108 | .navigationBarTitle("Navigation View 1", displayMode: .large)
109 | }
110 | }
111 | .prefersLargeTitle(true)
112 | ```
113 |
114 | ## License
115 |
116 | StatefulTabView is, and always will be, MIT licensed. See [LICENSE](LICENSE) for details.
117 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 4C1EFD07246CA0D3007F1C61 /* TabBarCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1EFD06246CA0D3007F1C61 /* TabBarCoordinator.swift */; };
11 | 4C1EFD09246CA684007F1C61 /* Tab+Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1EFD08246CA684007F1C61 /* Tab+Modifiers.swift */; };
12 | 4C6A944C2468D15000D5C6C7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6A944B2468D15000D5C6C7 /* AppDelegate.swift */; };
13 | 4C6A944E2468D15000D5C6C7 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6A944D2468D15000D5C6C7 /* SceneDelegate.swift */; };
14 | 4C6A94502468D15000D5C6C7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6A944F2468D15000D5C6C7 /* ContentView.swift */; };
15 | 4C6A94522468D15200D5C6C7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C6A94512468D15200D5C6C7 /* Assets.xcassets */; };
16 | 4C6A94552468D15200D5C6C7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C6A94542468D15200D5C6C7 /* Preview Assets.xcassets */; };
17 | 4C6A94582468D15200D5C6C7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4C6A94562468D15200D5C6C7 /* LaunchScreen.storyboard */; };
18 | 4C9E33202468E224007F2C0D /* StatefulTabView+Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9E331A2468E224007F2C0D /* StatefulTabView+Modifiers.swift */; };
19 | 4C9E33212468E224007F2C0D /* StatefulTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9E331B2468E224007F2C0D /* StatefulTabView.swift */; };
20 | 4C9E33222468E224007F2C0D /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9E331D2468E224007F2C0D /* Tab.swift */; };
21 | 4C9E33232468E224007F2C0D /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9E331E2468E224007F2C0D /* TabBarController.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXContainerItemProxy section */
25 | 4C6A945F2468D15200D5C6C7 /* PBXContainerItemProxy */ = {
26 | isa = PBXContainerItemProxy;
27 | containerPortal = 4C6A94402468D15000D5C6C7 /* Project object */;
28 | proxyType = 1;
29 | remoteGlobalIDString = 4C6A94472468D15000D5C6C7;
30 | remoteInfo = Example;
31 | };
32 | 4C6A946A2468D15200D5C6C7 /* PBXContainerItemProxy */ = {
33 | isa = PBXContainerItemProxy;
34 | containerPortal = 4C6A94402468D15000D5C6C7 /* Project object */;
35 | proxyType = 1;
36 | remoteGlobalIDString = 4C6A94472468D15000D5C6C7;
37 | remoteInfo = Example;
38 | };
39 | /* End PBXContainerItemProxy section */
40 |
41 | /* Begin PBXFileReference section */
42 | 4C1EFD06246CA0D3007F1C61 /* TabBarCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarCoordinator.swift; sourceTree = ""; };
43 | 4C1EFD08246CA684007F1C61 /* Tab+Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+Modifiers.swift"; sourceTree = ""; };
44 | 4C6A94482468D15000D5C6C7 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
45 | 4C6A944B2468D15000D5C6C7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
46 | 4C6A944D2468D15000D5C6C7 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
47 | 4C6A944F2468D15000D5C6C7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
48 | 4C6A94512468D15200D5C6C7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
49 | 4C6A94542468D15200D5C6C7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
50 | 4C6A94572468D15200D5C6C7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
51 | 4C6A94592468D15200D5C6C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
52 | 4C6A945E2468D15200D5C6C7 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
53 | 4C6A94692468D15200D5C6C7 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
54 | 4C9E331A2468E224007F2C0D /* StatefulTabView+Modifiers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StatefulTabView+Modifiers.swift"; sourceTree = ""; };
55 | 4C9E331B2468E224007F2C0D /* StatefulTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatefulTabView.swift; sourceTree = ""; };
56 | 4C9E331D2468E224007F2C0D /* Tab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; };
57 | 4C9E331E2468E224007F2C0D /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; };
58 | /* End PBXFileReference section */
59 |
60 | /* Begin PBXFrameworksBuildPhase section */
61 | 4C6A94452468D15000D5C6C7 /* Frameworks */ = {
62 | isa = PBXFrameworksBuildPhase;
63 | buildActionMask = 2147483647;
64 | files = (
65 | );
66 | runOnlyForDeploymentPostprocessing = 0;
67 | };
68 | 4C6A945B2468D15200D5C6C7 /* Frameworks */ = {
69 | isa = PBXFrameworksBuildPhase;
70 | buildActionMask = 2147483647;
71 | files = (
72 | );
73 | runOnlyForDeploymentPostprocessing = 0;
74 | };
75 | 4C6A94662468D15200D5C6C7 /* Frameworks */ = {
76 | isa = PBXFrameworksBuildPhase;
77 | buildActionMask = 2147483647;
78 | files = (
79 | );
80 | runOnlyForDeploymentPostprocessing = 0;
81 | };
82 | /* End PBXFrameworksBuildPhase section */
83 |
84 | /* Begin PBXGroup section */
85 | 4C6A943F2468D15000D5C6C7 = {
86 | isa = PBXGroup;
87 | children = (
88 | 4C6A944A2468D15000D5C6C7 /* Example */,
89 | 4C6A94492468D15000D5C6C7 /* Products */,
90 | );
91 | sourceTree = "";
92 | };
93 | 4C6A94492468D15000D5C6C7 /* Products */ = {
94 | isa = PBXGroup;
95 | children = (
96 | 4C6A94482468D15000D5C6C7 /* Example.app */,
97 | 4C6A945E2468D15200D5C6C7 /* ExampleTests.xctest */,
98 | 4C6A94692468D15200D5C6C7 /* ExampleUITests.xctest */,
99 | );
100 | name = Products;
101 | sourceTree = "";
102 | };
103 | 4C6A944A2468D15000D5C6C7 /* Example */ = {
104 | isa = PBXGroup;
105 | children = (
106 | 4C6A944B2468D15000D5C6C7 /* AppDelegate.swift */,
107 | 4C6A944D2468D15000D5C6C7 /* SceneDelegate.swift */,
108 | 4C6A944F2468D15000D5C6C7 /* ContentView.swift */,
109 | 4C9E33172468E224007F2C0D /* StatefulTabView */,
110 | 4C6A94512468D15200D5C6C7 /* Assets.xcassets */,
111 | 4C6A94562468D15200D5C6C7 /* LaunchScreen.storyboard */,
112 | 4C6A94592468D15200D5C6C7 /* Info.plist */,
113 | 4C6A94532468D15200D5C6C7 /* Preview Content */,
114 | );
115 | path = Example;
116 | sourceTree = "";
117 | };
118 | 4C6A94532468D15200D5C6C7 /* Preview Content */ = {
119 | isa = PBXGroup;
120 | children = (
121 | 4C6A94542468D15200D5C6C7 /* Preview Assets.xcassets */,
122 | );
123 | path = "Preview Content";
124 | sourceTree = "";
125 | };
126 | 4C9E33172468E224007F2C0D /* StatefulTabView */ = {
127 | isa = PBXGroup;
128 | children = (
129 | 4C9E331B2468E224007F2C0D /* StatefulTabView.swift */,
130 | 4C9E33182468E224007F2C0D /* Modifiers */,
131 | 4C9E331C2468E224007F2C0D /* Helpers */,
132 | );
133 | name = StatefulTabView;
134 | path = ../../Sources/StatefulTabView;
135 | sourceTree = "";
136 | };
137 | 4C9E33182468E224007F2C0D /* Modifiers */ = {
138 | isa = PBXGroup;
139 | children = (
140 | 4C9E331A2468E224007F2C0D /* StatefulTabView+Modifiers.swift */,
141 | 4C1EFD08246CA684007F1C61 /* Tab+Modifiers.swift */,
142 | );
143 | path = Modifiers;
144 | sourceTree = "";
145 | };
146 | 4C9E331C2468E224007F2C0D /* Helpers */ = {
147 | isa = PBXGroup;
148 | children = (
149 | 4C9E331D2468E224007F2C0D /* Tab.swift */,
150 | 4C9E331E2468E224007F2C0D /* TabBarController.swift */,
151 | 4C1EFD06246CA0D3007F1C61 /* TabBarCoordinator.swift */,
152 | );
153 | path = Helpers;
154 | sourceTree = "";
155 | };
156 | /* End PBXGroup section */
157 |
158 | /* Begin PBXNativeTarget section */
159 | 4C6A94472468D15000D5C6C7 /* Example */ = {
160 | isa = PBXNativeTarget;
161 | buildConfigurationList = 4C6A94722468D15200D5C6C7 /* Build configuration list for PBXNativeTarget "Example" */;
162 | buildPhases = (
163 | 4C6A94442468D15000D5C6C7 /* Sources */,
164 | 4C6A94452468D15000D5C6C7 /* Frameworks */,
165 | 4C6A94462468D15000D5C6C7 /* Resources */,
166 | );
167 | buildRules = (
168 | );
169 | dependencies = (
170 | );
171 | name = Example;
172 | productName = Example;
173 | productReference = 4C6A94482468D15000D5C6C7 /* Example.app */;
174 | productType = "com.apple.product-type.application";
175 | };
176 | 4C6A945D2468D15200D5C6C7 /* ExampleTests */ = {
177 | isa = PBXNativeTarget;
178 | buildConfigurationList = 4C6A94752468D15200D5C6C7 /* Build configuration list for PBXNativeTarget "ExampleTests" */;
179 | buildPhases = (
180 | 4C6A945A2468D15200D5C6C7 /* Sources */,
181 | 4C6A945B2468D15200D5C6C7 /* Frameworks */,
182 | 4C6A945C2468D15200D5C6C7 /* Resources */,
183 | );
184 | buildRules = (
185 | );
186 | dependencies = (
187 | 4C6A94602468D15200D5C6C7 /* PBXTargetDependency */,
188 | );
189 | name = ExampleTests;
190 | productName = ExampleTests;
191 | productReference = 4C6A945E2468D15200D5C6C7 /* ExampleTests.xctest */;
192 | productType = "com.apple.product-type.bundle.unit-test";
193 | };
194 | 4C6A94682468D15200D5C6C7 /* ExampleUITests */ = {
195 | isa = PBXNativeTarget;
196 | buildConfigurationList = 4C6A94782468D15200D5C6C7 /* Build configuration list for PBXNativeTarget "ExampleUITests" */;
197 | buildPhases = (
198 | 4C6A94652468D15200D5C6C7 /* Sources */,
199 | 4C6A94662468D15200D5C6C7 /* Frameworks */,
200 | 4C6A94672468D15200D5C6C7 /* Resources */,
201 | );
202 | buildRules = (
203 | );
204 | dependencies = (
205 | 4C6A946B2468D15200D5C6C7 /* PBXTargetDependency */,
206 | );
207 | name = ExampleUITests;
208 | productName = ExampleUITests;
209 | productReference = 4C6A94692468D15200D5C6C7 /* ExampleUITests.xctest */;
210 | productType = "com.apple.product-type.bundle.ui-testing";
211 | };
212 | /* End PBXNativeTarget section */
213 |
214 | /* Begin PBXProject section */
215 | 4C6A94402468D15000D5C6C7 /* Project object */ = {
216 | isa = PBXProject;
217 | attributes = {
218 | LastSwiftUpdateCheck = 1140;
219 | LastUpgradeCheck = 1220;
220 | ORGANIZATIONNAME = "Nicholas Bellucci";
221 | TargetAttributes = {
222 | 4C6A94472468D15000D5C6C7 = {
223 | CreatedOnToolsVersion = 11.4;
224 | };
225 | 4C6A945D2468D15200D5C6C7 = {
226 | CreatedOnToolsVersion = 11.4;
227 | TestTargetID = 4C6A94472468D15000D5C6C7;
228 | };
229 | 4C6A94682468D15200D5C6C7 = {
230 | CreatedOnToolsVersion = 11.4;
231 | TestTargetID = 4C6A94472468D15000D5C6C7;
232 | };
233 | };
234 | };
235 | buildConfigurationList = 4C6A94432468D15000D5C6C7 /* Build configuration list for PBXProject "Example" */;
236 | compatibilityVersion = "Xcode 9.3";
237 | developmentRegion = en;
238 | hasScannedForEncodings = 0;
239 | knownRegions = (
240 | en,
241 | Base,
242 | );
243 | mainGroup = 4C6A943F2468D15000D5C6C7;
244 | productRefGroup = 4C6A94492468D15000D5C6C7 /* Products */;
245 | projectDirPath = "";
246 | projectRoot = "";
247 | targets = (
248 | 4C6A94472468D15000D5C6C7 /* Example */,
249 | 4C6A945D2468D15200D5C6C7 /* ExampleTests */,
250 | 4C6A94682468D15200D5C6C7 /* ExampleUITests */,
251 | );
252 | };
253 | /* End PBXProject section */
254 |
255 | /* Begin PBXResourcesBuildPhase section */
256 | 4C6A94462468D15000D5C6C7 /* Resources */ = {
257 | isa = PBXResourcesBuildPhase;
258 | buildActionMask = 2147483647;
259 | files = (
260 | 4C6A94582468D15200D5C6C7 /* LaunchScreen.storyboard in Resources */,
261 | 4C6A94552468D15200D5C6C7 /* Preview Assets.xcassets in Resources */,
262 | 4C6A94522468D15200D5C6C7 /* Assets.xcassets in Resources */,
263 | );
264 | runOnlyForDeploymentPostprocessing = 0;
265 | };
266 | 4C6A945C2468D15200D5C6C7 /* Resources */ = {
267 | isa = PBXResourcesBuildPhase;
268 | buildActionMask = 2147483647;
269 | files = (
270 | );
271 | runOnlyForDeploymentPostprocessing = 0;
272 | };
273 | 4C6A94672468D15200D5C6C7 /* Resources */ = {
274 | isa = PBXResourcesBuildPhase;
275 | buildActionMask = 2147483647;
276 | files = (
277 | );
278 | runOnlyForDeploymentPostprocessing = 0;
279 | };
280 | /* End PBXResourcesBuildPhase section */
281 |
282 | /* Begin PBXSourcesBuildPhase section */
283 | 4C6A94442468D15000D5C6C7 /* Sources */ = {
284 | isa = PBXSourcesBuildPhase;
285 | buildActionMask = 2147483647;
286 | files = (
287 | 4C9E33202468E224007F2C0D /* StatefulTabView+Modifiers.swift in Sources */,
288 | 4C9E33232468E224007F2C0D /* TabBarController.swift in Sources */,
289 | 4C1EFD07246CA0D3007F1C61 /* TabBarCoordinator.swift in Sources */,
290 | 4C6A944C2468D15000D5C6C7 /* AppDelegate.swift in Sources */,
291 | 4C6A944E2468D15000D5C6C7 /* SceneDelegate.swift in Sources */,
292 | 4C9E33212468E224007F2C0D /* StatefulTabView.swift in Sources */,
293 | 4C1EFD09246CA684007F1C61 /* Tab+Modifiers.swift in Sources */,
294 | 4C6A94502468D15000D5C6C7 /* ContentView.swift in Sources */,
295 | 4C9E33222468E224007F2C0D /* Tab.swift in Sources */,
296 | );
297 | runOnlyForDeploymentPostprocessing = 0;
298 | };
299 | 4C6A945A2468D15200D5C6C7 /* Sources */ = {
300 | isa = PBXSourcesBuildPhase;
301 | buildActionMask = 2147483647;
302 | files = (
303 | );
304 | runOnlyForDeploymentPostprocessing = 0;
305 | };
306 | 4C6A94652468D15200D5C6C7 /* Sources */ = {
307 | isa = PBXSourcesBuildPhase;
308 | buildActionMask = 2147483647;
309 | files = (
310 | );
311 | runOnlyForDeploymentPostprocessing = 0;
312 | };
313 | /* End PBXSourcesBuildPhase section */
314 |
315 | /* Begin PBXTargetDependency section */
316 | 4C6A94602468D15200D5C6C7 /* PBXTargetDependency */ = {
317 | isa = PBXTargetDependency;
318 | target = 4C6A94472468D15000D5C6C7 /* Example */;
319 | targetProxy = 4C6A945F2468D15200D5C6C7 /* PBXContainerItemProxy */;
320 | };
321 | 4C6A946B2468D15200D5C6C7 /* PBXTargetDependency */ = {
322 | isa = PBXTargetDependency;
323 | target = 4C6A94472468D15000D5C6C7 /* Example */;
324 | targetProxy = 4C6A946A2468D15200D5C6C7 /* PBXContainerItemProxy */;
325 | };
326 | /* End PBXTargetDependency section */
327 |
328 | /* Begin PBXVariantGroup section */
329 | 4C6A94562468D15200D5C6C7 /* LaunchScreen.storyboard */ = {
330 | isa = PBXVariantGroup;
331 | children = (
332 | 4C6A94572468D15200D5C6C7 /* Base */,
333 | );
334 | name = LaunchScreen.storyboard;
335 | sourceTree = "";
336 | };
337 | /* End PBXVariantGroup section */
338 |
339 | /* Begin XCBuildConfiguration section */
340 | 4C6A94702468D15200D5C6C7 /* Debug */ = {
341 | isa = XCBuildConfiguration;
342 | buildSettings = {
343 | ALWAYS_SEARCH_USER_PATHS = NO;
344 | CLANG_ANALYZER_NONNULL = YES;
345 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
346 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
347 | CLANG_CXX_LIBRARY = "libc++";
348 | CLANG_ENABLE_MODULES = YES;
349 | CLANG_ENABLE_OBJC_ARC = YES;
350 | CLANG_ENABLE_OBJC_WEAK = YES;
351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
352 | CLANG_WARN_BOOL_CONVERSION = YES;
353 | CLANG_WARN_COMMA = YES;
354 | CLANG_WARN_CONSTANT_CONVERSION = YES;
355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
358 | CLANG_WARN_EMPTY_BODY = YES;
359 | CLANG_WARN_ENUM_CONVERSION = YES;
360 | CLANG_WARN_INFINITE_RECURSION = YES;
361 | CLANG_WARN_INT_CONVERSION = YES;
362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
366 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
368 | CLANG_WARN_STRICT_PROTOTYPES = YES;
369 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
370 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
371 | CLANG_WARN_UNREACHABLE_CODE = YES;
372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
373 | COPY_PHASE_STRIP = NO;
374 | DEBUG_INFORMATION_FORMAT = dwarf;
375 | ENABLE_STRICT_OBJC_MSGSEND = YES;
376 | ENABLE_TESTABILITY = YES;
377 | GCC_C_LANGUAGE_STANDARD = gnu11;
378 | GCC_DYNAMIC_NO_PIC = NO;
379 | GCC_NO_COMMON_BLOCKS = YES;
380 | GCC_OPTIMIZATION_LEVEL = 0;
381 | GCC_PREPROCESSOR_DEFINITIONS = (
382 | "DEBUG=1",
383 | "$(inherited)",
384 | );
385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
387 | GCC_WARN_UNDECLARED_SELECTOR = YES;
388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
389 | GCC_WARN_UNUSED_FUNCTION = YES;
390 | GCC_WARN_UNUSED_VARIABLE = YES;
391 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
392 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
393 | MTL_FAST_MATH = YES;
394 | ONLY_ACTIVE_ARCH = YES;
395 | SDKROOT = iphoneos;
396 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
397 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
398 | };
399 | name = Debug;
400 | };
401 | 4C6A94712468D15200D5C6C7 /* Release */ = {
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_ENABLE_OBJC_WEAK = YES;
412 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
413 | CLANG_WARN_BOOL_CONVERSION = YES;
414 | CLANG_WARN_COMMA = YES;
415 | CLANG_WARN_CONSTANT_CONVERSION = YES;
416 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
417 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
418 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
419 | CLANG_WARN_EMPTY_BODY = YES;
420 | CLANG_WARN_ENUM_CONVERSION = YES;
421 | CLANG_WARN_INFINITE_RECURSION = YES;
422 | CLANG_WARN_INT_CONVERSION = YES;
423 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
424 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
425 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
426 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
427 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
428 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
429 | CLANG_WARN_STRICT_PROTOTYPES = YES;
430 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
431 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
432 | CLANG_WARN_UNREACHABLE_CODE = YES;
433 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
434 | COPY_PHASE_STRIP = NO;
435 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
436 | ENABLE_NS_ASSERTIONS = NO;
437 | ENABLE_STRICT_OBJC_MSGSEND = YES;
438 | GCC_C_LANGUAGE_STANDARD = gnu11;
439 | GCC_NO_COMMON_BLOCKS = YES;
440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
442 | GCC_WARN_UNDECLARED_SELECTOR = YES;
443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
444 | GCC_WARN_UNUSED_FUNCTION = YES;
445 | GCC_WARN_UNUSED_VARIABLE = YES;
446 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
447 | MTL_ENABLE_DEBUG_INFO = NO;
448 | MTL_FAST_MATH = YES;
449 | SDKROOT = iphoneos;
450 | SWIFT_COMPILATION_MODE = wholemodule;
451 | SWIFT_OPTIMIZATION_LEVEL = "-O";
452 | VALIDATE_PRODUCT = YES;
453 | };
454 | name = Release;
455 | };
456 | 4C6A94732468D15200D5C6C7 /* Debug */ = {
457 | isa = XCBuildConfiguration;
458 | buildSettings = {
459 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
460 | CODE_SIGN_STYLE = Automatic;
461 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
462 | ENABLE_PREVIEWS = YES;
463 | INFOPLIST_FILE = Example/Info.plist;
464 | LD_RUNPATH_SEARCH_PATHS = (
465 | "$(inherited)",
466 | "@executable_path/Frameworks",
467 | );
468 | PRODUCT_BUNDLE_IDENTIFIER = com.Example;
469 | PRODUCT_NAME = "$(TARGET_NAME)";
470 | SWIFT_VERSION = 5.0;
471 | TARGETED_DEVICE_FAMILY = "1,2";
472 | };
473 | name = Debug;
474 | };
475 | 4C6A94742468D15200D5C6C7 /* Release */ = {
476 | isa = XCBuildConfiguration;
477 | buildSettings = {
478 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
479 | CODE_SIGN_STYLE = Automatic;
480 | DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\"";
481 | ENABLE_PREVIEWS = YES;
482 | INFOPLIST_FILE = Example/Info.plist;
483 | LD_RUNPATH_SEARCH_PATHS = (
484 | "$(inherited)",
485 | "@executable_path/Frameworks",
486 | );
487 | PRODUCT_BUNDLE_IDENTIFIER = com.Example;
488 | PRODUCT_NAME = "$(TARGET_NAME)";
489 | SWIFT_VERSION = 5.0;
490 | TARGETED_DEVICE_FAMILY = "1,2";
491 | };
492 | name = Release;
493 | };
494 | 4C6A94762468D15200D5C6C7 /* Debug */ = {
495 | isa = XCBuildConfiguration;
496 | buildSettings = {
497 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
498 | BUNDLE_LOADER = "$(TEST_HOST)";
499 | CODE_SIGN_STYLE = Automatic;
500 | INFOPLIST_FILE = ExampleTests/Info.plist;
501 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
502 | LD_RUNPATH_SEARCH_PATHS = (
503 | "$(inherited)",
504 | "@executable_path/Frameworks",
505 | "@loader_path/Frameworks",
506 | );
507 | PRODUCT_BUNDLE_IDENTIFIER = com.ExampleTests;
508 | PRODUCT_NAME = "$(TARGET_NAME)";
509 | SWIFT_VERSION = 5.0;
510 | TARGETED_DEVICE_FAMILY = "1,2";
511 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example";
512 | };
513 | name = Debug;
514 | };
515 | 4C6A94772468D15200D5C6C7 /* Release */ = {
516 | isa = XCBuildConfiguration;
517 | buildSettings = {
518 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
519 | BUNDLE_LOADER = "$(TEST_HOST)";
520 | CODE_SIGN_STYLE = Automatic;
521 | INFOPLIST_FILE = ExampleTests/Info.plist;
522 | IPHONEOS_DEPLOYMENT_TARGET = 13.4;
523 | LD_RUNPATH_SEARCH_PATHS = (
524 | "$(inherited)",
525 | "@executable_path/Frameworks",
526 | "@loader_path/Frameworks",
527 | );
528 | PRODUCT_BUNDLE_IDENTIFIER = com.ExampleTests;
529 | PRODUCT_NAME = "$(TARGET_NAME)";
530 | SWIFT_VERSION = 5.0;
531 | TARGETED_DEVICE_FAMILY = "1,2";
532 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example";
533 | };
534 | name = Release;
535 | };
536 | 4C6A94792468D15200D5C6C7 /* Debug */ = {
537 | isa = XCBuildConfiguration;
538 | buildSettings = {
539 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
540 | CODE_SIGN_STYLE = Automatic;
541 | INFOPLIST_FILE = ExampleUITests/Info.plist;
542 | LD_RUNPATH_SEARCH_PATHS = (
543 | "$(inherited)",
544 | "@executable_path/Frameworks",
545 | "@loader_path/Frameworks",
546 | );
547 | PRODUCT_BUNDLE_IDENTIFIER = com.ExampleUITests;
548 | PRODUCT_NAME = "$(TARGET_NAME)";
549 | SWIFT_VERSION = 5.0;
550 | TARGETED_DEVICE_FAMILY = "1,2";
551 | TEST_TARGET_NAME = Example;
552 | };
553 | name = Debug;
554 | };
555 | 4C6A947A2468D15200D5C6C7 /* Release */ = {
556 | isa = XCBuildConfiguration;
557 | buildSettings = {
558 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
559 | CODE_SIGN_STYLE = Automatic;
560 | INFOPLIST_FILE = ExampleUITests/Info.plist;
561 | LD_RUNPATH_SEARCH_PATHS = (
562 | "$(inherited)",
563 | "@executable_path/Frameworks",
564 | "@loader_path/Frameworks",
565 | );
566 | PRODUCT_BUNDLE_IDENTIFIER = com.ExampleUITests;
567 | PRODUCT_NAME = "$(TARGET_NAME)";
568 | SWIFT_VERSION = 5.0;
569 | TARGETED_DEVICE_FAMILY = "1,2";
570 | TEST_TARGET_NAME = Example;
571 | };
572 | name = Release;
573 | };
574 | /* End XCBuildConfiguration section */
575 |
576 | /* Begin XCConfigurationList section */
577 | 4C6A94432468D15000D5C6C7 /* Build configuration list for PBXProject "Example" */ = {
578 | isa = XCConfigurationList;
579 | buildConfigurations = (
580 | 4C6A94702468D15200D5C6C7 /* Debug */,
581 | 4C6A94712468D15200D5C6C7 /* Release */,
582 | );
583 | defaultConfigurationIsVisible = 0;
584 | defaultConfigurationName = Release;
585 | };
586 | 4C6A94722468D15200D5C6C7 /* Build configuration list for PBXNativeTarget "Example" */ = {
587 | isa = XCConfigurationList;
588 | buildConfigurations = (
589 | 4C6A94732468D15200D5C6C7 /* Debug */,
590 | 4C6A94742468D15200D5C6C7 /* Release */,
591 | );
592 | defaultConfigurationIsVisible = 0;
593 | defaultConfigurationName = Release;
594 | };
595 | 4C6A94752468D15200D5C6C7 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = {
596 | isa = XCConfigurationList;
597 | buildConfigurations = (
598 | 4C6A94762468D15200D5C6C7 /* Debug */,
599 | 4C6A94772468D15200D5C6C7 /* Release */,
600 | );
601 | defaultConfigurationIsVisible = 0;
602 | defaultConfigurationName = Release;
603 | };
604 | 4C6A94782468D15200D5C6C7 /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = {
605 | isa = XCConfigurationList;
606 | buildConfigurations = (
607 | 4C6A94792468D15200D5C6C7 /* Debug */,
608 | 4C6A947A2468D15200D5C6C7 /* Release */,
609 | );
610 | defaultConfigurationIsVisible = 0;
611 | defaultConfigurationName = Release;
612 | };
613 | /* End XCConfigurationList section */
614 | };
615 | rootObject = 4C6A94402468D15000D5C6C7 /* Project object */;
616 | }
617 |
--------------------------------------------------------------------------------