I have code that opens a window and displays a view on a connected display. My goal is to detect a connection/disconnection of a connected display and show/remove the view accordingly. I have that part working fine.
The problem I am having is closing the window upon disconnection, but then if a subsequent connection is made, and upon creating the window and view again, I get a EXC_BAD_ACCESS error.
I tried a different approach by setting the connectedDisplayWindow and connectedDisplayView to nil, after calling close() on the window when a connected display is removed. Maybe I am misunderstanding the close() method?
Apple Documentation
If the window is set to be released when closed, a release message is sent to the object after the current event is completed. For an NSWindow object, the default is to be released on closing, while for an NSPanel object, the default is not to be released. You can use the isReleasedWhenClosed property to change the default behavior...
Just to make sure, I tried setting the isReleasedWhenClosed to true, but it did not change the problem.
The other thing I see in the console is about seven repeated error strings immediately upon disconnection of the connected display: 2022-04-10 10:28:11.044155-0500 External Display[95744:4934855] [default] invalid display identifier 67EE0C44-4E3D-3AF2-3447-A867F9FC477D before the notification is fired, and one more after the notification occurs: 2022-04-10 10:28:11.067555-0500 External Display[95744:4934855] [default] Invalid display 0x4e801884. Could these be related to the issues I am having?
Full example code:
ViewController.swift
import Cocoa
let observatory = NotificationCenter.default
class ViewController: NSViewController {
var connectedDisplay: NSScreen?
var connectedDisplayWindow: NSWindow?
var connectedDisplayView: NSView?
var connectedDisplayCount: Int = 0
var connectedDisplayID: UInt32 = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupObservatory()
if NSScreen.screens.count > 1 {
handleDisplayConnectionChange(notification: nil)
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
override func viewWillDisappear() {
connectedDisplayWindow?.close()
}
func setupObservatory() {
observatory.addObserver(self, selector: #selector(handleDisplayConnectionChange), name: NSApplication.didChangeScreenParametersNotification, object: nil)
observatory.addObserver(forName: .setupConnectedDisplayWindow, object: nil, queue: nil, using: setupConnectedDisplayWindow)
}
#objc func handleDisplayConnectionChange(notification: Notification?) {
if connectedDisplayCount != NSScreen.screens.count {
if connectedDisplayCount < NSScreen.screens.count {
print("There is a connected display.")
connectedDisplayCount = NSScreen.screens.count
if let _ = NSScreen.screens.last {
if connectedDisplay != NSScreen.screens.last {
connectedDisplayID = NSScreen.screens.last!.displayID!
connectedDisplay = NSScreen.screens.last!
}
} else {
connectedDisplayID = 0
}
if connectedDisplayID != 0 && !connectedDisplayIsActive {
observatory.post(name: .setupConnectedDisplayWindow, object: nil)
}
} else if connectedDisplayCount > NSScreen.screens.count {
print("A connected display was removed.")
connectedDisplayCount = NSScreen.screens.count
connectedDisplayIsActive = false
connectedDisplayWindow?.close()
//connectedDisplayView = nil <- causes error #main in AppDelegate
//connectedDisplayWindow = nil <- causes error #main in AppDelegate
connectedDisplay = nil
connectedDisplayID = 0
}
}
}
func setupConnectedDisplayWindow(notification: Notification) {
if NSScreen.screens.count > 1 && !connectedDisplayIsActive {
connectedDisplay = NSScreen.screens.last
let mask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
connectedDisplayWindow = NSWindow(contentRect: connectedDisplay!.frame, styleMask: mask, backing: .buffered, defer: true, screen: connectedDisplay) // <- causes error on subsequent connection
connectedDisplayWindow?.level = .normal
connectedDisplayWindow?.isOpaque = false
connectedDisplayWindow?.backgroundColor = .clear
connectedDisplayWindow?.hidesOnDeactivate = false
let viewRect = NSRect(x: 0, y: 0, width: connectedDisplay!.frame.width, height: connectedDisplay!.frame.height)
connectedDisplayView = ConnectedDisplayView(frame: viewRect)
connectedDisplayWindow?.contentView = connectedDisplayView
connectedDisplayWindow?.orderFront(nil)
connectedDisplayView?.window?.toggleFullScreen(self)
connectedDisplayIsActive = true
observatory.post(name: .setupConnectedDisplayView, object: nil)
}
}
}
extension Notification.Name {
static var setupConnectedDisplayWindow: Notification.Name {
return .init(rawValue: "ViewController.setupConnectedDisplayView")
}
static var setupConnectedDisplayView: Notification.Name {
return .init(rawValue: "ConnectedDisplayView.setupConnectedDisplayView")
}
}
extension NSScreen {
var displayID: CGDirectDisplayID? {
return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID
}
}
ConnectedDisplayView.swift
import Cocoa
var connectedDisplayIsActive: Bool = false
class ConnectedDisplayView: NSView {
var imageView: NSImageView!
override init(frame: NSRect) {
super.init(frame: frame)
setupObservatory()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupObservatory() {
observatory.addObserver(forName: .setupConnectedDisplayView, object: nil, queue: nil, using: setupConnectedDisplayView)
}
func setupConnectedDisplayView(notification: Notification) {
let imageURL = URL(fileURLWithPath: "/Users/Shared/my image.png")
if let image = NSImage(contentsOf: imageURL) {
imageView = NSImageView(image: image)
imageView.wantsLayer = true
imageView.frame = self.frame
imageView.alphaValue = 1
self.addSubview(imageView)
}
}
}
I commented out the nil settings for the connectedDisplayWindow and connectedDisplayView objects and the error at #main in AppDelegate went away, but then I get an error when trying to reinitialize the connectedDisplayWindow if the connected display is removed or the connection is momentarily interrupted.
The default value of isReleasedWhenClosed is true and connectedDisplayWindow?.close() releases the window. Setting connectedDisplayWindow to nil or to another window releases the window again and causes a crash. Solution: set isReleasedWhenClosed to false.
Related
I want to save the content of the text view when the user closes the app.
I used the following codes to do so, but I cannot get the up-to-date string of the textview when closing the app. So, the produced text file is blank.
How should I access to the NSTextView from AppDelegate to save its content?
ViewController.swift
import Cocoa
class ViewController: NSViewController {
static var textViewString: String = ""
#IBOutlet var textView: NSTextView!{
didSet{
ViewController.textViewString = textView.string
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// start with hidden and show after moving to the main screen
DispatchQueue.main.async {
//keep the window top
self.view.window?.level = .floating
//set up the main display as the display where window shows up
let screens = NSScreen.screens
var pos = NSPoint()
pos.x = screens[0].visibleFrame.midX
pos.y = screens[0].visibleFrame.midY
self.view.window?.setFrameOrigin(pos)
self.view.window?.zoom(self)
self.view.window?.level = .floating
//self.view.window?.backgroundColor = NSColor.white
//stop the user from moving window
self.view.window?.isMovable = false
//disable resizable mode
self.view.window?.styleMask.remove(.resizable)
self.view.window?.setIsVisible(true)
}
//set up font for the reflectionForm
textView.font = NSFont.systemFont(ofSize: 30)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func saveTextViewString(){
if let documentDirectoryFileURL = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
let fileName = "savedText.txt"
let targetTextFilePath = documentDirectoryFileURL + "/" + fileName
do {
try ViewController.textViewString.write(toFile: targetTextFilePath, atomically: true, encoding: String.Encoding.utf8)
print("successfully recorded: \(ViewController.textViewString.description) at \(fileName.utf8CString)")
} catch let error as NSError {
print("failed to write: \(error)")
}
}
}
}
AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//save the string in the textview into a text file
ViewController().saveTextViewString()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
Thank you for the #jnpdx's comments, I was able to solve this by just declaring ViewController in the AppDelegate by stating var viewController: ViewController!
ViewController.swift
import Cocoa
class ViewController: NSViewController {
#IBOutlet var textView: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// start with hidden and show after moving to the main screen
DispatchQueue.main.async {
//keep the window top
self.view.window?.level = .floating
//set up the main display as the display where window shows up
let screens = NSScreen.screens
var pos = NSPoint()
pos.x = screens[0].visibleFrame.midX
pos.y = screens[0].visibleFrame.midY
self.view.window?.setFrameOrigin(pos)
self.view.window?.zoom(self)
self.view.window?.level = .floating
//self.view.window?.backgroundColor = NSColor.white
//stop the user from moving window
self.view.window?.isMovable = false
//disable resizable mode
self.view.window?.styleMask.remove(.resizable)
self.view.window?.setIsVisible(true)
}
//set up font for the reflectionForm
textView.font = NSFont.systemFont(ofSize: 30)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func saveTextViewString(){
if let documentDirectoryFileURL = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last {
let fileName = "savedText.txt"
let targetTextFilePath = documentDirectoryFileURL + "/" + fileName
do {
try textView.string.write(toFile: targetTextFilePath, atomically: true, encoding: String.Encoding.utf8)
print("successfully recorded: \(textView.string.description) at \(fileName.utf8CString)")
} catch let error as NSError {
print("failed to write: \(error)")
}
}
}
}
AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
//connect viewController with ViewController
var viewController: ViewController!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
//save the string in the textview into a text file
viewController.saveTextViewString()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
I already tried both loadingview.removeFromSuperView and loadingView.isHidden = true
Yes, it removes or hides the view, but I can't click on my root view anymore.
I also tried animatonview.background = .forceFinish, but doesn't do the job.
import UIKit
import Lottie
class LoadingAnimationView: UIView {
#IBOutlet weak var loadingView: UIView!
let animationView = AnimationView()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
}
func loadAnimation() {
let animation = Animation.named("success")
animationView.animation = animation
animationView.contentMode = .scaleAspectFill
loadingView.addSubview(animationView)
animationView.backgroundBehavior = .pauseAndRestore
animationView.translatesAutoresizingMaskIntoConstraints = false
animationView.topAnchor.constraint(equalTo: loadingView.layoutMarginsGuide.topAnchor).isActive = true
animationView.leadingAnchor.constraint(equalTo: loadingView.leadingAnchor, constant: 0).isActive = true
animationView.bottomAnchor.constraint(equalTo: loadingView.bottomAnchor).isActive = true
animationView.trailingAnchor.constraint(equalTo: loadingView.trailingAnchor, constant:0).isActive = true
animationView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
animationView.play(fromProgress: 0,
toProgress: 1,
loopMode: .playOnce,
completion: { (finished) in
if finished {
print("Animation Complete")
//please put solution here? dismiss or end loadingView or animationView
} else {
print("Animation cancelled")
}
})
}
EDIT 2:
I'm using the loadingView when the success message is received or 200.
func goOnlineMode(){
APIManager.sharedInstance.fetchServerStatus(completion: { data, error in
if error != nil{
print("Connection Failed")
} else {
if data?.status == 200 || data?.msg == "success" {
print("connected")
loadAnimation(true)
self.setCloudStateValue(value: true)
self.vc.cloudstateChecker()
} else {
print("fail to connect")
}
}
})
}
}
this is my function loading boolean in loadAnimation for Loading the xib.
func loadAnimation(_ display: Bool) {
if (display) {
let window = UIApplication.shared.keyWindow!
if Singleton.animationView == nil {
if let view = Bundle.main.loadNibNamed("LoadingAnimationView", owner: window, options:nil)![0] as? LoadingAnimationView {
Singleton.animationView = view
Singleton.animationView?.frame.size = CGSize(width: window.bounds.width, height: window.bounds.height)
window.addSubview(Singleton.animationView!)
window.layoutIfNeeded()
Singleton.animationView?.loadAnimation()
Singleton.animationView?.translatesAutoresizingMaskIntoConstraints = false
Singleton.animationView?.leftAnchor.constraint(equalTo: window.leftAnchor).isActive = true
Singleton.animationView?.rightAnchor.constraint(equalTo: window.rightAnchor).isActive = true
Singleton.animationView?.topAnchor.constraint(equalTo: window.topAnchor, constant:-60).isActive = true
Singleton.animationView?.bottomAnchor.constraint(equalTo: window.bottomAnchor).isActive = true
window.layoutIfNeeded()
}
}
} else {
if (Singleton.animationView != nil) {
Singleton.animationView?.removeFromSuperview()
Singleton.animationView = nil
}
}
}
Try with this:
Swift 5
animationView.play { (finished) in
animationViewNewOrder!.isHidden = true
}
I solved my problem by using NotificationCenter
Swift 4.2
Add this NotificationCenter Observer in your MainViewController, and also register a Notification.Name to your Constants
NotificationCenter.default.addObserver(self, selector: #selector(removeAnimation(notification:)), name: HIDE_ANIMATION, object: nil)
}
also add this together with your observer
#objc func removeAnimation(notification:NSNotification) {
loadingAnimation(false)
}
I put this Notification Post in my newly created hideAnimation function in LoadingAnimationView.
func hideAnimation() {
NotificationCenter.default.post(name: Notification.Name(HIDE_ANIMATION.rawValue), object: nil)
loadingView.removeFromSuperview()
}
and put the hideAnimation function to your completion finish.
Working in Swift3; I've got a pretty expensive operation running in a loop iterating through stuff and building it into an array that on return would be used as the content for an NSTableView.
I wanted a modal sheet showing progress for this so people don't think the app is frozen. By googling, looking around in here and not a small amount of trial and error I've managed to implement my progressbar and have it show progress adequately as the loop progresses.
The problem right now? Even though the sheet (implemented as an NSAlert, the progress bar is in the accesory view) works exactly as expected, the whole thing returns before the loop is finished.
Here's the code, hoping somebody can tell me what am I doing wrong:
class ProgressBar: NSAlert {
var progressBar = NSProgressIndicator()
var totalItems: Double = 0
var countItems: Double = 0
override init() {
progressBar.isIndeterminate = false
progressBar.style = .barStyle
super.init()
self.messageText = ""
self.informativeText = "Loading..."
self.accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
self.accessoryView?.addSubview(progressBar)
self.layout()
self.accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
self.addButton(withTitle: "")
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width:290, height: 16))
progressBar.usesThreadedAnimation = true
self.beginSheetModal(for: ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window!, completionHandler: nil)
}
}
static var allUTIs: [SWDAContentItem] = {
var wrappedUtis: [SWDAContentItem] = []
let utis = LSWrappers.UTType.copyAllUTIs()
let a = ProgressBar()
a.totalItems = Double(utis.keys.count)
a.progressBar.maxValue = a.totalItems
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
a.countItems += 1.0
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
Thread.sleep(forTimeInterval:0.0001)
DispatchQueue.main.async {
a.progressBar.doubleValue = a.countItems
if (a.countItems >= a.totalItems && a.totalItems != 0) {
ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window?.endSheet(a.window)
}
}
}
}
Swift.print("We'll return now...")
return wrappedUtis // This returns before the loop is finished.
}()
In short, you're returning wrappedUtis before the asynchronous code has had a chance to finish. You cannot have the initialization closure return a value if the update process itself is happening asynchronously.
You clearly successfully diagnosed a performance problem in the initialization of allUTIs, and while doing this asynchronously is prudent, you shouldn't be doing that in that initialization block of the allUTIs property. Move this code that initiates the update of allUTIs into a separate function.
Looking at ProgressBar, it's really an alert, so I'd call it ProgressAlert to make that clear, but expose the necessary methods to update the NSProgressIndicator within that alert:
class ProgressAlert: NSAlert {
private let progressBar = NSProgressIndicator()
override init() {
super.init()
messageText = ""
informativeText = "Loading..."
accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
accessoryView?.addSubview(progressBar)
self.layout()
accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
addButton(withTitle: "")
progressBar.isIndeterminate = false
progressBar.style = .barStyle
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width:290, height: 16))
progressBar.usesThreadedAnimation = true
}
/// Increment progress bar in this alert.
func increment(by value: Double) {
progressBar.increment(by: value)
}
/// Set/get `maxValue` for the progress bar in this alert
var maxValue: Double {
get {
return progressBar.maxValue
}
set {
progressBar.maxValue = newValue
}
}
}
Note, this doesn't present the UI. That's the job of whomever presented it.
Then, rather than initiating this asynchronous population in the initialization closure (because initialization should always be synchronous), create a separate routine to populate it:
var allUTIs: [SWDAContentItem]?
private func populateAllUTIs(in window: NSWindow, completionHandler: #escaping () -> Void) {
let progressAlert = ProgressAlert()
progressAlert.beginSheetModal(for: window, completionHandler: nil)
var wrappedUtis = [SWDAContentItem]()
let utis = LSWrappers.UTType.copyAllUTIs()
progressAlert.maxValue = Double(utis.keys.count)
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
DispatchQueue.main.async { [weak progressAlert] in
progressAlert?.increment(by: 1)
}
}
DispatchQueue.main.async { [weak self, weak window] in
self?.allUTIs = wrappedUtis
window?.endSheet(progressAlert.window)
completionHandler()
}
}
}
Now, you declared allUTIs to be static, so you can tweak the above to do that, too, but it seems like it's more appropriate to make it an instance variable.
Anyway, you can then populate that array with something like:
populateAllUTIs(in: view.window!) {
// do something
print("done")
}
Below, you said:
In practice, this means allUTIs is only actually initiated when the appropriate TabViewItem is selected for the first time (which is why I initialize it with a closure like that). So, I'm not really sure how to refactor this, or where should I move the actual initialization. Please keep in mind that I'm pretty much a newbie; this is my first Swift (also Cocoa) project, and I've been learning both for a couple of weeks.
If you want to instantiate this when the tab is selected, then hook into the child view controllers viewDidLoad. Or you can do it in the tab view controller's tabView(_:didSelect:)
But if the population of allUTIs is so slow, are you sure you want to do this lazily? Why not trigger this instantiation sooner, so that there's less likely to be a delay when the user selects that tab. In that case, you might trigger it the tab view controller's own viewDidLoad, so that the tab that needs those UTIs is more likely to have them.
So, if I were considering a more radical redesign, I might first change my model object to further isolate its update process from any specific UI, but rather to simply return (and update) a Progress object.
class Model {
var allUTIs: [SWDAContentItem]?
func startUTIRetrieval(completionHandler: (() -> Void)? = nil) -> Progress {
var wrappedUtis = [SWDAContentItem]()
let utis = LSWrappers.UTType.copyAllUTIs()
let progress = Progress(totalUnitCount: Int64(utis.keys.count))
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
DispatchQueue.main.async {
progress.completedUnitCount += 1
}
}
DispatchQueue.main.async { [weak self] in
self?.allUTIs = wrappedUtis
completionHandler?()
}
}
return progress
}
}
Then, I might have the tab bar controller instantiate this and share the progress with whatever view controller needed it:
class TabViewController: NSTabViewController {
var model: Model!
var progress: Progress?
override func viewDidLoad() {
super.viewDidLoad()
model = Model()
progress = model.startUTIRetrieval()
tabView.delegate = self
}
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
super.tabView(tabView, didSelect: tabViewItem)
if let item = tabViewItem, let controller = childViewControllers[tabView.indexOfTabViewItem(item)] as? ViewController {
controller.progress = progress
}
}
}
Then the view controller could observe this Progress object, to figure out whether it needs to update its UI to reflect this:
class ViewController: NSViewController {
weak var progress: Progress? { didSet { startObserving() } }
weak var progressAlert: ProgressAlert?
private var observerContext = 0
private func startObserving() {
guard let progress = progress, progress.completedUnitCount < progress.totalUnitCount else { return }
let alert = ProgressAlert()
alert.beginSheetModal(for: view.window!)
progressAlert = alert
progress.addObserver(self, forKeyPath: "fractionCompleted", context: &observerContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let progress = object as? Progress, context == &observerContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
dispatchPrecondition(condition: .onQueue(.main))
if progress.completedUnitCount < progress.totalUnitCount {
progressAlert?.doubleValue = progress.fractionCompleted * 100
} else {
progress.removeObserver(self, forKeyPath: "fractionCompleted")
view.window?.endSheet(progressAlert!.window)
}
}
deinit {
progress?.removeObserver(self, forKeyPath: "fractionCompleted")
}
}
And, in this case, the ProgressAlert only would worry about doubleValue:
class ProgressAlert: NSAlert {
private let progressBar = NSProgressIndicator()
override init() {
super.init()
messageText = ""
informativeText = "Loading..."
accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
accessoryView?.addSubview(progressBar)
self.layout()
accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
addButton(withTitle: "")
progressBar.isIndeterminate = false
progressBar.style = .barStyle
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width: 290, height: 16))
progressBar.usesThreadedAnimation = true
}
/// Set/get `maxValue` for the progress bar in this alert
var doubleValue: Double {
get {
return progressBar.doubleValue
}
set {
progressBar.doubleValue = newValue
}
}
}
I must note, though, that if these UTIs are only needed for that one tab, it raises the question as to whether you should be using a NSAlert based UI at all. The alert blocks the whole window, and you may want to block interaction with only that one tab.
I just made a simple testing app to display keycode of keystrokes along with modifiers. It works fine for 3 keystrokes, then the app crashes. When it crashes, debug console just shows (LLDB) at the end. Any suggestion what might be causing this? Maybe something has to do with thread or pointer, but I'm not sure how I can fix this. I'm including the code below. I'd really appreciate any help! Thanks!
import Cocoa
import Foundation
class ViewController: NSViewController {
#IBOutlet weak var textField: NSTextFieldCell!
let speech:NSSpeechSynthesizer = NSSpeechSynthesizer()
func update(msg:String) {
textField.stringValue = msg
print(msg)
speech.startSpeaking(msg)
}
func bridgeRetained<T : AnyObject>(obj : T) -> UnsafeRawPointer {
return UnsafeRawPointer(Unmanaged.passRetained(obj).toOpaque())
}
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async {
func myCGEventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let parent:ViewController = Unmanaged<ViewController>.fromOpaque(refcon!).takeRetainedValue()
if [.keyDown].contains(type) {
let flags:CGEventFlags = event.flags
let pressed = Modifiers(rawValue:flags.rawValue)
var msg = ""
if pressed.contains(Modifiers(rawValue:CGEventFlags.maskAlphaShift.rawValue)) {
msg+="caps+"
}
if pressed.contains(Modifiers(rawValue:CGEventFlags.maskShift.rawValue)) {
msg+="shift+"
}
if pressed.contains(Modifiers(rawValue:CGEventFlags.maskControl.rawValue)) {
msg+="control+"
}
if pressed.contains(Modifiers(rawValue:CGEventFlags.maskAlternate.rawValue)) {
msg+="option+"
}
if pressed.contains(Modifiers(rawValue:CGEventFlags.maskCommand.rawValue)) {
msg += "command+"
}
if pressed.contains(Modifiers(rawValue:CGEventFlags.maskSecondaryFn.rawValue)) {
msg += "function+"
}
var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
msg+="\(keyCode)"
DispatchQueue.main.async {
parent.update(msg:msg)
}
if keyCode == 0 {
keyCode = 6
} else if keyCode == 6 {
keyCode = 0
}
event.setIntegerValueField(.keyboardEventKeycode, value: keyCode)
}
return Unmanaged.passRetained(event)
}
let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), callback: myCGEventCallback, userInfo: UnsafeMutableRawPointer(mutating: self.bridgeRetained(obj: self))) else {
print("failed to create event tap")
exit(1)
}
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
CFRunLoopRun()
}
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
The main problem is the reference counting: You create a retained
reference to the view controller when installing the event handler, this happens exactly once.
Then you consume a reference in the callback, this happens for every
tap event. Therefore the reference count drops to zero eventually and
the view controller is deallocated, causing a crash.
Better pass unretained references to the callback, and take care that
the event handler is uninstalled when the view controller is deallocated.
Also there is no need to create a separate runloop for an OS X application, or to asynchronously dispatch the handler creation.
Make the callback a global function, not a method. Use
takeUnretainedValue() to get the view controller reference:
func myCGEventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let viewController = Unmanaged<ViewController>.fromOpaque(refcon!).takeUnretainedValue()
if type == .keyDown {
var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
let msg = "\(keyCode)"
DispatchQueue.main.async {
viewController.update(msg:msg)
}
if keyCode == 0 {
keyCode = 6
} else if keyCode == 6 {
keyCode = 0
}
event.setIntegerValueField(.keyboardEventKeycode, value: keyCode)
}
return Unmanaged.passRetained(event)
}
In the view controller, keep a reference to the run loop source
so that you can remove it in deinit, and use
passUnretained() to pass a pointer to the view controller to
the callback:
class ViewController: NSViewController {
var eventSource: CFRunLoopSource?
override func viewDidLoad() {
super.viewDidLoad()
let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
let userInfo = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
if let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap,
options: .defaultTap, eventsOfInterest: CGEventMask(eventMask),
callback: myCGEventCallback, userInfo: userInfo) {
self.eventSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), self.eventSource, .commonModes)
} else {
print("Could not create event tap")
}
}
deinit {
if let eventSource = self.eventSource {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, .commonModes)
}
}
// ...
}
Another option would be to install/uninstall the event handler in
viewDidAppear and viewDidDisappear.
I have been trying to display a toast message on iOS. What I did was when any notification comes, just I took the navigation controller view and added a subview for my toast message and displayed.
UIView *top_view = self.navigationController.view;
[top_view showToast:string];
Everything works fine. However my toast view is not adding over the keyboard(if the keyboard is at the front). Any idea what could be the problem... Little helps may save my time... Thanx..
You can display the toast by adding subview to your main window.
UIWindow *toastDisplaywindow = [[[UIApplication sharedApplication] delegate] window];;
for (UIWindow *testWindow in [[UIApplication sharedApplication] windows])
{
if (![[testWindow class] isEqual:[UIWindow class]])
{
self.toastDisplaywindow = testWindow;
break;
}
}
[toastDisplaywindow showToast:string];
If a keyboard is being displayed, it will be displayed as a separate window, above your usual main window. Hence a check made to find out if the keyboard is being displayed. If it is, then add the toast message on that window, else on the main window.
I found another method in this link, using which you can directly get to the UIView of the keyboard (If required).
You have to add your subview to:
UIWindow *window = [UIApplication sharedApplication].windows.lastObject;
which is on top of the keyboard.
Generally keyboard view is not part of your main window. it appears with new window when you get focused in any text field.
Try the following code to access your keyboard view.
[[[UIApplication sharedApplication] windows] objectAtIndex:1]
Remember, this will only work when you have keyboard on your screen.
Another way is to add a custom UIWindow, then setting it's WindowLevel to +1 of the last window.
Something like this
NSArray *windows = [[UIApplication sharedApplication] windows];
UIWindow *lastWindow = (UIWindow *)[windows lastObject];
myWindow.windowLevel = lastWindow.windowLevel + 1;
Take a look to this topic
https://forums.developer.apple.com/thread/16375
Update for Swift3
UIApplication.shared.windows.last
in iOS9 the answer by Adithya is not work,
UIWindow *window = [UIApplication sharedApplication].windows.lastObject;
work well
Try to add the view as a subview of the following class. This code snippet works for iOS 14 and above. Not sure about older versions. Reference: Toaster Github repo
Use it like:
ToastWindow.shared.addSubView(/your_view/)
public final class ToastWindow: UIWindow {
public static let shared = ToastWindow(frame: UIScreen.main.bounds, mainWindow: UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow } )
override public var rootViewController: UIViewController? {
get {
guard !self.isShowing else {
isShowing = false
return nil
}
guard let firstWindow = UIApplication.shared.delegate?.window else { return nil }
return firstWindow is ToastWindow ? nil : firstWindow?.rootViewController
}
set { /* Do nothing */ }
}
override public var isHidden: Bool {
willSet { isShowing = true }
didSet { isShowing = false }
}
/// Will not return `rootViewController` while this value is `true`. Needed for iOS 13.
private var isShowing = false
/// Returns original subviews. `ToastWindow` overrides `addSubview()` to add a subview to the
/// top window instead itself.
private var originalSubviews = NSPointerArray.weakObjects()
private weak var mainWindow: UIWindow?
// MARK: - Init
public init(frame: CGRect, mainWindow: UIWindow?) {
super.init(frame: frame)
self.mainWindow = mainWindow
self.isUserInteractionEnabled = false
self.gestureRecognizers = nil
self.windowLevel = .init(rawValue: .greatestFiniteMagnitude)
let keyboardWillShowName = UIWindow.keyboardWillShowNotification
let keyboardDidHideName = UIWindow.keyboardDidHideNotification
self.backgroundColor = .clear
self.isHidden = false
NotificationCenter.default.addObserver( self, selector: #selector(self.keyboardWillShow),
name: keyboardWillShowName,
object: nil )
NotificationCenter.default.addObserver( self, selector: #selector(self.keyboardDidHide),
name: keyboardDidHideName,
object: nil )
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented: please use ToastWindow.shared")
}
override public func addSubview(_ view: UIView) {
super.addSubview(view)
self.originalSubviews.addPointer(Unmanaged.passUnretained(view).toOpaque())
self.topWindow()?.addSubview(view)
}
public override func becomeKey() {
super.becomeKey()
mainWindow?.makeKey()
}
// MARK: - Keyboard methods
#objc private func keyboardWillShow() {
guard let topWindow = self.topWindow(),
let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
for subview in subviews {
topWindow.addSubview(subview)
}
}
#objc private func keyboardDidHide() {
guard let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
for subview in subviews {
super.addSubview(subview)
}
}
/// Returns top window that isn't self
private func topWindow() -> UIWindow? {
if let window = UIApplication.shared.windows.last(where: {
ToastWindowKeyboardObserver.shared.didKeyboardShow || $0.isOpaque
}), window !== self {
return window
}
return nil
}
}
final fileprivate class ToastWindowKeyboardObserver {
static let shared = ToastWindowKeyboardObserver()
private(set) var didKeyboardShow: Bool = false
private(set) var keyboardHeight = 0.0
private init() {
let keyboardWillShowName = UIWindow.keyboardWillShowNotification
let keyboardDidHideName = UIWindow.keyboardDidHideNotification
NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow),
name: keyboardWillShowName,
object: nil )
NotificationCenter.default.addObserver( self, selector: #selector(keyboardDidHide),
name: keyboardDidHideName,
object: nil )
}
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
keyboardHeight = keyboardRectangle.height
}
didKeyboardShow = true
}
#objc private func keyboardDidHide() {
didKeyboardShow = false
}
}