NSDocument Tab Window Restoration - swift

This question deals with tab window restoration in a document-based app.
In an OSX, document-based app, which allows a user to create and convert tab windows, I need to preserve and restore the 'tab' state of each window.
Currently, my document controller restores its documents windows, but not the tab deployment; I get back individual windows; I can merge all back into one, but this is too heavy-handed as their former groupings are lost.
My app document class's - makeWindowControllers() function is where I affect the new controllers, whether they should cascade, which I'd read be false, during restore:
// Determine cascade based on state of application delegate
controller.shouldCascadeWindows = <app did receive applicationWillFinishLaunching>
so it would be false until it's finished launching.
Finally, my window's class features methods:
override func addTabbedWindow(_ window: NSWindow, ordered: NSWindow.OrderingMode) {
super.addTabbedWindow(window, ordered: ordered)
window.invalidateRestorableState()
}
override func moveTabToNewWindow(_ sender: Any?) {
super.moveTabToNewWindow(sender)
self.invalidateRestorableState()
}
override func encodeRestorableState(with coder: NSCoder) {
if let tabGroup = self.tabGroup {
let tabIndex = tabGroup.windows.firstIndex(of: self)
coder.encode(tabIndex, forKey: "tabIndex" )
Swift.print("<- tabIndex: \(String(describing: tabIndex))")
}
}
override func restoreState(with coder: NSCoder) {
let tabIndex = coder.decodeInt64(forKey: "tabIndex")
Swift.print("-> tabIndex: \(tabIndex)")
}
to invalidate the window restore state when the tab state is changed. But I'm not sure with the NSWindowRestoration protocol implementation, who or what needs to implement the protocol when a document controller is involved.
I think this is the reason the last function is never called. I get debug output about the encoding but during the next app execution the restoreStore(coder:) function is never called.
So who implements this window restore protocol in such an environment I guess is my question, or a decent example doing so.

My question reveals you not require anything special for a document based app; I've updated my prototype which features this support and environment here SimpleViewer, which features Swift5, a document based app supporting tabs.

Related

Xcode warning: 'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead

when updating my App's Deployment target to 15.0, i receive this warning:
'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a
relevant window scene instead
I have tried to look on the net what could be done to remediate this, but couldn't find much info on this. Hope you could share some advice.
The line of code i am using where this alert occurred is:
let window = UIApplication.shared.windows[0]
followed by in my ViewDidLoad():
DispatchQueue.main.async { [weak self] in
if defaults.bool(forKey: "darkModeBoolSwitch") == true {
self?.window.overrideUserInterfaceStyle = .dark
} else if defaults.bool(forKey: "darkModeBoolSwitch") == false {
self?.window.overrideUserInterfaceStyle = .light
}
An alternative to #DuncanC's solution that may also work for you: UIApplication has a connectedScenes property, which lists all of the currently-active scenes doing work in your application (for most applications, this is just the one main scene).
Of those scenes, you can filter for scenes which are UIWindowScenes (ignoring scenes which are not currently active and in the foreground), and of those, find the first scene which has a window which is key:
extension UIApplication {
static var firstKeyWindowForConnectedScenes: UIWindow? {
UIApplication.shared
// Of all connected scenes...
.connectedScenes.lazy
// ... grab all foreground active window scenes ...
.compactMap { $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil }
// ... finding the first one which has a key window ...
.first(where: { $0.keyWindow != nil })?
// ... and return that window.
.keyWindow
}
}
I hesitate to call this extension something like UIApplication.keyWindow because the reason for the deprecation of these APIs is because of the generalization to multi-scene applications, each of which may have its own key window... But this should work.
If you still need to support iOS 14, which does not have UIWindowScene.keyWindow, you can replace the firstWhere(...)?.keyWindow with: flatMap(\.windows).first(where: \.isKeyWindow).
I am out-of-date with Apple's recent changes to implement scenes.
I did a little digging, and found a protocol UIWindowSceneDelegate
It looks like you are supposed to add an "Application Scene Manifest" to your app's info.plist file that tells the system the class that serves as the app's window scene delegate.
Then in that class you want to implement the method scene(_:willConnectTo:options:). When that method is called you sould try to cast the UIScene that's passed to you to to a UIWindowScene, and if that cast succeeds, you can ask the window scene for it's window and save it to an instance property.
That should allow you to save a pointer to your app's window and use it when needed.

KVO on NSStatusItem.isVisible fires twice

I'm trying to use key value observation to determine when an NSStatusItem is dragged out of the menu bar by the user with the removalAllowed behavior. This is supported according to the docs:
Status items with this behavior allow interactive removal from the menu bar. Upon removal, the item’s isVisible property changes to false. This change is observable using key-value observation.
However, the callback function seems to fire twice whenever the isVisible property is changed. Here's a minimal example (assume statusItem and observer are variables that are retained for the lifetime of the app, e.g. on AppDelegate).
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
statusItem.button!.image = NSImage(named: NSImage.addTemplateName)
statusItem.behavior = .removalAllowed
observer = statusItem.observe(\.isVisible, options: [.old, .new]) { object, change in
print("oldValue: \(change.oldValue!) newValue: \(change.newValue!)")
}
If you drag the icon out of the menu bar, it will print the following:
oldValue: false newValue: true
oldValue: false newValue: true
I've looked through every property on the change object and as far as I can tell, they're all identical, so there'd be no easy way to discard the duplicate event. I've also messed with the prior option, which doesn't seem to help either.
A smart way to get rid of duplicates is to replace the KVO observer with a Combine publisher.
import Combine
var cancellable : AnyCancellable?
cancellable = statusItem.publisher(for: \.isVisible)
.removeDuplicates()
.sink { value in
print(value)
}
If you are interested in the old value you could add the Scan operator

How to detect key press and release in swiftUI (macOS)

Not much to say other than the title. I want to be able to take action in a swiftUI view when a key is pressed and when it is released (on macOS). Is there any good way to do this in swiftUI and if not is there any workaround?
Unfortunately keyboard event handling is one of those areas where it's painfully obvious that SwiftUI was designed first and foremost for iOS, with macOS being an afterthought.
If the key you're trying to detect is a modifier to a mouse click, such as cmd, option, or shift, you can use the .modifiers with onTapGesture to distinguish it from an unmodified onTapGesture. In that case, my experience with it is that you want the .onTapGesture call that uses .modifiers to precede the unmodified one.
Handling general key events for arbitrary views requires going outside of SwiftUI.
If you just need it for one View, one possibility is to implement that view with AppKit so you can receive the keyboard events via the ordinary Cocoa firstResponder mechanism, and then wrap that view in SwiftUI's NSViewRepresentable. In that case your wrapped NSView would update some #State property in NSViewRespresentable. A lot of developers using SwiftUI for macOS do it this way. While this is fine for a small number of views, if it turns out that you have to implement a lot of views in AppKit to make them usable in SwiftUI, then you're kind of defeating the point of using SwiftUI anyway. In that case, just make it an ordinary Cocoa app.
But there is another way...
You could use another thread that uses CGEventSource to poll the keyboard state actively in conjunction with a SwiftUI #EnvironmentObject or #StateObject to communicate keyboard state changes to the SwiftUI Views that are interested in them.
Let's say you want to detect when the up-arrow is pressed. To detect the key, I use an extension on CGKeyCode.
import CoreGraphics
extension CGKeyCode
{
// Define whatever key codes you want to detect here
static let kVK_UpArrow: CGKeyCode = 0x7E
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
}
Of course, you have to use the right key codes. I have a gist containing all of the old key codes. Rename them to be more Swifty if you like. The names listed go back to classic MacOS and were defined in Inside Macintosh.
With that extension defined, you can test if a key is pressed anytime you like:
if CGKeyCode.kVK_UpArrow.isPressed {
// Do something in response to the key press.
}
Note these are not key-up or key-down events. It's simply a boolean detecting if the key is pressed when you perform the check. To behave more like events, you'll need to do that part yourself by keeping track of key state changes.
There are multiple ways of doing this, and the following code is not meant to imply that this is the "best" way. It is simply a way. In any case, something like the following code would go (or be called from) wherever you do global initialization when you app starts.
// These will handle sending the "event" and will be fleshed
// out further down
func dispatchKeyDown(_ key: CGKeyCode) {...}
func dispatchKeyUp(_ key: CGKeyCode) {...}
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate let sleepSem = DispatchSemaphore(value: 0)
fileprivate let someConcurrentQueue = DispatchQueue(label: "polling", attributes: .concurrent)
someConcurrentQueue.async
{
while true
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
// Sleep long enough to avoid wasting CPU cycles, but
// not so long that you miss key presses. You may
// need to experiment with the .milliseconds value.
let_ = sleepSem.wait(timeout: .now() + .milliseconds(50))
}
}
The idea is just to have some code that periodically polls key states, compares them with previous states, dispatches an appropriate "event" when they change, and updates the previous states. The code above does that by running an infinite loop in a concurrent task. It requires creating a DispatchQueue with the .concurrent attribute. You can't use it on DispatchQueue.main because that queue is serial not concurrent, so the infinite loop would block the main thread, and the program would become unresponsive. If you already have a concurrent DispatchQueue you use for other reasons, you can just use that one instead of creating one just for polling.
However, any code that accomplishes the basic goal of periodic polling will do, so if you don't already have a concurrent DispatchQueue and would prefer not to create one just to poll for keyboard states, which would be a reasonable objection, here's an alternate version that uses DispatchQueue.main with a technique called "async chaining" to avoid blocking/sleeping:
fileprivate var keyStates: [CGKeyCode: Bool] =
[
.kVK_UpArrow: false,
// populate with other key codes you're interested in
]
fileprivate func pollKeyStates()
{
for (code, wasPressed) in keyStates
{
if code.isPressed
{
if !wasPressed
{
dispatchKeyDown(code)
keyStates[code] = true
}
}
else if wasPressed
{
dispatchKeyUp(code)
keyStates[code] = false
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50))
{
// The infinite loop from previous code is replaced by
// infinite chaining.
pollKeyStates()
}
}
// Start up key state polling
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) {
pollKeyStates()
}
With code in place to detect when keys are pressed, you now need a way to communicate that to your SwiftUI Views. Again, there's more than one way to skin that cat. Here's an overly simplistic one that will update a View whenever the up-arrow is pressed, but you'll probably want to implement something a bit more sophisticated... probably something that allows views to specify what keys they're interested in responding to.
class UpArrowDetector: ObservableObject
{
#Published var isPressed: Bool = false
}
let upArrowDetector = UpArrowDetector()
func dispatchKeyDown(_ key: CGKeyCode)
{
if key == .kVK_UpArrow {
upArrowDetector.isPressed = true
}
}
func dispatchKeyUp(_ key: CGKeyCode) {
if key == .kVK_UpArrow {
upArrowDetector.isPressed = false
}
}
// Now we hook it into SwiftUI
struct UpArrowDetectorView: View
{
#StateObject var detector: UpArrowDetector
var body: some View
{
Text(
detector.isPressed
? "Up-Arrow is pressed"
: "Up-Arrow is NOT pressed"
)
}
}
// Use the .environmentObject() method of `View` to inject the
// `upArrowDetector`
struct ContentView: View
{
var body: some View
{
UpArrowDetectorView()
.environmentObject(upArrowDetector)
}
}
I've put a full, compilable, and working example at this gist patterned on code you linked to in comments. It's slightly refactored from the above code, but all the parts are there, including starting up the polling code.
I hope this points you in a useful direction.

