cocoa - Edit/Design NSView visually - xcode

How can I visually design a NSView in Xcode? I'm trying to build a statusBar app with a statusMenu only
As you can see above, the object appear but I can't edit it visually, then it go like this as a result:
I attempted to use a XIB file (only have a NSView, no ViewController), but I can't put the NSView in the XIB into the AppDelegate. I tried another ways and managed to use the XIB file, but then I can't use the Storyboard.
Do you have any idea for this situation?
UPDATE
Thank Max for answer my question. I actually tried that before but failed. I tried it again after your comment and there's still no luck. Here is how I did
I created a ViewController then I change its view to the MenuCustomView (NSView)
This is the code in AppDelegate
let statusBar = NSStatusBar.systemStatusBar().statusItemWithLength(-1)
var menuCustomView = NSView()
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
let statusIcon = NSImage(named: "statusIcon")
let statusMenu = NSMenu()
statusBar.image = statusIcon
statusBar.menu = statusMenu
let menuItem = NSMenuItem()
menuItem.title = "title"
var vc = ViewController()
menuCustomView = vc.view
menuItem.view = menuCustomView
statusMenu.addItem(menuItem)
}
Files I have in the project

Even though you don't need the view controller, you can use them to design the views. Create one in the storyboard, edit the view the way you want.
Then you can use the view by creating a viewController out of the storyboard and using its view property to attach the view to the NSStatusBarItem
My words in code:
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let viewController = storyboard?.instantiateControllerWithIdentifier("MyViewController") as! NSViewController
let view = viewController.view
I assume you want to add functioning code to the view. To, for example respond to the button click, you have to subclass the NSViewController and then implement the code you would have added to the AppDelegate.
If you need any more explanation, feel free to write me :)

Related

Cannot add viewcontroller´s frame to a scrollview in another viewcontroller

I want to add a swipe function to my app and swipe between viewcontrollers as in this tutorial: https://www.youtube.com/watch?v=3jAlg5BnYUU
The source code for the tutorial is here: https://www.veasoftware.com/posts/swipe-navigation-in-swift-xcode-7-ios-9-tutorial
In that guide they are using nib/xib files that they create together with the corresponding viewcontrollers that they then use to call for in their ViewController´s constructors.
I have done exact the same and it is wokring as in the tutorial. But I also want use a ViewController that not has a xib file because it is on my storyboard. That viewcontroller is called ViewController2 in my code below and it is the only that does not appear in the scrollview when I swipe. The other two do because I call for the xibName in the constructor call but ViewController2 does not have a xib file since it is on my storyboard.
So how can I add it to the scrollview so it appears when I swipe?
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
let vc0 = ViewController0(nibName: "ViewController0", bundle: nil)
self.addChildViewController(vc0)
self.scrollView.addSubview(vc0.view)
vc0.didMoveToParentViewController(self)
let vc1 = ViewController1(nibName: "ViewController1", bundle: nil)
var frame1 = vc1.view.frame
frame1.origin.x = self.view.frame.size.width
vc1.view.frame = frame1
self.addChildViewController(vc1)
self.scrollView.addSubview(vc1.view)
vc1.didMoveToParentViewController(self)
let vc2 = ViewController2()
var frame2 = vc2.view.frame
frame2.origin.x = self.view.frame.size.width
vc2.view.frame = frame2
self.addChildViewController(vc2)
self.scrollView.addSubview(vc2.view)
vc2.didMoveToParentViewController(self)
self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width * 3, self.view.frame.size.height - 66);
}
Try doing the following:
1) Getting the storyboard object programmatically.
2) Giving the View Controller 2 in your storyboard file a storyboard ID. You can do this by going to your Main.storyboard file and selecting the view controller you wish to give an identifier and then looking in the Identity Inspector (It should be in the under "Identity" section.
3) Instantiating an instance of View Controller 2 with the storyboard method instantiateViewControllerWithIdentifier. This method uses a storyboard ID as it's argument and adding that as the child of your scroll view.
Like so:
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
//1
let mainStoryboard = UIStoryboard.init(name: "Main", bundle: NSBundle.mainBundle())
//3
let vc2 = mainStoryboard.instantiateViewControllerWithIdentifier("<InsertStoryBoardID>") as! ViewController2
self.addChildViewController(vc2)
self.scrollView.addSubview(vc2.view)
vc2.didMoveToParentViewController(self)
self.scrollView.contentSize = CGSizeMake(self.view.frame.size.width * 3, self.view.frame.size.height - 66);
}
Hope this helps! Feel free to ask questions!

Connect to ViewController from AppDelegate (Swift)

