Friday, August 19, 2016

It goes without saying that I
 only covered a subset of what is new
 in Xcode 8. The next major release
 of Xcode is always something
 I look forward to.
Developers spend so much time
 in their code editor and it is therefore
 understandable that they care about
its evolution, both good and bad.
What is your favorite feature of
 Xcode 8? Share it in the
 comments.

Privacy and URL Schemes in iOS 9

As of iOS 9, third party applications are no longer able to query arbitrary URL schemes. Several third party applications, most notably Twitter, misused the canOpenURL(_:) method of the UIApplication class to track which applications are installed on a particular device.

Apple has put a number of restrictions in place to protect the privacy of its customers. In this quick tip, I tell you what you need to know about these changes and I show you how you can update your applications.

1. Querying URL Schemes

I assume you already know that an iOS application can ask the operating system to launch another application using a URL scheme. In its simplest form, it works something like this.
  1. let URLAsString = "tweetbot://_bartjacobs/timeline"
  2.  
  3. if let URL = NSURL.init(string: URLAsString) {
  4.     UIApplication.sharedApplication().openURL(URL)
  5. }
At times, it is useful to first ask the operating system whether a URL can be opened before opening it. This is especially useful if you want to update the user interface based on which applications the user has installed and your application can interact with. In the above example, openURL(_:) won't succeed if Tweetbot isn't installed on the user's device. To ask the operating system whether a URL can be opened, you can make use of the canOpenURL(_:) method of the UIApplication class.
  1. let URLAsString = "tweetbot://_bartjacobs/timeline"
  2.  
  3. if let URL = NSURL.init(string: URLAsString) {
  4.     if UIApplication.sharedApplication().canOpenURL(URL) {
  5.         UIApplication.sharedApplication().openURL(URL)
  6.     } else {
  7.         print("Cannot Open URL")
  8.     }
  9. }
Unfortunately, some applications, most notably Twitter, have been misusing canOpenURL(_:) to detect which applications are installed on the user's device. According to Apple, this breaches the user's privacy. As a result, Apple no longer tolerates this type of misuse in iOS 9 by imposing two restrictions.
  • Applications built against the iOS 9 SDK are forced to whitelist the URL schemes they would like to query. In other words, a call to canOpenURL(_:) fails if the URL isn't added to a whitelist in the application's Info.plist.
  • Applications not built against the iOS 9 SDK continue to work as expected. There is one limitation, though. An application can only query 50 distinct URL schemes. Subsequent requests return false and throw an error. The documentation emphasizes that this limitation is reset when the user reinstalls or upgrades the application.
2. Project Setup

Let me show you what this means in practice by creating a simple application that opens Tweetbot, my favorite Twitter client. Create a new project in Xcode 7 and choose the Single View Application template. Name the project Schemes and set Language to Swift.



Before we take a look at URL schemes, I want to set up the user interface. Open ViewController.swift and add an action, openTweetbot(_:), to the ViewController class. You can leave its implementation empty for now.
  1. // MARK: - Actions
  2.  
  3. @IBAction func openTweetbot(sender: AnyObject) {
  4.  
  5. }
Open Main.storyboard and add a button to the View Controller Scene. Set the title of the button to Open Tweetbot and connect the button with the view controller's openTweetbot(_:) action we created earlier.

We are not going to make anything too complicated. When I tap the Open Tweetbot button, the operating system opens Tweetbot, showing me my timeline. You can read more about the URL schemes for Tweetbot on the Tapbots website.

3. Before iOS 9

Before Apple imposed the aforementioned restrictions on querying URL schemes, you could do the following:
  1. @IBAction func openTweetbot(sender: AnyObject) {
  2.     let application = UIApplication.sharedApplication()
  3.     let URLAsString = "tweetbot://_bartjacobs/timeline"
  4.  
  5.     guard let URL = NSURL.init(string: URLAsString) else { return }
  6.     guard application.canOpenURL(URL) else { return }
  7.  
  8.     // Open URL
  9.     application.openURL(URL)
  10. }
We ask the operating system whether it can open the URL we pass to canOpenURL(_:). If this method returns false on iOS 8 and lower, we know the application we are interested in is not installed on the user's device, assuming the URL scheme relates to another application. This can be very useful, but Apple wants to put some restrictions on the API to avoid abuse.

4. iOS 9

If you build the application in Xcode 7 and tap the Open Tweetbot button, canOpenURL(_:) returns false and the operating system throws an error that looks something like this:
  1. Schemes[9227:3539016] -canOpenURL: failed for URL: "tweetbot://_bartjacobs/timeline" - error: "This app is not allowed to query for scheme tweetbot"
The operating system explicitly informs us that the application is not allowed to know whether it can open the URL we pass to canOpenURL(_:). It doesn't mean that the application is not allowed to open the URL we passed to openURL(_:), though.

If you update the implementation of openTweetbot(_:) as shown below and you have Tweetbot installed, the application is able to open Tweetbot when the button is tapped. This emphasizes that Apple wants to limit the (mis)use of canOpenURL(_:), not openURL(_:).
  1. @IBAction func openTweetbot(sender: AnyObject) {
  2.     let application = UIApplication.sharedApplication()
  3.     let URLAsString = "tweetbot://_bartjacobs/timeline"
  4.  
  5.     guard let URL = NSURL.init(string: URLAsString) else { return }
  6.  
  7.     // Open URL
  8.     application.openURL(URL)
  9. }
5. Whitelisting URL Schemes

