SWRevealViewController closes when opened beyond 'snappoint' - objective-c

I'm trying to make the SWRevealViewController work in my application.
What I did is I didn't want to be able to open the menu from everywhere on the screen so I changed all the UIPanGestureRecognizer with UIScreenEdgePanGestureRecognizer so it would only be triggered from the side of the screen.
to achieve this I also altered thepanGestureRecognizer method. This looks as follows right now
- (UIScreenEdgePanGestureRecognizer*)panGestureRecognizer
{
if ( _panGestureRecognizer == nil )
{
_panGestureRecognizer = [[SWRevealViewControllerPanGestureRecognizer alloc] initWithTarget:self action:#selector(_handleRevealGesture:)];
_panGestureRecognizer.delegate = self;
_panGestureRecognizer.edges = UIRectEdgeLeft;
[_contentView.frontView addGestureRecognizer:_panGestureRecognizer];
}
return _panGestureRecognizer;
}
However (not sure if this change is causing my problem) when I start to open the menu from the left side and expand it over the point where it will normally snap to when opened up it will collapse back in.
so let's say it opens to 500px of the screen. When I drag it beyond those 500px it will automatically close the menu.
Also it's currently not possible to close the menu again by swiping it. What I did for now is add a gesturerecognizer which will trigger the revealViewController.revealToggle(animated: true) method.
Does anyone happen to know how to fix this?

import #import "SWRevealViewController.h"
These two gestures helps you to open and close sw menu . Drag and click for open and close SW
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
[self.view addGestureRecognizer:self.revealViewController.tapGestureRecognizer];
Open sw menu normally
[self.revealViewController revealToggleAnimated:YES]

I ended up fixing it by using the delegate methods that come with SWRevealViewController my entire code looks like this
import UIKit
import SWRevealViewController
import UIKit.UIGestureRecognizerSubclass
class ViewController: UIViewController, SWRevealViewControllerDelegate {
#IBOutlet var openMenu: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
openMenu.target = self.revealViewController()
openMenu.action = #selector(SWRevealViewController.revealToggle(_:))
revealViewController().delegate = self
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
self.hideMenuWhenTappedAround()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func revealControllerPanGestureShouldBegin(_ revealController: SWRevealViewController!) -> Bool {
let point = revealController.panGestureRecognizer().location(in: self.view)
if revealController.frontViewPosition == FrontViewPosition.left && point.x < 50.0 {
return true
}
else if revealController.frontViewPosition == FrontViewPosition.right {
return true
}
return false
}
func revealController(_ revealController: SWRevealViewController!, panGestureMovedToLocation location: CGFloat, progress: CGFloat) {
if location >= revealController.rearViewRevealWidth {
revealController.panGestureRecognizer().state = UIGestureRecognizerState.ended
}
}
}
The self.hideMenuWhentappedAround() makes sure that the menu will close when you tap anywhere on the uiview. This method is created in an extension of the UIViewController and it looks as follows.
import UIKit
import SWRevealViewController
extension UIViewController {
func hideMenuWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(SWRevealViewController.dismissMenu))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
func dismissMenu() {
self.revealViewController().revealToggle(animated: true)
}
}

Related

Handle long press on a single UITabBarItem

I use the long press gesture on a tab bar. But I only need the long press gesture for one particular tab bar item.
How can I solve this problem? Could I customize the long press gesture in tab bar?
Here's how I did it using Swift 3:
protocol MyTabControllerProtocol: class {
func tabLongPressed()
}
class MyTabController: UITabBarController {
func viewDidLoad() {
super.viewDidLoad()
viewControllers = [
// add your view controllers for each tab bar item
// NOTE: if you want view controller to respond to long press, then it should extend MyTabControllerProtocol
]
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(astroButtonItemLongPressed(_:)))
tabBar.addGestureRecognizer(longPressRecognizer)
}
func astroButtonItemLongPressed(_ recognizer: UILongPressGestureRecognizer) {
guard recognizer.state == .began else { return }
guard let tabBar = recognizer.view as? UITabBar else { return }
guard let tabBarItems = tabBar.items else { return }
guard let viewControllers = viewControllers else { return }
guard tabBarItems.count == viewControllers.count else { return }
let loc = recognizer.location(in: tabBar)
for (index, item) in tabBarItems.enumerated() {
guard let view = item.value(forKey: "view") as? UIView else { continue }
guard view.frame.contains(loc) else { continue }
if let nc = viewControllers[index] as? UINavigationController {
if let vc = nc.viewControllers.first as? MyTabControllerProtocol {
vc.tabLongPressed()
}
} else if let vc = viewControllers[index] as? MyTabControllerProtocol {
vc.tabLongPressed()
}
break
}
}
}
You can subclass UITabBarController and add a UILongPressGestureRecognizer to it's tabBar. Acting as the delegate of the gesture recognizer will allow you to be selective over when it will detect a long press. Since the tab bar item will be selected as soon as the user touches it you can use the selectedItem property to perform this check.
#interface TabBarController () <UIGestureRecognizerDelegate>
#property (nonatomic, strong) UILongPressGestureRecognizer *longPressRecognizer;
#end
#implementation TabBarController
- (void)viewDidLoad {
[super viewDidLoad];
self.longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(recognizerFired:)];
self.longPressRecognizer.delegate = self;
[self.tabBar addGestureRecognizer:self.longPressRecognizer];
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
// This will ensure the long press only occurs for the
// tab bar item which has it's tag set to 1.
// You can set this in Interface Builder or in code
// wherever you are creating your tabs.
if (self.tabBar.selectedItem.tag == 1) {
return YES;
}
else {
return NO;
}
}
- (void)recognizerFired:(UILongPressGestureRecognizer *)recognizer {
// Handle the long press...
}
#end
Here is a solution in swift 5 :
Add longpress Gesture recognizer to the "Entire" tabbar using storyboard or code..
and Don't forget to let your ViewController be its delegate .. and implement the delegate method below
to check if the incoming touch is inside "one" of your tabbar subViews .. if yes return true ,, else return false ..
here are the code that will let the recognizer fire only when we longPress on the first tab:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if touch.view?.isDescendant(of: tabBar.subviews[1]) == true {return true}
return false
}
Note: tabbar.subviews array count is number of the items + 1 which is the background of the tabbar .. so if you want the view of the first item you can fint it and index 1 not 0
I did this by getting the specific tabBarItem's view that user can interact and simply added the long press gesture to that. With that way you do not have to write any protocols or subclass the TabBarViewController.
let longPressGestureRecognizer = UILongPressGestureRecognizer.init(target: self, action: #selector(longTap(_:)))
longPressGestureRecognizer.minimumPressDuration = 1.0
self.tabBarController?.orderedTabBarItemViews()[0].addGestureRecognizer(longPressGestureRecognizer)
And as for getting the tabBarItemViews :
extension UITabBarController {
func orderedTabBarItemViews() -> [UIView] {
let interactionViews = tabBar.subviews.filter({$0.isUserInteractionEnabled})
return interactionViews.sorted(by: {$0.frame.minX < $1.frame.minX})
}
P.S. : The viewController, namely "self" is the first item for the tabBarController.
If you just need to recognize a long press on one of the tabBar items, you can do this in the corresponding viewController's viewDidLoad method:
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget: self action: #selector(handleLongPress:)];
[self.tabBarController.tabBar addGestureRecognizer: longPressGesture];
And then:
- (void)handleLongPress:(UILongPressGestureRecognizer *) recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
UITabBar *tabBar = ((UITabBar* )recognizer.view);
if (tabBar.selectedItem == self.tabBarItem) {
doSomethingVeryExciting();
}
}
}
This won't fire if you just switch tabs.

Transitioning between view controller, OS X

I am trying to write a single window timer application, where when the user presses the start button I want it to show another view controller with countdown etc. I'm also using story board in Xcode, where I have got a segue which connects the start button and the second view controller. However, there are only three different styles i.e. modal, sheet, and pop-over. I want to replace the first view controller the second one in the window. I cannot find a way to do that. I tried using a custom style for the segue, and in that use presentViewController: animator: method but I cannot figure out what to send as the argument for the animator:.
What is the simplest/proper way to transition from one view controller to the other in one window and vice versa?
Also in the storyboard when I select a view controller it shows an attribute called "Presentation" which can be multiple and single, what do those represent?
I think the simplest way is that swapping contentViewController of NSWindow.
// in NSViewController's subclass
#IBAction func someAction(sender: AnyObject) {
let nextViewController = ... // instantiate from storyboard or elsewhere
if let window = view.window where window.styleMask & NSFullScreenWindowMask > 0 {
// adjust view size to current window
nextViewController.view.frame = CGRectMake(0, 0, window.frame.width, window.frame.height)
}
view.window?.contentViewController = nextViewController
}
This is option #1.
If you want to use segue, create custom one and set it to segue class with identifier in IB.
class ReplaceSegue: NSStoryboardSegue {
override func perform() {
if let fromViewController = sourceController as? NSViewController {
if let toViewController = destinationController as? NSViewController {
// no animation.
fromViewController.view.window?.contentViewController = toViewController
}
}
}
}
This is option #2.
Last option is using presentViewController:animator: of NSViewController. The code below is custom NSViewControllerPresentationAnimator for dissolve animation.
class ReplacePresentationAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentationOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
if let window = fromViewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
fromViewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
viewController.view.alphaValue = 0
window.contentViewController = viewController
viewController.view.animator().alphaValue = 1.0
})
}
}
func animateDismissalOfViewController(viewController: NSViewController, fromViewController: NSViewController) {
if let window = viewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
viewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
fromViewController.view.alphaValue = 0
window.contentViewController = fromViewController
fromViewController.view.animator().alphaValue = 1.0
})
}
}
}
Then present VC like this.
#IBAction func replaceAction(sender: AnyObject) {
let nextViewController = ... // instantiate from storyboard or elsewhere
presentViewController(nextViewController, animator: ReplacePresentationAnimator())
}
For dismissal, call presentingViewController's dismissViewController: in the presented VC.
#IBAction func dismissAction(sender: AnyObject) {
presentingViewController?.dismissViewController(self)
}
Swift4 Version
class ReplacePresentationAnimator: NSObject, NSViewControllerPresentationAnimator {
func animatePresentation(of viewController: NSViewController, from fromViewController: NSViewController) {
if let window = fromViewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
fromViewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
viewController.view.alphaValue = 0
window.contentViewController = viewController
viewController.view.animator().alphaValue = 1.0
})
}
}
func animateDismissal(of viewController: NSViewController, from fromViewController: NSViewController) {
if let window = viewController.view.window {
NSAnimationContext.runAnimationGroup({ (context) -> Void in
viewController.view.animator().alphaValue = 0
}, completionHandler: { () -> Void in
fromViewController.view.alphaValue = 0
window.contentViewController = fromViewController
fromViewController.view.animator().alphaValue = 1.0
})
}
}
}
Hope this help.
If you have one parent view controller, you can assign child view controllers to it, and use the transition method. Example code, to be placed in viewDidLoad of the parent view controller:
if let firstController = self.storyboard?.instantiateController(withIdentifier: "firstController") as? NSViewController {
firstController.view.autoresizingMask = [.width, .height]
firstController.view.frame = self.view.bounds
self.addChild(firstController)
self.view.addSubview(firstController.view)
}
if let secondController = self.storyboard?.instantiateController(withIdentifier: "secondController") as? NSViewController {
self.addChild(secondController)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if let firstController = self.children.first, let secondController = self.children.last {
self.transition(from: firstController, to: secondController, options: .crossfade, completionHandler: nil)
}
}
It's essential that the view of the first child controller is being added as sub view to the view of the parent controller, otherwise the transition method doesn't work.
In the example above, a storyboard is used with one main view controller (= self), one child view controller with storyboard ID "firstController', and another child view controller with storyboard ID "secondController'

Hiding the master view controller with UISplitViewController in iOS8

I have an iOS7 application, which was based on the Xcode master-detail template, that I am porting to iOS8. One area that has changed a lot is the UISplitViewController.
When in portrait mode, if the user taps on the detail view controller, the master view controller is dismissed:
I would also like to be able to programmatically hide the master view controller if the user taps on a row.
In iOS 7, the master view controller was displayed as a pop-over, and could be hidden as follows:
[self.masterPopoverController dismissPopoverAnimated:YES];
With iOS 8, the master is no longer a popover, so the above technique will not work.
I've tried to dismiss the master view controller:
self.dismissViewControllerAnimated(true, completion: nil)
Or tell the split view controller to display the details view controller:
self.splitViewController?.showDetailViewController(bookViewController!, sender: self)
But nothing has worked so far. Any ideas?
Extend the UISplitViewController as follows:
extension UISplitViewController {
func toggleMasterView() {
let barButtonItem = self.displayModeButtonItem()
UIApplication.sharedApplication().sendAction(barButtonItem.action, to: barButtonItem.target, from: nil, forEvent: nil)
}
}
In didSelectRowAtIndexPath or prepareForSegue, do the following:
self.splitViewController?.toggleMasterView()
This will smoothly slide the master view out of the way.
I got the idea of using the displayModeButtonItem() from this post and I am simulating a tap on it per this post.
I am not really happy with this solution, since it seems like a hack. But it works well and there seems to be no alternative yet.
Use preferredDisplayMode. In didSelectRowAtIndexPath or prepareForSegue:
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
self.splitViewController?.preferredDisplayMode = .Automatic
Unfortunately the master view abruptly disappears instead of sliding away, despite the documentation stating:
If changing the value of this property leads to an actual change in
the current display mode, the split view controller animates the
resulting change.
Hopefully there is a better way to do this that actually animates the change.
The code below hides the master view with animation
UIView.animateWithDuration(0.5) { () -> Void in
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
I was able to have the desired behavior in a Xcode 6.3 Master-Detail Application (universal) project by adding the following code in the MasterViewController's - prepareForSegue:sender: method:
if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
The complete - prepareForSegue:sender: implementation should look like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
let object = objects[indexPath.row] as! NSDate
let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
if view.traitCollection.userInterfaceIdiom == .Pad && splitViewController?.displayMode == .PrimaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
}
}
}
Using traitCollection may also be an alternative/supplement to displayMode in some projects. For example, the following code also works for a Xcode 6.3 Master-Detail Application (universal) project:
let traits = view.traitCollection
if traits.userInterfaceIdiom == .Pad && traits.horizontalSizeClass == .Regular {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .PrimaryHidden
}
let completion: Bool -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .Automatic
}
UIView.animateWithDuration(0.3, animations: animations, completion: completion)
}
Swift 4 update:
Insert it into prepare(for segue: ...
if splitViewController?.displayMode == .primaryOverlay {
let animations: () -> Void = {
self.splitViewController?.preferredDisplayMode = .primaryHidden
}
let completion: (Bool) -> Void = { _ in
self.splitViewController?.preferredDisplayMode = .automatic
}
UIView.animate(withDuration: 0.3, animations: animations, completion: completion)
}
Modifying the answers above this is all I needed in a method of my detail view controller that configured the view:
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
Of course it lacks the grace of animation.
try
let svc = self.splitViewController
svc.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
My solution in the Swift 1.2
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath){
var screen = UIScreen.mainScreen().currentMode?.size.height
if (UIDevice.currentDevice().userInterfaceIdiom == UIUserInterfaceIdiom.Pad) || screen >= 2000 && UIDevice.currentDevice().orientation.isLandscape == true && (UIDevice.currentDevice().userInterfaceIdiom == .Phone){
performSegueWithIdentifier("showDetailParse", sender: nil)
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
} else if (UIDevice.currentDevice().userInterfaceIdiom == .Phone) {
performSegueWithIdentifier("showParse", sender: nil)
}
}
for iPad add Menu button like this
UIBarButtonItem *menuButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:#"burger_menu"]
style:UIBarButtonItemStylePlain
target:self.splitViewController.displayModeButtonItem.target
action:self.splitViewController.displayModeButtonItem.action];
[self.navigationItem setLeftBarButtonItem:menuButtonItem];
This work great with both landscape and portrait mode.
To programmatically close the popover vc you just need to force the button action like this
[self.splitViewController.displayModeButtonItem.target performSelector:appDelegate.splitViewController.displayModeButtonItem.action];
Very similar to the method by phatmann, but a bit simpler in Swift 5. And it's not technically a 'hack', as it is what the iOS doc suggested.
In your prepareForSegue or other methods that handle touches, in
let barButton = self.splitViewController?.displayModeButtonItem
_ = barButton?.target?.perform(barButton?.action)
According to Apple, the splitViewController's displayModeButtonItem is set up for you to display the master view controller in a way that suits your device orientation. That is, .preferHidden in portrait mode.
All there's to do is to press the button, programatically. Or you can put it in an extension to UISplitViewController, like phatmann did.

Customize NSToolbar - Disable "Use small size"

How do I disable the "Use small size" option in the toolbar? I am using Xcode 4.
(That's the option that appears when users go to customize the Toolbar.)
If you're not distributing on the Mac App Store, and don't mind subclassing private methods, you can create an NSToolbarSubclass and override _allowsSizeMode: to return NO:
- (BOOL)_allowsSizeMode:(NSToolbarSizeMode)mode {
return mode != NSToolbarSizeModeSmall;
}
This has the added benefit of removing the checkbox from the customization sheet, as well.
You could subclass NSToolbar, override -setSizeMode: and in your implementation call [super setSizeMode: NSToolbarSizeModeRegular];.
If you're instantiating the toolbar in Interface Builder then make sure you assign your subclass to the toolbar in the nib.
#implementation RKToolbar
- (void)setSizeMode:(NSToolbarSizeMode)aSizeMode
{
[super setSizeMode:NSToolbarSizeModeRegular];
}
#end
This won't remove the checkbox from the customize panel but it will prevent it from doing anything.
There's not really a supported way to remove the checkbox. This does work but it's pretty hacky:
//in your NSToolbar subclass
- (void)runCustomizationPalette:(id)sender
{
[super runCustomizationPalette:sender];
NSWindow* toolbarWindow = [NSApp mainWindow];
NSWindow* sheet = [toolbarWindow attachedSheet];
for(NSView* view in [[sheet contentView] subviews])
{
if([view isKindOfClass:[NSButton class]])
{
if([[[(NSButton*)view cell] valueForKey:#"buttonType"] integerValue] == NSSwitchButton)
{
[view setHidden:YES];
}
}
}
}
Thanks to Rob Keniger for the excellent start. If you can have your custom toolbar as a delegate of your window, you can avoid having "Use small size" visible by getting at the sheet before it is displayed on screen. Do this by implementing [NSToolbar window:willPositionSheet:usingRect:] in the custom toolbar class. Elsewhere in your code, you'll need to do:
[myWindowWithToolbar setDelegate:myInstanceOfXXToolbar];
Here's the updated custom toolbar class:
#implementation XXToolbar
- (void)setSizeMode:(NSToolbarSizeMode)aSizeMode
{
[super setSizeMode:NSToolbarSizeModeRegular];
}
- (NSRect)window:(NSWindow *)window willPositionSheet:(NSWindow *)sheet usingRect:(NSRect)rect {
NSView *buttonView = nil;
for(NSView* view in [[sheet contentView] subviews])
{
if([view isKindOfClass:[NSButton class]])
{
if([[[(NSButton*)view cell] valueForKey:#"buttonType"] integerValue] == NSSwitchButton)
{
buttonView = view;
break;
}
}
}
if (buttonView) {
[buttonView setHidden:YES];
// This is important as it causes the sheet to redraw without the button off screen
[[sheet contentView] display];
}
return rect;
}
#end
Hope you find this useful.
Here's a Swift 2.2 version of #MacGreg's solution. You can keep your NSWindowDelegate wherever you like, just ensure at least the following is called:
var toolbar: UniformToolbar!
func window(window: NSWindow, willPositionSheet sheet: NSWindow, usingRect rect: NSRect) -> NSRect {
toolbar.removeSizeToggle(window: sheet)
return rect
}
Toolbar Subclass without the Checkbox
class UniformToolbar: NSToolbar {
override var sizeMode: NSToolbarSizeMode {
get {
return NSToolbarSizeMode.Regular
}
set { /* no op */ }
}
func removeSizeToggle(window window: NSWindow) {
guard let views = window.contentView?.subviews else { return }
let toggle: NSButton? = views.lazy
.flatMap({ (view: NSView) -> NSButton? in view as? NSButton })
.filter({ (button: NSButton) -> Bool in
guard let buttonTypeValue = button.cell?.valueForKey("buttonType")?.unsignedIntegerValue,
buttonType = NSButtonType(rawValue: buttonTypeValue)
else { return false }
return buttonType == .SwitchButton
})
.first
toggle?.hidden = true
window.contentView?.display()
}
}

How can I easily save the Window size and position state using Obj-C?

What is the best way to remember the Windows position between application loads using Obj-C? I am using Interface Builder for the interface, is it possible to do this with bindings.
What is the recommended method? Thank you.
Put a name that is unique to that window (e.g. "MainWindow" or "PrefsWindow") in the Autosave field under Attributes in Interface Builder. It will then have its location saved in your User Defaults automatically.
To set the Autosave name programmatically, use -setFrameAutosaveName:. You may want to do this if you have a document-based App or some other situation where it doesn't make sense to set the Autosave name in IB.
Link to documentation.
In Swift:
class MainWindowController : NSWindowController {
override func windowDidLoad() {
shouldCascadeWindows = false
window?.setFrameAutosaveName("MainWindow")
super.windowDidLoad()
}
According to the doc, to save a window's position:
NSWindow *window = // the window in question
[[window windowController] setShouldCascadeWindows:NO]; // Tell the controller to not cascade its windows.
[window setFrameAutosaveName:[window representedFilename]]; // Specify the autosave name for the window.
I tried all the solutions. It can only saves the position, not the size. So we should do that manually. This is how I do it on my GifCapture app https://github.com/onmyway133/GifCapture
class MainWindowController: NSWindowController, NSWindowDelegate {
let key = "GifCaptureFrameKey"
override func windowDidLoad() {
super.windowDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(_:)), name: Notification.Name.NSWindowWillClose, object: nil)
}
override func awakeFromNib() {
super.awakeFromNib()
guard let data = UserDefaults.standard.data(forKey: key),
let frame = NSKeyedUnarchiver.unarchiveObject(with: data) as? NSRect else {
return
}
window?.setFrame(frame, display: true)
}
func windowWillClose(_ notification: Notification) {
guard let frame = window?.frame else {
return
}
let data = NSKeyedArchiver.archivedData(withRootObject: frame)
UserDefaults.standard.set(data, forKey: key)
}
}
In Swift 5.2, in your NSWindowController class:
override func windowDidLoad() {
super.windowDidLoad()
self.windowFrameAutosaveName = "SomeWindowName"
}
That's all there is to it!
Based on onmyway133's answer I wrote a RestorableWindowController class. As long as your window controller inherits from it, position and size for your windows are restored.
import Cocoa
open class RestorableWindowController: NSWindowController {
// MARK: - Public -
open override func windowDidLoad() {
super.windowDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose), name: NSWindow.willCloseNotification, object: nil)
if let frame = storedFrame {
window?.setFrame(frame, display: true)
}
}
open override func awakeFromNib() {
super.awakeFromNib()
if let frame = storedFrame {
window?.setFrame(frame, display: true)
}
}
open override var contentViewController: NSViewController? {
didSet {
if let frame = storedFrame {
window?.setFrame(frame, display: true)
}
}
}
// MARK: - Private -
private var storedFrameKey: String {
String(describing: type(of: self)) + "/storedFrameKey"
}
private var storedFrame: NSRect? {
guard let string = UserDefaults.standard.string(forKey: storedFrameKey) else {
return nil
}
return NSRectFromString(string)
}
#objc private func windowWillClose() {
guard let frame = window?.frame else {
return
}
UserDefaults.standard.set(NSStringFromRect(frame), forKey: storedFrameKey)
}
}
For me, the following line in -applicationDidFinishLaunching in the app delegate workes fine (under Catalina, macOS 10.15):
[self.window setFrameAutosaveName: #"NameOfMyApp"];
it is important that this line
[self.window setDelegate: self];
is executed before setFrameAutosaveName in -applicationDidFinishLaunching !
In order to restore a window, you can set the Restoration ID in Interface Builder. This will be used as part of the key under which the frame is stored in NSUserDefaults. -- but that didn't (always) work for me.
NSWindow has setFrameUsingName(_:) etc. to configure this, like #BadmintonCat wrote, and you can serialize the window position manually, too, in case that doesn't work, either.
The simplest solution in my app though was to use the NSWindowController.windowFrameAutosaveName property and set it to something in awakeFromNib(_:). That single line affected loading and saving successfully.
Got sick and tired of Apples AutoSave and IB BS which sometimes does and sometimes doesn't work and depends on flag settings in System Prefs blah blah blah. Just do this, and it ALWAYS WORKS and even remembers users full screen state!
-(void)applicationDidFinishLaunching:(NSNotification *)notification
{
[_window makeKeyAndOrderFront:self];
// Because Saving App Position and Size is FUBAR
NSString *savedAppFrame = [userSettings stringForKey:AppScreenSizeAndPosition];
NSRect frame;
if(savedAppFrame) {
frame = NSRectFromString(savedAppFrame);
[_window setFrame:frame display:YES];
}
else
[_window center];
// Because saving of app size and position on screen doesn't remember full screen
if([userSettings boolForKey:AppIsFullScreen])
[_window toggleFullScreen:self];
}
-(void)windowDidEnterFullScreen:(NSNotification *)notification
{
[userSettings setBool:YES forKey:AppIsFullScreen];
}
-(BOOL)windowShouldClose:(NSWindow *)sender
{
// Have to use this to set zoom state because exit full screen state always called on close
if(sender == _window) {
[userSettings setBool:(_window.isZoomed ? YES:NO) forKey:AppIsFullScreen];
}
return YES;
}
-(void)applicationWillTerminate:(NSNotification *)aNotification
{
[userSettings setObject:NSStringFromRect(_window.frame) forKey:AppScreenSizeAndPosition];
[userSettings synchronize];
}
like everyone else I found that setting it programmatically works...
self.windowFrameAutosaveName = NSWindow.FrameAutosaveName("MyWindow")
but ONLY if you do NOT also set it in IB!
If you set it in both... you're back to not working.
BTW: I found this out by literally adding "WTF" to the end of the one in code, and suddenly having everything working! 😬