Skip to content

Instantly share code, notes, and snippets.

@GUIEEN
Created February 14, 2020 09:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GUIEEN/0fa2c5833c9f03a5f9f1ef8deaf9cee2 to your computer and use it in GitHub Desktop.
Save GUIEEN/0fa2c5833c9f03a5f9f1ef8deaf9cee2 to your computer and use it in GitHub Desktop.
WIP - same underline width with text width
//
// 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