Showing posts with label MVC. Show all posts
Showing posts with label MVC. Show all posts

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.

Sunday, November 8, 2015

Handlebars Essentials 3 - Template Precompilation

Assuming the project has templates in a dedicated folder, this step will optimize the way pages are rendered with Handlebars.

Why precompilation? Because Handlebars is designed to convert (compile) templates from their source form (with double-curly-braces) to a Javascript. And this step is expensive on a thin client. So, there is a quick and relatively easy extra step during development to compile all templates into a single .js file. That file is to be referenced by a <script> tag from the host page.


1. Make sure that Node.js is installed and up-to-date.

2. Use the following to install Handlebars module for Node.js.
$ sudo npm install -g handlebars


3. Place all templates into a subfolder, say “templates”. Then, from the root of the project do this:
$ handlebars templates/* -f templates/templates.js

This creates a single .js file with the code for all precompiled templates. To minimize the output and strip out spaces, use this instead:
$ handlebars templates/* -m -f templates/templates.js


4. Add a reference to the newly created templates.js.
<script src="templates/templates.js"></script>
<script src="js/initModel.js"></script>


5. App.js - replace the function to load templates with the following code. Note that partial templates still need to be loaded from the source as before, it appears this is the only way to register partials in Handlebars at present. And this is the only change required.
  function loadTemplatesAndRender() {
    indexTemplate = Handlebars.templates["index.hbs"];
    itemsTemplate = Handlebars.templates["items.hbs"];
    itemTemplate = Handlebars.templates["item.hbs"];

    // Note, the item template is a partial,
    // therefore also needs to be loaded as a source.
    var t3 = $.get("templates/item.hbs", function (data) {
      Handlebars.registerPartial("item", data);
    }, "html");
    // Wait for templates to finish loading.
    $.when(t3).done(function() {
      // Also wait for $(document).ready().
      $(function() {
        //alert("ALL DONE");
        renderPage();
        renderItems();
      })
    })
  }


UPDATE: There is a simpler way to "register" partials. Use the following somewhere after templates are loaded:
Handlebars.partials = Handlebars.templates;

For this to work the partial's reference needs to point to a template by file name, like so:
{{#each items}}
  {{> item.hbs language=@root.language}}
{{else}}
  {{language.noItemsFound}}
{{/each}}


Handlebars Essentials 2

This continues exploring Handlebars library.
Here you will see how to split markup into parts, with the goal to move templates and partials into their own units, and thus allow granular code management.
It will demonstrate how to build an MVC style framework of managing page elements, with pages being simple containers for subviews (hierarchies of views), and client-side logic in controllers being responsible for controlling a single view by feeding data from a model data source.

There is one deficiency here that will be tackled in the next post. It is about optimizing templates and speeding up page load by pre-compilation.


1. Index.html - this is a bare-bones host page for templated elements.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bunnies</title>
    <link rel="stylesheet" href="css/styles.css">
    <script src="lib/jquery/jquery-2.1.4.min.js"></script>
    <script src="lib/handlebars/handlebars-v4.0.4.js"></script>

    <script src="js/initModel.js"></script>
</head>

<body>
    <div id="main">
    </div>
    <script src="js/app.js"></script>
</body>
</html>


2. InitModel.js - loads and initializes a model object. It is a simple skeletal code to load from a local storage, initialize, and define basic operations on items.
(function() {

  var model = window.model = {};
  model.items = [];
  model.lang = 0;
  model.languages = [
    {
      langId: "lang0",
      siteTitle: "App Prototype (with Handlebars)",
      page1Name: "Home",
      page2Name: "Favorites",
      page3Name: "Contact us",
      lang1Name: "Eng",
      lang2Name: "C++",
      noItemsFound: "No items found",
      one: "Call one",
      two: "Call two",
      good: "Good for you",
      bad: "Bad for you"
    },
    {
      langId: "lang1",
      siteTitle: "template<T> void App (std::shared_ptr<Handlebars>);",
      page1Name: "home();",
      page2Name: "favorites();",
      page3Name: "info();",
      lang1Name: "std::shared_ptr<Eng>();",
      lang2Name: "this",
      noItemsFound: "NULL",
      one: "one();",
      two: "two();",
      good: "result = true;",
      bad: "result = false;"
    },
  ];

  // MARK: - System support functions.

  function parseUrl() {
    var queryParams = window.location.search.slice(1).split('&');
    var paramsObj = {};
    queryParams.forEach(function(val) {
      if (val) {
        var pair = val.split("=");
        paramsObj[pair[0]] = pair[1];
      }
    });
    return paramsObj;
  };

  // MARK: - Init functions.

  window.model.initLanguage = function() {
    var wasUnarchived = this.unarchiveLanguage();
    console.log(this.lang);

    var lang = parseUrl().lang;
    if (lang == undefined) {
      lang = this.lang;
    }
    console.log(lang);
    if (this.lang == lang) {
      return;
    }

    this.lang = lang;
    this.archiveLanguage();
  }

  function Item(id, image, name, isGood, moreInfo) {
    this.id = id;
    this.image = image;
    this.name = name;
    this.isGood = isGood;
    this.moreInfo = moreInfo;
    return this;
  }

  window.model.initItems = function() {
    if (this.unarchiveItems()) {
      return;
    }
    this.items = [
      new Item("bunny0", "bubby_bunny.jpg", "Bubby Bunny", false),
      new Item("bunny1", "cloudy_bunny.jpg", "Cloudy Bunny", false),
      new Item("bunny2", "meditating_cat.jpg", "Meditating Cat", true)
    ];
    this.archiveItems();
  }

  // MARK: - Item operations.

  window.model.chooseItem = function(itemId) {
    var item = this.findItem(itemId);
    if (item) {
      item.moreInfo = true;
      this.archiveItems();
    }
  }

  window.model.clearItem = function(itemId) {
    var item = this.findItem(itemId);
    if (item) {
      item.moreInfo = false;
      this.archiveItems();
    }
  }

  window.model.findItem = function(itemId) {
    var item;
    this.items.forEach(function(val) {
      if (val.id === itemId) {
        item = val;
      }
    });
    return item;
  }

  // MARK: - Archiver.

  window.model.archive = function() {
    this.archiveLanguage();
    this.archiveItems();
  }

  window.model.unarchive = function() {
    var result = this.unarchiveLanguage()
              && this.unarchiveItems();
    return result;
  }

  window.model.archiveLanguage = function() {
    window.localStorage.setItem("model.language", JSON.stringify(this.lang));
  }

  window.model.archiveItems = function() {
    window.localStorage.setItem("model.items", JSON.stringify(this.items));
  }

  window.model.unarchiveLanguage = function() {
    var data = window.localStorage.getItem("model.language");
    if (!data) {
      return false;
    }
    //console.log(data);
    this.lang = JSON.parse(data);
    return true;
  }

  window.model.unarchiveItems = function() {
    var data = window.localStorage.getItem("model.items");
    if (!data) {
      return false;
    }
    var json = JSON.parse(data);

    json.forEach(function(val) {
      model.items.push(new Item(val.id,
        val.image, val.name, val.isGood, val.moreInfo));
    });
    return true;
  }

  model.initLanguage();
  model.initItems();

})();


3. App.js - loads templates and binds controls. Note that it parallelizes loading for the three templates, and each loaded template is then compiled in the same async context because there is no need to involve a window yet. Then the loader awaits on all three to finish, and finally, after the document is ready, renders templates into markup by supplying a model. In this scenario the single app.js handles all template logic, and if needed the more involved code can be moved to separate units, that can be named to match their templates.
(function() {

  var indexTemplate = null;
  var itemsTemplate = null;
  var itemTemplate = null;

  loadTemplatesAndRender();

  function loadTemplatesAndRender() {
    var t1 = $.get("templates/index.hbs", function (data) {
      var compiled = Handlebars.compile(data);
      indexTemplate = compiled;
    }, "html");
    var t2 = $.get("templates/items.hbs", function (data) {
      var compiled = Handlebars.compile(data);
      itemsTemplate = compiled;
    }, "html");
    // Note, the item template is a partial,
    // compiling just in case it’s used as a standalone template.
    var t3 = $.get("templates/item.hbs", function (data) {
      Handlebars.registerPartial("item", data);
      var compiled = Handlebars.compile(data);
      //console.log(compiled);
      itemTemplate = compiled;
    }, "html");
    // Wait for templates to finish loading.
    $.when(t1, t2, t3).done(function() {
      // Also wait for $(document).ready().
      $(function() {
        //alert("ALL DONE");
        renderPage();
        renderItems();
      })
    })
  }

  function renderPage() {
    var compiled = indexTemplate;
    // language is set by the initModel.
    var rendered = compiled(model.languages[model.lang]);
    $("#main").html(rendered);
  }

  function renderItems() {
    var compiled = itemsTemplate;
    // items and a language are set by the initModel.
    var rendered = compiled({
      items: model.items,
      language: model.languages[model.lang]
    });
    $("#theItems").html(rendered);
    attachButtons();
  }

  function attachButtons() {
    $(".button-one").click(function() {
      var id = $(this).closest(".item-card").data("item-id");
      model.chooseItem(id);
      renderItems();
    });

    $(".button-two").click(function() {
      var id = $(this).closest(".item-card").data("item-id");
      model.clearItem(id);
      renderItems();
    });
  }

})();


4. Index.hbs - the top-level template. The binding tags will be replaced by texts from the model.languages[lang] dictionary. And “theItems” element will host renderings of the deeper levels of templates.
<div class="header">
  <div class="title">
      <h1>{{siteTitle}}</h1>
  </div>
  <div>
    <nav class="navigation">
      <a href="?option=page1">{{page1Name}}</a>
      <a href="?option=page2">{{page2Name}}</a>
      <a href="?option=page3">{{page3Name}}</a>
    </nav>
  </div>
</div>
<div id="languageSwitch">
  <nav class="navigation">
    <a href="?lang=0">{{lang1Name}}</a>
    <a href="?lang=1">{{lang2Name}}</a>
  </nav>
</div>
<div class="content">
  <div id="theItems">
  </div>
  <footer class="footer">
  </footer>
</div>


5. Items.hbs - a template-iterator to render all items from the model.items array. It is using a partial to render individual items. In this case the partial is hard-coded and accepts a language object from the root template. For a conditional template selection a helper function can be used (as described in prior post).
{{#each items}}
  {{> item language=@root.language}}
{{else}}
  {{language.noItemsFound}}
{{/each}}


6. Item.hbs - a final template to render an individual item’s card. It has a simple markup to present each item’s name, picture, additional info, and two buttons. The placeholders refer to properties of an item. Handlebars will assign the source item as a context to this template.
<div class="item-card" data-item-id="{{id}}">
  <div class="card-title" style="background: url('images/{{image}}') center 15% no-repeat #46B6AC;">
      <h2>{{name}}</h2>
  </div>
  <div class="card-border">
    <button class="button-one">
      {{language.one}}
    </button>
    <button class="button-two">
      {{language.two}}
    </button>
  </div>
  {{#if moreInfo}}
    {{#if isGood}}
      <span class="result-good">{{language.good}}</span>
    {{else}}
      <span class="result-bad">{{language.bad}}</span>
    {{/if}}
  {{/if}}
</div>



Monday, November 2, 2015

Handlebars Essentials

Handlebars is a good little helper library for client-side Javascript development. It allows creating templates MVC-style in an HTML page and binding to data sources.
Here are my Handlebars code bits as a reference.

1. A page with a target element.
<!DOCTYPE html>
<html>
<head>
    <script src="lib/jquery/jquery-2.1.4.min.js"></script>
    <script src="lib/handlebars/handlebars-v4.0.4.js"></script>
    <script src="lib/material-design-lite/material.min.js"></script>
    <script src=“js/MV.js"></script>
    <script src="js/app.js"></script>
</head>

<body>
    <div id="main" class=“mail-class">
    </div>
</body>
</html>


2. A page template. This is the main container of all visible elements.
<script type="text/x-handlebars-template" id="index-template">
  <div class="header">
    <div class="title">
        <h1>{{siteTitle}}</h1>
    </div>
    <div>
      <nav class="navigation">
        <a href="?option=page1">{{page1Name}}</a>
        <a href="?option=page2">{{page2Name}}</a>
        <a href="?option=page3">{{page3Name}}</a>
      </nav>
    </div>
  </div>
  <div class="content">
    <div id="theItems">
    </div>
    <footer class="footer">
    </footer>
  </div>
</script>

3. Partial templates (partials). These simplify code structure by allowing to refactor templates as components into separate source files.
<script type="text/x-handlebars-template" id="item-template">
  <div class="item-card” data-item-id="{{id}}">
    <div class="card-title" style="background: url('images/{{image}}') center 15% no-repeat #46B6AC;">
        <h2>{{name}}</h2>
    </div>
    <div class="card-border">
      <button class="button-one">
        {{language.one}}
      </button>
      <button class="button-two">
        {{language.two}}
      </button>
    </div>
    {{#if moreInfo}}
      {{#if isCorrect}}
        <span class="result-good">{{language.correct}}</span>
      {{else}}
        <span class="result-bad">{{language.incorrect}}</span>
      {{/if}}
    {{/if}}
  </div>
</script>

4. A template-iterator. This implements iteration through items and also references a partial template for rendering an individual item.
<script type="text/x-handlebars-template" id="items-template">
  {{#each items}}
    {{> item language=@root.language}}
  {{else}}
    {{language.noItemsFoundMessage}}
  {{/each}}
</script>

5. A script (app.js) to register partials and to render a page. Also binds page elements to event handlers.
(function() {

  $(function () {
    registerPartials();
    renderPage();
    renderItems();
  });

  function registerPartials() {
    Handlebars.registerPartial("item", $("#item-template").html());
  }

  function renderPage() {
    var template = $("#index-template").html();
    var compiled = Handlebars.compile(template);
    var rendered = compiled(window.language);
    $("#main").html(rendered);
    $("#languageSwitch").click(function() {
      MV.switchLanguage();
    });
  }

  function renderItems() {
    var template = $("#items-template").html();
    var compiled = Handlebars.compile(template);
    var rendered = compiled({ items: MV.items, language: window.language });
    $("#theItems").html(rendered);
    attachItemsButtons();
  }

  function attachItemsButtons() {
    $(".button-one").click(function() {
      var id = $(this).closest(".item-card").data("item-id");
      MV.itemActionOne(id);
      renderItems();
    });

    $(".button-two").click(function() {
      var id = $(this).closest(".item-card").data("item-id");
      MV.itemActionTwo(id);
      renderItems();
    });
  }

})();

6. Helpers. These are global functions that can be invoked out of any context. “this” will refer to a current context.
For example, in helpers.js:
Handlebars.registerHelper("getLanguageFilter", function(langId) {
  var queryParam = "";
  if (langId) {
    queryParam = "&language=" + Handlebars.escapeExpression(langId);
  }
  return new Handlebars.SafeString(queryParam);
});

Handlebars.registerHelper("generatePages", function(items) {
  var pages = [];
  var pageCount = Math.ceil(items.length / 10);
  for (var i = 1; i <= pageCount; i++) {
    var link = “?page=" + i;
    pages.push({
      number: i,
      link: link
    });
  }
  return pages;
});

Then, in a page or in a template:
<!-- Navigation -->
<script type="text/x-handlebars-template" id=“navigation-template">
  <div>
      <nav class="mdl-navigation">
          <a href="?filter=ones{{getLanguageFilter langId}}">{{itemsFilterOne}}</a>
          <a href="?filter=twos{{getLanguageFilter langId}}">{{itemsFilterTwo}}</a>
      </nav>
  </div>
</script>
...
<script type="text/x-handlebars-template" id="page-template">
  <ul>
    {{#each (generatePages items)}}
      <li><a href="{{link}}">{{number}}</a></li>
    {{/each}}
  </ul>
</script>