How to implement contextual menu for NSCollectionView - macos

In my OSX app I have a collection view which is a subclass of NSCollectionView.
I'm all satisfied with how the things are except the contextual menu, which I can't figure out yet.
So what I want is:
right-click on a collection view item brings up the contextual menu
the options picked from the menu (delete, edit, etc) are applied to the item that the click was performed on.
I know how to do it for NSOutlineView or NSTableView, but not for collection view.
I can't figure out how to get the index of the item clicked.
Does anyone have any ideas on how I can implement this?
Any kind of help is highly appreciated!

Basically, all of our solutions are capable of addressing requirements, but I would like to make a supplement to swift3+, which I think is a complete solution.
/// 扩展NSCollectionView功能,增加常用委托
class ANCollectionView: NSCollectionView {
// 扩展委托方式
weak open var ANDelegate: ANCollectionViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
var menu = super.menu(for: event);
let point = self.convert(event.locationInWindow, from: nil)
let indexPath = self.indexPathForItem(at: point);
if ANDelegate != nil{
menu = ANDelegate?.collectionView(self, menu: menu, at: indexPath);
}
return menu;
}
}
/// 扩展NSCollectionView的委托
protocol ANCollectionViewDelegate : NSObjectProtocol {
func collectionView(_ collectionView:NSCollectionView, menu:NSMenu?, at indexPath: IndexPath?) -> NSMenu?
}
This is what I wrote an extension, and I hope to help everyone.

One approach I've used is to not try to apply the contextual menu actions to the one specific item that was clicked on but to the selected items. And I make the clicked-on item add itself to the selection.
I used a custom view for the collection item view. The custom view class has an outlet, item, to its owning collection view item, which I connect in the NIB. It also overrides -rightMouseDown: to have the item add itself to the selection:
- (void) rightMouseDown:(NSEvent*)event
{
NSCollectionView* parent = self.item.collectionView;
NSUInteger index = NSNotFound;
NSUInteger count = parent.content.count;
for (NSUInteger i = 0; i < count; i++)
{
if ([parent itemAtIndex:i] == self.item)
{
index = i;
break;
}
}
NSMutableIndexSet* selectionIndexes = [[parent.selectionIndexes mutableCopy] autorelease];
if (index != NSNotFound && ![selectionIndexes containsIndex:index])
{
[selectionIndexes addIndex:index];
parent.selectionIndexes = selectionIndexes;
}
return [super rightMouseDown:event];
}
If you prefer, rather than adding the item to the selection, you can check if it's already in the selection. If it is, don't modify the selection. If it's not, replace the selection with just the item (making it the only selected item).
Alternatively, you could set a contextual menu on the item views rather than on the collection view. Then, the menu items could target either the item view or the collection view item.
Lastly, you could subclass NSCollectionView and override -menuForEvent:. You would still call through to super and return the menu it returns, but you could take the opportunity to record the event and/or the item at its location. To determine that, you'd do something like:
- (NSMenu*) menuForEvent:(NSEvent*)event
{
_clickedItemIndex = NSNotFound;
NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
NSUInteger count = self.content.count;
for (NSUInteger i = 0; i < count; i++)
{
NSRect itemFrame = [self frameForItemAtIndex:i];
if (NSMouseInRect(point, itemFrame, self.isFlipped))
{
_clickedItemIndex = i;
break;
}
}
return [super menuForEvent:event];
}

Here's Ken's idea to override menuForEvent: in an NSCollectionView subclass implemented in Swift:
// MARK: - Properties
/**
The index of the item the user clicked.
*/
var clickedItemIndex: Int = NSNotFound
// MARK: - Menu override methods
override func menuForEvent(event: NSEvent) -> NSMenu?
{
self.clickedItemIndex = NSNotFound
let point = self.convertPoint(event.locationInWindow, fromView:nil)
let count = self.content.count
for index in 0 ..< count
{
let itemFrame = self.frameForItemAtIndex(index)
if NSMouseInRect(point, itemFrame, self.flipped)
{
self.clickedItemIndex = index
break
}
}
return super.menuForEvent(event)
}

In Swift 5, you can use
class ClickedCollectionView: NSCollectionView {
var clickedIndex: Int?
override func menu(for event: NSEvent) -> NSMenu? {
clickedIndex = nil
let point = convert(event.locationInWindow, from: nil)
for index in 0..<numberOfItems(inSection: 0) {
let frame = frameForItem(at: index)
if NSMouseInRect(point, frame, isFlipped) {
clickedIndex = index
break
}
}
return super.menu(for: event)
}
}

