Оптимизация отрисовки waveform на iOS

В статье пойдёт речь о том, как визуализировать вейвформу оптимальным образом, что для этого подходит лучше – CAShapeLayer или drawRect(), а также о некоторых тонкостях работы со Swift. Информация будет полезна не только тем, кто разрабатывает сложные кастомные UI компоненты и работает со звуком, но и любым iOS-разработчикам для расширения кругозора.

Постановка задачи

Разработчики DENIVIP создали приложение для фильтрации шумов в аудиозаписи – Denoise. Для пользователя это выглядит примерно так: он выбирает видео из своего альбома, приложение строит вейвформу звуковой дорожки и пользователь может выбрать на ней область с шумом, который нужно подавить. После небольшой магии пользователь получает видео с отфильтрованным звуком.

Алгоритм подавления шумов в данной статье подробно не обсуждается, но по большому счету он не выходит за рамки университетской программы по цифровой обработке сигналов. Теоретическую базу можно найти в удобном виде на сайте MITOpenCourseWare. Если говорить очень кратко, когда пользователь выбирает участок с шумом, мы, используя прямое преобразование Фурье, получаем частотный спектр «шумного участка». Запомнив эти частоты как «плохие» и требующие подавления, алгоритм проходит по всей аудиодорожке, на каждом участке получает частотный спектр и применяет подавляющие коэффициенты к «плохим» частотам. Затем, после применения обратного преобразования Фурье, получается новая вейвформа (дискретные звуки), в которой шумы существенно подавлены. Вся сложность состоит лишь в том, как качественно выбрать «шумный» участок. При этом чем длиннее видео, тем важнее становится интерактивность вейвформы, т.к. пользователю может потребоваться выделить несколько секунд аудиодорожки в середине двухчасового видео.

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

Пример. Для 10-минутного видео 16-битная стерео аудиодорожка с частотой дискретизации 44,1 кГц будет «весить» 10 * 60 * 2 * 44100 * 2 ~ 100 MB. При этом большая нагрузка ложится и на GPU.

Очевидным решением будет разбить сигнал на равные интервалы, посчитать, например, средние значения на этих интервалах и полученный набор (средних) использовать для отображения вейвформы. На деле будем использовать средние и максимальные значения на интервалах — для каждой аудиодорожки будем строить 2 вейвформы одна над другой. Плюс может потребоваться добавлять вейвформы «на лету” (для отображения отфильтрованного сигнала поверх оригинального). Существенным усложнением данного подхода является необходимость изменять масштаб вейформы, а также «перемещаться» по оси времени для выбора нужной области.

Замечу, что чтение сэмплов из аудиофайла и построение вейвформы может занимать продолжительное время. Для примера, на iPhone 5s вейвформа для 10-минутной записи строится порядка 10 секунд. Как известно, хуже ожидания для пользователя может быть только отсутствие информации о том, сколько еще нужно ждать. Поэтому мы решили сделать отрисовку загружающейся вейвформы анимированной, чтобы пользователь видел прогресс в интуитивном формате.

Итого для решения задачи нужно:

  1. Уметь читать одиночные сэмплы аудиодорожки и строить по ним данные для отображения вейвформы

  2. Уметь рисовать несколько вейвформ (с возможностью добавлять вейфвмормы в любой момент).

  3. Иметь возможность рисовать вейвформу “анимированно” по ходу чтения сэмплов.

  4. Добавить возможность масштабировать вейвформу и смещать её по времени.

Существующие решения

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

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

Разработка

Архитектура waveform-модуля

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

Smooth waveform UI architecture

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

Здесь

  1. Diagram View — контейнер одиночных вейвформ. Он также реализует синхронизацию отрисовки вейвформ, для чего использует CADisplayLink.

  2. Plot View —  класс, занимающийся отрисовкой. Этот класс должен знать только ту информацию, которую необходимо отобразить в данный момент времени, и получать её он будет из соответствующего Plot ViewModel.

  3. Diagram ViewModel — контейнер вью-моделей отдельных вейвформ.

  4. Plot ViewModel — источник данных для одиночной вейвформы. Этот класс будет решать, какие семплы и в каком количестве отдавать на отрисовку, когда это необходимо

Здесь опущены протоколы, через которые на самом деле реализована связь от View к ViewModel. Замечу только, что архитектурой не предусмотрено обратных связей от ViewModel к View.

Благодаря такому подходу мы получили возможность оптимизировать построение данных для вейвформы и отрисовку вейвформы независимо друг от друга. Также это дало возможность внести серьезные изменения в построение данных без воздействия на слой View. Но обо всем по порядку.

Чтение сэмплов

Для построения вейвформы необходимо получить несжатые семплы аудиодорожки. Сделать это можно с помощью инструментов, предоставляемых фреймворками AVFoundation и CoreMedia.

Чтение данных целиком представлено в классе:

Не буду очень подробно останавливаться на этом коде. Скажу только, что перед началом чтения сэмплов можно узнать параметры аудиозаписи, такие как частота дискретизации и количество каналов (функция readAudioFormat), и использовать их при чтении. Но можно и пропустить этот момент и при настройке чтения использовать частоту, равную 44100 Гц, и читать по двум каналам. Глубину кодирования придется задать вручную (16 бит).

Также отмечу, что чтение сэмплов может занимать продолжительное время. Для видео в несколько минут время чтения будет порядка нескольких секунд. Поэтому алгоритм нужно использовать в фоновом потоке.

Обработка сэмплов

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

import Foundation
import AVFoundation

protocol AudioSamplesHandler: class {
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int)
    func didStopReadSamples(count: Int)
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int)
}

class SamplesHandler: AudioSamplesHandler {
    
    var asset: AVAsset
    init(asset: AVAsset) {
        self.asset = asset
    }
    
    var neededSamplesCount = 0

    var estimatedSampleCount: Int = 0
    var sampleBlockLength: Int    = 0
    var currentAvgDouble: Double  = 0
    var currentMax: Int16         = 0
    var globalMax: Int16          = 0
    var globalBlockIndex: Int     = 0
    
    var avgPulsesBuffer: [Int16] = []
    var maxPulsesBuffer: [Int16] = []
    
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int) {
        
        self.estimatedSampleCount = estimatedSampleCount
        
        sampleBlockLength = Int(Double(estimatedSampleCount) / Double(neededSamplesCount))
        
        currentAvgDouble = 0
        currentMax       = 0
        globalMax        = 0
        globalBlockIndex = 0
    }
    
    func didStopReadSamples(count: Int) {
        //
    }
    
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int) {
        var currentBlock = globalBlockIndex / sampleBlockLength
        
        for index in 0..<length {
            
            let sample = samples[channelsCount * index]
            currentAvgDouble += fabs(Double(sample) / Double(sampleBlockLength))
            
            if currentMax < sample {
                currentMax = sample
            }
            
            if (globalMax < sample) {
                globalMax = sample
            }
            
            globalBlockIndex += 1
            
            let oldBlock = currentBlock
            currentBlock = globalBlockIndex / sampleBlockLength
            
            if currentBlock > oldBlock {
                self.avgPulsesBuffer[oldBlock] = Int16(currentAvgDouble)
                self.maxPulsesBuffer[oldBlock] = currentMax
                currentAvgDouble = 0
                currentMax       = 0
            }
        }
    }
}

Swift vs ObjC. Что лучше?

Поскольку приложение уже было написано на Objective-C, начинать писать новый модуль на Swift было неочевидным решением. Например, что касается построения данных для графиков, требовалось убедиться, что Swift не создаст проблем с производительностью. Мы провели проверку аналогичных алгоритмов, написанных на обоих языках и использующих аналогичные типы данных (например, если в Swift использовался тип UnsafePointer<Double>, то в алгоритме на Objective-C — массив double*).

Результаты были следующие:

Данные по работе алгоритмов чтения аудиосемплов и построения данных для графика.

iPhone 5

Swift (Whole-Module-Optimization):  20.1 c

Objective-C (-Ofast):  41.4 c

iPhone 6s

Swift (Whole-Module-Optimization):  5.1 c

Objective-C (-Ofast):  4.1 c

Тесты проводились и на других моделях iPhone. Результаты показали сильные расхождения на моделях с 32-разрядными процессорами и небольшие — на 64-разрядных моделях. (Для дальнейшей работы над оптимизацией в основном использовался iPhone 5s).

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

Масштабирование вейвформы

В первоначальном варианте для зума использовалась возможность чтения сэмплов из произвольного отрезка аудиодорожки. См. свойство объекта AVAssetReader

assetReader.timeRange = CMTimeRange(start: kCMTimeZero, duration: asset.duration)

И при каждом зуме графика чтение начиналось снова.

pre_mov_crop

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

Рефакторинг обработки сэмплов

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

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

