はじめに
SwiftUIのsheetを閉じるとき、sheetが完全に閉じきるまで、次のsheetを開くことができません。
再度開くには、前のsheetが完全に閉じてからもう一度開くボタンを押す必要があるため、せっかちなユーザーはうんざりするかもしれません。
また、タイミングによっては、sheetが自動で無限に開閉を繰り返してしまうというバグも発生するらしいです(そうなったらAppをkillするしかないとか)(手元では再現できず…)
こんな(ニッチな?)問題を解決するためのpropertyWrapperを実装してみました。
実際の動作
sheetが完全に閉じる前にOpenボタンを押すと、閉じた瞬間に自動で次のsheetが開きます。
実装
中身
以下をコピペすれば使えます。
@propertyWrapper
struct SheetItemState<Item: Identifiable>: DynamicProperty {
private struct Wrapper<Item: Identifiable> {
var item: Item
private let uuid = UUID()
}
@State private var currentItem: Wrapper<Item>?
@State private var nextItem: Wrapper<Item>?
var wrappedValue: Item? {
get {
currentItem?.item
}
nonmutating set {
if let newItem = newValue.map(Wrapper.init) { // 開くボタンを押したとき
if currentItem == nil { // sheetを開いていないとき
if nextItem == nil {
currentItem = newItem
} else {
currentItem = nextItem
nextItem = newItem
}
} else { // sheetが閉じきっていないとき
if nextItem == nil {
nextItem = newItem
}
}
} else { // sheetが完全に閉じたとき
currentItem = nextItem
nextItem = nil
}
}
}
var projectedValue: Binding<Item?> {
Binding {
wrappedValue
} set: { newValue in
wrappedValue = newValue
}
}
}
使い方
普段 @State
しているところを @SheetItemState
にするだけです。
基本的には普通のStateと同様に扱えます(子ViewのBinding引数として $selectedItem
を渡すなども可)。
ただし、sheetのitem以外の使い方をすると意図しない挙動を示す可能性があるので、注意してください。
struct ContentView: View {
// @State var selectedItem: Item?
// から
@SheetItemState var selectedItem: Item?
// に変更
var body: some View {
Button("Open") {
selectedItem = Item(id: 0)
}
.sheet(item: $selectedItem) { item in
Text("\(item.id)")
}
}
}
※ Item
は Identifiable
protocolに準拠している必要があります( sheet(item:content)
がそういうものなので)
解説
currentItem
と nextItem
sheetを閉じるとき、 selectedItem
が nil
になるのは、sheetが完全に閉じた瞬間です。
つまり、sheetが閉じきる前に selectedItem
の中身をセットしても、閉じ切った瞬間 nil
になってしまい、次のsheetを開くことができないのです。
これを解決するため、バッファを用意しました。
すなわち、今sheetに使われている currentItem
とは別で、次に使うべき nextItem
を内部で保持するようにしました。
currentItem
が存在するときにボタンを押すと、新しいitemは一旦 nextItem
に格納され、sheetを閉じることで currentItem
が nil
になったタイミングで、nextItem
が currentItem
に送られます。
Wrapper
上記だけだと、sheetが閉じきる前にセットされるitemが同じ場合、閉じきったときに selectedItem
が nil
にならず、残り続けることになります。
こうなってしまうと、他のitemをselectするまで、一生sheetを開くことができません。
具体的には、 currentItem
と nextItem
の中身が同じ場合、以下の部分で currentItem
を更新できない(モノが変わらない)ことになります。
} else { // sheetが完全に閉じたとき
currentItem = nextItem
nextItem = nil
}
SwiftUIが「sheetを開く」という動作をしてくれるのは selectedItem
の変更を検知したときなので、更新できなければsheetは開かれないのです。
これを解決するため、UUIDを持つ Wrapper
を用いることにしました。
itemをセットするたび新たな Wrapper
インスタンスを生成することで、同じ内容の currentItem
と nextItem
であってもUUIDが異なるようにし、無事セットされたタイミングによってこれらを区別できるようになりました。
あとがき
sheetが完全に閉じる前に開くボタンを押した際に、前のsheetが閉じきった瞬間自動で次のsheetが開くような実装をしてみました。
せっかちなユーザーへの対策としては、sheetが完全に閉じるまでボタンを無効にするなどの策も考えられますが、次のsheetが自動で開くのが望ましい場合もあると思います(たぶん)。
自分で使う機会があるかは謎ですが、propertyWrapperの勉強になったので良かったです。
もっといい実装方法やバグがあれば、コメントで教えていただけると助かります!