-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathTabs.swift
156 lines (133 loc) · 4.4 KB
/
Tabs.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//
// Tabs.swift
//
// Made with ❤️ by Novum
//
// Copyright © Telefonica. All rights reserved.
//
import Foundation
import SwiftUI
public struct Tabs: View {
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State private var itemWidth: [Int: CGFloat] = [:]
private var tabItemViews: [TabItemView] = []
@Binding private var selection: Int
private var background: Color
public init(_ items: [TabItem], selection: Binding<Int>, background: Color = .background) {
_selection = selection
self.background = background
for (index, tabItem) in items.enumerated() {
tabItemViews.append(
TabItemView(
tabItem: tabItem,
indexRow: index,
selectedIndexRow: $selection
)
)
}
}
public var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
ZStack(alignment: .leading) {
itemsView(parentSize: proxy.size)
.animation(.misticaTimingCurve, value: selection)
dividerLine()
.expandHorizontally()
selectedLine()
.animation(.misticaTimingCurve, value: selection)
}
}
}
.onChange(of: tabItemViews, perform: { _ in itemWidth = [:] })
.frame(height: 56)
.background(background)
}
@ViewBuilder
func itemsView(parentSize: CGSize) -> some View {
LazyHStack(spacing: 0) {
ForEach(0 ..< self.tabItemViews.count, id: \.self) { index in
itemView(for: index)
// The width depends on the number of items.
.frame(width: itemWidth(parentSize: parentSize, index: index))
// This proxy will store the intrinsec content size of each element
// We need it to calculat the offset of the selected line.
.background(
GeometryReader { proxy in
Color.clear.preference(key: CGFloatPreferenceKey.self, value: proxy.size.width)
}
)
// Store the new value whenever it changes.
.onPreferenceChange(CGFloatPreferenceKey.self) { width in
self.itemWidth[index] = min(width, 208)
}
.onTapGesture {
self.selection = index
}
}
}
}
var itemHorizontalPadding: CGFloat {
horizontalSizeClass == .compact ? 16 : 32
}
@ViewBuilder
func itemView(for index: Int) -> some View {
tabItemViews[index]
.padding(.horizontal, itemHorizontalPadding)
}
func itemWidth(parentSize: CGSize, index: Int) -> CGFloat? {
if shouldUseScroll {
return itemWidth[index]
} else {
return parentSize.width / CGFloat(tabItemViews.count)
}
}
@ViewBuilder
func selectedLine() -> some View {
VStack(alignment: .leading, spacing: 0) {
Spacer()
Rectangle()
.frame(height: 2)
.foregroundColor(.controlActivated)
}
.frame(width: itemWidth[selection])
.offset(x: selectedLineOffset(), y: 0)
}
func selectedLineOffset() -> CGFloat {
(0 ..< selection)
.compactMap { itemWidth[$0] }
.reduce(0,+)
}
@ViewBuilder
func dividerLine() -> some View {
VStack(alignment: .leading, spacing: 0) {
Spacer()
Divider()
}
}
var shouldUseScroll: Bool {
tabItemViews.count > 3
}
}
// MARK: PreferenceKey
struct CGFloatPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: Value = 0
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
// MARK: Previews
#if DEBUG
struct TabsContainer: View {
@State private var selection = 0
var body: some View {
Tabs([.init(text: "Large text"), .init(text: "Large text")], selection: $selection)
}
}
struct Tabs_Previews: PreviewProvider {
static var previews: some View {
TabsContainer()
}
}
#endif