SwiftUI scroll view partially displays last item in Mac app - macos

How do I properly setup a ScrollView with SwiftUI in a Mac app? The example below clips the last item.
import SwiftUI
struct NameRow: View {
var name: String
var body: some View {
VStack {
Spacer()
Text("\(name)")
Spacer()
Divider()
}.frame(width: 100, height: 40)
}
}
struct ContentView: View {
let names = ["Homer", "Marge", "Lisa", "Bart", "Maggie", "Krusty", "Burns", "Nelson", "Otto"]
var body: some View {
VStack {
ScrollView {
ForEach(names, id: \.self) { name in
NameRow(name: name)
}
}
}.frame(width: 100, height: 160)
}
}
As shown in the image, the last item is clipped by the window. This view is scrolled to the bottom but the "Otto" item does not fully display.

If you remove the .fullSizeContentView from the window initialization in AppDelegate.swift so it looks like the following, then it will fully display the list.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false)
I don't know if this is a bug or by design.

Related

How to print a SwiftUI View with text and images on macOS?

I'm trying to print the contents of a SwiftUI view on macOS. According to the documentation, this seems like it should be possible by giving the NSPrintOperation an NSView created by an NSHostingView. The SwiftUI view's body contains several Text views and an Image view, with borders around those views. All of the Text gets printed, however the Image and the borders are not visible. Is there something else needed to make this work?
Here is a sample to demonstrate the problem. Just create a new macOS App, replace the ContentView with the code below and then enable Printing in Signing & Capabilities:
struct ContentView: View {
var body: some View {
VStack {
Button("Print", action: self.onPrint )
Divider()
Print_Preview()
}
}
private func onPrint() {
let pi = NSPrintInfo.shared
pi.topMargin = 0.0
pi.bottomMargin = 0.0
pi.leftMargin = 0.0
pi.rightMargin = 0.0
pi.orientation = .landscape
pi.isHorizontallyCentered = false
pi.isVerticallyCentered = false
pi.scalingFactor = 1.0
let rootView = Print_Preview()
let view = NSHostingView(rootView: rootView)
view.frame.size = CGSize(width: 300, height: 300)
let po = NSPrintOperation(view: view, printInfo: pi)
po.printInfo.orientation = .landscape
po.showsPrintPanel = true
po.showsProgressPanel = true
po.printPanel.options.insert(NSPrintPanel.Options.showsPaperSize)
po.printPanel.options.insert(NSPrintPanel.Options.showsOrientation)
if po.run() {
print("In Print completion")
}
}
struct Print_Preview: View {
var body: some View {
VStack(alignment: .leading) {
Text("Bordered Text Above Bordered Image")
.font(.system(size: 8))
.padding(5)
.border(Color.black, width: 2)
Image(systemName: "printer")
.resizable()
.padding(5)
.border(Color.black, width: 2)
.frame(width: 100, height: 100)
Text("Bordered Text Below Bordered Image")
.font(.system(size: 8))
.padding(5)
.border(Color.black, width: 2)
}
.padding()
.foregroundColor(Color.black)
.background(Color.white)
.frame(width: 200, height: 200)
}
}
}
Also, here are screenshots of the App and Print Panel.
Thanks to #Willeke for the comment with a pointer and to #user2120275 for asking a different question that happened to contain the trick needed to fix my problem. The solution is to create an NSImageView from the NSView returned by the NSHostingView, and then print that NSImageView instead of the original NSView.
My sample code above can be made to work by replacing these two lines:
view.frame.size = CGSize(width: 300, height: 300)
let po = NSPrintOperation(view: view, printInfo: pi)
with the following:
let contentRect = NSRect(x: 0, y: 0, width: 300, height: 300)
view.frame.size = contentRect.size
let newWindow = NSWindow(
contentRect: contentRect,
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
newWindow.contentView = view
let myNSBitMapRep = newWindow.contentView!.bitmapImageRepForCachingDisplay(in: contentRect)!
newWindow.contentView!.cacheDisplay(in: contentRect, to: myNSBitMapRep)
let myNSImage = NSImage(size: myNSBitMapRep.size)
myNSImage.addRepresentation(myNSBitMapRep)
let nsImageView = NSImageView(frame: contentRect)
nsImageView.image = myNSImage
let po = NSPrintOperation(view: nsImageView, printInfo: pi)

SwiftUI: Closing opened window on macOS causes crash

I can open a new window, but if I close it using the window's close button then my app crashes.
import SwiftUI
struct ContentView: View
{
var body: some View
{
Button(action: {openMyWindow()},
label: {Image(systemName: "paperplane")})
.padding()
}
}
func openMyWindow()
{
var windowRef:NSWindow
windowRef = NSWindow(
contentRect: NSRect(x: 100, y: 100, width: 100, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
windowRef.contentView = NSHostingView(rootView: WindowView())
windowRef.makeKeyAndOrderFront(nil)
}
struct WindowView: View
{
var body: some View
{
Text("Hello World")
.padding()
}
}
#main
struct Open_WindowApp: App
{
var body: some Scene
{
WindowGroup
{
ContentView()
}
}
}
I think I need to keep my windowRef active, but how do I do this?
declare windowRef outside the scope of openMyWindow()
if the window already exists, bring to front, don't make another.
keep the window from being dealloc'd on close, either
windowRef.isReleasedWhenClosed = false (shown below) (documentation), OR
someGlobalWindowController = NSWindowController(window: w)
var windowRef: NSWindow?
func openMyWindow()
{
if let curWindow = windowRef {
curWindow.makeKeyAndOrderFront(nil)
return
}
let w = NSWindow(
contentRect: NSRect(x: 100, y: 100, width: 100, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
w.contentView = NSHostingView(rootView: WindowView())
w.makeKeyAndOrderFront(nil)
w.isReleasedWhenClosed = false // <--- important
windowRef = w
}
We need to keep reference to window, try the following
struct ContentView: View
{
#State private var windowRef: NSWindow?
var body: some View
{
Button(action: {openMyWindow()},
label: {Image(systemName: "paperplane")})
.padding()
}
func openMyWindow()
{
// handle previously opened window here somehow if needed
guard windowRef == nil else { return }
windowRef = NSWindow(
contentRect: NSRect(x: 100, y: 100, width: 100, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
windowRef?.contentView = NSHostingView(rootView: WindowView())
windowRef?.makeKeyAndOrderFront(nil)
}
}

How can I hide statusBar of a View/Window in macOS in SwiftUI?

I was trying to hide statusBar of a view but I noticed it does not work in macOS, also Blur did not worked also opacity did not work! it look like I am trying programming new language, apple said code in place and apply every where! why I can not done those simple task in macOS?
struct ContentView: View {
var body: some View {
HStack {
Button { print("OK was clicked!") } label: { Text("OK").frame(width: 100) }
Button { print("Close was clicked!") } label: { Text("Close").frame(width: 100) }
}
.frame(width: 400, height: 200)
.background(Color.white.opacity(0.4).blur(radius: 0.5))
//.statusBar(hidden: true)
}
}
import SwiftUI
#main
struct macOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Hope it helps you.
Thank you.
class AppDelegate: NSObject, UIApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the content view, to display the contents
let contentView = ContentView()
//to display content view beyond status bar.
.edgesIgnoringSafeArea(.top)
// Now, just create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .texturedBackground, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
//So, this does the maigc
//Hides the titlebar.
window.titlebarAppearsTransparent = true
window.titleVisibility = .hidden
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
}

macOS SwiftUI popover with TextField closes on first edit

I'm building a macOS SwiftUI app. In it, I have a popover with a TextField. The TextField has a binding that updates an ObservableObject that then propagates its value back down a NavigationView to the Detail View.
On the first keyboard entry to the TextField, the popover closes. However, on all subsequent entries, the popover stays open.
I've also tried this with a DatePicker and have had the same results, so it's not limited to the TextField entry.
Note that this does not happen when running the same code on iOS (on iPhone where popovers are displayed as sheets or on iPad where they are popovers).
I've included a minimal example.
Anyone have any clue how to avoid the popover closing on first edit?
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(store: AppState())
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
}
class AppState : ObservableObject {
#Published var models : [String:Model] = ["1" : Model(id: "1", title: "June 14"),
"2" : Model(id: "2", title: "July 21"),
"3" : Model(id: "3", title: "Sept 5")]
func changeTitle(key: String, newTitle: String) {
guard var model = self.models[key] else {
return
}
model.title = newTitle
self.models[key] = model
}
}
struct Model {
var id : String
var title: String
}
struct ContentView: View {
#ObservedObject var store : AppState
var body: some View {
NavigationView {
VStack {
List {
ForEach(Array(store.models.values), id: \.id) { model in
NavigationLink(destination: DetailView(model: model,
changeTitle: { title in
self.store.changeTitle(key: model.id, newTitle: title)
})) {
VStack(alignment: .leading) {
Text(model.title).font(.headline)
}.padding(.vertical, 8)
}
}
}.frame(minWidth: 150, idealWidth: 200, maxWidth: 200, maxHeight: .infinity)
}
EmptyView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct DetailView : View {
var model : Model
var changeTitle: (String) -> Void
#State private var showPopover = false
var customBinding : Binding<String> {
Binding<String>(get: { self.model.title }, set: { newValue in
self.changeTitle(newValue)
})
}
var body: some View {
VStack {
Text(model.title)
Button("Edit") {
self.showPopover.toggle()
}.popover(isPresented: self.$showPopover) {
TextField("Text", text: self.customBinding)
.frame(minWidth: 300)
.padding()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

NSWindow contentView not cover full window size - macOS & SwiftUI

I'm starting a new macOS app with SwiftUI but I have a big problem.
The app needs a full size contentView (underneath titleBar) but I can't accomplish. On a new project using Storyboards works fine, but with SwiftUI not.
My code:
Result:
And it should look like this:
Any ideas?
Thanks!
I just used the following variant in AppDelegate, the content of ContentView and others can be any
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.edgesIgnoringSafeArea(.top) // to extend entire content under titlebar
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .texturedBackground, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.titlebarAppearsTransparent = true // as stated
window.titleVisibility = .hidden // no title - all in content
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
The safe area does not extend underneath a transparent title bar. You can use edgesIgnoringSafeArea to tell your content view edges to ignore the safe area. Something that resembles your example:
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Text("Hello, World!")
.frame(maxWidth: 200, maxHeight: .infinity)
.background(Color.red)
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
}.edgesIgnoringSafeArea(.all)
}
}
Update:
If you want to use a NavigationView, you have to add edgesIgnoringSafeArea to its contents as well:
struct ContentView: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.frame(maxWidth: 200, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.edgesIgnoringSafeArea(.all)
}.edgesIgnoringSafeArea(.all)
}
}
Unfortunately, this will initially show a title bar at the moment, apparently until you force a full window redraw. Once you move the window to a different display or hide and show it again, the title bar disappears. So, my guess is that this will be fixed at some point.
Right now, you can programmatically force a hide & show by adding
DispatchQueue.main.async {
self.window.orderOut(nil)
self.window.makeKeyAndOrderFront(nil)
}
after window.makeKeyAndOrderFront(nil) in applicationDidFinishLaunching. It will add a very short animation however. If it bothers you, you may be able to turn that off with NSWindow.animationBehavior or something like that.
Update 2: Apparently, if you remove the initial window.makeKeyAndOrderFront(nil) and replace it with the dispatch queue logic above it won't animate. So in the end you'll have
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView, .texturedBackground],
backing: .buffered, defer: false)
window.titlebarAppearsTransparent = true
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
// window.makeKeyAndOrderFront(self) <- don't call it here
DispatchQueue.main.async {
self.window.orderOut(nil)
self.window.makeKeyAndOrderFront(nil)
}
}
Just for more information in the case of SwiftUI App life cycle.
You need to set the window style to HiddenTitleBarWindowStyle :
WindowGroup {
ContentView()
}.windowStyle(HiddenTitleBarWindowStyle())
Minimal solution in pure SwiftUI.
#main
struct X_App: App {
var body: some Scene {
WindowGroup {
ContentView()
.edgesIgnoringSafeArea(.top)
}.windowStyle(.hiddenTitleBar)
}}

Resources