Showing posts with label iOS. Show all posts
Showing posts with label iOS. Show all posts

Wednesday, March 5, 2025

Demo of the upcoming word game.

For those who asked to see my latest project example. Here it is, a work in progress that will be available soon.

Note: this is GIF animation, the original graphics is real-time 60 FPS.

Stay tuned.

Cheers.

 


Wednesday, March 21, 2018

Microsoft's Imagine Cup Challenge - Team Heuristic Clinic

A month ago I prepared a quick team and a project for consideration at Microsoft's Imagine Cup. It was an intriguing experience to document details, to present in a video, and to polish a previously developed prototype - a wireless motion/gesture capturing app and a set of sensors.
I understood there was a slim chance to actually win the Cup, so I instead focused on setting up this project for possible future applications elsewhere.

The team is called Heuristic Clinic, the project is Applied Neural Diagnosis. Brief description can be found here: Heuristic Clinic, and the resulting certificate can be viewed here.
They were kind to offer $500 worth of Azure cloud, but I had to decline because the biggest expense is funding client-side development. Appreciated.


©

2018 Big Idea Challenge

Top 50 - 2018 Big Idea Challenge

This Certificate is Presented To:

alexey l

Team Heuristic Clinic

Congratulations and thank you for your hard work and dedication as a competitor in Imagine Cup 2018. You are now part of an elite international community of students who have shown remarkable creativity and innovation to push technology forward.
©
Pablo Veramendi
Imagine Cup Competition Manager
Microsoft Corporation

Wednesday, May 25, 2016

On UI composition, embedded view controllers, weak vars, multi-cast delegates.

Interface Builder is great for composing plain interfaces, where each view controller is represented by a single instance of view in a view hierarchy.

Embedded view controllers are different.

Even though you can create multiple Embed segues connecting different places in a storyboard to the same embedded view controller - during app initialization the framework will create as many instances of that controller as there are segues.

This is counter-intuitive at first. A collection of views is an ordered tree, where each node has a reference to its single parent. But in a storyboard the view controllers can be connected by Embed segues arbitrarily and can form cycles.

It appears Apple gets away with this by forcing each Embed segue to create a new instance of a destination view controller.

For example, the following storyboard with embedded view controllers (part of a project in a coursera.org course) has many parent-child Embed relations:




Note the three parent Container views, and two of them share a view controller.

A parent view controller will have outlets to instances of child view controllers. Those outlets are declared weak and fished out of the storyboard in an override of prepareForSegue() method. If there are multiple instances of the same embedded view controller (as in the screenshot) - you’d want them stored in an array of weak references. But the challenge with Swift 2 is that it does not support collections of weak references.

In a simple case - having separate weak refs is ok. The prepareForSegue() implementation might look like this:

    // MARK: - Navigation

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

        // !!! Embed segues are processed before viewDidLoad().
        switch segue.destinationViewController {
        case let vc as MyHobbiesCollectionViewController:
            // Note that this embedded VC has multiple instances,
            // need additional id check (i.e. by segue.identifier).
            switch segue.identifier {
                case .Some("My1stHobbiesEmbedSegue"):
                    my1stHobbiesCVC = vc;
            default:
                my2ndHobbiesCVC = vc;
            }
           
            // Also prepare non-embed segues.
        case let vc as EditHobbiesViewController:
            break;
        default: break;
        }

    }


In other more advanced cases, a recommended solution is to declare a weak ref in a structure and then have an array of those structures. Hopefully Apple is working to fix this minor annoyance.

A few words about how a hierarchy of view controllers should communicate with the rest of the tree.

KVO and NSNotificationCenter are ok in general case, but they lack type-safety of messages, and require not very nice symmetry of subscrbe/unsubscribe calls. Instead, I prefer declaring protocols for each child view controller, then a parent would implement those protocols, and assign itself to weak ref delegates of its children.

Events of the child controllers will first travel up a hierarchy, where a parent will route them to peers.

This messaging system could make use of a collection of weak ref delegates, which would be referred to as multi-cast delegates. But this will have to wait till a version of Swift that implements weak ref collections natively.

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.

Saturday, February 20, 2016

Slide-out menu (with hamburger).

This short piece is about interactive slide-out menu in iOS. The demo is using an embedded container view for a menu view controller, and that controller handles animations and interaction.

The following implementation will be used for interaction-animation control where view hierarchy remains mostly static.




1. Menu view consists of two container views - a full screen dim view and an overlapping menu view. Interaction is registered by a pan gesture recognizer and is converted to a normalized “progress” property value. Setting this property on a menu view controller updates both the alpha property of the dim view and an origin of a menu view (via a horizontal leading constraint). 