Initial app launch from storyboard fails to call NSDocument init

I've written a macOS Document-type app with a storyboard, using the Xcode template, and somewhere along the line the association between the initial app launch and the document has varied from the expected pattern such that none of the NSDocument initializers I expect are called when the app first launches (but are called every new window thereafter).
I've subclassed all four documented NSDocument initializers, as follows:
public class Simulation: NSDocument {
override init() {
debugPrint("--------------------\(#file)->\(#function) called")
super.init()
}
init(contentsOf: URL, ofType: String) throws {
debugPrint("--------------------\(#file)->\(#function) called")
fatalError()
}
init(for: URL?, withContentsOf: URL, ofType: String) throws {
debugPrint("--------------------\(#file)->\(#function) called")
fatalError()
}
convenience init(type: String) throws {
debugPrint("--------------------\(#file)->\(#function) called, type: \(type)")
self.init()
}
public override class func autosavesInPlace() -> Bool {
debugPrint("--------------------\(#file)->\(#function) called")
return false
}
}
None of the inits exhibit the debugPrint output when the app launches. The app window is created successfully on launch, with no apparent document association.
However, I notice some really odd behavior I can't explain:
Although I've seen no init call, autosavesInPlace is called three times after the app starts on some instance of the document
When I use cmd-N (i.e., File->New and therefore newDocument()) to create a new document, autosavesInPlace is called three more times, and then the document init is executed!
I never see a call to makeWindowControllers()
My NSDocument subclass is named Simulation. The anomaly seems to be that there's some magic in the initial startup that bypasses Simulation.init, but calls it every document+window creation thereafter.
Here are my questions:
Why does the initial launch not call Simulation.init()?
How does autosavesInPlace find an instance of Simulation to call when there's only that initial, seemingly partially constructed window?
In your storyboard, make sure both your Window Controller and its Content View Controller have Is Initial Controller unchecked and Presentation set to Multiple in the Attributes Inspector.
Having Is Initial Controller checked will cause the application to instantiate one window controller before any of the NSDocument/NSDocumentController "magic" happens. Presentation: Multiple should be selected for coherence, although it might not really make a difference.
Also, make sure your document types are properly setup in Info.plist, particularly the NSDocumentClass key (should contain $(PRODUCT_MODULE_NAME).Simulation).
I believe your question about autosavesInPlace is already answered in the comments…