Fortunately, there is an easy solution to this problem. As of iOS 9, Apple asks developers to whitelist the URL schemes an application would like to query. This is as simple as adding an entry to the application's Info.plist.

Open Info.plist, add a key named LSApplicationQueriesSchemes, and set the type of the value to Array. Add an item of type String to the array and set its value to tweetbot.


If we revert openTweetbot(_:) to its original implementation, we are able to invoke canOpenURL(_:) without the operating system throwing errors at us. You can add as many URL schemes to your application's Info.plist.
  1. @IBAction func openTweetbot(sender: AnyObject) {
  2.     let application = UIApplication.sharedApplication()
  3.     let URLAsString = "tweetbot://_bartjacobs/timeline"
  4.  
  5.     guard let URL = NSURL.init(string: URLAsString) else { return }
  6.     guard application.canOpenURL(URL) else { return }
  7.  
  8.     // Open URL
  9.     application.openURL(URL)
  10. }
Conclusion

Most applications won't have a problem with Apple's new policy. It is clear Apple aims to protect the privacy of its customers by limiting the information applications can extract from the operating system. Applications are sandboxed on iOS and Apple wants to control how much information an application can extract from the environment the sandbox lives in.

Whitelisting URL schemes isn't a big pain for most applications, but it can become tedious if you plan to create an application launcher, such as Launch Center Pro. It is still possible to create a launcher as long as you whitelist every URL scheme the application would like to query. This can be pretty tedious, though.
Written by Bart Jacobs

If you found this post interesting, follow and support us.
Suggest for you:

iOS 10 Projects: Build Amazing Apps with Apple's Newest iOS

iOS 10 & Swift 3: From Beginner to Paid Professional

Complete Beginners Guide to iOS Development - Build 10 Apps

The Complete iOS 10 Developer - Build Real Apps with Swift 3

Swift 3 and iOS 10 The Final Course Learn to Code like a Pro

Tuesday, August 16, 2016

Build an iOS app with Push Notifications using Ionic Framework_part 2 (end)


News

In app.js we have set up already two routes for the news section: tab.news and tab.details. On the first one we will see all news. Tapping on each of them will open view page with the news details.

News list
  1. <!-- www/templates/tab-news.html -->
  2. <ion-view view-title="Good News">
  3.   <ion-content>
  4.     <ion-refresher
  5.         pulling-text="Pull to refresh..."
  6.         on-refresh="refresh()">
  7.     </ion-refresher>
  8.     <ul class="list">
  9.       <li class="item" ng-repeat="n in news" ui-sref="tab.details({id: n._id})">
  10.         {{n.text}}
  11.       </li>
  12.     </ul>
  13.   </ion-content>
  14. </ion-view>
To refresh news list we have used pull to refresh, available in the Ionic and typical for native apps.
  1. // www/js/controllers.js
  2. ...
  3. .controller('NewsCtrl', function ($scope, NewsService, $ionicLoading) {
  4.   $ionicLoading.show({
  5.     template: 'Loading...'
  6.   });
  7.   NewsService.all().then(function (news) {
  8.     $scope.news = news;
  9.     $ionicLoading.hide();
  10.   });

  11.   $scope.refresh = function () {
  12.     NewsService.all().then(function (news) {
  13.       $scope.news = news;
  14.       $scope.$broadcast('scroll.refreshComplete');
  15.     });
  16.   };
  17. })
  18. ...
Details page
  1. <!-- www/templates/details.html -->
  2. <ion-view view-title="Good News">
  3.   <ion-content>
  4.     <div class="card">
  5.       <div class="item item-divider">
  6.         {{news.createdAt | date : fullDate}}
  7.       </div>
  8.       <div class="item item-text-wrap">
  9.         {{news.text}}
  10.       </div>
  11.       <div class="item item-divider">
  12.         by {{news.username}}
  13.       </div>
  14.     </div>
  15.   </ion-content>
  16. </ion-view>
  1. // www/js/controllers.js
  2. ...
  3. .controller('DetailsCtrl', function ($scope, $state, NewsService, 
  4.                                      $ionicLoading) {
  5.   $ionicLoading.show({
  6.     template: 'Loading...'
  7.   });
  8.   var id = $state.params.id;
  9.   NewsService.one(id).then(function (news) {
  10.     $scope.news = news;
  11.     $ionicLoading.hide();
  12.   });
  13. })
  14. ...
Let's create dedicated service for backend requests
  1. // www/js/newsService.js
  2. (function () {
  3.   function _NewsService($q, config, $http) {

  4.     function getOne(id) {
  5.       var deferred = $q.defer();

  6.       $http.get(config.server + '/news/' + id)
  7.         .success(function (data) {
  8.           if (data.error || !data.news) {
  9.             deferred.reject(data.error);
  10.           }
  11.           deferred.resolve(data.news);
  12.         })
  13.         .error(function () {
  14.           deferred.reject('error');
  15.         });
  16.         return deferred.promise;
  17.       }
  18.       function getAll() {
  19.         var deferred = $q.defer();

  20.         $http.get(config.server + '/news')
  21.           .success(function (data) {
  22.             if (data.error || !data.news) {
  23.               deferred.reject(data.error);
  24.             }
  25.             deferred.resolve(data.news);
  26.           })
  27.           .error(function () {
  28.             deferred.reject('error');
  29.           });
  30.           return deferred.promise;
  31.       }
  32.       return {
  33.         one: getOne,
  34.         all: getAll
  35.       };
  36.   }
  37.   _NewsService.$inject = ['$q', 'Config', '$http'];

  38.   angular.module('app.services')
  39.     .factory('NewsService', _NewsService);
  40. })();
  41. Server side (news)
