iOS 10 的 Photography

iOS 10中新特性:

  • 有史以来第一次,你的app 可以拍摄和编辑 live photos
  • 可以响应不同的图片捕获

一、相机捕获内容显示(输入)

  1. 创建名为PhotoMe并设置只有IPhone使用的项目,竖屏。
  2. 授权:在Info.plist 中添加
<key>NSCameraUsageDescription</key>
    <string>PhotoMe needs the camera to take photos. Duh!</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>PhotoMe needs the microphone to record audio with Live Photos.</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>PhotoMe will save photos in the Photo Library.</string>
  1. 自定义View:创建名为CameraPreviewView 的UIView子类
import UIKit
import AVFoundation
import Photos
class CameraPreviewView: UIView {
    //1
    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    }
    //2
    var cameraPreviewLayer: AVCaptureVideoPreviewLayer {
        return layer as! AVCaptureVideoPreviewLayer
    }
    //3
    var session: AVCaptureSession? {
        get {
            return cameraPreviewLayer.session
        }
        set {
            cameraPreviewLayer.session = newValue
        }
    } 
}
  1. 画界面:在Main.storyboard 中,拖入一个UIView,并设定约束,上,左,右和父视图对齐,宽高比为3:4,设置自定义类为CameraPreviewView,向ViewController连接名为cameraPreviewView的outlet属性。
  2. 在ViewController中:
import AVFoundation

添加属性:

    fileprivate let session = AVCaptureSession()
    fileprivate let sessionQueue = DispatchQueue(label: "com.razeware.PhotoMe.session-queue")
    var videoDeviceInput: AVCaptureDeviceInput!

添加方法:

    private func prepareCaptureSession() {
        // 1
        session.beginConfiguration()
        session.sessionPreset = AVCaptureSessionPresetPhoto
        
        do {
            // 2
            let videoDevice = AVCaptureDevice.defaultDevice(
                withDeviceType: .builtInWideAngleCamera,
                mediaType: AVMediaTypeVideo,
                position: .front)
            // 3
            let videoDeviceInput = try
                AVCaptureDeviceInput(device: videoDevice)
            
            // 4
            if session.canAddInput(videoDeviceInput) {
                session.addInput(videoDeviceInput)
                self.videoDeviceInput = videoDeviceInput
                
                // 5
                DispatchQueue.main.async {
                    self.cameraPreviewView.cameraPreviewLayer
                        .connection.videoOrientation = .portrait
                }
            } else {
                print("Couldn't add device to the session")
                return
            }
        } catch {
            print("Couldn't create video device input: \(error)")
            return
        }
        
        // 6
        session.commitConfiguration()
    }

在viewDidLoad() 中,授权,配置session:

        //1
        cameraPreviewView.session = session
        //2
        sessionQueue.suspend()
        //3
        AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) {
            success in
            if !success {
                print("Come on, it's a camera app!")
                return
            }
            //4
            self.sessionQueue.resume()
        }
        
        sessionQueue.async {
            [unowned self] in
            self.prepareCaptureSession()
        }

在viewWillAppear() 中,启动session:

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        sessionQueue.async {
            self.session.startRunning()
        }
    }

二、拍照(输出)

  1. 在ViewController 中:
    添加属性:
fileprivate let photoOutput = AVCapturePhotoOutput()

prepareCaptureSession()中,prepareCaptureSession()前面:

 if session.canAddOutput(photoOutput) {
     session.addOutput(photoOutput)
     // 必须在session启动之前配置,否则会发生闪烁
     photoOutput.isHighResolutionCaptureEnabled = true
 } else {
     print("Unable to add photo output")
     return
 }
  1. 画界面:
  2. 在Main.storyboard 中设置main view 和 camera preview view 的background 为black,main view的tint color 为 orange。
  3. main view拖入Visual Effect View With Blur,设置约束为左,下,右与父视图对齐。 Blur Style 为 Dark。
  4. Visual Effect View With Blur 拖入 button,名称为Take Photo!,字体为20,并将其放入stack View 中。
  5. 对stack View 添加顶部距父视图为5,底部距父视图为20的约束。
  6. 连接button到ViewController 中:
@IBOutlet weak var shutterButton: UIButton!
@IBAction func handleShutterButtonTap(_ sender: UIButton) {
        capturePhoto()
}
extension ViewController {
    fileprivate func capturePhoto() {
        // 1
        let cameraPreviewLayerOrientation = cameraPreviewView
            .cameraPreviewLayer.connection.videoOrientation
        
        // 2
        sessionQueue.async {
            if let connection = self.photoOutput
                .connection(withMediaType: AVMediaTypeVideo) {
                connection.videoOrientation =
                cameraPreviewLayerOrientation
            }
            
            // 3
            let photoSettings = AVCapturePhotoSettings()
            photoSettings.flashMode = .off
            photoSettings.isHighResolutionPhotoEnabled = true
            
        }
    }
}
  1. 创建代理:因为一张照片还没处理完就可以拍另一张,所以ViewController 持有一个键为AVCapturePhotoSettings,值为代理的字典。
import AVFoundation
import Photos
class PhotoCaptureDelegate: NSObject {
    // 1
    var photoCaptureBegins: (() -> ())? = .none
    var photoCaptured: (() -> ())? = .none
    fileprivate let completionHandler: (PhotoCaptureDelegate, PHAsset?)-> ()
    
    // 2
    fileprivate var photoData: Data? = .none
    
    // 3
    init(completionHandler: @escaping (PhotoCaptureDelegate, PHAsset?) -> ()) {
        self.completionHandler = completionHandler
    }
    
    // 4
    fileprivate func cleanup(asset: PHAsset? = .none) {
        completionHandler(self, asset)
    }
}
  1. 原理如图:


    photo capture.png
  2. 实现代理方法
extension PhotoCaptureDelegate: AVCapturePhotoCaptureDelegate {
    // Process data completed
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishProcessingPhotoSampleBuffer
        photoSampleBuffer: CMSampleBuffer?,
                 previewPhotoSampleBuffer: CMSampleBuffer?,
                 resolvedSettings: AVCaptureResolvedPhotoSettings,
                 bracketSettings: AVCaptureBracketedStillImageSettings?,
                 error: Error?) {
        
        guard let photoSampleBuffer = photoSampleBuffer else {
            print("Error capturing photo \(error)")
            return
        }
        photoData = AVCapturePhotoOutput
            .jpegPhotoDataRepresentation(
                forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
    }
    
    // Entire process completed
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishCaptureForResolvedSettings
        resolvedSettings: AVCaptureResolvedPhotoSettings,
                 error: Error?) {
        
        // 1
        guard error == nil, let photoData = photoData else {
            print("Error \(error) or no data")
            cleanup()
            return
        }
        
        // 2
        PHPhotoLibrary.requestAuthorization {
            [unowned self]
            (status) in
            // 3
            guard status == .authorized else {
                print("Need authorisation to write to the photo library")
                self.cleanup()
                return
            }
            // 4
            var assetIdentifier: String?
            PHPhotoLibrary.shared().performChanges({
                let creationRequest = PHAssetCreationRequest.forAsset()
                let placeholder = creationRequest
                    .placeholderForCreatedAsset
                
                creationRequest.addResource(with: .photo,
                                            data: photoData, options: .none)
                
                assetIdentifier = placeholder?.localIdentifier
                
            }, completionHandler: { (success, error) in
                if let error = error {
                    print("Error saving to the photo library: \(error)")
                }
                var asset: PHAsset? = .none
                if let assetIdentifier = assetIdentifier {
                    asset = PHAsset.fetchAssets(
                        withLocalIdentifiers: [assetIdentifier],
                        options: .none).firstObject
                }
                self.cleanup(asset: asset)
            })
        }
    }
}
  1. 在ViewController中:
    添加属性:
fileprivate var photoCaptureDelegates = [Int64 : PhotoCaptureDelegate]()

在capturePhoto()的queue 闭包末尾添加:

            // 1
            let uniqueID = photoSettings.uniqueID
            let photoCaptureDelegate = PhotoCaptureDelegate() {
                [unowned self] (photoCaptureDelegate, asset) in
                self.sessionQueue.async { [unowned self] in
                    self.photoCaptureDelegates[uniqueID] = .none
                }
            }
            // 2
            self.photoCaptureDelegates[uniqueID] = photoCaptureDelegate
            // 3
            self.photoOutput.capturePhoto(
                with: photoSettings, delegate: photoCaptureDelegate)

三、难以置信的

  1. 添加闪屏动画
    在capturePhoto()创建delegate 后,添加:
    photoCaptureDelegate.photoCaptureBegins = { [unowned self] in
    DispatchQueue.main.async {
    self.shutterButton.isEnabled = false
    self.cameraPreviewView.cameraPreviewLayer.opacity = 0
    UIView.animate(withDuration: 0.2) {
    self.cameraPreviewView.cameraPreviewLayer.opacity = 1
    }
    }
    }
    photoCaptureDelegate.photoCaptured = { [unowned self] in
    DispatchQueue.main.async {
    self.shutterButton.isEnabled = true
    }
    }
    在PhotoCaptureDelegate的extension 中:
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 willCapturePhotoForResolvedSettings
        resolvedSettings: AVCaptureResolvedPhotoSettings) {
        photoCaptureBegins?()
    }
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didCapturePhotoForResolvedSettings
        resolvedSettings: AVCaptureResolvedPhotoSettings) {
        photoCaptured?()
    }
  1. 展示缩略图
  • 获取缩略图:在PhotoCaptureDelegate添加属性:
var thumbnailCaptured: ((UIImage?) -> ())? = .none