I have created a new OS X Cocoa Application using the standard Xcode Swift template (using StoryBoards).
I have implemented an IBAction in AppDelegate.swift to handle when the users selects "Open..." from the "File" menu. If the chosen file is a valid image file, I create an NSImage which I then want to display in the view of ViewController.
#IBAction func openFile(sender: NSMenuItem) {
var openPanel = NSOpenPanel()
openPanel.beginWithCompletionHandler { (result :Int) -> Void in
if result == NSFileHandlingPanelOKButton {
if let imageURL = openPanel.URL {
let image = NSImage(contentsOfURL: imageURL)
// PRESENT image IN THE VIEW CONTROLLER
}
}
}
However, I don't see any way to connect to ViewController from AppDelegate. I have only managed to find suggestions that I should look at self.window! in AppDelegate, but there is no such thing as a window in AppDelegate.
Thanks,
Michael Knudsen
It seems that AppDelegate can connect to objects only within Application Scene in a storyboard. If you want to get a ViewController, instantiate it from a storyboard.
sample:
#IBAction func menuAction(sender: AnyObject) {
if let storyboard = NSStoryboard(name: "Main", bundle: nil) {
let controller = storyboard.instantiateControllerWithIdentifier("VC1") as NSViewController
if let window = NSApplication.sharedApplication().mainWindow {
window.contentViewController = controller // just swap
}
}
}
You can access the mainWinow property and the contentViewController property to create a reference to your custom ViewController class. This is similar to the iOS rootViewController property.
let rootViewController = NSApplication.shared().mainWindow?.windowController?.contentViewController as! ViewController
Now you can use this reference to access IBOutlets on your main storyboard from your AppDelegate.
rootViewController.myTextView.textStorage?.mutableString.setString("Cats and dogs.")
This is good for a simple app with one Window with one ViewController.
I was stuck trying to do this same thing recently and managed to get the event I needed to update my view by creating the #IBAction in my ViewController and control dragging to my Application's First Responder (above the menu in my storyboard view).
Here's the question that got me out of the woods:
Application Menu Items Xcode
And thanks to Bluedome for the suggestion to connect it to First Responder's action.
If you control-drag from the menu to the first responder (red cube above menu) and picked an existing action, then you can "responder chain" to your view controller. In my case I attached Open to openFile and then in my view controller I added the following
override var acceptsFirstResponder: Bool {
return true
}
func openFile(sender: NSMenuItem) {
print("In view controller")
}
and it worked without any changes in AppDelegate. Most of the menus are already hooked up to first responder so just add the matching function name in your view controller.
See this comment and this document on Event Handling Basics for more info.
In Swift 5 and accessing new windows array:
#IBAction func menuAction(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateInitialViewController()
// The windows in the array are ordered from back to front by window level;
// thus, the last window in the array is on top of all other app windows.
// On app launch, UIApplication.shared.windows.count == 1 anyway.
if let window = UIApplication.shared.windows.last {
window.rootViewController = controller
}
}

How to use dismiss an iPhone popover in an Adaptive Storyboard

