简体   繁体   中英

How to create Cocoa App Preferences for Mac?

I have created a simple app in Xcode for Mac, it builds and compiles fine.

How do I create a menu for preferences? Is there an easy way or do I have to create a new interface? How do I then get and put values into these preferences?

I did find one tutorial but it was for iOS, and from what I can see the 'Settings bundle' isn't available if you're developing for Mac.

EDIT: The following link is perfect for this: https://developer.apple.com/cocoa/cocoabindings.html

This is the documentation you want. More specifically How to provide a Preferences Interface .

Anyhow, you'll have to make your own preferences menu/window as you would make any other window, and have some controls that allow modifying values that are then stored in the User Defaults dictionary, which is where user preferences are usually stored (a process which Cocoa Bindings makes extremely easy).

Otherwise, manually, you would probably want a reference to [NSUserDefaults standardUserDefaults] so you can -setObject:ForKey: and so forth. It would be wise to read the entire documentation linked to above (including the other sections).

For more information on how Cocoa Bindings can be used to interface with NSUserDefaults to store/retrieve preferences, see the Apple docs here .

For the window itself, i would recommend the RHPreferences framework.

Available on GitHub . BSD Licensed.

Its a simple and easy Preferences window controller with multiple tabs for your next Mac application.

It also provides:

  • Auto resizing between different sized tab views (With animation)
  • Custom NSToolbarItem support
  • Persistence of the last used tab
  • Support for placeholder NSToolbarItems (eg NSToolbarFlexibleSpaceItemIdentifier & NSToolbarShowFontsItemIdentifier)

NSTabViewController.TabStyle.toolbar – A style that automatically adds any tabs to the window's toolbar. The tab view controller takes control of the window's toolbar and sets itself as the toolbar's delegate.

https://developer.apple.com/documentation/appkit/nstabviewcontroller/tabstyle

Keeping in mind above we can create NSWindowController with NSWindow.contentViewController set to NSTabViewController to get standard macOS Preferences window.

Here is a Preferences window made from code (Swift 4):

File: PreferencesPageID.swift – Keeps Preference page properties. Used in callbacks.

enum PreferencesPageID: Int, CaseIterable {

   case generic, misc

   var image: NSImage? {
      switch self {
      case .generic:
         return NSImage(named: NSImage.folderSmartName)
      case .misc:
         return NSImage(named: NSImage.networkName)
      }
   }

   var title: String {
      switch self {
      case .generic:
         return "Some"
      case .misc:
         return "Other"
      }
   }
}

File: PreferencesTabView.swift – Represents Preference page content view.

class PreferencesTabView: View {

   let id: PreferencesPageID

   init(id: PreferencesPageID) {
      self.id = id
      super.init()
   }

   required init?(coder decoder: NSCoder) {
      fatalError()
   }

   override func setupUI() {
      switch id {
      case .generic:
         backgroundColor = .red
         setIntrinsicContentSize(CGSize(width: 400, height: 200))
      case .misc:
         backgroundColor = .blue
         setIntrinsicContentSize(CGSize(width: 400, height: 300))
      }
   }
}

File: PreferenceItemViewController.swift - Controller which keeps Preference page content view. Needed mostly to fulfil macOS SDK requirements.

class PreferenceItemViewController: ViewController {

   private let contentView: PreferencesTabView

   override func loadView() {
      view = contentView
   }

   init(view: PreferencesTabView) {
      contentView = view
      super.init(nibName: nil, bundle: nil)
   }

   required init?(coder: NSCoder) {
      fatalError()
   }    

   override func viewDidLayout() {
       super.viewDidLayout()
       preferredContentSize = view.intrinsicContentSize
   }    
}

File: PreferencesViewController.swift – Used as a NSWindow.contentViewController .

class PreferencesViewController: TabViewController {

   enum Event {
      case selected(PreferencesPageID)
   }

   var eventHandler: ((Event) -> Void)?


   override func setupUI() {
      tabStyle = .toolbar // This will "turn" View Controller to standard Preferences window.
      transitionOptions = .allowUserInteraction
      canPropagateSelectedChildViewControllerTitle = false
      let views = [PreferencesTabView(id: .generic), PreferencesTabView(id: .misc)]
      views.forEach {
         let item = NSTabViewItem(viewController: PreferenceItemViewController(view: $0))
         item.label = $0.id.title
         item.image = $0.id.image
         addTabViewItem(item)
      }
   }

   override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
      super.tabView(tabView, didSelect: tabViewItem)
      if let view = tabViewItem?.viewController?.view as? PreferencesTabView {
         eventHandler?(.selected(view.id))
      }
   }

}

File: PreferencesWindowController.swift – Preferences window controller.

private class PreferencesWindowController: NSWindowController {

   private(set) lazy var viewController = PreferencesViewController()

   init() {
      let rect = CGRect(x: 400, y: 200, width: 400, height: 300)
      let window = NSWindow(contentRect: rect, styleMask: [.titled, .closable], backing: .buffered, defer: true)
      super.init(window: window)

      setupHandlers()

      let frameSize = window.contentRect(forFrameRect: window.frame).size
      viewController.view.setFrameSize(frameSize)
      window.contentViewController = viewController
   }

   required init?(coder: NSCoder) {
      fatalError()
   }

   // MARK: - Private

   private func setupHandlers() {
      viewController.eventHandler = { [weak self] in
         switch $0 {
         case .selected(let id):
            self?.window?.title = "Preferences — " + id.title
         }
      }
   }

}

Usage:

public class Application: NSApplication {

   private lazy var preferencesController = PreferencesWindowController()

   // Called from code or via IBAction
   private func showPreferences() {
      preferencesController.showWindow(nil)
   }

}

首选项屏幕

In MainMenu.xib of your project, you will see a drop down list after you click on your app name. Cntrl + Click + Drag Preferences to your project's AppDelegate.h file and create and IBAction method.

Create a class with NSWindowController (PreferenceWindowController) with xib, create a strong property of that PreferenceWindowController, alloc init it and add [self.preferenceWindowController showWindow:self]; in AppDelegate.m. This will create a window of Preferences for your OS X app.

Create a .nib and a controller (.h & .m) for each of your preference panes. Then hook them up dynamically in the AppDelegate.m of your app. I'm using it in my app that consists of a number dynamically loaded bundles such that each bundle has its own preferences.

You can see a really good concise example here: http://www.knowstack.com/nstoolbar-sample-code-objectivec/

In this example, it dynamically creates the NSToolbar and the NSToolbarItem. What you do in each window controller for each preference pane is up to you.

Here's the main parts of the AppDelegate.m:

//  AppDelegate.m
//  CocoaToolBars
//  Created by Debasis Das on 4/30/15.
//  Copyright (c) 2015 Knowstack. All rights reserved.

#import "AppDelegate.h"
@interface AppDelegate ()
@property (weak) IBOutlet NSWindow *window;
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  // Insert code here to initialize your application
  _toolbarTabsArray = [self toolbarItems];
  _toolbarTabsIdentifierArray = [NSMutableArray new];

  for (NSDictionary *dict in _toolbarTabsArray){
    [_toolbarTabsIdentifierArray addObject:dict[@"identifier"]];
  }
  _toolbar = [[NSToolbar alloc]    initWithIdentifier:@"ScreenNameToolbarIdentifier"];
  _toolbar.allowsUserCustomization = YES;
  _toolbar.delegate = self;
  self.window.toolbar = _toolbar;
}  

-(NSArray *)toolbarItems {
  NSArray *toolbarItemsArray = [NSArray arrayWithObjects:
                               [NSDictionary    dictionaryWithObjectsAndKeys:@"Find Departments",@"title",@"Department-50",@"icon",@"DepartmentViewController",@"class",@"DepartmentViewController",@"identifier", nil],
                              [NSDictionary dictionaryWithObjectsAndKeys:@"Find Accounts",@"title",@"Business-50",@"icon",@"AccountViewController",@"class",@"AccountViewController",@"identifier", nil],
                              [NSDictionary dictionaryWithObjectsAndKeys:@"Find Employees",@"title",@"Edit User-50",@"icon",@"EmployeeViewController",@"class",@"EmployeeViewController",@"identifier", nil],
                              nil];
  return  toolbarItemsArray;
}

- (void)applicationWillTerminate:(NSNotification *)aNotification {
  // Insert code here to tear down your application
}

- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
   itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag {
  NSDictionary *itemInfo = nil;

  for (NSDictionary *dict in _toolbarTabsArray) {
    if([dict[@"identifier"] isEqualToString:itemIdentifier]) {
      itemInfo = dict;
      break;
    }
  }

  NSAssert(itemInfo, @"Could not find preferences item: %@", itemIdentifier);

  NSImage *icon = [NSImage imageNamed:itemInfo[@"icon"]];
  if(!icon) {
    icon = [NSImage imageNamed:NSImageNamePreferencesGeneral];
  }
  NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
  item.label = itemInfo[@"title"];
  item.image = icon;
  item.target = self;
  item.action = @selector(viewSelected:);
  return item;
}

-(void)viewSelected:(id)sender {
  NSToolbarItem *item = sender;
  [self loadViewWithIdentifier:item.itemIdentifier withAnimation:YES];
}

-(void)loadViewWithIdentifier:(NSString *)viewTabIdentifier
                      withAnimation:(BOOL)shouldAnimate {
  NSLog(@"viewTabIdentifier %@",viewTabIdentifier);

  if ([_currentView isEqualToString:viewTabIdentifier]) {
    return;
  } else {
    _currentView = viewTabIdentifier;
  }
  //Loop through the view array and find out the class to load

  NSDictionary *viewInfoDict = nil;
  for (NSDictionary *dict in _toolbarTabsArray) {
    if ([dict[@"identifier"] isEqualToString:viewTabIdentifier]) {
      viewInfoDict = dict;
      break;
    }
  }
  NSString *class = viewInfoDict[@"class"];
  if(NSClassFromString(class)) {
    _currentViewController = [[NSClassFromString(class) alloc] init];
    NSView *newView = _currentViewController.view;

    NSRect windowRect = self.window.frame;
    NSRect currentViewRect = newView.frame;

    windowRect.origin.y = windowRect.origin.y + (windowRect.size.height - currentViewRect.size.height);
    windowRect.size.height = currentViewRect.size.height;
    windowRect.size.width = currentViewRect.size.width;

    self.window.title = viewInfoDict[@"title"];
    [self.window setContentView:newView];
    [self.window setFrame:windowRect display:YES animate:shouldAnimate];      
  } else {
    NSAssert(false, @"Couldn't load %@", class);
  }
}

- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
  NSLog(@"%s %@",__func__,_toolbarTabsIdentifierArray);
  return _toolbarTabsIdentifierArray;
}

- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
  NSLog(@"%s",__func__);
  return [self toolbarDefaultItemIdentifiers:toolbar];
}

- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar {
  NSLog(@"%s",__func__);
return [self toolbarDefaultItemIdentifiers:toolbar];
}

- (void)toolbarWillAddItem:(NSNotification *)notification {
  NSLog(@"%s",__func__);
}

- (void)toolbarDidRemoveItem:(NSNotification *)notification {
  NSLog(@"%s",__func__);
}

@end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM