Статьи

Давайте напишем приложение RubyMotion: часть 2

Конечный продукт
Что вы будете создавать

RubyMotion — это фантастическая среда для создания эффективных приложений iOS с использованием языка Ruby. В первой части этого руководства вы узнали, как настроить и реализовать приложение RubyMotion. Вы работали с Interface Builder для создания пользовательского интерфейса приложения, реализовали контроллер представления и научились писать тесты для своего приложения.

В этом руководстве вы узнаете о шаблоне проектирования Model-View-Controller или MVC и о том, как его использовать для структурирования приложения. Вы также реализуете вид рисования и добавите распознаватель жестов, который позволяет пользователю рисовать на экране. Когда вы закончите, у вас будет полное, полностью работающее приложение.

Apple рекомендует разработчикам iOS применять шаблон проектирования Model-View-Controller к своим приложениям. Этот шаблон разбивает классы на одну из трех категорий: модели, представления и контроллеры.

  • Модели содержат бизнес-логику вашего приложения, код, определяющий правила управления данными и взаимодействия с ними. Ваша модель — это то место, где живет основная логика вашего приложения.
  • Представления отображают информацию для пользователя и позволяют им взаимодействовать с приложением.
  • Контроллеры несут ответственность за связывание моделей и видов вместе. В iOS SDK используются контроллеры представлений, специализированные контроллеры, обладающие чуть большим знанием представлений, чем другие инфраструктуры MVC.

Как MVC применяется к вашему приложению? Вы уже начали реализовывать класс PaintingController , который соединит ваши модели и виды вместе. Для слоя модели вы добавите два класса:

  • Stroke Этот класс представляет один штрих в картине.
  • Painting Этот класс представляет всю картину и содержит один или несколько штрихов.

Для слоя представления вы создадите класс PaintingView который отвечает за отображение объекта Painting для пользователя. Вы также добавите StrokeGestureRecongizer который фиксирует сенсорный ввод от пользователя.

Давайте начнем с модели Stroke . Штрих будет состоять из цвета и нескольких точек, представляющих штрих. Для начала создайте файл для класса Stroke , app / models / stroke.rb , и еще один для его спецификации, spec / models / stroke.rb .

Затем, реализуйте скелет класса обводки и конструктор.

1
2
3
class Stroke
  attr_reader :points, :color
end

Класс Stroke имеет два атрибута: points , набор точек и color — цвет объекта Stroke . Далее реализуем конструктор.

1
2
3
4
5
6
7
8
class Stroke
  attr_reader :points, :color
 
  def initialize(start_point, color)
    @points = [ start_point ]
    @color = color
  end
end

Это выглядит великолепно до сих пор. Конструктор принимает два аргумента, start_point и color . Он устанавливает points в массив точек, содержащих start_point и color в соответствии с start_point цветом.

Когда пользователь проводит пальцем по экрану, вам нужен способ добавить точки к объекту Stroke . Добавьте метод add_point в Stroke .

1
2
3
def add_point(point)
  points << point
end

Это было легко. Для удобства добавьте еще один метод в класс Stroke который возвращает начальную точку.

1
2
3
def start_point
  points.first
end

Конечно, ни одна модель не будет полной без набора спецификаций.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
describe Stroke do
 
  before do
    @start_point = CGPoint.new(0.0, 50.0)
    @middle_point = CGPoint.new(50.0, 100.0)
    @end_point = CGPoint.new(100.0, 0.0)
    @color = UIColor.blueColor
 
    @stroke = Stroke.new(@start_point, @color)
    @stroke.add_point(@middle_point)
    @stroke.add_point(@end_point)
  end
 
  describe «#initialize» do
    before do
      @stroke = Stroke.new(@start_point, @color)
    end
 
    it «sets the color» do
      @stroke.color.should == @color
    end
  end
 
  describe «#start_point» do
    it «returns the stroke’s start point» do
      @stroke.start_point.should == @start_point
    end
  end
 
  describe «#add_point» do
    it «adds the points to the stroke» do
      @stroke.points.should == [ @start_point, @middle_point, @end_point ]
    end
  end
 
  describe «#start_point» do
    it «returns the start point» do
      @stroke.start_point.should == @start_point
    end
  end
end

Это должно начать чувствовать себя знакомым. Вы добавили четыре блока описания, которые тестируют методы initialize , start_point , add_point и start_point . Также есть блок before который устанавливает несколько переменных экземпляра для спецификаций. Обратите внимание, что в блоке describe для #initialize есть блок before который сбрасывает объект @stroke . Все в порядке. Со спецификациями вам не нужно заботиться о производительности, как с обычным приложением.

