内容
定数個のView
を横に並べたカルーセルの記事はよく拝見しましたが、動的に前ページ、現在ページ、次ページを作り替えていくパターンはぱっと見つからなかったのでやってみました。
移動アニメーションが終わった後に、移動前のページを新しい現在ページとするにはアニメーション終了検知が必要ですが、自分のスキル的にまだ難しそうだったので、onDisappear
を利用して近いことを実現しました。
軽い頭の体操くらいの気持ちで見てもらえればと思います。自分はこれをやったことでState
と再描画の理解が深まりました。
開発環境
macOS Monterey v.12.5
Xcode v.13.4.1
主にiPhoneSE(3rd)(iOS15.5)のシミュレーターを利用
結果gifとコード
タップするごとに無限に1ページずつ減っていきます。
最初に用意しているのは5,6,7ページのみで、onTapGesture
を実装しているのは現在ページのみです。タップのたびに「横移動->終了後に1ページずらして各ページ再生成」をやってます。
実装の大きな流れは
- page5(前ページ), page6(現在ページ), page7(次ページ)を作って
HStack
で横に並べる - 現在ページの
onTapGesture
で@Stateのphase
が変更される- HStack全体のoffsetが更新されて前ページに移動する(ように見える。実際は全体が右に動く)
- これだけでは移動して前ページが画面範囲に入っただけで、現在ページという扱いではなく、タップで前ページに移動することはできない
- そこで
@Stateのphase
が変わるとonDisappear
が発火するようダミーViewを配置しておき、そこでページ生成元の@Stateのpages
を更新し、前ページの内容が現在ページになるよう再描画
- そこで
コードです↓
import SwiftUI
struct ContentView: View {
@State var phase:String = "init"
@State var pages:[Int] = [5,6,7]
var body: some View {
return (
HStack(spacing:0){
// prev page
VStack{
Text("page \(pages[0])")
Text("タップして前ページへ").foregroundColor(Color.blue)
}.frame(width: UIScreen.main.bounds.width)
// dummy
if phase=="init"{
// initではなくなった時にonDisappearを走らせるためのダミーView
ZStack{
Text("")
}.onDisappear{
// 移動完了と同じタイミングでstateを更新して、prev pageの内容をcurrent pageに割り当て直す
pages = [pages[0]-1, pages[1]-1, pages[2]-1]
phase = "init"
}
}
// current page
VStack{
Text("page \(pages[1])")
Text("タップして前ページへ")
.foregroundColor(Color.blue)
.onTapGesture{
withAnimation(.linear(duration: 1)){
phase = "tapped"
}
}
}.frame(width: UIScreen.main.bounds.width)
// next page
ZStack{
Text("page \(pages[2])")
Text("タップして前ページへ").foregroundColor(Color.blue)
}.frame(width: UIScreen.main.bounds.width)
}
.offset(x: phase=="init" ? 0 : UIScreen.main.bounds.width) // tappedに変わったときに全体を動かしてページ移動
)
}
}
おわり