I am new to iOS development, and am trying to learn storyboarding, Swift, and the new features of iOS 8 at the same time.
I have created a very simple storyboard that uses a Popover presentation segue to display another view. On the simulator, if I run this for an iPad, it works as expected. However, if I run it for an iPhone, instead of a popover, it displays a full-screen view, on top of the original view. This is fine; however, there is no way to dismiss it and go back to the original screen.
I have watched the WWDC 2014 video "228 A Look inside presentation controllers" and they can show a dismiss button if they build the user interface entirely with code.
I have also watched the "411 What's new in interface builder" session, where they say that this can be done in Interface Builder, but they do not show it, promising to show how to do it in the lab, if anyone is interested. Unfortunately, I did not attend WWDC 2014, or know anyone who has. My Google searches have not returned anything helpful either.
You could add the navigation controller like this-
Set your popover view controller as the root view controller to a navigation controller.
Delete the popover segue that you are currently using
Reconnect the segue from the button you are displaying the popover from to the navigation controller.
On iPad you will get a popover and on iPhone you will get a modal presentation. Both the iPad and iPhone will show the navigation controller. Depending on your use case this may or may not be something you want. Here's a screen show of what the storyboard should look like.
If you really do want your view controller to always be a popover leave your storyboard the way it is and add something like this to your view controller that presents the popover-
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Your segue name"]) {
UIViewController *yourViewController = segue.destinationViewController;
yourViewController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popoverPresentationController = yourViewController.popoverPresentationController;
popoverPresentationController.delegate = self;
}
}
The view controller presenting the popover will also need to respond to this UIPopoverPresentationDelegate method
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
{
return UIModalPresentationNone;//always popover.
}
Lastly, you could do the following to only add the navigation controller to the modal presentation of your view controller on the iPhone and leave the popover on iPad without a navigation controller.
Leave your storyboard as is.
The proper place to inject the navigation controller is in - (UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style. In order for this to be called we must set ourselves as the delegate of the UIPopoverPresentationController.
Once again we will do this in prepareForSegue:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Your segue name"]) {
UIViewController *yourViewController = segue.destinationViewController;
yourViewController.modalPresentationStyle = UIModalPresentationPopover;
UIPopoverPresentationController *popoverPresentationController = yourViewController.popoverPresentationController;
popoverPresentationController.delegate = self;
}
}
Then we will do this in the delegate method that I mentioned above
-(UIViewController *)presentationController:(UIPresentationController *)controller viewControllerForAdaptivePresentationStyle:(UIModalPresentationStyle)style
{
UIViewController *presentedViewController = controller.presentedViewController;
UINavigationController *navigationController = [[UINavigationController alloc]
initWithRootViewController:presentedViewController];
UIBarButtonItem *dismissButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonItemStyleDone target:self action:#selector(done:)];
presentedViewController.navigationItem.rightBarButtonItem = dismissButton;
return navigationController;
}
Good Luck!
If what you want is a popover on your iPad but a modal sheet with a close button on your iPhone then you can do it without creating an extra navigation controller in storyboard for the popover.
In Xcode 6.3 storyboard, you simply hook up a view controller and designate the segue as a "Present as Popover"
The code below should go in the view controller that segues to the popover, not in the popover itself:
First you set up the popover delegate:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "myPopoverSegueName") {
let vc = segue.destinationViewController
vc.popoverPresentationController?.delegate = self
return
}
}
Then you add the delegate extension (below your view controller's code) and create the navigation controller / close button on the fly:
extension myViewController: UIPopoverPresentationControllerDelegate {
func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
let btnDone = UIBarButtonItem(title: "Done", style: .Done, target: self, action: "dismiss")
let nav = UINavigationController(rootViewController: controller.presentedViewController)
nav.topViewController.navigationItem.leftBarButtonItem = btnDone
return nav
}
}
Then you add your dismiss function and you should be good to go:
func dismiss() {
self.dismissViewControllerAnimated(true, completion: nil)
}
I am not sure why you need to do storyboard setup for the Done button, all the work can be done programmatically with few lines of code. The important part is to implement some UIAdaptivePresentationControllerDelegate protocol methods exactly like below:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle
{
return .FullScreen
}
func presentationController(controller: UIPresentationController,
viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController?{
var navController:UINavigationController = UINavigationController(rootViewController: controller.presentedViewController)
controller.presentedViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action:"done")
return navController
}
Then, a simple method to implement the dismissing behavior for the popover in case it was presented in full screen:
func done (){
presentedViewController?.dismissViewControllerAnimated(true, completion: nil)
}
and you done!
In my case, I had a small popup that I wanted to be a popup on both an iPhone and iPad - and wanted to avoid using a navigation bar with a Dismiss. Discovered that one needed to implement two delegate calls (Swift 3.0):
extension MyViewController : UIPopoverPresentationControllerDelegate {
// Needed for iPhone popup
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
// Needed for iPhone in landscape
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
}
Its possible to do it with mimimal code whilst putting the logic into the storyboard instead. In the view controller that presents the popover, just put in the marker method
#IBAction func unwindToContainerVC(segue: UIStoryboardSegue) {
}
It does not need any code but needs to be present so you can control drag to the Exit icon later on when you use interface builder.
I have my popover content not take up the entire background view but have a small margin around it. This means you can use interface builder to create a tap gesture recogniser for this view. Control drag the gesture recogniser to the Exit icon which then pops up some Exit choices, one of which is the unwindToContainerVC method as seen above.
Now any tap around the edge (such as in an iPhone 4S scenario) takes you back to the presenting view controller.
Here is the connections inspector for the gesture recogniser:

Present a view modally in NSDocument's window

I have an NSDocument subclass that has its own xib file. Also I have an NSViewController subclass with its own xib file too, and I want to present its view modally, like this one.
Problem is, it always shows it as a separate floating window without title bar.
The view I'm trying to present is contained in a window in that xib file. And yes, it's Mac OS X 10.10. Here's the code.
#IBAction func didPressEditF(sender: AnyObject) {
let controller = ViewController(nibName: "ViewController", bundle: nil)
let window = self.windowControllers[0].window as NSWindow
window.beginSheet(controller.view.window, completionHandler: didEndPresentingF)
}
It's OK if you help me using Objective-C.
Alright. I figured it out.
At first. We need a property of our ViewController class so it won't get released after showing.
var controller: ViewController?
Then we need a method that will return a window of the current document. Somehow self.windowControllers[0].window as NSWindow doesn't work.
func window() -> NSWindow {
let windowControllers = self.windowControllers
let controller = windowControllers[0] as NSWindowController
let window = controller.window
return window
}
And finally, the code that opens up the sheet window will look like this:
#IBAction func didPressEditF(sender: AnyObject) {
controller = ViewController(nibName: "ViewController", bundle: nil)
self.window().beginSheet(controller!.view.window, completionHandler: didEndPresentingF)
}
Apple HAS to do something with their outdated documentation.
Instead of digging through the document's window controllers, you could call windowForSheet, a method on NSDocument. E.g. self.windowForSheet.

