Carousel view SwiftUI
Asked Answered
C

2

6

How to achieve the Rotary type Carousel present in the iCarousel Framework of swift. The following is what I wanted to achieve with the SwiftUI

enter image description here

I checked many tutorials and the framework present but I could not able to achieve as shown above image

Cortezcortical answered 23/5, 2022 at 5:56 Comment(2)
Does this help? appcoda.com/learnswiftui/swiftui-carousel.htmlSarazen
@MrDeveloper Thanks for the reply. I checked that too but not matching my requirement.Cortezcortical
V
36

Here is a general approach: All items are drawn above each other in a ZStack, then their position and opacity is changed based on the "distance" to the foremost element.

The demo has a fixed size for the item, but can easily be adapted. Change the values in opacity and scaleEffectto your wishes.

enter image description here

struct Item: Identifiable {
    var id: Int
    var title: String
    var color: Color
}

class Store: ObservableObject {
    @Published var items: [Item]
    
    let colors: [Color] = [.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo, .black]

    // dummy data
    init() {
        items = []
        for i in 0...7 {
            let new = Item(id: i, title: "Item \(i)", color: colors[i])
            items.append(new)
        }
    }
}


struct ContentView: View {
    
    @StateObject var store = Store()
    @State private var snappedItem = 0.0
    @State private var draggingItem = 0.0
    
    var body: some View {
        
        ZStack {
            ForEach(store.items) { item in
                
                // article view
                ZStack {
                    RoundedRectangle(cornerRadius: 18)
                        .fill(item.color)
                    Text(item.title)
                        .padding()
                }
                .frame(width: 200, height: 200)
                
                .scaleEffect(1.0 - abs(distance(item.id)) * 0.2 )
                .opacity(1.0 - abs(distance(item.id)) * 0.3 )
                .offset(x: myXOffset(item.id), y: 0)
                .zIndex(1.0 - abs(distance(item.id)) * 0.1)
            }
        }
        .gesture(
            DragGesture()
                .onChanged { value in
                    draggingItem = snappedItem + value.translation.width / 100
                }
                .onEnded { value in
                    withAnimation {
                        draggingItem = snappedItem + value.predictedEndTranslation.width / 100
                        draggingItem = round(draggingItem).remainder(dividingBy: Double(store.items.count))
                        snappedItem = draggingItem
                    }
                }
        )
    }
    
    func distance(_ item: Int) -> Double {
        return (draggingItem - Double(item)).remainder(dividingBy: Double(store.items.count))
    }
    
    func myXOffset(_ item: Int) -> Double {
        let angle = Double.pi * 2 / Double(store.items.count) * distance(item)
        return sin(angle) * 200
    }
    
}
Vinegarish answered 23/5, 2022 at 16:53 Comment(10)
Excellent Approach, Thanks, is it possible to show from Index 0 to items.count and smoothen the scroll for smaller item size and end the scroll when it reaches on both side (Index 0 or items.count)Glacial
is it possible to detect current index in this code ?Chokefull
@SadmanSamee on my case i change draggingItem = snappedItem + value.predictedEndTranslation.width / 100 to this draggingItem = snappedItem + val.translation.width / 100Chokefull
@Vinegarish Is there is a way to change it on Button Click also?Carboniferous
Very nice. The other thing you did @Vinegarish was helped me better understand why I would use StateObject. From playing with this I see I want to use StateObject where the View itself controls itself controls the objects lifecycle. In my case, other views need that information so I may use Observable Obj...but it is very helpfulShanteshantee
@Vinegarish is there any way to set the active item on load?Undercoat
Hey @ChrisR, In the beginning, thanks for this approach Now I have a Question: What if I want to do it in the Linear way inside the routing? like here camo.githubusercontent.com/…Sukiyaki
@Undercoat to set the active item on load just assign a start value to draggingItem AND snappedItem.Vinegarish
@AbdelrahmanMohamed just get rid of the .scaleEffect and adapt the radius in myXOffset e.g. from 200 to 300. Of course this "carousel" still keeps items in a circle. It looks like you want it fully linear ...Vinegarish
Hey, @ChrisR, thanks again. I have another question. Can we add space between the current, previous, and subsequent images? Since my view has the whole picture with 20 px padding from the right and left? github.com/obadasemary/HomeScreenSwiftUIExample/blob/main/…Sukiyaki
R
10

Thank you @ChrisR this is a great way to achieve Carousel experience.

Added active index in the @ChrisR's answer, that might be useful for someone.

@ChrisR once you add active index in your answer, I can remove my post.

import SwiftUI

struct Item: Identifiable {
    var id: Int
    var title: String
    var color: Color
}

class Store: ObservableObject {
    @Published var items: [Item]
    
    let colors: [Color] = [.red, .orange, .blue, .teal, .mint, .green, .gray, .indigo, .black]
    
    // dummy data
    init() {
        items = []
        for i in 0...7 {
            let new = Item(id: i, title: "Item \(i)", color: colors[i])
            items.append(new)
        }
    }
}

struct ContentView: View {
    
    @StateObject var store = Store()
    @State private var snappedItem = 0.0
    @State private var draggingItem = 0.0
    @State var activeIndex: Int = 0
    
    var body: some View {
        
        ZStack {
            ForEach(store.items) { item in
                
                // article view
                ZStack {
                    RoundedRectangle(cornerRadius: 18)
                        .fill(item.color)
                    Text(item.title)
                        .padding()
                }
                .frame(width: 200, height: 200)
                
                .scaleEffect(1.0 - abs(distance(item.id)) * 0.2 )
                .opacity(1.0 - abs(distance(item.id)) * 0.3 )
                .offset(x: myXOffset(item.id), y: 0)
                .zIndex(1.0 - abs(distance(item.id)) * 0.1)
            }
        }
        .gesture(
            DragGesture()
                .onChanged { value in
                    draggingItem = snappedItem + value.translation.width / 100
                }
                .onEnded { value in
                    withAnimation {
                        draggingItem = snappedItem + value.predictedEndTranslation.width / 100
                        draggingItem = round(draggingItem).remainder(dividingBy: Double(store.items.count))
                        snappedItem = draggingItem
                        
                        //Get the active Item index
                        self.activeIndex = store.items.count + Int(draggingItem)
                        if self.activeIndex > store.items.count || Int(draggingItem) >= 0 {
                            self.activeIndex = Int(draggingItem)
                        }
                        print(self.activeIndex)
                    }
                }
        )
    }
    
    func distance(_ item: Int) -> Double {
        return (draggingItem - Double(item)).remainder(dividingBy: Double(store.items.count))
    }
    
    func myXOffset(_ item: Int) -> Double {
        let angle = Double.pi * 2 / Double(store.items.count) * distance(item)
        return sin(angle) * 200
    }
}
Rennin answered 7/1, 2023 at 17:5 Comment(2)
can we set activeIndex on load to show different item on first load?Undercoat
Hey @Anupam Mishra, In the beginning, thanks for additional info Now I have a Question: What if I want to do it in the Linear way inside the routing? like here camo.githubusercontent.com/…Sukiyaki

© 2022 - 2024 — McMap. All rights reserved.