I'm trying to show a WKWebView in SwiftUI on MacOS. When the app initially loads, the WKWebView has a large, white bar at the top. Moving or resizing the window causes this to immediately disappear and display correctly. Interestingly, the blue border around the view does not exhibit the bad behavior.
My guess is that I'm missing some action in updateNSView.
I'm on MacOS 11.1 Big Sur, Xcode 12.2, Intel. Another thing to note is that I need to enable the "Outgoing Connections" entitlement in the App Sandbox to get WKWebView to render anything at all, even tho the content is provided locally from a string.
import SwiftUI
import WebKit
#main
struct ProblemWKWebViewApp: App {
var body: some Scene {
WindowGroup {
SwiftUIWebView()
.border(Color.blue, width: 2)
}
}
}
struct SwiftUIWebView: NSViewRepresentable {
public typealias NSViewType = WKWebView
func makeNSView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.loadHTMLString("<body style=\"background-color: red;\"><h1>Hello World!</h1></body>", baseURL: nil)
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {
}
}
I had the same problem. I suppose that it is a bug. However I managed to fix it with adding ignoresSafeArea():
#main
struct ProblemWKWebViewApp: App {
var body: some Scene {
WindowGroup {
SwiftUIWebView()
.border(Color.blue, width: 2)
// Shows weird black bar on top without this on macOS
.ignoresSafeArea()
}
}
}
Related
I'm building an app for MacOS. The functionality requires the window to increase in size in certain situations and to scale back outside of these situations. Is this possible? any pointers or examples are appreciated
Yes, this is possible by updating the frame of the NSWindow for your application, using setFrame. This method allows for setting the new frame (position and size) with and/or without animation.
Without animation (see setFrame(NSRect, display: Bool))
window.setFrame(newFrame, display: true)
With animation (see setFrame(NSRect, display: Bool, animate: Bool))
window.setFrame(newFrame, display: true, animated: true)
Calling any of these two methods will update the window of your application to whatever new frame you pass, including position and size.
The tricky part in SwiftUI is to get a hold to the NSWindow, since it is not trivial. Here is the TLDR:
Create a NSViewRepresentable to access the backing window:
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
The window is set in the next run loop using dispatch async, since it would be nil beforehand.
Add a window property to your SwiftUI view, and use the window accessor with a background modifier.
struct MyView: View {
#State private var window: NSWindow?
var body: some View {
VStack {
// Your view content. Note: window might be nil until set
}
.background(WindowAccessor(window: $window))
.onChange(of: window) {
// This method will get called once when the window is set
}
}
}
I'm reworking my app for SwiftUI 2.0 but have come across a problem when replicating what I could do with AppDelegate.
I'm using NSViewRepresentable to get access to NSWindow so I can remove the titlebar of the window (I know it's not in the guidelines but this will never be submitted). When removing .titled from styleMask, the app crashes.
struct WindowAccessor: NSViewRepresentable {
#Binding var window: NSWindow?
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
self.window = view.window
self.window?.isOpaque = false
self.window?.titlebarAppearsTransparent = true
self.window?.backgroundColor = NSColor.clear
self.window?.styleMask = [.fullSizeContentView]
self.window?.isMovableByWindowBackground = true
self.window?.backingType = .buffered
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
#main
struct MyApp_App: App {
#State private var window: NSWindow?
var body: some Scene {
WindowGroup {
ContentView().background(WindowAccessor(window: $window))
}
}
}
struct ContentView: View {
var body: some View {
Text("Hello, world!").padding().background(Color(NSColor.windowBackgroundColor))
}
}
When I run the app I get Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
All I'm trying to achieve with my app is a Menu Bar application that looks exactly like Spotlight. No dock icon, no title bar, all preferences to be handled by a popover or another window.
EDIT:
Is this something to do with the canBecomeKey property?
I'm looking to not show a window for a SwiftUI application on macOS. The app uses SwiftUI's application lifecycle and only runs in the status bar. Showing a window on start up is unnecessary. I'm unsure however how to get around the WindowGroup. There's no such a thing as an EmptyScene and putting an EmptyView inside the WindowGroup of course creates an empty window.
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I basically only need the app delegate. I'm guessing using the default AppKit lifecycle makes more sense, but if there is a way to use SwiftUI's lifecycle, I'd love to know.
In your AppDelegate, do the following in your applicationDidFinishLaunching:
func applicationDidFinishLaunching(_ notification: Notification) {
// Close main app window
if let window = NSApplication.shared.windows.first {
window.close()
}
// Code continues here...
}
For example:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBar: StatusBarController?
func applicationDidFinishLaunching(_ notification: Notification) {
// Close main app window
if let window = NSApplication.shared.windows.first {
window.close()
}
// Create the SwiftUI view that provides the contents
let contentView = ContentView()
// Set the SwiftUI's ContentView to the Popover's ContentViewController
popover.contentSize = NSSize(width: 160, height: 160)
popover.contentViewController = NSHostingController(rootView: contentView)
// Create the Status Bar Item with the above Popover
statusBar = StatusBarController.init(popover)
}
}
You should be able to do something like this:
var body: some Scene {
WindowGroup {
ZStack {
EmptyView()
}
.hidden()
}
}
If you app has settings (who doesn't?), you can do like this:
#main
struct TheApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
Settings {
SettingsView()
}
}
}
The app uses SwiftUI's application lifecycle and only runs in the status bar.
Use a MenuBarExtra
scene to render a persistent control in the system menu bar with the native SwiftUI lifecycle. (Xcode 14.2, macOS Ventura)
#main
struct MyStatubarMenuApp: App {
var body: some Scene {
// -- no WindowGroup required --
// -- no AppDelegate needed --
MenuBarExtra("😎") {
Text("Hello Status Bar Menu!")
Divider()
Button(…)
MyCustomSubmenu()
}
}
}
Note: Add and set Application is agent = YES in Info.plist for the app icon to not show on the dock.
I am writing a small App for the Mac..
I need to disable to (Green Button) for full screen.
I am using SwiftUI App not AppKit App Delegate
Cant find how to disable the Full Screen Button for my app.
Any thoughts?
Because no one answered with a cleaner SwiftUI only version:
struct ContentView: View {
var body: some View {
HostingWindowFinder { window in
window?.standardWindowButton(.zoomButton)?.isHidden = true //this removes the green zoom button
}
Text("Hello world")
}
}
struct HostingWindowFinder: NSViewRepresentable {
var callback: (NSWindow?) -> ()
func makeNSView(context: Self.Context) -> NSView {
let view = NSView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
HostingWindowFinder concept taken from https://lostmoa.com/blog/ReadingTheCurrentWindowInANewSwiftUILifecycleApp/
According to Apple help modifier is only available in macOS11, so what is the workaround for adding a tooltip in macOS 10.15?
In SwiftUI on macOS 11, you can use the .help("Tooltip text") view modifier to add a tooltip. See the "What's new in SwiftUI" session for WWDC 2020.
REFERENCE
https://developer.apple.com/forums/thread/123243
The workaround is to use a overplayed old NSView
import SwiftUI
struct Tooltip: NSViewRepresentable {
let tooltip: String
func makeNSView(context: NSViewRepresentableContext<Tooltip>) -> NSView {
let view = NSView()
view.toolTip = tooltip
return view
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<Tooltip>) {
}
}
public extension View {
func toolTip(_ toolTip: String) -> some View {
self.overlay(Tooltip(tooltip: toolTip))
}
}
To use the modifier
Image("pin")
.resizable()
.toolTip("TEST")
Also an open-source solution can be found on GitHub, https://github.com/quassummanus/SwiftUI-Tooltip