Создание фреймворка

Практически у каждого разработчика наступает момент, когда все наработки становится неудобно держать в виде исходных файлов. Возникает обоснованное желание скомпилировать это добро в библиотеки, динамически подключаемые по мере надобности. Начиная с Xcode 6 такое реально.

Cocoa Touch Framework creation

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

Наполнение проекта фреймворка ничем не отличается от обычного. Разве что в коде нужно правильно выставлять уровни доступа элементов. Если уровень не задан явно, то примет значение по умолчанию — internal, запрещающее доступ извне модуля, поэтому весь открытый интерфейс должен быть public.

Framework sample code

После успешного построения в каталоге появится и сам фреймворк.

Framework file

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

Edit scheme menu Build post-action script

Фреймворк готов.

Чтобы им воспользоваться, в зависимых от него проектах необходимо выполнить два действия.

Первое — в General настройках добавить фреймворк в Embedded Binaries, убрав в опциях подтверждения флажок Copy items if needed для связи по ссылке, без физического копирования. При этом Linked Frameworks and Libraries заполнится автоматически.

Add framework Copy framework as link Added framework

Второе — в настройках Build Settings включить каталог фреймворка в Framework Search Paths.

Framework search path setting

Теперь фреймворк можно импортировать и использовать в коде.

Framework code usage

В случае визуальной привязки объекта к классу фреймворка, следует явно указывать модуль.

Framework storyboard usage

Конкурентное программирование

Мобильные устройства уже давно содержат многоядерные процессоры, открывающие доступ к многопоточности. Но не все так просто. Так как система сама не в состоянии верно, с учетом всех зависимостей разделить код по потокам, эта возможность вынесена в API и доступна разработчикам. Существует несколько способов распараллеливания задач.

NSThread

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

Grand Central Dispatch

Преимущества конкурентного программирования с применением очередей огромны. Разработчик избавляется от ручного управления потоками, учета системных ресурсов, доступности, а также приоритезации и распределения задач.

Одна из таких технологий — Grand Central Dispatch, сокращенно GCD. Ее особенностью является портированная как есть Си-подобная стилистика: все реализовано на функциях со специфичными названиями и типами.

Работая с GCD, надо знать ряд вещей.

  1. Хотя очереди можно создавать, лучше воспользоваться готовыми системными.
  2. Есть два вида очередей: последовательные — задания в них выполняются по одному в порядке добавления, и конкурентные (все системные такие, кроме исключения ниже) — включающие распараллеливание.
  3. Существует единственная главная последовательная очередь, в которой должны производиться изменения, связанные с графическим интерфейсом — это нельзя забывать.
  4. Задания представляют собой блоки. Добавлять их в очередь можно либо синхронно, с ожиданием завершения блока, либо асинхронно, с возвращением контроля сразу.
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 используется в качестве инструмента.

Relationships between operations, queues, GCD and threads

Эти очереди хорошо подходят как для простых случаев, когда операциями могут выступать банальные объектные обертки блоков.

import Foundation

let queue = NSOperationQueue()
let operation = NSBlockOperation {
    print(NSOperationQueue.currentQueue())
}

queue.addOperation(operation)

// Or equivalently
queue.addOperationWithBlock {
    print(NSOperationQueue.currentQueue())
}

Так и сложных, требующих создания дочерних классов от NSOperation. И здесь важны моменты.

  1. Стартовая логика должна находиться в методе start операций. Он будет вызываться очередью.
  2. У операций есть несколько статусных свойств:
    • ready — готовность к началу выполнения. По умолчанию — true.
    • executing — нахождение в процессе работы. По умолчанию — false.
    • finished — окончание, вследствие завершения или отмены. По умолчанию — false.
    • cancelled — отмена. По умолчанию — false.
    В подклассах необходимо вовремя и верно задавать как минимум finished и executing, потому что они участвуют в работе очередей и зависимостей. Остальные необязательно: ready является лишь дополнительным условием старта операции, основное — завершение всех зависимостей (finished у них должно быть true), если они есть; cancelled устанавливается при вызове метода cancel. Также, при изменении значений этих свойств, потребуется рассылка уведомлений методами willChangeValueForKey и didChangeValueForKey механизма KVO для соответствующих ключей, иначе очереди и зависимые операции не узнают об этом.
  3. Все операции в очереди выполняются конкурентно. Если же нужна определенная последовательность, это делается через зависимости (по-простому — ожидания одними операциями завершения других).
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

Для работы с форматом 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")
        }
    }
}