在代理方法didFinishProcessingPhotoSampleBuffer末尾添加:

        if let thumbnailCaptured = thumbnailCaptured,
            let previewPhotoSampleBuffer = previewPhotoSampleBuffer,
            let cvImageBuffer =
            CMSampleBufferGetImageBuffer(previewPhotoSampleBuffer) {
            
            let ciThumbnail = CIImage(cvImageBuffer: cvImageBuffer)
            let context = CIContext(options: [kCIContextUseSoftwareRenderer:
                false])
            let thumbnail = UIImage(cgImage: context.createCGImage(ciThumbnail,
                                                                   from: ciThumbnail.extent)!, scale: 2.0, orientation: .right)
            
            thumbnailCaptured(thumbnail)
        }
  • 画界面:设置switch 为 off,label 的文本颜色为white,imageView 的 clip to bounds 为 true,content mode 为 Aspect Fill。


    main storyboard.png

    UI.png

    连线:

    @IBOutlet weak var previewImageView: UIImageView!
    @IBOutlet weak var thumbnailSwitch: UISwitch!
  • 设置缩略图格式: 在capturePhoto()中,创建delegate之前添加:
            if self.thumbnailSwitch.isOn
                && photoSettings.availablePreviewPhotoPixelFormatTypes
                    .count > 0 {
                photoSettings.previewPhotoFormat = [
                    kCVPixelBufferPixelFormatTypeKey as String :
                        photoSettings
                            .availablePreviewPhotoPixelFormatTypes.first!,
                    kCVPixelBufferWidthKey as String : 160,
                    kCVPixelBufferHeightKey as String : 160
                ]
            }
  • 展示缩略图:在创建delegate之后添加:
            photoCaptureDelegate.thumbnailCaptured = { [unowned self] image in
                DispatchQueue.main.async {
                    self.previewImageView.image = image
                }
            }
  1. Live Photos
  • 画界面:在Option Stack 中拖入一Horizontal Stack View,嵌入一个switch 和 一个 label,设置switch 为 off,label 的text 为 Live Photo Mode, text color 为 white。向Control Stack 加入一个label ,text 为 capturing...,字体大小为35,hidden 为 true,text color 为 orange。
    连线:
    @IBOutlet weak var capturingLabel: UILabel!
    @IBOutlet weak var livePhotoSwitch: UISwitch!
  • 添加音频输入:在prepareCaptureSession()中,创建video device input 之后添加:
        do {
            let audioDevice = AVCaptureDevice.defaultDevice(withMediaType:
                AVMediaTypeAudio)
            let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
            if session.canAddInput(audioDeviceInput) {
                session.addInput(audioDeviceInput)
            } else {
                print("Couldn't add audio device to the session")
                return
            }
        } catch {
            print("Unable to create audio device input: \(error)")
            return
        }
  • 设置Photo 输出:在photoOutput.isHighResolutionCaptureEnabled = true 之后添加:
            photoOutput.isLivePhotoCaptureEnabled =
                photoOutput.isLivePhotoCaptureSupported
            DispatchQueue.main.async {
                self.livePhotoSwitch.isEnabled =
                    self.photoOutput.isLivePhotoCaptureSupported
  • 设置输出路径:在capturePhoto()中,创建delegate之前添加:
    if self.livePhotoSwitch.isOn {
    let movieFileName = UUID().uuidString
    let moviePath = (NSTemporaryDirectory() as NSString)
    .appendingPathComponent("(movieFileName).mov")
    photoSettings.livePhotoMovieFileURL = URL(
    fileURLWithPath: moviePath)
    }
  • 捕获live photo:在PhotoCaptureDelegate添加属性:
    var capturingLivePhoto: ((Bool) -> ())? = .none
    fileprivate var livePhotoMovieURL: URL? = .none

在代理方法willCapturePhotoForResolvedSettings末尾添加:

        if resolvedSettings.livePhotoMovieDimensions.width > 0
            && resolvedSettings.livePhotoMovieDimensions.height > 0 {
            capturingLivePhoto?(true)
        }

添加代理方法:

    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishRecordingLivePhotoMovieForEventualFileAt
        outputFileURL: URL,
                 resolvedSettings: AVCaptureResolvedPhotoSettings) {
        capturingLivePhoto?(false)
    }
    
    func capture(_ captureOutput: AVCapturePhotoOutput,
                 didFinishProcessingLivePhotoToMovieFileAt outputFileURL:
        URL,
                 duration: CMTime,
                 photoDisplay photoDisplayTime: CMTime,
                 resolvedSettings: AVCaptureResolvedPhotoSettings,
                 error: Error?) {
        if let error = error {
            print("Error creating live photo video: \(error)")
            return
        }
        livePhotoMovieURL = outputFileURL
    }
  • 添加视频到相册:在代理方法didFinishCaptureForResolvedSettings:error:)中,addResource 后添加:
                
                if let livePhotoMovieURL = self.livePhotoMovieURL {
                    let movieResourceOptions = PHAssetResourceCreationOptions()
                    movieResourceOptions.shouldMoveFile = true
                    creationRequest.addResource(with: .pairedVideo,
                                                fileURL: livePhotoMovieURL, options:
                        movieResourceOptions)
                }
  • 更新UI:在ViewController添加属性:
fileprivate var currentLivePhotoCaptures: Int = 0

在capturePhoto()中,创建delegate之后添加:

            // Live photo UI updates
            photoCaptureDelegate.capturingLivePhoto = { (currentlyCapturing) in
                DispatchQueue.main.async { [unowned self] in
                    self.currentLivePhotoCaptures += currentlyCapturing ? 1 : -1
                    UIView.animate(withDuration: 0.2) {
                        self.capturingLabel.isHidden =
                            self.currentLivePhotoCaptures == 0
                    }
                }
            }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容