前回は、SwiftUIで、写真などの画像をスワイプ(スライド)させて移動させたり、
画像をピンチイン・ピンチアウト(縮小・拡大)するアプリケーションを開発する
記事を紹介しました。
今回は、SwiftUIで、回転木馬(メリーゴーランド)のように画像を自動でスライド・循環する
カルーセルアプリを開発する記事を紹介しようと思います。
習得内容
今回、SwiftUIで、回転木馬(メリーゴーランド)のように画像を自動でスライド・循環する
カルーセルアプリを作成する事で、下記の内容を習得する事が出来ます。
- TabView:複数の子Veiwを切り替えることで、画像のカルーセルを実現します。
- NavigationStack:3種類の画面遷移から、配列コントロールした遷移を実現します。
- GeometryReader:色(RGB)スライダ調整アプリでも利用しましたが、画面サイズを取得する為に利用します。部品の位置や大きさを、相対的に設定する時に便利です。
- onTapGesture:押した(タップした)時のアクションを実行します。
- Timer.publish:指定された間隔で、現在の時刻を発行(発火)してくれるPublisher になります。
- onReceive:Publisher によって、発行(発火)されたデータを検出した時に実行します。
動作環境
今回も、アプリが動作する環境は下記になります。
- mac OS:Sequoia 15.6.1
- iOS:26.0
- Xcode:26.0.1
- Swift:6.2.1
NavigationStack
NavigationStack については、前々回の記事で詳しく説明しましたので、今回は、
詳細説明を省略させて頂きます。
NavigationStack → CarouselView
NavigationStack で、配列コントロールを実現する為に、Viewを作成して行きます。
- 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 – 1:currentIndex を1つ減算させる。
- 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 + 1:currentIndex を1つ増加させています。
- else { currentIndex = 0:currentIndex に 0 を代入しています。
- .onReceive(timer) { value in:timer.publish で、3秒毎にmainスレッドのcommonモードで、イベントを発行(発火?)しています。
- currentIndex = (currentIndex + 1) % imageNames.count:currentIndex に、currentIndex を1つ増加させた値から、画像(写真)の総数を除算した余りを代入しています。
Timer.publish:現在の時刻を発行(発火)
Timer.publish は、指定された間隔で、現在の時刻を繰り返し発行(発火)してくれる
Publisher です。
Timer.publish の引数は、下記になります。
- every:イベントの発行(発火)間隔を指定します。
- tolerance:設定すると、Timerは発行(発火)のタイミングを遅らせる事が出来ます。初期値はnilです。
- on:RunLoopを実行するスレッドになります。
- in:RunLoop.Modeの設定をします。
- options:実行するループスケジューラーの任意オプションになります。
Timer.publish については、下記のサイトを参考にしています。
TabVeiw:前回のおさらい
TabView とは、複数の 子View を切り替えることのできる View です。
SwiftUI の Veiw は、アプリの画面に表示されるものを指すので、画面や画像だけではなく、
ボタンや文字(テキスト)なども切り替えて、表示することが出来ます。
表示方法は、TabView {} の中に、View を入れる形になります。
上記のソースのように、TabView { Text(”…”) Image(”…”) } の要領です。
Tabですが、最大5つまで表示させる事が出来ます。Tabが6つ以上になると、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と画像を回転させるアプリケーションを
開発する記事を紹介しようと思います。
- イラスト:いらすとや より引用


























コメント