├── .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 |
--------------------------------------------------------------------------------