We need a simple form for creating news on the server
  1. <!-- adminpanel/add-news.html -->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.   <meta charset="utf-8">
  6.   <title>Add News</title>
  7. </head>
  8. <body>
  9.   <form action="/news" method="post">
  10.     <h2>Add good news</h2>
  11.     <p>
  12.       <textarea name="text" cols="50" rows="10" 
  13.                 placeholder="News body"></textarea>
  14.     </p>
  15.     <p>
  16.       <input type="text" required name="username" 
  17.              placeholder="Username"/>
  18.     </p>
  19.     <p>
  20.       <button type="submit">Add news</button>
  21.     </p>
  22.   </form>
  23. </body>
  24. </html>
express.static middleware
  1. // server.js
  2.  ...
  3. app.use(express.static(__dirname + '/adminpanel'));
  4. app.get('/add-news', function (req, res) {
  5.   res.sendFile(__dirname + '/adminpanel/add-news.html');
  6. });
  7.  ...

Now we have to describe news model.
  1. // models/news.js
  2. var mongoose = require('mongoose');
  3. var Schema = mongoose.Schema;

  4. module.exports = function () {
  5.   'use strict';

  6.   var NewsSchema = new Schema({
  7.     username: String,
  8.     text: String,
  9.     createdAt: {type: Date, default: Date.now}
  10.   });

  11.   mongoose.model('News', NewsSchema, 'News');
  12. };
On receiving a POST request we should create a new record in db and send push notifications with created news to all our users.
  1. // routes/news.js
  2. var mongoose = require('mongoose');
  3. var News = mongoose.model('News');
  4. var Users = mongoose.model('Users');
  5. var apn = require('apn');
  6. var _ = require('lodash');

  7. module.exports = function (app) {
  8.   'use strict';

  9.   var router = app.get('router');

  10.   router.get('/news', function (req, res) {
  11.     News.find().sort({createdAt: -1}).lean().exec(function (err, news) {
  12.       if (err) {
  13.         return res.json({error: err});
  14.       }

  15.       res.json({error: null, news: news});
  16.     });
  17.   });

  18.   router.get('/news/:id', function (req, res) {
  19.     var id = req.params.id;
  20.     News.findById(id).lean().exec(function (err, news) {
  21.       if (err) {
  22.         return res.json({error: err});
  23.       }

  24.       res.json({error: null, news: news});
  25.     });
  26.   });

  27.   router.post('/news', function (req, res) {
  28.     var username = req.body.username;
  29.     var text = req.body.text;

  30.     var news = new News({
  31.       username: username,
  32.       text: text
  33.     });

  34.     news.save(function (err, news) {
  35.       process.nextTick(function () {
  36.         sendPush(news);
  37.       });
  38.       res.redirect('/add-news');
  39.     });
  40.   });

  41.   function sendPush(news) {
  42.     var text = news.text.substr(0, 100);
  43.     Users.find({deviceRegistered: true}).lean().exec(function (err, users) {
  44.       if (!err) {
  45.         for (var i = 0; i < users.length; i++) {
  46.           var user = users[i];

  47.           var device = new apn.Device(user.deviceToken);
  48.           var note = new apn.Notification();
  49.           note.badge = 1;
  50.           note.contentAvailable = 1;
  51.           note.alert = {
  52.             body : text
  53.           };
  54.           note.device = device;

  55.           var options = {
  56.             gateway: 'gateway.sandbox.push.apple.com',
  57.             errorCallback: function(error){
  58.               console.log('push error', error);
  59.             },
  60.             cert: 'PushNewsCert.pem',
  61.             key:  'PushNewsKey.pem',
  62.             passphrase: 'superpass',
  63.             port: 2195,
  64.             enhanced: true,
  65.             cacheLength: 100
  66.           };
  67.           var apnsConnection = new apn.Connection(options);
  68.           console.log('push sent to ', user.username);
  69.           apnsConnection.sendNotification(note);
  70.         }
  71.       }
  72.     });
  73.   }
  74. };
We have used apn module to send push notifications. Also we used PushNewsCert.pem and PushNewsKey.pem files, we will create them in the next step.
Push notification will be sent, but how app will know about it? We must add an event listener to the app.js
  1. // www/js/app.js
  2. ...
  3. .run(function ($ionicPlatform, $rootScope) {
  4.   $ionicPlatform.ready(function () {
  5.     $rootScope.$on(
  6.       '$cordovaPush:notificationReceived', 
  7.       function (event, notification) {
  8.         if (notification.alert) {
  9.           navigator.notification.alert(notification.alert);
  10.         }
  11.       });
  12.   });
  13. })
  14. ...
Provisioning Profile and Certificates

For using push notifications in our app, we have to prepare the App ID, SSL Certificate and create Provisioning Profile in iOS Dev Center.

Create Certificate

Open Keychain Access on your mac and choose Request a Certificate from a Certificate Authority...

Enter you email, common name, check Saved to disk. Save file as PushNews.certSigningRequest

Now we can find new private key in keychain, let's export it as PushNewsKey.p12. Enter secure passphrase when it will prompt.


Log in to the iOS Dev Center and select the Certificates, Identifiers and Profiles from the right panel.

Select Identifiers in the iOS Apps section.

