Logo
Published on

Infinite Carousel IOS SwiftUI

Authors
  • Name
    Twitter

Hello guys in this tutorial I will teach you how we can create Infinite Carousal in SwiftUI. Let's start the work.

Create a brand new SwiftUI project from XCode and follow along with me.

Create a new file called CarousalView. In that file, we will create the carousal view and then use that in the ContentView.

Step 1

Create an enum DragState on the top of CarousalView file which will tell us whether the view is dragging or not. It has two computed properties.

  1. translation: If the drag is inactive then it will be zero otherwise it will be the translation that we will pass from DragGesture later on.
  2. isDragging: If DragState is inactive then it will be false otherwise will be true.

CarousalView1.swift:

import SwiftUI

enum DragState {
    case inactive
    case dragging(translation: CGSize)
    
    var translation: CGSize {
        switch self {
        case .inactive:
            return .zero
        case .dragging(let translation):
            return translation
        }
    }
    
    var isDragging: Bool {
        switch self {
        case .inactive:
            return false
        case .dragging:
            return true
        }
    }
}

Step 2

After that, create the following properties inside the CarousalView.

CarousalView2.swift:

@GestureState private var dragState = DragState.inactive
@State var carousalLocation = 0

// Parameters which we will pass when call the CarousalView 
var itemHeight: CGFloat
var views: [AnyView]
  1. dragState will control the state of the drag gesture
  2. carousalLocation is used to tell us which item is currently in the middle
  3. itemHeight and views are two parameters that we will give at the time of calling the CarousalView

Step 3

Create a function onDragEnded which will be called when the drag ends on the view which we will use later on.

This function will update the carousalLocation on the base of drag width which we will get from DragGesture.Value. If the width is greater than 200 then carousalLocation decreases by 1 otherwise will increase by 1.

CarousalView3.swift:

private func onDragEnded(drag: DragGesture.Value) {
    let dragThreshold: CGFloat = 200
    if drag.predictedEndTranslation.width > dragThreshold || drag.translation.width > dragThreshold {
        carousalLocation = carousalLocation - 1
    } else if (drag.predictedEndTranslation.width) < (-1 * dragThreshold) || (drag.translation.width) < (-1 * dragThreshold) {
        carousalLocation = carousalLocation + 1
    }
}

Step 4

Now create a function relativeLoc. This will give the relative location of the item when we drag the items all through the left or right and then restart from the first or last element respectively.

Suppose we have 7 elements. We drag them one by one to the left then we will get the 0, 1, 2, 3, 4, 5, and 6 values but when we drag it one time more to the left we will get the relative location again 0. That is the purpose of that function.

CarousalView4.swift:

func relativeLoc() -> Int {
  return ((views.count * 10000) + carousalLocation) % views.count
}

Step 5

Now create a function getOffset which will get the index as a parameter and give the offSet back of that element.

  1. If the index is equal to the relative location then the offset will be the width of the drag state
  2. If the index is equal to relative location plus 1 then the offset will be drag width plus (300 + 20) because 300 will be the width of every element and 20 will be the spacing between current element and next element.
  3. If the index is equal to relative location minus 1 then the offset will be drag width minus (300 +20) because 300 will be the width of every element and 20 will be the spacing between current element and previous element.

Here we are getting offset of middle, previous 3 and next 3 elements and the offset for remaining elements will be the 1000 and they will not show on to the screen.

CarousalView5.swift:

func getOffset(_ i: Int) -> CGFloat {
    if i == relativeLoc() {
        return dragState.translation.width
    } else if i == relativeLoc() + 1 || relativeLoc() == views.count - 1 && i == 0{
        return dragState.translation.width + (300 + 20)
    } else if i == relativeLoc() - 1 || relativeLoc() == 0 && i == views.count - 1 {
        return dragState.translation.width - (300 + 20)
    } else if i == relativeLoc() + 2 || (relativeLoc() == views.count - 1 && i == 1) || (relativeLoc() == views.count - 2 && i == 0) {
        return dragState.translation.width + (2*(300 + 20))
    } else if i == relativeLoc() - 2 || (relativeLoc() == 1 && i == views.count - 1) || (relativeLoc() == 0 && i == views.count - 2) {
        return dragState.translation.width - (2*(300 + 20))
    } else if i == relativeLoc() + 3 || (relativeLoc() == views.count - 1 && i == 2) || (relativeLoc() == views.count - 2 && i == 1) || (relativeLoc() == views.count - 3 && i == 0){
        return dragState.translation.width + (3*(300 + 20))
    } else if i == relativeLoc() - 3 || (relativeLoc() == 2 && i == views.count - 1) || (relativeLoc() == 1 && i == views.count - 2) || (relativeLoc() == 0 && i == views.count - 3) {
        return dragState.translation.width - (3*(300 + 20))
    } else {
        return 10000
    }
}

