Адаптивный 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 в своём коде как можно раньше. Ваш код сразу станет намного более простым для восприятия и дальнейших улучшений.