Pull to refresh

RubyMotion: нативные iOS приложения на Ruby (перевод)

Reading time 10 min
Views 14K

В 2007 году Лоран Сансонетти, разработчик из Apple, основал проект с открытым исходным кодом MacRuby. Его целью было создание интерпретатора Ruby поверх среды исполнения Objective-C, который бы обеспечивал прозрачное взаимодействие между Ruby и экосистемой OS X «Cocoa» — и ему это удалось. Теперь Сансонетти надеется сделать что-то подобное и под iOS.


С недавних пор Сансонетти уволился из Apple, где он работал последние семь лет, чтобы основать свой стартап под названием HipByte. Он анонсировал свой первый продукт сегодня [3 Мая — прим. перев.] — инструмент разработки под названием RubyMotion, который откроет возможности для написания нативных iOS приложений на языке программирования Ruby.

Я тестировал RubyMotion с Марта, когда Сансонетти дал мне доступ к ранней закрытой бете продукта. В этой статье я представлю эксклюзивный обзор RubyMotion из первых рук и опишу, как он может быть использован для написания ПО для iOS. Статья включает весь исходный код простой iOS демки, которую я создал, отображающей список топовых постов на reddit.

RubyMotion

RubyMotion создан поверх той самой реализации Objective-C Ruby, которая стоит за MacRuby, но использует новый LLVM компилятор для конвертации Ruby кода в оптимизированный машинный код. Компилятор RubyMotion дает на выходе эффективные нативные приложения, которые не имеют ограничений по быстродействию, присущих обычному Ruby коду.



Мобильные приложения, созданные при помощи RubyMotion, работают так же быстро, как и эквиваленты на Objective-C и используют соизмеримое количество аппаратных ресурсов. Более того, приложения RubyMotion полностью удовлетворяют требованиям App Store от Apple. Со слов Сансонетти, несколько RubyMotion приложений уже были приняты в App Store, пройдя все этапы валидации.

В RubyMotion доступны все стандартные API iOS, что означает — все возможности разработчиков Objective-C доступны и разработчикам Ruby. Помимо этого, приложения RubyMotion могут соответствовать внешнему виду остальной платформы, т.к. используют стандартный набор виджетов из UIKit.

Разработка ПО при помощи RubyMotion

Вместо того, что-бы попытаться встроить RubyMotion в IDE от Apple, Сансонетти решил пойти более традиционным для Ruby путем — использовать инструменты командной строки. Консольная команда motion генерирует новый проект по шаблону, который содержит болванку приложения с папками для кода, графики и других ресурсов. Любой файл .rb, который находится в директории app будет автоматически скомпилирован в конечную сборку. В RubyMotion вообще нет нужды использовать ключевое слово require.

Процесс сборки в RubyMotion опирается на инструмент Rake, знакомый большинству Ruby программистов. Опции сборки Rake используются для указания зависимостей и прочих настроек приложения. При запуске rake из командной строки, приложение будет скомпилировано и запущено на симуляторе iOS.

Помимо запуска приложения в симуляторе, в терминале становится доступен REPL, позволяющий интерактивно взаимодействовать с запущенным приложением, используя выражения на Ruby. Возможность «на лету» вносить изменения в свойства виджетов и внутренние структуры данных приложения чрезвычайно полезна при тестировании и выявлении ошибок.



Также вы можете использовать цели в Rake для запуска приложения на устройстве или создания пакета .ipa с последующей дистрибуцией в App Store. Из-за ограничений по цифровой подписи кода вам нужно иметь подписку на Apple Developer Program, чтобы тестировать свое приложения на устройствах.

Компиляция и запуск на симуляторе моей RubyMotion reddit-демки заняло около пяти секунд на MacBook Air 2011. Время компиляции не слишком большое, но выглядит это необычно для Ruby разработчиков, которые привыкли видеть результат сразу после внесения изменений. В прибавку к компилятору RubyMotion поставляется со своей собственной командой ruby, позволяющей запускать скрипты Ruby в окружении RubyMotion.

Процесс разработки на RubyMotion вполне комфортный. Я пишу код в Vim и держу открытым терминал, запуская Rake для проверки работы программы. Один ключевой недостаток по сравнению с Xcode — невозможность использования инструментов визуального проектирования интерфейсов. Это может доставить проблемы начинающим разработчикам, потому что им придется углубляться в API UIKit.

