How to send a message from menu item in SwiftUI App to ContentView

I'm just not getting it. My app adds a custom Import menu item to the File menu. I want to have it tell the sole ContentView to run the fileImporter. Here's how I have it set up. Changing the showFileImporter variable to supposed to make stuff happen, but it doesn't change.

@main
struct Blah: App {
	@State public var			contentView = ContentView();
	
	var body:some Scene {
		WindowGroup {
//			ContentView() // It started out defining the content like normal, but I saw somewhere that if I declared it as a var up top, then I'd have an actual object that I could tell to do things, like calling the importTerms() method below.
			self.contentView
		}
		.commands {
			CommandGroup(after:.newItem) { 
				Button("Import…") {
					contentView.importTerms();
				}
			}
		}
	}

struct ContentView: View {
    @State private var						showFileImporter = false;
    var body: some View {
        VStack {
...stuff...
        }
    }
    .fileImporter(isPresented:$showFileImporter, allowedContentTypes:[.text], allowsMultipleSelection:false) { result in
    }
	
	public func importTerms()
	{
		print("\(showFileImporter)");
// ->false
		showFileImporter = true;
		print("\(showFileImporter)");
// ->yep, still false
	}
}

But it doesn't work. It calls importTerms(), and a breakpoint inside that method does get hit. But it doesn't change the value of showFileImporter and the fileImporter never appears. What kind of weird world has Swift made where setting a variable to true doesn't set it to true and there's no error at build or runtime?

Problème comes from, not from command.

	@State public var	contentView = ContentView()

I have tested this very simple code to show:

struct ContentView: View {

    public func importTerms() {
        print("\(showFileImporter)")
        showFileImporter = true
        print("\(showFileImporter)")
    }

    var body: some View {
        Button("Test") {
            importTerms()
        }
    }
}

struct ContentView2: View {
    @State private var content = ContentView()
    var body: some View {
        Button("Test2") {
            content.importTerms()
        }

    }
}

If I call ContentView(), and tap "Test", I get the correct result.

false
true

But If I call ContentView2(), and tap "Test2", I get the wrong result, var is not changed.

false
false

My guess is that with this declaration of state var ContentView, you create 2 instances, and there is a confusion in the showFileImporter state var at system level.

you update var for instance2 which is ignored by the system which uses instance1 (in print).

Someone more expert in SwiftUI may provide more accurate explanation.

You could use environment variables for showFileImporter and remove the contentView state var to solve the issue.

Note: in Swift, you don't need to end statements with semicolon.

You could use environment variables for showFileImporter and remove the contentView state var to solve the issue.

I'd thought about that, but it seems like bad practice to keep a variable in the app when only the ContentView actually uses it. But then there are many things about Swift that are illogical.

Note: in Swift, you don't need to end statements with semicolon.

I know, yet I do, because I always have in all other languages I've ever used, and always will. They add important visual endings to each line of code. I missed one in my editing of the example in my original post.

I tried moving showFileImporter to the app and sharing it in the environment, but Swift hates that.

struct SpeakotronikApp: App {
	@State var					showFileImporter:Bool = false;

	var body:some Scene {
		WindowGroup {
			ContentView()
				.environment(showFileImporter)
		}
}
Instance method 'environment' requires that 'Bool' be a class type

Instance method 'environment' requires that 'Bool' conform to 'Observable'

Views in SwiftUI are different from AppKit and UIKit in terms that they are value types, and you think about them differently.

One way to implement the scenario is this:

@main struct MyApp: App {
    @State private var displayImporter = false

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(after:.newItem) {
                Button("Import…") {
                    displayImporter = true
                }
                .fileImporter(isPresented: $displayImporter, allowedContentTypes: [/**/]) { result in
                    ///
                }
            }
        }
    }
}

That crossed my mind as well, but again, it feels icky to put the code in the app when only the ContentView should be muddied up with it. I'll do that and move on. It's too bad this SwiftUI stuff isn't friendlier. Thanks.

OK, so now the fileImporter is being run from the command in the app. It calls a function on the ContentView to import files. That function needs to do some other things to itself, so it sets the value of a @State TranslationSession.Configuration in the ContentView to a new instance, which is supposed to kick off a translationTask. Nothing happens, I assume because it's not being called from within a View hierarchy, but from the app.

Why is it so difficult to make things work in SwiftUI?

Menu Bars are one of the three most confusing aspects of SwiftUI app development imo.

Imagine you had a multi-window app and each of those windows have an instance of your content view.

Maybe you want a specific window to show the file import view. Maybe you only want one window to use the result of said import. It’s probably the active window, since that’s what the user is interacting with.

