251 lines
13 KiB
Swift
251 lines
13 KiB
Swift
//
|
|
// PagerTabStripView.swift
|
|
// PagerTabStripView
|
|
//
|
|
// Copyright © 2022 Xmartlabs SRL. All rights reserved.
|
|
//
|
|
import SwiftUI
|
|
|
|
class SelectionState<SelectionType>: ObservableObject {
|
|
@Published var selection: SelectionType
|
|
|
|
init(selection: SelectionType) {
|
|
self.selection = selection
|
|
}
|
|
}
|
|
|
|
public struct HorizontalContainerEdge: OptionSet {
|
|
public let rawValue: Int
|
|
|
|
public init(rawValue: Int) {
|
|
self.rawValue = rawValue
|
|
}
|
|
|
|
public static let left = HorizontalContainerEdge(rawValue: 1 << 0)
|
|
public static let right = HorizontalContainerEdge(rawValue: 1 << 1)
|
|
|
|
public static let both: HorizontalContainerEdge = [.left, .right]
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
public struct PagerTabStripView<SelectionType, Content>: View where SelectionType: Hashable, Content: View {
|
|
private var content: () -> Content
|
|
private var swipeGestureEnabled: Binding<Bool>
|
|
private var edgeSwipeGestureDisabled: Binding<HorizontalContainerEdge>
|
|
private var selection: Binding<SelectionType>
|
|
@StateObject private var selectionState: SelectionState<SelectionType>
|
|
|
|
public init(swipeGestureEnabled: Binding<Bool> = .constant(true),
|
|
edgeSwipeGestureDisabled: Binding<HorizontalContainerEdge> = .constant([]),
|
|
selection: Binding<SelectionType>, @ViewBuilder content: @escaping () -> Content) {
|
|
self.swipeGestureEnabled = swipeGestureEnabled
|
|
self.edgeSwipeGestureDisabled = edgeSwipeGestureDisabled
|
|
self._selectionState = StateObject(wrappedValue: SelectionState(selection: selection.wrappedValue))
|
|
self.selection = selection
|
|
self.content = content
|
|
}
|
|
|
|
@MainActor public var body: some View {
|
|
WrapperPagerTabStripView(swipeGestureEnabled: swipeGestureEnabled,
|
|
edgeSwipeGestureDisabled: edgeSwipeGestureDisabled,
|
|
selection: selection, content: content)
|
|
}
|
|
}
|
|
|
|
extension PagerTabStripView where SelectionType == Int {
|
|
|
|
public init(swipeGestureEnabled: Binding<Bool> = .constant(true),
|
|
edgeSwipeGestureDisabled: Binding<HorizontalContainerEdge> = .constant([]),
|
|
@ViewBuilder content: @escaping () -> Content) {
|
|
self.swipeGestureEnabled = swipeGestureEnabled
|
|
self.edgeSwipeGestureDisabled = edgeSwipeGestureDisabled
|
|
let selectionState = SelectionState(selection: 0)
|
|
self._selectionState = StateObject(wrappedValue: selectionState)
|
|
self.selection = Binding(get: {
|
|
selectionState.selection
|
|
}, set: {
|
|
selectionState.selection = $0
|
|
})
|
|
self.content = content
|
|
}
|
|
}
|
|
|
|
private struct WrapperPagerTabStripView<SelectionType, Content>: View where SelectionType: Hashable, Content: View {
|
|
|
|
private var content: Content
|
|
@StateObject private var pagerSettings = PagerSettings<SelectionType>()
|
|
@Environment(\.pagerStyle) var style: PagerStyle
|
|
@Binding private var selection: SelectionType
|
|
@GestureState private var translation: CGFloat = 0
|
|
@Binding private var swipeGestureEnabled: Bool
|
|
@Binding private var edgeSwipeGestureDisabled: HorizontalContainerEdge
|
|
@State private var swipeOn: Bool = true
|
|
|
|
public init(swipeGestureEnabled: Binding<Bool>,
|
|
edgeSwipeGestureDisabled: Binding<HorizontalContainerEdge>,
|
|
selection: Binding<SelectionType>, @ViewBuilder content: @escaping () -> Content) {
|
|
self._swipeGestureEnabled = swipeGestureEnabled
|
|
self._edgeSwipeGestureDisabled = edgeSwipeGestureDisabled
|
|
self._selection = selection
|
|
self.content = content()
|
|
}
|
|
|
|
@MainActor public var body: some View {
|
|
GeometryReader { geometryProxy in
|
|
TabView(selection: $selection) {
|
|
content
|
|
.overlay(
|
|
GeometryReader { geo in
|
|
Color.clear
|
|
.preference(key: ScrollViewOffsetPreferenceKey.self, value: geo.frame(in: .named("PagerView")).minX)
|
|
}
|
|
)
|
|
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self, perform: { offset in
|
|
print("Debug -> selection: \(selection), offset: \(offset)")
|
|
})
|
|
}
|
|
.tabViewStyle(PageTabViewStyle.page(indexDisplayMode: .never))
|
|
.coordinateSpace(name: "PagerView")
|
|
.gesture(swipeGestureEnabled && swipeOn ?
|
|
DragGesture(minimumDistance: 25).onChanged { value in
|
|
swipeOn = !(edgeSwipeGestureDisabled.contains(.left) &&
|
|
(selection == pagerSettings.itemsOrderedByIndex.first && value.translation.width > 0) ||
|
|
edgeSwipeGestureDisabled.contains(.right) &&
|
|
(selection == pagerSettings.itemsOrderedByIndex.last && value.translation.width < 0))
|
|
}.updating($translation) { value, state, _ in
|
|
if selection == pagerSettings.itemsOrderedByIndex.first && value.translation.width > 0 {
|
|
let normTrans = value.translation.width / (geometryProxy.size.width + 50)
|
|
let logValue = log(1 + normTrans)
|
|
state = geometryProxy.size.width/1.5 * logValue
|
|
} else if selection == pagerSettings.itemsOrderedByIndex.last && value.translation.width < 0 {
|
|
let normTrans = -value.translation.width / (geometryProxy.size.width + 50)
|
|
let logValue = log(1 + normTrans)
|
|
state = -geometryProxy.size.width / 1.5 * logValue
|
|
} else {
|
|
state = value.translation.width
|
|
}
|
|
}.onEnded { value in
|
|
let offset = value.predictedEndTranslation.width / geometryProxy.size.width
|
|
let selectionIndex = pagerSettings.indexOf(tag: selection) ?? 0
|
|
let newPredictedIndex = (CGFloat(selectionIndex) - offset).rounded()
|
|
let newIndex = min(max(Int(newPredictedIndex), 0), pagerSettings.items.count - 1)
|
|
if newIndex > selectionIndex {
|
|
selection = pagerSettings.nextSelection(for: selection)
|
|
} else if newIndex < selectionIndex {
|
|
selection = pagerSettings.previousSelection(for: selection)
|
|
}
|
|
}
|
|
: nil)
|
|
.onAppear {
|
|
let frame = geometryProxy.frame(in: .local)
|
|
pagerSettings.width = frame.width
|
|
if let index = pagerSettings.indexOf(tag: selection) {
|
|
pagerSettings.contentOffset = -CGFloat(index) * frame.width
|
|
}
|
|
}
|
|
.onChange(of: pagerSettings.itemsOrderedByIndex) { _ in
|
|
pagerSettings.contentOffset = -(CGFloat(pagerSettings.indexOf(tag: selection) ?? 0) * geometryProxy.size.width)
|
|
}
|
|
.onChange(of: geometryProxy.frame(in: .local)) { geometry in
|
|
pagerSettings.width = geometry.width
|
|
if let index = pagerSettings.indexOf(tag: selection) {
|
|
pagerSettings.contentOffset = -(CGFloat(index)) * geometry.width
|
|
}
|
|
}
|
|
.onChange(of: selection) { newSelection in
|
|
pagerSettings.contentOffset = -(CGFloat(pagerSettings.indexOf(tag: newSelection) ?? 0) * geometryProxy.size.width)
|
|
swipeOn = true
|
|
}
|
|
.onChange(of: translation) { _ in
|
|
pagerSettings.contentOffset = translation - (CGFloat(pagerSettings.indexOf(tag: selection) ?? 0) * geometryProxy.size.width)
|
|
swipeOn = true
|
|
}
|
|
}
|
|
.modifier(NavBarModifier(selection: $selection))
|
|
.environmentObject(pagerSettings)
|
|
.clipped()
|
|
}
|
|
|
|
/*
|
|
@MainActor public var body: some View {
|
|
GeometryReader { geometryProxy in
|
|
LazyHStack(spacing: 0) {
|
|
content
|
|
.frame(width: geometryProxy.size.width)
|
|
}
|
|
.coordinateSpace(name: "PagerViewScrollView")
|
|
.offset(x: -CGFloat(pagerSettings.indexOf(tag: selection) ?? 0) * geometryProxy.size.width)
|
|
.offset(x: translation)
|
|
.animation(style.pagerAnimationOnTap, value: selection)
|
|
.animation(style.pagerAnimationOnSwipe, value: translation)
|
|
.gesture(swipeGestureEnabled && swipeOn ?
|
|
DragGesture(minimumDistance: 25).onChanged { value in
|
|
swipeOn = !(edgeSwipeGestureDisabled.contains(.left) &&
|
|
(selection == pagerSettings.itemsOrderedByIndex.first && value.translation.width > 0) ||
|
|
edgeSwipeGestureDisabled.contains(.right) &&
|
|
(selection == pagerSettings.itemsOrderedByIndex.last && value.translation.width < 0))
|
|
}.updating($translation) { value, state, _ in
|
|
if selection == pagerSettings.itemsOrderedByIndex.first && value.translation.width > 0 {
|
|
let normTrans = value.translation.width / (geometryProxy.size.width + 50)
|
|
let logValue = log(1 + normTrans)
|
|
state = geometryProxy.size.width/1.5 * logValue
|
|
} else if selection == pagerSettings.itemsOrderedByIndex.last && value.translation.width < 0 {
|
|
let normTrans = -value.translation.width / (geometryProxy.size.width + 50)
|
|
let logValue = log(1 + normTrans)
|
|
state = -geometryProxy.size.width / 1.5 * logValue
|
|
} else {
|
|
state = value.translation.width
|
|
}
|
|
}.onEnded { value in
|
|
let offset = value.predictedEndTranslation.width / geometryProxy.size.width
|
|
let selectionIndex = pagerSettings.indexOf(tag: selection) ?? 0
|
|
let newPredictedIndex = (CGFloat(selectionIndex) - offset).rounded()
|
|
let newIndex = min(max(Int(newPredictedIndex), 0), pagerSettings.items.count - 1)
|
|
if newIndex > selectionIndex {
|
|
selection = pagerSettings.nextSelection(for: selection)
|
|
} else if newIndex < selectionIndex {
|
|
selection = pagerSettings.previousSelection(for: selection)
|
|
}
|
|
}
|
|
: nil)
|
|
.onAppear {
|
|
let frame = geometryProxy.frame(in: .local)
|
|
pagerSettings.width = frame.width
|
|
if let index = pagerSettings.indexOf(tag: selection) {
|
|
pagerSettings.contentOffset = -CGFloat(index) * frame.width
|
|
}
|
|
}
|
|
.onChange(of: pagerSettings.itemsOrderedByIndex) { _ in
|
|
pagerSettings.contentOffset = -(CGFloat(pagerSettings.indexOf(tag: selection) ?? 0) * geometryProxy.size.width)
|
|
}
|
|
.onChange(of: geometryProxy.frame(in: .local)) { geometry in
|
|
pagerSettings.width = geometry.width
|
|
if let index = pagerSettings.indexOf(tag: selection) {
|
|
pagerSettings.contentOffset = -(CGFloat(index)) * geometry.width
|
|
}
|
|
}
|
|
.onChange(of: selection) { newSelection in
|
|
pagerSettings.contentOffset = -(CGFloat(pagerSettings.indexOf(tag: newSelection) ?? 0) * geometryProxy.size.width)
|
|
swipeOn = true
|
|
}
|
|
.onChange(of: translation) { _ in
|
|
pagerSettings.contentOffset = translation - (CGFloat(pagerSettings.indexOf(tag: selection) ?? 0) * geometryProxy.size.width)
|
|
swipeOn = true
|
|
}
|
|
}
|
|
.modifier(NavBarModifier(selection: $selection))
|
|
.environmentObject(pagerSettings)
|
|
.clipped()
|
|
}
|
|
*/
|
|
}
|
|
|
|
fileprivate struct ScrollViewOffsetPreferenceKey: PreferenceKey {
|
|
|
|
static var defaultValue: CGFloat = 0
|
|
|
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
value = nextValue()
|
|
}
|
|
}
|