Skip to content

Commit

Permalink
Merge pull request #1937 from Skyscanner/bellagio/horizontal_navigation
Browse files Browse the repository at this point in the history
Add BPKHorizontalNavigation component
  • Loading branch information
frugoman committed Apr 12, 2024
2 parents 253c8b8 + dc1bf59 commit acd55e5
Show file tree
Hide file tree
Showing 22 changed files with 386 additions and 9 deletions.
@@ -0,0 +1,151 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018-2023 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

public extension BPKHorizontalNavigation {
enum Size {
case `default`
case small

var titleStyle: BPKFontStyle {
switch self {
case .default: return .label1
case .small: return .label2
}
}

var verticalPadding: CGFloat {
switch self {
case .default: return 12
case .small: return BPKSpacing.md.value
}
}
}

struct Tab {
let title: String
let icon: BPKIcon?

public init(title: String, icon: BPKIcon? = nil) {
self.title = title
self.icon = icon
}
}
}

public struct BPKHorizontalNavigation: View {
let tabs: [Tab]
let size: Size
@Binding var selectedTab: Int

public init(tabs: [Tab], size: Size = .default, selectedTab: Binding<Int>) {
self.tabs = tabs
self.size = size
_selectedTab = selectedTab
}

public var body: some View {
ZStack(alignment: .bottom) {
HStack(spacing: BPKSpacing.none) {
ForEach(0..<tabs.count, id: \.self) { index in
Button {
withAnimation {
selectedTab = index
}
} label: {
TabCellView(
tab: tabs[index],
isSelected: selectedTab == index,
size: size
)
}
}
}
.padding(.vertical, size.verticalPadding)

GeometryReader { proxy in
let width = tabsWidth(for: proxy)
Color(.coreAccentColor)
.frame(width: width)
.offset(x: width * CGFloat(selectedTab))
}
.frame(height: 2)
}
.background(.surfaceDefaultColor)
}

private func tabsWidth(for proxy: GeometryProxy) -> CGFloat {
proxy.size.width / CGFloat(tabs.count)
}

struct TabCellView: View {
let tab: Tab
let isSelected: Bool
let size: Size

var body: some View {
HStack(spacing: .md) {
if let icon = tab.icon {
BPKIconView(icon)
.foregroundColor(foregroundColor)
}
BPKText(tab.title, style: size.titleStyle)
.foregroundColor(foregroundColor)
}
.frame(maxWidth: .infinity)
}

private var foregroundColor: BPKColor {
isSelected ? .coreAccentColor : .textPrimaryColor
}
}
}

struct BPKHorizontalNavigation_Previews: PreviewProvider {
static var previews: some View {
VStack {
BPKHorizontalNavigation(
tabs: [.init(title: "One"), .init(title: "Two"), .init(title: "Three")],
selectedTab: .constant(1)
)
BPKHorizontalNavigation(
tabs: [
.init(title: "One", icon: .flight),
.init(title: "Two", icon: .flight),
.init(title: "Three", icon: .flight)
],
selectedTab: .constant(1)
)
BPKHorizontalNavigation(
tabs: [.init(title: "One"), .init(title: "Two"), .init(title: "Three")],
size: .small,
selectedTab: .constant(2)
)
BPKHorizontalNavigation(
tabs: [
.init(title: "One", icon: .flight),
.init(title: "Two", icon: .flight),
.init(title: "Three", icon: .flight)
],
size: .small,
selectedTab: .constant(0)
)
}
}
}
56 changes: 56 additions & 0 deletions Backpack-SwiftUI/HorizontalNavigation/README.md
@@ -0,0 +1,56 @@
# HorizontalNavigation

