Все новости с меткой: swift


Существует довольно-таки распространенная  необходимость сигнализировать о каких-либо событиях от одного к нескольким объектам. В Swift это решается двумя способами из коробки:

  1. KVO, которое тянет за собой обязательство наследоваться от NSObject
  2. NotificationCenter, который до iOS 11 SDK хранил сильную ссылку на объект-наблюдатель, что приводило к утечке памяти, если вовремя не отписаться. Еще он неудобен тем, что необходимо отписываться явным образом от конкретной сигнатуры нотификации, чтобы случайно не отписать от других важных событий текущий объект, если использовать removeObserver(self)

Еще один годный вариант - сделать свой Observable, который будет:

  1. Работать на кложурах, без использования NSObject-based классов или @objc-методов
  2. Самостоятельно больше не трекать умерших наблюдателей и не слать нотификации мертвому объекту

Идея не нова, в С++ уже давно используется boost::signal, примерно похожее и удобное будем делать на Swift


Для начала нужно научиться хранить кложуру, чтобы впоследствии можно было бы ее вызвать. Создадим некий "держатель кложуры", шаблонным параметром которого будут являтся типы параметров вызова аргументов. Забегая вперед, держатель кложуры будет сообщать о своем уничтожении к Observable, чтобы тот почистил список наблюдателей

В самом Observable будет храниться список кложур, завернутых в ClosureHolder. Но не все так просто, список держателей кложур будет держать сильную ссылку на держателя кложуры, отсюда она никогда самостоятельно не уничтожится. Для этого нужно научиться складывать объекты в массив, при этом не увеличивая счетчик ссылок на этот объект. Еще один враппер:

Теперь осталось сделать сам Observable, который будет добавлять, очищать и вызывать кложуры:

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

Как вообще этим пользоваться:

Как видим, кейсы с Test 2 и Test 4 выведены не будут, т.к. наблюдатель был отписан от нотификаций. Полный кусок кода лежит на гитхабе.

Прощай, NotificationCenter. Ну почти

Подробнее




Недавно затащил в проект SwiftLint, чтобы код как можно ближе соответствовал Swift Style Guide, ну чтобы в общем был по всем канонам. Куча ворнингов, и довольно часто встречается вот такой:

Line Length Violation: Line should be 120 characters or less: currently 139 characters (line_length)

Вылазит для куска кода, где в wireframe инстанциируется View-слой для VIPER-модуля. Аналогично и для получения ячейки по идентификатору из UICollectionView / UITableView

Казалось бы, куда уж короче, ведь строчку некуда сокращать. Нашлась достаточно удобная оптимизация, позволяющая сделать код более читаемым, а также избавиться от строковых констант, для которых автокомплит не сработает. Кроме повышения читаемости кода, исключаем риск опечататься в строковом идентификаторе, который задан в Storyboard


Глядя на код выше, мы-таки можем что-то сделать, чтобы не писать 2 раза 2 раза MyViperModuleViewController. Как минимум идея заключается в том, чтобы давать названия для вьюконтроллеров в самих сторибоардах ровно так, как они и называются

Для начала научимся получать текстовый идентификатор из имени нужного класса:

Ну и сделать укороченные версии методов для UIStoryboard, UICollectionView и UITableView:

Получаем более читаемый код, в котором опущены id вьюконтроллеров и ячеек:

??????

PROFIT

Подробнее




Представим, что у нас бывают тяжеловатые задачи, которые на некоторое время заметно блокируют UI приложения. Например, это может быть чтение и десериализация какого-нибудь JSON-файла из Bundle на старте приложения. Для заметного ускорения можно разгрузить главный runloop приложения, а также задействовать для решения задачи другие свободные ядра процессора на смартфоне.

Для этого нам поможет класс-хелпер, чем-то напоминающий Promise, но без возможности зафейлиться, т.е. результат будет возвращаться всегда. Базироваться он будет на DispatchQueue, где в глобальной очереди будет исполнятся сама задача, а результат возвращаться в главную очередь обратно.


Как пользоваться?

А вот пользоваться получается очень удобно:

async {

    print("heavy task impl");

}.then {

    print("perform UI updates");

}

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

А использовать вызов этой фунции одно удовольствие: никакого вложенного спагетти из кода между DispatchQueue

loadImageAsync(url).then { image in
    imageView.image = image;
}

Подробнее




В общем, задача такая, сделать почти как в андроиде, чтобы плейсхолдер в UITextField в момент фокуса улетал наверх, а также само поле ввода было подчеркнуто и линия становилась полупрозрачной при потере фокуса.

Swift Custom TextField

 

Реализовано, конечно, не совсем как в гайдлайне материал дизайна, но задача была сформулирована именно таким образом. В реализации используется CoreGraphics Affine Transform для плавной анимации UILabel для плейсхолдера. Также переопределено само свойство placeholder со своими сеттерами и геттерами для текста нового UILabel. Можно задавать текст прямо из storyboard и, внимание, менять цвет текста плейсхолдера, ведь раньше приходилось хачить в рантайме через attributedPlaceholder. Исходный код лежит на гитхабе.