Step 6

Now create two functions getHeight and getOpacity. getHeight will return the height of every element and getOpacity will return opacity of every element.

Remember relativeLoc() is basically return the middle element index.

  1. So if the element is in the middle then the height will be the given height as itemHeight otherwise will be (itemHeight — 100)
  2. getOpacity function giving the opacity of current, previous 3 and next 3 elements to 1 and other elements opacity will be 0.

CarousalView6.swift:

func getHeight(_ i: Int) -> CGFloat {
    if i == relativeLoc(){
        return itemHeight
    }
    return itemHeight - 100
}

func getOpacity(_ i: Int) -> Double {
    if i == relativeLoc()
        || i + 1 == relativeLoc()
        || i - 1 == relativeLoc()
        || i + 2 == relativeLoc()
        || i - 2 == relativeLoc()
        || (i + 1) - views.count == relativeLoc()
        || (i - 1) - views.count == relativeLoc()
        || (i + 2) - views.count == relativeLoc()
        || (i - 2) - views.count == relativeLoc()
    {
        return 1
    }
    return 0
}

Step 7

Now create the ZStack and inside that ZStack use the ForEach to integrate the views into it. Show the views[i] into that ForEach.

  1. .frame is used for the size of the item
  2. .animation is used for a nice animation on the size of an item when dragging the carousel
  3. .background is used for the background color
  4. .cornerRadius is used for corner radius
  5. .opacity is used for opacity of the item
  6. .offset is used for positioning the item in x axis

CarousalView7.swift:

ZStack {
    ForEach(0..<views.count) { i in
         self.views[i]
            .frame(width: 300, height: getHeight(i))
            .animation(.interpolatingSpring(stiffness: 300, damping: 30, initialVelocity: 10))
            .background(.white)
            .cornerRadius(15)
            .opacity(getOpacity(i))
            .offset(x: getOffset(i))
            .animation(.interpolatingSpring(stiffness: 300, damping: 30, initialVelocity: 10))
    }
}
.gesture(
    DragGesture()
        .updating($dragState) { drag, state, transaction in
            state = .dragging(translation: drag.translation)
        }
        .onEnded(onDragEnded)
)

So after that, it is time to use that CaruosalView inside the ContentView. Are you excited? 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🥳🥳🥳🥳🥳🥳

Final Step

Add some pictures in assets and then create a little helper function imageView which will take the image name and return the View.

Now use CarousalView inside the ContentVew and pass itemHeight and views as following.

CarousalView8.swift:

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        CarousalView(
            itemHeight: 500,
            views: [
                AnyView(imageView(name: "Pexels1")),
                AnyView(imageView(name: "Pexels2")),
                AnyView(imageView(name: "Pexels3")),
                AnyView(imageView(name: "Pexels4")),
                AnyView(imageView(name: "Pexels5")),
                AnyView(imageView(name: "Pexels6")),
                AnyView(imageView(name: "Pexels7")),
                AnyView(imageView(name: "Pexels8"))
            ]
        )
    }
    
    func imageView(name: String) -> some View {
        Image(name)
            .aspectRatio(contentMode: .fill)
    }
}

Now it's time to run the code and test it. After running that code you will get the following output. 🚀 🚀 🚀 🚀 🚀 🚀 🚀 🚀 🚀

Complete Code:

CarousalView.swift:

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        CarousalView(itemHeight: 500, views: getViews())
    }
    
    func imageView(name: String) -> some View {
        Image(name)
            .aspectRatio(contentMode: .fill)
    }
    
    func getViews() -> [AnyView] {
        let images: [String] = [
            "Pexels1",
            "Pexels2",
            "Pexels3",
            "Pexels4",
            "Pexels5",
            "Pexels6",
            "Pexels7",
            "Pexels8",
        ]
        
        var views : [AnyView] = []
        
        for image in images {
            views.append(
                AnyView(
                    imageView(name: image)
                )
            )
        }
        
        return views
    }
}

