How to print a SwiftUI View with text and images on macOS? - 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)

Related

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)
}
}

How to update different views using SwiftUI?

I am aiming to make a program in which I am using using SwiftUI buttons to update by SCNView in SceneKit. I have a cylinder as a SCNCylinder in my SCNView inside a frame in SwiftUI. I want my cylinder to rotate about 180° after I press the button. In my current code I have used #State and #Binding to update the view. But somehow the cylinder rotates as soon as I run the code, not waiting for me to touch the button. Not sure why this happens
Here is my code :
struct ContentView: View {
#State var rotationAngle: Float = 180
var body: some View {
VStack{
Button(action: {
// What to perform
self.rotationAngle = 180
}) {
// How the button looks like
Text("180°")
.frame(width: 100, height: 100)
.position(x: 225, y: 500)
}
SceneKitView(angle: self.$rotationAngle)
.frame(width: 300, height: 300)
.position(x: 225, y: 0)
}
}
}
struct SceneKitView: UIViewRepresentable {
#Binding var angle: Float
func degreesToRadians(_ degrees: Float) -> CGFloat {
return CGFloat(degrees * .pi / 180)
}
func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {
let sceneView = SCNView()
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = UIColor.white
sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)
return sceneView
}
func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
let cylindernode = SCNNode(geometry: cylinder)
cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
cylinder.firstMaterial?.diffuse.contents = UIColor.green
cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)
let rotation = SCNAction.rotate(by: self.degreesToRadians(self.angle), around: SCNVector3(1, 0, 0), duration: 5)
sceneView.scene?.rootNode.addChildNode(cylindernode)
cylindernode.runAction(rotation)
}
typealias UIViewType = SCNView
}
I want the cylinder to rotate after I press the button. Please help me with this problem.
just set startingAngle to 0
#State var rotationAngle: Float = 0

Change window size based on NavigationView in a SwiftUI macOS app

