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.
Friday, August 19, 2016
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.
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.
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:
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:
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(_:).
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.
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
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.
- let URLAsString = "tweetbot://_bartjacobs/timeline"
- if let URL = NSURL.init(string: URLAsString) {
- UIApplication.sharedApplication().openURL(URL)
- }
- let URLAsString = "tweetbot://_bartjacobs/timeline"
- if let URL = NSURL.init(string: URLAsString) {
- if UIApplication.sharedApplication().canOpenURL(URL) {
- UIApplication.sharedApplication().openURL(URL)
- } else {
- print("Cannot Open URL")
- }
- }
- 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.
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.
![]() |
![]() |
- // MARK: - Actions
- @IBAction func openTweetbot(sender: AnyObject) {
- }
![]() |
3. Before iOS 9
Before Apple imposed the aforementioned restrictions on querying URL schemes, you could do the following:
- @IBAction func openTweetbot(sender: AnyObject) {
- let application = UIApplication.sharedApplication()
- let URLAsString = "tweetbot://_bartjacobs/timeline"
- guard let URL = NSURL.init(string: URLAsString) else { return }
- guard application.canOpenURL(URL) else { return }
- // Open URL
- application.openURL(URL)
- }
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:
- Schemes[9227:3539016] -canOpenURL: failed for URL: "tweetbot://_bartjacobs/timeline" - error: "This app is not allowed to query for scheme tweetbot"
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(_:).
- @IBAction func openTweetbot(sender: AnyObject) {
- let application = UIApplication.sharedApplication()
- let URLAsString = "tweetbot://_bartjacobs/timeline"
- guard let URL = NSURL.init(string: URLAsString) else { return }
- // Open URL
- application.openURL(URL)
- }
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.
![]() |
- @IBAction func openTweetbot(sender: AnyObject) {
- let application = UIApplication.sharedApplication()
- let URLAsString = "tweetbot://_bartjacobs/timeline"
- guard let URL = NSURL.init(string: URLAsString) else { return }
- guard application.canOpenURL(URL) else { return }
- // Open URL
- application.openURL(URL)
- }
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)
![]() |
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
- <!-- www/templates/tab-news.html -->
- <ion-view view-title="Good News">
- <ion-content>
- <ion-refresher
- pulling-text="Pull to refresh..."
- on-refresh="refresh()">
- </ion-refresher>
- <ul class="list">
- <li class="item" ng-repeat="n in news" ui-sref="tab.details({id: n._id})">
- {{n.text}}
- </li>
- </ul>
- </ion-content>
- </ion-view>
- // www/js/controllers.js
- ...
- .controller('NewsCtrl', function ($scope, NewsService, $ionicLoading) {
- $ionicLoading.show({
- template: 'Loading...'
- });
- NewsService.all().then(function (news) {
- $scope.news = news;
- $ionicLoading.hide();
- });
- $scope.refresh = function () {
- NewsService.all().then(function (news) {
- $scope.news = news;
- $scope.$broadcast('scroll.refreshComplete');
- });
- };
- })
- ...
- <!-- www/templates/details.html -->
- <ion-view view-title="Good News">
- <ion-content>
- <div class="card">
- <div class="item item-divider">
- {{news.createdAt | date : fullDate}}
- </div>
- <div class="item item-text-wrap">
- {{news.text}}
- </div>
- <div class="item item-divider">
- by {{news.username}}
- </div>
- </div>
- </ion-content>
- </ion-view>
- // www/js/controllers.js
- ...
- .controller('DetailsCtrl', function ($scope, $state, NewsService,
- $ionicLoading) {
- $ionicLoading.show({
- template: 'Loading...'
- });
- var id = $state.params.id;
- NewsService.one(id).then(function (news) {
- $scope.news = news;
- $ionicLoading.hide();
- });
- })
- ...
- // www/js/newsService.js
- (function () {
- function _NewsService($q, config, $http) {
- function getOne(id) {
- var deferred = $q.defer();
- $http.get(config.server + '/news/' + id)
- .success(function (data) {
- if (data.error || !data.news) {
- deferred.reject(data.error);
- }
- deferred.resolve(data.news);
- })
- .error(function () {
- deferred.reject('error');
- });
- return deferred.promise;
- }
- function getAll() {
- var deferred = $q.defer();
- $http.get(config.server + '/news')
- .success(function (data) {
- if (data.error || !data.news) {
- deferred.reject(data.error);
- }
- deferred.resolve(data.news);
- })
- .error(function () {
- deferred.reject('error');
- });
- return deferred.promise;
- }
- return {
- one: getOne,
- all: getAll
- };
- }
- _NewsService.$inject = ['$q', 'Config', '$http'];
- angular.module('app.services')
- .factory('NewsService', _NewsService);
- })();
- Server side (news)
- <!-- adminpanel/add-news.html -->
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>Add News</title>
- </head>
- <body>
- <form action="/news" method="post">
- <h2>Add good news</h2>
- <p>
- <textarea name="text" cols="50" rows="10"
- placeholder="News body"></textarea>
- </p>
- <p>
- <input type="text" required name="username"
- placeholder="Username"/>
- </p>
- <p>
- <button type="submit">Add news</button>
- </p>
- </form>
- </body>
- </html>
- // server.js
- ...
- app.use(express.static(__dirname + '/adminpanel'));
- app.get('/add-news', function (req, res) {
- res.sendFile(__dirname + '/adminpanel/add-news.html');
- });
- ...
![]() |
- // models/news.js
- var mongoose = require('mongoose');
- var Schema = mongoose.Schema;
- module.exports = function () {
- 'use strict';
- var NewsSchema = new Schema({
- username: String,
- text: String,
- createdAt: {type: Date, default: Date.now}
- });
- mongoose.model('News', NewsSchema, 'News');
- };
- // routes/news.js
- var mongoose = require('mongoose');
- var News = mongoose.model('News');
- var Users = mongoose.model('Users');
- var apn = require('apn');
- var _ = require('lodash');
- module.exports = function (app) {
- 'use strict';
- var router = app.get('router');
- router.get('/news', function (req, res) {
- News.find().sort({createdAt: -1}).lean().exec(function (err, news) {
- if (err) {
- return res.json({error: err});
- }
- res.json({error: null, news: news});
- });
- });
- router.get('/news/:id', function (req, res) {
- var id = req.params.id;
- News.findById(id).lean().exec(function (err, news) {
- if (err) {
- return res.json({error: err});
- }
- res.json({error: null, news: news});
- });
- });
- router.post('/news', function (req, res) {
- var username = req.body.username;
- var text = req.body.text;
- var news = new News({
- username: username,
- text: text
- });
- news.save(function (err, news) {
- process.nextTick(function () {
- sendPush(news);
- });
- res.redirect('/add-news');
- });
- });
- function sendPush(news) {
- var text = news.text.substr(0, 100);
- Users.find({deviceRegistered: true}).lean().exec(function (err, users) {
- if (!err) {
- for (var i = 0; i < users.length; i++) {
- var user = users[i];
- var device = new apn.Device(user.deviceToken);
- var note = new apn.Notification();
- note.badge = 1;
- note.contentAvailable = 1;
- note.alert = {
- body : text
- };
- note.device = device;
- var options = {
- gateway: 'gateway.sandbox.push.apple.com',
- errorCallback: function(error){
- console.log('push error', error);
- },
- cert: 'PushNewsCert.pem',
- key: 'PushNewsKey.pem',
- passphrase: 'superpass',
- port: 2195,
- enhanced: true,
- cacheLength: 100
- };
- var apnsConnection = new apn.Connection(options);
- console.log('push sent to ', user.username);
- apnsConnection.sendNotification(note);
- }
- }
- });
- }
- };
Push notification will be sent, but how app will know about it? We must add an event listener to the app.js
- // www/js/app.js
- ...
- .run(function ($ionicPlatform, $rootScope) {
- $ionicPlatform.ready(function () {
- $rootScope.$on(
- '$cordovaPush:notificationReceived',
- function (event, notification) {
- if (notification.alert) {
- navigator.notification.alert(notification.alert);
- }
- });
- });
- })
- ...
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...
![]() |
![]() |
![]() |
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
- $ openssl x509 -in aps_development.cer -inform der -out PushNewsCert.pem
.p12 into a .pem
- $ 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).
- <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
- <widget id="com.telnov.PushNews" version="0.0.1" xmlns="http://www.w3.org/ns/widgets"
- xmlns:cdv="http://cordova.apache.org/ns/1.0">
- <name>pushNews</name>
- ...
Test on device
OK, now you can check result. To launch app on iOS device, we have to add ios platform first:
- $ ionic platform add ios
- $ ionic build ios
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
![]() |
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
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:
- $ npm install -g ionic cordova
Creating Ionic app
For creating and serving basic Ionic app with tabs starter template, run
- $ ionic start pushNews tabs
- $ cd pushNews
- $ ionic serve
![]() |
- $ bower install --save ngCordova angular-local-storage
- <!-- index.html -->
- ...
- <script src="lib/angular-local-storage/dist/angular-local-storage.min.js"></script>
- <script src="lib/ngCordova/dist/ng-cordova.min.js"></script>
- <script src="cordova.js"></script>
- ...
- // www/js/app.js
- ...
- angular.module('app', ['ngCordova', 'LocalStorageModule'])
- ...
- Login page
- All news list
- News view page
- User profile page
- // www/js/app.js
- ...
- .config(function ($stateProvider, $urlRouterProvider) {
- $urlRouterProvider.otherwise('/login');
- $stateProvider
- .state('login', {
- url: '/login',
- templateUrl: 'templates/login.html',
- controller: 'LoginCtrl'
- })
- .state('tab', {
- abstract: true,
- templateUrl: "templates/tabs.html"
- })
- .state('tab.news', {
- url: '/news',
- views: {
- 'tab-news': {
- templateUrl: 'templates/tab-news.html',
- controller: 'NewsCtrl'
- }
- }
- })
- .state('tab.details', {
- url: '/news/:id',
- views: {
- 'tab-news': {
- templateUrl: 'templates/details.html',
- controller: 'DetailsCtrl'
- }
- }
- })
- .state('tab.profile', {
- url: '/profile',
- views: {
- 'tab-profile': {
- templateUrl: 'templates/tab-profile.html',
- controller: 'ProfileCtrl'
- }
- }
- });
- });
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.
- // server.js
- var express = require('express');
- var app = express();
- var bodyParser = require('body-parser');
- require('./config')(app);
- require('./models')(app);
- var config = app.get('config');
- app.use(function (req, res, next) {
- res.header('Access-Control-Allow-Credentials', true);
- res.header('Access-Control-Allow-Origin', req.headers.origin);
- res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
- res.header(
- 'Access-Control-Allow-Headers',
- 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'
- );
- if ('OPTIONS' === req.method) {
- res.status(200).end();
- } else {
- next();
- }
- });
- app.use(bodyParser.json());
- app.use(bodyParser.urlencoded({
- extended: true
- }));
- var router = express.Router();
- app.set('router', router);
- app.use(router);
- var http = require('http');
- http.createServer(app)
- .listen(config.PORT, function () {
- console.log('app start on port ' + config.PORT);
- });
- require('./routes')(app);
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:
- $ bower install --save jsSHA
- <!-- www/index.html -->
- ...
- <script src="lib/jsSHA/src/sha1.js"></script>
- ...
- $ cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-inappbrowser.git
A little earlier we have set login.html as template for the login state, let's create it:
- <!-- www/templates/login.html -->
- <div class="row row-center">
- <div class="col">
- <h2 class="text-center">Good News</h2>
- <div class="padding-top">
- <button class="button button-block button-calm" ng-click="twitter()">
- <i class="icon ion-social-twitter"></i> Login with Twitter
- </button>
- </div>
- </div>
- </div>
It should be noticed, that any cordova plugin call should be wraped with the deviceready event. Or use $ionicPlatform.ready() available in Ionic.
- // www/js/controllers.js
- ...
- .controller('LoginCtrl', function ($scope, $state, $cordovaOauth,
- UserService, Config, $ionicPlatform,
- $ionicLoading, $cordovaPush) {
- if (UserService.current()) {
- $state.go('tab.news');
- }
- $scope.twitter = function () {
- $ionicPlatform.ready(function () {
- $cordovaOauth.twitter(Config.twitterKey, Config.twitterSecret)
- .then(function (result) {
- $ionicLoading.show({
- template: 'Loading...'
- });
- UserService.login(result).then(function (user) {
- if (user.deviceToken) {
- $ionicLoading.hide();
- $state.go('tab.news');
- return;
- }
- $ionicPlatform.ready(function () {
- $cordovaPush.register({
- badge: true,
- sound: true,
- alert: true
- }).then(function (result) {
- UserService.registerDevice({
- user: user,
- token: result
- }).then(function () {
- $ionicLoading.hide();
- $state.go('tab.news');
- }, function (err) {
- console.log(err);
- });
- }, function (err) {
- console.log('reg device error', err);
- });
- });
- });
- }, function (error) {
- console.log('error', error);
- });
- });
- };
- })
- ...
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:
- $ cordova plugin add https://github.com/phonegap-build/PushPlugin.git
- // www/js/userService.js
- (function () {
- function _UserService($q, config, $http, localStorageService, $state) {
- var user;
- function loginUser(post) {
- var deferred = $q.defer();
- $http.post(config.server + '/user/login', post)
- .success(function (data) {
- if (data.error || !data.user) {
- deferred.reject(data.error);
- }
- localStorageService.set('user', data.user);
- user = data.user;
- deferred.resolve(data.user);
- })
- .error(function () {
- deferred.reject('error');
- });
- return deferred.promise;
- }
- function logoutUser() {
- localStorageService.remove('user');
- user = null;
- $state.go('login');
- }
- function currentUser() {
- if (!user) {
- user = localStorageService.get('user');
- }
- return user;
- }
- function registerDevice(putData) {
- var deferred = $q.defer();
- $http.put(config.server + '/user/registerDevice', putData)
- .success(function (data) {
- if (data.error || !data.user) {
- deferred.reject(data.error);
- }
- localStorageService.set('user', data.user);
- user = data.user;
- deferred.resolve(data.user);
- })
- .error(function () {
- deferred.reject('error');
- });
- return deferred.promise;
- }
- return {
- login: loginUser,
- logout: logoutUser,
- current: currentUser,
- registerDevice: registerDevice
- };
- }
- function _ConfigService() {
- return {
- server: 'http://push-news.herokuapp.com',
- twitterKey: 'your_twitter_key',
- twitterSecret: 'your_twitter_secret'
- };
- }
- _UserService.$inject = [
- '$q', 'Config', '$http', 'localStorageService',
- '$state', '$cordovaPush', '$ionicPlatform'
- ];
- angular.module('app.services')
- .factory('UserService', _UserService)
- .service('Config', _ConfigService);
- })();
- <!-- index.html -->
- ...
- <script src="js/userService.js"></script>
- ...
We should add user model and routes for /user/login and /user/registerDevice on our server
- // models/users.js
- var mongoose = require('mongoose');
- var Schema = mongoose.Schema;
- module.exports = function () {
- 'use strict';
- var UsersSchema = new Schema({
- socialId: {type: String, index: true, unique: true},
- username: {type: String, unique: true},
- deviceToken: {type: String, unique: true},
- deviceRegistered: {type: Boolean, default: false},
- createdAt: {type: Date, default: Date.now}
- });
- mongoose.model('Users', UsersSchema, 'Users');
- };
-----------------------------------------------------------------------------
- // models/users.js
- var mongoose = require('mongoose');
- var Schema = mongoose.Schema;
- module.exports = function () {
- 'use strict';
- var UsersSchema = new Schema({
- socialId: {type: String, index: true, unique: true},
- username: {type: String, unique: true},
- deviceToken: {type: String, unique: true},
- deviceRegistered: {type: Boolean, default: false},
- createdAt: {type: Date, default: Date.now}
- });
- mongoose.model('Users', UsersSchema, 'Users');
- };
---------------------------------------------------------------------------
- // routes/user.js
- var mongoose = require('mongoose');
- var Users = mongoose.model('Users');
- module.exports = function (app) {
- 'use strict';
- var router = app.get('router');
- router.post('/user/login', function (req, res) {
- var socialId = req.body.user_id;
- var username = req.body.screen_name;
- Users.findOne({socialId: socialId}).lean().exec(function (err, user) {
- if (err) {
- return res.json({error: err});
- }
- if (user) {
- return res.json({error: null, user: user});
- }
- var newUser = new Users({
- socialId: socialId,
- username: username
- });
- newUser.save(function (err, user) {
- if (err) {
- return res.json({error: err});
- }
- res.json({user: user, error: null});
- });
- });
- });
- router.put('/user/registerDevice', function (req, res) {
- var user = req.body.user;
- var token = req.body.token;
- Users.findByIdAndUpdate(user._id, {
- $set: {
- deviceToken: token,
- deviceRegistered: true
- }
- }).lean().exec(function (err, user) {
- if (err || !user) {
- return res.json({error: err});
- }
- res.json({error: null, user: user});
- });
- });
- };
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
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 UIWindow, UIViewController, UIView, 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
- 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.
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.
![]() |
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.
- override func viewDidAppear(animated: Bool) {
- super.viewDidAppear(animated)
- let rightButtonIds = [3, 6]
- for buttonId in rightButtonIds {
- if let button = buttonWithTag(buttonId) {
- let focusGuide = UIFocusGuide()
- view.addLayoutGuide(focusGuide)
- focusGuide.widthAnchor.constraintEqualToAnchor(button.widthAnchor).active = true
- focusGuide.heightAnchor.constraintEqualToAnchor(button.heightAnchor).active = true
- focusGuide.leadingAnchor.constraintEqualToAnchor(button.trailingAnchor, constant: 60.0).active = true
- focusGuide.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor).active = true
- focusGuide.preferredFocusedView = buttonWithTag(buttonId-2)
- }
- }
- let leftButtonIds = [1, 4]
- for buttonId in leftButtonIds {
- if let button = buttonWithTag(buttonId) {
- let focusGuide = UIFocusGuide()
- view.addLayoutGuide(focusGuide)
- focusGuide.widthAnchor.constraintEqualToAnchor(button.widthAnchor).active = true
- focusGuide.heightAnchor.constraintEqualToAnchor(button.heightAnchor).active = true
- focusGuide.trailingAnchor.constraintEqualToAnchor(button.leadingAnchor, constant: -60.0).active = true
- focusGuide.centerYAnchor.constraintEqualToAnchor(button.centerYAnchor).active = true
- focusGuide.preferredFocusedView = buttonWithTag(buttonId+2)
- }
- }
- }
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:
- override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) {
- super.didUpdateFocusInContext(context, withAnimationCoordinator: coordinator)
- if let focusedButton = context.previouslyFocusedView as? UIButton where buttons.contains(focusedButton) {
- coordinator.addCoordinatedAnimations({
- focusedButton.alpha = 0.5
- }, completion: {
- // Run completed animation
- })
- }
- }
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.
![]() |
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:
- // Running custom timed animation
- let duration = UIView.inheritedAnimationDuration()
- UIView.animateWithDuration(duration/2.0, delay: 0.0, options: .OverrideInheritedDuration, animations: {
- // Animations
- }, completion: { (completed: Bool) in
- // Completion block
- })
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:
- override func shouldUpdateFocusInContext(context: UIFocusUpdateContext) -> Bool {
- let focusedButton = context.previouslyFocusedView as? UIButton
- if focusedButton == buttonWithTag(2) || focusedButton == buttonWithTag(3) {
- if context.focusHeading == .Down {
- return false
- }
- }
- return super.shouldUpdateFocusInContext(context)
- }
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
Subscribe to:
Comments (Atom)





















