Адаптивный UICollectionView

Недавно мне понадобилось сделать адаптивный лэйаут для UICollectionView для поддержки экранов всех iPhone и iPad, а так же новой функции SplitView. Старый метод, который я использовал требовал слишком много кода, в котором можно было легко запутаться.

Как я делал раньше

Класс контроллера я назначал делегатом UICollectionViewDelegateFlowLayout и использовал эти методы:

collectionView(_:layout:sizeForItemAtIndexPath:)
collectionView(_:layout:insetForSectionAtIndex:)
collectionView(_:layout:minimumLineSpacingForSectionAtIndex:)
collectionView(_:layout:minimumInteritemSpacingForSectionAtIndex:)

Пример. Мне нужно красиво уместить ячейки 100х100 pt  на экране с расстояниями в 4 pt между ними, я делал так:

    // настраиваем размер и расстояния. insetForSectionAtIndex задан в сториборде, так как менять его в коде нет необходимости
    let cellWidth:CGFloat = 100, spacing:CGFloat = 4

    // задаем размер ячейки в зависимости от ширины collection view
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        let columns:CGFloat = ceil(collectionView.bounds.size.width / cellWidth)
        let size = (collectionView.bounds.size.width - ((columns-1)*spacing)) / columns
        return CGSizeMake(size, size)
    }

    // возвращаем расстояния
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
        return spacing
    }

    // здесь я обновлял высоту collection view при показе страницы и при изменении ориентации устройства (viewDidLoad: и viewWillTransitionToSize:
    func collectionViewUpdateLayout() {
        self.collectionView.performBatchUpdates(nil) { (_) -> Void in
            self.collectionViewHeight.constant = self.collectionView.contentSize.height
        }
    }

Пример 2. Если ширина UICollectionView больше 450 pt, используем две колонки квадратных ячеек. Расстояния между ними и границами UICollectionView – 4 pt. Если ширина меньше, то колонка одна, а расстояния – 0.

   // Задаем минимальную ширину для двух колонок. если ширина collection view станет меньше, у нас будет отображаться одна колонка ячеек. Так же задаем отступы между ячейками и границами collection view.
    let minimalCollectionViewWidthForDoubleColumnMode:CGFloat = 450,
    edgeInsets:CGFloat = 4,
    spacing:CGFloat = 4,
    lineSpacingForOneColumnMode:CGFloat = 0

    // считаем размер ячеек
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        let size = collectionView.bounds.size.width
        if size > minimalCollectionViewWidthForDoubleColumnMode {
            return CGSizeMake((size-edgeInsets*2-spacing)/2, (size-edgeInsets*2-spacing)/2)
        }
        else{
            return CGSizeMake(size, size)
        }
    }

    // возвращаем расстояния
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
        return spacing
    }

    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
        let size = collectionView.bounds.size.width
        if size > minimalCollectionViewWidthForDoubleColumnMode {
            return spacing
        }
        else {
            return lineSpacingForOneColumnMode
        }
    }

    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
        if collectionView.bounds.size.width > minimalCollectionViewWidthForDoubleColumnMode {
            return UIEdgeInsetsMake(edgeInsets, edgeInsets, edgeInsets, edgeInsets)
        }
        else{
            return UIEdgeInsetsZero
        }
    }

(в коде выше могут быть серьёзные недочёты, но он служил мне верой и правдой в реальном приложении)

Мой новый способ

Я написал расширение для UICollectionViewLayout, которое позволяет задавать все эти значения в одну строчку кода, а если нужно — создавать сложные правила для разной ширины UICollectionView.

Улучшаем Пример 1.

     // Та самая функция, которую мы вызываем во viewDidLoad: и viewWillTransitionToSize: withTransitionCoordinator:
    func collectionViewUpdateLayout() {
        // Задаем правило
        collectionView.collectionViewLayout.makeAdaptive(withMinimumCellWidth: 100, cellAspectRatio: 1, spacing: CGSizeMake(4, 4), andEdgeInsets:UIEdgeInsetsMake(4, 4, 4, 4))
       // Обновляем высоту collection view
        self.collectionView.performBatchUpdates(nil) { (_) -> Void in
            self.collectionViewHeight.constant = self.collectionView.contentSize.height
        }
    }

Улучшаем Пример 2.

     // Та самая функция, которую мы вызываем во viewDidLoad: и viewWillTransitionToSize: withTransitionCoordinator:
     func collectionViewUpdateLayout() {
        // Создаём два правила для разных размеров экрана
        let rule1 = AdaptiveLayoutRule(columns: 1, width: 0, spacing: CGSizeZero, andInsets:UIEdgeInsetsZero)
        let rule2 = AdaptiveLayoutRule(columns: 2, width: 450, spacing: CGSizeMake(4, 4), andInsets:UIEdgeInsetsMake(4, 4, 4, 4))
       // Применяем правило
        self.collectionViewLayout.makeAdaptiveWithRules([rule1, rule2], aspectRatio: 1)
       // Обновляем высоту collection view
        self.collectionView.performBatchUpdates(nil) { (_) -> Void in
            self.collectionViewHeight.constant = self.collectionView.contentSize.height
        }
    }

 

Вот так

Если в вашем проекте есть необходимость использовать UICollectionView с простым и адаптивным лэйаутом, надеюсь, моё расширение вам пригодится.

Guard — зачем же он нужен?

Вы читаете перевод статьи Эрика Керни

Когда я впервые увидел оператор guard во время Apple’s Platform State of the Union, я не мог до конца понять, зачем бы я стал им пользоваться и что же он из себя представляет? Вот короткое описание:

Как и оператор if, guard исполняет код полагаясь на логическое значение выражения. В отличии от if, guard исполняет код только при получении false. Можно думать о нём как об Assert, только программа не будет завершена.

Вливаемся

Возьмём простой пример для сравнения старой техники и использования guard:

func fooManualCheck(x: Int?) {
    if x == nil || x <= 0 {
        // Условия не соблюдены, выходим из функции
        return
    }
    
    // Работаем с x
    x!.description
}

Это самый простой способ (в стиле Objective-C) убедиться, что значение существует и удовлетворяет условию. Хотя, работает оно прекрасно, в нём есть несколько недостатков:

  1. Мы проверяем условие, которое нам не нужно, вместо проверки значения, которое нас интересует. Код становится очень запутанным, если у нас несколько таких проверок. Мы ведь надеемся, что наше условие на самом деле не пройдёт.
  2. Так же нужно “силой развернуть” (force unwrap) опциональное значение.

Swift представил нам более чистый способ сделать это и избавил нас от этих недостатков с помощью Optional Binding:

func fooBinding(x: Int?) {
    if let x = x where x > 0 {
        // Работаем с x
        x.description
    }
    
    // Условия не соблюдены, выходим из функции
}

Этот вариант убирает оба недостатка старой функции, но добавляет один новый. Мы помещаем нужный нам код внутрь проверки условия, вместо того, чтобы писать его после. Поначалу проблему можно и не заметить, но можете представить, как запутанно это будет выглядеть, если вложить в функцию ещё несколько условий, которые нужно выполнить перед запуском кода.

Самый чистый способ — сперва проверить каждое условие и выйти, если одно из них не встречено. Это позволит легко понять, какие условия вызывают выход из функции.

И тут на помощь приходит guard:

func fooGuard(x: Int?) {
    guard let x = x where x > 0 else {
        // Условия не соблюдены, выходим из функции
        return
    }
    
    // Работаем с x
    x.description
}

Использование guard решает все 3 проблемы, упомянутые выше:

  1. Идёт проверка условий, которые нам действительно важны. Если условие не соблюдено, запускается блок else, который обязательно выводит из функции. Если вы забудете постаивть return, компилятор сообщит вам об ошибке.
  2. Если условие соблюдено, опциональная переменная автоматически развёрнута и доступна внутри guard.
  3. Мы проверяем условия рано и функция будет просто читаема.

Классно ещё то, что это работает и для не-опциональных значений:

func fooNonOptionalGood(x: Int) {
    guard x > 0 else {
        // Условия не соблюдены, выходим из функции
        return
    }
    
    // Работаем с x
}
 
func fooNonOptionalBad(x: Int) {
    if x <= 0 {
        // Условия не соблюдены, выходим из функции
        return
    }
    
    // Работаем с x
}

Сворачиваемся

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

Interface Builder хочет вас убить

Interface Builder — инструмент, входящий в состав Xcode. Он помогает создавать интерфейсы для приложений iOS, OS X и других платформ Apple. Последние разработки Apple, от начала и до конца доступны всем, остаётся лишь скачать последнюю версию.

Тут-то и начинаются проблемы…

The document “Main_iPhone.storyboard” could not be opened. The operation couldn’t be completed. (com.apple.InterfaceBuilder error -1.)

2014 год, загружен Xcode 6 beta 1. В одном из моих проектов главный файл интерфейса отказался напрочь открываться в Interface Builder. Как человек, надеющийся на корпорацию добра и пафоса, я решил подождать. Не может же ошибка быть у всех? Нельзя судить софт по бета версиям.

Проходит месяц, другой. Выходит Xcode 6 GM, а затем и релиз — без результатов. Как не открывался сториборд, так и не открывается.

Пользуясь старым Xcode 5 я беспощадно вырезал вью-контроллеры из сториборда, лишь бы открылся. После долгого мучения, я нашёл корень проблемы — заданные ранее .png изображения в UIImageView. Вот… Как так получилось? Удалил — всё работает.

Мой вопрос на Stack Overflow набрал 2.5 тысячи просмотров, а значит случай далеко не уникальный.

Проходит год. На дворе 2015-ый, загружен Xcode 7 GM. Прошлогоднюю проблему я не забыл и скрестив пальцы успешно открыл своё сториборд. Отлично! Однако, долго радоваться не пришлось — приложение намертво зависало при открытии некоторых вью-контроллеров.

Накопив опыт за год разработки я воспользовался Instruments, ещё одним полезным инструментом из набора Xcode и дал ответ на свой вопрос даже раньше, чем кто-то его прочитал. Углубившись в стэк вызовов, я заметил, что один из методов UITextView помогает поглощать 99% времени процессора. Не долго думая я очистил заданный текст в UITextView в зависавших вью-контроллерах, и угадайте что? Заработало!

Немногим позже. Xcode 7, релиз. Решил опробовать новый элемент StackView, который Apple представила нам в этом году. Расставил элементы, натянул автолэйаут и тут — Xcode повис. С моей манией жать cmd+C я долго не думал, завершил процесс и открыл проект заново. Но не тут-то было. При открытии сториборда Xcode упорно продолжал зависать, не давая мне входу.

Исходом этой проблемы был откат на старую версию файла без StackView, которые мне так и не удалось опробовать в бою. А продолжать я не стал, так как хотел вспомнил, что они доступны только с iOS 9.

Я не представляю, как такие странные ошибки возникают в столь закрытой экосистеме, их постоянство не может не удивлять. Но как бы больно ни было, я люблю Xcode, куда же я без него?