Подробнее




В какой-то момент времени мы понимаем, что первый релиз уже на подходе и пора бы добавить локализацию проекта. Делается это легко, настройках проекта добавляется новая локализация, на основе базового storyboard будет сгенерирован файл Main.strings, в котором по ObjectId идет замена заголовков контролов на нужный язык.

Это все очень классно, но проблема таится в следующем - как обновить этот strings-файл новыми строками в добавок к уже существующем, к следующему релизу обязательно появляются новые ViewController-ы. Решения нормального нет: можно либо перегенерить заново этот strings-файл с непереведенными строчками, потом глазом искать новые и добавлять в старый файл. Либо руками выцеплять ObjectId, тыкая на свойства каждого контрола и вручную добавлять для них поля для перевода. Все это неудобно и не понятно, по каким причинам нельзя было встроить это в XCode.

В целом я придумал простое решение в виде скрипта на bash, который генерит заново файл локализации, смотрит в существующий файл и если не находит в нем существующего ObjectID - добавляет новую строчку с локализацией
 


Подробнее




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

Выглядит все это как-то вот так:

func delay(seconds: Double, closure: () -> ())  {

dispatch_after(

dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC))),

dispatch_get_main_queue(),

closure

);

}

 В итоге на практике все стало гораздо проще

print("Before");

delay(0.5) {

print("After delay");

}

print("After");

Подробнее




Раньше в стареньком Objective-С для локализации строк активно использовалась функция NSLocalizedString(@"myString", nil), которая затем мигрировала в свифт, но почему-то без optional параметра comment. Тягать такую конструкцию всякий раз стало крайне раздражительным и неуклюжим.

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

prefix func ~(s: String) -> String {
    return NSLocalizedString(s, comment: "");
}

Ну вот, теперь можно удобно определять, какие строки должны подвергнуться локализации

print(~"Hello world");

Список доступных операторов для перегрузки можно увидеть здесь
 

Подробнее




Вот прямо руки чешутся написать про это, а точнее высказать свое негодование.

Представьте, что вам вдруг во время программирования яблокодевайса понадобилось указать какой-либо цвет (ну там подхайлайтить что-то), то вы, определившись с цветом с помощью пипетки или на глаз (ну там ВЫРВИГЛАЗНОЖЕЛТЫЙ) пытаетесь создать объект класса UIColor. И тут начинается ступор. Я конечно понимаю, что компания Apple еще те извращенцы пытается быть не как все, но чтобы извратиться и придумать конструктор от объекта UIColor в виде покомпонентно разложенных каналов, причем не от нуля до 0xff (да-да, я чуть не охренел от эротической фантазии того, кто это придумал), — а от дробного числа в  интервале [0..1]!!!!

Блин, ну ребята, задавать цвет в rgb hex формате - это стандарт, который используется ну просто везде, начиная с самых древнейших версий HTML, CSS, Qt и так далее. Даже в андроиде не поленились написать метод Color.parseColor. Максимум, где я встретил такую нотацию - это при создании цвета в OpenGL. Т.е. примерно у каждого ui-разработчика есть представление в голове, что вот, противно- приятно-голубой цвет, который используется просто повсюду - это #0099cc, оранжевый - это наоборот надо поменять местами каналы, и так далее. И тут у тебя начинается разрыв мозга, как привычный цвет в голове быстренько перевести в систему счисления от нуля до единицы?)) не, ну я конечно заметил, что разработчики ПРОВЕЛИ ИССЛЕДОВАНИЯ и заметили, что 50 оттенков серого настолько популярны, что они сделали специальный конструктор для этого - colorWithWhite:alpha:

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


UIColor* ColorFromStr(NSString *colorStr)
{
	unsigned int color = 0;
	float r, g, b, a = 1.0;
	NSScanner *scanner = [NSScanner scannerWithString:colorStr];
	[scanner setScanLocation:1];
	[scanner scanHexInt:&color];
	
	if ([colorStr length] == 9) {
		a = (color & 0xff) / 255.0;
		color >>= 8;
	}
	r = ((color & 0xff0000) >> 16) / 255.0;
	g = ((color & 0xff00) >> 8) / 255.0;
	b = (color & 0xff) / 255.0;
	return [UIColor colorWithRed:r green:g blue:b alpha:a];
} 

UPD: Либо можно тоже самое изобразить на Swift:

extension UIColor {
    convenience init(htmlColor: String) {
        var color:UInt32 = 0;
        var r:CGFloat, g:CGFloat, b:CGFloat, a:CGFloat = 1.0;
        let scanner:NSScanner = NSScanner(string: htmlColor);

        scanner.scanLocation = 1;
        scanner.scanHexInt(&color)
        
        if (htmlColor.characters.count == 9) {
            a = CGFloat(color & 0xff) / 255.0;
            color >>= 8;
        }
        r = CGFloat((color & 0xff0000) >> 16) / 255.0;
        g = CGFloat((color & 0xff00) >> 8) / 255.0;
        b = CGFloat(color & 0xff) / 255.0;
        self.init(red:r, green:g, blue:b, alpha:a);
    }
}

Подробнее