Это момент истины, пришло время заставить ваше приложение что-то нарисовать. Начните с создания файла для класса PaintingView в app / views / painting_view.rb . Поскольку мы делаем некоторые специализированные рисунки, класс PaintingView сложно проверить. Ради краткости, я собираюсь пока пропустить спецификации.

Затем, реализуйте класс PaintingView .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PaintingView < UIView
  attr_accessor :stroke
 
  def drawRect(rectangle)
    super
 
    # ensure the stroke is provided
    return if stroke.nil?
 
    # set up the drawing context
    context = UIGraphicsGetCurrentContext()
    CGContextSetStrokeColorWithColor(context, stroke.color.CGColor)
    CGContextSetLineWidth(context, 20.0)
    CGContextSetLineCap(context, KCGLineCapRound)
    CGContextSetLineJoin(context, KCGLineJoinRound)
 
    # move the line to the start point
    CGContextMoveToPoint(context, stroke.start_point.x, stroke.start_point.y)
 
    # add each line in the path
    stroke.points.drop(1).each do |point|
      CGContextAddLineToPoint(context, point.x, point.y)
    end
 
    # stroke the path
    CGContextStrokePath(context);
  end
end

Фу, это много кода. Давайте разберем его по частям. Класс PaintingView расширяет класс UIView . Это позволяет PaintingView добавляться как подпредставление представления PaintingController . Класс PaintingView имеет один атрибут stroke , который является экземпляром класса модели Stroke .

Что касается шаблона MVC, то при работе с iOS SDK для представления приемлемо знать о модели, но для модели не совсем нормально знать о представлении.

В классе PaintingView мы переопределили UIView drawRect: метод UIView . Этот метод позволяет реализовать пользовательский код для рисования. В первой строке этого метода super вызывается метод суперкласса, в данном примере UIView , с предоставленными аргументами.

В drawRect: мы также проверяем, что атрибут stroke не равен nil . Это предотвращает ошибки, если stroke еще не была установлена. Затем мы UIGraphicsGetCurrentContext текущий контекст рисования, вызывая UIGraphicsGetCurrentContext , настраиваем обводку, которую мы собираемся нарисовать, перемещаем контекст рисования в start_point обводки и добавляем линии для каждой точки в объекте stroke . Наконец, мы вызываем CGContextStrokePath чтобы CGContextStrokePath путь, рисуя его в виде.

Добавьте розетку в PaintingController для представления живописи.

1
outlet :painting_view

UIView Interface Builder, запустив bundle exec rake ib:open и добавьте объект UIView в представление PaintingController из библиотеки Ojbect справа. Установите класс представления в PaintingView в Инспекторе идентичности . Убедитесь, что вид рисования расположен под кнопками, которые вы добавили ранее. Вы можете настроить порядок подпредставлений, изменив положения представлений в иерархии представлений слева.

Управляйте и перетащите из контроллера представления в PaintingView и выберите выход painting_view из появившегося меню.

Выберите вид рисования и установите его цвет фона: 250 красных, 250 зеленых и 250 синих.

Не забудьте добавить спецификацию в spec / controllers / painting_controller_spec.rb для вывода painting_view .

1
2
3
4
5
describe «#painting_view» do
  it «is connected in the storyboard» do
    controller.painting_view.should.not.be.nil
  end
end

Чтобы убедиться, что код вашего рисунка работает правильно, добавьте следующий фрагмент кода в класс PaintingController и запустите ваше приложение. Вы можете удалить этот фрагмент кода, когда вы убедились, что все работает должным образом.

