├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Package.swift
├── README.md
├── Sources
└── VTabView
│ └── VTabView.swift
└── Tests
├── LinuxMain.swift
└── VTabViewTests
├── VTabViewTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "swiftui-vertical-tab-view",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v10_15),
10 | .tvOS(.v13),
11 | .watchOS(.v6)
12 | ],
13 | products: [
14 | .library(
15 | name: "VTabView",
16 | targets: ["VTabView"])
17 | ],
18 | targets: [
19 | .target(name: "VTabView")
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUI VerticalTabView 🔝
2 | `VTabView` is a native way to display paged vertical content in SwiftUI.
3 |
4 | To work it makes use of the new iOS 14 `TabView` `PageTabViewStyle`.
5 |
6 | ## Usage
7 |
8 | Use like any other TabView:
9 |
10 | ```swift
11 | import SwiftUI
12 | import VTabView
13 |
14 | VTabView {
15 | Text("The First Tab")
16 | .tabItem {
17 | Image(systemName: "square.fill")
18 | }
19 | Text("Another Tab")
20 | .tabItem {
21 | Image(systemName: "circle.fill")
22 | }
23 | Text("The Last Tab")
24 | .tabItem {
25 | Image(systemName: "triangle.fill")
26 | }
27 | }
28 | .tabViewStyle(PageTabViewStyle())
29 | ```
30 |
31 | You can also move index to the right
32 | ```swift
33 | VTabView(indexPosition: .trailing) {
34 | ...
35 | }
36 | .tabViewStyle(PageTabViewStyle())
37 | ```
38 | or remove it
39 | ```swift
40 | VTabView {
41 | ...
42 | }
43 | .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
44 | ```
45 |
46 | ## Limitation
47 |
48 | `TabView` bounces in all directions by default.
49 |
50 | `VTabView` is meant to be used with `.tabViewStyle(PageTabViewStyle())` but you can also use `DefaultTabViewStyle`.
51 | Only remember that tab items will not have the orientation you would probably like to obtain.
52 | A workaround for this would be in tabItem to not use `Text` but only an `Image` correctly transformed.
53 |
54 | ## Installation
55 |
56 | 1. In Xcode, open your project and navigate to **File** → **Swift Packages** → **Add Package Dependency...**
57 | 2. Paste the repository URL (`https://github.com/lorenzofiamingo/swiftui-vertical-tab-view`) and click **Next**.
58 | 3. Click **Finish**.
59 |
60 | ## Other projects
61 |
62 | [SwiftUI VariadicViews 🥞](https://github.com/lorenzofiamingo/swiftui-variadic-views)
63 |
64 | [SwiftUI AsyncButton 🖲️](https://github.com/lorenzofiamingo/swiftui-async-button)
65 |
66 | [SwiftUI MapItemPicker 🗺️](https://github.com/lorenzofiamingo/swiftui-map-item-picker)
67 |
68 | [SwiftUI PhotosPicker 🌇](https://github.com/lorenzofiamingo/swiftui-photos-picker)
69 |
70 | [SwiftUI CachedAsyncImage 🗃️](https://github.com/lorenzofiamingo/swiftui-cached-async-image)
71 |
72 | [SwiftUI SharedObject 🍱](https://github.com/lorenzofiamingo/swiftui-shared-object)
73 |
--------------------------------------------------------------------------------
/Sources/VTabView/VTabView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Created by Lorenzo Fiamingo on 04/11/20.
3 | //
4 | import SwiftUI
5 |
6 | /// A view that switches between multiple child views using interactive user
7 | /// interface elements.
8 | ///
9 | /// To create a user interface with tabs, place views in a `TabView` and apply
10 | /// the ``View/tabItem(_:)`` modifier to the contents of each tab. The following
11 | /// creates a tab view with three paged vertical tabs:
12 | ///
13 | /// VTabView {
14 | /// Text("The First Tab")
15 | /// .tabItem {
16 | /// Image(systemName: "square.fill")
17 | /// Text("First")
18 | /// }
19 | /// Text("Another Tab")
20 | /// .tabItem {
21 | /// Image(systemName: "circle.fill")
22 | /// Text("Second")
23 | /// }
24 | /// Text("The Last Tab")
25 | /// .tabItem {
26 | /// Image(systemName: "triangle.fill")
27 | /// Text("Third")
28 | /// }
29 | /// }
30 | /// .font(.headline)
31 | /// .tabViewStyle(PageTabViewStyle())
32 | ///
33 | /// Tab views only support tab items of type ``Text``, ``Image``, or an image
34 | /// followed by text. Passing any other type of view results in a visible but
35 | /// empty tab item.
36 | @available(iOS 14.0, *)
37 | public struct VTabView: View where Content: View, SelectionValue: Hashable {
38 |
39 | private var selection: Binding?
40 |
41 | private var indexPosition: IndexPosition
42 |
43 | private var content: () -> Content
44 |
45 | /// Creates an instance that selects from content associated with
46 | /// `Selection` values.
47 | public init(selection: Binding?, indexPosition: IndexPosition = .leading, @ViewBuilder content: @escaping () -> Content) {
48 | self.selection = selection
49 | self.indexPosition = indexPosition
50 | self.content = content
51 | }
52 |
53 | private var flippingAngle: Angle {
54 | switch indexPosition {
55 | case .leading:
56 | return .degrees(0)
57 | case .trailing:
58 | return .degrees(180)
59 | }
60 | }
61 |
62 | public var body: some View {
63 | GeometryReader { proxy in
64 | TabView(selection: selection) {
65 | Group {
66 | content()
67 | }
68 | .frame(width: proxy.size.width, height: proxy.size.height)
69 | .rotationEffect(.degrees(-90))
70 | .rotation3DEffect(flippingAngle, axis: (x: 1, y: 0, z: 0))
71 | }
72 | .frame(width: proxy.size.height, height: proxy.size.width)
73 | .rotation3DEffect(flippingAngle, axis: (x: 1, y: 0, z: 0))
74 | .rotationEffect(.degrees(90), anchor: .topLeading)
75 | .offset(x: proxy.size.width)
76 | }
77 | }
78 |
79 | public enum IndexPosition {
80 | case leading
81 | case trailing
82 | }
83 | }
84 |
85 | @available(iOS 14.0, *)
86 | extension VTabView where SelectionValue == Int {
87 |
88 | public init(indexPosition: IndexPosition = .leading, @ViewBuilder content: @escaping () -> Content) {
89 | self.selection = nil
90 | self.indexPosition = indexPosition
91 | self.content = content
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import VTabViewTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += VTabViewTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/VTabViewTests/VTabViewTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import VTabView
3 |
4 | final class VTabViewTests: XCTestCase {
5 | func testExample() {
6 | }
7 |
8 | static var allTests = [
9 | ("testExample", testExample),
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/VTabViewTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(VTabViewTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------