В моем текущем проекте я помогал клиенту в разработке собственного приложения 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.