Поговорим с Сансонетти

Я спросил его про несовместимость с визуальными инструментами Xcode, он ответил что интеграция с Xcode будет осуществлена, но не в ближайшем будущем.

Вместо этого он создает набор Ruby библиотек для программного создания интерфейсов iOS. Эти библиотеки будут высокоуровневыми обертками над API UIKit, которые значительно легче использовать. Этот подход такой же как и у библиотеки HotCocoa для MacRuby, но с более высоким уровнем абстракции.

«Мы фокусируемся над созданием набора высокоуровневых Ruby гемов для RubyMotion. Один из них — легковесная обертка над UIKit», сказал мне Сансонетти. «Мы решили реализовать систему разметки интерфейса с использованием предметно-ориентированного языка, немного похожего на CSS, так что Ruby разработчики будут чувствовать себя как дома. Наша идея схожа с автоматической разметкой Cocoa [Cocoa auto layout — прим. перев.], которая построена на ASCII-подобном языке, но мы можем сделать это на Ruby еще лучше. Мы считаем что это более верный путь создания UI, нежели Interface Builder, т.к. вы можете все это визуально представить в коде.»

Вместо того, чтобы встраивать эти библиотеки в RubyMotion, Сансонетти собирается выложить их на GitHub под пермиссивной лицензией. Он считает, что приверженцы RubyMotion будут создавать свои варианты библиотек разметки с разными стилями. А у разработчиков приложений будет большой выбор — использовать родные обертки или сторонние.

Я спросил Сансонетти о его взаимодействии с проектом MacRuby, который он основал как open source проект от Apple, и как теперь проект будет развиваться после того как он сфокусировался над разработкой RubyMotion. Он объяснил, что стремление дальше работать над MacRuby было основной причиной его ухода из Apple и создания своей компании.

«Как главный разработчик команды Core OS в Apple, я создал и развивал MacRuby, но MacRuby был только одной частью всех моих обязанностей», сказал он. «После того как MacRuby стал достаточно стабильным, я осознал, что мне придется оставить его разработку. Я не хотел бросать проект и его изумительное сообщество, поэтому я, подумав над этим некоторое время, понял, что создание стартапа RubyMotion будет лучшим, что я мог сделать — я буду продолжать работать над MacRuby и зарабатывать на жизнь одновременно.»

Хотя он и был вынужден временно перестать принимать участие в разработке MacRuby в течение шестимесячного переходного периода после ухода из Apple, теперь он вернулся обратно и не собирается останавливать работу. На данный момент он единственный сотрудник его новой компании, но в планах собрать команду, наняв «несколько участников из сообщества MacRuby ближе к концу года».

Создание RubyMotion в одиночку было сложной технической задачей. Мы попросили его описать некоторые моменты, которые ему приходилось преодолевать во время разработки.

«Самое сложное было — это реализация совершенно нового статического компилятора и модели памяти для RubyMotion», сказал он. «Также сложна была реализация ARM ABI и низкоуровневых протоколов. Это отняло значительное время для доводки скорости выполнения на устройствах».

Также Сансонетти поделился видением того, как выразительность и гибкость Ruby может улучшить скорость разработки.

«Одна из наиболее важных фитч — интерактивная среда, что рубисты принимают как должное, которая отсутствует на многих других платформах. Иметь возможность наблюдать за изменениями в реальном времени очень полезно при отладке и тестировании приложения», сказал он. «Ruby краткий и выразительный язык. Приложение iOS, написанное на Ruby будет содержать значительно меньше строк кода, чем аналогичное приложение на Objective-C. Меньше кода означает более короткий цикл разработки, меньше багов, легче в сопровождении, а значит больше времени на игру в Skyrim».

Наводим мосты

Перед тем как мы создадим рабочее приложение, давайте немного взглянем на то, как RubyMotion наводит мосты между Ruby и средой исполнения Objective-C.

Objective-C это расширение C, которое добавляет такие возможности, как опциональное динамическое типизирование и объектно-ориентированное программирование. NeXT лицензировали этот язык программирования у Stepstone, который был у истоков языка, и использовали его для создания фреймворков под NeXTstep OS, которая позже стала Mac OS X. С тех пор Apple продолжает дорабатывать и улучшать Objective-C.

Objective-C имеет несколько общих свойств с Ruby, которые облегчают взаимодейтсвие между языками. Ruby и Objective-C имеют похожий механизм вызова методов, взятый из Smalltalk — отправка сообщений. Их объектные модели похожи в том, что соблюдают одиночное наследование и имеют большие возможности интроспекции. С недавних пор Objective-C получил новый синтаксический механизм для создания легких анонимных функций схожих с блоками Ruby.

Несмотря на то, что эти особенности позволяют чувствовать себя привычно при разработке RubyMotion, есть и ложка дегтя. Самым большим препятствием является представление сигнатур методов в Objective-C и их вызовов, которое радикально отличается от того, что разработчики видят в других распространенных языках программирвоания.

Методы в Objective-C устроены так, что параметры являются частью их имени. С первого взгляда это может показаться просто соглашением именования, но на практике все иначе (описание синтаксиса методов Objective-C выходит за рамки этой статьи, однако есть несколько ресурсов с подробным описанием).

Главный вывод для пользователей RubyMotion в том, что API Objective-C представлены в немного непривычном формате при их использовании на Ruby. Для иллюстрации проблемы, я собираюсь показать вам пример из UIKit, который будет использоваться в демонстрационном reddit приложении далее в статье. В этом приложении мне нужно было снять выделение со строки в таблице. В Objective-C это выглядит так:

[tableView deselectRowAtIndexPath:indexPath animated:YES]


Фактическое название метода такое deselectRowAtIndexPath:animated. Значения параметров, которые имеют тип NSIndexPath* и BOOL встроены в имя функции на месте их следования при вызове. Ruby не имеет такого синтаксиса. Вы по прежнему следуете соглашению об именованных параметрах, но в более традиционном стиле со скобками:

tableView.deselectRowAtIndexPath(indexPath, animated:true)


MacRuby и RubyMotion имеют гибридную систему, которая позволяет им быть на одном уровне с Objective-C. В этой гибридной системе все стандартные типы Ruby реализованы поверх стандартных типов Objective-C. К примеру, все Ruby объекты наследуют у NSObject, а любая строка в Ruby — это NSMutableString. Вы можете получить представление о том, что это значит, вызвав команду String.ancestors в консоли приложения MacRuby:

=> [String, NSMutableString, NSString, Comparable, NSObject, Kernel]


Такой вид эквивалентности типов очень эффективен в рамках среды исполнения. Нет нужды производить тяжелые вычисления по конвертированию комплексных типов данных между Ruby и API Objective-C.

Другим преимуществом является то, что все часто используемые и полезные методы, которые идут с этими базовыми классами, стали повсеместно доступными. Это дает большое преимущество при работе с контейнерами, например, при использовании chained методов NSMutableArray map, group_by и sort_by с блоками.

Демонстрационное приложение

За последний месяц я создал несколько приложений на RubyMotion, а также использовал его, чтобы портировать простые MacRuby приложения на iOS. В этой статье я хочу показать вам простое демо приложение, которое я написал, пока разбирался с некоторыми API UIKit.

Это приложение — простой клиент для сайта Reddit. Оно скачивает список текущих топовых постов через Reddit JSON API, затем парсит данные и отображает их в табличном виде на экране. Когда пользователь тапает по элементу в списке, соответствующая ссылка открывается в мобильном Safari.

class RedditPost
  attr_accessor :title, :url, :author
  attr_accessor :comments, :score
  attr_accessor :subreddit

  def initialize(data)
    @url = data["url"]
    @title = data["title"]
    @author = data["author"]
    @comments = data["num_comments"]
    @link = data["permalink"]
    @score = data["score"]
    @subreddit = data["subreddit"]
  end
end