import UIKit


class RootViewController: UIViewController, UIGestureRecognizerDelegate{

    @IBOutlet weak var menuContainerView: UIView!
    @IBOutlet weak var mainContainerView: UIView!
    
    weak var menuVC: MenuViewController!;

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        
        // Instead of fishing for an embedded controller in a prepareForSegue.
        for vc in childViewControllers {
            if let mvc = vc as? MenuViewController {
                menuVC = mvc;
            }
        }
    }

    
    // MARK: - Gesture Recognizer
    
    var initialProgress: Float = 0.0;
    var ignoreGesture = false;

    @IBAction func handlePanGesture(recognizer: UIPanGestureRecognizer) {
        let vel = recognizer.velocityInView(view);
        let velThreshold = menuVC.velocityThreshold;

        switch(recognizer.state) {
        case .Began:
            ignoreGesture = abs(vel.y) > abs(vel.x);
            if (ignoreGesture) {
                return;
            }
            initialProgress = menuVC.progress;
            recognizer.setTranslation(CGPointZero, inView: view)
            
        case .Changed:
            if (ignoreGesture) {
                return;
            }
            let translation = recognizer.translationInView(view);
            let tX = Float(translation.x);
            let progress = initialProgress + tX / velThreshold;
            // The following can be used to prevent menu progress overshot (not a good "responsive UI").
            // progress = min(1.0, max(0.0, progress));
            menuVC.progress = progress;
            
        case .Ended:
            if (ignoreGesture) {
                return;
            }
            let velX = Float(vel.x);
            let expand = ((menuVC.progress > 0.5 && velX > -velThreshold) || (menuVC.progress < 0.5 && velX > velThreshold));
            menuVC.animateMenu(expand);
            
        default:
            break
        }
    }
    
}



2. Progress property setter does a few things differently if the value is to be animated. Because menu works in conjunction with the embedding superview, the progress will enable/disable the superview as needed so that collapsed menu stays out of way when user interacts with the main content. To animate this property from its current value - there is a function animateMenu:.


import UIKit

class MenuViewController: UIViewController {
    @IBOutlet weak var dimView: UIView!
    @IBOutlet weak var menuView: UIView!
    @IBOutlet weak var menuLeadingConstraint: NSLayoutConstraint!

    var _useAnimation = false;
    var _progress: Float = 0.0;
    
    // To animate to a value set the _useAnimation true.
    var progress: Float {
        get { return _progress; }
        set {
            if (abs(newValue) > 1.0) {
                _progress = min(1.5, pow(abs(newValue), 0.2) * (newValue >= 0.0 ? 1.0 : -1.0));
            }
            else {
                _progress = newValue;
            }
            if (newValue > 1e-5 && !menuEnabled) {
                // Don't just set to condition via assignment, "false" will break animation (0.0 will collapse the menu before animation begins).
                menuEnabled = true;
            }
            
            let animations = {
                self.dimView.alpha = CGFloat(min(1.0, self._progress) / 2);
                let menuOriginX = CGFloat(self.velocityThreshold * self._progress) - self.menuView.frame.width;
                self.menuLeadingConstraint.constant = menuOriginX;
                self.view.layoutIfNeeded();
            };
            let completion: (Bool -> Void) = { finished in
                if (newValue <= 1e-5) {
                    self.menuEnabled = false;
                }
                self._useAnimation = false;
            };

            if (_useAnimation) {
                UIView.animateWithDuration(0.25, delay: 0.0, options: .CurveEaseOut, animations: animations, completion: completion);
            } else {
                animations();
                completion(true);
            }
        }
    }
    
    private(set) var velocityThreshold: Float = 100.0;
    
    // Enables/disables container view.
    var menuEnabled: Bool {
        get {
            return !view.superview!.hidden;
        }
        set {
            view.superview!.hidden = !newValue;
        }
    }

    func animateMenu(expand: Bool) {
        _useAnimation = true;
        
        if (expand) {
            progress = 1;
        } else {
            progress = 0;
        }
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        
        // Initial menu view offset is a design-time position for an "expanded" state.
        velocityThreshold = Float(menuView.frame.width + menuView.frame.origin.x);
    }
    
    override func viewDidAppear(animated: Bool) {
        menuEnabled = true;
        progress = 1.0;
        animateMenu(false);
    }

    @IBAction func tapDimView(sender: AnyObject) {
        animateMenu(false);
    }
}




Other ways to implement this, such as using UIScrollView or custom transition animators, are more appropriate if standard transitions (on navigation controller) are desired. I find them either lacking in brevity of code or having tight dependence on standard view controllers.






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.