I am trying to make a SystemStatusBar popover for Mac. (Roughly, translating this Cocoa app to a Swift app). However, the view that I am using never shows up and the popup appears in the bottom left of the screen replicating the StatusBarItem.
This is what I expect (and that happens in the example from case of example from the link):
and this is what actually shows up (in my, Swift version of the application) instead of the NSPopover being shown in the StatusBar [Showing or Hiding the popup is controlled by the two buttons as shown in the previous figure. In this screenshot I have not added that window as it remains the same.]:
This is the AppDelegate:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var statusView : StatusView!
var popController: PopViewController!
#IBAction func showPop(sender: NSButton)
{
statusView.showPopup()
}
#IBAction func hidePop(sender: NSButton)
{
statusView.hidePopup()
}
func applicationDidFinishLaunching(aNotification: NSNotification)
{
var height = NSStatusBar.systemStatusBar().thickness
statusView = StatusView(frame: NSMakeRect(0, 0, CGFloat(height), CGFloat(height)))
}
}
The CustomView:
class StatusView : NSView, NSMenuDelegate
{
var imageView: NSImageView!
var statusItem: NSStatusItem!
var popover: NSPopover!
var popController: PopViewController!
required init? (coder: NSCoder)
{
super.init(coder: coder)
}
override init(frame frameRect: NSRect)
{
var height = NSStatusBar.systemStatusBar().thickness
imageView = NSImageView(frame: NSMakeRect(0, 0, CGFloat(height), CGFloat(height)))
statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(CGFloat(height))
super.init(frame: frameRect)
imageView.image = NSImage(named: "mf-image-black.png")
self.addSubview(imageView)
statusItem.view = self
popover = NSPopover()
popController = PopViewController(nibName: "PopViewController", bundle: nil)
popController.view = self
popover.contentViewController = popController
}
func showPopup()
{
if(!popover.shown)
{
popover.showRelativeToRect(self.frame, ofView: self, preferredEdge: NSMinYEdge)
}
}
func hidePopup()
{
if(popover.shown)
{
popover.close()
}
}
}
and the ViewController:
class PopViewController: NSViewController
{
#IBOutlet var statusView: StatusView!
override init?(nibName: String?, bundle: NSBundle?) {
super.init(nibName: nibName, bundle: bundle)
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
}
}
I am not exactly sure what is that I am missing here. The StatusItem never seems to make use of the PopViewController nib.
Related
I am trying to get the splash view ( back_button ) to remove itself once the webpage fully loads, I had done quite a bit of researching and they always point to the same code as an answer. However, mine will not get called. Can somebody advise what I am doing wrong?
import UIKit
import WebKit
class ViewController: UIViewController {
#IBOutlet weak var LoadingView: UIImageView!
var webView: WKWebView?
var bgImage: UIImageView?
var imageViewObject :UIImageView?
override func viewDidLoad() {
super.viewDidLoad()
//var imageViewObject :UIImageView
let screenSize: CGRect = UIScreen.main.bounds
imageViewObject = UIImageView(frame:CGRect(x:0,y: 0, width: screenSize.width, height: screenSize.height))
imageViewObject?.image = UIImage(named:"back_image")
webView?.navigationDelegate = self as? WKNavigationDelegate
self.view.addSubview(imageViewObject!)
let myURL = URL(string: "https://www.google.com")
let myRequest = URLRequest(url: myURL!)
_ = webView?.load(myRequest)
}
override func loadView() {
super.loadView()
//self.view.sendSubview(toBack: imageViewObject)
//self.view = self.LoadingView
self.webView = WKWebView()
self.view = self.webView
}
#IBAction func BackButton(_ sender: Any) {
if(webView?.canGoBack)!
{
webView?.goBack();
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("TEST")
imageViewObject?.removeFromSuperview()
}
func webViewDidFinishLoad(webView: WKWebView) {
imageViewObject?.removeFromSuperview()
self.view = self.webView
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
you have to conform to it's protocol delegate by putting this line of code in viewDidLoad
webView?.uiDelegate = self
after doing that , now if you want to use delegation method's you have to put your viewController as a subclass of UIWebViewDelegate
class ViewController: UIViewController ,UIWebViewDelegate{
I fixed this by making
webView?.navigationDelegate = self
and adding this to the Class
class ViewController: UIViewController, WKNavigationDelegate
here is my BorderView and The task I'm trying to do is to hookup the custom object (Border) to the main storyboard.
import UIKit
protocol BorderViewDataSource: class{
func colorForBorderView(sender:BorderView) -> String? }
class BorderView: UIView {
#IBOutlet weak var topLeftImage: UIImageView!
#IBOutlet weak var topRightImage: UIImageView!
#IBOutlet weak var bottomLeftImage: UIImageView!
#IBOutlet weak var bottomRightImage: UIImageView!
var view: UIView!
weak var dataSource:BorderViewDataSource?
override init(frame: CGRect) {
super.init(frame: frame)
xtraSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
xtraSetup()
}
func xtraSetup() {
view = loadViewFromNib()
// use bounds not frame or it'll be offset
view.frame = bounds
// Make the view stretch with containing view
view.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
// Adding custom subview on top of our view (over any custom drawing > see note below)
addSubview(view)
let borderColor = dataSource?.colorForBorderView(self) ?? "red"
applyColor(borderColor)
}
// this is an actual load from nib module
func loadViewFromNib() -> UIView {
let bundle = NSBundle(forClass: self.dynamicType)
let nib = UINib(nibName: "BorderView", bundle: bundle)
let view = nib.instantiateWithOwner(self, options: nil)[0] as! UIView
return view
}
func applyColor(borderColor:String){
var imageName = "border-topleft-\(borderColor)"
topLeftImage.image = UIImage(named: imageName)
imageName = "border-topright-\(borderColor)"
print("Border color is \(borderColor), and Last image name is \(imageName)")
}
}
So when i open the app it will show the image of different color depending on what the value of the color is set in the ViewContorller.swift and here is my viewcontroller. swift code
class ViewController: UIViewController, BorderViewDataSource {
var borderType = 2
#IBOutlet weak var borderView: BorderView!{
didSet{
borderView.dataSource = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func colorForBorderView(sender: BorderView) -> String? {
var borderColor = "blue"
switch borderType {
case 1: borderColor = "blue"
case 2: borderColor = "purple"
case 3: borderColor = "red"
default: break
}
print("Border Type is \(borderType), and border color is \(borderColor)")
return borderColor
}
}
but colorForBorderView method is not calling when i running the app
It seems when the view is initialized, the dataSource property of that view is nil. And you only call that method during initialization of your view. You can apply the color after viewDidLoad() in your view controller
From the documentation:
When you assign a default value to a stored property, or set its
initial value within an initializer, the value of that property is set
directly, without calling any property observers.
That's the reason why the datasource is not set. Put the line borderView.dataSource = self into viewDidLoad()
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.
NSViewController, when instantiated from storyboards under Swift, seems to have a reference cycle somewhere.
Calling the following code multiple times will instantiate and set a new view controller, but the old view controller is never dealloced. In the code, containerViewController is an NSViewController which should contain a single NSViewController, containerView is a subview within containerViewController, and identifier is the storyboard identifier to instantiate.
// Remove any sub viewcontrollers and their views
for viewController in containerViewController.childViewControllers as [NSViewController] {
viewController.view.removeFromSuperview()
viewController.removeFromParentViewController()
}
// Create and set up the new view controller and view.
let viewController = storyboard!.instantiateControllerWithIdentifier(identifier) as NSViewController
let view = viewController.view
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(viewController.view)
containerViewController.addChildViewController(viewController)
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: nil, metrics: nil, views: ["view": view]))
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: nil, metrics: nil, views: ["view": view]))
(Sample project no longer available)
I used an Apple TSI and they agree it's a bug, which I have filed, but I expected someone else to have come up against this by now seeing as NSViewControllers and storyboards are now the de facto on OSX. How have you worked around this problem? Or does it not affect anyone else and I am doing something wrong?
Pre-bounty edit: Each view controller must be able to link to any other view controller from code as the destination is determined on the fly. This seems to remove segues as an option.
Bug fixed
As of Xcode 6.3 this is no longer a bug.
Another answer.
It seems, only the segues defined on Storyboard can perform view controller deallocation.
So, Here is a very ugly but working workaround.
class DismissSegue: NSStoryboardSegue {
var nextViewControllerIdentifier:String?
override func perform() {
let src = self.sourceController as NSViewController
let windowController = src.view.window!.windowController() as TopLevelWindowController
src.view.removeFromSuperview()
src.removeFromParentViewController()
if let identifier = nextViewControllerIdentifier {
windowController.setNewViewController(identifier)
}
}
}
class TopLevelWindowController: NSWindowController {
var containerView: NSView!
var containerViewController: ContainerViewController! {
didSet {
setNewViewController("FirstView")
}
}
func setNewViewController(identifier: String) {
// Create and set up the new view controller and view.
let viewController = storyboard!.instantiateControllerWithIdentifier(identifier) as NSViewController
let view = viewController.view
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(viewController.view)
containerViewController.addChildViewController(viewController)
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: nil, metrics: nil, views: ["view": view]))
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: nil, metrics: nil, views: ["view": view]))
}
}
class ContainerViewController: NSViewController {
#IBOutlet var containerView: NSView!
override func viewDidAppear() {
super.viewDidAppear()
if let window = view.window {
if let topLevelWindowController = window.windowController() as? TopLevelWindowController {
topLevelWindowController.containerView = containerView
topLevelWindowController.containerViewController = self
}
}
}
}
class FirstViewController: NSViewController {
required init?(coder: NSCoder) {
super.init(coder: coder)
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("First VC init at \(pointerAddress)")
}
deinit {
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("First VC de-init at \(pointerAddress)")
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
if let segue = segue as? DismissSegue {
segue.nextViewControllerIdentifier = "SecondView"
}
}
}
class SecondViewController: NSViewController {
required init?(coder: NSCoder) {
super.init(coder: coder)
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("Second VC init at \(pointerAddress)")
}
deinit {
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("Second VC de-init at \(pointerAddress)")
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
if let segue = segue as? DismissSegue {
segue.nextViewControllerIdentifier = "FirstView"
}
}
}
Procedure for modifying your Storyboard:
Disconnect #IBAction from buttons.
Create "DUMMY" view controller scene with normal NSViewController.
Connect custom segues from buttons to "DUMMY".
Configure these segue as shown.
Please let me know if this solution doesn't meet your demand.
This might not solve your problem, but the only workaround I found is:
Embed initial view controller using "Container View"
No #IBAction
Use custom NSStoryboardSegue to Switch between view controllers.
Something like this:
import Cocoa
class TopLevelWindowController: NSWindowController {
}
class ContainerViewController: NSViewController {
}
class FirstViewController: NSViewController {
required init?(coder: NSCoder) {
super.init(coder: coder)
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("First VC init at \(pointerAddress)")
}
deinit {
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("First VC de-init at \(pointerAddress)")
}
}
class SecondViewController: NSViewController {
required init?(coder: NSCoder) {
super.init(coder: coder)
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("Second VC init at \(pointerAddress)")
}
deinit {
let pointerAddress = NSString(format: "%p", unsafeBitCast(self, Int.self))
NSLog("Second VC de-init at \(pointerAddress)")
}
}
class MySegue: NSStoryboardSegue {
override func perform() {
let source = self.sourceController as NSViewController
let destination = self.destinationController as NSViewController
if let containerViewController = source.parentViewController {
source.view.removeFromSuperview()
source.removeFromParentViewController()
let view = destination.view
let containerView = containerViewController.view
view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(view)
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view]|", options: nil, metrics: nil, views: ["view": view]))
containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|", options: nil, metrics: nil, views: ["view": view]))
containerViewController.addChildViewController(destination)
}
}
}
I have made a statusBar application with a drop down. I would like to open a settingsWindow from that dropdown. I have made the settings window with its own ViewController.
The issue is that i can't figure out how to instantiate and show the settingsWindow that i have made. I have tried to follow every thread on the internet without any success.
My Viewcontroller:
class SettingsViewController: NSViewController {
#IBOutlet var ipAddress: NSTextField!
#IBOutlet var port: NSTextField!
#IBAction func connect(sender: AnyObject) {}
override func viewDidLoad() {
super.viewDidLoad()
}
}
My AppDelegate:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet var statusMenu: NSMenu!
var statusItem: NSStatusItem?
var tcpService: TcpService = TcpService()
func applicationDidFinishLaunching(aNotification: NSNotification?) {
let bar = NSStatusBar.systemStatusBar()
statusItem = bar.statusItemWithLength(20)
statusItem!.menu = statusMenu
statusItem!.image = NSImage(byReferencingFile: NSBundle.mainBundle().pathForResource("16*16", ofType: "png"))
statusItem!.highlightMode = true
tcpService.initOutputStream("192.168.1.1", Port: 8888)
}
func applicationWillTerminate(aNotification: NSNotification?) {
// Insert code here to tear down your application
}
#IBAction func openSettings(sender: AnyObject) {
// open settings for ip and port optional port
}
}
in swift 3:
var myWindow: NSWindow? = nil
let storyboard = NSStoryboard(name: "Main",bundle: nil)
let controller: EditorViewController = storyboard.instantiateController(withIdentifier: "editorViewController") as! ViewController
myWindow = NSWindow(contentViewController: controller)
myWindow?.makeKeyAndOrderFront(self)
let vc = NSWindowController(window: myWindow)
vc.showWindow(self)
For 2022
in your normal Main storyboard, tap to add a new window controller.
tap precisely on the red "X", then the blue circle, and then enter "ExampleID" at the green entry.
in your app's ordinary main view controller, add this
variable:
var otherWindow: NSWindowController?
function:
private func otherWindow() {
let sb = NSStoryboard(name: "Main", bundle: nil)
otherWindow = sb.instantiateController(
withIdentifier: "ExampleID") as! NSWindowController
otherWindow?.showWindow(self)
}
That's it.
Call otherWindow when you want to.
Problem:
Inevitably you will want to set up the otherWindow in a certain way, example, transparent, whatever. Unfortunately this is a whole topic in itself, but you do it like this:
private func otherWindow() {
... as above ...
otherWindow?.window?.ExampleSetup()
}
and then
extension NSWindow {
func ExampleSetup() {
self.styleMask = .borderless
self.collectionBehavior = [.fullScreenPrimary]
self.level = .floating
self.isMovable = false
self.titleVisibility = .hidden
// etc etc etc ..
guard let screen = self.screen ?? NSScreen.main else {
print("what the???")
return
}
self.setFrame(screen.frame, display: true)
// consider also .visibleFrame
}
}
enum Storyboards: String {
case main = "Main"
func instantiateVC<T>(_ identifier: T.Type) -> T? {
let storyboard = NSStoryboard(name: rawValue, bundle: nil)
guard let viewcontroller = storyboard.instantiateController(withIdentifier: String(describing: identifier)) as? T else { return nil}
return viewcontroller
}
}
var ssoLoginController: IDSSOLoginViewController?
var myWindow: NSWindow? = nil
ssoLoginController = Storyboards.main.instantiateVC(IDSSOLoginViewController.self)
myWindow = NSWindow(contentViewController: ssoLoginController!)
myWindow?.makeKeyAndOrderFront(self)
let vc = NSWindowController(window: myWindow)
vc.showWindow(self)
I am not 100% that I fully understand your problem, but assuming that you are using a storyboard (you should if you are starting fresh), adding few lines to your applicationDidFinishLaunching method will help:
var myWindow: NSWindow? = nil
let storyboard = NSStoryboard(name: "Main",bundle: nil)
let controller: SettingsViewController = storyboard?.instantiateControllerWithIdentifier("SettingsViewController") as SettingsViewController
myWindow = controller.window
myWindow?.makeKeyAndOrderFront(self)
Do not forget to set the Storyboard ID in IB (in the example above to SettingsViewController)!