You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/Session/Shared/SessionCarouselView+SwiftUI...

229 lines
7.3 KiB
Swift

// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import SwiftUI
public struct SessionCarouselView_SwiftUI: View {
@Binding var index: Int
let isOutgoing: Bool
var contentInfos: [Attachment]
let numberOfPages: Int
public init(index: Binding<Int>, isOutgoing: Bool, contentInfos: [Attachment]) {
self._index = index
self.isOutgoing = isOutgoing
self.contentInfos = contentInfos
self.numberOfPages = contentInfos.count
let first = self.contentInfos.first!
let last = self.contentInfos.last!
self.contentInfos.append(first)
self.contentInfos.insert(last, at: 0)
}
public var body: some View {
HStack(spacing: 0) {
ArrowView(index: $index, numberOfPages: numberOfPages, type: .decrement)
.zIndex(1)
PageView(index: $index, numberOfPages: self.numberOfPages) {
ForEach(self.contentInfos) { attachment in
MediaView_SwiftUI(
attachment: attachment,
isOutgoing: self.isOutgoing,
cornerRadius: 0
)
}
}
.aspectRatio(1, contentMode: .fit)
ArrowView(index: $index, numberOfPages: numberOfPages, type: .increment)
.zIndex(1)
}
}
}
struct ArrowView: View {
@Binding var index: Int
let numberOfPages: Int
let maxIndex: Int
let type: ArrowType
enum ArrowType {
case increment
case decrement
}
init(index: Binding<Int>, numberOfPages: Int, type: ArrowType) {
self._index = index
self.numberOfPages = numberOfPages
self.maxIndex = numberOfPages + 1
self.type = type
}
var body: some View {
let imageName = self.type == .decrement ? "chevron.left" : "chevron.right"
Button {
if self.type == .decrement {
decrement()
} else {
increment()
}
} label: {
Image(systemName: imageName)
.font(.system(size: 20))
.foregroundColor(themeColor: .textPrimary)
.frame(width: 30, height: 30)
}
}
func decrement() {
withAnimation(.easeOut) {
self.index -= 1
}
if self.index == 0 {
self.index = self.maxIndex - 1
}
}
func increment() {
withAnimation(.easeOut) {
self.index += 1
}
if self.index == self.maxIndex {
self.index = 1
}
}
}
struct PageView<Content>: View where Content: View {
@Binding var index: Int
let numberOfPages: Int
let maxIndex: Int
let content: () -> Content
@State private var offset = CGFloat.zero
@State private var dragging = false
init(index: Binding<Int>, numberOfPages: Int, @ViewBuilder content: @escaping () -> Content) {
self._index = index
self.numberOfPages = numberOfPages
self.content = content
self.maxIndex = numberOfPages + 1
}
var body: some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
self.content()
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.content.offset(x: self.offset(in: geometry), y: 0)
.frame(width: geometry.size.width, alignment: .leading)
.clipShape(RoundedRectangle(cornerRadius: 15))
.gesture(
DragGesture(coordinateSpace: .local)
.onChanged { value in
self.dragging = true
self.offset = -CGFloat(self.index) * geometry.size.width + value.translation.width
}
.onEnded { value in
let predictedEndOffset = -CGFloat(self.index) * geometry.size.width + value.predictedEndTranslation.width
let predictedIndex = Int(round(predictedEndOffset / -geometry.size.width))
self.index = self.clampedIndex(from: predictedIndex)
withAnimation(.easeOut(duration: 0.2)) {
self.dragging = false
}
// FIXME: This is a workaround for withAnimation() not having completion callback
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
switch self.index {
case 0: self.index = self.maxIndex - 1
case self.maxIndex: self.index = 1
default: break
}
}
}
)
}
.clipped()
PageControl(index: $index, maxIndex: numberOfPages - 1)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0))
}
}
func offset(in geometry: GeometryProxy) -> CGFloat {
if self.dragging {
return max(min(self.offset, 0), -CGFloat(self.maxIndex) * geometry.size.width)
} else {
return -CGFloat(self.index) * geometry.size.width
}
}
func clampedIndex(from predictedIndex: Int) -> Int {
let newIndex = min(max(predictedIndex, self.index - 1), self.index + 1)
guard newIndex >= 0 else { return 0 }
guard newIndex <= maxIndex else { return maxIndex }
return newIndex
}
}
struct PageControl: View {
@Binding var index: Int
let maxIndex: Int
var body: some View {
ZStack {
Capsule()
.foregroundColor(.init(white: 0, opacity: 0.4))
HStack(spacing: 4) {
ForEach(0...maxIndex, id: \.self) { index in
Circle()
.fill(index == ((self.index - 1) % (self.maxIndex + 1)) ? Color.white : Color.gray)
.frame(width: 7, height: 7)
}
}
.padding(6)
}
.fixedSize(horizontal: true, vertical: true)
.frame(
maxWidth: .infinity,
maxHeight: 19
)
}
}
struct SessionCarouselView_SwiftUI_Previews: PreviewProvider {
@State static var index = 1
static var previews: some View {
ZStack {
if #available(iOS 14.0, *) {
Color.black.ignoresSafeArea()
} else {
Color.black
}
SessionCarouselView_SwiftUI(
index: $index,
isOutgoing: true,
contentInfos: [
Attachment(
variant: .standard,
contentType: "jpeg",
byteCount: 100
),
Attachment(
variant: .standard,
contentType: "jpeg",
byteCount: 100
)
]
)
}
}
}