はじめに
iOS26では、デザイン面で大きな変化がありました。
特にSwiftUIで提供されるコンポーネントのうち、TabView
やToolbar
などのナビゲーション系のコンポーネントが、デフォルトでLiquid Glassを用いたデザインとなります。
今回はtoolbarモディファイア
内の要素を、iOS26のAPIと組み合わせやすい方法で共通化します。
環境
Liquid Glassが表示可能なiOS26 beta2で動作確認をしています
beta版OSでの挙動ですので、RC版などとは挙動が異なる可能性があります。
TL;DR
- SwiftUIの
toolbarモディファイア
内に渡すToolbarItem
は、ToolbarContentプロトコル
を利用することでstructに切り出し、共通化することができる -
ToolbarContentプロトコル
に準拠したstruct内では、Viewプロトコル
内と同様に@State
による状態の管理が行える
これまでの実装とその問題点
私の個人開発アプリでは、以下のような仕様のボタンをtoolbarに配置していました
- タップすると、画面Aをsheetで表示する
- このボタンを複数の画面のtoolbarに配置する
各画面で毎回実装したくはないので、ToolbarSheetButtonModifier
を作って共通化していました。
/// navigationBarにsheetで開くボタンを配置するModifier
/// - parameters:
/// - placement: toolbar上のどの位置にボタンを配置するか
/// - buttonLabel: 表示するボタンの外観を設定する
/// - sheetContent: ボタンを押したときに開くシートの内容を設定する
struct ToolbarSheetButtonModifier<ButtonLabel: View, SheetContent: View>: ViewModifier {
@State private var isSheetPresented = false
private let placement: ToolbarItemPlacement
private let buttonLabel: ButtonLabel
private let sheetContent: SheetContent
init(
placement: ToolbarItemPlacement = .topBarTrailing,
@ViewBuilder buttonLabel: () -> ButtonLabel,
@ViewBuilder sheetContent: () -> SheetContent
) {
self.placement = placement
self.buttonLabel = buttonLabel()
self.sheetContent = sheetContent()
}
func body(content: Content) -> some View {
content
.toolbar {
ToolbarItem(placement: placement) {
Button(
action: { isSheetPresented = true },
label: { buttonLabel }
)
}
}
.sheet(isPresented: $isSheetPresented) { sheetContent }
}
}
extension View {
func toolbarSheetButton<ButtonLabel: View, SheetContent: View>(
placement: ToolbarItemPlacement = .topBarTrailing,
@ViewBuilder buttonLabel: () -> ButtonLabel,
@ViewBuilder sheetContent: () -> SheetContent
) -> some View {
self.modifier(
ToolbarSheetButtonModifier(
placement: placement,
buttonLabel: buttonLabel,
sheetContent: sheetContent
)
)
}
}
利用時には、このモディファイアをつけるだけでsheetを表示するボタンが作れます。
シート制御のためのstateも作る必要がありません。
他の要素を表示する場合は別途toolbar
モディファイアを使っても問題なく動作します
struct ContentView: View {
……
var body: some View {
Text("ContentView")
// モディファイアをつけるだけでtoolbarにsheetを表示するボタンを表示
.toolbarSheetButton(
buttonLabel: { … },
sheetContent: { … }
)
// 通常のtoolbarと同時に使うこともできます
.toolbar {
ToolbarItem(placement: .topBarLeading) { … }
}
}
……
}
しかし、Liquid Glassをベースとしたデザインに移行する際、この方法では柔軟な表示ができなくなりました
きっかけは、iOS26から、ToolbarSpacerというAPIが導入されたことです。
Liquid Glassは液体のような表現を伴うため、隣接するToolbarItem
同士が一つの大きなまとまりを形成するような表現のUIを作ります。
そして、ToolbarSpacer
という新たなAPIを利用することで、2つのToolbarItem
を別のまとまりへと明示的に区切りることができます。
このAPIは単一のtoolbarモディファイア
内の要素を区切るために利用しますが、toolbarの内容をモディファイアを使って共通化すると、toolbar
のスコープが複数できてしまうため、うまく要素の分離ができません
ToolbarItemをViewに切り出すことを試みる
ToolbarSpacer
の登場で、toolbarの中身を共通化するときにはtoolbarモディファイア
単位ではなく、ToolbarItem
単位での切り出しが必要になりました
しかし、ToolbarItemは、Viewと同様の方法では切り出しができません
例えば、以下のようなコードはコンパイルエラーになります
/// Static method 'buildExpression' requires that 'ToolbarItem<(), Image>' conform to 'View'
struct SampleToolbarItem: View {
var body: some View {
ToolbarItem(placement: .primaryAction) {
Image(systemName: "sun.max")
}
}
}
困りました。どうやら、ToolbarItem
はViewに準拠していないようです。
ToolbarItemはToolbarContentに切り出す
調べてみると、ToolbarItem
はToolbarContent
というプロトコルを利用することで共通化可能なようです。
/// ViewではなくToolbarContentに準拠する必要がある
struct SampleToolbarItem: ToolbarContent {
var body: some ToolbarContent {
ToolbarItem(placement: .primaryAction) {
Image(systemName: "sun.max")
}
}
}
また、この構造体内では@State
による状態管理がView構造体と同様に可能なようでした。
以下のコードで、toolbarのボタン押下時にsheetが表示されます
struct SampleToolbarItem: ToolbarContent {
@State var isSheetShown = false
var body: some ToolbarContent {
ToolbarItem(placement: .primaryAction) {
Button(
action: { isSheetShown.toggle() },
label: { Image(systemName: "sun.max") }
)
.sheet(isPresented: $isSheetShown) {
Text("sheetを表示")
}
}
}
}
struct ContentView: View {
……
var body: some View {
Text("ContentView")
.toolbar {
SampleToolbarItem()
ToolbarSpacer(.fixed, placement: .primaryAction)
ToolbarItem(placement: .primaryAction) { … }
}
}
……
これで、ToolbarSpacer
を利用しつつ、toolbar
内のボタンを共通化することができそうです。
まとめ
-
ToolbarSpacer
の登場で、1つのViewに複数のtoolbarモディファイア
を付ける方法では、柔軟なViewの実装がしづらくなりました -
ToolbarContentプロトコル
に準拠した構造体を作ることで、ToolbarItem
単位での共通化を行うことができます -
ToolbarContent
に準拠した構造体内では、@State
を利用した状態管理が可能です