I am currently working on having a carousel view by displaying an array of objects in a TabView. Currently, I have it set so the TabView allows me to swipe horizontally to the next or previous object or choose to let the app rotate automatically to the next object every 20 seconds (defined in an earlier part of the TakesHomePageView
code). The tab view indicator is meant to represent the index of said objects and update accordingly.
extension TakesHomepageView {
var featuredTakeView: some View {
VStack(alignment: .leading) {
UText("Featured Takes", size: 20, type: .semiBold)
if !viewModel.featuredTakes.isEmpty {
VStack {
TabView(selection: $viewModel.currentIndex) {
ForEach(viewModel.featuredTakes.indices, id: .self) { index in
FeaturedTakeView(take: viewModel.featuredTakes[index])
.tag(index)
.transition(.asymmetric(insertion: .opacity, removal: AnyTransition.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))) // Use asymmetric for different in/out animations
.onAppear {
viewModel.currentIndex = index // Ensure currentIndex is updated when featured take appears
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.frame(height: 275) // Set a fixed height for the carousel
.animation(.easeInOut(duration: viewModel.isSwiping ? 0.3 : 2.0)) // Adjust animation duration based on swiping or automatic rotation
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width > 100 {
viewModel.swipeToFeaturedTake(forward: true)
} else if value.translation.width < -100 {
viewModel.swipeToFeaturedTake(forward: false)
}
}
.onEnded { _ in
viewModel.isSwiping = false // Reset isSwiping flag after swipe gesture ends
}
)
}
} else {
UText("Check back later for Featured Takes")
}
}
.horizontalPadding()
}
}
Currently in the code above, the former works. When the view loads, the takes are loaded perfectly, I can swipe to the next or previous takes and when the app rotates to the next take automatically, the indicator updates itself accordingly. However, when I manually swipe to the next or previous object, the indicator does not update but rather stays as is.
Here is the associated view model code:
class TakesHomepageViewModel: ObservableObject {
@Published var selectedCategory: TakesCategory = .all {
didSet {
service = .init(containerType: .live, category: selectedCategory)
Task {
await getTakes()
}
}
}
// Featured Take
var currentIndex: Int = 0
var isSwiping: Bool = false
@Published var featuredTake: TakeOBJ?
var featuredTakes: [TakeOBJ] = []
@Published var featuredTakeLoaded: Bool = false
var showFeaturedTake: Bool {
featuredTake != nil && featuredTakeLoaded
}
// Expiring Takes
@Published var expiringSoon: [TakeOBJ] = []
@Published var expiringTakesLoaded: Bool = false
// Live Takes
@Published var liveTakes: [TakeOBJ] = []
@Published var liveTakesLoaded: Bool = false
var service: TakesContainerService = .init(containerType: .live, category: .all)
@MainActor
func getAll() async {
allLoaded = false
await getPromoTake()
await getExpiringTakes()
await getTakes()
allLoaded = true
}
@Published var allLoaded: Bool = false
var canLoadMore: Bool {
(liveTakesLoaded && liveTakes.count == 20) && !(!liveTakesLoaded && liveTakes.isEmpty)
}
@Published var noTakes: Bool = false
}
// MARK: Category Takes
extension TakesHomepageViewModel {
@MainActor
func getTakes() async {
liveTakes.removeAll()
liveTakesLoaded = false
do {
let takes: [TakeOBJ] = try await service.getTakes()
print("HELLO: (takes)")
self.liveTakes = takes
self.noTakes = takes.isEmpty
} catch {
}
liveTakesLoaded = true
}
@MainActor
func getNextPage() async {
self.service.page += 1
liveTakesLoaded = false
do {
let takes: [TakeOBJ] = try await service.getTakes()
self.liveTakes.append(contentsOf: takes)
self.noTakes = takes.isEmpty
} catch {
}
liveTakesLoaded = true
}
}
// MARK: Expiring Takes Functions
extension TakesHomepageViewModel {
@MainActor
func getExpiringTakes() async {
do {
self.expiringSoon = try await TakeService.getExpiringTakes()
expiringTakesLoaded = true
} catch {
expiringTakesLoaded = true
}
}
}
// MARK: Featured Take Functions
extension TakesHomepageViewModel {
@MainActor
func getPromoTake() async {
do {
let takes: [TakeOBJ] = try await FirebaseService.getPromoTakes()
self.featuredTakes = takes
self.featuredTake = takes.first
featuredTakeLoaded = true
} catch {
featuredTakeLoaded = true
}
}
func rotateFeaturedTake() {
if currentIndex + 1 >= featuredTakes.count {
currentIndex = 0
} else {
currentIndex += 1
}
if currentIndex >= featuredTakes.count {
return
}
featuredTake = featuredTakes[currentIndex]
}
func swipeToFeaturedTake(forward: Bool) {
var newIndex = currentIndex
if forward {
newIndex = (currentIndex - 1 + featuredTakes.count) % featuredTakes.count
} else {
newIndex = (currentIndex + 1) % featuredTakes.count
}
featuredTake = featuredTakes[newIndex]
currentIndex = newIndex
}
}
The swipeToFeaturedTake()
function is called when a gesture is detected so that the indicator updates its index when going to a different object in the carousel view, but for some reason, it seems like the gesture isn’t being detected. If this is the case, how can it be fixed so that the indicator correctly updates when swiping through the carousel view? If any more code is needed, do let me know. All help is appreciated.
Edit: While doing some extra testing, I found that if I swipe through the carousel while it’s rotating to the next object after the 20 seconds are up, the swipeToFeaturedTake()
function is called at the same time as rotateToFeaturedTake()
and then the swipe function isn’t called again until I recreate the scenario.