Go to App IDs in the sidebar and click the "+" button.

Enter App ID registration data:
  • App ID Description / Name - PushNews
  • Explicit App ID / Bundle ID - com.your_domain.PushNews (this ID and cordova app name should be the same)
  • App Services / Enable Services - Push Notifications
Press Continue and Submit.
Now we have to set up our app. Select PushNews in Apps IDs list and press Edit in the bottom.
Find Push Notifications section and press Create Certificate... in the Development SSL Certificate section.

Press Continue and Choose PushNews.certSigningRequest created early, press Generate

Download generated certificate and save it as aps_development.cer

Making PEM files

Go to the directory where you've saved files and run these commands in Terminal for converting:

.cer file into a .pem
  1. $ openssl x509 -in aps_development.cer -inform der -out PushNewsCert.pem
.p12 into a .pem
  1. $ openssl pkcs12 -nocerts -in PushNewsKey.p12 -out PushNewsKey.pem
You will be asked to enter your passphrase for .p12 file and new pass for .pem file. Copy both .pem files to the app's root directory.

Making the Provisioning Profile

Let's return to iOS Dev Center. Click the Provisioning Profiles/Development in the sidebar and click the "+" button.
  • Choose iOS App Development
  • Select PushNews App in App ID list
  • Select your certificate
  • Select devices you want to include in this provisioning profile
  • Enter Profile Name (I will use "PushNews Development")
Press Generate and download profile when it will be created, we will use it later.


Let's change cordova id in config.xml (by default it will be something like com.ionicframework.PushNews456803) to Explicit App ID / Bundle ID value you have entered above (com.your_domain.PushNews).
  1. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  2. <widget id="com.telnov.PushNews" version="0.0.1" xmlns="http://www.w3.org/ns/widgets"
  3.         xmlns:cdv="http://cordova.apache.org/ns/1.0">
  4.     <name>pushNews</name>
  5.     ...
Test on device

OK, now you can check result. To launch app on iOS device, we have to add ios platform first:
  1. $ ionic platform add ios
  2. $ ionic build ios
  • Open platforms/ios/pushNews.xcodeproj in the Xcode
  • Connect your iOS device via usb
  • Switch to the Build Settings tab
  • Select your Provisioning Profile
  • Select your device
  • Click run

You will see login page. After tapping the Login with Twitter button, you will see twitter auth page.





At the first time, when you are trying to launch any application that use push notifications, you should give an access, so after you entering twitter credentials, you will see popup


Try adding news and enjoy the results.


Conclusion

We have created the project, which demonstrates how easy you can build iOS applications using web technologies only.
Written by Oleksandr Telnov

If you found this post interesting, follow and support us.
Suggest for you:

Monday, August 15, 2016

Build an iOS app with Push Notifications using Ionic Framework_part 1


Introduction

Hybrid apps are gaining popularity. The Gartner research firm predicts that by 2016, hybrid apps will constitute the majority of mobile apps. So it is good time to learn some modern technologies for building hybrid apps.

In this tutorial we are going to see how it is possible to build Cordova application with push notifications based on the Ionic Framework and ngCordova created by the Ionic team.

Ionic uses AngularJS, so it is assumed you are familiar with it.

What are we building in this tutorial?

Unfortunately, there are a lot of bad things happening every day. So we are going to create an iOS application to show some good news. We will also create own server for sending push notifications using apn, based on Node.js and MongoDB with a simple form to create a new post.

The name of the application will be PushNews, but of course you can use your own.


Prerequisites
  • Mac OS X
  • iOS Developer Program membership
  • Node.js/npm installed on your mac
  • MongoDB
For deploying we will use Heroku and Mongolab, so you need to have accounts there or you can use your own hosting instead.

Install Ionic

Now we should install/update CLI for Cordova and Ionic, to have an opportunity to create new apps, add cordova plugins, etc.

Run this command in your Terminal:
  1. $ npm install -g ionic cordova
App Development

Creating Ionic app

For creating and serving basic Ionic app with tabs starter template, run
  1. $ ionic start pushNews tabs
  2. $ cd pushNews
  3. $ ionic serve

Installing additional libs and setting up routes
  1. $ bower install --save ngCordova angular-local-storage
Include ng-cordova.min.js and angular-local-storage.min.js in the index.html
  1. <!-- index.html -->
  2. ...
  3. <script src="lib/angular-local-storage/dist/angular-local-storage.min.js"></script>
  4. <script src="lib/ngCordova/dist/ng-cordova.min.js"></script>
  5. <script src="cordova.js"></script>
  6. ...
Inject as an Angular dependency:
  1. // www/js/app.js
  2. ...
  3. angular.module('app', ['ngCordova', 'LocalStorageModule'])
  4. ...
We need to change routes now. There will be four of them:
  • Login page
  • All news list
  • News view page
  • User profile page
  1. // www/js/app.js
  2. ...
  3. .config(function ($stateProvider, $urlRouterProvider) {
  4.   $urlRouterProvider.otherwise('/login');
  5.   $stateProvider
  6.     .state('login', {
  7.       url: '/login',
  8.       templateUrl: 'templates/login.html',
  9.       controller: 'LoginCtrl'
  10.     })
  11.     .state('tab', {
  12.       abstract: true,
  13.       templateUrl: "templates/tabs.html"
  14.     })
  15.     .state('tab.news', {
  16.       url: '/news',
  17.       views: {
  18.         'tab-news': {
  19.           templateUrl: 'templates/tab-news.html',
  20.           controller: 'NewsCtrl'
  21.         }
  22.       }
  23.     })
  24.     .state('tab.details', {
  25.       url: '/news/:id',
  26.       views: {
  27.         'tab-news': {
  28.           templateUrl: 'templates/details.html',
  29.           controller: 'DetailsCtrl'
  30.         }
  31.       }
  32.     })
  33.     .state('tab.profile', {
  34.       url: '/profile',
  35.       views: {
  36.         'tab-profile': {
  37.           templateUrl: 'templates/tab-profile.html',
  38.           controller: 'ProfileCtrl'
  39.         }
  40.       }
  41.     });
  42. });
