Change the width of Master in UISplitViewController - ios

The iPad programming guide says that the splitView's left pane is fixed to 320 points. But 320 pixels for my master view controller is too much. I would like to reduce it and give more space to detail view controller. Is it possible by anyway?
Link to the document which speaks about fixed width.

If you subclass UISplitViewController, you can implement -viewDidLayoutSubviews and adjust the width there. This is clean, no hacks or private APIs, and works even with rotation.
- (void)viewDidLayoutSubviews
{
const CGFloat kMasterViewWidth = 240.0;
UIViewController *masterViewController = [self.viewControllers objectAtIndex:0];
UIViewController *detailViewController = [self.viewControllers objectAtIndex:1];
if (detailViewController.view.frame.origin.x > 0.0) {
// Adjust the width of the master view
CGRect masterViewFrame = masterViewController.view.frame;
CGFloat deltaX = masterViewFrame.size.width - kMasterViewWidth;
masterViewFrame.size.width -= deltaX;
masterViewController.view.frame = masterViewFrame;
// Adjust the width of the detail view
CGRect detailViewFrame = detailViewController.view.frame;
detailViewFrame.origin.x -= deltaX;
detailViewFrame.size.width += deltaX;
detailViewController.view.frame = detailViewFrame;
[masterViewController.view setNeedsLayout];
[detailViewController.view setNeedsLayout];
}
}

In IOS 8.0 you can easily do this by doing the following:
1. In your MasterSplitViewController.h add
#property(nonatomic, assign) CGFloat maximumPrimaryColumnWidth NS_AVAILABLE_IOS(8_0);
2. In your MasterSplitViewController.m viewDidLoad method add
self.maximumPrimaryColumnWidth = 100;
self.splitViewController.maximumPrimaryColumnWidth = self.maximumPrimaryColumnWidth;
This is a really good, simple and easy feature of IOS 8.

this code is work for me
[splitViewController setValue:[NSNumber numberWithFloat:200.0] forKey:#"_masterColumnWidth"];

No.
There are two private properties
#property(access,nonatomic) CGFloat masterColumnWidth;
#property(access,nonatomic) CGFloat leftColumnWidth; // both are the same!
but being private mean they can't be used for AppStore apps.

iOS 8 introduced a new property:
// An animatable property that can be used to adjust the maximum absolute width of the primary view controller in the split view controller.
#property(nonatomic, assign) CGFloat maximumPrimaryColumnWidth NS_AVAILABLE_IOS(8_0); // default: UISplitViewControllerAutomaticDimension
Use this property to adjust your master viewcontroller to your desired width.

Here is how I did this in iOS8 with Swift.
class MainSplitViewController: UISplitViewController {
override func viewDidLoad() {
self.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
self.maximumPrimaryColumnWidth = 100 // specify your width here
}
}
If you need to change the width dynamically from within your master/detail view in the split view, then do something like this:
var splitViewController = self.splitViewController as MainSplitViewController
splitViewController.maximumPrimaryColumnWidth = 400

The storyboard way would be this one, mentioned by #Tim:
Furthermore, if you want the Master view to always take up a certain percentage of the screen then you can use the Key Path = "preferredPrimaryColumnWidthFraction" instead and set the value to 0.2 (for 20% screen size).
Please note that the "maximumPrimaryColumnWidth" is set to 320, so if you try the screen percent value of 0.5 (50%) it won't go above 320. You can add a key path for maximumPrimaryColumnWidth if you need to override this.

None of the answers worked for me on iOS7, so I did some of my own research and created a working solution. This will involve subclassing UISplitViewController for the full functionality.
I will present the answer as if we just created a new project for iPad with all device orientations and have set the custom UISplitViewController as the main view controller.
Create your custom UISplitViewController. In this example mine is called MySplitViewController. All code will be based in MySplitViewController.m.
We're going to need to access a method from the UISplitViewControllerDelegate so add that and set the delegate. We'll also setup a delegate forwarder incase you need to call the delegate methods from another class.
#interface MySplitViewController () <UISplitViewControllerDelegate>
#property (nonatomic, weak) id<UISplitViewControllerDelegate> realDelegate;
#end
#implementation MySplitViewController
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
self.delegate = self;
}
return self;
}
- (void)setDelegate:(id<UISplitViewControllerDelegate>)delegate {
[super setDelegate:nil];
self.realDelegate = (delegate != self) ? delegate : nil;
[super setDelegate:delegate ? self : nil];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
id delegate = self.realDelegate;
return [super respondsToSelector:aSelector] || [delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
id delegate = self.realDelegate;
return [delegate respondsToSelector:aSelector] ? delegate : [super forwardingTargetForSelector:aSelector];
}
Setup the master and detail view controllers.
- (void)viewDidLoad {
[super viewDidLoad];
UIViewController* masterViewController = [[UIViewController alloc] init];
masterViewController.view.backgroundColor = [UIColor yellowColor];
UIViewController* detailViewController = [[UIViewController alloc] init];
detailViewController.view.backgroundColor = [UIColor cyanColor];
self.viewControllers = #[masterViewController, detailViewController];
}
Lets add our desired width to a method for easy reference.
- (CGFloat)desiredWidth {
return 200.0f;
}
We'll manipulate the master view controller before presenting it.
- (void)splitViewController:(UISplitViewController *)svc popoverController:(UIPopoverController *)pc willPresentViewController:(UIViewController *)aViewController {
id realDelegate = self.realDelegate;
if ([realDelegate respondsToSelector:#selector(splitViewController:popoverController:willPresentViewController:)]) {
[realDelegate splitViewController:svc popoverController:pc willPresentViewController:aViewController];
}
CGRect rect = aViewController.view.frame;
rect.size.width = [self desiredWidth];
aViewController.view.frame = rect;
aViewController.view.superview.clipsToBounds = NO;
}
However, now we're left with a display like this.
So were going to override a private method. Yes a private method, it will still be acceptable in the App Store since its not an underscore private method.
- (CGFloat)leftColumnWidth {
return [self desiredWidth];
}
This deals with portrait mode. So a similar thing for -splitViewController:willShowViewController:invalidatingBarButtonItem: and you should be set for landscape.
However none of this will be needed in iOS8. You'll be able to simply call a min and max width property!

use the following code before assigning to the rootviewcontroller. It works for me with ios7
[self.splitViewController setValue:[NSNumber numberWithFloat:256.0] forKey:#"_masterColumnWidth"];
self.window.rootViewController = self.splitViewController;

Since no one mentioned that this can be done from IB, I want to add this answer. Apparently, you can set "User Defined Runtime Attributes" for the UISplitViewContorller with following details:
Key Path:masterColumnWidth
Type: Number
Value: 250

In my case, I had to set both maximum and minimum to make this work
mySplitViewController.preferredDisplayMode = .allVisible;
mySplitViewController.maximumPrimaryColumnWidth = UIScreen.main.bounds.width/2;
mySplitViewController.minimumPrimaryColumnWidth = UIScreen.main.bounds.width/2;

You can use GSSplitViewController. This one will work on iOS 7 and 8
splitView = [[GSSplitViewController alloc] init];
splitView.masterPaneWidth = 180;
You can also include it by adding pod 'GSSplitViewController' to your Podfile.

ViewController.h
#property(nonatomic, assign) CGFloat maximumPrimaryColumnWidth NS_AVAILABLE_IOS(8_0);
ViewController.m
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
if (SYSTEM_VERSION_LESS_THAN(#"10.0")) {
[self setValue:[NSNumber numberWithFloat:200.0]forKey:#"_masterColumnWidth"];
}else{
self.maximumPrimaryColumnWidth = 200;
self.splitViewController.maximumPrimaryColumnWidth = self.maximumPrimaryColumnWidth;
}

Swift 3.0 you use like
let widthfraction = 2.0 //Your desired value for me 2.0
splitViewController?.preferredPrimaryColumnWidthFraction = 0.40
let minimumWidth = min((splitViewController?.view.bounds.size.width)!,(splitViewController?.view.bounds.height)!)
splitViewController?.minimumPrimaryColumnWidth = minimumWidth / widthFraction
splitViewController?.maximumPrimaryColumnWidth = minimumWidth / widthFraction
let leftNavController = splitViewController?.viewControllers.first as! UINavigationController
leftNavController.view.frame = CGRect(x: leftNavController.view.frame.origin.x, y: leftNavController.view.frame.origin.y, width: (minimumWidth / widthFraction), height: leftNavController.view.frame.height)

// in UISplitViewController subclass
// let more space for detail in portrait mode
- (void)viewWillLayoutSubviews
{
CGFloat width;
if (UIInterfaceOrientationIsPortrait(UIApplication.sharedApplication.statusBarOrientation)){
width = CGRectGetWidth(self.view.bounds) * 0.25f;
}
else {
width = CGRectGetWidth(self.view.bounds) * 0.33f;
}
width = (NSInteger)fminf(260, fmaxf(120, width));
self.minimumPrimaryColumnWidth = width;
self.maximumPrimaryColumnWidth = width;
[super viewWillLayoutSubviews];
}

This code work for me:)
#interface UISplitViewController(myExt)
- (void)setNewMasterSize:(float)size;
#end
#implementation UISplitViewController(myExt)
- (void)setNewMasterSize:(float)size
{
_masterColumnWidth = size;
}
#end
and use it on each operation with view (like rotation)

Related

iOS - Switch between dismiss and scroll gestures

There's a behavior in the Line messenger app (the de facto messenger app in Japan) that I'm trying to emulate.
Basically, they have a modal view controller with a scroll view inside. When the scroll action reaches the top of its content, the view controller seamlessly switches to an interactive dismissal animation. Also, when the gesture returns the view to the top of the screen, control is returned to the scroll view.
Here's a gif of how it looks.
For the life of me, I can't figure out how they did it. I've tried a few different methods, but they've all failed, and I'm out of ideas. Can anyone point me in the right direction?
EDIT2
To clarify, the behavior that I want to emulate isn't just simply dragging the window down. I can do that, no problem.
I want to know how the same scroll gesture (without lifting the finger) triggers the dismissal transition and then transfers control back to the scroll view after the view has been dragged back to the original position.
This is the part that I can't figure out.
End EDIT2
EDIT1
Here's what I have so far. I was able to use the scroll view delegate methods to add a target-selector that handles the regular dismissal animation, but it still doesn't work as expected.
I create a UIViewController with a UIWebView as a property. Then I put it in a UINavigationController, which is presented modally.
The navigation controller uses animation/transition controllers for the regular interactive dismissal (which can be done by gesturing over the navigation bar).
From here, everything works fine, but the dismissal can't be triggered from the scroll view.
NavigationController.h
#interface NavigationController : UINavigationController <UIViewControllerTransitioningDelegate>
#property (nonatomic, strong) UIPanGestureRecognizer *gestureRecog;
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer;
#end
NavigationController.m
#import "NavigationController.h"
#import "AnimationController.h"
#import "TransitionController.h"
#implementation NavigationController {
AnimationController *_animator;
TransitionController *_interactor;
}
- (instancetype)init {
self = [super init];
self.transitioningDelegate = self;
_animator = [[AnimationController alloc] init];
_interactor = [[TransitionController alloc] init];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set the gesture recognizer
self.gestureRecog = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:_gestureRecog];
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
if (animator == _animator && _interactor.hasStarted) {
return _interactor;
}
return nil;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
if (dismissed == self || [self.viewControllers indexOfObject:dismissed] != NSNotFound) {
return _animator;
}
return nil;
}
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecog {
CGFloat threshold = 0.3f;
CGPoint translation = [gestureRecog translationInView:self.view];
CGFloat verticalMovement = translation.y / self.view.bounds.size.height;
CGFloat downwardMovement = fmaxf(verticalMovement, 0.0f);
CGFloat downwardMovementPercent = fminf(downwardMovement, 1.0f);
switch (gestureRecog.state) {
case UIGestureRecognizerStateBegan: {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged: {
if (!_interactor.hasStarted) {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
}
_interactor.shouldFinish = downwardMovementPercent > threshold;
[_interactor updateInteractiveTransition:downwardMovementPercent];
break;
}
case UIGestureRecognizerStateCancelled: {
_interactor.hasStarted = NO;
[_interactor cancelInteractiveTransition];
break;
}
case UIGestureRecognizerStateEnded: {
_interactor.hasStarted = NO;
if (_interactor.shouldFinish) {
[_interactor finishInteractiveTransition];
} else {
[_interactor cancelInteractiveTransition];
}
break;
}
default: {
break;
}
}
}
#end
Now, I have to get that gesture handling to trigger when the scroll view has reached the top. So, here's what I did in the view controller.
WebViewController.m
#import "WebViewController.h"
#import "NavigationController.h"
#interface WebViewController ()
#property (weak, nonatomic) IBOutlet UIWebView *webView;
#end
#implementation WebViewController {
BOOL _isHandlingPan;
CGPoint _topContentOffset;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.webView.scrollView setDelegate:self];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ((scrollView.panGestureRecognizer.state == UIGestureRecognizerStateBegan ||
scrollView.panGestureRecognizer.state == UIGestureRecognizerStateChanged) &&
! _isHandlingPan &&
scrollView.contentOffset.y < self.navigationController.navigationBar.translucent ? -64.0f : 0) {
NSLog(#"Adding scroll target");
_topContentOffset = CGPointMake(scrollView.contentOffset.x, self.navigationController.navigationBar.translucent ? -64.0f : 0);
_isHandlingPan = YES;
[scrollView.panGestureRecognizer addTarget:self action:#selector(handleGesture:)];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(#"Did End Dragging");
if (_isHandlingPan) {
NSLog(#"Removing action");
_isHandlingPan = NO;
[scrollView.panGestureRecognizer removeTarget:self action:#selector(handleGesture:)];
}
}
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer {
[(NavigationController*)self.navigationController handleGesture:gestureRecognizer];
}
This still doesn't work quite right. Even during the dismissal animation, the scroll view is still scrolling with the gesture.
End EDIT1
That is a custom interactive transition.
First, you need set transitioningDelegate of UIViewController
id<UIViewControllerTransitioningDelegate> transitioningDelegate;
Then implment these two method to
//Asks your delegate for the transition animator object to use when dismissing a view controller.
- animationControllerForDismissedController:
//Asks your delegate for the interactive animator object to use when dismissing a view controller.
- interactionControllerForDismissal:
When drag to top, you start the transition, you may use UIPercentDrivenInteractiveTransition to control the progress during scrolling.
You can also refer to the source code of ZFDragableModalTransition
Image of ZFDragableModalTransition
As explained here the solution is quite complex. The person who answered, #trungduc, programmed a little demo published on github doing the sought behaviour. You can find it here.
The easiest way of making this work is to copy the 4 files found in /TestPanel/Presentation/ in the attached github repository, to your project. Then add the PanelAnimationControllerDelegate to your View Controller containing the scroll view (i.e. using the protocol).
Add the following to your View Controller, to satisfy the protocol:
func shouldHandlePanelInteractionGesture() -> Bool {
return (scrollView.contentOffset.y == 0);
}
Add this to deactivate the bouncing effect at the top:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10);
}
Set scrollView.delegate = self
Before presenting your View Controller containing the scroll view set the following propreties to your View Controller:
ScrollViewController.transitioningDelegate = self.panelTransitioningDelegate
ScrollViewController.modalPresentationStyle = .custom
If you want to change the size of your ScrollViewController, you will need to comment out the override of the frameOfPresentedViewInContainerView in the PanelPresentationController file (one of the 4). Then in the presentationTransitionWillBegin method, you will need to set let frameOfPresentedViewInContainerView = self.frameOfPresentedViewInContainerView.insetBy(dx: 0, dy: 20) with the wanted inset of dx and dy.
Thank you to trungduc for this amazing solution!!

View factory for iOS

I am trying to create a View factory for iOS and I am getting into some trouble. The view factory should be a able to create multiple UIImageViews, MKMapViews or UIWebViews and add them to a view vertically. I have a problem with the web view cause I have to wait for webViewDidFinishLoad. If the methods are implemented in the ViewController which implements the UIWebViewDelegate and the delegate of the webviews is set to the controller everything works fine. The thing is that I want to put all of this in a separate class but the webViewDidFinishLoad is never been called. Is there a way to achieve that?
Tell me if you need any code
Code:
Part is added
- (void)addPart
{
if (self.viewPartStrings.count != 0) {
NSString *viewPartString = [self.viewPartStrings objectAtIndex:0];
NSString *viewPartIdentifier = [self getViewPartIdentifier:viewPartString];
NSString *viewPartDefinition = [self getViewPartDefinition:viewPartString];
if ([viewPartIdentifier isEqualToString:kWeb]) {
[self createTextWebViewPart:viewPartIdentifier];
} else if ([viewPartIdentifier isEqualToString:kMap]) {
[self createMapViewPart:viewPartDefinition];
} else if ([viewPartIdentifier isEqualToString:kImage]) {
[self createImageViewPart:viewPartDefinition];
}
}
}
Views are created:
- (void)createTextWebViewPart:(NSString *)viewPartText
{
WebViewPart *webViewPart = [[WebViewPart alloc]initWithFrame:[self getViewPartFrameForHeight:0]];
[webViewPart createViewForText:viewPartText];
[webViewPart setDelegate:self];
}
- (void)createImageViewPart:(NSString *)viewPartDefinition
{
//create image
}
- (void)createMapViewPart:(NSString *)viewPartDefinition
{
//create map
}
But here is the point. This method is never called. Not if the class extends NSObject and not if the class extends UIView:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
CGRect frame = webView.frame;
frame.size.height = 1;
webView.frame = frame;
CGSize fittingSize = [webView sizeThatFits:CGSizeZero];
frame.size = fittingSize;
[webView stringByEvaluatingJavaScriptFromString:#"document.body.style.webkitTouchCallout='none';"];
int height = frame.size.height + 70;
[self addToView:webView forHeight:height];
}
EDIT: I also tried doing this in the Controller:
#interface DetailController ()
#property (nonatomic, strong) ViewPartsFactory *viewPartsFactory;
#end
Then:
- (void)setViewForModel:(DealModel *)model
{
//...
self.viewPartsFactory = [[ViewPartsFactory alloc]initWithViewParts: [descriptionParser getViewParts]];
UIView *view = [self.viewPartsFactory getView]
[scrollView addSubview:view];
//..
}
and
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[self.viewPartsFactory webViewDidFinishLoad:webView];
}
setting the webview delegates to the controller but still nothing.
I'll answer this in super-pseudo code and hopefully it'll give you an idea of what I'm suggesting.
// ViewPartsFactory.h
-(void)webViewDidFinishLoad; // implement your logic in this method
// Ensure you DO NOT set the delegate anywhere here, otherwise it'll override when you set it in the view controller.
//--------------------------------------------------//
// DetailController.m
#property (nonatomic, weak) id <UIWebViewDelegate> delegate;
DetailController <UIWebViewDelegate>
-(void)viewDidLoad {
self.delegate = viewPartsFactory;
}

Cordova/PhoneGap iOS: WebView becoming shorter after taking a picture or opening InAppBrowser

When I take a picture with Camera plugin, the whole WebView becomes shorter. It's like the screen is being eaten from bottom for the height of the status bar - 20px. It doesn't have to do with taking a picture at all, it also happens if I only open gallery and then close it. The same thing happens if I open InAppBrowser and then close it. Here is an example:
This is how it looks before choosing a photo
Then the photo choosing dialog appears (doesn't matter if it's emulator, it behaves the same on the real device)
And then this happens when I close the dialog - look at the bottom of the screen
And if I continue to add photos, each time the screen gets 'eaten' by 20px
I found out that the window.innerHeight is getting reduced by 20px, so it has something to do with the status bar. How can I fix this?
I stumbled upon the answer in another thread. Here it is:
In MainViewController.h:
#interface MainViewController : CDVViewController
#property (atomic) BOOL viewSizeChanged;
#end
In MainViewController.m:
#implementation MainViewController
#synthesize viewSizeChanged;
[...]
- (id)init
{
self = [super init];
if (self) {
// On init, size has not yet been changed
self.viewSizeChanged = NO;
// Uncomment to override the CDVCommandDelegateImpl used
// _commandDelegate = [[MainCommandDelegate alloc] initWithViewController:self];
// Uncomment to override the CDVCommandQueue used
// _commandQueue = [[MainCommandQueue alloc] initWithViewController:self];
}
return self;
}
[...]
- (void)viewWillAppear:(BOOL)animated
{
// View defaults to full size. If you want to customize the view's size, or its subviews (e.g. webView),
// you can do so here.
// Lower screen 20px on ios 7 if not already done
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7 && !self.viewSizeChanged) {
CGRect viewBounds = [self.webView bounds];
viewBounds.origin.y = 20;
viewBounds.size.height = viewBounds.size.height - 20;
self.webView.frame = viewBounds;
self.viewSizeChanged = YES;
}
[super viewWillAppear:animated];
}
Source: https://stackoverflow.com/a/19407903/1555838

My modal keeps eating memory

I am experiencing a rather serious issue with my iPhone app using ARC.
I have a viewcontroller (lets call this A). This viewcontroller opens a navigationcontroller as a modal which runs through 3 different viewcontrollers (lets call these 1, 2 and 3). After viewing number 3 the navigationcontroller closes and we're back to A again.
So the flow is: A opens navigationcontroller and goes through 1->2->3 and then it closes again.
Every time I go through this flow i lose memory. I've searched through all my files looking for any retain og strong properties, un-invalidated timers or similar in order to solve this problem.
I have one idea, which might be the problem. At viewcontroller 1 i present a animation using coreanimation and a sprite. I'm using a implementation made by someone else. It seems like if i disable the animations the memory used seems quite constant (and thereby no memory loss). I have modified the implementation a bit to use ARC. This is the implementation I use for my sprite animations:
MCSpriteLayer.h
//
// MCSpriteLayer.h
//
// Created by Miguel Angel Friginal on 8/20/10.
// Copyright 2010 Mystery Coconut Games. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
#interface MCSpriteLayer : CALayer {
unsigned int sampleIndex;
}
// SampleIndex needs to be > 0
#property (nonatomic) unsigned int sampleIndex;
// For use with sample rects set by the delegate
+ (id)layerWithImage:(CGImageRef)img;
- (id)initWithImage:(CGImageRef)img;
// If all samples are the same size
+ (id)layerWithImage:(CGImageRef)img sampleSize:(CGSize)size :(int)useRetina;
- (id)initWithImage:(CGImageRef)img sampleSize:(CGSize)size;
// Use this method instead of sprite.sampleIndex to obtain the index currently displayed on screen
- (unsigned int)currentSampleIndex;
#end
MCSpriteLayer.m
//
// MCSpriteLayer.m
//
// Created by Miguel Angel Friginal on 8/20/10.
// Copyright 2010 Mystery Coconut Games. All rights reserved.
//
#import "MCSpriteLayer.h"
#implementation MCSpriteLayer
#synthesize sampleIndex;
#pragma mark -
#pragma mark Initialization, variable sample size
- (id)initWithImage:(CGImageRef)img;
{
self = [super init];
if (self != nil)
{
self.contents = (__bridge id)img;
sampleIndex = 1;
}
return self;
}
+ (id)layerWithImage:(CGImageRef)img;
{
MCSpriteLayer *layer = [(MCSpriteLayer*)[self alloc] initWithImage:img];
return layer;
}
#pragma mark -
#pragma mark Initialization, fixed sample size
- (id)initWithImage:(CGImageRef)img sampleSize:(CGSize)size;
{
self = [self initWithImage:img];
if (self != nil)
{
CGSize sampleSizeNormalized = CGSizeMake(size.width/CGImageGetWidth(img), size.height/CGImageGetHeight(img));
self.bounds = CGRectMake( 0, 0, size.width, size.height );
self.contentsRect = CGRectMake( 0, 0, sampleSizeNormalized.width, sampleSizeNormalized.height );
}
return self;
}
+ (id)layerWithImage:(CGImageRef)img sampleSize:(CGSize)size :(int)useRetina;
{
CGSize newSampleSize;
if(useRetina == 1) {
// Supporting retina displays
if ([[UIScreen mainScreen] respondsToSelector:#selector(displayLinkWithTarget:selector:)] &&
([UIScreen mainScreen].scale == 2.0)) {
newSampleSize = CGSizeMake(size.width*2, size.height*2);
} else {
newSampleSize = size;
}
} else
newSampleSize = size;
MCSpriteLayer *layer = [[self alloc] initWithImage:img sampleSize:newSampleSize];
return layer;
}
#pragma mark -
#pragma mark Frame by frame animation
+ (BOOL)needsDisplayForKey:(NSString *)key;
{
return [key isEqualToString:#"sampleIndex"];
}
// contentsRect or bounds changes are not animated
+ (id < CAAction >)defaultActionForKey:(NSString *)aKey;
{
if ([aKey isEqualToString:#"contentsRect"] || [aKey isEqualToString:#"bounds"])
return (id < CAAction >)[NSNull null];
return [super defaultActionForKey:aKey];
}
- (unsigned int)currentSampleIndex;
{
return ((MCSpriteLayer*)[self presentationLayer]).sampleIndex;
}
// Implement displayLayer: on the delegate to override how sample rectangles are calculated; remember to use currentSampleIndex, ignore sampleIndex == 0, and set the layer's bounds
- (void)display;
{
if ([self.delegate respondsToSelector:#selector(displayLayer:)])
{
[self.delegate displayLayer:self];
return;
}
unsigned int currentSampleIndex = [self currentSampleIndex];
if (!currentSampleIndex)
return;
CGSize sampleSize = self.contentsRect.size;
self.contentsRect = CGRectMake(
((currentSampleIndex - 1) % (int)(1/sampleSize.width)) * sampleSize.width,
((currentSampleIndex - 1) / (int)(1/sampleSize.width)) * sampleSize.height,
sampleSize.width, sampleSize.height
);
}
#end
Is this implementation somehow not realeasing correctly or retaining anything? Thanks in advance.
Update
- I am using Instruments to measure the memory. I am using the Memory Monitor where I keep an eye on the Physical Memory Free
- The image is created like this:
NSString *path = [[NSBundle mainBundle] pathForResource:#"round_start.png" ofType:nil];
CGSize fixedSize = CGSizeMake(320, 480);
mascot = [MCSpriteLayer layerWithImage:[UIImage imageWithContentsOfFile:path].CGImage sampleSize:fixedSize :0];
mascot.frame = CGRectMake(ANIMATION_X, ANIMATION_Y, ANIMATION_WIDTH, ANIMATION_HEIGHT);
[self.view.layer addSublayer:mascot2];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"sampleIndex"];
anim.delegate = self;
anim.fromValue = [NSNumber numberWithInt:1];
anim.toValue = [NSNumber numberWithInt:52];
anim.duration = ANIMATION_DURATION;
anim.repeatCount = 1;
[mascot addAnimation:anim forKey:nil];
- I've been experiencing with closing the modal with
[self dismissModalViewControllerAnimated:YES];
and
[self.navigationController dismissModalViewControllerAnimated:YES];
Dismissing the navigation controller does not release it, assuming you have a strong reference to it (that would need to get nil'd). In the view controllers used by it, add a log message in the dealloc method, so you know they are getting dealloced (you can do this for any subclassed item). If needed you can create a simple UINavigation subclass for the sole purpose of adding a message in dealloc). You will find that one or more of these items are not getting dealloced, and then you need to figure out if they are retained by a property/ivar or because they still have a superview.

iOS: iTunes-like Badge in UITabbar

I have got a UITabBarController in a Storyboard. Right now, it has got 5 UITabBarItems. When I am in the other UITabBarItem, I want to update the Badge on the other UITabBarItem(my "Downloads") just like the iTunes App does with this "jump-like" animation when you buy a song or album. Is this possible? If Yes, how?
Thank you.
Yes...
There is a lot to an animation like the I'll call it "send to Downloads" type animation. I'll answer this question using an example.
Warning: this example breaks the MVC paradigm more than I'd like, but it's long enough as it is.
I'll use a simple Storyboard like this (in fact, exactly this):
I'll start by describing the "First View Controller - First":
Those many buttons in the view are connected to the one listed IBAction method. And that's about all the description needed for that view controller. Here is its .m file:(truncated)
//#import "First_View_Controller.h"
#interface First_View_Controller ()
#property (weak, nonatomic) DownloadViewController *downloadViewController;
#end
#implementation First_View_Controller
#synthesize downloadViewController = _downloadViewController;
-(DownloadViewController *)downloadViewController{
if (!_downloadViewController){
// Code to find instance of DownloadViewController in the tabBarController's view controllers.
for (UIViewController *vc in self.tabBarController.viewControllers) {
if ([vc isKindOfClass:[DownloadViewController class]]){
_downloadViewController = (DownloadViewController *)vc;
break;
}
}
}
return _downloadViewController;
}
-(IBAction)buttonPush:(UIButton *)button{
[self.downloadViewController addADownload:nil withViewToAnimate:button];
}
// Other typical VC crap...
#end
The IBAction is fairly self-explanatory. It gets reference to the instance of DownloadViewController, by looking through the tabBarController's view controllers, and passes the view to animate to that instance.
Now for DownloadViewController.m. It's a lot of code. I've commented it, to try to make it clear:
#import "DownloadViewController.h"
#import <QuartzCore/QuartzCore.h>
// A Category on UITabBar to grab the view of a tab by index.
#implementation UITabBar (WhyIsntThisBuiltIn)
-(UIView *)nj_ViewOfTabNumber:(NSUInteger)number{
if (number == NSNotFound) return nil;
// Fairly standard method for getting tabs, getting the UIControl objects from the 'subviews' array.
// I pulled the next few lines from an SO question.
NSMutableArray *tabs = [[NSMutableArray alloc] init];
[self.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
if ([(NSObject *)obj isKindOfClass:UIControl.class]){
[tabs addObject:obj];
}
}];
// The code above gets the tabs' views, but they may not be in the correct order.
// This sort is required if a view controller has been replaced,...
// Since, in that case, the order in which the tabs' views appear in the 'subviews' array will not be the left-to-right order.
[tabs sortUsingComparator:^NSComparisonResult(UIView *obj1, UIView *obj2){
CGFloat v1 = obj1.center.x;
CGFloat v2 = obj2.center.x;
if (v1<v2) return NSOrderedAscending;
if (v1>v2) return NSOrderedDescending;
return NSOrderedSame;
}];
// This if is required for the case where the view controller is in the "more" tab.
if (number >= tabs.count) number = tabs.count-1;
return [tabs objectAtIndex:number];
}
#end
// A Category on UITabBarController to get the view of a tab that represents a certain view controller.
#implementation UITabBarController (WhyIsntThisBuiltIn)
-(UIView *)nj_viewOfTabForViewController:(UIViewController *)viewController{
// Find index of the passed in viewController.
NSUInteger indexOfViewController = [self.viewControllers indexOfObject:viewController];
if (indexOfViewController == NSNotFound) return nil;
// Return the view of the tab representing the passed in viewController.
return [self.tabBar nj_ViewOfTabNumber:indexOfViewController];
}
#end
// Insert required warning about using #defines here.
#define MY_ANIMATION_DURATION 0.8
#implementation DownloadViewController{
NSUInteger _numberOfDownloads;
}
-(void)updateBadgeValue{
self.tabBarItem.badgeValue = [NSString stringWithFormat:#"%i",_numberOfDownloads];
}
// This method creates a "snapshot" of the animation view and animates it to the "downloads" tab.
// Removal of the original animationView must, if desired, be done manually by the caller.
-(void)addADownload:(id)someDownload withViewToAnimate:(UIView *)animationView{
// update model...
_numberOfDownloads++;
// Animate if required
if (animationView){
// Create a `UIImageView` of the "animationView" name it `dummyImageView`
UIGraphicsBeginImageContextWithOptions(animationView.frame.size, NO, [[UIScreen mainScreen] scale]);
[animationView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *dummyImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *dummyImageView = [[UIImageView alloc] initWithImage:dummyImage];
dummyImageView.frame = animationView.frame;
// Determine UIView of tab using non-private API.
UITabBarController *tabBarController = self.tabBarController;
UIView *downloadsTab = [tabBarController nj_viewOfTabForViewController:self];
// Determine animation points in tabBarController's view's coordinates.
CGPoint animationStartPoint = [tabBarController.view convertPoint:dummyImageView.center fromView:dummyImageView.superview];
CGPoint animationEndPoint = [tabBarController.view convertPoint:downloadsTab.center fromView:downloadsTab.superview];
CGFloat totalXTravel = animationEndPoint.x - animationStartPoint.x;
// This is an arbitrary equation to create a control point, this is by no means canonical.
CGPoint controlPoint = CGPointMake(animationEndPoint.x, animationStartPoint.y - fabs(totalXTravel/1.2));
// Create the animation path.
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:animationStartPoint];
[path addQuadCurveToPoint:animationEndPoint controlPoint:controlPoint];
// Create the CAAnimation.
CAKeyframeAnimation *moveAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnimation.duration = MY_ANIMATION_DURATION;
moveAnimation.path = path.CGPath;
moveAnimation.removedOnCompletion = NO;
moveAnimation.fillMode = kCAFillModeBoth;
[tabBarController.view addSubview:dummyImageView];
dummyImageView.center = animationStartPoint;
// Animate the move.
[dummyImageView.layer addAnimation:moveAnimation forKey:#""];
// Use the block based API to add size reduction and handle completion.
[UIView animateWithDuration:MY_ANIMATION_DURATION
animations:^{
dummyImageView.transform = CGAffineTransformMakeScale(0.3, 0.3);
}
completion:^(BOOL b){
// Animate BIG FINISH! nah, just...
[dummyImageView removeFromSuperview];
[self updateBadgeValue];
}];
}
}
// Other typical VC crap...
#end
And that's about it. When run, this code produces a fairly strong jump from the buttons on the top left, but the buttons on the right, especially on the lower right, are sort-of tossed. And as the animation ends the badge on the downloads tab counts up. A pretty decent knock-off of the effect Apple uses when you purchase content on iTunes.
Remember to add the Quartz Framework to your app.

Resources