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