Building the Server
  Let's create Express server. I will use Heroku to host it. Follow official Getting Started if you are not familar with Heroku. As an alternative you can use for development your localhost with ngrok.
  1. // server.js
  2. var express = require('express');
  3. var app = express();
  4. var bodyParser = require('body-parser');

  5. require('./config')(app);
  6. require('./models')(app);

  7. var config = app.get('config');

  8. app.use(function (req, res, next) {
  9.   res.header('Access-Control-Allow-Credentials', true);
  10.   res.header('Access-Control-Allow-Origin', req.headers.origin);
  11.   res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
  12.   res.header(
  13.     'Access-Control-Allow-Headers', 
  14.     'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'
  15.   );
  16.   if ('OPTIONS' === req.method) {
  17.     res.status(200).end();
  18.   } else {
  19.     next();
  20.   }
  21. });

  22. app.use(bodyParser.json());
  23. app.use(bodyParser.urlencoded({
  24.   extended: true
  25. }));

  26. var router = express.Router();
  27. app.set('router', router);
  28. app.use(router);

  29. var http = require('http');
  30. http.createServer(app)
  31.   .listen(config.PORT, function () {
  32.     console.log('app start on port ' + config.PORT);
  33.   });

  34. require('./routes')(app);
Models and routes we will describe later.

Authorization and Push Notifications

There are multiple ways to manage auth process in Ionic apps. We will use the next one: user login with twitter, then we store returned data in localStorage and remove it on logout. If user exists in localStorage he is logged in, if not, we will redirect him to login page. Don't forget to add CSRF or other protection for the real app.

Login with Twitter

To use twitter we must also add sha1 library:
  1. $ bower install --save jsSHA
  1. <!-- www/index.html -->
  2. ...
  3. <script src="lib/jsSHA/src/sha1.js"></script>
  4. ...
Install inAppBrowser plugin:
  1. $ cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-inappbrowser.git
Also you have to create your own twitter app on Twitter App Management to get keys.

A little earlier we have set login.html as template for the login state, let's create it:
  1. <!-- www/templates/login.html -->
  2. <div class="row row-center">
  3.   <div class="col">
  4.     <h2 class="text-center">Good News</h2>

  5.     <div class="padding-top">
  6.       <button class="button button-block button-calm" ng-click="twitter()">
  7.         <i class="icon ion-social-twitter"></i>&nbsp;Login with Twitter
  8.       </button>
  9.     </div>
  10.   </div>
  11. </div>
Login Controller:

It should be noticed, that any cordova plugin call should be wraped with the deviceready event. Or use $ionicPlatform.ready() available in Ionic.
  1. // www/js/controllers.js
  2. ...
  3. .controller('LoginCtrl', function ($scope, $state, $cordovaOauth, 
  4.                                    UserService, Config, $ionicPlatform,
  5.                                    $ionicLoading, $cordovaPush) {
  6.   if (UserService.current()) {
  7.     $state.go('tab.news');
  8.   }
  9.   $scope.twitter = function () {
  10.     $ionicPlatform.ready(function () {
  11.       $cordovaOauth.twitter(Config.twitterKey, Config.twitterSecret)
  12.         .then(function (result) {
  13.           $ionicLoading.show({
  14.             template: 'Loading...'
  15.           });
  16.           UserService.login(result).then(function (user) {
  17.             if (user.deviceToken) {
  18.               $ionicLoading.hide();
  19.               $state.go('tab.news');
  20.               return;
  21.             }

  22.             $ionicPlatform.ready(function () {
  23.               $cordovaPush.register({
  24.                 badge: true,
  25.                 sound: true,
  26.                 alert: true
  27.               }).then(function (result) {
  28.                 UserService.registerDevice({
  29.                   user: user, 
  30.                   token: result
  31.                 }).then(function () {
  32.                   $ionicLoading.hide();
  33.                   $state.go('tab.news');
  34.                 }, function (err) {
  35.                   console.log(err);
  36.                 });
  37.               }, function (err) {
  38.                 console.log('reg device error', err);
  39.               });
  40.             });
  41.           });
  42.         }, function (error) {
  43.           console.log('error', error);
  44.         });
  45.     });
  46.   };
  47. })
  48. ...
We have been using $cordovaOauth.twitter() to allow user to login with twitter in our app. This plugin will do for us all rough work, but you should check sources to understand that there is no server on the iPhone and we can't set up callback to redirect twitter back to our app like we could do this on the web. So we should open a new window, parce a token and close the window manually.
To send push notification to our users, we must know device token. We will receive it using $cordovaPush.register() function. Cordova plugin should be installed:
  1. $ cordova plugin add https://github.com/phonegap-build/PushPlugin.git
After getting user data, we send it to the server. Check this article by Jim Cooper to understand why we shouldn't do this in Controller. Let's create Service.
  1. // www/js/userService.js
  2. (function () {
  3.   function _UserService($q, config, $http, localStorageService, $state) {
  4.     var user;
  5.     function loginUser(post) {
  6.       var deferred = $q.defer();

  7.       $http.post(config.server + '/user/login', post)
  8.         .success(function (data) {
  9.           if (data.error || !data.user) {
  10.             deferred.reject(data.error);
  11.           }
  12.           localStorageService.set('user', data.user);
  13.           user = data.user;

  14.           deferred.resolve(data.user);
  15.         })
  16.         .error(function () {
  17.           deferred.reject('error');
  18.         });

  19.         return deferred.promise;
  20.     }

  21.     function logoutUser() {
  22.       localStorageService.remove('user');
  23.       user = null;
  24.       $state.go('login');
  25.     }

  26.     function currentUser() {
  27.       if (!user) {
  28.         user = localStorageService.get('user');
  29.       }
  30.       return user;
  31.     }

  32.     function registerDevice(putData) {
  33.       var deferred = $q.defer();

  34.       $http.put(config.server + '/user/registerDevice', putData)
  35.         .success(function (data) {
  36.           if (data.error || !data.user) {
  37.             deferred.reject(data.error);
  38.           }

  39.           localStorageService.set('user', data.user);
  40.           user = data.user;

  41.           deferred.resolve(data.user);
  42.         })
  43.         .error(function () {
  44.           deferred.reject('error');
  45.         });

  46.         return deferred.promise;
  47.     }

  48.     return {
  49.       login: loginUser,
  50.       logout: logoutUser,
  51.       current: currentUser,
  52.       registerDevice: registerDevice
  53.     };
  54.   }

  55.   function _ConfigService() {
  56.     return {
  57.       server: 'http://push-news.herokuapp.com',
  58.       twitterKey: 'your_twitter_key',
  59.       twitterSecret: 'your_twitter_secret'
  60.     };
  61.   }

  62.   _UserService.$inject = [
  63.     '$q', 'Config', '$http', 'localStorageService',
  64.     '$state', '$cordovaPush', '$ionicPlatform'
  65.   ];

  66.   angular.module('app.services')
  67.     .factory('UserService', _UserService)
  68.     .service('Config', _ConfigService);
  69. })();
Don't forget to include the new file in the index.html
  1. <!-- index.html -->
  2. ...
  3. <script src="js/userService.js"></script>
  4. ...
Server side (user)

We should add user model and routes for /user/login and /user/registerDevice on our server
  1. // models/users.js 
  2. var mongoose = require('mongoose');
  3. var Schema = mongoose.Schema;

  4. module.exports = function () {
  5.   'use strict';

  6.   var UsersSchema = new Schema({
  7.     socialId: {type: String, index: true, unique: true},
  8.     username: {type: String, unique: true},
  9.     deviceToken: {type: String, unique: true},
  10.     deviceRegistered: {type: Boolean, default: false},
  11.     createdAt: {type: Date, default: Date.now}
  12.   });

  13.   mongoose.model('Users', UsersSchema, 'Users');
  14. };
-----------------------------------------------------------------------------
  1. // models/users.js 
  2. var mongoose = require('mongoose');
  3. var Schema = mongoose.Schema;

  4. module.exports = function () {
  5.   'use strict';

  6.   var UsersSchema = new Schema({
  7.     socialId: {type: String, index: true, unique: true},
  8.     username: {type: String, unique: true},
  9.     deviceToken: {type: String, unique: true},
  10.     deviceRegistered: {type: Boolean, default: false},
  11.     createdAt: {type: Date, default: Date.now}
  12.   });

  13.   mongoose.model('Users', UsersSchema, 'Users');
  14. };
---------------------------------------------------------------------------
  1. // routes/user.js
  2. var mongoose = require('mongoose');
  3. var Users = mongoose.model('Users');

  4. module.exports = function (app) {
  5.   'use strict';

  6.   var router = app.get('router');

  7.   router.post('/user/login', function (req, res) {
  8.     var socialId = req.body.user_id;
  9.     var username = req.body.screen_name;

  10.     Users.findOne({socialId: socialId}).lean().exec(function (err, user) {
  11.       if (err) {
  12.         return res.json({error: err});
  13.       }

  14.       if (user) {
  15.         return res.json({error: null, user: user});
  16.       }

  17.       var newUser = new Users({
  18.         socialId: socialId,
  19.         username: username
  20.       });

  21.       newUser.save(function (err, user) {
  22.         if (err) {
  23.           return res.json({error: err});
  24.         }

  25.         res.json({user: user, error: null});
  26.       });
  27.     });
  28.   });

  29.   router.put('/user/registerDevice', function (req, res) {
  30.     var user = req.body.user;
  31.     var token = req.body.token;

  32.     Users.findByIdAndUpdate(user._id, {
  33.       $set: {
  34.         deviceToken: token,
  35.         deviceRegistered: true
  36.       }
  37.     }).lean().exec(function (err, user) {
  38.       if (err || !user) {
  39.         return res.json({error: err});
  40.       }

  41.       res.json({error: null, user: user});
  42.     });
  43.   });
  44. };
If you found this post interesting, follow and support us.
Suggest for you:

Saturday, August 13, 2016

Taking Control of the tvOS Focus Engine



Introduction

On iOS, users normally interact with your apps via the device's touch screen. On tvOS, however, user interaction is handled by moving the current focus between views on the screen.

