Created
February 14, 2020 09:26
-
-
Save GUIEEN/0fa2c5833c9f03a5f9f1ef8deaf9cee2 to your computer and use it in GitHub Desktop.
WIP - same underline width with text width
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SlidingTabView.swift | |
// | |
// Copyright (c) 2019 Quynh Nguyen | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in | |
// all copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
// THE SOFTWARE. | |
// | |
import SwiftUI | |
@available(iOS 13.0, *) | |
public struct SlidingTabView : View { | |
// MARK: Required Properties | |
/// Binding the selection index which will re-render the consuming view | |
@Binding var selection: Int | |
/// The title of the tabs | |
let tabs: [String] | |
// Mark: View Customization Properties | |
/// The font of the tab title | |
let font: Font | |
/// The selection bar sliding animation type | |
let animation: Animation | |
/// The accent color when the tab is selected | |
let activeAccentColor: Color | |
/// The accent color when the tab is not selected | |
let inactiveAccentColor: Color | |
/// The color of the selection bar | |
let selectionBarColor: Color | |
/// The tab color when the tab is not selected | |
let inactiveTabColor: Color | |
/// The tab color when the tab is selected | |
let activeTabColor: Color | |
/// The height of the selection bar | |
let selectionBarHeight: CGFloat | |
/// The selection bar background color | |
let selectionBarBackgroundColor: Color | |
/// The height of the selection bar background | |
let selectionBarBackgroundHeight: CGFloat | |
// MARK: init | |
public init(selection: Binding<Int>, | |
tabs: [String], | |
font: Font = .body, | |
animation: Animation = .spring(), | |
activeAccentColor: Color = .blue, | |
inactiveAccentColor: Color = Color.black.opacity(0.4), | |
selectionBarColor: Color = .blue, | |
inactiveTabColor: Color = .clear, | |
activeTabColor: Color = .clear, | |
selectionBarHeight: CGFloat = 3, | |
selectionBarBackgroundColor: Color = Color.gray.opacity(0.2), | |
selectionBarBackgroundHeight: CGFloat = 1) { | |
self._selection = selection | |
self.tabs = tabs | |
self.font = font | |
self.animation = animation | |
self.activeAccentColor = activeAccentColor | |
self.inactiveAccentColor = inactiveAccentColor | |
self.selectionBarColor = selectionBarColor | |
self.inactiveTabColor = inactiveTabColor | |
self.activeTabColor = activeTabColor | |
self.selectionBarHeight = selectionBarHeight | |
self.selectionBarBackgroundColor = selectionBarBackgroundColor | |
self.selectionBarBackgroundHeight = selectionBarBackgroundHeight | |
} | |
// MARK: View Construction | |
public var body: some View { | |
assert(tabs.count > 1, "Must have at least 2 tabs") | |
return | |
HStack(spacing: 0) { | |
ForEach(self.tabs, id:\.self) { tab in | |
HStack { | |
Spacer() | |
Button(action: { | |
self.selection = self.tabs.firstIndex(of: tab) ?? 0 | |
}) { | |
GeometryReader { geometry in | |
// tabs | |
VStack(alignment: .center,spacing: 0) { | |
Text(tab) | |
.font(self.font) | |
.fontWeight(.semibold) | |
.lineLimit(1) | |
.truncationMode(.tail) | |
.animation(self.animation) | |
// // FIXME: Workaround to have the same width underline with given text format | |
// // Since foreground color will be `.clear`, if char having a position under the line, intersection with underline will be also `.clear` color. | |
// Text(self.changeString(tab)) | |
// .foregroundColor(.clear) | |
// .underline(true, color: self.isSelected(tabIdentifier: tab) ? self.selectionBarColor : self.selectionBarBackgroundColor) | |
// .font(self.font) | |
// .fontWeight(.semibold) | |
// .lineLimit(1) | |
// .truncationMode(.tail) | |
// .animation(self.animation) | |
// .frame(height: 0) | |
// tabs underbar | |
ZStack { | |
if self.isSelected(tabIdentifier: tab) { | |
Rectangle() | |
.fill(self.selectionBarColor) | |
.frame(width: geometry.size.width, height: self.selectionBarHeight, alignment: .center) | |
.animation(self.animation) | |
} else { | |
Rectangle() | |
.fill(self.selectionBarBackgroundColor) | |
.frame(width: geometry.size.width, height: self.selectionBarBackgroundHeight, alignment: .center) | |
} | |
} | |
} | |
}.fixedSize(horizontal: false, vertical: true) | |
} | |
.frame(minWidth: 0, maxWidth: .infinity) | |
.accentColor( | |
self.isSelected(tabIdentifier: tab) | |
? self.activeAccentColor | |
: self.inactiveAccentColor) | |
.background( | |
self.isSelected(tabIdentifier: tab) | |
? self.activeTabColor | |
: self.inactiveTabColor) | |
Spacer() | |
} | |
} | |
} | |
} | |
// MARK: Private Helper | |
private func changeString(_ text: String) -> String { | |
// MARK: - Convert the font which has a position under the line to `-` | |
var newString = String(text.map { char in | |
// # English | |
if char == "y" { | |
return "-" | |
} | |
if char == "g" { | |
return "-" | |
} | |
if char == "p" { | |
return "-" | |
} | |
if char == "q" { | |
return "-" | |
} | |
if char == "j" { | |
return "-" | |
} | |
return char | |
}) | |
// MARK: - Padding | |
for _ in 0..<4 { | |
newString += "-" | |
} | |
return newString | |
} | |
private func isSelected(tabIdentifier: String) -> Bool { | |
return tabs[selection] == tabIdentifier | |
} | |
private func selectionBarXOffset(from totalWidth: CGFloat) -> CGFloat { | |
return self.tabWidth(from: totalWidth) * CGFloat(selection) | |
} | |
private func tabWidth(from totalWidth: CGFloat) -> CGFloat { | |
return totalWidth / CGFloat(tabs.count) | |
} | |
} | |
#if DEBUG | |
@available(iOS 13.0, *) | |
struct SlidingTabConsumerView : View { | |
@State private var selectedTabIndex = 0 | |
var body: some View { | |
VStack(alignment: .leading) { | |
SlidingTabView(selection: self.$selectedTabIndex, | |
tabs: ["First", "Second"], | |
font: .body, | |
activeAccentColor: Color.blue, | |
selectionBarColor: Color.blue) | |
(selectedTabIndex == 0 ? Text("First View") : Text("Second View")).padding() | |
Spacer() | |
} | |
.padding(.top, 50) | |
.animation(.none) | |
} | |
} | |
@available(iOS 13.0.0, *) | |
struct SlidingTabView_Previews : PreviewProvider { | |
static var previews: some View { | |
SlidingTabConsumerView() | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment