システム開発<基本編>SwiftUIで画像を自動的にスライド・循環するカルーセルアプリ

Swift

前回は、SwiftUIで、写真などの画像スワイプ(スライド)させて移動させたり、

画像ピンチイン・ピンチアウト(縮小・拡大)するアプリケーションを開発する

記事を紹介しました。

 

 

今回は、SwiftUIで、回転木馬(メリーゴーランド)のように画像自動スライド・循環する

カルーセルアプリを開発する記事を紹介しようと思います。

SwiftUIで画像を自動的にスライド・循環するカルーセルアプリ

 

 

習得内容

今回、SwiftUIで、回転木馬(メリーゴーランド)のように画像自動スライド・循環する

カルーセルアプリを作成する事で、下記の内容を習得する事が出来ます。

  • TabView複数子Veiwを切り替えることで、画像カルーセルを実現します。
  • NavigationStack3種類画面遷移から、配列コントロールした遷移を実現します。
  • GeometryReader色(RGB)スライダ調整アプリでも利用しましたが、画面サイズを取得する為に利用します。部品位置大きさを、相対的に設定する時に便利です。
  • onTapGesture押した(タップした)時のアクションを実行します。
  • Timer.publish指定された間隔で、現在の時刻発行(発火)してくれるPublisher になります。
  • onReceivePublisher によって、発行(発火)されたデータを検出した時に実行します。

 

 

動作環境

今回も、アプリが動作する環境は下記になります。

  • mac OS:Sequoia 15.6.1
  • iOS:26.0
  • Xcode:26.0.1
  • Swift:6.2.1

 

 

NavigationStack

NavigationStack については、前々回の記事で詳しく説明しましたので、今回は、

詳細説明省略させて頂きます。

 

NavigationStack → CarouselView

NavigationStack で、配列コントロールを実現する為に、Viewを作成して行きます。

  1. CarouselView回転木馬(メリーゴーランド)のように画像自動スライド・循環を実現します。

 

Xcode の上部タブメニューより、File → New → File from Template を選択します。

 

SwiftUI View を選択します。

 

名称は、CarouselView を入力してみます。

 

Create ボタンを押して、CarouselView.swift を作成します。

 

ContentView:メインメニュー(ランチャー)

前回の記事で作成したメインメニュー(ランチャー)ContentView を確認します。

 


import SwiftUI

struct ContentView: View {

    @State var isPopupView_1 = false
    @State var isPopupView_2 = false
    @State var isPopupView_3 = false
    @State var isPopupView_4 = false
    @State var isPopupView_5 = false

    var body: some View {
        
        VStack{
            Button {
                self.isPopupView_1.toggle()
            } label: {
                Text("QRCodeView")
                    .font(.title2)
            }
            .fullScreenCover(isPresented: $isPopupView_1, content: {
                QRCodeView(isPopupView_1: $isPopupView_1)
            })
            .padding()

            Button {
                self.isPopupView_2.toggle()
            } label: {
                Text("ColorPickerView")
                    .font(.title2)
            }
            .sheet(isPresented: $isPopupView_2, content: {
                ColorPickerView(isPopupView_2: $isPopupView_2)
            })
            .padding()

            Button {
                self.isPopupView_3.toggle()
            } label: {
                Text("ColorSliderRGBView")
                    .font(.title2)
            }
            .sheet(isPresented: $isPopupView_3, content: {
                ColorSliderRGBView(isPopupView_3: $isPopupView_3)
            })
            .padding()

            Button {
                self.isPopupView_4.toggle()
                
            } label: {
                Text("ColorSliderHSBView")
                    .font(.title2)
            }
            .sheet(isPresented: $isPopupView_4, content: {
                ColorSliderHSBView(isPopupView_4: $isPopupView_4)
            })
            .padding()

            Button {
                self.isPopupView_5.toggle()
                
            } label: {
                Text("NavigationListView")
                    .font(.title2)
            }
            .sheet(isPresented: $isPopupView_5, content: {
                NavigationListView(isPopupView_5: $isPopupView_5)
            })
            .padding()
        }
    }
}

#Preview {
    ContentView()
}

 

メインメニュー(ランチャー)は、以下の内容です。

  • QRCodeView
  • ColorPickerView
  • ColorSliderRGBView
  • ColorSliderHSBView
  • NavigationListView

今回は、メインメニュー(ランチャー)ContentView に変更は有りませんので、

このまま利用して行きます。

 

NavigationListView:配列コントロール

NavigationListVeiw のソースに、追記・修正をして行きます。

 

ソース例:NavigationListVeiw(配列コントロール)

前回の記事で、配列コントロール向けとして作成した NavigationListVeiw のソースを

確認してみます。

 


import SwiftUI


struct NavigationListView: View {

    @Binding var isPopupView_5: Bool
    @State var path: [String]=[]

    var navi_lists = [
        "SwipeView",
        "CarouselView",
        "TabVeiw",
        "TabBarView"
    ]

    var body: some View {
        VStack {

            NavigationStack(path: $path) {

                List(navi_lists, id: \.self) { navi_list in

                    NavigationLink(navi_list, value: navi_list)
                        .font(.system(size: 22))
                }
                .navigationDestination(for: String.self) { navi_list in

                    switch (navi_list) {

                    case "SwipeView":
                        SwipeView(path: $path)
                            .navigationTitle(navi_list)
                            .navigationBarTitleDisplayMode(.inline)

                    case "SwipeDetailView":
                        SwipeDetailView(path: $path)
                            .navigationTitle(navi_list)
                            .navigationBarTitleDisplayMode(.inline)

                    case "CarouselView":
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.pink)
                            .padding()

                    case "TabVeiw":
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.green)
                            .padding()

                    case "TabBarView":
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.orange)
                            .padding()

                    default:
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.blue)
                            .padding()
                    }
                }
                .navigationTitle("NavigationList")
                .font(.system(size: 22))
                .foregroundColor(.purple)
            }
        }

        Button(action: {
            withAnimation {
                self.isPopupView_5.toggle()
            }
        }, label: {
            Text("Close")
                .font(.title2)
                .foregroundColor(.black)
        })
        .padding()
    }


}

#Preview {
    NavigationListView(isPopupView_5: .constant(false))
}

 

ソース例:NavigationListVeiw(配列コントロール:追加・修正版)

NavigationListVeiw のソースに、追記・修正をしたソースを紹介します。

 


import SwiftUI


struct NavigationListView: View {

    @Binding var isPopupView_5: Bool
    @State var path: [String]=[]

    var navi_lists = [
        "SwipeView",
        "CarouselView",
        "TabVeiw",
        "TabBarView"
    ]

    var body: some View {
        VStack {

            NavigationStack(path: $path) {

                List(navi_lists, id: \.self) { navi_list in

                    NavigationLink(navi_list, value: navi_list)
                        .font(.system(size: 22))
                }
                .navigationDestination(for: String.self) { navi_list in

                    switch (navi_list) {

                    case "SwipeView":
                        SwipeView(path: $path)
                            .navigationTitle(navi_list)
                            .navigationBarTitleDisplayMode(.inline)

                    case "SwipeDetailView":
                        SwipeDetailView(path: $path)
                            .navigationTitle(navi_list)
                            .navigationBarTitleDisplayMode(.inline)

                    case "CarouselView":
                        CarouselView(path: $path)
                            .navigationTitle(navi_list)
                            .navigationBarTitleDisplayMode(.inline)

                    case "TabVeiw":
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.green)
                            .padding()

                    case "TabBarView":
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.orange)
                            .padding()

                    default:
                        Text(navi_list)
                            .font(.system(size: 22))
                            .foregroundColor(.blue)
                            .padding()
                    }
                }
                .navigationTitle("NavigationList")
                .font(.system(size: 22))
                .foregroundColor(.purple)
            }
        }

        Button(action: {
            withAnimation {
                self.isPopupView_5.toggle()
            }
        }, label: {
            Text("Close")
                .font(.title2)
                .foregroundColor(.black)
        })
        .padding()
    }

}

#Preview {
    NavigationListView(isPopupView_5: .constant(false))
}

追記・修正をした部分を説明します。

  • CarouselView(path: $path)CarouselView に、path の引数を渡しています。

 

CarouselView

CarouselView のソースを紹介して行きますが、事前に、Viewで利用する画像(写真)

Xcode に追加して行きます。

 

画像(写真)の追加

著作権肖像権考慮して、今回も、筆者の家で飼っている写真を利用します。

Finder から、好きな写真ドラッグ&ドロップします。

画像(写真)は、著作権肖像権問題が無いものを、各環境に合わせて、投入して下さい。

 

今回も、6枚写真ドラッグ&ドロップしてみました。

もし、画像(写真)名称をそのまま利用したく無い場合は、Assets 内で名称

変更する事も出来ますので、試してみて下さい。

これで、SwiftUIのソースから直接、画像(写真)を呼び出して利用する事が出来ます。

 

 

ソース例:CarouselView

NavigationStack から選択して、回転木馬(メリーゴーランド)のように画像自動

スライド・循環するカルーセルアプリを実現する CarouselView のソースを紹介します。

 


import SwiftUI

struct CarouselView: View {
    @Binding var path: [String]
    @State private var currentIndex: Int = 0

    private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()

    var imageNames: [String] = [
        "IMG_0088",
        "IMG_0148",
        "IMG_0234",
        "IMG_0096",
        "IMG_0090",
        "IMG_0976",
    ]

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Color.white
                    .edgesIgnoringSafeArea(.all)

                let geo_width: CGFloat = geometry.size.width
                let geo_height: CGFloat = geometry.size.height

                ZStack {
                    TabView(selection: $currentIndex) {
                        ForEach(0..<imageNames.count, id: \.self) { imageIndex in VStack { Text(imageNames[imageIndex]) .font(.system(size: 20)) .foregroundColor(.purple) .tag(imageIndex) Image(imageNames[imageIndex]) .resizable() .scaledToFill() .tag(imageIndex) } } } .tabViewStyle(PageTabViewStyle()) .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always)) Image(systemName: "arrowshape.backward.circle") .font(.system(size: 30)) .foregroundColor(.gray) .position(x: geo_width * 0.05, y: geo_height * 0.5) .onTapGesture { withAnimation { if currentIndex > 0 && currentIndex <= imageNames.count - 1 { currentIndex = currentIndex - 1 } else { currentIndex = imageNames.count - 1 } } } Image(systemName: "arrowshape.forward.circle") .font(.system(size: 30)) .foregroundColor(.gray) .position(x: geo_width * 0.95, y: geo_height * 0.5) .onTapGesture { withAnimation { if currentIndex >= 0 && currentIndex < imageNames.count - 1 {
                                    currentIndex = currentIndex + 1
                                } else {
                                    currentIndex = 0
                                }
                            }
                        }
                }
                .onReceive(timer) { value in
                    withAnimation {
                        currentIndex = (currentIndex + 1) % imageNames.count
                    }
                }
            }
        }
    }
}

#Preview {
    CarouselView(path: .constant(["CarouselView"]))
}

  • private let timer = Timer.publish(every: 3, on: .main, in .common).autoconnect()3秒毎に、mainスレッドcommonモードで、イベント発行(発火?)する宣言をしています。ゲームアプリでは無いので、カルーセル速過ぎると、見ずらくなるので、3秒以上の間隔で設定する事をお勧めします。
  • var imageNames: [String] = [“IMG_0088″,”IMG_0148”,…]写真配列で宣言しています。
  • GeometryReader { geometry in画面サイズを取得する為に利用します。
  • let geo_width: CGFloat = geometry.size.width画面を、geo_width に代入しています。
  • let geo_height: CGFloat = geometry.size.height画面高さを、geo_height に代入しています。
  • TabView(selection: $currentIndex) {カルーセルを実現する為に、TabView を設定します。
  • ForEach(0..<imageNames.count, id: \.self) { imageIndex in写真配列を、ForEach で繰り替えして表示します。
  • Text(imageNames[imageIndex])写真名称を設定しています。
  • Image(imageNames[imageIndex])写真画像を設定しています。

 

  • Image(systemName:arrowshape.backward.circle):下記の SF Symbols で、arrowshape.backward.circleアイコンを設定しています。
  • .position(x: geo_width * 0.05, y: geo_height * 0.5):ZStack で、画像(写真)左側に重ねて設定しています。

  • .onTapGesture {arrowshape.backward.circleアイコン押した(タップした)時のアクションを実行します。
  • if currentIndex > 0 && currentIndex <= imageNames.count – 1 {currentIndex 0 より大きく 且つ 画像(写真)総数より 1減算した値以下の場合
  • currentIndex = currentIndex – 1currentIndex1減算させる。
  • else { currentIndex = imageNames.count – 1:currentIndex に、画像(写真)総数より 1減算した値を代入しています。

 

  • Image(systemName:arrowshape.forward.circle):下記の SF Symbols で、arrowshape.forward.circleアイコンを設定しています。
  • .position(x: geo_width * 0.95, y: geo_height * 0.5):ZStack で、画像(写真)右側に重ねて設定しています。

  • .onTapGesture {arrowshape.forward.circleアイコン押した(タップした)時のアクションを実行します。
  • if currentIndex >= 0 && currentIndex < imageNames.count – 1 {currentIndex 0 以上 且つ 画像(写真)総数より 1減算した値未満の場合
  • currentIndex = currentIndex + 1currentIndex1増加させています。
  • else { currentIndex = 0currentIndex 0 を代入しています。
  • .onReceive(timer) { value intimer.publish で、3秒毎mainスレッドcommonモードで、イベント発行(発火?)しています。
  • currentIndex = (currentIndex + 1) % imageNames.countcurrentIndex に、currentIndex1増加させた値から、画像(写真)総数除算した余りを代入しています。

 

Timer.publish:現在の時刻を発行(発火)

Timer.publish は、指定された間隔で、現在の時刻繰り返し発行(発火)してくれる

Publisher です。

 

Timer.publish 引数は、下記になります。

  • everyイベント発行(発火)間隔を指定します。
  • tolerance:設定すると、Timer発行(発火)タイミング遅らせる事が出来ます。初期値はnilです。
  • onRunLoopを実行するスレッドになります。
  • inRunLoop.Modeの設定をします。
  • options:実行するループスケジューラー任意オプションになります。

 

Timer.publish については、下記のサイトを参考にしています。

 

 

TabVeiw:前回のおさらい

TabView とは、複数子View 切り替えることのできる View です。

SwiftUIVeiw は、アプリの画面に表示されるものを指すので、画面画像だけではなく、

ボタン文字(テキスト)なども切り替えて、表示することが出来ます。

表示方法は、TabView {} の中に、View を入れる形になります。

上記のソースのように、TabView { Text(”…”)  Image(”…”) } の要領です。

Tabですが、最大5つまで表示させる事が出来ます。Tab6つ以上になると、More という

リストTabになり、5つ目以降Tabが格納されます。

 

 

アプリ実行:シミュレータ起動

では、実際にアプリを実行してみます。

Xcode の上部タブメニューより、Product → Run を選択するか、command + R

押してみて下さい。

 

エラーが無ければ、iPhoneシミュレータが起動します。

 

ContntVeiw(メインメニュー)が表示されました。

 

NavigationListView:ナビゲーション画面

続けて、NavigationListViewボタン押してみます(タップします)

 

ContntVeiw(メインメニュー)の上に NavigationListVeiw(ナビゲーション機能)

重なって、表示されたました。

 

NavigationList の内容は、下記の通りで変更はありません。

  • SwipeView
  • CarouselView
  • TabVeiw
  • TabBarView

現在の 配列(path の状態は、下記になります。

  • path = []

 

CarouselView:カルーセル

次に、NavigationList 2段目CarouselView を押してみます(タップします)

 

 

前回の SwipeDetaillView に似た画面になりましたが、両端矢印(←,→)ボタン

表示されている事や、画像の下には、NavigationListVeiw ボタンが無いが

確認出来ると思います。

画面上部には、CarouselView のタイトルが表示されていますが、

NavigationListVeiw で、タイトルを表示するように設定していました。

 

現在の 配列(path の状態は、下記になりますが、今回は、CarouselView より

深い階層View は有りません。

  • path = [CarouselView]

 

暫くすると、下記の2枚目画像自動的に遷移します。

 

画像下側には、点々「…」が表示されていて、白〇印の現在地は、

左から2枚目画像である事が確認出来ます。

 

暫くすると、下記の3枚目画像自動的に遷移します。

 

画像下側には、点々「…」が表示されていて、白〇印の現在地は、

左から3枚目画像である事が確認出来ます。

 

また暫くすると、下記の4枚目画像自動的に遷移します。

 

画像下側には、点々「…」が表示されていて、白〇印の現在地は、

左から4枚目画像である事が確認出来ます。

 

また暫くすると、下記の5枚目画像自動的に遷移します。

 

画像下側には、点々「…」が表示されていて、白〇印の現在地は、

左から5枚目画像である事が確認出来ます。

 

また暫くすると、下記の6枚目画像自動的に遷移します。

 

画像下側には、点々「…」が表示されていて、白〇印の現在地は、

左から6枚目(右端)画像である事が確認出来ます。

 

また暫くすると、今度は1枚目画像自動的に遷移します。

 

画像下側には、点々「…」が表示されていて、白〇印の現在地は、

左から1枚目画像である事が確認出来ます。

 

 

CarouselView:スワイプ(スライド)&ボタン遷移(手動)

CarouselView は、画像自動スライド・循環しますが、スワイプ(スライド)

実現しますので、試してみて下さい。

 

スワイプ(スライド)しても、カルーセル自体は停止しませんので、

暫くすると、次の画像自動的に遷移します。

 

また、下記のうように画面両端に、矢印(←,→)ボタンが表示されていますので、

オレンジ枠矢印(←)ボタン押して(タップして)みて下さい。

 

 

3枚目画像に、手動で遷移する事が確認出来ると思います。

 

 

今度は反対に、オレンジ枠矢印(→)ボタン押して(タップして)みて下さい。

 

 

4枚目画像に、手動で遷移する事が確認出来ると思います。

 

 

暫くすると、5枚目画像に遷移します。

 

矢印(←,→)ボタン押して(タップして)も、カルーセル自体は停止しませんので、

暫くすると、次の画像自動的に遷移します。

 

まとめ

今回は、SwiftUIで、回転木馬(メリーゴーランド)のように画像自動スライド・循環する

カルーセルアプリの開発記事を紹介しました。

 

次回は、SwiftUIで、ベーシックTabView画像回転させるアプリケーション

開発する記事を紹介しようと思います。

 

コメント

タイトルとURLをコピーしました