I'm still learning how the view works, but I found a problem that I couldn't solve...
I got the class GraficsBalancGlobalViewControllerwhich is subclass of the class GraficViewController
class GraficsBalancGlobalViewController: GraficViewController {
#IBAction func afegeixGrafic(sender: NSButton) {
addNewGrafic() // which is set on the GraficViewController
}
}
And when I perform the IBAction afegeixGrafic my program crashes on the line marked below:
class GraficViewController: NSViewController, GraficViewDataSource {
#IBAction func button(sender: NSButton) {
addNewGrafic()
}
func addNewGrafic() {
let frame = NSRect(x: 0, y: 0, width: self.view.bounds.width , height: self.view.bounds.width * 0.25)
let nouGrafic = GraficView(frame: frame)
scrollView.addSubview(nouGrafic) <---- BREAK here!
}
#IBOutlet weak var scrollView: NSView!
//...more code
}
The compiler says that:
fatal error: unexpectedly found nil while unwrapping an Optional value
but the button (IBAction) inside the GraficViewController works well!! So i suppose that the problem is related with the scrollView, but I have no idea of what can be.. It is initialized..
just to mention that the GraficView(frame: frame)is not the problem because I try it and works well.
I do believe ! is the culprit of your woes:
#IBOutlet weak var scrollView: NSView!
Xcode does generate this entry with force unwrap (!) for IBOutlets, but it should really be an optional (?) because you have no guaranties when that reference is going to be set. If you have some logic that depend on scrollView's existence, you can do so by relying on didSet:
#IBOutlet weak var scrollView: NSView? {
didSet {
guard let sview = scrollView else {
return // because scrollView is nil for some reason
}
// do your scrollView existence dependent logic here (eg. reload content)
}
}
func addNewGrafic() {
let frame = NSRect(x: 0, y: 0, width: self.view.bounds.width , height: self.view.bounds.width * 0.25)
let nouGrafic = GraficView(frame: frame)
scrollView?.addSubview(nouGrafic)
}
I hope this helps.
Related
I have a MacOS cocoa statusBarApp without any Storyboard with main.swift file.
The statusBarIcon shows up a Menu which presents a custom view with a button, which should open a settingsWindow - which it does. If I close the settingsWindow and reopen it and close it again, I got a EXC_BAD_ACCESS Error. It seems, that the window is deallocate but the reference is still present. I don't know how to fix this.
Edit the question like Willeke´s advice:
Thx, to your answer. Ok, hier is a minimal reproducible example:
create a new Xcode project, with storyboard and swift for macOS app.
Under Project-Infos / General / Deployment Info: Delete the main entry to the storyboard. Then delete the storyboard file itself.
Under Info set the "application is agent" flag to yes, so the app is statusBarApp only.
then you only need the code below.
The Exception Breakpoint leads to this line:
settingsWindow = NSWindow(
To reproduce the error: start the app, click on statusItem, click on menuItem, a window opens, close the window, click again all first steps and reopen the window. sometimes that's the point of crash. sometimes a few more attempts of closing the window are necessary, but not more then three times.
main.swift
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
AppDelegate.swift
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
var settingsWindow: NSWindow!
var statusItemMain: NSStatusItem?
var menuMain = NSMenu()
var menuItemMain = NSMenuItem()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
statusItemMain = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let itemImage = NSImage(systemSymbolName: "power", accessibilityDescription: nil)
itemImage?.isTemplate = true
statusItemMain?.button?.image = itemImage
menuItemMain.target = self
menuItemMain.isEnabled = true
menuItemMain.action = #selector(createWindow)
menuMain.addItem(menuItemMain)
menuMain.addItem(.separator())
statusItemMain?.menu = menuMain
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
#objc func createWindow() {
settingsWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 500),
styleMask: [.miniaturizable, .closable, .resizable, .titled],
backing: .buffered, defer: false)
settingsWindow.center()
settingsWindow.title = "No Storyboard Window"
settingsWindow.makeKeyAndOrderFront(nil)
settingsWindow?.contentViewController = ViewController()
}
}
ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func loadView() {
self.view = NSView(frame: NSRect(x: 0, y: 0, width: 750, height: 500))
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
NSWindow is released when it is closed. Before ARC this was a usefull feature. It can be switched off by setting the isReleasedWhenClosed property to false. But then the window stays in memory when it is closed because the settingsWindow property is holding on to it. Implement delegate method windowWillClose and set settingsWindow to nil so window is released.
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var settingsWindow: NSWindow!
// other methods
#objc func createWindow() {
settingsWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 500),
styleMask: [.miniaturizable, .closable, .resizable, .titled],
backing: .buffered, defer: false)
settingsWindow.isReleasedWhenClosed = false
settingsWindow.delegate = self
settingsWindow.center()
settingsWindow.title = "No Storyboard Window"
settingsWindow?.contentViewController = ViewController()
settingsWindow.makeKeyAndOrderFront(nil)
}
func windowWillClose(_ notification: Notification) {
settingsWindow = nil
}
}
I am trying to implement drop delegates on a NSCollectionViewController and having issues using a custom NSCollectionViewItem with an additional View Layer I've added onto the CollectionView Item. FWIW, The additional view is used draw a dashed border to indicate a drop area.
The drag event works fine on this collectionItem, and all other collectionItems without this view when it is hidden, but as soon as the drag event occurs on top of this view, the drag event pauses.
The drag event resumes as soon as the mouse is dragged outside of the view, but nothing happens if I release the drag while the mouse is over the view.
I would love to know what is happening here and how to prevent the custom view from "stealing" the mouse event from the CollectionViewContoller.
Delegate Method on DropViewController
func collectionView(_ collectionView: NSCollectionView, validateDrop draggingInfo: NSDraggingInfo, proposedIndexPath proposedDropIndexPath: AutoreleasingUnsafeMutablePointer<NSIndexPath>, dropOperation proposedDropOperation: UnsafeMutablePointer<NSCollectionView.DropOperation>) -> NSDragOperation {
print("1")
if proposedDropIndexPath.pointee.item <= self.destinationDirectoryArray.count {
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
} else if proposedDropIndexPath.pointee.item == self.destinationDirectoryArray.count {
//There's some stuff here validating the URL removed for brevity. It works okay when the focus is outside the view, but happy to add back in if helpful
if proposedDropOperation.pointee == NSCollectionView.DropOperation.on {
return .move
}
}
return[]
}
Configuring Collection View
func configureCollectionView() {
let flowLayout = NSCollectionViewFlowLayout()
flowLayout.minimumInteritemSpacing = 8.0
flowLayout.minimumLineSpacing = 8.0
destinationCollectionView.delegate = self
destinationCollectionView.dataSource = self
destinationCollectionView.register(NSNib(nibNamed: "DestinationCollectionItem", bundle: nil), forItemWithIdentifier: directoryItemIdentifier)
destinationCollectionView.collectionViewLayout = flowLayout
destinationCollectionView.registerForDraggedTypes([.fileURL])
destinationCollectionView.setDraggingSourceOperationMask(NSDragOperation.move, forLocal: true)
}
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
}
}
Custom Border View - Applied custom class in Xib and linked to File's Owner
class BorderedView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let path : NSBezierPath = NSBezierPath(roundedRect: self.bounds, xRadius: 10.0, yRadius: 10.0)
path.addClip()
let dashHeight: CGFloat = 2
let dashLength: CGFloat = 7
let dashColor: NSColor = .lightGray
// setup the context
let currentContext = NSGraphicsContext.current!.cgContext
currentContext.setLineWidth(dashHeight)
currentContext.setLineDash(phase: 0, lengths: [dashLength])
currentContext.setStrokeColor(dashColor.cgColor)
// draw the dashed path
let cgPath : CGPath = CGPath(roundedRect: NSRectToCGRect(self.bounds), cornerWidth: 10.0, cornerHeight: 10.0, transform: nil)
currentContext.addPath(cgPath)
currentContext.strokePath()
}
}
Well - I solved this one pretty quick.
While I previously tried adding unregisterDraggedTypes() to the backgroundLayer, the issue turned out to also be occurring on the image layer. I applied it to both the Image and backgroundLayer and it works now.
Collection View Item Setup
class DestinationCollectionItem: NSCollectionViewItem {
#IBOutlet weak var backgroundLayer: NSView!
override func viewDidLoad() {
super.viewDidLoad()
self.highlightState = .none
view.wantsLayer = true
view.layer?.cornerRadius = 8.0
backgroundLayer.isHidden = true
backgroundLayer.unregisterDraggedTypes()
self.imageView?.unregisterDraggedTypes()
self.textField?.unregisterDraggedTypes()
}
}
I am currently developing an iOS application with login and sign up forms. To make sure that the keyboard does not cover any UITextFields I've implemented the following solution provided by Apple and discussed in this issue.
To briefly sum it up, this solution uses a UIScrollView in which the different UI elements are placed and UIKeyboardDidShowNotification and UIKeyboardDidHideNotification to move the elements up and down when the keyboard appears/disappears so that the UITextFields aren't hidden.
This works like a charm except for one thing: for all my UIViewControllers I have to repeat the same code. To tackle my problem I have tried:
to create a base UIViewController, providing an implementation for the different functions, that can be subclasses by the other UIViewControllers;
to use a protocol and a protocol extension to provide a default implementation for the different functions and make my UIViewControllers conform to it.
Both solutions didn't solve my problem. For the first solution, I wasn't able to connect the UIScrollView of my base class through the Interface Builder although it was declared.
#IBOutlet weak var scrollView: UIScrollView!
When trying to implement the second solution, the UIViewController implementing my protocol somehow did not recognise the declared methods and their implementations.
The protocol declaration:
protocol ScrollViewProtocol {
var scrollView: UIScrollView! { get set }
var activeTextField: UITextField? { get set }
func addTapGestureRecognizer()
func singleTapGestureCaptured()
func registerForKeyboardNotifications()
func deregisterForKeyboardNotifications()
func keyboardWasShown(notification: NSNotification)
func keyboardWillBeHidden(notification: NSNotification)
func setActiveTextField(textField: UITextField)
func unsetActiveTextField()
}
The protocol extension implements all functions expect for the addTapGestureRecognizer() as I would like to avoid using #objc:
extension ScrollViewProtocol where Self: UIViewController {
// The implementation for the different functions
// as described in the provided links expect for the following method
func registerFromKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil, usingBlock: { notification in
self.keyboardWasShown(notification)
})
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidHideNotification, object: nil, queue: nil, usingBlock: { notification in
self.keyboardWillBeHidden(notification)
})
}
}
Does anyone have a good solution to my problem, knowingly how could I avoid repeating the code related to moving the UITextFields up and down when the keyboard appears/disappears? Or does anyone know why my solutions did not work?
I found a solution. I'll post it in case someone once to do the same thing.
So, I ended up deleting the UIScrollView outlet in my base class and replacing it with a simple property that I set in my inheriting classes. The code for my base class look as follow:
import UIKit
class ScrollViewController: UIViewController, UITextFieldDelegate {
// MARK: Properties
var scrollView: UIScrollView!
var activeTextField: UITextField?
// MARK: View cycle
override func viewDidLoad() {
super.viewDidLoad()
let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapGestureCaptured))
scrollView.addGestureRecognizer(singleTap)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
registerForKeyboardNotifications()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
deregisterFromKeyboardNotifications()
}
// MARK: Gesture recognizer
func singleTapGestureCaptured(sender: AnyObject) {
view.endEditing(true)
}
// MARK: Keyboard management
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWasShown), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillBeHidden), name: UIKeyboardWillHideNotification, object: nil)
}
func deregisterFromKeyboardNotifications() {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWasShown(notification: NSNotification) {
scrollView.scrollEnabled = true
let info : NSDictionary = notification.userInfo!
let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue().size
let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
var aRect : CGRect = self.view.frame
aRect.size.height -= keyboardSize!.height
if let activeFieldPresent = activeTextField {
if (!CGRectContainsPoint(aRect, activeFieldPresent.frame.origin)) {
scrollView.scrollRectToVisible(activeFieldPresent.frame, animated: true)
}
}
}
func keyboardWillBeHidden(notification: NSNotification) {
let info : NSDictionary = notification.userInfo!
let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue().size
let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
view.endEditing(true)
scrollView.scrollEnabled = false
}
// MARK: Text field management
func textFieldDidBeginEditing(textField: UITextField) {
activeTextField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
activeTextField = nil
}
}
And here is the inheriting class code:
class ViewController: ScrollViewController {
#IBOutlet weak var scrollViewOutlet: UIScrollView! {
didSet {
self.scrollView = self.scrollViewOutlet
}
}
// Your view controller functions
}
I hope this will help!
How can be possible that the scrollview is nil when using the first:#IBAction afegeixGrafic but when using #IBAction func button it isn't'?
I got the class GraficsBalancGlobalViewController which is subclass of the class GraficViewController
class GraficsBalancGlobalViewController: GraficViewController {
#IBAction func afegeixGrafic(sender: NSButton) {
addNewGrafic() // which is set on the GraficViewController
}
}
And when I perform the IBAction afegeixGrafic my program crashes on the line marked below:
class GraficViewController: NSViewController, GraficViewDataSource {
#IBAction func button(sender: NSButton) {
addNewGrafic()
}
func addNewGrafic() {
let frame = NSRect(x: 0, y: 0, width: self.view.bounds.width , height: self.view.bounds.width * 0.25)
let nouGrafic = GraficView(frame: frame)
scrollView.addSubview(nouGrafic) <---- BREAK here!
}
#IBOutlet weak var scrollView: NSView!
//...more code
}
The compiler says that:
fatal error: unexpectedly found nil while unwrapping an Optional value
but the button (IBAction) inside the GraficViewController works well!! So i suppose that the problem is related with the scrollView, but I have no idea of what can be.. It is initialized..
I try this too:
#IBOutlet weak var scrollView: NSView? {
didSet {
guard let sview = scrollView else {
addNewGrafic()
return // because scrollView is nil for some reason, but don't work
}
}
}
Full code:
class GraficsBalancGlobalViewController: GraficViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
#IBAction func afegeixGrafic(sender: NSButton) {
//performSegueWithIdentifier("Nou Grafic", sender: nil)
//let viewController = storyboard!.instantiateControllerWithIdentifier(identifier!) as! GraficsBalancGlobalViewController
addNewGrafic()
}
#IBAction func eliminaGrafic(sender: NSButton) {
}
}
Its nil because of how you instantiated GraficsBalancGlobalViewController. You set it up with a xib or a storyboard however you instantiated it without one.
Do this:
let viewController = storyboard.instantiateControllerWithIdentifier(identifier) as GraficsBalancGlobalViewController
Not This:
let viewController = GraficsBalancGlobalViewController()
I'm trying to observering collectionView.contentSize like this :
func startObserveCollectionView() {
collectionView.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.Old.union(NSKeyValueObservingOptions.New), context: &SearchDasboardLabelContext)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &SearchDasboardLabelContext {
if object === collectionView && keyPath! == "contentSize" {
print(change)
}
}
}
and in xcode terminal I got a NSSize not CGSize like this :
Optional(["old": NSSize: {320, 0}, "new": NSSize: {375, 39.5}, "kind": 1])
In objective-c I used method CGSizeValue
CGSize newContentSize = [[change objectForKey:NSKeyValueChangeNewKey] CGSizeValue];
Is there any method like CGSizeValue in swift
I have tried in swift var newContentSize = change[NSKeyValueChangeNewKey]?.CGSizeValue() but got error
could not find member 'CGSizeValue'
need help anyone? Thanks
With Swift 4, you can cast the result of the change dictionary for the key NSKeyValueChangeKey.newKey as being of type CGSize:
if let size = change?[NSKeyValueChangeKey.newKey] as? CGSize {
/* ... */
}
The following UIViewController implementation shows how to set a KVO stack in order to observe the changes of the contentSize property of any UIScrollView subclass (e.g UITextView):
import UIKit
private var myContext = 0
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
/* ... */
override func viewDidLoad() {
super.viewDidLoad()
textView.addObserver(self, forKeyPath: #keyPath(UITextView.contentSize), options: [NSKeyValueObservingOptions.new], context: &myContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &myContext,
keyPath == #keyPath(UITextView.contentSize),
let contentSize = change?[NSKeyValueChangeKey.newKey] as? CGSize {
print("contentSize:", contentSize)
}
}
deinit {
textView.removeObserver(self, forKeyPath: #keyPath(UITextView.contentSize))
}
}
Note that with Swift 4, as an alternative to addObserver(_:, forKeyPath:, options:, context:) and observeValue(forKeyPath:, of:, change:, context:), you can use observe(_:, options:, changeHandler:) in order to track your UIScrollView subclass contentSize property changes:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
var observer: NSKeyValueObservation?
/* ... */
override func viewDidLoad() {
super.viewDidLoad()
let handler = { (textView: UITextView, change: NSKeyValueObservedChange<CGSize>) in
if let contentSize = change.newValue {
print("contentSize:", contentSize)
}
}
observer = textView.observe(\UITextView.contentSize, options: [NSKeyValueObservingOptions.new], changeHandler: handler)
}
}
Are you on iOS? Because I am, I did the same thing and arrived at the same question; why NSSize? Maybe that's just the xcode terminal playing a trick on us.
Anyway, you can cast it to an NSValue then you will be able to use CGSizeValue:
if let zeChange = change as? [NSString: NSValue] {
let oldSize = zeChange[NSKeyValueChangeOldKey]?.CGSizeValue()
let newSize = zeChange[NSKeyValueChangeNewKey]?.CGSizeValue()
}
There's a simpler and arguably swiftier alternative.
You can subclass UICollectionViewLayout (or any of its subclasses, like UICollectionViewFlowLayout) and override a computed property collectionViewContentSize. By calling super you'll get the contentSize of your collection and be able to delegate this value back to your code.
So you'll have something like this:
protocol FlowLayoutDelegate: class {
func collectionView(_ collectionView: UICollectionView?, didChange contentSize: CGSize)
}
class FlowLayout: UICollectionViewFlowLayout {
weak var delegate: FlowLayoutDelegate?
override var collectionViewContentSize: CGSize {
let contentSize = super.collectionViewContentSize
delegate?.collectionView(collectionView, didChange: contentSize)
return contentSize
}
}
Check out this example code:
if context == ApprovalObservingContext{
if let theChange = change as? [NSString: Bool]{
var newContentSize = change[NSKeyValueChangeNewKey]?.CGSizeValue()
}
}
This is not giving any error.