Протоколы

Одной из особенностей языка Swift являются протоколы. По сути — это интерфейсы, определяющие список свойств и методов, которые должны быть реализованы принимающими классами, структурам или перечислениями. Для выстраивания иерархии, протоколы поддерживают наследование, в том числе множественное. Также их разрешается использовать как типы; отличительной чертой служит возможность создавать типы-композиции через protocol<Protocol1, Protocol2, ..., ProtocolN>.

import UIKit

protocol Selectable {
    var selected: Bool { get set }
    
    func select()
    func deselect()
}

protocol Colorable {
    func paint()
}

// Shape protocol inherits Selectable and Colorable protocols
protocol Shape: Selectable, Colorable {
    var type: String { get }
}

protocol Rounded {
    var radius: Double { get set }
}

// Oval inherits UIView class and adopts protocols Rounded and Shape
class Oval: UIView, Rounded, Shape {
    let type = "oval"
    var radius = 10.0
    var selected = false

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    func select() {
        selected = true
    }

    func deselect() {
        selected = false
    }

    func paint() {
        backgroundColor = UIColor.redColor()
    }
}

class Squircle: UIView, Rounded, Shape {
    let type = "squircle"
    var radius = 3.0
    var selected = false

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    func select() {
        selected = true
    }

    func deselect() {
        selected = false
    }

    func paint() {
        backgroundColor = UIColor.greenColor()
    }
}

// Define composition protocol type
typealias RoundedShape = protocol<Rounded, Shape>

// Array initialization as [protocol<Rounded, Shape>]() doesn't work
var roundedShapes = [RoundedShape]()

roundedShapes.append(Oval())
roundedShapes.append(Squircle())

С выходом Swift 2.0 протоколы обзавелись расширениями. Они позволяют добавлять реализацию по умолчанию методов и свойств (новых или уже существующих). Если необходимо, область действия расширений ограничивается только нужными типами, делается это конструкцией where.

import UIKit

// Make Selectable protocol adoptable only by classes (not structs, enums)
protocol Selectable: class {
    var selected: Bool { get set }

    func select()
    func deselect()
}

/*
Add default implementations of select() and deselect() protocol methods
and new toggleSelection() method only for UIView subclasses that adopt Selectable protocol
*/
extension Selectable where Self: UIView {
    func select() {
        selected = true
    }

    func deselect() {
        selected = false
    }

    func toggleSelection() {
        selected = !selected
    }
}

class Shape: UIView, Selectable {
    var selected = false
}

Встроенные протоколы тоже можно расширять.

import Foundation

extension BooleanType {
    var trueReverse: Bool {
        if self {
            return !Bool(self)
        } else {
            return Bool(self)
        }
    }
}

print(true.trueReverse)
// Output: false

print(false.trueReverse)
// Output: false

Дополнительные материалы:

Загрузка данных — NSURLSession

До выхода iOS 7.0 единственным интерфейсом для загрузки данных по сети служил класс NSURLConnection, затем был добавлен NSURLSession, который со временем стал полноценным преемником, обладающим расширенными и более гибкими возможностями. Начиная с iOS 9.0 NSURLConnection более не рекомендуется использовать.

NSURLSession поддерживает основные протоколы передачи информации, в том числе защищенные. Работа осуществляется как с обычными файлами, так и объектами NSData.

Для простых задач у класса есть метод NSURLSession.sharedSession(), возвращающий преднастроенный синглтон-экземпляр. Настройки по умолчанию достаточно оптимальны, единственным серьезным минусом является невозможность задать делегата, отслеживающего все события процесса, а не только завершение загрузки посредством функции-обработчика.

import Foundation
import XCPlayground

// Playground configuration
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

let url = NSURL(string: "http://valery.bashkatov.org/favicon.png")!
let session = NSURLSession.sharedSession()

let task = session.dataTaskWithURL(url,
                                   completionHandler: 
                                   // This function is called when the task is completed
                                   {
                                       (data: NSData?,
                                        response: NSURLResponse?,
                                        error: NSError?) -> Void in
                                       if error == nil {
                                           print("Downloading completed")
                                       } else {
                                           print(error)
                                       }
                                   })

