のっけから宣伝になりますが、個人的にアプリをリリースしております。
Apple Musicの1億曲のライブラリー全てからランダムで音楽を再生するというアプリです。
UIはSwiftUIで実装しており、基本的な音楽プレイヤーの機能は一通り備えています。
このうちシークバーの部分は自前で実装しています。
Slider | Apple Developer Documentation
SwiftUIには標準でスライダーバーが用意されていますが、デザインのカスタマイズが今ひとつ不自由だったので自前の実装にしました。
シークバーは当初以下のように実装していました。
struct SeekBar<V>: View where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint {
// 〜中略〜
/// 初期化
/// - Parameters:
/// - value: 値
/// - bounds: 範囲
/// - onEditingChanged: ドラッグ中のイベント
init(value: Binding<V>, in bounds: Binding<ClosedRange<V>>, onEditingChanged: ((Bool) -> Void)?) {
// 〜中略〜
/// ドラッグのハンドラ
private var drag: some Gesture {
DragGesture()
.onChanged {
onEditingChanged?(dragHandler($0))
}
.onEnded {
onEditingChanged?(false)
}
}
使う際は以下のようになります。
SeekBar(value: $playbackTime, in: $playbackRange) {
// ドラッグ中のイベント
}
これはSwiftUI標準のスライダーバーに合わせたものです。
init(value: in:onEditingChanged:) | Apple Developer Documentation
スライダーをいじっている時のイベントハンドリングはクロージャ(onEditingChanged:
)で行うようになっています。
ただ、ふと、DragGesture().onChanged
のように、メソッドチェーンでのイベントハンドリングを実装したことがない、というのが気になりました。
公式がクロージャだし、今の実装で事足りるのですが、気になってしまったので試してみることにします。
真っ先に思いついたのはViewModifier
ですが、func body(content: Self.Content) -> Self.Body
を実装する必要があり、クロージャを格納すれば事足りる今回の用途では少し大袈裟です。
またcontent
がイミュータブルのため、プロパティーの変更ができません。
@State
なども試しましたが、クロージャの状態を管理する必要は全くないので、少し違う気がします。
そんなふうに色々試し、色々調べた結果、結論を言ってしまえば、以下のページに答えがありました。
func myCustomTapHandler(onAction: @escaping () -> Void) -> Self {
var view = self
view.action = onAction
return view
}
自身をミュータブルコピーして、プロパティーを変更し、コピーを返す。
・・・なるほど・・・。
自分の頭の中では、ビューそのものを操作しないといけないと思い込んでいたのですが、SwiftUIはstructベースで、ビューの再作成が繰り返される前提のフレームワークなので、元のビューを返す必要がないんですね。
@Binding
とかはどうなるんだろうと思い、調べてみましたが、
上記ページの「@Stateと@Bindingの違い」の項を見る限り、問題なさそうです。
今回の結果を受けて作り直したシークバーが以下になります。
以前はかなり決めうちでデザインを設定していたのですが、イベントハンドラの件に合わせて、デザインもメソッドチェーンで変更できるようにしました。
複数のビューに共通の変更が必要でない限りは、ViewModifier
を使う必要はないですね。
これで使いまわせるようになったと思います。