I'm using SwiftUI for a Mac app where the main window contains a NavigationView. This NavigationView contains a sidebar list. When an item in the sidebar is selected, it changes the view displayed in the detail view. The views presented in the detail view are different sizes which should cause the size of the window to change when they are displayed. However, when the detail view changes size the window does not change size to accommodate the new detail view.
How can I make the window size change according to the size of the NavigationView?
My example code for the app is below:
import SwiftUI
struct View200: View {
var body: some View {
Text("200").font(.title)
.frame(width: 200, height: 400)
.background(Color(.systemRed))
}
}
struct View500: View {
var body: some View {
Text("500").font(.title)
.frame(width: 500, height: 300)
.background(Color(.systemBlue))
}
}
struct ViewOther: View {
let item: Int
var body: some View {
Text("\(item)").font(.title)
.frame(width: 300, height: 200)
.background(Color(.systemGreen))
}
}
struct DetailView: View {
let item: Int
var body: some View {
switch item {
case 2:
return AnyView(View200())
case 5:
return AnyView(View500())
default:
return AnyView(ViewOther(item: item))
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(1...10, id: \.self) { index in
NavigationLink(destination: DetailView(item: index)) {
Text("Link \(index)")
}
}
}
.listStyle(SidebarListStyle())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is what the example app looks like when the detail view changes size:
Here is demo of possible approach that works. I did it on one different view, because you will need to redesign your solution to adopt it.
Demo
1) The view requiring window animated resize
struct ResizingView: View {
public static let needsNewSize = Notification.Name("needsNewSize")
#State var resizing = false
var body: some View {
VStack {
Button(action: {
self.resizing.toggle()
NotificationCenter.default.post(name: Self.needsNewSize, object:
CGSize(width: self.resizing ? 800 : 400, height: self.resizing ? 350 : 200))
}, label: { Text("Resize") } )
}
.frame(minWidth: 400, maxWidth: .infinity, minHeight: 200, maxHeight: .infinity)
}
}
2) Window's owner (in this case AppDelegate)
import Cocoa
import SwiftUI
import Combine
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var subscribers = Set<AnyCancellable>()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ResizingView()
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), // just default
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
.sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
if let size = notificaiton.object as? CGSize {
var frame = self.window.frame
let old = self.window.contentRect(forFrameRect: frame).size
let dX = size.width - old.width
let dY = size.height - old.height
frame.origin.y -= dY // origin in flipped coordinates
frame.size.width += dX
frame.size.height += dY
self.window.setFrame(frame, display: true, animate: true)
}
}
.store(in: &subscribers)
}
...
Asperi's answer works for me, but the animation is not working on Big Sur 11.0.1, Xcode 12.2. Thankfully, the animation works if you wrap it in an NSAnimationContext:
NSAnimationContext.runAnimationGroup({ context in
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
window!.animator().setFrame(frame, display: true, animate: true)
}, completionHandler: {
})
Also it should be mentioned that ResizingView and window don't have to be initialized inside AppDelegate; you can continue using SwiftUI's App struct:
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ResizingView()
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow?
var subscribers = Set<AnyCancellable>()
func applicationDidBecomeActive(_ notification: Notification) {
self.window = NSApp.mainWindow
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
setupResizeNotification()
}
private func setupResizeNotification() {
NotificationCenter.default.publisher(for: ResizingView.needsNewSize)
.sink(receiveCompletion: {_ in}) { [unowned self] notificaiton in
if let size = notificaiton.object as? CGSize, self.window != nil {
var frame = self.window!.frame
let old = self.window!.contentRect(forFrameRect: frame).size
let dX = size.width - old.width
let dY = size.height - old.height
frame.origin.y -= dY // origin in flipped coordinates
frame.size.width += dX
frame.size.height += dY
NSAnimationContext.runAnimationGroup({ context in
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
window!.animator().setFrame(frame, display: true, animate: true)
}, completionHandler: {
})
}
}
.store(in: &subscribers)
}
}
the following will not solve your problem, but might (with some extra work), lead you to a solution.
I did not have much to investigate further, but it's possible to overwrite the setContentSize method in NSWindow (by subclassing of course). That way you can override the default behavior, which is setting the window frame without an animation.
The problem you will have to figure out is the fact that for complex views such as yours, the setContentSize method is called repeatedly, causing the animation not to work properly.
The following example works fine, but that's because we are dealing with a very simple view:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = AnimatableWindow(
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)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
class AnimatableWindow: NSWindow {
var lastContentSize: CGSize = .zero
override func setContentSize(_ size: NSSize) {
if lastContentSize == size { return } // prevent multiple calls with the same size
lastContentSize = size
self.animator().setFrame(NSRect(origin: self.frame.origin, size: size), display: true, animate: true)
}
}
struct ContentView: View {
#State private var flag = false
var body: some View {
VStack {
Button("Change") {
self.flag.toggle()
}
}.frame(width: self.flag ? 100 : 300 , height: 200)
}
}

Getting and setting the position of content in a SwiftUI ScrollView

I have a horizontal scroll view where I'd like to set a position programmatically. Here is the body of the view:
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
...
var body: some View {
GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finalmente.")
.font(Font.title)
.frame(width: scrollWidth, height: scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
}
.frame(width: viewGeometry.size.width, height: scrollHeight)
.padding([.top, .bottom], spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: circleDiameter, height: circleDiameter)
Spacer(minLength: spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
How do I change this code so that I can get the current position of "The quick brown fox" and restore it at a later time? I'm just trying to do something like we've always done with contentOffset in UIKit.
I can see how a GeometryReader might be useful to get the content's current frame, but there's no equivalent writer. Setting a .position() or .offset() for the scroll view or text hasn't gotten me anywhere either.
Any help would be most appreciated!
I've been playing around with a solution and posted a Gist to what I have working in terms of programmatically setting content offsets https://gist.github.com/jfuellert/67e91df63394d7c9b713419ed8e2beb7
With the regular SwiftUI ScrollView, as far as I can tell, you can get the position with GeometryReader with proxy.frame(in: .global).minY (see your modified example below), but you cannot set the "contentOffset".
Actually if you look at the Debug View Hierarchy you will notice that our content view is embedded in an internal SwiftUI other content view to the scrollview. So you will offset vs this internal view and not the scroll one.
After searching for quite a while, I could not find any way to do it with the SwiftUI ScrollView (I guess will have to wait for Apple on this one). The best I could do (with hacks) is a scrolltobottom.
UPDATE: I previously made a mistake, as it was on the vertical scroll. Now corrected.
class SGScrollViewModel: ObservableObject{
var scrollOffset:CGFloat = 0{
didSet{
print("scrollOffset: \(scrollOffset)")
}
}
}
struct ContentView: View {
public var scrollModel:SGScrollViewModel = SGScrollViewModel()
let radius = CGFloat(25)
let scrollWidth = CGFloat(700)
let scrollHeight = CGFloat(100)
let spacing = CGFloat(20)
let circleDiameter = CGFloat(50)
var body: some View {
var topMarker:CGFloat = 0
let scrollTopMarkerView = GeometryReader { proxy -> Color in
topMarker = proxy.frame(in: .global).minX
return Color.clear
}
let scrollOffsetMarkerView = GeometryReader { proxy -> Color in
self.scrollModel.scrollOffset = proxy.frame(in: .global).minX - topMarker
return Color.clear
}
return GeometryReader { viewGeometry in
ScrollView () {
VStack(spacing: 0) {
Spacer(minLength: self.spacing)
Circle()
.fill(Color.black.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
scrollTopMarkerView.frame(height:0)
ScrollView(.horizontal) {
Text("The quick brown fox jumps over the lazy dog. Finally.")
.font(Font.title)
.frame(width: self.scrollWidth, height: self.scrollHeight)
.foregroundColor(Color.white)
.background(Color.white.opacity(0.25))
.background(scrollOffsetMarkerView)
}
.frame(width: viewGeometry.size.width, height: self.scrollHeight)
.padding([.top, .bottom], self.spacing)
Circle()
.fill(Color.white.opacity(0.5))
.scaledToFit()
.frame(width: self.circleDiameter, height: self.circleDiameter)
Spacer(minLength: self.spacing)
}
.frame(width: viewGeometry.size.width)
}
.background(Color.orange)
}
.frame(width: 324 / 2, height: spacing * 4 + circleDiameter * 2 + scrollHeight) // testing
.cornerRadius(radius)
.background(Color.black)
}
}
I messed around with several solutions involving a ScrollView with one or more GeometryReaders, but ultimately I found everything easier if I just ignored ScrollView and rolled my own using View.offset(x:y:) and a DragGesture:
This allows the LinearGradient to be panned by either dragging it like a ScrollView, or by updating the binding, in the case via a Slider
struct SliderBinding: View {
#State var position = CGFloat(0.0)
#State var dragBegin: CGFloat?
var body: some View {
VStack {
Text("\(position)")
Slider(value: $position, in: 0...400)
ZStack {
LinearGradient(gradient: Gradient(colors: [.blue, .red]) , startPoint: .leading, endPoint: .trailing)
.frame(width: 800, height: 200)
.offset(x: position - 400 / 2)
}.frame(width:400)
.gesture(DragGesture()
.onChanged { gesture in
if (dragBegin == nil) {
dragBegin = self.position
} else {
position = (dragBegin ?? 0) + gesture.translation.width
}
}
.onEnded { _ in
dragBegin = nil
}
)
}
.frame(width: 400)
}
}
Clamping the drag operation to the size of the scrolled area is omitted for brevity. This code allows horizontal scrolling, use CGPoints instead of CGSize to implement it horizontally and vertically.

SwiftUI scroll view partially displays last item in Mac app

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.

Resources