enum DragState {
    case inactive
    case dragging(translation: CGSize)
    
    var translation: CGSize {
        switch self {
        case .inactive:
            return .zero
        case .dragging(let translation):
            return translation
        }
    }
    
    var isDragging: Bool {
        switch self {
        case .inactive:
            return false
        case .dragging:
            return true
        }
    }
}

struct CarousalView: View {
    
    @GestureState private var dragState = DragState.inactive
    @State var carousalLocation = 0
    
    var itemHeight: CGFloat
    var views: [AnyView]
    
    var body: some View {
        VStack {
            ZStack {
                ForEach(0..<views.count) { i in
                    VStack {
                        Spacer()
                        
                        self.views[i]
                            .frame(width: 300, height: getHeight(i))
                            .animation(.interpolatingSpring(stiffness: 300, damping: 30, initialVelocity: 10))
                            .background(.white)
                            .cornerRadius(15)
                            .opacity(getOpacity(i))
                            .offset(x: getOffset(i))
                            .animation(.interpolatingSpring(stiffness: 300, damping: 30, initialVelocity: 10))
                        
                        Spacer()
                    }
                }
            }
            .gesture(
                DragGesture()
                    .updating($dragState) { drag, state, transaction in
                        state = .dragging(translation: drag.translation)
                    }
                    .onEnded(onDragEnded)
            )
            
        }
    }
    
    private func onDragEnded(drag: DragGesture.Value) {
        let dragThreshold: CGFloat = 200
        if drag.predictedEndTranslation.width > dragThreshold || drag.translation.width > dragThreshold {
            carousalLocation = carousalLocation - 1
        } else if (drag.predictedEndTranslation.width) < (-1 * dragThreshold) || (drag.translation.width) < (-1 * dragThreshold) {
            carousalLocation = carousalLocation + 1
        }
    }
    
    func relativeLoc() -> Int {
        return ((views.count * 10000) + carousalLocation) % views.count
    }
    
    func getOffset(_ i: Int) -> CGFloat {
        if i == relativeLoc() {
            return dragState.translation.width
        } else if i == relativeLoc() + 1 || relativeLoc() == views.count - 1 && i == 0{
            return dragState.translation.width + (300 + 20)
        } else if i == relativeLoc() - 1 || relativeLoc() == 0 && i == views.count - 1 {
            return dragState.translation.width - (300 + 20)
        } else if i == relativeLoc() + 2 || (relativeLoc() == views.count - 1 && i == 1) || (relativeLoc() == views.count - 2 && i == 0) {
            return dragState.translation.width + (2*(300 + 20))
        } else if i == relativeLoc() - 2 || (relativeLoc() == 1 && i == views.count - 1) || (relativeLoc() == 0 && i == views.count - 2) {
            return dragState.translation.width - (2*(300 + 20))
        } else if i == relativeLoc() + 3 || (relativeLoc() == views.count - 1 && i == 2) || (relativeLoc() == views.count - 2 && i == 1) || (relativeLoc() == views.count - 3 && i == 0){
            return dragState.translation.width + (3*(300 + 20))
        } else if i == relativeLoc() - 3 || (relativeLoc() == 2 && i == views.count - 1) || (relativeLoc() == 1 && i == views.count - 2) || (relativeLoc() == 0 && i == views.count - 3) {
            return dragState.translation.width - (3*(300 + 20))
        } else {
            return 10000
        }
    }
    
    func getHeight(_ i: Int) -> CGFloat {
        if i == relativeLoc(){
            return itemHeight
        }
        return itemHeight - 100
    }
    
    func getOpacity(_ i: Int) -> Double {
        if i == relativeLoc()
            || i + 1 == relativeLoc()
            || i - 1 == relativeLoc()
            || i + 2 == relativeLoc()
            || i - 2 == relativeLoc()
            || (i + 1) - views.count == relativeLoc()
            || (i - 1) - views.count == relativeLoc()
            || (i + 2) - views.count == relativeLoc()
            || (i - 2) - views.count == relativeLoc()
        {
            return 1
        }
        return 0
    }
}

Hope you guys enjoy that artical. If you have any queries then let me know.


Reference

SwiftUI - Infinite Carousel (Inspired by the Health App) - 30 Minutes - Advanced