RubyMotion — это фантастическая среда для создания эффективных приложений iOS с использованием языка Ruby. В первой части этого руководства вы узнали, как настроить и реализовать приложение RubyMotion. Вы работали с Interface Builder для создания пользовательского интерфейса приложения, реализовали контроллер представления и научились писать тесты для своего приложения.
В этом руководстве вы узнаете о шаблоне проектирования Model-View-Controller или MVC и о том, как его использовать для структурирования приложения. Вы также реализуете вид рисования и добавите распознаватель жестов, который позволяет пользователю рисовать на экране. Когда вы закончите, у вас будет полное, полностью работающее приложение.
1. Модель-Вид-Контроллер
Apple рекомендует разработчикам iOS применять шаблон проектирования Model-View-Controller к своим приложениям. Этот шаблон разбивает классы на одну из трех категорий: модели, представления и контроллеры.
- Модели содержат бизнес-логику вашего приложения, код, определяющий правила управления данными и взаимодействия с ними. Ваша модель — это то место, где живет основная логика вашего приложения.
- Представления отображают информацию для пользователя и позволяют им взаимодействовать с приложением.
- Контроллеры несут ответственность за связывание моделей и видов вместе. В iOS SDK используются контроллеры представлений, специализированные контроллеры, обладающие чуть большим знанием представлений, чем другие инфраструктуры MVC.
Как MVC применяется к вашему приложению? Вы уже начали реализовывать класс PaintingController
, который соединит ваши модели и виды вместе. Для слоя модели вы добавите два класса:
-
Stroke
Этот класс представляет один штрих в картине. -
Painting
Этот класс представляет всю картину и содержит один или несколько штрихов.
Для слоя представления вы создадите класс PaintingView
который отвечает за отображение объекта Painting
для пользователя. Вы также добавите StrokeGestureRecongizer
который фиксирует сенсорный ввод от пользователя.
2. Удары
Давайте начнем с модели 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
. Все в порядке. Со спецификациями вам не нужно заботиться о производительности, как с обычным приложением.
3. Рисование
Это момент истины, пришло время заставить ваше приложение что-то нарисовать. Начните с создания файла для класса 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
|
4. Живопись
Теперь, когда вы можете нарисовать штрих, пришло время выровнять всю картину. Давайте начнем с 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
|
5. Устройства распознавания жестов
Ваше приложение будет довольно скучным, если вы не позволите людям рисовать на экране пальцами. Давайте добавим эту функциональность сейчас. Создайте файл для класса 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
|
Вот и все. С этими изменениями ваше приложение теперь должно позволять человеку рисовать на экране. Запустите ваше приложение и получайте удовольствие.
6. Последние штрихи
Осталось добавить несколько последних штрихов, прежде чем ваша заявка будет завершена. Поскольку ваше приложение иммерсивно, строка состояния немного отвлекает. Вы можете удалить его, установив значения 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 .
Даже если ваше приложение готово, вы можете добавить к нему гораздо больше. Вы можете добавить кривые между линиями, больше цветов, разную ширину линии, сохранить, отменить и повторить, и все, что вы можете себе представить. Что вы будете делать, чтобы сделать ваше приложение лучше? Позвольте мне знать в комментариях ниже.