class RedditController < UITableViewController
  def viewDidLoad
    @posts = []
    view.dataSource = view.delegate = self
    refresh "top.json"
  end

  def tableView(tv, numberOfRowsInSection:section)
    @posts.size
  end

  def tableView(tv, cellForRowAtIndexPath:indexPath)
    cid = "PostCell"
    cell = tv.dequeueReusableCellWithIdentifier(cid) ||
           UITableViewCell.alloc.initWithStyle(
                UITableViewCellStyleSubtitle,
                reuseIdentifier:cid)

    p = @posts[indexPath.row]

    cell.textLabel.text = p.title
    cell.detailTextLabel.text = "Posted by #{p.author} in #{p.subreddit}"
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator

    cell
  end

  def tableView(tv, didSelectRowAtIndexPath:indexPath)
    url = NSURL.URLWithString @posts[indexPath.row].url
    UIApplication.sharedApplication.openURL url
    tv.deselectRowAtIndexPath(indexPath, animated:true)
  end

  def get(address)
    err = Pointer.new_with_type "@"
    url = NSURL.URLWithString address

    raise "Loading Error: #{err[0].description}" unless
      data = NSData.alloc.initWithContentsOfURL(
        url, options:0, error:err)

    raise "Parsing Error: #{err[0].description}" unless
      json = NSJSONSerialization.JSONObjectWithData(
        data, options:0, error:err)

    json
  end

  def refresh(endpoint)
    Dispatch::Queue.concurrent.async do
      begin
        response = get "http://reddit.com/#{endpoint}"
        data = response["data"]["children"].map {|i| RedditPost.new i["data"] }
        Dispatch::Queue.main.sync { @posts = data; view.reloadData }
      rescue Exception => msg
        puts "Loading Failed: #{msg}"
      end
    end
  end
end

class AppDelegate
  def application(app, didFinishLaunchingWithOptions:launchOptions)
    @win = UIWindow.alloc.initWithFrame(
            UIScreen.mainScreen.applicationFrame)

    @win.rootViewController = RedditController.alloc.initWithStyle(
                                  UITableViewStylePlain)

    @win.rootViewController.wantsFullScreenLayout = true
    @win.makeKeyAndVisible
    return true
  end
end


Все приложение разработано как один .rb файл с менее чем 100 строками кода. Метод application в классе AppDelegate вызывается при запуске приложения. Мы используем этот метод, чтобы создать и вывести на экран экземпляр вида, которой хотим показать пользователю при старте.

Почти вся логика приложения реализована в классе RedditController, являющимся подклассом UITableViewController. Метод viewDidLoad, который вызывается при загрузке класса в пользовательский интерфейс, устанавливает переменные экземпляра, содержащие посты, а потом вызывает метод refresh, который загружает данные.

Метод refresh вызывает get, который использует класс NSData для скачивания данных в формате JSON и использует NSJSONSerialization, чтобы преобразовать их в легко используемую структуру данных. Вы могли заметить, что метод refresh использует Grand Central Dispatch (GCD), чтобы загрузить и преобразовать данные в фоновом потоке.

Несколько методов tableView в классе RedditController обрабатывают различные аспекты поведения вида. Метод tableView:cellForRowAtIndexPath используется для создания объектов строк и заполнения их данными. Метод tableView:didSelectRowAtIndexPath вызывается, когда пользователь тапает по элементу в таблице. Код в этом методе определяет какой объект был выбран и открывает URL этого объекта в браузере, а потом убирает выделение с объекта.

Заключение

RubyMotion предлагает разработчикам приложений iOS силу и выразительность Ruby без компромисов. Трудно представить более привлекательный способ разработки нативных приложений под iOS. Реализация RubyMotion действительно впечатляет, и ощущается зрелость технологий, лежащих в основе.

После некоторого времени, проведенного за разработкой приложений с помощью RubyMotion, единственным недостатком мне показалась нехватка интеграции с Xcode для создания пользовательского интерфейса. Для разработчиков без опыта работы с UIKit, написание кода вручную может оказаться долгим и сложным.

Хотя этот недочет не отрицает продуктивность работы с Ruby, новым разработчикам придется пройти начальный этап обучения, прежде чем они смогут в полном объеме получить пользу от RubyMotion. К счастью, библиотеки высокого уровня, которые уже скоро выйдут, должны исправить эту ситуацию.

Лицензия RubyMotion, включающая в себя годовую поддержку обновлений, стоит $199.99. Сейчас ее можно приобрести со скидкой за $149.99. Более подробную информацию можно найти на официальном сайте продукта. Там же вы можете посмотреть вступительный скринкаст от Pragmatic studio.

Данная статья является переводом. Оригинал доступен по ссылке.
Tags:
Hubs:
+45
Comments 27
Comments Comments 27

Articles