В качестве массива обработанных данных был использован UnsafeMutablePointer<T>, который по своей сути является аналогом массива на языке C (см. документацию (англ.): Структура указателя UnsafeMutablePointer, Взаимодействие с API языка C). Пример реализации брался отсюда.

Примечание. Здесь опущен очень важный момент: этот класс будет использоваться параллельно в разных потоках (запись в бэкграунде, чтение в главном потоке). В Swift встроенных средств для работы с многопоточностью нет, так что атомарное поведение буфера придется реализовать вручную (например, вот так). В нашем случае, при использовании Array<T> вместо UnsafeMutablePointer<T> и без синхронизации мы периодически сталкивались с выходом за границы массива. С UnsafeMutablePointer<T> такая проблема не возникала.

Про числовые типы

Пару слов скажу про ассоциированный тип T в классе Channel<T>.

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

Теперь в коде класса Channel можно указать следующее:

Параллельные расчеты для нескольких масштабов

Теперь можно в класс Channel добавить обработку сэмплов (например, нахождение максимумов).

Обработка (сразу по нескольким каналам, для нескольких масштабов) будет выглядеть примерно так.

import Foundation

protocol ChannelSource {
    var channelsCount: Int { get }
    var onChannelsChanged: () -> () { get set }
    func channelAtIndex(index: Int) -> Channel<Int16>
}

class SamplesHandler: ChannelSource, AudioSamplesHandler {
    
    var scaleInterLevelFactor = 2
    var numberOfScaleLevels   = 10
    var neededSamplesCount    = 2000
    
    var channels = [Channel<Int16>]()
    
    var scaleIndex = 0
    var onChannelsChanged: () -> () = {_ in}
    
    var channelsCount: Int = 1
    
    func channelAtIndex(index: Int) -> Channel<Int16> {
        return channels[index + scaleIndex * channelsCount]
    }
    
    func createChannelsForDefaultLogicTypes() {
        
        var channels = [Channel<Int16>]()
        
        for _ in 0..<numberOfScaleLevels {
            let channel = Channel<Int16>()
            channels.append(channel)
        }
    }
    
    func willStartReadSamples(estimatedSampleCount estimatedSampleCount: Int) {
        for index in 0..<numberOfScaleLevels {
            
            var totalCount = Int(Double(neededSamplesCount) * pow(Double(scaleInterLevelFactor), Double(index)))
            let blockSize  = Int(ceil(Double(estimatedSampleCount)/Double(totalCount)))
            totalCount = Int(Double(estimatedSampleCount)/Double(blockSize))
            
            let channel = channels[index]
            
            channel.totalCount = totalCount
            channel.blockSize  = blockSize
        }
    }
    
    func reset(scale: Double) {
    
        var scaleIndex = Int(floor(log(scale)/log(Double(scaleInterLevelFactor))))
        scaleIndex     = min(self.numberOfScaleLevels - 1, scaleIndex)
        if scaleIndex != self.scaleIndex {
            self.scaleIndex = scaleIndex
            self.onChannelsChanged()
        }
    }
    
    func didStopReadSamples(count: Int) {
        for channel in channels {
            channel.complete()
        }
    }
    
    func handleSamples(samples: UnsafePointer<Int16>, length: Int, channelsCount: Int) {
    
        for channelIndex in 0..<numberOfScaleLevels {
            
            let channel = channels[channelIndex * channelsCount]

            for sampleIndex in 0..<length {
                let sample = samples[channelsCount * sampleIndex]
                channel.handleValue(sample)
            }
        }
    }
}

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

По такому же принципу можно добавить вычисление средних значений. А чтобы все это работало вместе, можно создать один абстрактный класс Channel и 2 наследника MaxChannel и AvgChannel (для максимумов и средних значений, соответственно).

После всех манипуляций зум выглядит так:

post_mov_crop

Оптимизация

Построение набора данных для вейвформы

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

Для оптимизации было сделано следующее:

  1. Создали базовый класс, который инкапсулирует логику обработки сэмплов.

  2. В классе-канале создали свойство этого типа.

  3. В классе-логике создали слабую ссылку на класс-канал.

  4. Отнаследовали от этого типа конкретные обработчики (для средних и для максимумов).

  5. Сам класс-канал сделали ненаследуемым (с помощью ключевого слова final).

  6. Включили оптимизацию Whole-Module-Optimization.

Про 2 последних пункта читайте здесь.

Режим Whole-Module-Optimization включается в настройках проекта.

whole-module-optimization

 

Результаты сравнения скорости работы алгоритма (продолжительность видеоролика 10 мин):

В случае с наследованием каналов и логикой обработки внутри каналов время построения данных: 27.95 с

Для варианта с “внешней” логикой — 6.66 с

В конечном итоге, буфер также решено было спрятать внутрь отдельного класса для того, чтобы подружить класс Channel с ObjC (ObjC не умеет работать с обобщенными типами Swift).

Графика

Немного расскажем про работу с графикой.

По задаче требуется уметь рисовать большое количество вертикальных линий.

Исходный вариант

Изначальный вариант базировался на использовании CAShapeLayer и отрисовки с помощью линии обводки (stroke).

import UIKit

protocol PlotDataSource: class {
    var dataSourceFrame: CGRect { get }
    var pointsCount: Int { get }
    func pointAtIndex(index: Int) -> CGPoint
}

class Plot: UIView {
    weak var dataSource: PlotDataSource?
    var lineColor = UIColor.blackColor()
    var pathLayer = CAShapeLayer()
    
    convenience init(){
        self.init(frame: CGRectZero)
        self.setupPathLayer()
    }

    func setupPathLayer() {
        self.pathLayer.strokeColor = UIColor.blackColor().CGColor
        self.pathLayer.lineWidth   = 1.0
        self.layer.addSublayer(self.pathLayer)
        
        self.pathLayer.drawsAsynchronously = true
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        self.pathLayer.frame = self.bounds
        self.redraw()
    }
    
    func redraw() {
        self.pathLayer.path = self.newPathPart()
    }
    
    func newPathPart() -> CGPathRef {
        
        guard let dataSource = self.dataSource else {
            return CGPathCreateMutable()
        }
        
        let currentCount = dataSource.pointsCount
        let sourceBounds = dataSource.dataSourceFrame.size
        
        let mPath        = CGPathCreateMutable()
        
        CGPathMoveToPoint(mPath, nil, 0, self.bounds.midY)
        
        let wProportion = self.bounds.size.width / sourceBounds.width
        let hPropostion = self.bounds.size.height / sourceBounds.height
        
        for index in 0..<currentCount {
            let point         = dataSource.pointAtIndex(index)
            let adjustedPoint = CGPoint(
                x: point.x * wProportion,
                y: point.y * hPropostion / 2.0)
            
            CGPathMoveToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY - adjustedPoint.y)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY + adjustedPoint.y)
            CGPathMoveToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY)
        }
        CGPathMoveToPoint(mPath, nil, CGPathGetCurrentPoint(mPath).x, self.bounds.midY)
        
        return mPath
    }
}

Время отрисовки для 2000 точек на каждую из 2 вейвформ, на iPhone 5s: 18 с

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

Вариант с оптимизацией №1  (построение CGPath без CGPathMoveToPoint)

Для начала избавились от использования функции CGPathMoveToPoint и там, где нужно, стали просто добавлять лишнюю линию вместо перемещения между точками.

import UIKit
class _Plot: Plot {
    
    func newPathPart() -> CGPathRef {
        
        guard let dataSource = self.dataSource else {
            return CGPathCreateMutable()
        }
        
        let currentCount = dataSource.pointsCount
        let sourceBounds = dataSource.dataSourceFrame.size
        
        let mPath        = CGPathCreateMutable()
        
        CGPathMoveToPoint(mPath, nil, 0, self.bounds.midY)
        
        let wProportion = self.bounds.size.width / sourceBounds.width
        let hPropostion = self.bounds.size.height / sourceBounds.height
        
        for index in 0..<currentCount {
            let point         = dataSource.pointAtIndex(index)
            let adjustedPoint = CGPoint(
                x: point.x * wProportion,
                y: point.y * hPropostion / 2.0)
            
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY - adjustedPoint.y)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY + adjustedPoint.y)
            CGPathAddLineToPoint(mPath, nil, adjustedPoint.x, self.bounds.midY)
        }
        CGPathMoveToPoint(mPath, nil, CGPathGetCurrentPoint(mPath).x, self.bounds.midY)
        
        return mPath
    }
}

Время отрисовки для тех же исходных данных: 14.8 с. 

Вариант с оптимизацией №2 (stroke против fill)

Вейвформы у нас довольно простые: строим линию от середины вверх в соответствии с уровнем сэмпла, повторяем симметрично вниз, возвращаемся на середину, переходим к следующему значению. Это позволило совсем отказаться от stroke и строить линии, закрашивая нужные области (fill). При этом по прежнему рисуем замкнутую фигуру.

В этом случае получаем:  12.4 с.

Прирост скорости небольшой и заметен только на относительно старых устройствах (до iPhone 5s включительно).

Вариант с оптимизацией №3 (drawRect против CAShapeLayer)

Также попробовали вместо CAShapeLayer использовать отрисовку того же самого CGPath в drawRect (алгоритм построения CGPath как варианте № 1)

class _Plot: Plot {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.opaque = false
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.opaque = false
    }
    
    func redraw() {
        self.setNeedsDisplay()
    }
    
    override func drawRect(rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        
        CGContextSetLineWidth(context, 1/UIScreen.mainScreen().scale)
        CGContextAddPath(context, self.newPathPart())
        CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor)
        CGContextSetInterpolationQuality(context, .None);
        CGContextSetAllowsAntialiasing(context, false);
        CGContextSetShouldAntialias(context, false);
        CGContextStrokePath(context)
    }
}

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

Результат теста: 9.46 c. 

В итоге, в результате оптимизации построения CGPath и переходе на отрисовку в drawRect(), можно ускорить построение вейвформы почти в 2 раза. Полученного результата уже достаточно для плавной отрисовки нашей вейвформы.

Вместо заключения

  1. Как видим, Swift вполне подходит для задач подобного уровня.
  2. Оптимизация алгоритмов на Swift – это одна большая тема, которую мы еще постараемся осветить.
  3. Для достижения оптимальных параметров отрисовки вашей графики не обязательно переходить на OpenGL или Metal.
  4. MVVM-подобный подход к построению архитектуры с разделением ответственностей View и ViewModel хорошо подходит для реализации  сложного интерактивного интерфейса.

Целиком проект с демоприложением можно найти на GitHub по ссылке.

Синхронизация данных в iOS приложениях, использующих Core Data


the-cloud-rainbow-unicorn

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

Continue reading

Использование возможностей UIDynamicAnimator в приложениях под iOS 7

После выхода iOS 7 у разработчиков появилось очень много новых инструментов для создания интересных и увлекательных эффектов и анимаций. Но уместность использования многих из них по-прежнему остаётся под вопросом. Изучив некоторые из них, мы решили в новом нашем проекте сделать анимацию, аналогичную реализованной Apple в приложении «Сообщения».

Для реализации такого поведения нам пришлось заменить используемый сначала экземпляр класса UITableView на UICollectionView, так как только последний был адаптирован Apple для работы с объектом-аниматором UIDynamicAnimator

 

После этого мы переопределили стандартный UICollectionViewFlowLayout:

в котором определили  объект UIDynamicAnimator и переопределили необходимые для реализации функции:

Динамическое поведение становится активным, когда вы его добавляете к объекту- аниматору, который является экземпляром UIDynamicAnimator. Аниматор определяет контекст в котором динамическое поведение выполняется.

Объект UIAttachmentBehavior определяет связь между динамическим элементом класса UICollectionViewLayoutAttributes и точкой item.center. Когда точка перемещается, присоединенный элемент также перемещается. C помощью свойств length, damping и frequency можно настроить поведение нужным нам образом.

Также важно правильно подобрать resistanceFactor

Другие статьи по теме:

  1. Эффект параллакса в iOS приложениях
  2. Осваиваем Core Motion в iOS

 

Создание кастомного UIActivity для публикации фото и текста в социальной сети ВКонтакте

Во время работы над очередной версией приложения PhotoSuerte возникла задача сделать публикацию фото в социальной сети ВКонтакте через стандартный контроллер UIActivityViewController.

activity

Continue reading

Настройка Jenkins CI на Mac OS X для сборки Android- и iOS-приложений Phonegap/Cordova и размещения их в TestFlight/HockeyApp

Разработка мобильных приложений в большинстве случаев является весьма увлекательным занятием, особенно если вам самим нравится то, что вы создаете. Работая с приличным количеством внешних проектов (мы разрабатываем приложения и сайты на заказ), а также с растущим числом своих внутренних приложений, мы задумались о минимизации времени и сил затрачиваемых на подготовку тестовых и релизных сборок приложений. При наличии нескольких разработчиков, тестировщиков и приличного числа проектов, затраты на подготовку сборок становятся весьма существенными. Поэтому чтобы не тратить силы зря и заниматься тем, что действительно важно для ваших сервисов, предлагаем инструкцию по созданию системы автоматической сборки приложений. Описанный далее подход мы используем, в первую очередь, в своих приложениях, с которыми предлагаем вам познакомиться: Together, PhotoSuerte, Routes.Tips. Continue reading

