Паттерны проектирования, взгляд iOS разработчика. Часть 2. Наблюдатель

Содержание:

Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель

Сегодня мы разберемся с «начинкой» паттерна «Наблюдатель». Сразу оговорюсь, что в мире iOS у вас не будет острой необходимости реализовывать этот паттерн, поскольку в SDK уже есть NotificationCenter. Но в образовательных целях мы полностью разберем анатомию и применение этого паттерна. К тому же, самостоятельная реализация может обладать большей гибкостью и, в некоторых случаях, быть более полезной.

«Кажется дождь собирается» (с)

Авторы книги «Паттерны проектирования» (Эрик и Элизабет Фримен), в качестве примера, предлагают применять паттерн «Наблюдатель» к разработке приложения Weather Station. Представьте, что у нас есть: метеостанция, и объект WeatherData, который обрабатывает данные от ее датчиков и передает их нам. Приложение же состоит из трех экранов: экрана текущего состояния погоды, экрана статистики и экрана прогноза.

Мы знаем, что WeatherData предоставляет нам такой интерфейс:

// Objective-C
- (double)getTemperature;
- (double)getHumidity;
- (double)getPressure;
- (void)measurementsChanged;
// Swift
func getTemperature() -> Double
func getHumidity() -> Double
func getPressure() -> Double
func measurementsChanged()

Также разработчики WeatherData сообщили, что при каждом обновлении погодных датчиков будет вызван метод measurementsChanged.

Конечно же, самое простое решение — написать код непосредственно в этом методе:

// Objective-C
- (void)measurementsChanged {
    double temp = [self getTemperature];
    double humidity = [self getHumidity];
    double pressure = [self getPressure];

    [currentConditionsDisplay updateWithTemp:temp humidity:humidity andPressure:pressure];
    [statisticsDisplay updateWithTemp:temp humidity:humidity andPressure:pressure];
    [forecastDisplay updateWithTemp:temp humidity:humidity andPressure:pressure];
}
// Swift
func measurementsChanged() {
    let temp = self.getTemperature()
    let humidity = self.getHumidity()
    let pressure = self.getPressure()

    currentConditionsDisplay.update(with: temp, humidity: humidity, and: pressure)
    statisticsDisplay.update(with: temp, humidity: humidity, and: pressure)
    forecastDisplay.update(with: temp, humidity: humidity, and: pressure)
}

Такой подход конечно же плох, потому что:
— программируем на уровне конкретных реализаций;
— сложная расширяемость в будущем;
— нельзя в рантайме добавлять/убирать экраны, на которых будет показана информация;
— … (свой вариант);

Поэтому паттерн «Наблюдатель» будет в этой ситуации очень кстати. Поговорим немного о характеристиках этого паттерна.

«Наблюдатель». Что под капотом?

Основные характеристики этого паттерна — наличие СУБЪЕКТА и, собственно, НАБЛЮДАТЕЛЕЙ. Связь, как вы уже догадались, один ко многим, и при изменении состояния СУБЪЕКТА происходит оповещение его НАБЛЮДАТЕЛЕЙ. На первый взгляд все просто.

Первое что нам понадобится — интерфейсы (протоколы) для наблюдателей и субъекта:

// Objective-C
@protocol Observer <NSObject>

- (void)updateWithTemperature:(double)temperature
                     humidity:(double)humidity
                  andPressure:(double)pressure;

@end

@protocol Subject <NSObject>

- (void)registerObserver:(id<Observer>)observer;
- (void)removeObserver:(id<Observer>)observer;
- (void)notifyObservers;

@end
// Swift
protocol Observer: class {
    func update(with temperature: Double, humidity: Double, and pressure: Double)
}

protocol Subject: class {
    func register(observer: Observer)
    func remove(observer: Observer)
    func notifyObservers()
}

Теперь нужно привести в порядок WeatherData (подписать на соотв. протокол и не только):

// Objective-C

// файл заголовка WeatherData.h
@interface WeatherData : NSObject <Subject>

- (void)measurementsChanged;
- (void)setMeasurementWithTemperature:(double)temperature
                             humidity:(double)humidity
                          andPressure:(double)pressure; // test method

@end

// файл реализации WeatherData.m
@interface WeatherData()

@property (strong, nonatomic) NSMutableArray<Observer> *observers;
@property (assign, nonatomic) double temperature;
@property (assign, nonatomic) double humidity;
@property (assign, nonatomic) double pressure;

@end

@implementation WeatherData

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.observers = [[NSMutableArray<Observer> alloc] init];
    }
    return self;
}

- (void)registerObserver:(id<Observer>)observer {
    [self.observers addObject:observer];
}

- (void)removeObserver:(id<Observer>)observer {
    [self.observers removeObject:observer];
}

- (void)notifyObservers {
    for (id<Observer> observer in self.observers) {
        [observer updateWithTemperature:self.temperature
                               humidity:self.humidity
                            andPressure:self.pressure];
    }
}

- (void)measurementsChanged {
    [self notifyObservers];
}

- (void)setMeasurementWithTemperature:(double)temperature
                             humidity:(double)humidity
                          andPressure:(double)pressure {

    self.temperature = temperature;
    self.humidity = humidity;
    self.pressure = pressure;
    [self measurementsChanged];
}

@end
// Swift
class WeatherData: Subject {

    private var observers: [Observer]
    private var temperature: Double!
    private var humidity: Double!
    private var pressure: Double!

    init() {
        self.observers = [Observer]()
    }

    func register(observer: Observer) {
        self.observers.append(observer)
    }

    func remove(observer: Observer) {
        self.observers = self.observers.filter { $0 !== observer }
    }

    func notifyObservers() {
        for observer in self.observers {
            observer.update(with: self.temperature, humidity: self.humidity, and: self.pressure)
        }
    }

    func measurementsChanged() {
        self.notifyObservers()
    }

    func setMeasurement(with temperature: Double,
                        humidity: Double,
                        and pressure: Double) { // test method

        self.temperature = temperature
        self.humidity = humidity
        self.pressure = pressure
        self.measurementsChanged()
    }

}

Мы добавили тестовый метод setMeasurement для имитации изменения состояний датчиков.

Поскольку методы register и remove у нас редко будут меняться от субъекта к субъекту, было бы хорошо иметь их реализацию по умолчанию. В Objective-C для этого нам понадобится дополнительный класс. Но для начала переименуем наш протокол и уберем из него эти методы:

// Objective-C
@protocol SubjectProtocol <NSObject>

- (void)notifyObservers;

@end

Теперь добавим класс Subject:

// Objective-C

// файл заголовка Subject.h
@interface Subject : NSObject

@property (strong, nonatomic) NSMutableArray<Observer> *observers;

- (void)registerObserver:(id<Observer>)observer;
- (void)removeObserver:(id<Observer>)observer;

@end

// файл реализации Subject.m
@implementation Subject

- (void)registerObserver:(id<Observer>)observer {
    [self.observers addObject:observer];
}

- (void)removeObserver:(id<Observer>)observer {
    [self.observers removeObject:observer];
}

@end

Как видите, в этом классе два метода и массив наших наблюдателей. Теперь в классе WeatherData убираем этот массив из свойств и унаследуемся от Subject, а не от NSObject:

// Objective-C
@interface WeatherData : Subject <SubjectProtocol>

В свифте, благодаря расширениям протоколов, дополнительный класс не понадобится.
Мы просто включим в протокол Subject свойство observers:

// Swift
protocol Subject: class {
    var observers: [Observer] { get set }

    func register(observer: Observer)
    func remove(observer: Observer)
    func notifyObservers()
}

А в расширении протокола напишем реализацию методов register и remove по умолчанию:

// Swift
extension Subject {

    func register(observer: Observer) {
        self.observers.append(observer)
    }

    func remove(observer: Observer) {
        self.observers = self.observers.filter {$0 !== observer }
    }

}

Принимаем сигналы

Теперь нам нужно реализовать экраны нашего приложения. Мы реализуем только один из них: CurrentConditionsDisplay. Реализация остальных аналогична.

Итак, создаем класс CurrentConditionsDisplay, добавляем в него два свойства и метод display (этот экран должен показывать текущее состояние погоды, как мы помним):

// Objective-C
@interface CurrentConditionsDisplay()

@property (assign, nonatomic) double temperature;
@property (assign, nonatomic) double humidity;

@end

@implementation CurrentConditionsDisplay

- (void)display {
    NSLog(@"Current conditions: %f degrees and %f humidity", self.temperature, self.humidity);
}

@end
// Swift
private var temperature: Double!
private var humidity: Double!

func display() {
    print("Current conditions: \(self.temperature) degrees and \(self.humidity) humidity")
}

Теперь нам нужно «подписать» этот класс на протокол Observer и реализовать необходимый метод:

// Objective-C

// в файле заголовка CurrentConditionsDisplay.h
@interface CurrentConditionsDisplay : NSObject <Observer>

// в файле реализации CurrentConditionsDisplay.m
- (void)updateWithTemperature:(double)temperature
                     humidity:(double)humidity
                  andPressure:(double)pressure {

    self.temperature = temperature;
    self.humidity = humidity;
    [self display];
}
// Swift
class CurrentConditionsDisplay: Observer {

    func update(with temperature: Double, humidity: Double, and pressure: Double) {
        self.temperature = temperature
        self.humidity = humidity
        self.display()
    }

Почти готово. Осталось зарегистрировать нашего наблюдателя у субъекта (также не забывайте удалять регистрацию при деинициализации).

Для этого нам понадобится еще одно свойство:

// Objective-C
@property (weak, nonatomic) Subject<SubjectProtocol> *weatherData;
// Swift
private weak var weatherData: Subject?

И инициализатор с деинициализатором:

// Objective-C
- (instancetype)initWithSubject:(Subject<SubjectProtocol> *)subject {
    self = [super init];
    if (self) {
        self.weatherData = subject;
        [self.weatherData registerObserver:self];
    }
    return self;
}

- (void)dealloc
{
    [self.weatherData removeObserver:self];
}
// Swift
init(with subject: Subject) {
    self.weatherData = subject
    self.weatherData?.register(observer: self)
}

deinit {
    self.weatherData?.remove(observer: self)
}

Заключение

Мы написали довольно простую реализацию паттерна «Наблюдатель». Наш вариант, конечно же не без изъянов. Например, если мы добавим четвертый датчик, то нужно будет переписывать интерфейс наблюдателей и реализации этого интерфейса (чтоб доставлять до наблюдателей четвертый параметр), а это не есть хорошо. В NotificationCenter, о котором я упоминал в самом начале статьи, такой проблемы не существует. Дело в том, что там передача данных происходит одним-единым параметром-словарем.

Спасибо за внимание, учитесь и учите других.
Ведь пока мы учимся — мы остаемся молодыми. 🙂

«Мочим» объекты с помощью Cuckoo

Пост написан по мотивам статьи Mocking in Swift with Cuckoo by Godfrey Nolan

По долгу своей «службы» мобильным разработчиком, предстала передо мной задача: разобраться с созданием и использованием Моков для юнит-тестирования. Моим коллегой была рекомендована библиотека Cuckoo. Стал я с ней разбираться и вот что из этого вышло.

Документация

Прочитав документацию на гитхабе мне, к сожалению, не удалось «завести» Cuckoo в моем проекте. Через CocoaPods этот фреймворк был установлен, но вот с Run-скриптом возникли проблемы: предложенный пример не создавал файл GeneratedMocks.swift в папке с тестами, и я бы и не разобрался почему, если бы не нашел через гугл статью, которую упомянул в начале поста.

Итак, пройдем все этапы вместе и разберемся с некоторыми нюансами.

Тестовый проект

Естественно, нам нужен какой-нибудь проект в который мы подключим Cuckoo и напишем несколько тестов. Откройте Xcode, и создайте новый Single View Application: язык — Swift, обязательно поставьте галочку Include Unit Tests, имя проекта — UrlWithCuckoo.

Добавьте в проект новый Swift-файл и назовите его UrlSession.swift. Вот полный код:

import Foundation

class UrlSession {
    var url:URL?
    var session:URLSession?
    var apiUrl:String?

    func getSourceUrl(apiUrl:String) -> URL {
        url = URL(string:apiUrl)
        return url!
    }

    func callApi(url:URL) -> String {
        session = URLSession()
        var outputdata:String = ""
        let task = session?.dataTask(with: url as URL) { (data, _, _) -> Void in
            if let data = data {
                outputdata = String(data: data, encoding: String.Encoding.utf8)!
                print(outputdata)
            }
        }
        task?.resume()
        return outputdata
    }
}

Как видите, это простой класс с тремя свойствами и двумя методами. Именно для этого класса мы и будем создавать Мок.

Подключаем Cuckoo

Я использую в работе CocoaPods, поэтому для подключения Cuckoo добавлю в каталог с проектом Podfile такого вида:

platform :ios, '9.0'
use_frameworks!

target 'UrlWithCuckooTests' do
    pod 'Cuckoo'
end

Естественно нужно запустить pod install в терминале из каталога с проектом, и после завершения установки открыть в Xcode UrlWithCuckoo.xcworkspace.

Следующим шагом добавляем Run-скрипт в Build Phases нашего «таргета» тестирования (нужно нажать «+» и выбрать «New Run Script Phase»):

Вот полный текст скрипта:

# Define output file; change "${PROJECT_NAME}Tests" to your test's root source folder, if it's not the default name
OUTPUT_FILE="./${PROJECT_NAME}Tests/GeneratedMocks.swift"
echo "Generated Mocks File = ${OUTPUT_FILE}"

# Define input directory; change "${PROJECT_NAME}" to your project's root source folder, if it's not the default name
INPUT_DIR="./${PROJECT_NAME}"
echo "Mocks Input Directory = ${INPUT_DIR}"

# Generate mock files; include as many input files as you'd like to create mocks for
${PODS_ROOT}/Cuckoo/run generate --testable "${PROJECT_NAME}" \
--output "${OUTPUT_FILE}" \
"${INPUT_DIR}/UrlSession.swift"

Как видите, в комментариях в скрипте написано о необходимости заменить ${PROJECT_NAME} и ${PROJECT_NAME}Tests, но в нашем примере в этом нет необходимости.

Генерируем Мок(и)

Дальше нам нужно, чтоб этот скрипт сработал и создал в каталоге с тестами файл GeneratedMocks.swift, и просто сбилдить проект (Cmd+B) для этого недостаточно. Нужно сделать Build For -> Testing (Shift+Cmd+U):

Проверьте что в каталоге UrlWithCuckooTests появился файл GeneratedMocks.swift. Его (файл) также нужно добавить в сам проект: просто перетащите его из Finder в Xcode в UrlWithCuckooTests:

Наши Моки готовы, поговорим о некоторых нюансах.

1. Сложные файловые структуры

Если у вас в проекте присутствует нормальная файловая структура и файлы разложены по подпапкам, а не просто находятся в корневом каталоге, то в скрипт нужно внести некоторые корректировки.

Допустим вы используете в своем проекте MVP и вам нужен Мок для вью-контроллера модуля MainModule (он у вас в проекте, конечно же, лежит по адресу /Modules/MainModule/MainModuleViewController.swift). В этом случае вам нужно поменять последнюю строку в скрипте из нашего примера "${INPUT_DIR}/UrlSession.swift" на "${INPUT_DIR}/Modules/MainModule/MainModuleViewController.swift".

Также если вы хотите, чтоб файл GeneratedMocks.swift попадал не просто в корневой каталог тестов, а, например, в подпапку Modules, то вам нужно подкорректировать в скрипте вот эту строку: OUTPUT_FILE="./${PROJECT_NAME}Tests/GeneratedMocks.swift".

2. Нужны Моки нескольких классов

Очень вероятно (ожидаемая вероятность — 99.9%), что вам понадобятся Моки нескольких классов. Их можно сделать просто перечислив в конце скрипта файлы из которых нужно сделать Моки, разделив их обратными слэшами:

"${INPUT_DIR}/UrlSession.swift" \
"${INPUT_DIR}/Modules/MainModule/MainModuleViewController.swift" \
"${INPUT_DIR}/MyAwesomeObject.swift"

3. Аннотации типов

В классах к которым вы создаете Моки у всех свойств должны быть аннотации типов. Если у вас есть что-то типа такого:

var someBoolVariable = false

То при генерации Мока вы получите ошибку:

И в файле GeneratedMocks.swift будет фигурировать __UnknownType:

К сожалению Cuckoo не умеет определять тип по значению по умолчанию, и в таком случае необходимо явно указывать тип свойства:

var someBoolVariable: Bool = false

Пишем тесты

Теперь напишем несколько простых тестов используя наш Мок. Откроем файл UrlWithCuckooTests.swift и удалим из него два метода, которые создаются по умолчанию: func testExample() и func testPerformanceExample(). Они нам не понадобятся. И, конечно, не забудьте:

import Cuckoo

1. Свойства

Сначала напишем тесты для свойств. Создаем новый метод:

func testVariables() {

}

Инициализируем в нем наш Мок и пару дополнительных констант:

let mock = MockUrlSession()
let urlStr  = "http://habrahabr.ru"
let url  = URL(string:urlStr)!

Теперь нам нужно написать stub-ы для свойств:

// Arrange
stub(mock) { (mock) in
    when(mock.url).get.thenReturn(url)
}

stub(mock) { (mock) in
    when(mock.session).get.thenReturn(URLSession())
}

stub(mock) { (mock) in
    when(mock.apiUrl).get.thenReturn(urlStr)
}

Stub — это что-то типа подмены возвращаемого результата. Грубо говоря, мы описываем что вернет свойство нашего Мока, когда мы к нему обратимся. Как видите, мы используем thenReturn, но можем использовать и then. Это даст возможность не только вернуть значение, но и выполнить дополнительные действия. Например, наш первый stub можно описать и вот так:

// Arrange
stub(mock) { (mock) in
    when(mock.url).get.then { (_) -> URL? in

        // some actions here

        return url
    }
}

И, собственно, проверки (на значения и на nil):

// Act and Assert
XCTAssertEqual(mock.url?.absoluteString, urlStr)
XCTAssertNotNil(mock.session)
XCTAssertEqual(mock.apiUrl, urlStr)

XCTAssertNotNil(verify(mock).url)
XCTAssertNotNil(verify(mock).session)
XCTAssertNotNil(verify(mock).apiUrl)

2. Методы

Теперь протестируем вызовы методов нашего Мока. Создадим два тестовых метода:

func testGetSourceUrl() {

}

func testCallApi() {

}

В обоих методах также инициализируем наш Мок и вспомогательные константы:

let mock = MockUrlSession()
let urlStr  = "http://habrahabr.ru"
let url  = URL(string:urlStr)!

Также в методе testCallApi() добавим счетчик вызовов:

var callApiCount = 0

Дальше в обоих методах напишем stub-ы.

testGetSourceUrl():

// Arrange
stub(mock) { (mock) in
    mock.getSourceUrl(apiUrl: urlStr).thenReturn(url)
}

testCallApi():

// Arrange
stub(mock) { mock in
    mock.callApi(url: equal(to: url, equalWhen: { $0 == $1 })).then { (_) -> String in
        callApiCount += 1
        return "{'firstName': 'John','lastName': 'Smith'}"
    }
}

Проверяем первый метод:

// Act and Assert
XCTAssertEqual(mock.getSourceUrl(apiUrl: urlStr), url)
XCTAssertNotEqual(mock.getSourceUrl(apiUrl: urlStr), URL(string:"http://google.com"))
verify(mock, times(2)).getSourceUrl(apiUrl: urlStr)

(в последней строке мы проверяем, что метод вызывался два раза)

И второй:

// Act and Assert
XCTAssertEqual(mock.callApi(url: url),"{'firstName': 'John','lastName': 'Smith'}")
XCTAssertNotEqual(mock.callApi(url: url), "Something else")
verify(mock, times(2)).callApi(url: equal(to: url, equalWhen: { $0 == $1 }))
XCTAssertEqual(callApiCount, 2)

(тут мы тоже проверяем количество вызовов, причем двумя способами: с помощью verify и счетчика вызовов callApiCount, который мы объявляли ранее)

Запускаем тесты

После запуска проекта на тестирование (Cmd+U) мы увидим вот такую картину:

Все работает, отлично. 🙂

И напоследок

Ссылка на то что у нас в итоге получилось: https://github.com/ssuhanov/UrlWithCuckoo

Спасибо за внимание.

Паттерны проектирования, взгляд iOS разработчика. Часть 1. Стратегия

Содержание:

Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель

Напомню, что в этой серии статей, я разбираю книгу «Паттерны проектирования» Эрика и Элизабет Фримен. И сегодня мы изучим паттерн «Стратегия». Поехали.

Откуда растут ноги (и крылья)

Авторы книги рассказывают нам историю о создании приложения SimUDuck. Начнем с реализации начального состояния приложения: у нас есть абстрактный класс Duck и два его наследника: MallardDuck и RedheadDuck. Тут же мы сталкиваемся с первой сложностью: в Objective-C и Swift нет абстрактных классов.

Выходим из ситуации теми инструментами, что есть: для Objective-C объявляем обычный класс Duck (в нем будут реализации по умолчанию) и к нему добавляем протокол AbstractDuck (в нем будут абстрактные методы, которые нужно реализовать в наследниках). Выглядит это так:

// Objective-C
@protocol AbstractDuck <NSObject>

- (void)display;

@end

@interface Duck : NSObject

- (void)quack;
- (void)swim;

@end

Соответственно наследники будут такими:

// Objective-C
@interface MallardDuck : Duck <AbstractDuck>

@end

@implementation MallardDuck

- (void)display {

}

@end

@interface RedheadDuck : Duck <AbstractDuck>

@end

@implementation RedheadDuck

- (void)display {

}

@end

В Swift это сделать немного проще: достаточно протокола и его расширения (в расширении можно некоторые методы протокола реализовать по умолчанию):

// Swift
protocol Duck {
    func quack()
    func swim()
    func display()
}

extension Duck {

