Document based app using one single window - swift

I have a document-based macOS application, which is a basic text editor. The default behavior is to create a new window for every opened document. But I want to only have one window displayed at a time and when opening a document or creating a new one, this should happen in the same window and thus replace the old document.
I've tried to do some hacking in the makeWindowControllers method from NSDocument by not instantiating a new window controller but reusing an old one. But after some problems with this approach, I figured this is not the right way to go. I was wondering if there is a common approach to this problem.
This is the code I've tried
class Document: NSDocument {
static var sharedWindow: NSWindowController?
override func makeWindowControllers() {
// Instaniate window controller there is none
if Document.sharedWindow == nil {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
Document.sharedWindow = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as? NSWindowController
}
guard let sharedWindow = Document.sharedWindow else { return }
if let previousDocument = sharedWindow.document as? NSDocument {
previousDocument.close()
}
self.addWindowController(sharedWindow)
sharedWindow.contentViewController?.representedObject = content
(sharedWindow.contentViewController as? ViewController)?.handleOpenDocumentOperation()
}
...
}

Related

Create a Window on a status bar app for macOS

Warning: macOS dev beginner here.
I have a menu bar app (with no dock). Most of the app's functionality is in the menu (and implementation is in AppDelegate), but I need a separate window that will open once I click one of the menu items.
I want to use SwiftUI, Swift 5, Xcode 11.3.
I haven't found an appropriate way to do this. Which files and similar need to be created? How to open this window programatically?
#objc func openPreferences() {
// open a new window here...
}
You have to create a window programatically. I have attached sample code of one of my apps:
private var windowController: NSWindowController?
fileprivate func createWindow()
{
let storyboard = NSStoryboard(name: "Main", bundle: nil)
self.windowController = storyboard.instantiateInitialController() as? NSWindowController
// This is example code to show how to customize the hosted view controller. You can pass additional arguments here (may an important global variables that is declared in the AppDelegate).
if let contentController = windowController?.contentViewController as? MyWindowViewController
{
// Do some assignments here
// contentController.variable = ....
// self.windowViewController = contentController // Maybe save for later use.
}
}
#objc fileprivate func open()
{
if self.windowViewController == nil
{
self.createWindow()
}
self.windowController?.showWindow(self)
NSApp.activate(ignoringOtherApps: true) // Bring window to front.
}
I have linked the open() function to a button call (hence the #objc keyword). I think that you already did this, so my open() function would be your openPreferences function.

Quicklook always displays "no file to preview" error (url is valid)

I'm trying to use QuickLookController subclass as a child controller, setting its view as a subview in the parent. However, it always displays "no file to preview" message in the opening window. URL in the data source is valid, but the controller is never trying to get it! func previewItemAt index is never invoked!
func "numberOfPreviewItems" invokes always.
Please, help!
I get it. driven by example in article https://williamboles.me/hosting-viewcontrollers-in-cells/ I loaded my controller from bundle:
static func createFromStoryBoard() -> PreviewControler {
let storyboard = UIStoryboard(name: "PreviewControler", bundle: Bundle(for: PreviewControler.self))
guard let viewController = storyboard.instantiateViewController(withIdentifier: "PreviewControler") as? PreviewControler else {
fatalError("PreviewControler should be present in storyboard")
}
return viewController
}
But QuickLook controller must be created with it's constructor, so change to
let viewController = PreviewController()
solved the problem. Now all is fine.

Saving with NSDocument in a Second ViewController

I am trying to simply save using NSDocument to a rtf. The code works fine but when I try to save to a view controller that isn't the initial child to the window controller it throws an error from a modal saying 'The document “” could not be saved as “”.'
How can I save the file to the Second View Controller?
Window Controller
|
Login View Controller
| |
SidebarViewContoller ViewController1
|
TableViewController 2 Replaces VC1
Save TextView in this VC
I want to be able to write data into My NSDocument from the textView in ViewController2 and save it to the desktop
Just like you would for instance in Pages
Here is the code
// Document.swift
class Document: NSDocument {
var text = NSAttributedString()
var documentViewController: DocumentViewController? {
return windowControllers[0].contentViewController as? DocumentViewController
}
override init() {
super.init()
// Add your subclass-specific initialization here.
}
override class var autosavesInPlace: Bool {
return true
}
override func makeWindowControllers() {
// Returns the Storyboard that contains your Document window.
let storyboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil)
let windowController = storyboard.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("Document Window Controller")) as! NSWindowController
self.addWindowController(windowController)
}
override func data(ofType typeName: String) throws -> Data {
// Save the text view contents to disk
if let textView = documentViewController?.textView {
let rangeLength = textView.string.count
textView.breakUndoCoalescing()
let textRange = NSRange(location: 0, length: rangeLength)
if let contents = textView.rtf(from: textRange) {
return contents
}
}
throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
}
override func read(from data: Data, ofType typeName: String) throws {
if let contents = NSAttributedString(rtf: data, documentAttributes: nil) {
text = contents
}
}
//SecondViewController
override func viewDidAppear() {
let document = self.view.window?.windowController?.document as! Document
textView.textStorage?.setAttributedString(document.text)
}
The way you have things set up in your app, you are not going to be able to put an entire NSDocument's contents in a text view. Suppose you have 10 items in the table view and selecting an item fills the text view with some text. You're not going to be able to have a NSDocument for each of the 10 table view items inside a single document.
What you are going to have to do is create a data structure that represents a text file. Think of a chapter in a book or a scene in a screenplay. In your NSDocument subclass, you will have an array of these data structures. When you save the document, you will save it as a file wrapper, which is a directory of files that looks like a single file in the Finder. There will be one text file in the file wrapper for each item in the table view. Refer to the following article to learn more about file wrappers:
Working with File Wrappers in Swift
Now what you want to know is how to fill the text view when the table view selection changes. This is painful to do with Mac storyboards because the table view's controller and the text view's controller are separate scenes. Implement the delegate method tableViewSelectionDidChange in the view controller for the table view. Use the parent property to get the split view controller. Use the split view controller to get the text view's controller. Pass the selected row to the text view's controller and use that to get the right text file to display in the text view.
The split view controller should store a reference to the NSDocument. Use the parent property to access the split view controller from the text view's controller. With access to the split view controller, you can access the document to fill the text view with the contents of the text file corresponding to the selected item in the table view.

Relaunch view when app is closed

I am creating a status bar app for Mac with a settings view.
I have created a NSMenuItem to launch the settings but I don't find any solutions to launch this view.
What I have tried:
NSWorkspace.shared().launchApplication("AppName")
and
StatusMenuController.swift
func showSettings() {
var mainWindow: MainWindowController!
mainWindow = MainWindowController()
mainWindow.showWindow(nil)
}
MainWindowController.swift
override func windowDidLoad() {
super.windowDidLoad()
self.window?.center()
self.window?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
I saw this problem was faced in many cases and I found a solution that I don't completely understand but that I'll try to explain.
Here is how documents are managed in macOS :
From my experience, you need to check the box "Create Document-Based Application" when creating a new Mac app to have a NSDocument class (You can also add it later) which will handle how your views are being showed or not.
Adding this NSDocument class to my project made the following code work (and wasn't before) :
let storyboard = NSStoryboard(name: "Main", bundle: nil)
var windowController: NSWindowController!
windowController = storyboard.instantiateController(withIdentifier: "ScanWindowController") as! NSWindowController
windowController.showWindow(nil)

What is the correct way to make a NSWindowController Singleton in Swift?

I have a sample project as:
https://github.com/ericgorr/nspanel_show.git
My project is a storyboard, document based application. I would like to use a custom segue to toggle the visible state of the inspector window. What I have should work, but I cannot quite determine how to make the inspector window a singleton.
I believe I should start with:
class InspectorWindowController: NSWindowController
{
static let sharedInstance = InspectorWindowController()
// override func init()
// {
//
// }
override func windowDidLoad()
{
super.windowDidLoad()
NSLog( ":::: %#", InspectorWindowController.sharedInstance );
}
}
But exactly what the initialization should look like in my situation is escaping me, especially since the window is inside of a storyboard.
You can select the window controller from the window controller scene and in the attributes inspector select Single from the pop up under Presentation. This will ensure the show segue only uses a single instance of the window controller. See this answer for more information.
Here's how I would modify your code:
In Main.storyboard give your InspectorWindowController an identifier, such as "Inspector Window Controller"
In InspectorWindowController, implement your singleton as follows:
static let shared: InspectorWindowController = {
let storyboard = NSStoryboard(name:"Main", bundle: nil)
let controller = storyboard.instantiateController(withIdentifier: "Inspector Window Controller")
return controller as! InspectorWindowController
}()
In Main.storyboard delete the segue from WindowController to InspectorWindowController
In WindowController replace the showMyPanel() and hideMyPanel() IBActions with:
#IBAction func toggleInspectorPanel( _ sender: AnyObject ) {
let inspectorWindow = InspectorWindowController.shared.window!
if inspectorWindow.isVisible {
inspectorWindow.orderOut(self)
} else {
inspectorWindow.makeKeyAndOrderFront(self)
}
}
Also in WindowController, remove the NSLog() call from windowDidLoad(). It causes a recursive call to the InspectorWindowController.shared initialization code.
In Main.storyboard link the Inspector toolbar button to toggleInspectorPanel()
The InspectorWindowController.shared singleton will be initialized, and the inspector panel loaded (but not shown), the first time it is referenced.