Search bar overlaps with status bar on iOS 11 - cocoa-touch

I am using a UISearchController and a UISearchResultsController to implement search functionality.
MySearchResultsController implements UISearchResultsUpdating and UISearchBarDelegate:
override open func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = [];
self.automaticallyAdjustsScrollViewInsets = false;
}
I display the searchbar in the tableHeader like this in MyTableViewController:
- (void)viewDidLoad {
[super viewDidLoad];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:self.searchResultsController];
self.searchController.searchResultsUpdater = self.searchResultsController;
self.searchController.searchBar.delegate = self.searchResultsController;
self.searchController.searchBar.scopeButtonTitles = #[NSLocalizedString(#"SEARCH_SCOPE_TEMPERATURES", nil), NSLocalizedString(#"SEARCH_SCOPE_KNOWHOW", nil)];
self.tableView.tableHeaderView = self.searchController.searchBar;
self.definesPresentationContext = YES;
}
This worked perfectly before, but under iOS 11 the search bar overlaps with the status bar as soon as I tap into it (see screenshots). I tried lots of different things to get it to display correctly but haven't found a solution yet.

I found that the problem was that the presenting view Controller also sets
override open func viewDidLoad() {
super.viewDidLoad()
self.edgesForExtendedLayout = [];
self.automaticallyAdjustsScrollViewInsets = false;
}
I have to do this because the table view does not actually extend all the way to the top.
I solved this like that in my presenting view Controller:
override open func viewDidLoad() {
super.viewDidLoad()
self.automaticallyAdjustsScrollViewInsets = false;
if (#available(iOS 11.0, *)) {
//NSLog(#"iOS 11.0");
} else {
self.edgesForExtendedLayout = UIRectEdgeNone;
//NSLog(#"iOS < 11.0");
}
}
Seems to be an iOS 11 bug, or at least an odd behavior…

This is what Worked for me:
override func viewDidLoad() {
// to fix the Status Bar Issue:
if #available(iOS 11.0, *) {
definesPresentationContext = true
}
// You'll also need this properties on your Search Bar:
searchController = UISearchController.init(searchResultsController: nil)
searchController?.searchResultsUpdater = self
searchController?.hidesNavigationBarDuringPresentation = false
}

I managed to solve this by subclassing UISearchController. My answer is in Swift but maybe the principles works with ojective-c as well. Please see my answer here: https://stackoverflow.com/a/46339336/8639272

Related

How can I force focus to change to a specific view in tvOS?

I am implementing custom code to handle a click on the Menu button on the Siri Remote.
How can I force focus to change to my custom menu when pressing the menu button?
For ios 10 you should use preferredFocusEnvironments instead of preferredFocusedView .
In below example if you want to focus on button then see below code.
#IBOutlet weak var button: UIButton!
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [button]
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsFocusUpdate()
updateFocusIfNeeded()
}
Finally figured it out myself. You have to override the preferredFocusedView property of your UIView or UIViewController.
In Swift it works like this:
func myClickHandler() {
someCondition = true
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
someCondition = false
}
override weak var preferredFocusedView: UIView? {
if someCondition {
return theViewYouWant
} else {
return defaultView
}
}
I can't quite remember how to override getters in Objective-C so if someone want to post that I'll edit the answer.
Here is another implementation based on Slayters answer above. Its slightly more elegant I think than using the conditional booleans.
Put this in your viewcontroller
var viewToFocus: UIView? = nil {
didSet {
if viewToFocus != nil {
self.setNeedsFocusUpdate();
self.updateFocusIfNeeded();
}
}
}
override weak var preferredFocusedView: UIView? {
if viewToFocus != nil {
return viewToFocus;
} else {
return super.preferredFocusedView;
}
}
Then to use it in your code
viewToFocus = myUIView;
here is the objective C
- (UIView *)preferredFocusedView
{
if (someCondition) {
// this is if your menu is a tableview
NSIndexPath *ip = [NSIndexPath indexPathForRow:2 inSection:0];
UITableViewCell * cell = [self.categoryTableView cellForRowAtIndexPath:ip];
return cell;
}
return self.view.preferredFocusedView;
}
in your viewDidLoad or view did appear do something like this:
UIFocusGuide *focusGuide = [[UIFocusGuide alloc]init];
focusGuide.preferredFocusedView = [self preferredFocusedView];
[self.view addLayoutGuide:focusGuide];
if you want to do it when it first launches
Here's a nice little Swift 2 copy / paste snippet:
var myPreferredFocusedView: UIView?
override var preferredFocusedView: UIView? {
return myPreferredFocusedView
}
func updateFocus(to view: UIView) {
myPreferredFocusedView = napDoneView
setNeedsFocusUpdate()
updateFocusIfNeeded()
}
Use it like this:
updateFocus(to: someAwesomeView)
#elu5ion 's answer, but in objective-c
first declare:
#property (nonatomic) UIView *preferredView;
Set these methods:
-(void)setPreferredView:(UIView *)preferredView{
if (preferredView != nil) {
_preferredView = nil;
UIFocusGuide *focusGuide = [[UIFocusGuide alloc]init];
[self.view addLayoutGuide:focusGuide];
focusGuide.preferredFocusedView = [self preferredFocusedView];
[self setNeedsFocusUpdate];
[self updateFocusIfNeeded];
}
_preferredView = preferredView;
}
- (UIView *)preferredFocusedView {
if (_preferredView) {
return _preferredView;
}
return self.view.preferredFocusedView;
}

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.

UIPopoverPresentationController on iOS 8 iPhone

Does anyone know if UIPopoverPresentationController can be used to present popovers on iPhones? Wondering if Apple added this feature on iOS 8 in their attempt to create a more unified presentation controllers for iPad and iPhone.
Not sure if its OK to ask/answer questions from Beta. I will remove it in that case.
You can override the default adaptive behaviour (UIModalPresentationFullScreen in compact horizontal environment, i.e. iPhone) using the
adaptivePresentationStyleForPresentationController: method available through UIPopoverPresentationController.delegate.
UIPresentationController uses this method to ask the new presentation style to use, which in your case, simply returning UIModalPresentationNone will cause the UIPopoverPresentationController to render as a popover instead of fullscreen.
Here's an example of the popover using a segue setup in storyboard from a UIBarButtonItem to "present modally" a UIViewController
class SomeViewController: UIViewController, UIPopoverPresentationControllerDelegate {
// override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) { // swift < 3.0
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "PopoverSegue" {
if let controller = segue.destinationViewController as? UIViewController {
controller.popoverPresentationController.delegate = self
controller.preferredContentSize = CGSize(width: 320, height: 186)
}
}
}
// MARK: UIPopoverPresentationControllerDelegate
//func adaptivePresentationStyleForPresentationController(controller: UIPresentationController!) -> UIModalPresentationStyle { // swift < 3.0
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
// Return no adaptive presentation style, use default presentation behaviour
return .None
}
}
This trick was mentioned in WWDC 2014 session 214 "View Controller Advancement in iOS8" (36:30)
If anybody wants to present a popover with code only, you can use the following approach.
OBJECTIVE - C
Declare a property of UIPopoverPresentationController:
#property(nonatomic,retain)UIPopoverPresentationController *dateTimePopover8;
Use the following method to present the popover from UIButton:
- (IBAction)btnSelectDatePressed:(id)sender
{
UINavigationController *destNav = [[UINavigationController alloc] initWithRootViewController:dateVC];/*Here dateVC is controller you want to show in popover*/
dateVC.preferredContentSize = CGSizeMake(280,200);
destNav.modalPresentationStyle = UIModalPresentationPopover;
_dateTimePopover8 = destNav.popoverPresentationController;
_dateTimePopover8.delegate = self;
_dateTimePopover8.sourceView = self.view;
_dateTimePopover8.sourceRect = sender.frame;
destNav.navigationBarHidden = YES;
[self presentViewController:destNav animated:YES completion:nil];
}
Use the following method to present the popover from UIBarButtonItem:
- (IBAction)btnSelectDatePressed:(id)sender
{
UINavigationController *destNav = [[UINavigationController alloc] initWithRootViewController:dateVC];/*Here dateVC is controller you want to show in popover*/
dateVC.preferredContentSize = CGSizeMake(280,200);
destNav.modalPresentationStyle = UIModalPresentationPopover;
_dateTimePopover8 = destNav.popoverPresentationController;
_dateTimePopover8.delegate = self;
_dateTimePopover8.sourceView = self.view;
CGRect frame = [[sender valueForKey:#"view"] frame];
frame.origin.y = frame.origin.y+20;
_dateTimePopover8.sourceRect = frame;
destNav.navigationBarHidden = YES;
[self presentViewController:destNav animated:YES completion:nil];
}
Implement this delegate method too in your view controller:
- (UIModalPresentationStyle) adaptivePresentationStyleForPresentationController: (UIPresentationController * ) controller {
return UIModalPresentationNone;
}
To dismiss this popover, simply dismiss the view controller. Below is the code to dismiss the view controller:
-(void)hideIOS8PopOver
{
[self dismissViewControllerAnimated:YES completion:nil];
}
SWIFT
Use the following method to present the popover from UIButon:
func filterBooks(sender: UIButon)
{
let filterVC = FilterDistanceViewController(nibName: "FilterDistanceViewController", bundle: nil)
var filterDistanceViewController = UINavigationController(rootViewController: filterVC)
filterDistanceViewController.preferredContentSize = CGSizeMake(300, 205)
let popoverPresentationViewController = filterDistanceViewController.popoverPresentationController
popoverPresentationViewController?.permittedArrowDirections = .Any
popoverPresentationViewController?.delegate = self
popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem
popoverPresentationViewController!.sourceView = self.view;
popoverPresentationViewController!.sourceRect = sender.frame
filterDistanceViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
filterDistanceViewController.navigationBarHidden = true
self.presentViewController(filterDistanceViewController, animated: true, completion: nil)
}
Use the following method to present the popover from UIBarButtonItem:
func filterBooks(sender: UIBarButtonItem)
{
let filterVC = FilterDistanceViewController(nibName: "FilterDistanceViewController", bundle: nil)
var filterDistanceViewController = UINavigationController(rootViewController: filterVC)
filterDistanceViewController.preferredContentSize = CGSizeMake(300, 205)
let popoverPresentationViewController = filterDistanceViewController.popoverPresentationController
popoverPresentationViewController?.permittedArrowDirections = .Any
popoverPresentationViewController?.delegate = self
popoverPresentationController?.barButtonItem = self.navigationItem.rightBarButtonItem
popoverPresentationViewController!.sourceView = self.view;
var frame:CGRect = sender.valueForKey("view")!.frame
frame.origin.y = frame.origin.y+20
popoverPresentationViewController!.sourceRect = frame
filterDistanceViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
filterDistanceViewController.navigationBarHidden = true
self.presentViewController(filterDistanceViewController, animated: true, completion: nil)
}
Implement this delegate method too in your view controller:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle{
return .None
}
Please make sure to add delegate UIPopoverPresentationControllerDelegate in .h/.m/.swift file
PROBLEM: iPhone popover displays fullscreen and does not respect preferredContentSize value.
SOLUTION: Contrary to what Apple suggests in the UIPopoverPresentationController Class reference, presenting the view controller after getting a reference to the popover presentation controller and configuring it.
// Get the popover presentation controller and configure it.
//...
// Present the view controller using the popover style.
[self presentViewController:myPopoverViewController animated: YES completion: nil];
Make sure to implement UIAdaptivePresentationControllerDelegate
like this:
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
return UIModalPresentationNone;
}
If you don't want full-screen popovers
I've found some workaround.
On Xcode6.1, use presentationController.delegate instead of popoverPresentationController.delegate.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier compare:#"showPopOver"] == NSOrderedSame) {
UINavigationController * nvc = segue.destinationViewController;
UIPresentationController * pc = nvc.presentationController;
pc.delegate = self;
}
}
#pragma mark == UIPopoverPresentationControllerDelegate ==
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
{
return UIModalPresentationNone;
}
In WWDC 2014 "View Controller Advancements in iOS8", below codes can show popover on iPhone.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
UINavigationController * nvc = segue.destinationViewController;
UIPopoverPresentationController * pvc = nvc.popoverPresentationController;
pvc.delegate = self;
}
#pragma mark == UIPopoverPresentationControllerDelegate ==
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller
{
return UIModalPresentationNone;
}
But On Xcode 6.1, these codes shows FullScreen presentation...
(nvc.popoverPresentationController is nil)
I doubt it might be an Apple's bug.
In iOS 8.3 and later, use the following syntax in the UIPopoverPresentationControllerDelegate protocol to override your popup's UIModalPresentationStyle.
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
You can extend the UIPopoverPresentationControllerDelegate like this:
protocol PopoverPresentationSourceView {}
extension UIBarButtonItem : PopoverPresentationSourceView {}
extension UIView : PopoverPresentationSourceView {}
extension UIPopoverPresentationControllerDelegate where Self : UIViewController {
func present(popover: UIViewController,
from sourceView: PopoverPresentationSourceView,
size: CGSize, arrowDirection: UIPopoverArrowDirection) {
popover.modalPresentationStyle = .popover
popover.preferredContentSize = size
let popoverController = popover.popoverPresentationController
popoverController?.delegate = self
if let aView = sourceView as? UIView {
popoverController?.sourceView = aView
popoverController?.sourceRect = CGRect(x: aView.bounds.midX, y: aView.bounds.midY, width: 0, height: 0)
} else if let barButtonItem = sourceView as? UIBarButtonItem {
popoverController?.barButtonItem = barButtonItem
}
popoverController?.permittedArrowDirections = arrowDirection
present(popover, animated: true, completion: nil)
}
}
You can now call present(popover: from: size: arrowDirection: ) from any view controller that implements UIPopoverPresentationControllerDelegate eg.
class YourViewController : UIViewController {
#IBAction func someButtonPressed(_ sender: UIButton) {
let popover = SomeViewController()
present(popover: popover, from: sender, size: CGSize(width: 280, height: 400), arrowDirection: .right)
}
}
extension YourViewController : UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
add these two methods in your WEBVIEW class. and add
-(void) prepareForSegue: (UIStoryboardSegue * ) segue sender: (id) sender {
// Assuming you've hooked this all up in a Storyboard with a popover presentation style
if ([segue.identifier isEqualToString: #"showPopover"]) {
UINavigationController * destNav = segue.destinationViewController;
pop = destNav.viewControllers.firstObject;
// This is the important part
UIPopoverPresentationController * popPC = destNav.popoverPresentationController;
popPC.delegate = self;
}
}
- (UIModalPresentationStyle) adaptivePresentationStyleForPresentationController: (UIPresentationController * ) controller {
return UIModalPresentationNone;
}
In the UIAdaptivePresentationControllerDelegate you must use this method:
func adaptivePresentationStyle(for: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
instead of this:
func adaptivePresentationStyle(for: UIPresentationController) -> UIModalPresentationStyle

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! 😬