Статьи

Разработка родного приложения для iOS с Ionic

В моем текущем проекте я помогал клиенту в разработке собственного приложения iOS для своих клиентов. Он написан в основном на Objective-C и говорит о REST API. Я говорил о том, как мы документировали наш REST APIпару дней назад. Мы разработали прототип для этого приложения еще в декабре, используя AngularJS и Bootstrap . Вместо того, чтобы использовать PhoneGap , мы загрузили наше приложение в UIWebView.

Казалось, все работало хорошо, пока нам не нужно было прочитать код активации с камеры устройства. Так как мы не знали, как сделать OCR в JavaScript, мы решили, что в основном это родное приложение. В январе мы наняли стороннюю компанию для разработки под iOS, а они разрабатывают приложение с начала февраля. За последние пару недель мы столкнулись с некоторыми экранами, которые выглядели подходящими для HTML5, поэтому мы вернулись к нашему прототипу AngularJS.

Прототип активно использовал Bootstrap, но мы быстро поняли, что он не похож на приложение для iOS 7, как того требовал наш UX Designer. Сотрудник указал на Ionic , разработанный Дрифти . Это в основном Bootstrap для Native , поэтому разрабатываемые вами приложения выглядят и ведут себя как мобильные приложения.

Что такое ионное?

Бесплатный и открытый исходный код Ionic предлагает библиотеку оптимизированных для мобильных устройств компонентов HTML, CSS и JS для создания высокоинтерактивных приложений. Построен с Sass и оптимизирован для AngularJS.

Я начал разрабатывать с Ionic несколько недель назад. Используя его CSS-классы и директивы AngularJS, я смог за несколько дней создать несколько новых экранов. Большую часть времени я изучал новые вещи: как переопределить поведение кнопки «Назад» (для запуска обратно в собственное приложение), как настроить маршруты с помощью ui-router и как сделать службу $ ionicLoading выглядеть как родную. Теперь, когда я знаю много основ, я чувствую, что действительно могу запустить некоторый код.

Подсказка: я узнал, как работают подпредставления с ui-router, благодаря видео Tim Kindberg на YouTube об Angular UI-Router . Тем не менее, подпункты никогда не имели полностью смысла, пока я не увидел диаграмму Джареда Белла .

Чтобы продемонстрировать, как легко использовать Ionic, я быстро набрал пример приложения. Вы можете получить исходный код на GitHub по адресу https://github.com/mraible/boot-ionic . Приложение представляет собой переработанную версию x-auth-security Джоша Лонга, которая использует Ionic вместо сырых AngularJS и Bootstrap. Для простоты я не разрабатывал нативное приложение, которое оборачивает HTML.

Ниже приведены шаги, которые я использовал для преобразования AngularJS + Bootstrap в Ionic. Если вы хотите преобразовать простое приложение AngularJS в Ionic, надеюсь, это поможет.

1. Скачайте Ionic и добавьте его в свой проект.

Ionic 1.0 Beta была выпущена ранее на этой неделе. Вы можете скачать его здесь . Добавьте его файлы в ваш проект. В этом примере я добавил их в src / main / resources / public . В моем index.html я удалил CSS Bootstrap и заменил его на Ionic.

-    <link href="webjars/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
+    <link rel="stylesheet" href="css/ionic.css"/>
  </head>
-<body style="padding-top: 60px">
+<body>

Затем я заменил ссылки на JavaScript в Angular, Bootstrap и jQuery.

-    <script src="webjars/jquery/2.0.3/jquery.js"></script>
-    <script src="webjars/bootstrap/3.1.1/js/bootstrap.min.js"></script>
-    <script src="webjars/angularjs/1.2.13/angular.js"></script>
+    <script src="js/ionic.bundle.js"></script>
     <script src="webjars/angularjs/1.2.13/angular-resource.js"></script>
-    <script src="webjars/angularjs/1.2.13/angular-route.js"></script>
     <script src="webjars/angularjs/1.2.13/angular-cookies.js"></script>

Что насчет WebJars?

Вы можете спросить — почему бы не использовать
WebJars ? Вы можете, как только
этот запрос на прием будет принят, и обновленная версия будет развернута в Maven central.
Вот как изменится приложение.

2. Переключитесь с Angular’s Router на ui-router.

Ionic использует ui-router для сопоставления URL-адресов и загрузки определенных страниц. Необработанная угловая маршрутизация выглядит примерно так же, как и с ui-router, за исключением того, что $stateProviderвместо нее используется служба $routeProvider. Вы заметите, что я также добавил ‘ionic’ в качестве зависимости.

-angular.module('exampleApp', ['ngRoute', 'ngCookies', 'exampleApp.services'])
+angular.module('exampleApp', ['ionic', 'ngCookies', 'exampleApp.services'])
    .config(
-       [ '$routeProvider', '$locationProvider', '$httpProvider', function($routeProvider, $locationProvider, $httpProvider) {
+       [ '$stateProvider', '$urlRouterProvider', '$httpProvider', function($stateProvider, $urlRouterProvider, $httpProvider) {
 
-       $routeProvider.when('/create', { templateUrl: 'partials/create.html', controller: CreateController});
+           $stateProvider.state('create', {url: '/create', templateUrl: 'partials/create.html', controller: CreateController})
+               .state('edit', {url: '/edit/:id', templateUrl: 'partials/edit.html', controller: EditController})
+               .state('login', {url: '/login', templateUrl: 'partials/login.html', controller: LoginController})
+               .state('index', {url: '/index', templateUrl: 'partials/index.html', controller: IndexController});
 
-       $routeProvider.when('/edit/:id', { templateUrl: 'partials/edit.html', controller: EditController});
-       $routeProvider.when('/login', { templateUrl: 'partials/login.html', controller: LoginController});
-       $routeProvider.otherwise({templateUrl: 'partials/index.html', controller: IndexController});
-
-       $locationProvider.hashPrefix('!');
+       $urlRouterProvider.otherwise('/index');

3. Добавьте ионные элементы в ваш index.html.

В отличие от панели навигации Bootstrap, Ionic имеет элементы верхнего и нижнего колонтитула. Вместо использования директивы ng-view вы используете <ion-nav-view>. Когда вы это понимаете, это довольно удобная настройка, особенно потому, что она позволяет легко переопределять поведение кнопок назад и кнопок навигации .

-    <nav class="navbar navbar-fixed-top navbar-default" role="navigation">
-        <!-- lots of HTML here -->
-    </nav>
-
-    <div class="container">
-        <div class="alert alert-danger" ng-show="error">{{error}}</div>
-        <div ng-view></div>
-    </div>
+    <ion-nav-bar class="bar-positive nav-title-slide-ios7"></ion-nav-bar>
+    <ion-nav-view animation="slide-left-right">
+        <div class="alert alert-danger" ng-show="error">{{error}}</div>
+    </ion-nav-view>
+    <ion-footer-bar class="bar-dark" ng-show="user">
+        <button class="button button-assertive" ng-click="logout()">
+            Logout
+        </button>
+    </ion-footer-bar>

4. Измените ваши шаблоны на использование <ion-view> и <ion-content>.

После переноса маршрутов и работы базовой навигации вам необходимо изменить шаблоны, чтобы использовать <ion-view> и <ion-content>. Вот разница с самой сложной страницы в приложении.

-<div style="float: right">
-   <a href="#!/create" class="btn btn-default" ng-show="hasRole('ROLE_ADMIN')">Create</a>
-</div>
-<div class="page-header">
-   <h3>News</h3>
-</div>
+<ion-view title="News">
+    <ion-content>
+        <ion-nav-buttons side="left">
+            <div class="buttons" ng-show="hasRole('ROLE_ADMIN')">
+                <button class="button button-icon icon ion-ios7-minus-outline"
+                        ng-click="data.showDelete = !data.showDelete"></button>
+            </div>
+        </ion-nav-buttons>
+        <ion-nav-buttons side="right">
+            <a href="#/create" class="button button-icon icon ion-ios7-plus-outline"
+               ng-show="hasRole('ROLE_ADMIN')"></a>
+        </ion-nav-buttons>
 
-<div ng-repeat="newsEntry in newsEntries">
-   <hr />
-   <div class="pull-right">
-       <a ng-click="deleteEntry(newsEntry)" class="btn btn-xs btn-default" ng-show="hasRole('ROLE_ADMIN')">Remove</a>
-       <a href="#!/edit/{{newsEntry.id}}" class="btn btn-xs btn-default" ng-show="hasRole('ROLE_ADMIN')">Edit</a>
-   </div>
-   <h4>{{newsEntry.date | date}}</h4>
-   <p>{{newsEntry.content}}</p>
-</div>
-<hr />
+        <ion-list show-delete="data.showDelete" on-delete="deleteEntry(item)"
+                  option-buttons="itemButtons" can-swipe="hasRole('ROLE_ADMIN')">
+            <ion-item ng-repeat="newsEntry in newsEntries" item="newsEntry">
+                <h4>{{newsEntry.date | date}}</h4>
+                <p>{{newsEntry.content}}</p>
+            </ion-item>
+        </ion-list>
+    </ion-content>
+</ion-view>

Я перешел на использование <ion-list> с кнопками delete / options , поэтому потребовались некоторые дополнительные изменения JavaScript.

-function IndexController($scope, NewsService) {
+function IndexController($scope, $state, NewsService) {
 
    $scope.newsEntries = NewsService.query();
 
+     $scope.data = {
+         showDelete: false
+     };
+
    $scope.deleteEntry = function(newsEntry) {
         newsEntry.$remove(function() {
              $scope.newsEntries = NewsService.query();
         });
    };
+
+     $scope.itemButtons = [{
+         text: 'Edit',
+         type: 'button-assertive',
+         onTap: function (item) {
+              $state.go('edit', {id: item.id});
+         }
+     }];
}

Screenshots

After making all these changes, the app looks pretty good in Chrome.

Tips and Tricks

In additional to figuring out how to use Ionic, I discovered a few other tidbits along the way. First of all, we had a different default color for the header. Since Ionic uses generic color names (e.g. light, stable, positive, calm), I found it easy to change the default value for «positive» and then continue to use their class names.

Modifying CSS variable colors
To modify the base color for «positive», I cloned the source, and modified scss/_variables.scss.

$light: #fff !default;
$stable: #f8f8f8 !default;
-$positive: #4a87ee !default;
+$positive: #589199 !default;
$calm: #43cee6 !default;
$balanced: #66cc33 !default;
$energized: #f0b840 !default;

After making this change, I ran «grunt» and copied dist/css/ionic.css into our project.

iOS Native Integration
Our app uses a similar token-based authentication mechanism as x-auth-security, except its backed by Crowd. However, since users won’t be logging directly into the Ionic app, we added the «else» clause in app.js to allow a token to be passed in via URL. We also allowed the backend API path to be overridden.

/* Try getting valid user from cookie or go to login page */
var originalPath = $location.path();
$location.path("/login");
var user = $cookieStore.get('user');
 
if (user !== undefined) {
    $rootScope.user = user;
    $http.defaults.headers.common[xAuthTokenHeaderName] = user.token;
    $location.path(originalPath);
} else {
    // token passed in from native app
    var authToken = $location.search().token;
    if (authToken) {
        $http.defaults.headers.common['X-Auth-Token'] = authToken;
    }
}
 
// allow overriding the base API path
$rootScope.apiPath = '/api/v1.0';
if ($location.search().apiPath) {
    $rootScope.apiPath = $location.search().apiPath;
}

By adding this logic, the iOS app can pull up any particular page in a webview and let the Ionic app talk to the API. Here’s what the Objective-C code looks like:

NSString *versionNumber = @"v1.0";
NSString *apiPath = @"https://server.com/api/";
NSString *authToken = [TemporaryDataStore sharedInstance].authToken;
// webapp is a symbolic link to the Ionic app, created with Angular Seed
NSString *htmlFilePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"webapp/app"];
 
// Note: We need to do it this way because 'fileURLWithPath:' would encode the '#' to '%23" which breaks the html page
NSURL *htmlFileURL = [NSURL fileURLWithPath:htmlFilePath];
 
NSString *webappURLPath = [NSString stringWithFormat:@"%@#/news?apiPath=%@%@&token=%@",
                           htmlFileURL.absoluteString, apiPath, versionNumber, authToken];
 
// Now convert the string to a URL (doesn't seem to encode the '#' this way)
NSURL *webappURL = [NSURL URLWithString:webappURLPath];
[super updateWithURL:webappURL];

We also had to write some logic to navigate back to the native app. We used a custom URL scheme to do this, and the Ionic app simply called it. To override the default back button, I added an «ng-controller» attribute to <ion-nav-bar> and added a custom back button.

<ion-nav-bar class="bar-positive nav-title-slide-ios7" ng-controller="NavController">
    <ion-nav-back-button class="button-icon" ng-click="goBack()">
        <i class="ion-arrow-left-c"></i>
    </ion-nav-back-button>
</ion-nav-bar>

To detect if the app was loaded by iOS (vs. a browser, which we tested in), we used the following logic:

// set native app indicator
if (document.location.toString().indexOf('appName.app') > -1) {
    $rootScope.isNative = true;
}

Our Ionic app has three entry points, defined by «stateName1», «stateName2» and «stateName3» in this example. The code for our NavController handles navigating back normally (when in a browser) or back to the native app. The «appName» reference below is a 3-letter acronym we used for our app.

.controller('NavController', function($scope, $ionicNavBarDelegate, $state) {
    $scope.goBack = function() {
        if ($scope.isNative && backToNative($state)) {
            location.href='appName-ios://back';
        } else {
            $ionicNavBarDelegate.back();
        }
    };
 
    function backToNative($state) {
        var entryPoints = ['stateName1', 'stateName2', 'stateName3'];
        return entryPoints.some(function (entry) {
            return $state.current === $state.get(entry);
        });
    }
})

Summary

I’ve enjoyed working with Ionic over the last month. The biggest change I’ve had to make to our AngularJS app has been to integrate ui-router. Apart from this, the JavaScript didn’t change much. However, the HTML had to change quite a bit. As far as CSS is concerned, I found myself tweaking things to fit our designs, but less so than I did with Bootstrap. When I’ve run into issues with Ionic, the community has been very helpful on their forum. It’s the first forum I’ve used that’s powered by Discourse, and I dig it.

You can find the source from this article in my boot-ionic project. Clone it and run «mvn spring-boot:run», then open http://localhost:8080.

If you’re looking to create a native app using HTML5 technologies, I highly recommend you take a look at Ionic. We’re glad we did. Angular 2.0 will target mobile apps and Ionic is already making them look pretty damn good.