Практически у каждого разработчика наступает момент, когда все наработки становится неудобно держать в виде исходных файлов. Возникает обоснованное желание скомпилировать это добро в библиотеки, динамически подключаемые по мере надобности. Начиная с Xcode 6 такое реально.
В отличие от статических библиотек, фреймворк кроме кода может содержать весь набор ресурсов: иконки, изображения, звуки, конфигурационные файлы и прочие данные.
Наполнение проекта фреймворка ничем не отличается от обычного. Разве что в коде нужно правильно выставлять уровни доступа элементов. Если уровень не задан явно, то примет значение по умолчанию — internal, запрещающее доступ извне модуля, поэтому весь открытый интерфейс должен быть public.
После успешного построения в каталоге появится и сам фреймворк.
Его, конечно, можно и вручную каждый раз копировать отсюда в зависимые проекты, но лучше настроить автокопирование в общую папку (где потом и забирать). Для этого понадобится скрипт постобработки.
Фреймворк готов.
Чтобы им воспользоваться, в зависимых от него проектах необходимо выполнить два действия.
Первое — в General настройках добавить фреймворк в Embedded Binaries, убрав в опциях подтверждения флажок Copy items if needed для связи по ссылке, без физического копирования. При этом Linked Frameworks and Libraries заполнится автоматически.
Второе — в настройках Build Settings включить каталог фреймворка в Framework Search Paths.
Теперь фреймворк можно импортировать и использовать в коде.
В случае визуальной привязки объекта к классу фреймворка, следует явно указывать модуль.
Мобильные устройства уже давно содержат многоядерные процессоры, открывающие доступ к многопоточности. Но не все так просто. Так как система сама не в состоянии верно, с учетом всех зависимостей разделить код по потокам, эта возможность вынесена в API и доступна разработчикам. Существует несколько способов распараллеливания задач.
NSThread
Низкоуровневый подход, наиболее сложный и наименее предпочтительный из всех — прямая работа с потоками через NSThread. В подавляющем большинстве случаев этот способ нецелесообразен, лучше сразу перейти к технологиям более высокого уровня, основанным на очередях.
Grand Central Dispatch
Преимущества конкурентного программирования с применением очередей огромны. Разработчик избавляется от ручного управления потоками, учета системных ресурсов, доступности, а также приоритезации и распределения задач.
Одна из таких технологий — Grand Central Dispatch, сокращенно GCD. Ее особенностью является портированная как есть Си-подобная стилистика: все реализовано на функциях со специфичными названиями и типами.
Работая с GCD, надо знать ряд вещей.
- Хотя очереди можно создавать, лучше воспользоваться готовыми системными.
- Есть два вида очередей: последовательные — задания в них выполняются по одному в порядке добавления, и конкурентные (все системные такие, кроме исключения ниже) — включающие распараллеливание.
- Существует единственная главная последовательная очередь, в которой должны производиться изменения, связанные с графическим интерфейсом — это нельзя забывать.
- Задания представляют собой блоки. Добавлять их в очередь можно либо синхронно, с ожиданием завершения блока, либо асинхронно, с возвращением контроля сразу.
import UIKit
class ViewController: UIViewController {
var label: UILabel!
var animator: UIDynamicAnimator!
var gravity: UIGravityBehavior!
var collision: UICollisionBehavior!
override func viewDidLoad() {
super.viewDidLoad()
label = UILabel(frame: CGRect(x: 100, y: 50, width: 100, height: 50))
animator = UIDynamicAnimator(referenceView: view)
gravity = UIGravityBehavior(items: [self.label])
collision = UICollisionBehavior(items: [self.label])
collision.translatesReferenceBoundsIntoBoundary = true
view.addSubview(label)
// Asynchronously submit long calculations to the system global concurrent queue
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)) {
let result = "\(self.complexCalculation())"
// Back to the main serial queue for update GUI
dispatch_async(dispatch_get_main_queue()) {
self.label.text = result
self.animator.addBehavior(self.gravity)
self.animator.addBehavior(self.collision)
}
}
}
// Long running method
func complexCalculation() -> Double {
return M_PI
}
}
NSOperationQueue
Другая технология — очереди операций. В отличие от GCD, она построена по модели ООП: очереди — объекты NSOperationQueue, задания — объекты NSOperation. Но в то же время GCD используется в качестве инструмента.
Эти очереди хорошо подходят как для простых случаев, когда операциями могут выступать банальные объектные обертки блоков.
import Foundation
let queue = NSOperationQueue()
let operation = NSBlockOperation {
print(NSOperationQueue.currentQueue())
}
queue.addOperation(operation)
// Or equivalently
queue.addOperationWithBlock {
print(NSOperationQueue.currentQueue())
}
Так и сложных, требующих создания дочерних классов от NSOperation. И здесь важны моменты.
- Стартовая логика должна находиться в методе start операций. Он будет вызываться очередью.
- У операций есть несколько статусных свойств:
- ready — готовность к началу выполнения. По умолчанию — true.
- executing — нахождение в процессе работы. По умолчанию — false.
- finished — окончание, вследствие завершения или отмены. По умолчанию — false.
- cancelled — отмена. По умолчанию — false.
В подклассах необходимо вовремя и верно задавать как минимум finished и executing, потому что они участвуют в работе очередей и зависимостей. Остальные необязательно: ready является лишь дополнительным условием старта операции, основное — завершение всех зависимостей (finished у них должно быть true), если они есть; cancelled устанавливается при вызове метода cancel. Также, при изменении значений этих свойств, потребуется рассылка уведомлений методами willChangeValueForKey и didChangeValueForKey механизма KVO для соответствующих ключей, иначе очереди и зависимые операции не узнают об этом.
- Все операции в очереди выполняются конкурентно. Если же нужна определенная последовательность, это делается через зависимости (по-простому — ожидания одними операциями завершения других).
import UIKit
// Utility class for image data
class Picture {
var url: NSURL
var originalImage: UIImage!
var filteredImage: UIImage!
init(url: String) {
self.url = NSURL(string: url)!
}
}
// Base image operations class
class ImageOperation: NSOperation {
// MARK: - Properties
// Private executing and finished properties with KVO notifications
private var isExecuting = false {
willSet {
willChangeValueForKey("isExecuting")
}
didSet {
didChangeValueForKey("isExecuting")
}
}
private var isFinished = false {
willSet {
willChangeValueForKey("isFinished")
}
didSet {
didChangeValueForKey("isFinished")
}
}
override var executing: Bool {
get {
return isExecuting
}
set {
isExecuting = newValue
}
}
override var finished: Bool {
get {
return isFinished
}
set {
isFinished = newValue
}
}
}
class ImageDownloadOperation: ImageOperation {
// MARK: - Properties
private var picture: Picture!
lazy var imageDownloadTask: NSURLSessionDataTask = {
return NSURLSession.sharedSession().dataTaskWithURL(self.picture.url,
completionHandler:
{
(data: NSData?,
response: NSURLResponse?,
error: NSError?) -> Void in
if error == nil {
self.picture.originalImage = UIImage(data: data!)!
self.executing = false
self.finished = true
} else {
print(error)
self.executing = false
self.finished = true
self.cancel()
}
})
} ()
// MARK: - Initialization
init(picture: Picture) {
super.init()
self.picture = picture
}
override func start() {
executing = true
// If operation is cancelled, stop process
guard !cancelled else {
executing = false
finished = true
return
}
imageDownloadTask.resume()
}
}
class ImageFilterOperation: ImageOperation {
// MARK: - Properties
private var picture: Picture!
// MARK: - Initialization
init(picture: Picture) {
super.init()
self.picture = picture
}
override func start() {
executing = true
// If operation is cancelled, stop process
guard !cancelled else {
executing = false
finished = true
return
}
// Make filtered image
let inputImage = CIImage(image: picture.originalImage)
let context = CIContext(EAGLContext: EAGLContext(API: EAGLRenderingAPI.OpenGLES2))
let filter = CIFilter(name: "CIPhotoEffectMono")!
filter.setValue(inputImage, forKey: kCIInputImageKey)
let outputImage = filter.outputImage
let outputCGImage = context.createCGImage(outputImage!, fromRect: outputImage!.extent)
guard !cancelled else {
executing = false
finished = true
return
}
picture.filteredImage = UIImage(CGImage: outputCGImage)
executing = false
finished = true
}
}
class ViewController: UIViewController {
let queue = NSOperationQueue()
let picture = Picture(url:
"http://valery.bashkatov.org/images/concurrent-programming/sample.jpg")
let originalImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
let filteredImageView = UIImageView(frame: CGRect(x: 0, y: 200, width: 300, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(originalImageView)
view.addSubview(filteredImageView)
/*
Create operations with dependencies: originalImageDownload - originalImageShow
|
originalImageFilter - filteredImageShow
*/
let originalImageDownload = ImageDownloadOperation(picture: picture)
let originalImageShow = NSBlockOperation {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.originalImageView.image = self.picture.originalImage
}
}
originalImageShow.addDependency(originalImageDownload)
let originalImageFilter = ImageFilterOperation(picture: picture)
originalImageFilter.addDependency(originalImageDownload)
let filteredImageShow = NSBlockOperation {
NSOperationQueue.mainQueue().addOperationWithBlock {
self.filteredImageView.image = self.picture.filteredImage
}
}
filteredImageShow.addDependency(originalImageFilter)
queue.addOperations([originalImageDownload,
originalImageShow,
originalImageFilter,
filteredImageShow], waitUntilFinished: false)
}
}
Скачать Concurrency.zip
Дополнительные материалы:
Для работы с форматом JSON во фреймворке Foundation имеется класс NSJSONSerialization. Он позволяет выполнять две основные вещи: переводить объекты в JSON-данные — сериализовать, и наоборот, из JSON-данных получать объекты — десериализовать.
/*
articles.json format:
[
{
"article": {
"title": "Холст для рисования",
"url": "http://valery.bashkatov.org/paper/canvasview",
"date": "2016-01-22"
}
},
{
"article": {
"title": "Вызов метода по таймеру",
"url": "http://valery.bashkatov.org/paper/method-call-by-timer",
"date": "2016-01-17"
}
}
]
*/
import Foundation
typealias Article = [String: [String: String]]!
let url = NSURL(string: "http://valery.bashkatov.org/files/json/articles.json")!
var data = NSData(contentsOfURL: url)!
var articles: [Article]!
do {
// JSON data deserialization into the object
articles = try NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions()) as! [Article]
} catch {
print(error)
}
for article in articles {
print("article")
}
if NSJSONSerialization.isValidJSONObject(articles) {
do {
// Object serialization into the JSON data
data = try NSJSONSerialization.dataWithJSONObject(articles,
options: NSJSONWritingOptions.PrettyPrinted)
} catch {
print(error)
}
}
print(String(data: data, encoding: NSUTF8StringEncoding)!)
Рисование фигур внутри UIView производится через задание в подклассе логики методу drawRect. Каждый раз создавать нового наследника непрактично, кроме того, такой способ не отличается гибкостью в плане управления процессом рисования.
Полезно иметь класс-холст. Для себя я разработал CanvasView. С ним рисование сводится к добавлению нужных путей в массив. CanvasPath — банальный наследник UIBezierPath с цветами обводки и заливки.
import UIKit
/**
The `CanvasPath` class extends UIBezierPath by adding stroke and fill colors.
Used in `CanvasView` class.
*/
class CanvasPath: UIBezierPath {
// MARK: - Properties
/**
The stroke color.
*/
var strokeColor: UIColor?
/**
The fill color.
*/
var fillColor: UIColor?
}
/**
The `CanvasView` class makes it easy to draw the shapes inside.
*/
class CanvasView: UIView {
// MARK: - Properties
/**
The paths that are be drawn.
*/
var paths = [CanvasPath]() {
didSet {
// Redraw paths
setNeedsDisplay()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
// Set false for default transparent background color.
opaque = false
}
// MARK: - Drawing
override func drawRect(rect: CGRect) {
for path in paths {
if path.strokeColor != nil {
path.strokeColor!.setStroke()
path.stroke()
}
if path.fillColor != nil {
path.fillColor!.setFill()
path.fill()
}
}
}
}
Пример использования.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let canvas = CanvasView(frame: CGRect(x: 0, y: 0, width: 400, height: 300))
let borders = CanvasPath()
borders.strokeColor = UIColor(white: 0.9, alpha: 1)
borders.moveToPoint(CGPoint(x: 20, y: 50))
borders.addLineToPoint(CGPoint(x: 350, y: 50))
borders.addLineToPoint(CGPoint(x: 350, y: 275))
borders.addLineToPoint(CGPoint(x: 20, y: 275))
borders.addLineToPoint(CGPoint(x: 20, y: 50))
let white = CanvasPath(rect: CGRect(x: 20, y: 50, width: 330, height: 75))
white.fillColor = UIColor.whiteColor()
let blue = CanvasPath(rect: CGRect(x: 20, y: 125, width: 330, height: 75))
blue.fillColor = UIColor(red: 0, green: 0.224, blue: 0.651, alpha: 1)
let red = CanvasPath(rect: CGRect(x: 20, y: 200, width: 330, height: 75))
red.fillColor = UIColor(red: 0.835, green: 0.169, blue: 0.118, alpha: 1)
canvas.paths += [borders, white, blue, red]
view.addSubview(canvas)
}
}
Скачать CanvasView.zip
Если код нужно выполнить через определенный интервал времени, поможет класс NSTimer и его статический метод scheduledTimerWithTimeInterval.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
/* Parameters:
1) interval — number of seconds between firings
2) target — object-owner of the called method
3) selector — name of the called method
4) userInfo — additional user info object (may be nil)
5) repeats — timer is fired once (false) or until invalidated (true)
*/
NSTimer.scheduledTimerWithTimeInterval(5,
target: self,
selector: "changeBackgroundColor:",
userInfo: nil,
repeats: true)
}
func changeBackgroundColor(timer: NSTimer) {
// Generate random color
let red = CGFloat(arc4random_uniform(3)) / 2
let green = CGFloat(arc4random_uniform(3)) / 2
let blue = CGFloat(arc4random_uniform(3)) / 2
let alpha = CGFloat(1)
let color = UIColor(red: red, green: green, blue: blue, alpha: alpha)
// Animate background color changes
UIView.animateWithDuration(1, animations: {self.view.backgroundColor = color})
print("New background color: \(color)")
// If the new color is white, then stop the timer
if color == UIColor(red: 1, green: 1, blue: 1, alpha: 1) {
timer.invalidate()
print("The timer is stopped")
}
}
}