// Run task
task.resume()

Если же требования выше, NSURLSession конфигурируется через объект NSURLSessionConfiguration. Сначала необходимо создать одну из базовых конфигураций вызовом соответствующего метода класса (возвращается новый экземпляр, а не синглтон):

Затем возможно изменить неустраивающие значения свойств.

Во время инициализации NSURLSession разрешается также указать делегата, получающего контроль над процессом загрузки через реализацию протоколов:

import UIKit

class ViewController: UIViewController, NSURLSessionDataDelegate {
    var data: NSMutableData!
    var dataURL: NSURL!
    var session: NSURLSession!
    var sessionConfiguration: NSURLSessionConfiguration!
    var sessionTask: NSURLSessionDataTask!

    override func viewDidLoad() {
        super.viewDidLoad()

        data = NSMutableData()
        dataURL = NSURL(string: "http://valery.bashkatov.org/favicon.png")!
        
        // Used ephemeral session for disable disk-based cache
        sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
        sessionConfiguration.timeoutIntervalForRequest = 10
        session = NSURLSession(configuration: sessionConfiguration,
                               delegate: self,
                               delegateQueue: nil)

        sessionTask = session.dataTaskWithURL(dataURL)
        sessionTask.resume()
    }

    // Method defined in NSURLSessionDataDelegate protocol
    func URLSession(session: NSURLSession,
                    dataTask: NSURLSessionDataTask,
                    didReceiveResponse response: NSURLResponse,
                    completionHandler: (NSURLSessionResponseDisposition) -> Void) {
        print("Received server response")
        print("Expected file size: \(response.expectedContentLength) bytes")

        completionHandler(NSURLSessionResponseDisposition.Allow)
        print("Downloading started")
    }

    // Method defined in NSURLSessionDataDelegate protocol
    func URLSession(session: NSURLSession,
                    dataTask: NSURLSessionDataTask,
                    didReceiveData data: NSData) {
        print("  Received: \(data.length) bytes")

        // Collect full data, chunk by chunk
        data.enumerateByteRangesUsingBlock
        {
            [unowned self] (bytes: UnsafePointer<Void>,
                            byteRange: NSRange,
                            stop: UnsafeMutablePointer<ObjCBool>) in
            self.data.appendBytes(bytes, length: byteRange.length)
        }
    }

    // Method defined in NSURLSessionTaskDelegate protocol
    func URLSession(session: NSURLSession,
                    task: NSURLSessionTask,
                    didCompleteWithError error: NSError?) {
        guard error == nil else {
            print(error)
            return
        }
        print("Downloading completed")
    }
}

Посимвольная работа со строками

Как и другие современные языки, Swift работает со строками в Юникоде, поэтому соответствие один символ — один байт не всегда верно.

import Foundation

var string = "Hello"

print(string.characters.count)
// Output: 5

print(string.utf8.count)
// Output: 5

string = "H?llo"

print(string.characters.count)
// Output: 5

print(string.utf8.count)
// Output: 6

Чтобы получить список именно символов, а не отдельных скаляров, есть свойство characters (типа String.CharacterView). Кроме прочих возможностей, оно поддерживает посимвольные циклы for-in и индексное обращение через [].

Особенностью является специальный тип индексов — String.CharacterView.Index (он же String.Index). Причем сами значения индексов задаются не абсолютно, а вычисляются относительно позиционных маркеров в начале и конце строки путем сдвига от них на нужное количество символов.

import Foundation

var string = "Hello"

for character in string.characters {
    print(character)
}
/*
Output: 
H
e
l
l
o
*/

print(string.characters[0])
// Output: Cannot subscript a value of type 'String.CharacterView' with an index of type 'Int'

print(string.characters[string.startIndex])
// Output: H

string.replaceRange(string.startIndex.successor()...string.endIndex.advancedBy(-4), with: "a")
print(string)
// Output: Hallo

Сайзклассы

В 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