🦋
SwiftUI: ViewのObservableの関数をSceneで呼ぶ方法
macOSだとScene
に.commands()
をつけることでメニューバーのコマンドを追加したり上書きしたりできます。しかし、コマンドで実際に動かす処理はScene
ではなくView
やそのObservable class
に定義されているのが一般的だと思います。そのため、Scene
からView
の関数を呼ぶ手段が欲しくなります。
struct SomeView: View {
@State var isPresented = false
var body: some View {
Button("Import") {
isPresented = true
}
.fileImporter(
isPresented: $isPresented,
allowedContentTypes: [UTType.image],
onCompletion: { _ in //読み込み処理 }
)
}
}
struct SomeApp: App {
var body: some Scene {
WindowGroup {
SomeView()
}
.commands {
CommandGroup(before: .newItem) {
Button {
// ここでSomeViewのButton("Import")を押した時と同じ処理をしたい
} label: {
Text("Import")
}
}
}
}
}
そんな時はFocusedValueKey
とFocusedValues
、.focusedSceneValue()
を使えば関数を外から叩けます。
まずはSomeView
の関数を伝搬させるための構造体を定義します。
そして、その構造体にFocusedValuesからアクセスできるように下準備をします。
struct SomeViewAction {
var importAction: @MainActor @Sendable () -> Void
}
struct SomeViewActionKey: FocusedValueKey {
typealias Value = SomeViewAction
}
extension FocusedValues {
var someViewAction: SomeViewAction? {
get { self[SomeViewActionKey.self] }
set { self[SomeViewActionKey.self] = newValue }
}
}
次に、実際にSomeView
側でfocusedSceneValue()
を使ってSomeViewAction
を渡して、Scene
の方では@FocusedValue
で受け取ります。
struct SomeView: View {
@State var isPresented = false
var body: some View {
Button("Import") {
isPresented = true
}
.focusedSceneValue(\.someViewAction, SomeViewAction(
importAction: { isPresented = true }
))
.fileImporter(
isPresented: $isPresented,
allowedContentTypes: [UTType.image],
onCompletion: { _ in //読み込み処理 }
)
}
}
struct SomeApp: App {
@FocusedValue(\.someViewAction) private var someViewAction
var body: some Scene {
WindowGroup {
SomeView()
}
.commands {
CommandGroup(before: .newItem) {
Button {
someViewAction?.importAction()
} label: {
Text("Import")
}
}
}
}
}
応用
コマンドを状態によってdisabled()
したいこともあります。
そんな時は条件もSomeViewAction
経由で渡せるようにしてしまいます。
struct SomeViewAction {
var importAction: @MainActor @Sendable () -> Void
var canImport: @MainActor @Sendable () -> Bool
}
struct SomeView: View {
@State var isPresented = false
@State var canImport = true
var body: some View {
Button("Import") {
isPresented = true
}
.disabled(!canImport)
.focusedSceneValue(\.someViewAction, SomeViewAction(
importAction: { isPresented = true },
canImport: { canImport }
))
.fileImporter(
isPresented: $isPresented,
allowedContentTypes: [UTType.image],
onCompletion: { _ in /*読み込み処理*/ }
)
}
}
struct SomeApp: App {
@FocusedValue(\.someViewAction) private var someViewAction
var body: some Scene {
WindowGroup {
SomeView()
}
.commands {
CommandGroup(before: .newItem) {
Button {
someViewAction?.importAction()
} label: {
Text("Import")
}
.disabled(!(someViewAction?.canImport() ?? false))
}
}
}
}
Observable classを経由する
これは難しくありません。普通にObservable
を使えば良いです。
@MainActor @Observable class SomeViewModel {
var isPresented = false
var canImport = true
func onImport() {
isPresented = true
}
}
struct SomeView: View {
@State var viewModel = SomeViewModel()
var body: some View {
Button("Import") {
viewModel.onImport()
}
.disabled(!viewModel.canImport)
.focusedSceneValue(\.someViewAction, SomeViewAction(
importAction: { viewModel.onImport() },
canImport: { viewModel.canImport }
))
.fileImporter(
isPresented: $viewModel.isPresented,
allowedContentTypes: [UTType.image],
onCompletion: { _ in /*読み込み処理*/ }
)
}
}
Discussion