Сайзклассы

В iOS для характеристики доступного на экране места введено понятие сайзклассов. Каждому устройству и положению дисплея задано по два сайзкласса: вертикальный и горизонтальный, чьи значения определяют размеры (обычный или компактный) соответствующих сторон. Device size classes overview

Можно заметить, что на Айпадах сайзклассы всегда одинаковые, исключением является режим многозадачности, в нем каждое окно будет иметь свои значения в зависимости от занимаемой доли экрана. iPad multitasking size classes overview

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

Для примера рассмотрим ситуацию — есть один общий для всех устройств интерфейс с привязкой объектов к сайзклассам, занимающей в портретном режиме больше вертикаль, а в альбомном горизонталь. Но как мы знаем, на Айпадах сайзклассы не меняются при повороте, остается выходить из ситуации программными путями.

На текущий момент в iOS SDK для этих целей существует один способ — метод setOverrideTraitCollection класса UIViewController. Через него можно переопределять различные характеристики дочерних контроллеров. Последнее означает, что придется создавать контроллер-контейнер, в который помещать основной. Возвращаясь к нашему примеру, алгоритм изменения сайзклассов при повороте Айпада может выглядеть следующим образом:

import UIKit

class ContainerViewController: UIViewController {
    weak var viewController: ViewController!

    override func viewDidLoad() {
        super.viewDidLoad()
        viewController = childViewControllers[0] as! ViewController
    }

    // Catch iPad display resizing and change size classes for main view controller
    override func viewWillTransitionToSize(size: CGSize,
          withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
        
        if traitCollection.userInterfaceIdiom == .Pad {
            let oldTraitCollection = viewController.traitCollection
            let newTraitCollection: UITraitCollection
            
            if size.width > size.height {
                // Landscape orientation
                newTraitCollection = UITraitCollection(traitsFromCollections:
                                       [oldTraitCollection,
                                        UITraitCollection(horizontalSizeClass: .Regular),
                                        UITraitCollection(verticalSizeClass: .Compact)])
            } else {
                // Portrait orientation
                newTraitCollection = UITraitCollection(traitsFromCollections:
                                       [oldTraitCollection,
                                        UITraitCollection(horizontalSizeClass: .Compact),
                                        UITraitCollection(verticalSizeClass: .Regular)])
            }
            
            setOverrideTraitCollection(newTraitCollection, 
                                       forChildViewController: viewController)
        }
    }
}

Готовый тестовый проект доступен по ссылке: ManualSizeClassesChange.zip.

Округление числа до нужной точности

Стандартная библиотека содержит ряд полезных функций для работы с числами: floor, round и trunc. Все они позволяют так или иначе округлять значения, но при этом точность задать, увы, нельзя. Обойти данное ограничение можно небольшой надстройкой:

import Foundation

func floorDouble(doubleValue: Double, toPrecision: Int) -> Double {
    return floor(doubleValue * pow(10, Double(toPrecision))) / pow(10, Double(toPrecision))
}

func roundDouble(doubleValue: Double, toPrecision: Int) -> Double {
    return round(doubleValue * pow(10, Double(toPrecision))) / pow(10, Double(toPrecision))
}

func truncDouble(doubleValue: Double, toPrecision: Int) -> Double {
    return trunc(doubleValue * pow(10, Double(toPrecision))) / pow(10, Double(toPrecision))
}

let number: Double = 35712.5745

floor(number)
// Output: 35712

floorDouble(number, toPrecision: 3)
// Output: 35712.574

round(number)
// Output: 35713

roundDouble(number, toPrecision: 3)
// Output: 35712.575

trunc(number)
// Output: 35712

truncDouble(number, toPrecision: 3)
// Output: 35712.574

truncDouble(number, toPrecision: -3)
// Output: 35000

Автосброс оттенка и насыщенности цвета

В процессе разработки компонента выбора цвета пришлось столкнуться с парой неприятных моментов, касающихся работы с HSB-составляющими на нулевых значениях. Первым открытием был сброс оттенка при нулевой насыщенности, а вторым — обнуление и оттенка, и насыщенности при нулевой яркости.

import UIKit

var color: UIColor

var hue = CGFloat()
var saturation = CGFloat()
var brightness = CGFloat()

color = UIColor(hue: 1, saturation: 1, brightness: 1, alpha: 1)
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
// Output: 1.0 1.0 1.0

// Saturation = 0 resets hue to 0
color = UIColor(hue: 1, saturation: 0, brightness: 1, alpha: 1)
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
// Output: 0.0 0.0 1.0

// Brightness = 0 resets hue and saturation to 0
color = UIColor(hue: 1, saturation: 1, brightness: 0, alpha: 1)
color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
// Output: 0.0 0.0 0.0

NSLayoutAnchor — альтернатива NSLayoutConstraint

До выхода iOS 9 программное создание ограничений происходило через инициализацию объектов NSLayoutConstraint, включавшую длинный список передаваемых аргументов или код на Visual Format Language, однако с появлением нового класса NSLayoutAnchor процесс можно заметно упростить.

По сути, теперь ограничиваемые атрибуты добавлены в UIView как свойства с типом одного из подклассов NSLayoutAnchor: NSLayoutXAxisAnchor (для горизонтальных ограничений), NSLayoutYAxisAnchor (вертикальных) или NSLayoutDimension (высоты и ширины). И установка ограничений с нужными взаимосвязями доступна через их методы.

if #available(iOS 9.0, *) {
    view.widthAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.5).active = true
    view.centerXAnchor.constraintEqualToAnchor(self.centerXAnchor, constant: 10).active = true
    view.centerYAnchor.constraintEqualToAnchor(self.centerYAnchor, constant: 10).active = true
    view.widthAnchor.constraintLessThanOrEqualToAnchor(self.widthAnchor).active = true
    view.heightAnchor.constraintLessThanOrEqualToAnchor(self.heightAnchor).active = true
} else {
    addConstraints([
        NSLayoutConstraint(item: view,
            attribute: .Width,
            relatedBy: .Equal,
            toItem: view,
            attribute: .Height,
            multiplier: 0.5,
            constant: 0),

        NSLayoutConstraint(item: view,
            attribute: .CenterX,
            relatedBy: .Equal,
            toItem: self,
            attribute: .CenterX,
            multiplier: 1,
            constant: 10),

        NSLayoutConstraint(item: view,
            attribute: .CenterY,
            relatedBy: .Equal,
            toItem: self,
            attribute: .CenterY,
            multiplier: 1,
            constant: 10),

        NSLayoutConstraint(item: view,
           attribute: .Width,
           relatedBy: .LessThanOrEqual,
           toItem: self,
           attribute: .Width,
           multiplier: 1,
           constant: 0),

        NSLayoutConstraint(item: view,
            attribute: .Height,
            relatedBy: .LessThanOrEqual,
            toItem: self,
            attribute: .Height,
            multiplier: 1,
            constant: 0)
    ])
}

Обновление if и for-in в Swift 2

Во второй версия языка операторам if и for-in добавили способности, позволяющие писать еще более компактный и читаемый код.

Теперь в условном операторе можно сравнивать с диапазоном значений:

import UIKit

let number = arc4random_uniform(50)

if case 0...25 = number {
    print("Number is between 0 and 25")
} else {
    print("Number is over 25")
}

И фильтровать прямо в конструкции for-in:

let numbers = [0, 1, 2, 3, -7, 12, 13, -8, -11]

for number in numbers where number >= 0 {
    print("Positive number: \(number)")
}