Launch windows in a SwiftUI App-based project from custom events on macOS - swift

In a SwiftUI macOS app that uses the SwiftUI App lifecycle I have been struggling to understand how to launch a new window and then assign it some initial state.
Using Xcode 12.5’s new project we get:
import SwiftUI
#main
struct StackOverflowExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
To keep the question simple, let’s say that I would like to launch a new window every minute and send the time the window was launched to the view. For this we can use a Timer like so:
Timer.scheduledTimer(
withTimeInterval: 60,
repeats: true,
block: {_ in
// do something here that launches a new window containing
// ContentView and sends it the current time
}
)
Any ideas on how to proceed?

Related

Remember #State variable after switching views

I'm making an application for macOS with SwiftUI. I have a button that starts a download process asynchronously by calling Task.init and an async function. When the task starts, I set a #State busy variable to true so I can display a ProgressView while the download is happening. The problem is if I switch to a different view then back while it is downloading, the state gets reset although the task still runs. How can I make it so it remembers the state, or maybe check if the task is still running?
Here is a stripped down example:
import SwiftUI
struct DownloadRow: View {
let content: Content
#State var busy = false
var body: some View {
if !busy {
Button() {
Task.init {
busy = true
await content.download()
busy = false
}
} label: {
Label("Download")
}
} else {
ProgressView()
}
}
}
You could put the variable that tracks the download's progress into an ObservableObject class and make the progress variable #Published. Have the object that tracks your progress as an #ObservedObject in the view. By doing so, you decouple the progress tracker from the view's lifecycle. Make sure this view does not initialize the progress tracker, or a new object will be built when the view is built again.

SwiftUI MenuBar App with Assertion on #main

I have a SwiftUI Menu Bar App with no visible Windows on startup, except an entry in the Menu Bar with a Button showing a View on click.
This works great, but sporadically, the App throws an assertion and the MenuBar Button is not added, though its impossible to click it.
Here is what does not solve the issue:
Cleaning Build + Derived Data
Reboots
Code in AppDelegate (needed for the MenuBar)
#main // Assertion thrown here
struct MenuBarApp: App {
#Environment(\.scenePhase) var scenePhase
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
// This surpresses opening an empty Window on launch
ZStack{
EmptyView()
}.hidden() // removing .hidden() sometimes make the assertion go away, sometimes having .hidden() here triggers it
// Sometimes, a second ZStack makes the assertion dissappear, sometimes it triggers it.
}
}
}
The assertion reads:
2022-05-24 12:58:06.263464+0200 MenuBar[7119:96419] *** Assertion failure in void _NSWindowSetFrameIvar(NSWindow *, NSRect)(), NSWindow.m:935
2022-05-24 13:04:40.477258+0200 MenuBar[7119:96419] Invalid parameter not satisfying: <SwiftUI.SwiftUIWindow: 0x159e63780>. "frame=CGRectContainsRect(CGRectMake((CGFloat)INT_MIN, (CGFloat)INT_MIN, (CGFloat)INT_MAX - (CGFloat)INT_MIN, (CGFloat)INT_MAX - (CGFloat)INT_MIN), frame)"
On other machines the build and launch of the same Code runs perfectly fine.
func applicationDidFinishLaunching(_ notification: Notification) is not getting called in the assert case
macOS 12.4, M1 Max, XCode 13.3.1
Code in AppDelegate (needed for the MenuBar)
The AppDelegate is no longer needed with the release of MenuBarExtra as of Xcode 14.2, macOS 13 Ventura. A MenuBarExtra scene renders a persistent control in the system menu bar with the native SwiftUI lifecycle.
#main
struct MyStatubarMenuApp: App {
var body: some Scene {
// -- no WindowGroup required --
// -- no AppDelegate needed --
MenuBarExtra("😎") {
Text("Hello Status Bar Menu!")
}
}
}

How can I make a window take over the entire screen? MacOS SwiftUI

How can I make a window that is the fullscreen of the MacOS screen that goes over the menuBar, and goes over the applications at the bottom. I saw functions like 'setMenuBarVisible'; however this does not work in SwiftUI. Any help would be appreciated
This code will make your app toggle fullscreen on launch.
WindowGroup {
ContentView()
.onAppear {
DispatchQueue.main.async {
if let window = NSApplication.shared.windows.last {
window.toggleFullScreen(nil)
}
}
}
}
I'm not sure if this is the answer you are looking for but.

How can I open URLs in watchOS?

Can a standalone Apple Watch app open a URL like we can in iOS:
UIApplication.sharedApplication().openURL(url!)
I want to display a message about an update when a new app is published. I want the user to go the URL for the update. Is there any good way?
The following answer works in watchOS 6.2 and above. It initiates a web authentication session, which opens a web view, similar to WKWebView on iOS. You can even swipe from the left to go back.
Keep in mind that it doesn't save cookies, so, for example, if you log in to a website and close it, you have to log in again when you open it back up.
import AuthenticationServices
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Tap me") {
guard let url = URL(string: "https://example.com") else {
return
}
// Source: https://www.reddit.com/r/apple/comments/rcn2h7/comment/hnwr8do/
let session = ASWebAuthenticationSession(
url: url,
callbackURLScheme: nil
) { _, _ in
}
// Makes the "Watch App Wants To Use example.com to Sign In" popup not show up
session.prefersEphemeralWebBrowserSession = true
session.start()
}
}
}
I don't know if you're using SwiftUI or not, but the Link View is shown in the Documentation as available for watchOS. However using it in practice only prompts the user to open the link on their iPhone. As a standalone app since Safari is not available on watchOS you might want to consider other options to alert your user of an update.
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
Link("Apple.com", destination: URL(string: "https://www.apple.com")!)
}
}
}

SwiftUI - Making a View focusable and trigger an action on macOS

How is it possible to make a View on macOS focusable. I tried the it like below but the action is never triggered. I know that for NSView you have to implement acceptsFirstResponder to return true but can not find similar in SwiftUI.
Is this still a Beta related bug or a missing functionality for macOS ?
struct FocusableView: View {
var body: some View {
Group {
Text("Hello World!")
.padding(20)
.background(Color.blue.cornerRadius(8))
.focusable(true) { isFocused in
print("Focused", isFocused)
}
}
}
}
I think I found a work around for the problem to shift the focus to a SwiftUI view.
I am working on macOS Catalina, 10.15, Swift 5, Xcode 11.1
The problem is to shift the focus to a SwiftUI view. I could imagine that this has not been perfectly implemented, yet.
Only upon shifting the focus to the required SwiftUI view within the SwiftUI framework the onFocusChange closure of a focusable view will be called and the view will be focused.
My intention was: when I click into the SwiftUI view I want to execute the onFocusChange closure and I want it to be focused (for subsequent paste commands)
My SwiftUI view is built into the Window using an NSHostingView subclass - ImageHostingView.
When I add this ImageHostingView to the view hierarchy I define its previous key view:
theImageHostingView.superview.nextKeyView = theImageHostingView
I added a mouseDown handler to the ImageHostingView:
override dynamic func mouseDown(with event: NSEvent) {
if let theWindow = self.window, let theView = self.previousValidKeyView {
theWindow.selectKeyView(following:theView)
}
}
Calling the NSWindow's selectKeyView(following:) triggers within the SwiftUI framework a focus change to the desired view (the root view of ImageHostingView).
This is clearly a work-around and works only for final views which will be represented by a NSHostingView. But it highlights the problem (shifting the focus to a SwiftUI view upon a certain action) and it might be helpful in many cases.
The only way I've been able to get this to work is by checking the "Use keyboard navigation to move focus between controls" checkbox in Keyboard System Preferences