Node.JS & MongoDB: молниеносная быстрота создания прототипов

nodejs

Те, кто разрабатывает мобильные приложения, обычно начинают думать о рабочем прототипе сразу же после создания предварительного макета. В контексте методологии Lean Startup и концепции минимально жизнеспособного продукта (MVP) не будет преувеличением сказать, что создание прототипа до момента принятия окончательных решений архитектурного плана жизненно важно для успеха проекта. Раньше у многих была удобная отговорка, что создание прототипа дело исключительно трудоемкое. Однако сегодня для большинства проектов разработки мобильных приложений это совсем не так. Разработав прототип, вы можете гораздо быстрее получить ценные отклики от своих пользователей, а значит, гораздо более осознанно переосмыслить архитектуру и динамику своего приложения.

В этой статье я хочу поделиться методологией, которой мы обычно придерживаемся в наших проектах. При разработке прототипов для наших новых приложений мы всегда используем сочетание Node.js и MongoDB. Это позволяет нам избежать существенных трудозатрат на разработку серверной части и получить готовый сервис всего за несколько недель. Более того, полученную архитектуру можно в дальнейшем масштабировать. Теперь давайте подробнее разберем наш подход на примере недавно запущенного приложения PhotoSuerte. Это приложение позволяет случайным пользователям общаться друг с другом, обмениваясь фотографиями. Предлагаю сразу перейти к делу, чтобы не тратить ваше драгоценное время. Прочитав эту статью, вы можете сразу приступить к созданию прототипа вашего следующего мегаприложения! Continue reading

PhotoSuerte: обменивайтесь фотографиями и общайтесь с людьми по всему миру!

Карта PhotoSuerte 2 декабря 2013, Москва, Россия. Cегодня фоточат стал одним из самых популярных и увлекательных видов общения между людьми. C новым приложением PhotoSuerte любой человек может в любой момент заглянуть в любую точку земного шара.

Компания DENIVIP Group, российский разработчик программного обеспечения, объявляет о выпуске новейшего мобильного приложения для фоточата. Знакомьтесь: PhotoSuerte. PhotoSuerte в случайном порядке подбирает собеседников и позволяет им обмениваться друг с другом фотографиями. Приложение дарит вам уникальную возможность увидеть другие страны и континенты глазами тех, кто там живет. PhotoSuerte — это не только яркие новые впечатления, но и возможность разделить с другими людьми страсть к фотографии и творческому самовыражению. Continue reading

Реализация лайв-стриминга снимаемого видео в приложении для iOS

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

Continue reading

Продвижение стартапа до публичного релиза. Case Study.

запуска роста

В данной статье я хотел бы рассказать вам историю продвижения предварительного релиза нашего нового приложения — инновационной видеокамеры, которую мы назвали Together. В настоящее время существует множество способов привлечения внимания первых пользователей к программному продукту. Здесь нам удалось проложить собственный путь, и мы спешим поделиться им с вами. Сначала я посвятил несколько дней (или даже недель) поиску существующих надежных и проверенных методов продвижения. Потом у меня ушло еще больше времени на поиск и апробацию моего собственного метода (конечно, с учетом полученных знаний). Итак, для начала давайте посмотрим, какие действия я предпринял и каких результатов добился. Continue reading

Эффект параллакса в iOS приложениях

layersParallax

Идея использования параллакса в дизайне мобильных приложений не нова. Как и многие другие, она пришла к нам из мира веб-дизайна, где поначалу стала очень распространённым направлением. Параллакс придаёт дизайну впечатляющий эффект глубины и кажущийся объём. К настоящему моменту в силу некоторых неудобств, связанных со спецификой разработки веб-приложений, мода на параллакс в оформлении сайтов поутихла. Однако мобильные приложения — это совсем другой разговор. Параллакс в мобильном дизайне живёт и пока не собирается никуда уходить, и даже добавлен по-умолчанию в дизайн новой iOS 7!

В этой статье мы расскажем вам о нашем компоненте DVParallaxView, и продемонстрируем на примере его устройства, как добавить в приложение точно такой же эффект параллакса, как и в home screen в iOS 7. И даже лучше. Continue reading