There’s a way to achieve this and it is the technique that I would recommend.

In an extension on the FocusedValues enum, I declare a Binding.

You can probably do this with an @Entry too.

var fousedWindowSheet: Binding<FocusedWindowSheet?>? {
        get { self[FousedWindowSheetKey.self] }
        set { self[FousedWindowSheetKey.self] = newValue }
    }

This allows focused windows to provide a hook that the menu bar can then leverage to present things.

You content view can do this with the following: .focusedSceneValue(\.fousedWindowSheet, $showingFocusedWindowSheet)

(it provides it's own @State to the focus system and can use that state in its own .sheet modifiers.

Okay, sure, but how does the CommandGroup get access to the focused value that's set by the actively focused scene?

Boom

@FocusedBinding(\.fousedWindowSheet) private var focusedWindowSheet

I declare one of those in a struct that conforms to Commands and I pass an instance of that into .commands

struct MyCommands: Commands {
    @FocusedBinding(\.fousedWindowSheet) private var focusedWindowSheet
    
    var body: some Commands {
        CommandGroup(after: .newItem) {
<blah blah blah>
        }
    }
}
.commands {
   MyCommands()
}

Honestly, this was a poorly structured dump of information more than a "guide" on how to do this.

I hope, though, that the keywords and technique of using FocusedSceneValue in conjunction with FocusedBinding get you on the right track!

It also sounds like you may need a refresher on how State propagates through swiftUI applications. View's are initiated on demand, and body methods are called at will by the system.

If you want state to last beyond those moments of re-invocation it has to be stored in an @State somewhere.

Keep replying with your code updates and I'll help you get something working.

While SwiftUI has some difficult parts, it is not "difficult" it's just "different".

It's important to remember you're not making views, you're just telling the SwiftUI runtime system how to make views, and it can be picky.

Well, I need more than a refresher course, since I've just been trying to understand how each piece of SwiftUI works when I come to it. I never been so stymied and angry at a language in all my 48 years of programming.

Here's the relevant code. Comments say what works and what doesn't:

@main
struct SpeakotronikApp: App {
	@State public var			contentView = ContentView();
	@State var					showFileImporter:Bool = false;

	var body:some Scene {
		WindowGroup {
			self.contentView
				.environment(langModel)
		}
		.commands {
			CommandGroup(after:.newItem) { 
				Button("Import…") {
					self.showFileImporter = true;
				}
				.fileImporter(isPresented:$showFileImporter, allowedContentTypes:[.text], allowsMultipleSelection:false) { result in
					switch result {
						case .success(let urls):
							for url in urls {
								// Tell the content view to read the file and add new terms that will be translated:
								self.contentView.importTerms(url:url);
							}
						case .failure(let error):
							print("Error selecting file: \(error.localizedDescription)");
					}
				}
			}
		}
	}
}

struct ContentView: View {
	@Environment(LangViewModel.self) private var	langModel;
	@State private var								selectedLang:Locale.Language = ...a language...;
	// This variable causes .translationTask to run when called from code contained in ContentView (.onChange), which works:
	@State var										sequenceTranslationConfig:TranslationSession.Configuration?;
	
    var body: some View {
		VStack {
			...buttons and stuff...
		}
		.onChange(of:selectedLang) {
    // Trigger the sequence translations to run:
			   sequenceTranslationConfig = .init(source:Locale.current.language, target:selectedLang, preferredStrategy:.highFidelity);
    }	
    .translationTask(sequenceTranslationConfig) { session in
			await langModel.translateSequence(using:session, terms:self.terms);
		}
	}
	
	public func importTerms(url:URL)
	{
		// Gain temporary permission to read security-scoped sandbox files
		guard url.startAccessingSecurityScopedResource() else { return }
		
		// Defer release of the system permission
		defer { url.stopAccessingSecurityScopedResource() }
		
		do {
			...read file and do stuff with it...
			
			// Trigger my sequence translations to happen, which does NOT work:
			sequenceTranslationConfig = .init(source:Locale.current.language, target:selectedLang, preferredStrategy:.highFidelity);
		}
		catch {
			print("Error reading file: \(error.localizedDescription)")
		}
	}
}

I found that I can send a custom notification from the function that receives the menu item action to my ContentView, and that works, getting around the whole "this is not the view that you're searching for" junk.

Pretty sure this will be the last SwiftUI app I'll ever write. I tried one before that should've been simple, but again I hit a wall of complexity and things that just aren't possible and went back to the glory that is Cocoa for that. At least things show up in the debugger there.

How to send a message from menu item in SwiftUI App to ContentView
 
 
Q