Luckily, the tvOS implementations of the UIKit APIs handle the changing of focus between views automatically. While this built-in system works very well, for specific view layouts and/or purposes, it may be necessary to sometimes manually control the focus engine.

In this tutorial, we take an in-depth look at the tvOS focus engine. You learn how it works and how to control it however you want to.

Prerequisites

This tutorial requires that you are running Xcode 7.3 or higher with the latest tvOS 9.2 SDK. If you want to follow along, you also need to download the starter project from GitHub.

1. Focus Engine Overview

The purpose of the focus engine of tvOS is to help developers concentrate on their own app's unique content rather than reimplementing basic navigation behaviors. This means that, while many users will use Apple TV's Siri Remote, the focus engine automatically supports all current and future Apple TV input devices.

This means that, as a developer, you don't have to worry about how a user is interacting with your app. Another important goal of the focus engine is to create a consistent user experience between applications. Because of this, there is no API that allows an application to move the focus.

Focus Movement
When the user interacts with the remote of the Apple TV by swiping on the glass Touch surface in a particular direction, the focus engine looks for a possible focusable view in that direction and, if found, moves the focus to that view. If no focusable view is found, the focus remains where it currently is.

In addition to moving the focus in a particular direction, the focus engine also handles several other, more advanced behaviors, such as:
  • moving the focus past particular views if, for example, the user swipes fast on the Touch surface of the Apple TV remote
  • running animations at speeds based on the velocity of the focus change
  • playing navigation sounds when the focus changes
  • animating scroll view offsets automatically when the focus needs to move to a currently off-screen view
When determining where the focus should move to in an app, the focus engine takes an internal picture of your app's current interface and highlights all of the visible elements that are focusable. This means that any hidden views, including views with an alpha value of 0, cannot be focused. This also means that, for any view that is hidden by another view, only the visible part is considered by the focus engine.

If the focus engine finds a view it can move the focus to, it notifies the objects conforming to the UIFocusEnvironment protocol that are involved with the change. The UIKit classes that conform to the UIFocusEnvironmen protocol are UIWindowUIViewControllerUIView, and UIPresentationController. The focus engine calls the shouldUpdateFocusInContext(_:) method of all the focus environment objects that contain either the currently focused view or the view the focus is moving to. If any of these method calls returns false, the focus is not changed.

Initial Focus
The UIFocusEnvironment protocol represents an object that is known as a focus environment. The protocol defines a preferredFocusView property that specifies where the focus should move to if the current environment becomes focussed itself.

For example, a UIViewController object's default preferredFocusView is its root view. As each UIView object can also specify its own preferred focus view, a preferred focus chain can be created. The tvOS focus engine follows this chain until a particular object returns either self or nil from its preferredFocusView property. By using these properties, you can redirect focus throughout the user interface and also specify which view  should be focussed first when a view controller appears on-screen.

It is important to note that, if you don't change any of the preferredFocusView properties of your views and view controllers, the focus by default engine focuses the view closest to the top left corner of the screen.

Focus Update
A focus update occurs when one of three events take place:
  • the user causes a focus movement
  • the app explicitly requests a focus update
  • the system triggers and automatic update
Whenever an update takes place, the following events follow:
  • The current UIScreen object's focusedView property is changed to the view that the focus is moving to.
  • The focus engine calls the didUpdateFocusInContext(_:withAnimationCoordinator:) of every focus environment object involved in the focus update. These are the same set of objects which the focus engine checks by calling each object's shouldUpdateFocusInContext(_:) method before updating the focus. It is at this point that you can add custom animations to run in conjunction with the focus-related animations the system provides.
  • All of the coordinated animations, both system and custom animations, are run simultaneously.
  • If the view the focus is moving to is currently off-screen and in a scroll view, the system scrolls the view on-screen so that the view becomes visible to the user.
To manually update the focus in the user interface, you can invoke the setNeedsFocusUpdate() method of any focus environment object. This resets the focus and moves it back to the environment's preferredFocusView.

The system can also trigger an automatic focus update in several situations, including when a focussed view is removed from the view hierarchy, a table or collection view reloads its data, or when a new view controller is presented or dismissed.

While the tvOS focus engine is quite complex and has a lot of moving parts, the UIKit APIs provided to you make it very easy to utilize this system and make it work how you want it to.

2. Controlling the Focus Engine

Focus Guides
To extend the focus engine, we are going to implement a wrap-around behavior. Our current app has a grid of six buttons as shown in the below screenshot.


What we are going to do is allow the user to move the focus towards the right, from buttons 3 and 6, and make the focus wrap back around to buttons 1 and 4 respectively. As the focus engine ignores any invisible views, this can not be done by inserting an invisible UIView (including a view with a width and height of 0) and changing its preferredFocusedView property.

Instead, we can accomplish this using the UIFocusGuide class. This class is a subclass of UILayoutGuide and represents a rectangular focusable region on the screen while being completely invisible and not interacting with the view hierarchy. On top of all the UILayoutGuide properties and methods, the UIFocusGuide class adds the following properties:
  • preferredFocusedView: This property works as I described earlier. You can think of this as the view that you want the focus guide to redirect to.
  • enabled: This property lets you enable or disable the focus guide.