1
2
3
4
5
6
7
8
9
def viewDidLoad
  stroke = Stroke.new(CGPoint.new(80, 100), ‘#ac5160’.uicolor)
  stroke.add_point(CGPoint.new(240, 100))
  stroke.add_point(CGPoint.new(240, 428))
  stroke.add_point(CGPoint.new(80, 428))
  stroke.add_point(CGPoint.new(80, 100))
  painting_view.stroke = stroke
  painting_view.setNeedsDisplay
end

Теперь, когда вы можете нарисовать штрих, пришло время выровнять всю картину. Давайте начнем с Painting модели. Создайте файл для класса в app / models / painting.rb и реализуйте класс Painting .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
class Painting
  attr_accessor :strokes
 
  def initialize
    @strokes = []
  end
 
  def start_stroke(point, color)
    strokes << Stroke.new(point, color)
  end
 
  def continue_stroke(point)
    current_stroke.add_point(point)
  end
 
  def current_stroke
    strokes.last
  end
end

Модель Painting подобна классу Stroke . Конструктор инициализирует strokes пустым массивом. Когда человек касается экрана, приложение запускает новый штрих, вызывая start_stroke . Затем, когда пользователь перетаскивает палец, он добавляет точки с continue_stroke . Не забудьте спецификации для класса Painting .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
describe Painting do
 
  before do
    @point1 = CGPoint.new(10, 60)
    @point2 = CGPoint.new(20, 50)
    @point3 = CGPoint.new(30, 40)
    @point4 = CGPoint.new(40, 30)
    @point5 = CGPoint.new(50, 20)
    @point6 = CGPoint.new(60, 10)
 
    @painting = Painting.new
  end
 
  describe «#initialize» do
 
    before do
      @painting = Painting.new
    end
 
    it «sets the stroke to an empty array» do
      @painting.strokes.should == []
    end
  end
 
  describe «#start_stroke» do
 
    before do
      @painting.start_stroke(@point1, UIColor.redColor)
      @painting.start_stroke(@point2, UIColor.blueColor)
    end
 
    it «starts new strokes» do
      @painting.strokes.length.should == 2
      @painting.strokes[0].points.should == [ @point1 ]
      @painting.strokes[0].color.should == UIColor.redColor
      @painting.strokes[1].points.should == [ @point2 ]
      @painting.strokes[1].color.should == UIColor.blueColor
    end
  end
 
  describe «#continue_stroke» do
 
    before do
      @painting.start_stroke(@point1, UIColor.redColor)
      @painting.continue_stroke(@point2)
      @painting.start_stroke(@point3, UIColor.blueColor)
      @painting.continue_stroke(@point4)
    end
 
    it «adds points to the current strokes» do
      @painting.strokes[0].points.should == [ @point1, @point2 ]
      @painting.strokes[1].points.should == [ @point3, @point4 ]
    end
  end
end

Затем измените класс PaintingView чтобы рисовать объект Painting вместо объекта Stroke .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class PaintingView < UIView
  attr_accessor :painting
 
  def drawRect(rectangle)
    super
 
    # ensure the painting is provided
    return if painting.nil?
 
    painting.strokes.each do |stroke|
      draw_stroke(stroke)
    end
  end
 
  def draw_stroke(stroke)
 
    # set up the drawing context
    context = UIGraphicsGetCurrentContext()
    CGContextSetStrokeColorWithColor(context, stroke.color.CGColor)
    CGContextSetLineWidth(context, 20.0)
    CGContextSetLineCap(context, KCGLineCapRound)
    CGContextSetLineJoin(context, KCGLineJoinRound)
 
    # move the line to the start point
    CGContextMoveToPoint(context, stroke.start_point.x, stroke.start_point.y)
 
    # add each line in the path
    stroke.points.drop(1).each do |point|
      CGContextAddLineToPoint(context, point.x, point.y)
    end
 
    # stroke the path
    CGContextStrokePath(context);
  end
end

Вы изменили атрибут stroke на painting . Метод drawRect: теперь перебирает все штрихи на рисунке и рисует каждый из них с помощью draw_stroke , который содержит код рисования, который вы написали ранее.

Вам также необходимо обновить контроллер представления, чтобы он содержал модель Painting . Вверху класса PaintingController добавьте attr_reader :painting . Как следует из названия, метод UIViewController класса UIViewController — суперкласс класса PaintingController — вызывается, когда контроллер представления завершил загрузку своего представления. viewDidLoad метод viewDidLoad является хорошим местом для создания экземпляра Painting и установки атрибута PaintingView объекта PaintingView .

1
2
3
4
def viewDidLoad
  @painting = Painting.new
  painting_view.painting = painting
end

Как всегда, не забудьте добавить тесты для viewDidLoad в spec / controllers / painting_controller_spec.rb .

01
02
03
04
05
06
07
08
09
10
describe «#viewDidLoad» do
 
  it «sets the painting» do
    controller.painting.should.be.instance_of Painting
  end
 
  it «sets the painting attribute of the painting view» do
    controller.painting_view.painting.should == controller.painting
  end
end

Ваше приложение будет довольно скучным, если вы не позволите людям рисовать на экране пальцами. Давайте добавим эту функциональность сейчас. Создайте файл для класса StrokeGestureRecognizer вместе с его спецификацией, выполнив следующие команды из командной строки.

1
2
touch app/views/stroke_gesture_recognizer.rb
touch spec/views/stroke_gesture_recognizer_spec.rb

Затем создайте скелет для класса.

1
2
3
class StrokeGestureRecognizer < UIGestureRecognizer
  attr_reader :position
end

Класс StrokeGestureRecognizer расширяет класс UIGestureRecognizer , который обрабатывает сенсорный ввод. Он имеет атрибут position который класс PaintingController будет использовать для определения положения пальца пользователя.

В классе StrokeGestureRecognizer необходимо реализовать четыре метода touchesBegan:withEvent: touchesMoved:withEvent: touchesEnded:withEvent: и touchesCancelled:withEvent: Метод touchesBegan:withEvent: вызывается, когда пользователь начинает касаться экрана пальцем. Метод touchesMoved:withEvent: вызывается повторно, когда пользователь перемещает палец, и метод touchesEnded:withEvent: вызывается, когда пользователь touchesEnded:withEvent: палец с экрана. Наконец, touchesCancelled:withEvent: метод вызывается, если жест отменен пользователем.

Ваш распознаватель жестов должен сделать две вещи для каждого события, обновить атрибут position и изменить свойство state .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class StrokeGestureRecognizer < UIGestureRecognizer
  attr_accessor :position
 
  def touchesBegan(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateBegan
  end
 
  def touchesMoved(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateChanged
  end
 
  def touchesEnded(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateEnded
  end
 
  def touchesCancelled(touches, withEvent: event)
    super
    @position = touches.anyObject.locationInView(self.view)
    self.state = UIGestureRecognizerStateEnded
  end
end

Оба touchesEnded:withEvent: и touchesCancelled:withEvent: устанавливают состояние в UIGestureRecognizerStateEnded . Это потому, что не имеет значения, если пользователь прерван, рисунок должен оставаться нетронутым.

Чтобы протестировать класс StrokeGestureRecognizer , вы должны иметь возможность создать экземпляр UITouch . К сожалению, нет общедоступного API для этого. Чтобы заставить это работать, мы будем использовать библиотеку насмешек Facon .

Добавьте gem 'motion-facon' в ваш Gemfile и запустите пакетную bundle install . Затем добавьте require "motion-facon" ниже require "sugarcube-color" в Rakefile проекта.

Затем StrokeGestureRecognizer спецификацию StrokeGestureRecognizer .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
describe StrokeGestureRecognizer do
  extend Facon::SpecHelpers
 
  before do
    @stroke_gesture_recognizer = StrokeGestureRecognizer.new
    @touch1 = mock(UITouch, :»locationInView:» => CGPoint.new(100, 200))
    @touch2 = mock(UITouch, :»locationInView:» => CGPoint.new(300, 400))
    @touches1 = NSSet.setWithArray [ @touch1 ]
    @touches2 = NSSet.setWithArray [ @touch2 ]
  end
 
  describe «#touchesBegan:withEvent:» do
 
    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
    end
 
    it «sets the position to the gesture’s position» do
      @stroke_gesture_recognizer.position.should == CGPoint.new(100, 200)
    end
 
    it «sets the state of the gesture recognizer» do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateBegan
    end
  end
 
  describe «#touchesMoved:withEvent:» do
 
    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
      @stroke_gesture_recognizer.touchesMoved(@touches2, withEvent: nil)
    end
 
    it «sets the position to the gesture’s position» do
      @stroke_gesture_recognizer.position.should == CGPoint.new(300, 400)
    end
 
    it «sets the state of the gesture recognizer» do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateChanged
    end
  end
 
  describe «#touchesEnded:withEvent:» do
 
    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
      @stroke_gesture_recognizer.touchesEnded(@touches2, withEvent: nil)
    end
 
    it «sets the position to the gesture’s position» do
      @stroke_gesture_recognizer.position.should == CGPoint.new(300, 400)
    end
 
    it «sets the state of the gesture recognizer» do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateEnded
    end
  end
 
  describe «#touchesCancelled:withEvent:» do
 
    before do
      @stroke_gesture_recognizer.touchesBegan(@touches1, withEvent: nil)
      @stroke_gesture_recognizer.touchesCancelled(@touches2, withEvent: nil)
    end
 
    it «sets the position to the gesture’s position» do
      @stroke_gesture_recognizer.position.should == CGPoint.new(300, 400)
    end
 
    it «sets the state of the gesture recognizer» do
      @stroke_gesture_recognizer.state.should == UIGestureRecognizerStateEnded
    end
  end
end

extend Facon::SpecHelpers делает несколько методов доступными в ваших спецификациях, включая mock . mock — это простой способ создания тестовых объектов, которые работают точно так, как вы этого хотите. В блоке before в начале спецификации вы UITouch экземпляры UITouch с помощью метода locationInView: который возвращает предопределенную точку.

Затем добавьте метод stroke_gesture_changed в класс PaintingController . Этот метод будет получать экземпляр класса StrokeGestureRecognizer при каждом обновлении жеста.

01
02
03
04
05
06
07
08
09
10
def stroke_gesture_changed(stroke_gesture_recognizer)
 
  if stroke_gesture_recognizer.state == UIGestureRecognizerStateBegan
    painting.start_stroke(stroke_gesture_recognizer.position, selected_color)
  else
    painting.continue_stroke(stroke_gesture_recognizer.position)
  end
 
  painting_view.setNeedsDisplay
end

Когда состояние распознавателя жестов — UIGestureRecognizerStateBegan , этот метод запускает новый штрих в объекте Painting используя StrokeGestureRecognizer и selected_color . В противном случае он продолжает текущий ход.

Добавьте спецификации для этого метода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
describe «#stroke_gesture_changed» do
 
  before do
    drag(controller.painting_view, :points => [ CGPoint.new(100, 100), CGPoint.new(150, 150), CGPoint.new(200, 200) ])
  end
 
  it «adds the points to the stroke» do
    controller.painting.strokes.first.points[0].should == CGPoint.new(100, 100)
    controller.painting.strokes.first.points[1].should == CGPoint.new(150, 150)
    controller.painting.strokes.first.points[2].should == CGPoint.new(200, 200)
  end
 
  it «sets the stroke’s color to the selected color» do
    controller.painting.strokes.first.color.should == controller.selected_color
  end
end

RubyMotion предоставляет несколько вспомогательных методов для имитации взаимодействия с пользователем, включая drag . Используя drag , вы можете симулировать взаимодействие пользователя с экраном. Опция points позволяет указать массив точек для перетаскивания.

Если бы вы запустили спецификации сейчас, они потерпели бы неудачу. Это потому, что вам нужно добавить распознаватель жестов на раскадровку. Запустите Interface Builder, запустив bundle exec rake ib:open . Из библиотеки объектов перетащите объект на сцену и измените его класс на StrokeGestureRecognizer в инспекторе удостоверений справа.

Управляйте и перетаскивайте из объекта StrokeGestureRecognizer в PaintingController и выберите метод select_color из select_color меню. Это обеспечит select_color метода select_color каждый раз, когда запускается распознаватель жестов. Затем управляйте и перетащите объект PaintingView объект StrokeGestureRecognizer и выберите gestureRecognizer меню.

Добавьте спецификацию для распознавателя жестов в спецификации PaintingController в #painting_view describe #painting_view .

01
02
03
04
05
06
07
08
09
10
11
describe «#painting_view» do
 
  it «is connected in the storyboard» do
    controller.painting_view.should.not.be.nil
  end
 
  it «has a stroke gesture recognizer» do
    controller.painting_view.gestureRecognizers.length.should == 1
    controller.painting_view.gestureRecognizers[0].should.be.instance_of StrokeGestureRecognizer
  end
end

Вот и все. С этими изменениями ваше приложение теперь должно позволять человеку рисовать на экране. Запустите ваше приложение и получайте удовольствие.

Осталось добавить несколько последних штрихов, прежде чем ваша заявка будет завершена. Поскольку ваше приложение иммерсивно, строка состояния немного отвлекает. Вы можете удалить его, установив значения UIStatusBarHidden и UIViewControllerBasedStatusBarAppearance в Info.plist приложения. Это легко сделать в блоке setup RubyMotion внутри Rakefile проекта.

1
2
3
4
5
Motion::Project::App.setup do |app|
  app.name = ‘Paint’
  app.info_plist[‘UIStatusBarHidden’] = true
  app.info_plist[‘UIViewControllerBasedStatusBarAppearance’] = false
end

Значки приложения и изображения запуска включены в исходные файлы этого учебного пособия. Загрузите изображения и скопируйте их в каталог ресурсов проекта. Затем установите значок приложения в конфигурации Rakefile. Возможно, вам придется очистить сборку, запустив bundle exec rake clean:all , чтобы увидеть новый образ запуска.

1
2
3
4
5
6
Motion::Project::App.setup do |app|
  app.name = ‘Paint’
  app.info_plist[‘UIStatusBarHidden’] = true
  app.info_plist[‘UIViewControllerBasedStatusBarAppearance’] = false
  app.icons = [ «icon.png» ]
end

Вот и все. Теперь у вас есть полное приложение, которое готово для миллионов загрузок в App Store. Вы можете просмотреть и скачать исходный код этого приложения с GitHub .

Даже если ваше приложение готово, вы можете добавить к нему гораздо больше. Вы можете добавить кривые между линиями, больше цветов, разную ширину линии, сохранить, отменить и повторить, и все, что вы можете себе представить. Что вы будете делать, чтобы сделать ваше приложение лучше? Позвольте мне знать в комментариях ниже.