├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ └── quynh.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Package.swift ├── README.md ├── Sources └── SlidingTabView │ └── SlidingTabView.swift └── Tests ├── LinuxMain.swift └── SlidingTabViewTests ├── SlidingTabViewTests.swift └── XCTestManifests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/quynh.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SlidingTabView.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | SlidingTabView 16 | 17 | primary 18 | 19 | 20 | SlidingTabViewTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 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: "SlidingTabView", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "SlidingTabView", 12 | targets: ["SlidingTabView"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "SlidingTabView", 23 | dependencies: [], 24 | path: "Sources"), 25 | .testTarget( 26 | name: "SlidingTabViewTests", 27 | dependencies: ["SlidingTabView"]), 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | **SlidingTabView** is a simple Android-Like tab view that is built using the latest and greatest SwiftUI. Almost everything is customizable! 7 | 8 | ## Installation 9 | Please use Swift Package Manager to install **SlidingTabView** 10 | 11 | ## Usage 12 | Just instantiate and bind it to your state. That is it! 13 | ```swift 14 | @State private var selectedTabIndex = 0 15 | SlidingTabView(selection: $selectedTabIndex,tabs: ["First Tab", "Second Tab"] 16 | ``` 17 | 18 | ## Canvas Preview 19 | ```swift 20 | struct SlidingTabConsumerView : View { 21 | @State private var selectedTabIndex = 0 22 | 23 | var body: some View { 24 | VStack(alignment: .leading) { 25 | SlidingTabView(selection: self.$selectedTabIndex, tabs: ["First", "Second"]) 26 | (selectedTabIndex == 0 ? Text("First View") : Text("Second View")).padding() 27 | Spacer() 28 | } 29 | .padding(.top, 50) 30 | .animation(.none) 31 | } 32 | } 33 | 34 | @available(iOS 13.0.0, *) 35 | struct SlidingTabView_Previews : PreviewProvider { 36 | static var previews: some View { 37 | SlidingTabConsumerView() 38 | } 39 | } 40 | ``` 41 | 42 | ## Suggestions or feedback? 43 | Feel free to create a pull request! 44 | -------------------------------------------------------------------------------- /Sources/SlidingTabView/SlidingTabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SlidingTabView.swift 3 | // 4 | // Copyright (c) 2019 Quynh Nguyen 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | // 24 | 25 | import SwiftUI 26 | 27 | @available(iOS 13.0, *) 28 | public struct SlidingTabView : View { 29 | 30 | // MARK: Internal State 31 | 32 | /// Internal state to keep track of the selection index 33 | @State private var selectionState: Int = 0 { 34 | didSet { 35 | selection = selectionState 36 | } 37 | } 38 | 39 | // MARK: Required Properties 40 | 41 | /// Binding the selection index which will re-render the consuming view 42 | @Binding var selection: Int 43 | 44 | /// The title of the tabs 45 | let tabs: [String] 46 | 47 | // Mark: View Customization Properties 48 | 49 | /// The font of the tab title 50 | let font: Font 51 | 52 | /// The selection bar sliding animation type 53 | let animation: Animation 54 | 55 | /// The accent color when the tab is selected 56 | let activeAccentColor: Color 57 | 58 | /// The accent color when the tab is not selected 59 | let inactiveAccentColor: Color 60 | 61 | /// The color of the selection bar 62 | let selectionBarColor: Color 63 | 64 | /// The tab color when the tab is not selected 65 | let inactiveTabColor: Color 66 | 67 | /// The tab color when the tab is selected 68 | let activeTabColor: Color 69 | 70 | /// The height of the selection bar 71 | let selectionBarHeight: CGFloat 72 | 73 | /// The selection bar background color 74 | let selectionBarBackgroundColor: Color 75 | 76 | /// The height of the selection bar background 77 | let selectionBarBackgroundHeight: CGFloat 78 | 79 | // MARK: init 80 | 81 | public init(selection: Binding, 82 | tabs: [String], 83 | font: Font = .body, 84 | animation: Animation = .spring(), 85 | activeAccentColor: Color = .blue, 86 | inactiveAccentColor: Color = Color.black.opacity(0.4), 87 | selectionBarColor: Color = .blue, 88 | inactiveTabColor: Color = .clear, 89 | activeTabColor: Color = .clear, 90 | selectionBarHeight: CGFloat = 2, 91 | selectionBarBackgroundColor: Color = Color.gray.opacity(0.2), 92 | selectionBarBackgroundHeight: CGFloat = 1) { 93 | self._selection = selection 94 | self.tabs = tabs 95 | self.font = font 96 | self.animation = animation 97 | self.activeAccentColor = activeAccentColor 98 | self.inactiveAccentColor = inactiveAccentColor 99 | self.selectionBarColor = selectionBarColor 100 | self.inactiveTabColor = inactiveTabColor 101 | self.activeTabColor = activeTabColor 102 | self.selectionBarHeight = selectionBarHeight 103 | self.selectionBarBackgroundColor = selectionBarBackgroundColor 104 | self.selectionBarBackgroundHeight = selectionBarBackgroundHeight 105 | } 106 | 107 | // MARK: View Construction 108 | 109 | public var body: some View { 110 | assert(tabs.count > 1, "Must have at least 2 tabs") 111 | 112 | return VStack(alignment: .leading, spacing: 0) { 113 | HStack(spacing: 0) { 114 | ForEach(self.tabs, id:\.self) { tab in 115 | Button(action: { 116 | let selection = self.tabs.firstIndex(of: tab) ?? 0 117 | self.selectionState = selection 118 | }) { 119 | HStack { 120 | Spacer() 121 | Text(tab).font(self.font) 122 | Spacer() 123 | } 124 | } 125 | .padding(.vertical, 16) 126 | .accentColor( 127 | self.isSelected(tabIdentifier: tab) 128 | ? self.activeAccentColor 129 | : self.inactiveAccentColor) 130 | .background( 131 | self.isSelected(tabIdentifier: tab) 132 | ? self.activeTabColor 133 | : self.inactiveTabColor) 134 | } 135 | } 136 | GeometryReader { geometry in 137 | ZStack(alignment: .leading) { 138 | Rectangle() 139 | .fill(self.selectionBarColor) 140 | .frame(width: self.tabWidth(from: geometry.size.width), height: self.selectionBarHeight, alignment: .leading) 141 | .offset(x: self.selectionBarXOffset(from: geometry.size.width), y: 0) 142 | .animation(self.animation) 143 | Rectangle() 144 | .fill(self.selectionBarBackgroundColor) 145 | .frame(width: geometry.size.width, height: self.selectionBarBackgroundHeight, alignment: .leading) 146 | }.fixedSize(horizontal: false, vertical: true) 147 | }.fixedSize(horizontal: false, vertical: true) 148 | 149 | } 150 | } 151 | 152 | // MARK: Private Helper 153 | 154 | private func isSelected(tabIdentifier: String) -> Bool { 155 | return tabs[selectionState] == tabIdentifier 156 | } 157 | 158 | private func selectionBarXOffset(from totalWidth: CGFloat) -> CGFloat { 159 | return self.tabWidth(from: totalWidth) * CGFloat(selectionState) 160 | } 161 | 162 | private func tabWidth(from totalWidth: CGFloat) -> CGFloat { 163 | return totalWidth / CGFloat(tabs.count) 164 | } 165 | } 166 | 167 | #if DEBUG 168 | 169 | @available(iOS 13.0, *) 170 | struct SlidingTabConsumerView : View { 171 | @State private var selectedTabIndex = 0 172 | 173 | var body: some View { 174 | VStack(alignment: .leading) { 175 | SlidingTabView(selection: self.$selectedTabIndex, 176 | tabs: ["First", "Second"], 177 | font: .body, 178 | activeAccentColor: Color.blue, 179 | selectionBarColor: Color.blue) 180 | (selectedTabIndex == 0 ? Text("First View") : Text("Second View")).padding() 181 | Spacer() 182 | } 183 | .padding(.top, 50) 184 | .animation(.none) 185 | } 186 | } 187 | 188 | @available(iOS 13.0.0, *) 189 | struct SlidingTabView_Previews : PreviewProvider { 190 | static var previews: some View { 191 | SlidingTabConsumerView() 192 | } 193 | } 194 | #endif 195 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SlidingTabViewTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SlidingTabViewTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SlidingTabViewTests/SlidingTabViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SlidingTabView 3 | 4 | final class SlidingTabViewTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | } 10 | 11 | static var allTests = [ 12 | ("testExample", testExample), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /Tests/SlidingTabViewTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SlidingTabViewTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------