In your project, open ViewController.swift and implement the viewDidAppear(_:) method of the ViewController class as shown below:
  1. override func viewDidAppear(animated: Bool) {
  2.     super.viewDidAppear(animated)
  3.    
  4.     let rightButtonIds = [3, 6]
  5.     for buttonId in rightButtonIds {
  6.         if let button = buttonWithTag(buttonId) {
  7.             let focusGuide = UIFocusGuide()
  8.             view.addLayoutGuide(focusGuide)
  9.             focusGuide.widthAnchor.constraintEqualToAnchor(button.widthAnchor).active = true
  10.             focusGuide.heightAnchor.constraintEqualToAnchor(button.heightAnchor).active = true
  11.             focusGuide.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 60.0).active = true
  12.             focusGuide.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor).active = true
  13.             focusGuide.preferredFocusedView = buttonWithTag(buttonId-2)
  14.         }
  15.     }
  16.    
  17.     let leftButtonIds = [1, 4]
  18.     for buttonId in leftButtonIds {
  19.         if let button = buttonWithTag(buttonId) {
  20.             let focusGuide = UIFocusGuide()
  21.             view.addLayoutGuide(focusGuide)
  22.             focusGuide.widthAnchor.constraintEqualToAnchor(button.widthAnchor).active = true
  23.             focusGuide.heightAnchor.constraintEqualToAnchor(button.heightAnchor).active = true
  24.             focusGuide.trailingAnchor.constraintEqualToAnchor(button.leadingAnchor, constant: -60.0).active = true
  25.             focusGuide.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor).active = true
  26.             focusGuide.preferredFocusedView = buttonWithTag(buttonId+2)
  27.         }
  28.     }
  29. }
In viewDidAppear(_:), we create focus guides to the right of buttons 3 and 6, and to the left of buttons 1 and 4. As these focus guides represent a focusable region in the user interface, they must have a set height and width. With this code, we make the regions the same size as the other buttons so that the momentum-based logic of the focus engine feels  consistent with the visible buttons.

Coordinated Animations
To illustrate how coordinated animations work, we update the alpha property of the buttons when the focus changes. In ViewController.swift, implement the didUpdateFocusInContext(_:withAnimationCoordinator:) method in the ViewController class:
  1. override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
  2.     super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
  3.    
  4.     if let focusedButton = context.previouslyFocusedView as? UIButton where buttons.contains(focusedButton) {
  5.         coordinator.addCoordinatedAnimations({
  6.             focusedButton.alpha = 0.5
  7.         }, completion: {
  8.             // Run completed animation
  9.         })
  10.     }
  11. }
The context parameter of didUpdateFocusInContext(_:withAnimationCoordinator:) is a UIFocusUpdateContext object that has the following properties:

previouslyFocusedView: references the view the focus is moving from
nextFocusedView: references the view the focus is moving to
focusHeading: a UIFocusHeading enumeration value representing the direction the focus is moving in
With the implementation of didUpdateFocusInContext(_:withAnimationCoordinator:), we add a coordinated animation to change the alpha value of the previously focused button to 0.5 and that of the currently focused button to 1.0.

Run the app in the simulator and move the focus between the buttons in the user interface. You can see that the currently focused button has an alpha of 1.0 while the previously focused button has an alpha of 0.5.

The first closure of the addCoordinatedAnimations(_:completion:) method works similarly to a regular UIView animation closure. The difference is that it inherits its duration  and timing function from the focus engine.

If you want to run an animation with a custom duration, you can add any UIView animation within this closure with the OverrideInheritedDuration animation option. The following code is an example of how to implement a custom animation that runs in half the time of the focus animations:
  1. // Running custom timed animation
  2. let duration = UIView.inheritedAnimationDuration()
  3. UIView.animateWithDuration(duration/2.0, delay: 0.0, options: .OverrideInheritedDuration, animations: { 
  4.     // Animations
  5. }, completion: { (completed: Bool) in
  6.     // Completion block
  7. })
By using the UIFocusGuide class and by utilizing custom animations, you can extend the standard behavior of the tvOS focus engine to suit your needs.

Limiting the Focus Engine
As I mentioned earlier, when deciding whether or not the focus should be moved from one view to another, the focus engine calls the shouldUpdateFocusInContext(_:) method on every focus environment involved. If any of these method calls returns false, the focus is not changed.

In our app, we are going to override this method in the ViewController class so that the focus cannot be moved down if the currently focused button is 2 or 3. To do so, implement shouldUpdateFocusInContext(_:) in the ViewController class as shown below:
  1. override func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool {
  2.     let focusedButton = context.previouslyFocusedView as? UIButton
  3.      
  4.     if focusedButton == buttonWithTag(2) || focusedButton == buttonWithTag(3) {
  5.         if context.focusHeading == .Down {
  6.             return false
  7.         }
  8.     }
  9.      
  10.     return super.shouldUpdateFocusInContext(context)
  11. }
In shouldUpdateFocusInContext(_:), we first check whether the previously focused view is button 2 or 3. We then inspect the focus heading. If the heading is equal to Down, we return false so that the current focus does not change.

Run your app one last time. You cannot move the focus down from buttons 2 and 3 to buttons 5 and 6.

Conclusion

You should now be comfortable controlling and working with the focus engine of tvOS. You now know how the focus engine works and how you can manipulate it to fit whatever needs you have for your own Apple TV apps.
Written by Davis Allie

If you found this post interesting, follow and support us.
Suggest for you:

Professional iOS Chat Apps with Social Login using Firebase3

The Complete iOS 9 Developer Course - Build 18 Apps

The Complete iOS 10 Developer Course - Build 21 Apps

Intro To iOS Backend Development: Image Uploads

How to Make a Freaking Video Game - iOS, SpriteKit, SceneKit