Thanks for this solution. I wrapped it into a NSCollectionView subclass:
#import <Cocoa/Cocoa.h>
#interface TAClickableCollectionViewItem : NSCollectionViewItem
#property (nonatomic, assign) BOOL isClicked;
#end
#interface TAClickableCollectionView : NSCollectionView <NSMenuDelegate>
#property (nonatomic, readonly) id clickedObject;
#property (nonatomic, readonly) TAClickableCollectionViewItem *clickedItem;
#end
So you can use bindings in Interface Builder to highlight clicked items as well.
#import "TAClickableCollectionView.h"
#implementation TAClickableCollectionViewItem
#end
#implementation TAClickableCollectionView
- (NSMenu*) menuForEvent:(NSEvent*)event
{
NSInteger _clickedItemIndex = NSNotFound;
NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
NSUInteger count = self.content.count;
for (NSUInteger i = 0; i < count; i++)
{
NSRect itemFrame = [self frameForItemAtIndex:i];
if (NSMouseInRect(point, itemFrame, self.isFlipped))
{
_clickedItemIndex = i;
break;
}
}
if(_clickedItemIndex < self.content.count) {
id obj = [self.content objectAtIndex:_clickedItemIndex];
TAClickableCollectionViewItem *item = (TAClickableCollectionViewItem *)[self itemAtIndex:_clickedItemIndex];
if(item != _clickedItem) {
[self willChangeValueForKey:#"clickedObject"];
_clickedItem.isClicked = NO;
_clickedItem = item;
[self didChangeValueForKey:#"clickedObject"];
}
item.isClicked = YES;
if(obj != _clickedObject) {
[self willChangeValueForKey:#"clickedObject"];
_clickedObject = obj;
[self didChangeValueForKey:#"clickedObject"];
}
}
return [super menuForEvent:event];
}
- (void)menuDidClose:(NSMenu *)menu {
_clickedItem.isClicked = NO;
}
#end

Related

UICollectionView Auto Scroll Paging

my project have a UIcollectionView. This Horizontal paging and have four object. I want my collectionView Auto Scroll Paging. which use methods?
As for theory, I just explain the key point. For example, if you want to auto-cycle display 5 images in somewhere, just as ads bar. You can create a UICollectionView with 100 sections, and there're 5 items in every section. After creating UICollectionView, set its original position is equal to the 50th section 0th item(middle of MaxSections
) so that you can scroll to left or right. Don't worry it will ends, because I have reset the position equal to middle of MaxSections when the Timer runs. Never argue with me :" it also will ends when the user keep scrolling by himself" If one find there're only 5 images, will he keep scrolling!? I believe there is few such silly user in this world!
Note: In the following codes,the headerView is UICollectionView,headerNews is data. And I suggest set MaxSections = 100.
Add a NSTimer after custom collectionView(Ensure the UICollectionViewFlowLayout is properly set at first):
- (void)addTimer
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:#selector(nextPage) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.timer = timer;
}
then implement #selector(nextPage):
- (void)nextPage
{
// 1.back to the middle of sections
NSIndexPath *currentIndexPathReset = [self resetIndexPath];
// 2.next position
NSInteger nextItem = currentIndexPathReset.item + 1;
NSInteger nextSection = currentIndexPathReset.section;
if (nextItem == self.headerNews.count) {
nextItem = 0;
nextSection++;
}
NSIndexPath *nextIndexPath = [NSIndexPath indexPathForItem:nextItem inSection:nextSection];
// 3.scroll to next position
[self.headerView scrollToItemAtIndexPath:nextIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
last implement resetIndexPath method:
- (NSIndexPath *)resetIndexPath
{
// currentIndexPath
NSIndexPath *currentIndexPath = [[self.headerView indexPathsForVisibleItems] lastObject];
// back to the middle of sections
NSIndexPath *currentIndexPathReset = [NSIndexPath indexPathForItem:currentIndexPath.item inSection:MaxSections / 2];
[self.headerView scrollToItemAtIndexPath:currentIndexPathReset atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
return currentIndexPathReset;
}
In order to control NSTimer , you have to implement some other methods and UIScrollView's delegate methods:
- (void)removeTimer
{
// stop NSTimer
[self.timer invalidate];
// clear NSTimer
self.timer = nil;
}
// UIScrollView' delegate method
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self removeTimer];
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
[self addTimer];
}
Swift 2.0 version for #Elijah_Lam 's answer:
func addTimer()
{
timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "nextPage", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
}
func nextPage()
{
var ar = cvImg.indexPathsForVisibleItems()
///indexPathsForVisibleItems()'s result is not sorted. Sort it by myself...
ar = ar.sort{if($0.section < $1.section)
{
return true
}
else if($0.section > $1.section)
{
return false
}
///in same section, compare the item
else if($0.item < $1.item)
{
return true
}
return false
}
var currentIndexPathReset = ar[1]
if(currentIndexPathReset.section > MaxSections)
{
currentIndexPathReset = NSIndexPath(forItem:0, inSection:0)
}
cvImg.scrollToItemAtIndexPath(currentIndexPathReset, atScrollPosition: UICollectionViewScrollPosition.Left, animated: true)
}
func removeTimer()
{
// stop NSTimer
timer.invalidate()
}
// UIScrollView' delegate method
func scrollViewWillBeginDragging(scrollView: UIScrollView)
{
removeTimer()
}
func scrollViewDidEndDragging(scrollView: UIScrollView,
willDecelerate decelerate: Bool)
{
addTimer()
}

NSTextView keyDown Event

I want trigger the keyDown Event with every Key action. Therefore I have created a subclass of NSView.
#interface CodrTextView : NSView {
NSTextField *lineNumberSpace;
NSTextView *mainTextView;
}
- (id) initWithTextView:(NSTextView *)textView lineNumberSpace:(NSTextField *)textField;
I already have the method's:
- (id) initWithTextView:(NSTextView *)textView lineNumberSpace:(NSTextField *)textField {
self = [self init];
if (self){
mainTextView = textView;
lineNumberSpace = textField;
}
return self;
}
- (BOOL) acceptsFirstResponder {
return YES;
}
- (BOOL)canBecomeKeyView {
return YES;
}
My plan is to count the lines in the textView and write the numbers in the lineNumberSpace. I know that the methods work, because I have test it already with an IBAction on a button. This is the method:
- (long) getLineCount{
NSString *content = [mainTextView string];
NSUInteger numberOfLines, index, contentLength = [content length];
for (index = 0, numberOfLines = 0; index < contentLength; numberOfLines++){
index = NSMaxRange([content lineRangeForRange:NSMakeRange(index, 0)]);
}
NSLayoutManager *layoutManager = [mainTextView layoutManager];
NSUInteger numberOfGlyphs =[layoutManager numberOfGlyphs];
NSRange lineRange;
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
(void) [layoutManager lineFragmentRectForGlyphAtIndex:index
effectiveRange:&lineRange];
index = NSMaxRange(lineRange);
}
numberOfLines++;
return numberOfLines;
}
- (void)keyDown:(NSEvent *)event {
NSMutableString *lineNumberString = [NSMutableString string];
long numberOfLines = [self getLineCount];
for (int i = 1; i <= numberOfLines; i++){
[lineNumberString appendString:([NSString stringWithFormat:#"%d \n", i])];
}
[lineNumberSpace setStringValue:lineNumberString];
}
This method works surely. The problem is, with a button the number in the lineNumberSpace is changing correctly but with the keyDown Event it don't work. What is my mistake here?
Ahhh, looking at your code I now realize what the problem is.
Your "CodrTextView" object is not subclassed from "NSTextView" (and also, when you instantiate the object programatically or via your XIB or Storyboard, make sure the text view object is a custom class of "CodrTextView" and not just another "NSTextView"), so it's not actually getting the "acceptsFirstResponder" or "canBecomeKeyView" method calls either.
You need to descend "CodrTextView" from "NSTextView" instead of "NSView", OR you need to create another subclassed "NSTextView" object which will receive the "keyDown:" event and then it'll call the code that calculates the string that goes into "lineNumberSpace" of your main view.
Does this make sense to you now?

xcode 4: How to save user changes from more tab bar [duplicate]

I am having problems finding any other information than the docs for how to save the tab order for my UITabBarController, so that the user's customization is saved for next app launch. I have searched online, but have been unable to find any blog posts or articles that goes through the proper code for doing this.
I realize I have to use the delegate methods for the UITabBarController (didEndCustomizingViewControllers:) but I am not sure how I best approach persistance in terms of saving the state of the order the user wants the tabs in.
Can someone post some code, point me in the right direction or perhaps you have a link for something saved? :)
Thanks
As far as you've asked for some sample code I will simply post here how I dealt with the same task in my app.
Quick intro: I was using a NIB file for storing initial UITabBarController state and to differ my tabs one from another I simply defined tag variables for UITabBarItem objects assigned to each UIViewController stuffed in my UITabBarController. To be able to accurately track last selected tab (including the 'More' one) I've implemented following methods for UITabBarControllerDelegate of my UITabBarController and UINavigationControllerDelegate of its moreNavigationController. Here they are:
#pragma mark UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
[[NSUserDefaults standardUserDefaults] setInteger:mainTabBarController.selectedIndex forKey:#"mainTabBarControllerSelectedIndex"];
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
[[NSUserDefaults standardUserDefaults] setInteger:mainTabBarController.selectedIndex forKey:#"mainTabBarControllerSelectedIndex"];
}
#pragma mark UITabBarControllerDelegate
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
[[NSUserDefaults standardUserDefaults] setInteger:tabBarController.selectedIndex forKey:#"mainTabBarControllerSelectedIndex"];
}
And here's the code for saving the tabs order:
#pragma mark UITabBarControllerDelegate
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed {
int count = mainTabBarController.viewControllers.count;
NSMutableArray *savedTabsOrderArray = [[NSMutableArray alloc] initWithCapacity:count];
for (int i = 0; i < count; i ++) {
[savedTabsOrderArray addObject:[NSNumber numberWithInt:[[[mainTabBarController.viewControllers objectAtIndex:i] tabBarItem] tag]]];
}
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithArray:savedTabsOrderArray] forKey:#"tabBarTabsOrder"];
[savedTabsOrderArray release];
}
As you can see I've been storing the order of tabs' indexes in an array in NSUserDefaults.
On app's launch in applicationDidFinishLaunching: method I reordered the UIViewControllers using following code:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
mainTabBarController.delegate = self;
int count = mainTabBarController.viewControllers.count;
NSArray *savedTabsOrderArray = [[userDefaults arrayForKey:#"tabBarTabsOrder"] retain];
if (savedTabsOrderArray.count == count) {
BOOL needsReordering = NO;
NSMutableDictionary *tabsOrderDictionary = [[NSMutableDictionary alloc] initWithCapacity:count];
for (int i = 0; i < count; i ++) {
NSNumber *tag = [[NSNumber alloc] initWithInt:[[[mainTabBarController.viewControllers objectAtIndex:i] tabBarItem] tag]];
[tabsOrderDictionary setObject:[NSNumber numberWithInt:i] forKey:[tag stringValue]];
if (!needsReordering && ![(NSNumber *)[savedTabsOrderArray objectAtIndex:i] isEqualToNumber:tag]) {
needsReordering = YES;
}
}
if (needsReordering) {
NSMutableArray *tabsViewControllers = [[NSMutableArray alloc] initWithCapacity:count];
for (int i = 0; i < count; i ++) {
[tabsViewControllers addObject:[mainTabBarController.viewControllers objectAtIndex:
[(NSNumber *)[tabsOrderDictionary objectForKey:
[(NSNumber *)[savedTabsOrderArray objectAtIndex:i] stringValue]] intValue]]];
}
[tabsOrderDictionary release];
mainTabBarController.viewControllers = [NSArray arrayWithArray:tabsViewControllers];
[tabsViewControllers release];
}
}
[savedTabsOrderArray release];
if ([userDefaults integerForKey:#"mainTabBarControllerSelectedIndex"]) {
if ([userDefaults integerForKey:#"mainTabBarControllerSelectedIndex"] == 2147483647) {
mainTabBarController.selectedViewController = mainTabBarController.moreNavigationController;
}
else {
mainTabBarController.selectedIndex = [userDefaults integerForKey:#"mainTabBarControllerSelectedIndex"];
}
}
mainTabBarController.moreNavigationController.delegate = self;
[window addSubview:mainTabBarController.view];
}
It's quite tricky and may seem strange, but don't forget that my UITabBarController was fully created in a nib file. If you construct it programmatically you may simply do the same but following the saved order.
P.S.: and don't forget to synchronize NSUserDefaults when your app terminates.
- (void)applicationWillTerminate:(UIApplication *)application {
[[NSUserDefaults standardUserDefaults] synchronize];
}
I hope this will help. If something is not clear please do comment and ask.
First I voted up the previous answer, but then I noticed how ridiculously complex it is. It can and should be simplified.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSArray *initialViewControllers = [NSArray arrayWithArray:self.tabBarController.viewControllers];
NSArray *tabBarOrder = [[AppDelegate sharedSettingsService] tabBarOrder];
if (tabBarOrder) {
NSMutableArray *newViewControllers = [NSMutableArray arrayWithCapacity:initialViewControllers.count];
for (NSNumber *tabBarNumber in tabBarOrder) {
NSUInteger tabBarIndex = [tabBarNumber unsignedIntegerValue];
[newViewControllers addObject:[initialViewControllers objectAtIndex:tabBarIndex]];
}
self.tabBarController.viewControllers = newViewControllers;
}
NSInteger tabBarSelectedIndex = [[AppDelegate sharedSettingsService] tabBarSelectedIndex];
if (NSIntegerMax == tabBarSelectedIndex) {
self.tabBarController.selectedViewController = self.tabBarController.moreNavigationController;
} else {
self.tabBarController.selectedIndex = tabBarSelectedIndex;
}
/* Add the tab bar controller's current view as a subview of the window. */
[self.window addSubview:self.tabBarController.view];
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSInteger tabBarSelectedIndex = self.tabBarController.selectedIndex;
[[AppDelegate sharedSettingsService] setTabBarSelectedIndex:tabBarSelectedIndex];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed {
NSUInteger count = tabBarController.viewControllers.count;
NSMutableArray *tabOrderArray = [[NSMutableArray alloc] initWithCapacity:count];
for (UIViewController *viewController in viewControllers) {
NSInteger tag = viewController.tabBarItem.tag;
[tabOrderArray addObject:[NSNumber numberWithInteger:tag]];
}
[[AppDelegate sharedSettingsService] setTabBarOrder:[NSArray arrayWithArray:tabOrderArray]];
[tabOrderArray release];
}
All this happens in AppDelegate. You set UITabBarController's delegate to AppDelegate instance in Interface Builder. sharedSettingsService is what persists the data for me. Basically it can be a NSUserDefaults front-end or anything you like (CoreData for example). So everything is simple, Interface Builder helps here, not makes things more complex.
Perhaps late to the game, but been learning Swift for less than two months at school and sat perhaps more than fifteen hours with this because I couldn't find a decent explanation on the interwebz.
Here is a solution in Swift
Give all your tabItem's a tag, starting at 1. You do this in each separate view. If you got six views, their tabItems will in that case have a unique number each ranging between 1 and 6.
Add UITabBarControllerDelegate to all the ViewControllers' classes so you can use the function explained later in point 5.
class FirstViewController: UIViewController, UITabBarControllerDelegate {
Add the following variable globally (right after the code above, as an example) so you can save variables locally on the phone from any function within the class.
let defaults = NSUserDefaults.standardUserDefaults()
Delegate the tabBarController to the view so the view can update any changes to the tabBarController. Put the following into your viewDidLoad().
tabBarController!.delegate = self
Implement the following code. This one will activate when the user is editing the tab view. What the code does is taking the [ViewControllers]'s tags in the order they are in (after the user changed it) and saves it locally on the phone. The first viewController's tag is saved as an integer in the variable "0", the second tag in a variable called "1", and so on.
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool) {
if (changed) {
print("New tab order:")
for (var i=0; i<viewControllers.count; i++) {
defaults.setInteger(viewControllers[i].tabBarItem.tag, forKey: String(i))
print("\(i): \(viewControllers[i].tabBarItem.title!) (\(viewControllers[i].tabBarItem.tag))")
}
}
}
The prints tell you the new order of the tabs. Nothing you will need, but I think it's nice to see what's happening in the background. All this was only to save the order of the tabs. You will now have to retrieve them when the program is starting.
Switch from your UIViewControl file to AppDelegate.swift.
Finally, write the following in func application(application: UIApplication, didFinishLaunchingWithOptions... that you can find at the top of AppDelegate.swift.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Let you read and write to local variables on your phone
let defaults = NSUserDefaults.standardUserDefaults()
// Getting access to your tabBarController
let tabBar: UITabBarController = self.window?.rootViewController as! UITabBarController
var junkViewControllers = [UIViewController]()
// returns 0 if not set, hence having the tabItem's tags starting at 1.
var tagNumber : Int = defaults.integerForKey("0")
if (tagNumber != 0) {
for (var i=0; i<tabBar.viewControllers?.count; i++) {
// the tags are between 1-6 but the order of the
// viewControllers in the array are between 0-5
// hence the "-1" below.
tagNumber = defaults.integerForKey( String(i) ) - 1
junkViewControllers.append(tabBar.viewControllers![tagNumber])
}
tabBar.viewControllers = junkViewControllers
}
}
What is good to know is that all views that a tabBarController contains is stored as an array in tabBarController.viewControllers.
This code basically creates an array called junkViewControllers. The for-loop then adds the existing UIViewControllers from the program in the order from the previous stored variables, based on the UIViewControllers' tags. When all this is done, the tabBarController shortened to tabBar is overwritten with the array junkViewController.
This is what did the trick for me, building on the answers from Rickard & Jesper. All of the code goes into the main TabBarController.
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
let tabOrderKey = "customTabBarOrder"
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
loadCustomTabOrder()
}
func loadCustomTabOrder() {
let defaults = NSUserDefaults.standardUserDefaults()
let standardOrderChanged = defaults.boolForKey(tabOrderKey)
if standardOrderChanged {
print("Standard Order has changed")
var VCArray = [UIViewController]()
var tagNumber = 0
let tabBar = self as UITabBarController
if let countVC = tabBar.viewControllers?.count {
print("\(countVC) VCs in total")
for var x = 0; x < countVC; x++ {
tagNumber = defaults.integerForKey("tabPosition\(x)")
for VC in tabBar.viewControllers! {
if tagNumber == VC.tabBarItem.tag {
VCArray.append(VC)
print("Position \(x): \(VCArray[x].tabBarItem.title!) VC (tag \(tagNumber))")
}
}
}
}
tabBar.viewControllers = VCArray
}
}
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool) {
print("Change func called")
if changed {
print("Order has changed")
let defaults = NSUserDefaults.standardUserDefaults()
for var x = 0; x < viewControllers.count; x++ {
defaults.setInteger(viewControllers[x].tabBarItem.tag, forKey: "tabPosition\(x)")
print("\(viewControllers[x].tabBarItem.title!) VC (with tag: \(viewControllers[x].tabBarItem.tag)) is now in position \(x)")
}
defaults.setBool(true, forKey: tabOrderKey)
} else {
print("Nothing has changed")
}
}
}
I will explain how to do this programmatically. NOTE: This is using ARC, so you may have to insert retain/release calls as needed.
You use the tag property of the UITabBarItem for sorting. For every UIViewController that you are adding to the UITabBarController, make sure that each has a unique tag.
- (id)init
{
self = [super init];
if (self) {
self.tabBarItem.tag = 0;
self.tabBarItem.image = <image>;
self.tabBarItem.title = <title>;
}
return self;
}
Presumably you would just use their default sorting order for their tags, so whatever you have as your original first view controller would be 0, followed by 1, 2, 3, etc.
Set up your UIViewControllers in the AppDelegate's didFinishLaunchingWithOptions as you normally would, making sure that you are instantiating them in their "default order". As you do so, add them to an instance of a NSMutableArray.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.delegate = self;
NSMutableArray *unsortedControllers = [NSMutableArray array];
UIViewController *viewOne = [[UIViewController alloc] init];
[unsortedControllers addObject:viewOne];
UIViewController *viewTwo = [[UIViewController alloc] init];
[unsortedControllers addObject:viewTwo];
...
After they are all instantiated and added to the array, you will check to see if the user has customized their order by querying NSUserDefaults. In the defaults, you will store an array of the user's customized tab bar order. This will be an array of NSNumbers (how this is created is explain in the last code snippet). Use these to create a new "sorted" array of view controllers and pass that to the tab bar controller. If they haven't customized the order, the default will return nil and you can simply used the unsorted array.
...
NSArray *tabBarOrder = [[NSUserDefaults standardUserDefaults] arrayForKey:#"tabBarOrder"];
if (tabBarOrder)
{
NSMutableArray *sortedControllers = [NSMutableArray array];
for (NSNumber *sortNumber in tabBarOrder)
{
[sortedControllers addObject:[unsortedControllers objectAtIndex:[sortNumber intValue]]];
}
self.tabBarController.viewControllers = sortedControllers;
} else {
self.tabBarController.viewControllers = unsortedControllers;
}
[self.window setRootViewController:self.tabBarController];
[self.window makeKeyAndVisible];
return YES;
}
To create to customized sort order, use the UITabBarController's delegate method:
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed
{
NSMutableArray *tabOrderArray = [[NSMutableArray alloc] init];
for (UIViewController *vc in self.tabBarController.viewControllers)
{
[tabOrderArray addObject:[NSNumber numberWithInt:[[vc tabBarItem] tag]]];
}
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithArray:tabOrderArray] forKey:#"tabBarOrder"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Simplified Rickard Elimää answer even further. "Swift" solution to saving and loading Customized ViewControllers using the delegate function of tabBarController CustomizingViewControllers.
This is how I did it.
class TabBarController: UITabBarController, UITabBarControllerDelegate {
let kOrder = "customOrder"
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
loadCustomizedViews()
}
func loadCustomizedViews(){
let defaults = NSUserDefaults.standardUserDefaults()
// returns 0 if not set, hence having the tabItem's tags starting at 1.
let changed : Bool = defaults.boolForKey(kOrder)
if changed {
var customViewControllers = [UIViewController]()
var tagNumber: Int = 0
for (var i=0; i<self.viewControllers?.count; i++) {
// the tags are between 0-6 and the
// viewControllers in the array are between 0-6
// so we swap them to match the custom order
tagNumber = defaults.integerForKey( String(i) )
//print("TabBar re arrange i = \(i), tagNumber = \(tagNumber), viewControllers.count = \(self.viewControllers?.count) ")
customViewControllers.append(self.viewControllers![tagNumber])
}
self.viewControllers = customViewControllers
}
}
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool){
if (changed) {
let defaults = NSUserDefaults.standardUserDefaults()
//print("New tab order:")
for (var i=0; i<viewControllers.count; i++) {
defaults.setInteger(viewControllers[i].tabBarItem.tag, forKey: String(i))
//print("\(i): \(viewControllers[i].tabBarItem.title!) (\(viewControllers[i].tabBarItem.tag))")
}
defaults.setBool(changed, forKey: kOrder)
}
}
}
Updated Answer for Swift 2.0
`
let tabBarOrderKey = "tabBarOrderKey"
extension TabBarButtonsController: UITabBarControllerDelegate {
// Saves new tab bar custom order
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool) {
var orderedTagItems = [Int]()
if changed {
for viewController in viewControllers {
let tag = viewController.tabBarItem.tag
orderedTagItems.append(tag)
}
NSUserDefaults.standardUserDefaults().setObject(orderedTagItems, forKey: tabBarOrderKey)
}
}
// set up tag to compare with when pulling from defaults and for saving initial tab bar change
func setUpTabBarItemTags() {
var tag = 0
if let viewControllers = viewControllers {
for view in viewControllers {
view.tabBarItem.tag = tag
tag += 1
}
}
}
// Get Saved Tab Bar Order from defaults
func getSavedTabBarItemsOrder() {
var newViewControllerOrder = [UIViewController]()
if let initialViewControllers = viewControllers {
if let tabBarOrder = NSUserDefaults.standardUserDefaults().objectForKey(tabBarOrderKey) as? [Int] {
for tag in tabBarOrder {
newViewControllerOrder.append(initialViewControllers[tag])
}
setViewControllers(newViewControllerOrder, animated: false)
}
}
}
}
`
Remember to set the delegate and call these methods in the view did load
I'd like to share the code I have been working on for nearly 3 days now trying all sorts of combinations with many errors and failure - the general life of coding! ha ha. Anyway the code below works with Swift 5.
It's the extraction of How to: Save order of tabs when customizing tabs in UITabBarController , Sam's Code, but with the for loop update for Swift 5.
I have got this working great, all in the TabBarViewController.swift file.
So if you just paste this into your Swift file, make sure your Tab Bar Items have a tag number from the Attributes Inspector and your good to go!
Thanks again to Sam for the Code in the first place.
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
let tabOrderKey = "customTabBarOrder"
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
loadCustomTabOrder()
}
func loadCustomTabOrder() {
let defaults = UserDefaults.standard
let standardOrderChanged = defaults.bool(forKey: tabOrderKey)
if standardOrderChanged {
print("Standard Order has changed")
var VCArray = [UIViewController]()
var tagNumber = 0
let tabBar = self as UITabBarController
if let countVC = tabBar.viewControllers?.count {
print("\(countVC) VCs in total")
for x in 0..<countVC {
tagNumber = defaults.integer(forKey: "tabPosition\(x)")
for VC in tabBar.viewControllers! {
if tagNumber == VC.tabBarItem.tag {
VCArray.append(VC)
print("Position \(x): \(VCArray[x].tabBarItem.title!) VC (tag \(tagNumber))")
}
}
}
}
tabBar.viewControllers = VCArray
}
}
func tabBarController(_ tabBarController: UITabBarController, didEndCustomizing viewControllers: [UIViewController], changed: Bool) {
print("Change func called")
if changed {
print("Order has changed")
let defaults = UserDefaults.standard
for x in 0..<(viewControllers.count) {
defaults.set(viewControllers[x].tabBarItem.tag, forKey: "tabPosition\(x)")
print("\(viewControllers[x].tabBarItem.title!) VC (with tag: \(viewControllers[x].tabBarItem.tag)) is now in position \(x)")
}
defaults.set(true, forKey: tabOrderKey)
} else {
print("Nothing has changed")
}
}
}

Changing Dynamic UIView subviews with single Tap in UIGestureRecognizer Method

UPDATE:Solved issue, see below!
The situation: I have several dynamically loaded UIViews on a UIScrollView in a nib.
Expected behavior: I want to single TAP any one of the UIViews and it will change background color to indicate it was tapped. If it was already tapped it should then change back to its initial look.
I have set up a UITapGesture recognizer on each of the UIViews and here is the selector method where I am doing the behavior. I have confused myself. I apologize for the sketchy logic here (it is a ruff draft). I have set up a isTapped BOOL set to "NO" initially in the init in the file.
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
isTapped = !isTapped;
UIView *v = gestureRecognizer.view;
NSInteger currentIndex = [studentCellArray indexOfObjectIdenticalTo:v];
if (oldIndex != currentIndex) {
isTapped = YES;
}
//check to see if obj in array then switch on/off
if ([tappedViewArray indexOfObjectIdenticalTo:v] != NSNotFound) {
oldIndex = currentIndex;
}
if (currentIndex == v.tag) {
isTapped = !isTapped;
}
if (isTapped) {
[tappedViewArray addObject:v];
[super formatViewTouchedNiceGrey:v];
}else{
[tappedViewArray removeObject:v];
[super formatViewBorder:v];
}
if (currentIndex == oldIndex) {
isTapped = !isTapped;
}
}
Actual Behavior: After Tapping the First UIView it selects fine and changes, a second tap will change it back, however after successive taps it stays selected. Also, if you select a UIView and go to another view - you have to double tap the successive views.
I would like to just tap once to turn off or on any of the UIViews in the scrollview.
UPDATE: Well, after some Hand writing and other vain attempts at trying to focus on this issue ---- I have solved it this way and it BEHAVES properly!
here is my solution:
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
isTapped = !isTapped;
UIView *v = gestureRecognizer.view;
NSInteger currentIndex = [studentCellArray indexOfObjectIdenticalTo:v];
if (((isTapped && currentIndex != oldIndex) || (!isTapped && currentIndex != oldIndex)) && [tappedViewArray indexOfObject:v] == NSNotFound) {
oldIndex = currentIndex;
[tappedViewArray addObject:v];
[super formatCornerRadiusWithGreyBackgrnd:v];
} else {
[super formatViewBorder:v];
[tappedViewArray removeObject:v];
}
}
So I hope this helps someone with this issue.
The key was to check for the isTapped and indexes being not equal AND the view object NOT being in the array I was assembling to indicate items touched/Tapped....

How to expand and collapse NSSplitView subviews with animation?

Is it possible to animate the collapsing and expanding of NSSplitView subviews? (I am aware of the availability of alternative classes, but would prefer using NSSplitView over having animations.)
I am using the method - (void)setPosition:(CGFloat)position ofDividerAtIndex:(NSInteger)dividerIndex to perform the collapsing and expanding.
After some more trying, I found the answer: yes, it's possible.
The code below shows how it can be done. The splitView is the NSSplitView which is vertically divided into mainView (on the left) and the inspectorView (on the right). The inspectorView is the one that collapses.
- (IBAction)toggleInspector:(id)sender {
if ([self.splitView isSubviewCollapsed:self.inspectorView]) {
// NSSplitView hides the collapsed subview
self.inspectorView.hidden = NO;
NSMutableDictionary *expandMainAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[expandMainAnimationDict setObject:self.mainView forKey:NSViewAnimationTargetKey];
NSRect newMainFrame = self.mainView.frame;
newMainFrame.size.width = self.splitView.frame.size.width-lastInspectorWidth;
[expandMainAnimationDict setObject:[NSValue valueWithRect:newMainFrame] forKey:NSViewAnimationEndFrameKey];
NSMutableDictionary *expandInspectorAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[expandInspectorAnimationDict setObject:self.inspectorView forKey:NSViewAnimationTargetKey];
NSRect newInspectorFrame = self.inspectorView.frame;
newInspectorFrame.size.width = lastInspectorWidth;
newInspectorFrame.origin.x = self.splitView.frame.size.width-lastInspectorWidth;
[expandInspectorAnimationDict setObject:[NSValue valueWithRect:newInspectorFrame] forKey:NSViewAnimationEndFrameKey];
NSViewAnimation *expandAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:expandMainAnimationDict, expandInspectorAnimationDict, nil]];
[expandAnimation setDuration:0.25f];
[expandAnimation startAnimation];
} else {
// Store last width so we can jump back
lastInspectorWidth = self.inspectorView.frame.size.width;
NSMutableDictionary *collapseMainAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[collapseMainAnimationDict setObject:self.mainView forKey:NSViewAnimationTargetKey];
NSRect newMainFrame = self.mainView.frame;
newMainFrame.size.width = self.splitView.frame.size.width;
[collapseMainAnimationDict setObject:[NSValue valueWithRect:newMainFrame] forKey:NSViewAnimationEndFrameKey];
NSMutableDictionary *collapseInspectorAnimationDict = [NSMutableDictionary dictionaryWithCapacity:2];
[collapseInspectorAnimationDict setObject:self.inspectorView forKey:NSViewAnimationTargetKey];
NSRect newInspectorFrame = self.inspectorView.frame;
newInspectorFrame.size.width = 0.0f;
newInspectorFrame.origin.x = self.splitView.frame.size.width;
[collapseInspectorAnimationDict setObject:[NSValue valueWithRect:newInspectorFrame] forKey:NSViewAnimationEndFrameKey];
NSViewAnimation *collapseAnimation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:collapseMainAnimationDict, collapseInspectorAnimationDict, nil]];
[collapseAnimation setDuration:0.25f];
[collapseAnimation startAnimation];
}
}
- (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview {
BOOL result = NO;
if (splitView == self.splitView && subview == self.inspectorView) {
result = YES;
}
return result;
}
- (BOOL)splitView:(NSSplitView *)splitView shouldCollapseSubview:(NSView *)subview forDoubleClickOnDividerAtIndex:(NSInteger)dividerIndex {
BOOL result = NO;
if (splitView == self.splitView && subview == self.inspectorView) {
result = YES;
}
return result;
}
Here's a simpler method:
http://www.cocoabuilder.com/archive/cocoa/304317-animating-nssplitpane-position.html
(Link above dead, new link here.)
Which says create a category on NSSplitView as follows, and then animate with
[[splitView animator] setSplitPosition:pos];
Works for me.
Category:
#implementation NSSplitView (Animation)
+ (id)defaultAnimationForKey:(NSString *)key
{
if ([key isEqualToString:#"splitPosition"])
{
CAAnimation* anim = [CABasicAnimation animation];
anim.duration = 0.3;
return anim;
}
else
{
return [super defaultAnimationForKey:key];
}
}
- (void)setSplitPosition:(CGFloat)position
{
[self setPosition:position ofDividerAtIndex:0];
}
- (CGFloat)splitPosition
{
NSRect frame = [[[self subviews] objectAtIndex:0] frame];
if([self isVertical])
return NSMaxX(frame);
else
return NSMaxY(frame);
}
#end
For some reason none of the methods of animating frames worked for my scrollview.
I ended up creating a custom animation to animate the divider position. This ended up taking less time than I expected. If anyone is interested, here is my solution:
Animation .h:
#interface MySplitViewAnimation : NSAnimation
#property (nonatomic, strong) NSSplitView* splitView;
#property (nonatomic) NSInteger dividerIndex;
#property (nonatomic) float startPosition;
#property (nonatomic) float endPosition;
#property (nonatomic, strong) void (^completionBlock)();
- (instancetype)initWithSplitView:(NSSplitView*)splitView
dividerAtIndex:(NSInteger)dividerIndex
from:(float)startPosition
to:(float)endPosition
completionBlock:(void (^)())completionBlock;
#end
Animation .m
#implementation MySplitViewAnimation
- (instancetype)initWithSplitView:(NSSplitView*)splitView
dividerAtIndex:(NSInteger)dividerIndex
from:(float)startPosition
to:(float)endPosition
completionBlock:(void (^)())completionBlock;
{
if (self = [super init]) {
self.splitView = splitView;
self.dividerIndex = dividerIndex;
self.startPosition = startPosition;
self.endPosition = endPosition;
self.completionBlock = completionBlock;
[self setDuration:0.333333];
[self setAnimationBlockingMode:NSAnimationNonblocking];
[self setAnimationCurve:NSAnimationEaseIn];
[self setFrameRate:30.0];
}
return self;
}
- (void)setCurrentProgress:(NSAnimationProgress)progress
{
[super setCurrentProgress:progress];
float newPosition = self.startPosition + ((self.endPosition - self.startPosition) * progress);
[self.splitView setPosition:newPosition
ofDividerAtIndex:self.dividerIndex];
if (progress == 1.0) {
self.completionBlock();
}
}
#end
I'm using it like this - I have a 3 pane splitter view, and am moving the right pane in/out by a fixed amount (235).
- (IBAction)togglePropertiesPane:(id)sender
{
if (self.rightPane.isHidden) {
self.rightPane.hidden = NO;
[[[MySplitViewAnimation alloc] initWithSplitView:_splitView
dividerAtIndex:1
from:_splitView.frame.size.width
to:_splitView.frame.size.width - 235
completionBlock:^{
;
}] startAnimation];
}
else {
[[[MySplitViewAnimation alloc] initWithSplitView:_splitView
dividerAtIndex:1
from:_splitView.frame.size.width - 235
to:_splitView.frame.size.width
completionBlock:^{
self.rightPane.hidden = YES;
}] startAnimation];
}
}
There are a bunch of answers for this. In 2019, the best way to do this is to establish constraints on your SplitView panes, then animate the constraints.
Suppose I have a SplitView with three panes: leftPane, middlePane, rightPane. I want to not just collapse the two panes on the side, I want to also want to dynamically resize the widths of various panes when certain views come in or go out.
In IB, I set up a WIDTH constraint for each of the three panes. leftPane and rightPane have widths set to 250 with a priority of 1000 (required).
In code, it looks like this:
#class MyController: NSViewController
{
#IBOutlet var splitView: NSSplitView!
#IBOutlet var leftPane: NSView!
#IBOutlet var middlePane: NSView!
#IBOutlet var rightPane: NSView!
#IBOutlet var leftWidthConstraint: NSLayoutConstraint!
#IBOutlet var middleWidthConstraint: NSLayoutConstraint!
#IBOutlet var rightWidthConstraint: NSLayoutConstraint!
override func awakeFromNib() {
// We use these in our animation, but want them off normally so the panes
// can be resized as normal via user drags, window changes, etc.
leftWidthConstraint.isActive = false
middleWidthConstraint.isActive = false
rightWidthConstraint.isActive = false
}
func collapseRightPane()
{
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
context.duration = 0.15
rightWidthConstraint.constant = 0
rightWidthConstraint.isActive = true
// Critical! Call this in the animation block or you don't get animated changes:
splitView.layoutSubtreeIfNeeded()
}) { [unowned self] in
// We need to tell the splitView to re-layout itself before we can
// remove the constraint, or it jumps back to how it was before animating.
// This process tells the layout engine to recalculate and update
// the frames of everything based on current constraints:
self.splitView.needsLayout = true
self.splitView.needsUpdateConstraints = true
self.splitView.needsDisplay = true
self.splitView.layoutSubtreeIfNeeded()
self.splitView.displayIfNeeded()
// Now, disable the width constraint so we can resize the splitView
// via mouse, etc:
self.middleWidthConstraint.isActive = false
}
}
}
extension MyController: NSSplitViewDelegate
{
final func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool
{
// Allow collapsing. You might set an iVar that you can control
// if you don't want the user to be able to drag-collapse. Set the
// ivar to false usually, but set it to TRUE in the animation block
// block, before changing the constraints, then back to false in
// in the animation completion handler.
return true
}
final func splitView(_ splitView: NSSplitView, shouldHideDividerAt dividerIndex: Int) -> Bool {
// Definitely do this. Nobody wants a crappy divider hanging out
// on the side of a collapsed pane.
return true
}
}
You can get more complex in this animation block. For example, you could decide that you want to collapse the right pane, but also enlarge the middle one to 500px at the same time.
The advantage to this approach over the others listed here is that it will automatically handle cases where the window's frame is not currently large enough to accommodate "expanding" a collapsed pane. Plus, you can use this to change the panes' sizes in ANY way, not just expanding and collapsing them. You can also have all those changes happen at once, in a smooth, combined animation.
Notes:
Obviously the views that make up leftPane, middlePane, and rightPane never change. Those are "containers" to which you add/remove other views as needed. If you remove the pane views from the SplitView, you'll destroy the constraints you set up in IB.
When using AutoLayout, if you find yourself setting frames manually, you're fighting the system. You set constraints; the autolayout engine sets frames.
The -setPosition:ofDividerAtIndex: approach does not work well when the splitView isn't big enough to set the divider where you want it to be. For example, if you want to UN-collapse a right-hand pane and give it 500 width, but your entire window is currently just 300 wide. This also gets messy if you need to resize multiple panes at once.
You can build on this approach to do more. For example, maybe you want to set minimum and maximum widths for various panes in the splitView. Do that with constraints, then change the constants of the min and max width constraint as needed (perhaps when different views come into each pane, etc).
CRITICAL NOTE:
This approach will fail if any subview in one of the panes has a width or minimumWidth constraint that has a priority of 1000. You'll get a "can't satisfy constraints" notice in the log. You'll need to make sure your subviews (and their child views, all the way down the hierarchy) don't have a width constraint set at 1000 priority. Use 999 or less for such constraints so that the splitView can always override them to collapse the view.
Solution for macOS 10.11.
Main points:
NSSplitViewItem.minimumThickness depends of NSSplitViewItem .viewController.view width/height, if not set explicitly.
NSSplitViewItem .viewController.view width/height depends of explicitly added constraints.
NSSplitViewItem (i.e. arranged subview of NSSplitView) can be fully collapsed, if it can reach Zero dimension (width or height).
So, we just need to deactivate appropriate constrains before animation and allow view to reach Zero dimension. After animation we just need to activate needed constraints.
class SplitViewAnimationsController: ViewController {
private lazy var toolbarView = StackView().autolayoutView()
private lazy var revealLeftViewButton = Button(title: "Left").autolayoutView()
private lazy var changeSplitOrientationButton = Button(title: "Swap").autolayoutView()
private lazy var revealRightViewButton = Button(title: "Right").autolayoutView()
private lazy var splitViewController = SplitViewController()
private lazy var viewControllerLeft = ContentViewController()
private lazy var viewControllerRight = ContentViewController()
private lazy var splitViewItemLeft = NSSplitViewItem(viewController: viewControllerLeft)
private lazy var splitViewItemRight = NSSplitViewItem(viewController: viewControllerRight)
private lazy var viewLeftWidth = viewControllerLeft.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
private lazy var viewRightWidth = viewControllerRight.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
private lazy var viewLeftHeight = viewControllerLeft.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 40)
private lazy var viewRightHeight = viewControllerRight.view.heightAnchor.constraint(greaterThanOrEqualToConstant: 40)
private lazy var equalHeight = viewControllerLeft.view.heightAnchor.constraint(equalTo: viewControllerRight.view.heightAnchor, multiplier: 1)
private lazy var equalWidth = viewControllerLeft.view.widthAnchor.constraint(equalTo: viewControllerRight.view.widthAnchor, multiplier: 1)
override func loadView() {
super.loadView()
splitViewController.addSplitViewItem(splitViewItemLeft)
splitViewController.addSplitViewItem(splitViewItemRight)
contentView.addSubviews(toolbarView, splitViewController.view)
addChildViewController(splitViewController)
toolbarView.addArrangedSubviews(revealLeftViewButton, changeSplitOrientationButton, revealRightViewButton)
}
override func viewDidAppear() {
super.viewDidAppear()
splitViewController.contentView.setPosition(contentView.bounds.width * 0.5, ofDividerAt: 0)
}
override func setupDefaults() {
setIsVertical(true)
}
override func setupHandlers() {
revealLeftViewButton.setHandler { [weak self] in guard let this = self else { return }
self?.revealOrCollapse(this.splitViewItemLeft)
}
revealRightViewButton.setHandler { [weak self] in guard let this = self else { return }
self?.revealOrCollapse(this.splitViewItemRight)
}
changeSplitOrientationButton.setHandler { [weak self] in guard let this = self else { return }
self?.setIsVertical(!this.splitViewController.contentView.isVertical)
}
}
override func setupUI() {
splitViewController.view.translatesAutoresizingMaskIntoConstraints = false
splitViewController.contentView.dividerStyle = .thin
splitViewController.contentView.setDividerThickness(2)
splitViewController.contentView.setDividerColor(.green)
viewControllerLeft.contentView.backgroundColor = .red
viewControllerRight.contentView.backgroundColor = .blue
viewControllerLeft.contentView.wantsLayer = true
viewControllerRight.contentView.wantsLayer = true
splitViewItemLeft.canCollapse = true
splitViewItemRight.canCollapse = true
toolbarView.distribution = .equalSpacing
}
override func setupLayout() {
var constraints: [NSLayoutConstraint] = []
constraints += LayoutConstraint.Pin.InSuperView.horizontally(toolbarView, splitViewController.view)
constraints += [
splitViewController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
toolbarView.topAnchor.constraint(equalTo: splitViewController.view.bottomAnchor),
toolbarView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
]
constraints += [viewLeftWidth, viewLeftHeight, viewRightWidth, viewRightHeight]
constraints += [toolbarView.heightAnchor.constraint(equalToConstant: 48)]
NSLayoutConstraint.activate(constraints)
}
}
extension SplitViewAnimationsController {
private enum AnimationType: Int {
case noAnimation, `default`, rightDone
}
private func setIsVertical(_ isVertical: Bool) {
splitViewController.contentView.isVertical = isVertical
equalHeight.isActive = isVertical
equalWidth.isActive = !isVertical
}
private func revealOrCollapse(_ item: NSSplitViewItem) {
let constraintToDeactivate: NSLayoutConstraint
if splitViewController.splitView.isVertical {
constraintToDeactivate = item.viewController == viewControllerLeft ? viewLeftWidth : viewRightWidth
} else {
constraintToDeactivate = item.viewController == viewControllerLeft ? viewLeftHeight : viewRightHeight
}
let animationType: AnimationType = .rightDone
switch animationType {
case .noAnimation:
item.isCollapsed = !item.isCollapsed
case .default:
item.animator().isCollapsed = !item.isCollapsed
case .rightDone:
let isCollapsedAnimation = CABasicAnimation()
let duration: TimeInterval = 3 // 0.15
isCollapsedAnimation.duration = duration
item.animations = [NSAnimatablePropertyKey("collapsed"): isCollapsedAnimation]
constraintToDeactivate.isActive = false
setActionsEnabled(false)
NSAnimationContext.runImplicitAnimations(duration: duration, animations: {
item.animator().isCollapsed = !item.isCollapsed
}, completion: {
constraintToDeactivate.isActive = true
self.setActionsEnabled(true)
})
}
}
private func setActionsEnabled(_ isEnabled: Bool) {
revealLeftViewButton.isEnabled = isEnabled
revealRightViewButton.isEnabled = isEnabled
changeSplitOrientationButton.isEnabled = isEnabled
}
}
class ContentViewController: ViewController {
override func viewDidLayout() {
super.viewDidLayout()
print("frame: \(view.frame)")
}
}

Resources