Sync two web views

I have a need to sync two web views, so that anything that happens in one web view happens simultaneously in the other.
I have tried various ways, without success, and the more I try the more convoluted and likely bug ridden this is getting.
I feel there maybe a very simple way to do this, but can not figure it out.
One of the things that I note is not allowed is having the same NSTextField as the "takeStringURLFrom"
override func controlTextDidEndEditing(obj: NSNotification) {
webViewLeft.takeStringURLFrom(hiddenTextField)
webViewRight.takeStringURLFrom(urlField)
}
override func webView(sender: WebView!, didCommitLoadForFrame frame: WebFrame!) {
if frame == sender.mainFrame {
urlField.stringValue = sender.mainFrameURL
hiddenTextField.stringValue = sender.mainFrameURL
webViewRight.takeStringURLFrom(urlField)
webViewLeft.takeStringURLFrom(hiddenTextField)
printLn("realised just creating an infinite loop here")
}
}
I don't like this but it appears to work as needed. I think it needs some refinement. Using a hidden text field to mimic the url for the second web view and tie each web view to there respective text fields via the web view's referenced action "takeStringUrlFrom"
override func webView(sender: WebView!, didStartProvisionalLoadForFrame frame: WebFrame!) {
if frame == sender.mainFrame {
urlField.stringValue = sender.mainFrameURL
hiddenTextField.stringValue = sender.mainFrameURL
window.makeFirstResponder(nil)
window.makeFirstResponder(hiddenTextField)
window.makeFirstResponder(nil)
window.makeFirstResponder(urlField)
window.makeFirstResponder(nil)
}
}