[![Cocoapods](https://img.shields.io/cocoapods/v/Backpack-SwiftUI.svg?style=flat)](hhttps://cocoapods.org/pods/Backpack-SwiftUI)
[![class reference](https://img.shields.io/badge/Class%20reference-iOS-blue)](https://backpack.github.io/ios/versions/latest/swiftui/Structs/BPKBadge.html)
[![view on Github](https://img.shields.io/badge/Source%20code-GitHub-lightgrey)](https://github.com/Skyscanner/backpack-ios/tree/main/Backpack-SwiftUI/Badge)

## Default

| Day | Night |
| --- | --- |
| <img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_horizontal-navigation___default_lm.png" alt="" width="375" /> | <img src="https://raw.githubusercontent.com/Skyscanner/backpack-ios/main/screenshots/iPhone-swiftui_horizontal-navigation___default_dm.png" alt="" width="375" /> |

## Usage

```swift
@State var selectedTab: Int = 0
BPKHorizontalNavigation(
tabs: [
.init(title: "Flights", icon: .flight),
.init(title: "Hotels", icon: .hotel),
.init(title: "Cars", icon: .car)
],
selectedTab: $selectedTab
)
```

### Setting the Size

BPkHorizontalNavigation supports both a `default` and a `small` size.

```swift
@State var selectedTab: Int = 0
BPKHorizontalNavigation(
tabs: [
.init(title: "Flights", icon: .flight),
.init(title: "Hotels", icon: .hotel),
.init(title: "Cars", icon: .car)
],
size: .small,
selectedTab: $selectedTab
)
```

### Tabs withou icons

```swift
@State var selectedTab: Int = 0
BPKHorizontalNavigation(
tabs: [
.init(title: "Flights"),
.init(title: "Hotels"),
.init(title: "Cars")
],
selectedTab: $selectedTab
)
```
@@ -0,0 +1,86 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import XCTest
import SwiftUI
@testable import Backpack_SwiftUI

class BPKHorizontalNavigationTests: XCTestCase {
func test_allTabsWithoutIcon_defaultSize() {
assertSnapshot(
BPKHorizontalNavigation(
tabs: [.init(title: "One"), .init(title: "Two"), .init(title: "Three")],
selectedTab: .constant(1)
)
.frame(width: 400)
)
}

func test_allTabsWithoutIcon_smallSize() {
assertSnapshot(
BPKHorizontalNavigation(
tabs: [.init(title: "One"), .init(title: "Two"), .init(title: "Three")],
size: .small,
selectedTab: .constant(1)
)
.frame(width: 400)
)
}

func test_allTabsWithIcon_defaultSize() {
assertSnapshot(
BPKHorizontalNavigation(
tabs: [
.init(title: "One", icon: .flight),
.init(title: "Two", icon: .flight),
.init(title: "Three", icon: .flight)
],
selectedTab: .constant(1)
)
.frame(width: 400)
)
}

func test_allTabsWithIcon_smallSize() {
assertSnapshot(
BPKHorizontalNavigation(
tabs: [
.init(title: "One", icon: .flight),
.init(title: "Two", icon: .flight),
.init(title: "Three", icon: .flight)
],
size: .small,
selectedTab: .constant(1)
)
.frame(width: 400)
)
}

func test_accessibility() {
assertA11ySnapshot(
BPKHorizontalNavigation(
tabs: [
.init(title: "One", icon: .flight),
.init(title: "Two", icon: .flight),
.init(title: "Three", icon: .flight)
],
selectedTab: .constant(1)
)
)
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions Example/Backpack Screenshot/SwiftUIScreenshots.swift
Expand Up @@ -798,5 +798,10 @@ class SwiftUIScreenshots: BackpackSnapshotTestCase {
saveScreenshot(component: "app-search-modal", scenario: "error", userInterfaceStyle: userInterfaceStyle)
tapBackButton()
}

await navigate(title: "Horizontal navigation") {
switchTab(title: "SwiftUI")
saveScreenshot(component: "horizontal-navigation", scenario: "default", userInterfaceStyle: userInterfaceStyle)
}
}
}
24 changes: 18 additions & 6 deletions Example/Backpack.xcodeproj/project.pbxproj
Expand Up @@ -34,6 +34,7 @@
5390DB612909940700F0F790 /* ColorTokensViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5390DB602909940700F0F790 /* ColorTokensViewController.swift */; };
539897BF2BA852E500C8D939 /* ImageGalleryPreviewExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539897BE2BA852E500C8D939 /* ImageGalleryPreviewExampleView.swift */; };
539897C82BA8A8FF00C8D939 /* ImageGallerySlideshowExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539897C72BA8A8FF00C8D939 /* ImageGallerySlideshowExampleView.swift */; };
539E51892BC84770001FB36F /* HorizontalNavigationExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539E51882BC844CA001FB36F /* HorizontalNavigationExampleView.swift */; };
53B5822127DA3CF4004101A3 /* CalendarUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B5822027DA3CF3004101A3 /* CalendarUITest.swift */; };
53B6DB5927FB6F930042B7C0 /* ComponentCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B6DB5727FB6F930042B7C0 /* ComponentCells.swift */; };
53B6DB5A27FB6F930042B7C0 /* TokenCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B6DB5827FB6F930042B7C0 /* TokenCells.swift */; };
Expand Down Expand Up @@ -261,6 +262,7 @@
5390DB602909940700F0F790 /* ColorTokensViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorTokensViewController.swift; sourceTree = "<group>"; };
539897BE2BA852E500C8D939 /* ImageGalleryPreviewExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageGalleryPreviewExampleView.swift; sourceTree = "<group>"; };
539897C72BA8A8FF00C8D939 /* ImageGallerySlideshowExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallerySlideshowExampleView.swift; sourceTree = "<group>"; };
539E51882BC844CA001FB36F /* HorizontalNavigationExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalNavigationExampleView.swift; sourceTree = "<group>"; };
53B5822027DA3CF3004101A3 /* CalendarUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarUITest.swift; sourceTree = "<group>"; };
53B6DB5727FB6F930042B7C0 /* ComponentCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComponentCells.swift; sourceTree = "<group>"; };
53B6DB5827FB6F930042B7C0 /* TokenCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCells.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -525,20 +527,20 @@
path = Select;
sourceTree = "<group>";
};
256F76132BB479610047AD1C /* SearchInputSummary */ = {
1AF3D2C82BB18924001623A4 /* ImageGalleryGridView */ = {
isa = PBXGroup;
children = (
256F76142BB479850047AD1C /* SearchInputSummaryExampleView.swift */,
1AF3D2C92BB18924001623A4 /* ImageGalleryGridExampleView.swift */,
);
path = SearchInputSummary;
path = ImageGalleryGridView;
sourceTree = "<group>";
};
1AF3D2C82BB18924001623A4 /* ImageGalleryGridView */ = {
256F76132BB479610047AD1C /* SearchInputSummary */ = {
isa = PBXGroup;
children = (
1AF3D2C92BB18924001623A4 /* ImageGalleryGridExampleView.swift */,
256F76142BB479850047AD1C /* SearchInputSummaryExampleView.swift */,
);
path = ImageGalleryGridView;
path = SearchInputSummary;
sourceTree = "<group>";
};
2A8000102AB3DC3B009FDB10 /* TextArea */ = {
Expand Down Expand Up @@ -609,6 +611,14 @@
path = ImageGallerySlideshow;
sourceTree = "<group>";
};
539E51872BC844C1001FB36F /* HorizontalNavigation */ = {
isa = PBXGroup;
children = (
539E51882BC844CA001FB36F /* HorizontalNavigationExampleView.swift */,
);
path = HorizontalNavigation;
sourceTree = "<group>";
};
53B6DB5627FB6F930042B7C0 /* FeatureStories */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -985,6 +995,7 @@
793EC5502836139A00D627F6 /* Components */ = {
isa = PBXGroup;
children = (
539E51872BC844C1001FB36F /* HorizontalNavigation */,
1AF3D2C82BB18924001623A4 /* ImageGalleryGridView */,
E2B9D19D2BB3579F0053C14C /* NavigationTabGroup */,
E2B9D19C2BB357950053C14C /* NavigationTab */,
Expand Down Expand Up @@ -1871,6 +1882,7 @@
5318E3342AF506FA00C66D18 /* CalendarExampleSingleView.swift in Sources */,
3A7D2D47214AB9F400ECBD5B /* BPKButtonsViewController.m in Sources */,
D2644E3022C0EB4E008B50C0 /* TappableLinkLabelsSelectorViewController.swift in Sources */,
539E51892BC84770001FB36F /* HorizontalNavigationExampleView.swift in Sources */,
53C6622529EA0DAB00BF1A62 /* PanelExampleView.swift in Sources */,
D24A31D5219B147A009B75E4 /* UICollectionViewMasonryFlowLayout.swift in Sources */,
53C6621C29EA0DAB00BF1A62 /* PageIndicatorExampleView.swift in Sources */,
Expand Down

0 comments on commit acd55e5

Please sign in to comment.