How to present a modal atop the current view in Swift

(Xcode6, iOS8, Swift, iPad)
I am trying to create a classic Web-like modal view, where the outside of the dialog box is "grayed-out." To accomplish this, I've set the alpha value of the backgroundColor of the view for the modal to 0.5, like so:
self.view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5)
The only problem is that when the modal becomes full-screen, the presenting view is removed. (Ref Transparent Modal View on Navigation Controller).
(A bit irritated at the concept here. Why remove the underlying view? A modal is, by definition, to appear atop other content. Once the underlying view is removed, it's not really a modal anymore. it's somewhere between a modal and a push transition. Wa wa wa... Anyway..)
To prevent this from happening, I've set the modalPresentationStyle to CurrentContext in the viewDidLoad method of the parent controller, and in Storyboard... but no luck.
self.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
How do I prevent the presenting view from being removed when the modal becomes full screen?
tyvm.. more info below.
Also in Storyboard, like so (Presentation: Current Context)
Thx for your help... documentation below:
First, remove all explicit setting of modal presentation style in code and do the following:
In the storyboard set the ModalViewController's modalPresentation style to Over Current context
Check the checkboxes in the Root/Presenting ViewController - Provide Context and Define Context.
They seem to be working even unchecked.
You can try this code for Swift:
let popup : PopupVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopupVC") as! PopupVC
let navigationController = UINavigationController(rootViewController: popup)
navigationController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.presentViewController(navigationController, animated: true, completion: nil)
For swift 4 latest syntax using extension:
extension UIViewController {
func presentOnRoot(`with` viewController : UIViewController){
let navigationController = UINavigationController(rootViewController: viewController)
navigationController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(navigationController, animated: false, completion: nil)
}
}
How to use:
let popup : PopupVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopupVC") as! PopupVC
self.presentOnRoot(with: popup)
The only problem I can see in your code is that you are using CurrentContext instead of OverCurrentContext.
So, replace this:
self.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
for this:
self.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
self.navigationController.modalPresentationStyle = UIModalPresentationStyle.OverCurrentContext
This worked for me in Swift 5.0. Set the Storyboard Id in the identity inspector as "destinationVC".
#IBAction func buttonTapped(_ sender: Any) {
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let destVC = storyboard.instantiateViewController(withIdentifier: "destinationVC") as! MyViewController
destVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
destVC.modalTransitionStyle = UIModalTransitionStyle.crossDissolve
self.present(destVC, animated: true, completion: nil)
}
The problem with setting the modalPresentationStyle from code was that you should have set it in the init() method of the presented view controller, not the parent view controller.
From UIKit docs: "Defines the transition style that will be used for this view controller when it is presented modally. Set
this property on the view controller to be presented, not the presenter. Defaults to
UIModalTransitionStyleCoverVertical."
The viewDidLoad method will only be called after you already presented the view controller.
The second problem was that you should use UIModalPresentationStyle.overCurrentContext.
The only way I able to get this to work was by doing this on the presenting view controller:
func didTapButton() {
self.definesPresentationContext = true
self.modalTransitionStyle = .crossDissolve
let yourVC = self.storyboard?.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
let navController = UINavigationController(rootViewController: yourVC)
navController.modalPresentationStyle = .overCurrentContext
navController.modalTransitionStyle = .crossDissolve
self.present(navController, animated: true, completion: nil)
}
I am updating a simple solution. First add an id to your segue which presents modal. Than in properties change it's presentation style to "Over Current Context". Than add this code in presenting view controller (The controller which is presenting modal).
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let Device = UIDevice.currentDevice()
let iosVersion = NSString(string: Device.systemVersion).doubleValue
let iOS8 = iosVersion >= 8
let iOS7 = iosVersion >= 7 && iosVersion < 8
if((segue.identifier == "chatTable")){
if (iOS8){
}
else {
self.navigationController?.modalPresentationStyle = UIModalPresentationStyle.CurrentContext
}
}
}
Make sure you change segue.identifier to your own id ;)

Resources