Всем привет! Я — начинающий свифтер, то есть изучаю Swift без опыта ObjC. Недавно мы с компаньонами начали проект, требующий приложение под iOS. А еще у нас есть idée fixe: с нами непременно должен работать студент из Физтеха, а приложение должно быть написано на языке Swift. И вот, пока мы ищем физтеховцев и знакомимся с ними, я решил не терять время и параллельно начать своими силами пилить проект на Swift. Так я впервые открыл XCode.
Вдруг обнаружилось много знакомых, которые точно так же не имея опыта мобильной разработки, стали осваивать ее именно посредством Swift, а не ObjC. Кто-то из них подтолкнул меня поделиться опытом на Хабре.
Итак, вот топ пять «ловушек», своевременное понимание которых точно бы сэкономило мне время.
1. Блоки (замыкания) могут порождать утечки памяти
Если вы, как и я, пришли в мобильную разработку минуя ObjC, то, наверное, одним из самых важных вводных материалов я бы назвал документацию Apple по Automatic Reference Counting. Дело в том, что при «скоростном» изучении нового языка путем погружения (то есть, начав сразу пилить реальный проект) у вас может развиться склонность пропускать «теорию», не имеющую отношения к задачам типа «показать всплывающее окно здесь и сейчас». Однако мануал по ARC содержит очень важный раздел, специально объясняющий неочевидное свойство замыканий, порождающее утечки.
Итак, пример «ловушки». Простой контроллер, который никогда не очистится из памяти:
class ViewController: UIViewController {
var theString = "Hello World"
var whatToDo: (()->Void)!
override func viewDidLoad() {
whatToDo = { println(self.theString) }
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
whatToDo()
navigationController!.setViewControllers([], animated: true)
}
deinit { println("removed from memory") }
}
Запускаем и тычем пальцем в экран. Если у нас мало опыта, то мы ошибочно ожидаем увидеть в консоли:
Hello World
removed from memory
Но на самом деле мы видим:
Hello World
То есть мы потеряли возможность обращаться к нашему контроллеру, а тот остался висеть в памяти.
Почему же? Оказывается, вызов self вот в этой невинной строчке
{ println(self.theString) }
автоматически создает строгую ссылку на контроллер из замыкания whatToDo. Так как на whatToDo уже строго ссылается сам контроллер, то в результате мы получаем два объекта в памяти, строго ссылающихся друг на друга — и они никогда не вычистятся.
Если внутри замыкания НЕ используется вызов self, то такого подвоха НЕ возникает.
В свифте, конечно, предусмотрено решение, которое Apple почему-то называет элегантным. Вот оно:
whatToDo = { [unowned self] in println(self.theString) }
Et voila! Вывод: будьте внимательны с жизненным циклом всех замыканий, содержащих вызов self.
2. Array, Dictionary и Struct по умолчанию немутабельные типы, никогда не передающиеся по ссылке
Когда стоит задача освоить новый язык очень быстро, я склонен забивать на чтение доков по таким интуитивно очевидным типам, как массивы и словари, полагаясь на то, что autocomplete научит меня всему, что надо, непосредственно в процессе кодинга. Такой торопливый подход все-таки подвел меня в ключевом месте, когда я всю дорогу воспринимал «массивы массивов» и «массивы страктов» как наборы ссылок (по аналогии с JS) — они оказался наборами копий.
После прочтения доков я все-таки прозрел: в Свифте массивы и словари являются страктами и поэтому, как любые стракты, передаются не по ссылке, а по значению (путем копирования, который компилятор оптимизирует под капотом).
Пример, иллюстрирующий мега-подвох, который вам приготовил Свифт:
struct Person : Printable {
var name:String
var age:Int
var description:String { return name + " ((age))" }
}
class ViewController: UIViewController {
var teamLeader:Person!
var programmers:[Person] = []
func addJoeyTo(var persons:[Person]) {
persons.append(Person(name: "Joey", age: 25))
}
override func viewDidLoad() {
teamLeader = Person(name: "Peter", age: 30)
programmers.append(teamLeader)
// Строим ошибочные ожидания...
teamLeader.name = "Peter the Leader"
addJoeyTo(programmers)
// ...и вот он, момент истины
println(programmers)
}
}
При запуске, если мы ошибочно мыслим в ключе «передача по ссылке», то ожидаем увидеть в консоли:
[Peter the Leader (30), Joey (25)] // Результат 1
Вместо этого видим:
[Peter (30)] // Результат 2
Будьте внимательны! Как же выйти из положения, если нам в действительности нужен именно первый результат? На самом деле, каждый конкретный случай требует индивидуального решения. В данном примере сработает вариант замены struct на class и замены [Person] на NSMutableArray.
3. Singleton Instance — выбираем наилучший «хак»
Ловушка заключается в том, что на текущий момент классы в Swift не могут иметь статических хранимых свойств, а только статические методы (class func) или статические вычисляемые свойства (class var x:Int {return 0}).
При этом сам Apple вообще не имеет предубеждений против глобальных инстансов в духе паттерна Singleton — в этом мы регулярно убеждаемся, используя такие перлы, как NSUserDefaults.standardUserDefaults(), NSFileManager.defaultManager(), NSNotificationCenter.defaultCenter(), UIApplication.sharedApplication(), ну и так далее. Мы действительно получим статические переменные в следующем общем обновлении — Swift 1.2.
Так как же нам создать собственные такие же инстансы в текущей версии Swift? Есть несколько возможных «хаков» под общим названием Nested Struct, но самый лаконичный из них — это следующий:
extension MyManager {
class var instance: MyManager {
func instantiate() -> MyManager {
return ... // постройте свой инстанс здесь
}
struct Static {
static let instance = instantiate() // lazily loaded + thread-safe!
}
return Static.instance
}
}
Стракты в свифте не только поддерживают статические хранимые свойства, но также по умолчанию дают им отложенную поточно-ориентированную инициализацию. Вот это профит! Не зная об этом заранее, можно зря потратить время на написание и отладку лишнего кода.
Внимание! В следующей версии свифта (1.2) этот «хак» уже не понадобится, но дата общего релиза не известна. (Уже доступна бета-версия для тестирования, но для этого необходима также бета-версия XСode6.3, билд из которой от вас не примет Appstore. Короче — ждем глобального релиза.)
4. Методы didSet и willSet не будут вызваны в процессе выполнения конструктора
Вроде мелочь, но это способно ввести вас в тотальный ступор при отладке багов, если вы не знаете этого. Поэтому если вы запланировали какой-то набор манипуляций внутри didSet, который важен как при инициализации, так и далее в течение жизненного цикла объекта, делать это нужно таким образом:
class MyClass {
var theProperty:OtherClass! {
didSet {
doLotsOfStuff()
}
}
private func doLotsOfStuff () {
// здесь реагируем на didSet theProperty
}
...
init(theProperty:OtherClass)
{
self.theProperty = theProperty
doLotsOfStuff()
}
}
5. Нельзя просто так взять и обновить UI, когда пришел ответ с сервера
Программисты с опытом ObjC могут посмеяться над этой «ловушкой», потому что она должна быть общеизвестна: методы, связанные с UI, безопасно дергать только из главного потока. Иначе — непредсказуемость и баги, толкающие в тотальный ступор. Но это наставление почему-то проходило мимо меня, пока я, наконец, не столкнулся с жуткими багами.
Пример «проблемного» кода:
func fetchFromServer() {
let url = NSURL(string:urlString)!
NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: { data, response, error in
if (error != nil) {
...
} else {
self.onSuccess(data)
}
})!.resume()
}
func onSuccess(data) {
updateUI()
}
Обратите внимание на блок completionHandler — все это будет исполняться вне главного потока! Тем, кто еще не столкнулся с последствиями, советую не экспериментировать, а просто не забыть обставить updateUI следующим образом:
func onSuccess(data) {
dispatch_sync(dispatch_get_main_queue(), {
updateUI()
})
}
Это типичное решение. Одной строчкой мы возвращаем updateUI обратно в главный поток и избегаем неожиданностей.
На сегодня все. Всем новичкам успехов!
Опытныее из mobile — ваши замечания будут очень полезны мне и всем начинающим свифтерам.
Автор: nkul