一篇較好的學(xué)習(xí)文章
AVFoundation Tutorial: Adding Overlays and Animations to Videos
AVFoundation的一些應(yīng)用
音視頻合成
超有梗1.0編輯頁重做了,我也對以前代碼做了優(yōu)化,現(xiàn)在錄音、音樂、音效的添加,都視作一個(gè)音塊。具體功能大家可以在AppStore下來自己看看??
首先看一下model
代碼里都添加了詳細(xì)注釋
class MediaBrick: NSObject {
// MARK: - 音塊一共有4種,原視頻、錄音、音樂、音效
enum MediaType {
case video
case record
case music
case soundEffect
}
var type: MediaType!
/// 最早開始時(shí)間,由于音塊可以拖動(dòng)范圍,這個(gè)其實(shí)是最早時(shí)間的限制
var startTime: TimeInterval = 0
// (1)
/// 最晚結(jié)束時(shí)間,由于音塊可以拖動(dòng)范圍,這個(gè)其實(shí)是最晚時(shí)間的限制
var endTime: TimeInterval = 0
/// 被編輯的開始時(shí)間
let modifiedStartTimeVarible = Variable<TimeInterval>(0)
/// 被編輯的結(jié)束時(shí)間
let modifiedEndTimeVarible = Variable<TimeInterval>(0)
/// 用于計(jì)算 以上時(shí)間都是基于視頻時(shí)間
var videoDuration: TimeInterval = 0
/// 這是一個(gè)view state,本來放在model是不合適的,不過這樣很方便讀取和傳遞
var collectionViewContentWidth: CGFloat = 0
/// 媒體文件的沙盒路徑
var fileUrl: URL?
/// 該段媒體文件的音量
var preferredVolume: Float = 1
/// 用于 type == .record
var pitchType: PitchType = .original
/// 用于 type == .music, 已經(jīng)被裁剪過
var musicAsset: AVAsset?
/// 用于 type == .soundEffect
let soundEffectIconUrlVariable = Variable<URL?>(nil)
// MARK: - 處理UI邏輯
let isFoldVariable = Variable<Bool>(false)
let isSelectedVariable = Variable<Bool>(false)
let deleteSubject = PublishSubject<Void>()
let beganModifyTimeSubject = PublishSubject<Void>()
let endModifyTimeSubject = PublishSubject<Void>()
/// 控制是否需要合成
var isNeedCompose: Bool = true
deinit {
print("\(description) deinit")
}
/// 一個(gè)新的對象,只復(fù)制了4個(gè)時(shí)間,僅用于計(jì)算和處理UI
func copy() -> MediaBrick {
let mediaBrick = MediaBrick()
mediaBrick.startTime = startTime
mediaBrick.endTime = endTime
mediaBrick.modifiedStartTimeVarible.value = modifiedStartTimeVarible.value
mediaBrick.modifiedEndTimeVarible.value = modifiedEndTimeVarible.value
return mediaBrick
}
}
(1)Variable對象是RxSwift對象,它本身有存儲(chǔ)功能,例如:
let modifiedStartTimeVarible = Variable<TimeInterval>(0)
modifiedStartTimeVarible.value = 1
print(modifiedStartTimeVarible.value)
可以使用modifiedStartTimeVarible.value來讀寫值。
合成
// 主要是把音頻合在視頻上,所以視頻的處理會(huì)有一些不同,傳參的時(shí)候把視頻的model和其他音頻的model分開了
static func compose(videoBrick: MediaBrick, audioBricks: [MediaBrick]) -> (AVMutableComposition, AVMutableAudioMix)? {
// 這個(gè)是最后的合成對象,新建的時(shí)候相當(dāng)于是一張白紙,準(zhǔn)備往上面畫畫
let composition = AVMutableComposition()
// 這個(gè)是控制最后的composition的音量的,一般來說都會(huì)被設(shè)計(jì)成composition的屬性,但iOS設(shè)計(jì)成了2個(gè)對象
let audioMix = AVMutableAudioMix()
// 初始化該屬性為一個(gè)空數(shù)組,之后可以直接往數(shù)組里添加對象
audioMix.inputParameters = []
// 如果沒有視頻文件,return nil并且記錄失敗
guard let fileUrl = videoBrick.fileUrl else {
logFail(mediaBrick: videoBrick)
return nil
}
let videoAsset = AVAsset(url: fileUrl)
// 視頻的全長范圍
let range = CMTimeRange(start: kCMTimeZero, duration: videoAsset.duration)
// 因?yàn)樾陆ǖ腸omposition是空的,先把原視頻的視軌添加上去
// 依次獲取視頻資源的視軌originVideoAssetTrack;創(chuàng)建composition新加的視軌originVideoCompotionTrack
guard let originVideoAssetTrack = videoAsset.tracks(withMediaType: .video).first,
let originVideoCompotionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
logFail(mediaBrick: videoBrick)
return nil
}
do {
// 將originVideoCompotionTrack填滿originVideoAssetTrack的內(nèi)容
try originVideoCompotionTrack.insertTimeRange(range, of: originVideoAssetTrack, at: kCMTimeZero)
} catch {
logFail(mediaBrick: videoBrick, error: error)
return nil
}
// 到此添加完畢
// 添加原視頻的音軌,音軌可能有多個(gè),先檢查沒有音軌return nil并且記錄失敗
let audioTracks = videoAsset.tracks(withMediaType: .audio)
guard audioTracks.count != 0 else {
logFail(mediaBrick: videoBrick)
return nil
}
// 所有被新建的originAudioCompositionTrack需要持有起來,之后被重合的音軌需要?jiǎng)h除原音音軌
var originAudioCompositionTracks: [AVMutableCompositionTrack] = []
for originAudioAssetTrack in audioTracks {
// 循環(huán)里和上面的邏輯一樣
guard let originAudioCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
logFail(mediaBrick: videoBrick)
continue
}
do {
try originAudioCompositionTrack.insertTimeRange(range, of: originAudioAssetTrack, at: kCMTimeZero)
originAudioCompositionTracks.append(originAudioCompositionTrack)
} catch {
logFail(mediaBrick: videoBrick, error: error)
continue
}
}
// 到此準(zhǔn)備工作做完了,現(xiàn)在composition已經(jīng)和原視頻文件具有相同的視軌和音軌了
// 開始合成錄音、音樂、音效
for audioBrick in audioBricks {
var mediaAsset: AVAsset!
switch audioBrick.type! {
case .record:
// 獲取本地錄音資源文件,從pcm轉(zhuǎn)到aac,并且完成變音功能
guard let fileUrl = getAACFileUrl(recordBrick: audioBrick) else { continue }
mediaAsset = AVAsset(url: fileUrl)
case .music:
// 因?yàn)橐魳房梢韵染庉?,?yōu)先取編輯之后的資源文件,再去原音樂資源文件
if let asset = audioBrick.musicAsset {
mediaAsset = asset
} else if let fileUrl = audioBrick.fileUrl {
mediaAsset = AVAsset(url: fileUrl)
} else {
continue
}
case .soundEffect:
// 獲取本地音效資源文件
guard let fileUrl = audioBrick.fileUrl else { continue }
mediaAsset = AVAsset(url: fileUrl)
default:
continue
}
// 和上面的總體邏輯一樣,獲取資源文件的音軌,添加composition的音軌
for audioAssetTrack in mediaAsset.tracks(withMediaType: .audio) {
guard let audioCompositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
logFail(mediaBrick: audioBrick)
continue
}
// 然后把資源文件的音軌插入到composition的音軌
// 但是這些音頻文件主要是在插入時(shí)間上有不同,原音軌用全范圍即可,這里用到的范圍會(huì)比較多
// 一些范圍檢查
let modifiedStartTime = max(audioBrick.modifiedStartTimeVarible.value, 0)
let modifiedEndTime = min(audioBrick.modifiedEndTimeVarible.value, videoAsset.duration.seconds)
guard modifiedStartTime < modifiedEndTime else { continue }
// 參照音頻文件的時(shí)間,是該音頻資源內(nèi)部的時(shí)間
// 被編輯的時(shí)間 - 最早時(shí)間,即是內(nèi)部的時(shí)間,這里使用的時(shí)間是CMTime
let startTimeByAudio = CMTime(seconds: modifiedStartTime - audioBrick.startTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
// 這段音頻的總時(shí)長
let audioDuration = CMTime(seconds: modifiedEndTime - modifiedStartTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
// 根據(jù)上面兩個(gè)時(shí)間,做出CMTimeRange
let rangeByAudio = CMTimeRangeMake(startTimeByAudio, audioDuration)
// 參照視頻文件的時(shí)間
let startTimeByVideo = CMTime(seconds: modifiedStartTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
do {
// 開始填充audioCompositionTrack,將上面準(zhǔn)備好的參數(shù)填入
try audioCompositionTrack.insertTimeRange(rangeByAudio, of: audioAssetTrack, at: startTimeByVideo)
} catch {
logFail(mediaBrick: audioBrick, error: error)
continue
}
// 這是控制這段音頻音量的代碼
let inputParameter = AVMutableAudioMixInputParameters(track: audioCompositionTrack)
inputParameter.setVolume(audioBrick.preferredVolume, at: kCMTimeZero)
audioMix.inputParameters.append(inputParameter)
// 如果是錄音和音樂,需要把原音軌對應(yīng)的聲音去掉,所以去掉對應(yīng)的范圍
if audioBrick.type! != .soundEffect {
// replace origin audio to empty
let removeRange = CMTimeRangeMake(startTimeByVideo, audioDuration)
originAudioCompositionTracks.forEach {
$0.removeTimeRange(removeRange)
$0.insertEmptyTimeRange(removeRange)
}
}
}
}
// 返回的composition和audioMix,會(huì)被用在AVPlayer上進(jìn)行播放
return (composition, audioMix)
}
裁剪
// 視頻支持裁剪功能,第一個(gè)參數(shù)其實(shí)是上面compose方法產(chǎn)生的composition,同時(shí)需要視頻的model來獲取裁剪時(shí)間
static func crop(asset: AVMutableComposition, videoBrick: MediaBrick) -> (AVMutableComposition, AVMutableVideoComposition?)? {
// 同樣是新建一個(gè)空的composition
let composition = AVMutableComposition()
// 范圍檢查
let startTime = videoBrick.modifiedStartTimeVarible.value
let endTime = videoBrick.modifiedEndTimeVarible.value
guard startTime < endTime else { return nil }
// 這里和之前類似,將視頻資源的視軌插入到composition新加的視軌上
guard let videoAssetTrack = asset.tracks(withMediaType: .video).first,
let videoCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
logFail(mediaBrick: videoBrick)
return nil
}
// 區(qū)別是范圍的取值,范圍取成裁剪后的范圍,裁剪功能就完成了
let startCMTime = CMTime(seconds: startTime, preferredTimescale: videoAssetTrack.naturalTimeScale)
let endCMTime = CMTime(seconds: endTime, preferredTimescale: videoAssetTrack.naturalTimeScale)
let range = CMTimeRange(start: startCMTime, end: endCMTime)
do {
try videoCompositionTrack.insertTimeRange(range, of: videoAssetTrack, at: kCMTimeZero)
} catch {
logFail(mediaBrick: videoBrick, error: error)
return nil
}
// 這里是對豎直視頻的處理,如果視頻的方向不對,需要矯正(用手機(jī)豎直拍攝的視頻方向就不對)
// 下面的代碼看做是固定處理代碼吧
// (其實(shí)所有視軌插入都需要這段代碼,不過目前用來合成的視頻方向都是正確的,而自己上傳的視頻都會(huì)先被裁剪、矯正,再進(jìn)入編輯頁)
var videoComposition: AVMutableVideoComposition?
if videoAssetTrack.preferredTransform != .identity {
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
// (1)
let transform = videoAssetTrack.ks.transform
layerInstruction.setTransform(transform, at: startCMTime)
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = range
instruction.layerInstructions = [layerInstruction]
videoComposition = AVMutableVideoComposition()
// (2)
videoComposition!.renderSize = videoAssetTrack.ks.renderSize
videoComposition!.frameDuration = CMTime(value: 1, timescale: 30)
videoComposition!.instructions = [instruction]
}
// 下面和之前的邏輯類似,根據(jù)范圍裁剪
for audioAssetTrack in asset.tracks(withMediaType: .audio) {
guard let audioCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
logFail(mediaBrick: videoBrick)
continue
}
let startCMTime = CMTime(seconds: startTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
let endCMTime = CMTime(seconds: endTime, preferredTimescale: audioAssetTrack.naturalTimeScale)
let range = CMTimeRange(start: startCMTime, end: endCMTime)
do {
try audioCompositionTrack.insertTimeRange(range, of: audioAssetTrack, at: kCMTimeZero)
} catch {
logFail(mediaBrick: videoBrick, error: error)
continue
}
}
// 返回的composition、videoComposition會(huì)在導(dǎo)出的時(shí)候使用
return (composition, videoComposition)
}
(1)(2)帶有.ks.的寫法都是自己添加的extension,具體代碼如下,主要是根據(jù)視頻的方向調(diào)整寬高
extension Kuso where T: AVAssetTrack {
var renderSize: CGSize {
let preferredTransform = base.preferredTransform
let width = floor(base.naturalSize.width)
let height = floor(base.naturalSize.height)
if preferredTransform.b != 0 {
return CGSize(width: height, height: width)
} else {
return CGSize(width: width, height: height)
}
}
var transform: CGAffineTransform {
let preferredTransform = base.preferredTransform
let width = floor(base.naturalSize.width)
let height = floor(base.naturalSize.height)
if preferredTransform.b == 1 { // home在左
return CGAffineTransform(translationX: height, y: 0).rotated(by: CGFloat.pi/2)
} else if preferredTransform.b == -1 { // home在右
return CGAffineTransform(translationX: 0, y: width).rotated(by: CGFloat.pi/2 * 3)
} else { // home在上
return CGAffineTransform(translationX: width, y: height).rotated(by: CGFloat.pi)
}
}
var appropriateExportPreset: String {
if renderSize.width <= 640 {
return AVAssetExportPreset640x480
} else if renderSize.width <= 960 {
return AVAssetExportPreset960x540
} else if renderSize.width <= 1280 {
return AVAssetExportPreset1280x720
} else {
return AVAssetExportPreset1920x1080
}
}
}
導(dǎo)出
// 完成視頻編輯后,需要把內(nèi)存里的composition audioMix videoComposition都導(dǎo)出到沙盒,存儲(chǔ)起來,用來上傳
static func exportComposedVideo(composition: AVComposition, audioMix: AVAudioMix? = nil, videoComposition: AVVideoComposition? = nil) -> Observable<URL> {
return Observable<URL>.create({ (observer) -> Disposable in
// 根據(jù)視軌的分辨率取得合適的導(dǎo)出分辨率
let exportPreset = composition.ks.appropriateExportPreset
// 獲取兼容性的exportSession
// (1)
guard let exportSession = AVAssetExportSession.ks.compatibleSession(asset: composition, priorPresetName: exportPreset) else {
return Disposables.create()
}
// 根據(jù)時(shí)間戳新建一個(gè)視頻文件路徑
let outputUrl = FileManager.ks.newEditVideoUrl
// 設(shè)置exportSession的參數(shù)
exportSession.audioMix = audioMix
exportSession.videoComposition = videoComposition
exportSession.outputFileType = .mp4
exportSession.outputURL = outputUrl
exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronously { [weak exportSession] in
guard let es = exportSession else {
return
}
switch es.status {
case .completed:
// 成功則發(fā)出最終的url
observer.onNext(outputUrl)
case .failed:
// 失敗則拋出錯(cuò)誤
if let error = es.error {
logFail(error: error)
observer.onError(error)
}
default:
break
}
}
return Disposables.create {
// 如果這個(gè)observer被取消了,也把正在export的session取消掉
exportSession.cancelExport()
}
})
// 很隨意的異步一下,其實(shí)意義不大
.observeOn(MainScheduler.asyncInstance)
}
(1)其實(shí)就是按照分辨率等級(jí)依次取合適的AVAssetExportSession
let defaultPresets = [AVAssetExportPreset1280x720, AVAssetExportPreset960x540, AVAssetExportPreset640x480, AVAssetExportPresetMediumQuality, AVAssetExportPresetLowQuality]
extension Kuso where T == AVAssetExportSession {
static func compatibleSession(asset: AVAsset, priorPresetName: String) -> AVAssetExportSession? {
if let es = T(asset: asset, presetName: priorPresetName) {
return es
} else {
let compatiblePresets = T.exportPresets(compatibleWith: asset)
for defaultPreset in defaultPresets {
guard compatiblePresets.contains(defaultPreset) else {
continue
}
return T(asset: asset, presetName: defaultPreset)
}
return nil
}
}
}
添加水印、文字等
static func addWatermark(fileUrl: URL) -> Observable<URL> {
return Observable<URL>.create { (observer) -> Disposable in
// 都是從空的composition開始
let composition = AVMutableComposition()
// 資源文件、視頻范圍
let videoAsset = AVAsset(url: fileUrl)
let range = CMTimeRange(start: kCMTimeZero, end: videoAsset.duration)
// 獲取資源文件視軌,創(chuàng)建新的待添加的視軌
guard let videoAssetTrack = videoAsset.tracks(withMediaType: .video).first,
let videoCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
return Disposables.create()
}
do {
// 將videoCompostionTrack填滿videoAssetTrack的內(nèi)容
try videoCompositionTrack.insertTimeRange(range, of: videoAssetTrack, at: kCMTimeZero)
} catch {
observer.onError(error)
return Disposables.create()
}
// 加水印需要使用AVMutableVideoCompositionLayerInstruction
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
// 如果方向不對 矯正
if videoAssetTrack.preferredTransform != .identity {
let transform = videoAssetTrack.ks.transform
layerInstruction.setTransform(transform, at: kCMTimeZero)
}
// 固定寫法
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = range
instruction.layerInstructions = [layerInstruction]
let videoComposition = AVMutableVideoComposition()
videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
let renderSize = videoAssetTrack.ks.renderSize
videoComposition.renderSize = renderSize
videoComposition.instructions = [instruction]
// 加水印的層級(jí)分為3個(gè)layer parentLayer作為底 videoLayer上放的是視頻 還有一個(gè)watermarkLayer上放置水印或者其他自定義類容 例如文字
let parentLayer = CALayer()
let videoLayer = CALayer()
parentLayer.addSublayer(videoLayer)
[parentLayer, videoLayer].forEach{
$0.frame = CGRect(origin: .zero, size: renderSize)
}
// 固定寫法
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
// 3個(gè)layer從下到上依次為 parentLayer videoLayer watermarkLayer,后2個(gè)layer的順序可以根據(jù)需求交換,改變size大小等
// 這里創(chuàng)建的watermarkLayer已經(jīng)被添加了一些CoreAnimation,這樣加出來的水印就可以動(dòng)了
let watermarkLayer = self.createWatermarkLayer(parentSize: renderSize)
parentLayer.addSublayer(watermarkLayer)
// 兼容某些質(zhì)量很差的視頻,把導(dǎo)出參數(shù)降低,AVAssetExportPresetMediumQuality其實(shí)是一種很兼容,視頻很模糊的選項(xiàng)
var exportPreset: String!
let minFrameDuration = videoAssetTrack.minFrameDuration
if minFrameDuration.seconds < 0.001 {
exportPreset = AVAssetExportPresetMediumQuality
} else {
exportPreset = videoAssetTrack.ks.appropriateExportPreset
}
// 對音頻做上面的類似操作,只需要加進(jìn)去即可
for originAudioAssetTrack in videoAsset.tracks(withMediaType: .audio) {
guard let audioCompositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
continue
}
do {
try audioCompositionTrack.insertTimeRange(range, of: originAudioAssetTrack, at: kCMTimeZero)
} catch {
observer.onError(error)
return Disposables.create()
}
}
// 導(dǎo)出到沙盒
guard let exportSession = AVAssetExportSession.ks.compatibleSession(asset: composition, priorPresetName: exportPreset) else {
return Disposables.create()
}
// 根據(jù)時(shí)間戳新建一個(gè)水印目錄下的文件
let outputUrl = FileManager.ks.newWatermarkVideoUrl
exportSession.videoComposition = videoComposition
exportSession.outputFileType = .mp4
exportSession.outputURL = outputUrl
exportSession.shouldOptimizeForNetworkUse = true
// exportSession有progress可以讀取,但是不能kvo或者有回調(diào)通知,只能加個(gè)timer來讀取進(jìn)度
let timer = Timer(timeInterval: 0.05, repeats: true, block: { [weak exportSession] (timer) in
guard let es = exportSession else {
return
}
let progress = Double(es.progress) * 0.49 + 0.5
self.progressHandler?(progress)
if es.progress == 1 {
timer.invalidate()
}
})
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)
timer.fire()
exportSession.exportAsynchronously { [weak exportSession] in
guard let es = exportSession else {
return
}
switch es.status {
case .completed:
// 成功后發(fā)出最后的url
observer.onNext(outputUrl)
case .failed:
// 有錯(cuò)誤則發(fā)出錯(cuò)誤
if let error = es.error {
observer.onError(error)
}
default:
break
}
}
return Disposables.create {
// 如果該操作被取消,停掉timer和exportSession
timer.invalidate()
exportSession.cancelExport()
}
}
}
/* 下面是創(chuàng)建layer的坐標(biāo)、大小計(jì)算,以及動(dòng)畫添加 */
static func createWatermarkLayer(parentSize: CGSize) -> CALayer {
// 坐標(biāo)軸原點(diǎn)為0,0 右上角為 +,+
let multiper = max(parentSize.width, parentSize.height)/1080 * 2.3
let layerSize = CGSize(width: multiper * 95, height: multiper * 61)
let layerStartPosition = CGPoint(x: layerSize.width/2, y: parentSize.height - layerSize.height/2)
let layerEndPosition = CGPoint(x: parentSize.width - layerSize.width/2, y: layerSize.height/2)
let layer = CALayer()
layer.frame = CGRect(origin: .zero, size: layerSize)
layer.position = layerStartPosition
addPositionAnimation(layer: layer, startPosition: layerStartPosition, endPosition: layerEndPosition)
let logoSize = CGSize(width: multiper * 90, height: multiper * 50)
let logoPosition = CGPoint(x: logoSize.width/2, y: 11 * multiper + logoSize.height/2)
let logoLayer = CALayer()
logoLayer.frame = CGRect(origin: .zero, size: logoSize)
logoLayer.position = logoPosition
addContentsAnimation(layer: logoLayer)
layer.addSublayer(logoLayer)
let idSize = CGSize(width: layerSize.width, height: multiper * 16.5)
let idPosition = CGPoint(x: idSize.width/2 - 11.5 * multiper, y: 5 * multiper + idSize.height/2)
let idLayer = CATextLayer()
idLayer.frame = CGRect(origin: .zero, size: idSize)
idLayer.position = idPosition
idLayer.string = "ID: \(userId.description)"
idLayer.foregroundColor = UIColor.white.cgColor
idLayer.fontSize = 12 * multiper
idLayer.font = CGFont.init(UIFont.boldSystemFont(ofSize: idLayer.fontSize).fontName as CFString)
idLayer.alignmentMode = kCAAlignmentRight
layer.addSublayer(idLayer)
return layer
}
static func addPositionAnimation(layer: CALayer, startPosition: CGPoint, endPosition: CGPoint) {
let keyframe = CAKeyframeAnimation(keyPath: "position")
keyframe.values = [startPosition, endPosition]
keyframe.duration = 10
keyframe.isRemovedOnCompletion = false
keyframe.fillMode = kCAFillModeForwards
keyframe.beginTime = AVCoreAnimationBeginTimeAtZero
keyframe.calculationMode = kCAAnimationDiscrete
layer.add(keyframe, forKey: "position")
}
static func addContentsAnimation(layer: CALayer) {
let imgs = (0...21).map { idx -> CGImage in
let name = "wm_\(idx)"
return UIImage(named: name)!.cgImage!
}
layer.contents = imgs.first
let keyframe = CAKeyframeAnimation(keyPath: "contents")
keyframe.duration = 1
keyframe.values = imgs
keyframe.repeatCount = .greatestFiniteMagnitude
keyframe.isRemovedOnCompletion = false
keyframe.beginTime = AVCoreAnimationBeginTimeAtZero
layer.add(keyframe, forKey: "contents")
}