    func quack() {

    }

    func swim() {

    }

}

И наследники:

// Swift
class MallardDuck: Duck {

    func display() {

    }

}

class RedheadDuck: Duck {

    func display() {

    }

}

Приложение развивается и у уток появляется возможность летать

Для этого соответствующий метод появляется в родительском классе Duck. И вскоре после этого выясняется, что есть еще один наследник — RubberDuck. Резиновые утки ведь не летают, а поскольку метод добавлен в родительский класс, то он будет доступен и для резиновых уток. В общем: ситуация оказалась не из простых. При дальнейшем расширении приложения будут возникать сложности с поддержкой функций полета (и не только с ней, с функцией кряканья та же история) и с другими видами уток (деревянных, например).

Сначала авторы книги предлагают решать проблему вынесением функций полета и кряканья в отдельные интерфейсы (для Objective-c и Swift — протоколы) Flyable и Quackable. Но этот вариант оказывается совсем не так хорош, каким кажется на первый взгляд. Малейшее изменение функции полета, которое должно быть применено ко всем летающим уткам влечет за собой внесение одного и того же кода во многих местах программы. Так что такое решение определенно не подходит.

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

Ну и к тому же, одна из главных целей паттерна — подменяемая реализация поведений во время выполнения, поэтому авторы предлагают выносить реализации поведений из класса Duck.

Для этого создадим протоколы FlyBehavior и QuackBehavior:

// Objective-C
@protocol FlyBehavior <NSObject>

- (void)fly;

@end

@protocol QuackBehavior <NSObject>

- (void)quack;

@end
// Swift
protocol FlyBehavior {
    func fly()
}

protocol QuackBehavior {
    func quack()
}

И конкретные классы реализующие эти протоколы: FlyWithWings и FlyNoWay для FlyBehavior, а также Quack, Squeak и MuteQuack для QuackBehavior (приведу пример для FlyWithWings, остальные реализуются очень схожим образом) :

// Objective-C
@interface FlyWithWings : NSObject <FlyBehavior>

@end

@implementation FlyWithWings

- (void)fly {

    // fly implementation

}

@end
// Swift
class FlyWithWings: FlyBehavior {

    func fly() {

        // fly implementation

    }

}

Делегирование наше все

Теперь мы, по сути, делегируем наше поведение любому другому классу, который реализует соответствующий интерфейс (протокол). Как сказать нашей утке каким должно быть ее поведение в полете и при кряканьи? Очень просто, добавляем в наш класс (в Swift — протокол) Duck два свойства:

// Objective-C
@property (strong, nonatomic) id<FlyBehavior> flyBehavior;
@property (strong, nonatomic) id<QuackBehavior> quackBehavior;
// Swift
var flyBehavior: FlyBehavior { get set }
var quackBehavior: QuackBehavior { get set }

Как видите у них не определен конкретный тип, определено лишь, что это класс подписанный на соответствующий протокол.

Методы fly и quack нашего родительского класса (или протокола) Duck заменим аналогичными:

// Objective-C
- (void)performFly {
    [self.flyBehavior fly];
}

- (void)performQuack {
    [self.quackBehavior quack];
}
// Swift
func performFly() {
    flyBehavior.fly()
}

func performQuack() {
    quackBehavior.quack()
}

Теперь наша утка просто делегирует свое поведение соответствующему поведенческому объекту. Как мы устанавливаем поведение каждой утке? Например при инициализации (пример для MallardDuck):

// Objective-C
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.flyBehavior = [[FlyWithWings alloc] init];
        self.quackBehavior = [[Quack alloc] init];
    }
    return self;
}
// Swift
init() {
    self.flyBehavior = FlyWithWings()
    self.quackBehavior = Quack()
}

Наш паттерн готов 🙂

Заключение

В iOS разработке паттерн «Стратегия» вы можете встретить, например, в архитектуре MVP: в ней презентер является не чем иным как поведенческим объектом для вью-контроллера (вью-контроллер, как вы помните, только сообщает презентеру о действиях пользователя, а вот логику обработки данных определяет презентер), и наоборот: вью-контроллер — поведенческий объект для презентера (презентер лишь говорит «показать пользователю данные», но как именно они будут показаны — решит вью-контроллер). Также этот паттерн вы встретите и в VIPER, если, конечно, надумаете его использовать в вашем приложении. 🙂

Паттерны проектирования, взгляд iOS разработчика. Часть 0. Синглтон-Одиночка

Я почув і забув.
Я записав і запам’ятав.
Я зробив і зрозумів.
Я навчив іншого, тепер я майстер.
(В. В. Бублик)

Небольшое вступление.

Я не зря вынес в начало поста цитату на украинском языке. Дело в том, что именно эти слова я услышал от своего преподавателя программирования на втором курсе университета, и именно в таком виде я вспоминаю эти слова до сих пор. Как вы можете догадаться, эта цитата является отсылкой к высказыванию Конфуция, но в ней есть очень важное дополнение о достижении мастерства.

И именно эти слова и сподвигли меня на написание данной серии постов. Дело в том, что я — начинающий iOS разработчик, и я очень хочу разобраться в паттернах проектирования. И я не придумал лучшего способа, чем взять книгу «Паттерны проектирования» Эрика и Элизабет Фримен, и написать примеры каждого паттерна на Objective-C и Swift. Таким образом я смогу лучше понять суть каждого паттерна, а также особенности обоих языков.

Содержание:

Часть 0. Синглтон-Одиночка
Часть 1. Стратегия
Часть 2. Наблюдатель

Итак, начнем с самого простого на мой взгляд паттерна.

Одиночка, он же — синглтон.

Основная задача синглтона — предоставить пользователю один и только один объект определенного класса на весь жизненный цикл приложения. В iOS-разработке, как по мне — самый лучший пример необходимости такого объекта — класс UIApplication. Вполне логично, что в течение жизни нашего приложения у нас должен быть один-единственный объект класса UIApplication.

Итак, разберемся что такое синглтон в Objective-C и Swift на примерах из книги.

Давайте сначала узнаем как вообще создать объект какого-нибудь класса. Очень просто:

// Objective-C
[[MyClass alloc] init]
// Swift
MyClass()

И тут авторы подводят нас к мысли, что приведенным выше способом можно создать сколько угодно объектов этого класса. Таким образом, первое что нужно сделать на пути к синглтону — запретить создание объектов нашего класса извне. В этом нам поможет приватный инициализатор.

И если в swift это реализуется тривиально:

// Swift
class MyClass {

    private init() {}

}

То в objective-c все не так просто на первый взгляд. Дело в том, что все классы obj-c имеют одного общего предка: NSObject, в котором есть общедоступный инициализатор. Поэтому в файле заголовка нашего класса нужно указать на недоступность этого метода для нашего класса:

// Objective-C
@interface MyClass : NSObject

- (instancetype)init UNAVAILABLE_ATTRIBUTE;

@end

Таким образом попытка создать объект нашего класса извне вызовет ошибку на этапе компиляции. Окей. Теперь и в objective-c у нас есть запрет на создание объектов нашего класса. Правда это еще не совсем приватный инициализатор, но мы к этому вернемся через пару секунд.

Итак, по сути мы получили класс, объекты которого не могут создаваться, потому что конструктор — приватный. И что со всем этим делать? Будем создавать объект нашего класса внутри нашего же класса. И будем использовать для этого статический метод (метод класса, а не объекта):

// Swift
class MyClass {

    private init() {}

    static func shared() -> MyClass {
        return MyClass()
    }

}
// Objective-C
@implementation MyClass

+ (instancetype)sharedInstance {
    return [[MyClass alloc] init];
}

@end

И если для swift опять все просто и понятно, то с objective-c возникает проблема с инициализацией:

Вполне логично, ведь мы сказали ранее, что - (instancetype)init недоступен. И он недоступен в том числе и внутри нашего класса. Что делать? Написать свой приватный инициализатор в файле реализации и использовать его в статическом методе:

// Objective-C
@implementation MyClass

- (instancetype)initPrivate
{
    self = [super init];
    return self;
}

+ (instancetype)sharedInstance {
    return [[MyClass alloc] initPrivate];
}

@end

(да, и не забудьте вынести метод + (instancetype)sharedInstance в файл заголовка, он должен быть публичным)

Теперь все компилируется и мы можем получать объекты нашего класса таким способом:

// Objective-C
[MyClass sharedInstance]
// Swift
MyClass.shared()

Наш синглтон почти готов. Осталось только исправить статический метод так, чтобы объект создавался только один раз:

// Objective-C
@implementation Singleton

- (instancetype)initPrivate
{
    self = [super init];
    return self;
}

+ (instancetype)sharedInstance {
    static Singleton *uniqueInstance = nil;
    if (nil == uniqueInstance) {
        uniqueInstance = [[Singleton alloc] initPrivate];
    }
    return uniqueInstance;
}

@end
// Swift
class Singleton {

    private static var uniqueInstance: Singleton?

    private init() {}

    static func shared() -> Singleton {
        if uniqueInstance == nil {
            uniqueInstance = Singleton()
        }
        return uniqueInstance!
    }

}

Как видите, для этого нам понадобилась статическая переменная, в которой и будет храниться единожды созданный объект нашего класса. Каждый раз при вызове нашего статического метода она проверяется на nil и, если объект уже создан и записан в эту переменную — он не создается заново. Наш синглтон готов, ура! 🙂

Теперь немного примеров из жизни из книги.

Итак, у нас есть шоколадная фабрика и для приготовления мы используем высокотехнологичный нагреватель шоколада с молоком (я просто обожаю молочный шоколад), который будет управляться нашим программным кодом:

// Objective-C

// файл заголовка ChocolateBoiler.h
@interface ChocolateBoiler : NSObject

- (void)fill;
- (void)drain;
- (void)boil;
- (BOOL)isEmpty;
- (BOOL)isBoiled;

@end

// файл реализации ChocolateBoiler.m
@interface ChocolateBoiler ()

@property (assign, nonatomic) BOOL empty;
@property (assign, nonatomic) BOOL boiled;

@end

@implementation ChocolateBoiler

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.empty = YES;
        self.boiled = NO;
    }
    return self;
}

- (void)fill {
    if ([self isEmpty]) {
        // fill boiler with milk and chocolate
        self.empty = NO;
        self.boiled = NO;
    }
}

- (void)drain {
    if (![self isEmpty] && [self isBoiled]) {
        // drain out boiled milk and chocolate
        self.empty = YES;
    }
}

- (void)boil {
    if (![self isEmpty] && ![self isBoiled]) {
        // boil milk and chocolate
        self.boiled = YES;
    }
}

- (BOOL)isEmpty {
    return self.empty;
}

- (BOOL)isBoiled {
    return self.boiled;
}

@end
// Swift
class ChocolateBoiler {

    private var empty: Bool
    private var boiled: Bool

    init() {
        self.empty = true
        self.boiled = false
    }

    func fill() {
        if isEmpty() {
            // fill boiler with milk and chocolate
            self.empty = false
            self.boiled = false
        }
    }

    func drain() {
        if !isEmpty() && isBoiled() {
            // drain out boiled milk and chocolate
            self.empty = true
        }
    }

    func boil() {
        if !isEmpty() && !isBoiled() {
            // boil milk and chocolate
            self.boiled = true
        }
    }

    func isEmpty() -> Bool {
        return empty
    }

    func isBoiled() -> Bool {
        return boiled
    }

}

Как видите — нагреватель сначала заполняется смесью (fill), затем доводит ее до кипения (boil), и после — передает ее на изготовление молочных шоколадок (drain). Для избежания проблем нам нужно быть уверенными, что в нашей программе присутствует только один экземпляр нашего класса, который управляет нашим нагревателем, поэтому внесем изменения в программный код:

// Objective-C
@implementation ChocolateBoiler

- (instancetype)initPrivate
{
    self = [super init];
    if (self) {
        self.empty = YES;
        self.boiled = NO;
    }
    return self;
}

+ (instancetype)sharedInstance {
    static ChocolateBoiler *uniqueInstance = nil;

    if (nil == uniqueInstance) {
        uniqueInstance = [[ChocolateBoiler alloc] initPrivate];
    }

    return uniqueInstance;
}

// other methods

@end
// Swift
class ChocolateBoiler {

    private var empty: Bool
    private var boiled: Bool

    private static var uniqueInstance: ChocolateBoiler?

    private init() {
        self.empty = true
        self.boiled = false
    }

    static func shared() -> ChocolateBoiler {
        if uniqueInstance == nil {
            uniqueInstance = ChocolateBoiler()
        }
        return uniqueInstance!
    }

    // other methods

}

Итак, все отлично. Мы на 100% уверены (точно на 100%?), что у нас есть только один объект нашего класса и никаких непредвиденных ситуаций на фабрике не произойдет. И если наш код на objective-c выглядит довольно неплохо, то swift выглядит недостаточно swifty. Попробуем его немного переписать:

// Swift
class ChocolateBoiler {

    private var empty: Bool
    private var boiled: Bool

    static let shared = ChocolateBoiler()

    private init() {
        self.empty = true
        self.boiled = false
    }

    // other methods

}

Дело в том, что мы можем спокойно хранить наш объект-одиночку в статической константе shared и нам совсем необязательно писать для этого целый метод с проверками на nil. Сам объект будет создан при первом обращении к этой константе и записан в нее один-единственный раз.

А как же многопоточность?

Все будет работать хорошо ровно до того момента, как мы захотим применить в нашей программе работу с потоками. Как же сделать наш синглтон потокобезопасным?

И опять же: в swift, как оказывается, совершенно не нужно выполнять каких-либо дополнительных действий. Константа уже потокобезопасна, ведь значение в нее может быть записано только один раз и это сделает тот поток, который доберется до нее первым.

А вот в objective-c необходимо внести коррективы в наш статический метод:

// Objective-C
+ (instancetype)sharedInstance {
    static ChocolateBoiler *uniqueInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        uniqueInstance = [[ChocolateBoiler alloc] initPrivate];
    });

    return uniqueInstance;
}

Блок внутри dispatch_once гарантированно выполнится только один раз, когда самый первый поток до него доберется, все остальные потоки — будут ждать, когда закончится выполнение блока.

Итоги подведем.

Итак, мы разобрались как правильно писать синглтоны на objective-c и swift. Приведу вам итоговый код класса Singleton на обоих языках:

// Objective-C

// файл заголовка Singleton.h
@interface Singleton : NSObject

- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)sharedInstance;

@end

// файл реализации Singleton.m
@implementation Singleton

- (instancetype)initPrivate
{
    self = [super init];
    return self;
}

+ (instancetype)sharedInstance {
    static Singleton *uniqueInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        uniqueInstance = [[Singleton alloc] initPrivate];
    });

    return uniqueInstance;
}

@end
// Swift
class Singleton {

    static let shared = Singleton()

    private init() {}

}

П.С.

Хочу попросить всех читателей и комментаторов: если вы увидели какую-нибудь неточность или знаете как какой-либо из приведенных мною кусков кода написать правильнее/красивее/корректнее — скажите об этом. Я пишу здесь совсем не для того, чтоб учить других, а для того, чтоб научиться самому. Ведь пока мы учимся — мы остаемся молодыми.

Спасибо вам за внимание.

VIPER

Мы тут на работке потихоньку учим архитектуру VIPER. Поэтому, чтоб не терялись материалы, сложу их тут.

Первое с чего я начал было два видосика от Константина Кокорина:

Для закрепления небольшой пример построения master-detail приложения (от все того же Константина):

Конечно же книжка по VIPER от компании Rambler:
https://habrahabr.ru/company/rambler-co/blog/311248/

Хороший видос с презентацией по архитектуре:

И, самое главное — генератор модулей от того же Рамблера.

Xcode: наверное, лучший способ работы со сторибордами.

Этот пост является вольным переводом статьи Xcode: A Better Way to Deal with Storyboards by Stan Ostrovskiy

Некоторые примеры кода в оригинальной статье устарели (ввиду выхода Swift 3) и в переводе были изменены.

Советы и рекомендации по работе с Interface Builder.

Apple серьезно улучшили Interface Builder в новом Xcode 8. Использование size classes стало более интуитивным, возможность масштабирования сториборда — очень удобной, а полное превью прям в Interface Builder — просто великолепным. Для тех у кого были сомнения насчет использования Interface Builder, это может стать хорошими плюсами.

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

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

1. Если вы работаете в команде, используйте отдельный сториборд для каждого экрана. Даже если вы работаете один — это наверняка станет хорошей привычкой.

В вашем проекте есть один файл main.storyboard, который выглядит вот так?

С точки зрения дизайнера, все хорошо: полностью видно UI и навигацию. И это именно то, для чего Interface Builder и был создан.

Но для разработчика это несет множество проблем:

  • Контроль версий: конфликты слияния сторибордов очень трудно решать, так что работа в отдельных сторибордах сделает жизнь вашей команды проще.
  • Файл сториборда становится объемным и в нем сложно ориентироваться. Как часто вы случайно меняли constraint кликом мышки не в том вью-контроллере?
  • Вам необходимо присваивать каждому вью-контроллеру свой storyboard ID и это может привести к ошибкам: вам нужно «хардкодить» этот ID каждый раз когда хотите использовать этот вью-контроллер в коде.

Как же связать различные сториборды в вашем проекте? Есть два способа.

  1. Используйте ссылки на сториборды (storyboard referencing), которые появились в Xcode 7.

  2. Связывайте сториборды непосредственно в коде.

О первом способе вы можете почитать детальнее здесь.

Я расскажу о втором способе, так как он широко используется для сложных проектов.

2. Используйте одни и те же имена для файла со сторибордом и для связанного класса контроллера (наследника UIViewController).

Это упростит правила именования, а также даст некоторые «плюшки» о которых поговорим в пункте 3.

3. Инициализируйте сториборд непосредственно в классе контроллера.

Когда дело доходит до инициализации вью-контроллера через сториборд, я часто вижу следующий код:

let storyboard = UIStoryboard(name: “Main”, bundle: nil)
let homeViewController = storyboard.instantiateViewController(withIdentifier: “HomeViewController”)

Немного «грязновато»: вам нужно назвать сториборд, вам нужен storyboard ID вью-контроллера, и вам необходимо использовать этот паттерн каждый раз, когда вы создаете HomeViewController.

Лучше перенести этот код в сам класс контроллера и использовать статический метод, чтоб инициализировать контроллер с помощью сториборда:

class HomeViewController: UIViewController { 
    static func storyboardInstance() -> HomeViewController? { 
        let storyboard = UIStoryboard(name: “HomeViewController”, bundle: nil)
        return storyboard.instantiateInitialViewController() as? HomeViewController    
    }
}

Если вы последуете предыдущему совету (одинаковые имена файлов), то можете избежать «харкода» имени сториборда и воспользоваться String(describing:):

let storyboard = UIStoryboard(name: String(describing: self), bundle: nil)

Убедитесь, что у файла сториборда такое же имя как и у класса контроллера. Иначе ваше приложение будет «крэшится» когда вы попытаетесь создать ссылку на такой сториборд.

Это делает ваш код более читаемым и отказоустойчивым:

class HomeViewController: UIViewController {
    static func storyboardInstance() -> HomeViewController? { 
        let storyboard = UIStoryboard(name: String(describing: self), bundle: nil)
        return storyboard.instantiateInitialViewController() as? HomeViewController 
    }
}

Если вы хотите иметь доступ к вью-контроллеру через instantiateInitialViewController() убедитесь, что вы указали этот вью-контроллер как initialViewController в Interface Builder. Если у вас несколько вью-контроллеров на одном сториборде, вам придется использовать instantiateViewController(withIdentifier: _ )

Теперь, инициализация такого вью-контроллера займет одну строку:

let homeViewController = HomeViewController.storyboardInstance()

Просто и понятно, не так ли?

Вы можете использовать этот же подход для инициализации вью из nib:

class LoginView: UIView {
    static func nibInstance() -> LoginView? {
        let nib = Bundle.main.loadNibNamed(String(describing: self), owner: nil, options: nil)
        return nib?.first as? LoginView
    }
}
4. Не перегружайте свой проект переходами на сториборде.

У вас не будет переходов, если вы последуете совету из пункта 1. Но даже если у вас есть несколько вью-контроллеров в одном сториборде, использование переходов (segues) для навигации между ними — не очень хорошая идея:

  • Вам нужно дать имя каждому переходу (segue), что само по себе может привести к ошибкам. «Хардкодить» строки с именами — плохая практика.
  • Метод prepareForSegue будет просто нечитаем, когда вы будете работать в нем с несколькими segue, используя операторы ветвления if/else или switch.

Какова альтернатива? Когда вы хотите перейти к следующему вью-контроллеру по нажатию на кнопку, просто добавьте IBAction для этой кнопки и инициализируйте вью-контроллер в коде: это ведь всего одна строка, как вы помните из пункта 3.

@IBAction func didTapHomeButton(_ sender: AnyObject) {
    if let nextViewController = NextViewController.storyboardInstance() {
        // initialize all your class properties
        // nextViewController.property1 = … 
        // nextViewController.property2 = … 

        // either push or present the nextViewController,
        // depending on your navigation structure 
        // present(nextViewController, animated: true, completion: nil) 

        // or push  
        navigationController?.pushViewController(nextViewController, animated: true)
    }
}
5. Unwind segue? Не, не слышал.

Иногда навигация предполагает возврат пользователя к предыдущему экрану.

Очень распространенная ошибка: использовать новый переход для навигации к предыдущему вью-контроллеру. Такой переход создает новый экземпляр вью-контроллера, который уже находится в стэке, вместо того, чтоб убрать текущий вью-контроллер и таким образом вернуться к предыдущему.

Начиная с iOS 7, Interface Builder дает вам возможность сделать «unwind» навигационного стэка.

Unwind segue позволяет вам указать возврат на предыдущий экран. Это звучит довольно просто, но на практике это требует некоторых дополнительных действий и только сбивает с толку разработчика:

  • Обычно, когда вы создаете действие для кнопки (action), Interface Builder создаст для вас код (IBAction). В этом же случае, ожидается, что код уже написан до того, как вы зажмете «Ctrl» и перетащите действие от вашей кнопки к «Exit».
  • Обычно когда вы создаете действие для кнопки, код этого действия создается в том же классе, которому и принадлежит кнопка. Для Unwind Segues, вам нужно писать код в классе того вью-контроллера, в который этот переход произойдет.
  • Метод prepareForUnwind будет иметь все те же недостатки, что и метод prepareForSegue (см. предыдущий пункт).

Каков же более простой способ?

Проще делать это в коде: вместо создания действия «unwind» для вашей кнопки, создайте обычный IBAction и используйте dismissViewController или popViewController (в зависимости от вашей навигации):

@IBAction func didTapBackButton(_ sender: AnyObject) { 
    // if you use navigation controller, just pop ViewController:
    if let nvc = navigationController {   
        nvc.popViewController(animated: true)
    } else {
        // otherwise, dismiss it
        dismiss(animated: true, completion: nil)
    }
}

На сегодня это все. Я надеюсь, вы найдете что-то полезное для себя.

От переводчика:

Благодаря методу описанному в этой статье, я очень сильно упростил работу со сторибордами в своем текущем проекте Zumme. Пока я работал над ним один — все было прекрасно, но как только появились другие разработчики — работа со сторибордом превратилась в настоящий ад. От отчаянья мы практически перешли к «банановому методу» (можно почитать здесь в разделе «Pass the banana»).

Конечно же, в идеале нужно будет рано или поздно прийти к VIPER. Но об этом будет уже другой пост. 🙂

Как отметить свои TODO, FIXME и ERROR в Xcode.

Этот пост является вольным переводом статьи How to highlight your TODOs, FIXMEs, & ERRORs in Xcode by Hector Matos

Это был самый обычный день: я писал код, устранял баги и вообще все было прекрасно. Именно тогда я написал блок кода, к которому нужно было вернуться позже. Это обычный случай, с которым вы тоже вероятно сталкивались: нужно было взаимодействовать с API который еще не был готов. Я знал общую структуру объекта, который получу по API, но я еще не мог протестировать работу с ним. Как и любой другой разработчик, я написал комментарий, который выглядит так:

В этот момент я хотел бы создать предупреждение в Xcode, такое же как мы привыкли делать в Objective-C с помощью директив компилятора:

Но увы, так не получилось и я загрустил.

Как человек действия, я сделал то, что и следовало: я действовал. Оказывается вы можете добавить run-скрипт, чтоб получить необходимый функционал.

RUN SCRIPT BUILD PHASES

Xcode поддерживает внутренние bash-команды или скрипты в различных фазах вашего цикла разработки. Вы можете запустить bash-скрипт в любое время до или после сборки, запуска, тестирования, профилирования, анализа или даже архивирования!

Чтобы сделать это, перейдите в «Build Phases» своего проекта в Xcode, нажмите на «+» слева вверху и затем из выпадающего меню выберите «New Run Script Phase»:

Затем вы увидите новый раздел в котором можете написать bash-скрипт. Если вы уже эксперт по написанию Swift-скриптов после прочтения поста scripting in swift, вы можете просто положить файл со скриптом в корневой каталог проекта и вызвать его из своего нового run-скрипта.

# Отмечаем ваши TODO, FIXME и ERROR с помощью «родных» предупреждений Xcode

В тело вашего run-скрипта поместите вот этот замечательный код:

TAGS="TODO:|FIXME:"
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"

С этого момента вы будете видеть предупреждения когда поставите метку TODO: или FIXME: в комментарии! Посмотрите как работает эта магия:

Не будем останавливаться на достигнутом и исправим скрипт таким образом, чтобы подсвечивать ошибки используя //ERROR: в комментариях. Как вы знаете, бывают ситуации, когда мы хотим обратить особое внимание, выделив ошибку с помощью //ERROR:. Чтобы сделать это, измените ваш bash-скрипт вот так:

TAGS="TODO:|FIXME:"
ERRORTAG="ERROR:"
find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -print0 | xargs -0 egrep --with-filename

Не знаю как вы, но я, наверное — самый забывчивый человек в мире. В конце дня у меня не всегда закончен текущий фрагмент кода и мне нравится использовать //ERROR: чтобы напомнить самому себе над чем работать завтра.

Когда моя IDE выглядит так, я сразу чувствую, что необходимо закончить уже начатое. И не переживайте, ошибки генерируемые этим скриптом не препятствуют сборке проекта.

ЗАКЛЮЧЕНИЕ

В своей повседневной работе вы всегда столкнетесь с блоком кода, к которому нужно будет вернуться позже, но сейчас вы вынуждены поставить «заплатку» и двигаться дальше. К сожалению, даже простого //TODO:, //FIXME: или //ERROR: в комментарии, просто недостаточно. Вы удивитесь сколько людей забывают о своих //TODO:, //FIXME: и //ERROR: в проекте. Использование run-скрипта в этой ситуации — отличный способ, чтобы убедиться, что не упустите ничего в своем цикле разработки. Надеюсь это поможет!

Счастливого кодинга, товарищи ботаники!

П.С. Из комментариев к оригинальному посту извлечена еще одна модификация скрипта, на случай если вам все-таки нужно препятствовать сборке проекта при наличии пометок //ERROR: в коде:

TAGS="TODO:|FIXME:"
ERRORTAG="ERROR:"
OUTPUT=$(find "${SRCROOT}" \( -name "*.h" -or -name "*.m" -or -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$|($ERRORTAG).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/" | perl -p -e "s/($ERRORTAG)/ error: \$1/")
ECHO "$OUTPUT"

if [[ $OUTPUT == *" error: "* ]]
then
exit 1
fi