Одной из особенностей языка 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
Дополнительные материалы:
До выхода 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. Сначала необходимо создать одну из базовых конфигураций вызовом соответствующего метода класса (возвращается новый экземпляр, а не синглтон):
- NSURLSessionConfiguration.defaultSessionConfiguration() — включает настройки по умолчанию, для кэширования используется диск.
- NSURLSessionConfiguration.ephemeralSessionConfiguration() — аналогично defaultSessionConfiguration за исключением кэширования, все хранится только в оперативной памяти.
- NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(_ identifier: String) — загрузка осуществляется в фоновом режиме даже при неактивном приложении, кроме того, после внештатного завершения задание может быть продолжено по идентификатору. Этот режим обладает рядом ограничений.
Затем возможно изменить неустраивающие значения свойств.
Во время инициализации 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 для характеристики доступного на экране места введено понятие сайзклассов. Каждому устройству и положению дисплея задано по два сайзкласса: вертикальный и горизонтальный, чьи значения определяют размеры (обычный или компактный) соответствующих сторон.
Можно заметить, что на Айпадах сайзклассы всегда одинаковые, исключением является режим многозадачности, в нем каждое окно будет иметь свои значения в зависимости от занимаемой доли экрана.
Вся работы по переключению сайзклассов целиком лежит на системе, в большинстве случаев это плюс, и лишь очень редко может пригодиться ручное вмешательство.
Для примера рассмотрим ситуацию — есть один общий для всех устройств интерфейс с привязкой объектов к сайзклассам, занимающей в портретном режиме больше вертикаль, а в альбомном горизонталь. Но как мы знаем, на Айпадах сайзклассы не меняются при повороте, остается выходить из ситуации программными путями.
На текущий момент в 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