Skip to content

Instantly share code, notes, and snippets.

@kazuhiro4949
Created March 20, 2023 14:12
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 kazuhiro4949/f29fa328c780cff172326b78404a7a84 to your computer and use it in GitHub Desktop.
Save kazuhiro4949/f29fa328c780cff172326b78404a7a84 to your computer and use it in GitHub Desktop.
import SwiftUI
private var menus: [String] = [
"cat",
"car",
"human",
"robot",
"coffee"
]
class SelectionState: ObservableObject {
@Published var tab: Int = 0
@Published var maskOffset: CGFloat = 0
}
struct ScrollingState: Equatable {
let offset: CGFloat
let index: Int
}
struct OffsetPreferencekey: PreferenceKey {
static var defaultValue = ScrollingState(offset: 0, index: 0)
static func reduce(value: inout ScrollingState, nextValue: () -> ScrollingState) {
let offset = value.offset + nextValue().offset
let index = value.index + nextValue().index
value = ScrollingState(offset: offset, index: index)
}
}
struct ContentView: View {
static let menuSize = CGSize(width: 100, height: 44)
@StateObject private var selectionState = SelectionState()
fileprivate func inactiveMenuItem(_ index: Int) -> some View {
Button {
selectionState.tab = index
withAnimation(.spring(response: 0.3, dampingFraction: 0.75)) {
selectionState.maskOffset = Double(index) * Self.menuSize.width
}
} label: {
ZStack {
Color.white
Text("\(menus[index])")
.bold()
.foregroundColor(.black)
}
.frame(width: Self.menuSize.width)
}
.buttonStyle(.plain)
.id("inactiveMenuItem_\(index)")
}
fileprivate func activeMenuItem(_ index: Int) -> some View {
ZStack {
Color.gray
Text("\(menus[index])")
.bold()
.foregroundColor(.white)
}
.frame(width: Self.menuSize.width)
}
fileprivate func maskMenuItem() -> some View {
return HStack() {
RoundedRectangle(cornerRadius: 16)
.frame(width: Self.menuSize.width - 8, height: Self.menuSize.height - 16, alignment: .center)
.padding(.horizontal, 4)
}
.frame(width: Self.menuSize.width, height: Self.menuSize.height, alignment: .leading)
.offset(x: selectionState.maskOffset)
}
fileprivate func menu() -> some View {
ScrollView(.horizontal, showsIndicators: false) {
ZStack {
HStack(spacing: 0) {
ForEach(0..<menus.count, id: \.self) { index in
inactiveMenuItem(index)
}
}
HStack(spacing: 0) {
ForEach(0..<menus.count, id: \.self) { index in
activeMenuItem(index)
}
}
.mask(alignment: .leading) {
maskMenuItem()
}
.allowsHitTesting(false)
}
}
}
fileprivate func content(geometryProxy: GeometryProxy, scrollViewProxy: ScrollViewProxy) -> some View {
TabView(selection: $selectionState.tab) {
ForEach(0..<menus.count, id: \.self) { index in
Image(menus[index])
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: geometryProxy.size.width)
.clipped()
.ignoresSafeArea()
.tag(index)
.background {
GeometryReader { bgGeometryProxy in
let frame = bgGeometryProxy.frame(in: .named("TabView"))
if index == selectionState.tab {
Color.clear
.preference(
key: OffsetPreferencekey.self,
value: ScrollingState(
offset: frame.origin.x,
index: index))
} else {
Color.clear
}
}
}
}
}
.coordinateSpace(name: "TabView")
.tabViewStyle(.page(indexDisplayMode: .never))
.onPreferenceChange(OffsetPreferencekey.self) { value in
let width = geometryProxy.size.width
let percent = (width - value.offset) / width
let base = CGFloat(value.index - 1)
selectionState.maskOffset = (base + percent) * Self.menuSize.width
}
.onReceive(selectionState.$tab.scan((0, 0), { ($0.1, $1) })) { output in
withAnimation {
scrollViewProxy.scrollTo("inactiveMenuItem_\(output.1)", anchor: .center)
}
}
}
var body: some View {
GeometryReader { geometryProxy in
ScrollViewReader { scrollViewProxy in
VStack(spacing: 0) {
menu()
.frame(height: Self.menuSize.height)
content(geometryProxy: geometryProxy, scrollViewProxy: scrollViewProxy)
}
}
}
.ignoresSafeArea(edges: [.leading, .trailing, .bottom])
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment