Showing posts with label Objective-C. Show all posts
Showing posts with label Objective-C. Show all posts

Tuesday, May 10, 2016

Quick Auto Layout rig for a responsive tab bar control

Let’s say I need to create a tab bar with 3 tabs, each having a button and an indicator. It needs to be responsive (autoresizeable) and each tab equally sized with the others.





Using Auto Layout here is how I do it.

1. Select a view that renders a tab (here called “TabView”). Copy-paste as many as needed. Customize each copy.





2. Add missing constraints manually. Select each tab view, uncheck “Constrain to margins”, and add space constraints. The first tab needs 4 constraints, the rest - only 3.







3. Add size constraints (width or height) by selecting all tab views, then Equal Widths.







4. Finally, update all frames in a parent view.





Embed in a parent controller, declare protocols to handle taps, etc.

Sunday, March 29, 2015

Optimal use of std::ifstream to read into std::string

This is an exemplary bit of code to help read a file by std::ifstream and avoid needless buffer reallocations.
1. DataPath, returns a "Data" subdirectory inside application bundle. In Xcode to package resource files along with a bundle, add the resource directory to a project as a directory reference. Don't name it as "Resources", internal packaging system reserves that name (for whatever reason) and will throw an annoying "iOS Simulator failed to install the application" error.
So, I've chosen to scope resources inside a "Data" directory.
2. ReadFile, will read entire file content and return in a std::string. Will work for cross-platform file access, just paste in a .cpp file.


<FileSystem.hpp>

#include <string>


namespace FileSystem
{
    
    std::string DataPath();
    
    std::string ReadFile(const char* filePath);
    
}



<FileSystem.mm>

#import <Foundation/Foundation.h>
#include <iostream>
#include <fstream>
#import "FileSystem.hpp"


namespace FileSystem
{

    using namespace std;
    
    string DataPath()
    {
        string result;
        NSBundle* bundle = [NSBundle mainBundle];
        
        if (bundle == nil) {
            #ifdef DEBUG
                NSLog(@"Bundle is nil... which should never happen.");
            #endif
        } else {
            NSString* path = [bundle bundlePath];
            // Also, to get Documents directory:
            // path = [NSHomeDirectory() stringByAppendingString: @"/Documents/"];
            path = [NSString stringWithFormat: @"%@%s", path, "/Data/"];
            result = string([path UTF8String]);
        }
        
        return result;
    }
    
    string ReadFile(const char* filePath)
    {
        string result;
        
        ifstream ifs(filePath, ios::in | ios::binary | ios::ate);
        if (!ifs) {
            throw invalid_argument(string("Error opening '") + filePath + "'.");
        }
        
        auto fileSize = ifs.tellg();
        result.resize((string::size_type)fileSize);
        
        ifs.seekg(0, ios::beg);
        auto buf = &result[0];
        ifs.read(buf, (streamsize)fileSize);
        return result;
    }

}




Monday, February 3, 2014

Modal Views in iOS 7 Storyboards


Walkthrough to make a secondary scene in a storyboard act as an animated modal dialog, similar to the Alert view.


Prerequisites:

1. Create a single-view app project.
2. In Project Settings/General/Deployment Info make sure the storyboard name (Main.storyboard) appears in Main Interface popover.


3. Add/remove remove view controllers (VCs) to the storyboard as needed.
4. To make a VC the very first controller - click it to highlight blue, then in Attributes Inspector check the Initial Scene. This will set origin arrow.

5. Create a modal Manual Segue ("seg-way"). In the IB zoom out by double-click on an empty space. Then Ctrl-drag from the presenting VC to the modal VC. Select "modal" in a dialog.

6. To make sure the transition is animated select the segue, check the Animates attribute in the Attribute Inspector.


Now, to use the default animation provided by Xcode - do the following:

1. Add a new View Controller (VC), set it as a CustomClass/Class attribute in Identity Inspector for the presenting view (from-view). Add a button and a tap event handler to the presenting view.

//
//  ALYViewController.m
//

#import "ALYViewController.h"
#import "ALYZoomAnimator.h"

NSString *const MySegueID = @"MySegueID";

@interface ALYViewController ()
@property (strong, nonatomic) ALYZoomAnimator *zoomAnimator;

- (IBAction)showModalViewTapped:(id)sender;
@end


@implementation ALYViewController

- (IBAction)showModalViewTapped:(id)sender {
    [self performSegueWithIdentifier: MySegueID sender: nil];
}
@end

2. Do the same for the modal view, except the code will be dismissing this dialog.
//
//  ALYModalViewController.m
//

#import "ALYModalViewController.h"

@interface ALYModalViewController ()

- (IBAction)dismissTapped:(id)sender;
@end

@implementation ALYModalViewController

- (IBAction)dismissTapped:(id)sender {
    [self dismissViewControllerAnimated: YES completion: nil];
}
@end

3. Run the app. Click the button to reveal the modal dialog.


To add a custom transition with added benefit of controlling size and appearance of the modal dialog (for interactivity, cancellation, etc. in later posts), do the following:

1. Implement <UIViewControllerTransitioningDelegate> and <UIViewControllerAnimatedTransitioning> in a separate animator class. This will handle in/out transitions for a modal dialog. Unfortunately there is no context property to tell which transition is being initiated, in or out. One workaround is to record a VC being presented and then compare it against the to-VC of the transitionContext.

//
//  ALYZoomAnimator.h
//

#import <Foundation/Foundation.h>

@interface ALYZoomAnimator : NSObject <UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning>

@end 

//
//  ALYZoomAnimator.m
//

#import "ALYZoomAnimator.h"

#define IN_DURATION 1.0
#define OUT_DURATION 0.3

@interface ALYZoomAnimator()
@property (weak, nonatomic) UIViewController *presentedVC;

@end


@implementation ALYZoomAnimator

// Returns true while the IN animation is active to reveal the modal sub-dialog.
- (BOOL) isBeingPresented: (id<UIViewControllerContextTransitioning>) transitionContext
{
 UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    return toVC == self.presentedVC;
}


#pragma mark - <UIViewControllerTransitioningDelegate>

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    self.presentedVC = presented;
    return self;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return self;
}


#pragma mark - UIViewControllerAnimatedTransitioning

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return [self isBeingPresented: transitionContext] ? IN_DURATION : OUT_DURATION;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIView *container = transitionContext.containerView;
 
 UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *fromView = fromVC.view;
    UIView *toView = toVC.view;
    
    CGRect cb = container.bounds;
    CGRect startFrame = CGRectMake(cb.size.width / 2, cb.size.height / 2 - 50, 0, 0);
 CGRect endFrame = CGRectInset(startFrame, -100, -100);
    UIView *snapshot;
    
 if ([self isBeingPresented: transitionContext]) {
        fromView.userInteractionEnabled = NO;
        fromView.tintColor = [UIColor grayColor];
        toView.frame = endFrame;
  snapshot = [toView snapshotViewAfterScreenUpdates:YES];
  snapshot.frame = startFrame;
        snapshot.alpha = 0.0;
        [container addSubview: snapshot];
        [UIView animateWithDuration: [self transitionDuration: transitionContext]
                              delay: 0
             usingSpringWithDamping: 500 initialSpringVelocity: 15
                            options: 0
                         animations: ^{
                             fromView.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
                             snapshot.frame = endFrame;
                             snapshot.alpha = 1.0;
                         }
                         completion: ^(BOOL finished) {
                             toView.frame = endFrame;
                             [container addSubview: toView];
                             [snapshot removeFromSuperview];
                             [transitionContext completeTransition:YES];
                         }];
 } else {
  snapshot = [fromView snapshotViewAfterScreenUpdates:YES];
        snapshot.frame = endFrame;
        [container addSubview: snapshot];
        [fromView removeFromSuperview];
        [UIView animateWithDuration: [self transitionDuration: transitionContext]
                              delay: 0
             usingSpringWithDamping: 500 initialSpringVelocity: 15
                            options: 0
                         animations: ^{
                             toView.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
                             snapshot.frame = startFrame;
                             snapshot.alpha = 0.0;
                         }
                         completion: ^(BOOL finished) {
                             [snapshot removeFromSuperview];
                             toView.userInteractionEnabled = YES;
                             [transitionContext completeTransition:YES];
                         }];
 }
}

@end


2. Provide -prepareForSegue method in the presenting VC.

//
//  ALYViewController.m
//

#import "ALYViewController.h"
#import "ALYZoomAnimator.h"

NSString *const MySegueID = @"MySegueID";

@interface ALYViewController ()
@property (strong, nonatomic) ALYZoomAnimator *zoomAnimator;

- (IBAction)showModalViewTapped:(id)sender;
@end


@implementation ALYViewController

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if (![segue.identifier isEqualToString: MySegueID]) {
        return;
    }
    if (!self.zoomAnimator) {
        self.zoomAnimator = [[ALYZoomAnimator alloc] init];
    }
    
    [segue.destinationViewController setTransitioningDelegate: self.zoomAnimator];
    [segue.destinationViewController setModalPresentationStyle: UIModalPresentationCustom];
}

- (IBAction)showModalViewTapped:(id)sender {
    [self performSegueWithIdentifier: MySegueID sender: nil];
}
@end

3. Run the app. Observe the modal dialog animations, transparency, presenting view in the background providing a context to help users keep track of their position in app. This is unlike the normal transitions that will hide the presenting view.