ラベル iOS10 の投稿を表示しています。 すべての投稿を表示
ラベル iOS10 の投稿を表示しています。 すべての投稿を表示

2017年3月1日水曜日

AVCapturePhotoOutputで少し難しくカメラアプリを作る

カメラで撮影するアプリを作るには UIImagePickerController を使うのが一番楽なんだけど、これだと細かいことができないので、 AVCapturePhotoOutput を使った書き方をしてみる。
iOS9までは別のクラス使ってたんだけど、iOS10からは非推奨になったので、これを使わにゃいかんのだ。

必要なFrameworkはAVFoundation。
以下に全コードを載せる。
撮影ボタンをUIButtonじゃなくて上部のツールバーにしてるけど、まあいいっしょ?
コードをシンプルにして構造をわかりやすくするため、極力エラー処理は省いてある。


カメラの使用とフォトライブラリへのアクセスをするので、あらかじめInfo.plistにそれに関するプライバシーの警告テキストを入力しておくこと。んでないと落ちる。
AVCaptureSessionクラス に入出力の情報をいろいろ設定し、startRunning() でカメラのライブビュー(プレビュー)表示を開始。ライブビュー映像はLayerを指定しないといけないようなので、imageViewにLayerを追加して表示している。
撮影ボタンを押すと takePhoto() が呼ばれ撮影。
撮影された画像はdelegateメソッドである capture(〜)メソッド の引数として受け取れるので、そこでDataからUIImageに変換。
それを普通に UIImageWriteToSavedPhotosAlbum()メソッド で写真ライブラリに保存している。

シミュレータじゃカメラ機能が使えないからiPadの実機で試したんだけど、撮影時のflashModeの行をコメントアウトしないと落ちてしまう。
iPadにはストロボないけど、.autoにしとけば自動で判断してくれそうなものだが。

コード

import UIKit
import AVFoundation

//キャプチャの入出力管理クラス
var captureSession:AVCaptureSession?
//静止画データの出力用
var stillImageOutput:AVCapturePhotoOutput?
//キャプチャ画像のプレビュー用
var videoPreviewLayer:AVCaptureVideoPreviewLayer?
//プレビュー表示用
var imageView = UIImageView()

class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //ツールバーに撮影ボタン追加
        let toolBar = UIToolbar(frame: CGRect(x: 0, y: 10, width: view.bounds.size.width, height: 44))
        let takePhotoButton = UIBarButtonItem(title: "撮影", style: .plain, target: self, action: #selector(ViewController.takePhoto))
        toolBar.items = [takePhotoButton]
        view.addSubview(toolBar)

        //プレビューに使うimageViewを作成
        imageView = UIImageView(frame: CGRect(x: 0, y: toolBar.frame.size.height, width: view.bounds.size.width, height: view.bounds.size.height))
        self.view.addSubview(imageView)

        
        //カメラの画像をキャプチャするクラスを作る
        captureSession = AVCaptureSession()
        //撮影解像度設定
        captureSession?.sessionPreset = AVCaptureSessionPresetPhoto
        
        //キャプチャーデバイスをビデオカメラに設定
        let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
        //ビデオを入力する。try!なのでエラー処理をせず、入力エラーの場合クラッシュ
        let input = try! AVCaptureDeviceInput(device: device)
        //キャプチャー管理のクラスに入力情報追加
        captureSession?.addInput(input)
        //キャプチャー管理のクラスに出力情報追加
        stillImageOutput = AVCapturePhotoOutput()
        captureSession?.addOutput(stillImageOutput)
        
        
        //プレビューレイヤー作成
        videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        //プレビューレイヤーのframe設定
        videoPreviewLayer?.frame = imageView.bounds
        //UIImageViewのContentModeに相当
        videoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
        //Layerのbounds部分以外をマスクする
        videoPreviewLayer?.masksToBounds = true
        //imageViewのlayerとしてプレビューレイヤーを乗せる
        imageView.layer.addSublayer(videoPreviewLayer!)

        
        //セッション開始
        captureSession?.startRunning()
        
    }
    
    func takePhoto() {
        let settingsForMonitoring = AVCapturePhotoSettings()
        //カメラのストロボモード
//        settingsForMonitoring.flashMode = .auto //コメントにしないと落ちる
        //カメラの手ぶれ補正
        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true
        //最高解像度で撮影するか否か
        settingsForMonitoring.isHighResolutionPhotoEnabled = false
        //画面キャプチャ処理
        //撮影された画像はdelegateメソッドで処理する
        stillImageOutput?.capturePhoto(with: settingsForMonitoring, delegate: self)
    }
    
    //AVCapturePhotoCaptureDelegateメソッド
    func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
        //結果がnilじゃなければ
        if let photoSampleBuffer = photoSampleBuffer {
            //キャプチャされたphotoSampleBufferの画像をJPEGとしてDataに直す
            //previewPhotoはサムネイル画像らしい
            let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
            //DataをUIImageに変換
            let image = UIImage(data: photoData!)
            //写真ライブラリに保存
            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
        }
    }

}


エラーチェックをする場合

     で挟まれたところを置き換える。

//キャプチャするデバイスをビデオに設定
        guard let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else {
            print("デバイスエラー")
            return
        }
        //ビデオを入力する。try!なのでエラー処理をせず、入力エラーの場合クラッシュ
        let input = try! AVCaptureDeviceInput(device: device)
        //追加可能な入力かどうか調べる
        if captureSession!.canAddInput(input) {
            //キャプチャー管理のクラスに入力情報追加
            captureSession?.addInput(input)
            //静止画の出力クラスを設定
            stillImageOutput = AVCapturePhotoOutput()
            //追加可能な出力かどうか調べる
            if captureSession!.canAddOutput(stillImageOutput) {
                //キャプチャー管理のクラスに出力情報追加
                captureSession?.addOutput(stillImageOutput)
                
                //プレビューレイヤー作成
                videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
                //guardは、nilでなければunwrapして返し、nilならそこで終る
                guard let _videoPreviewLayer = videoPreviewLayer else {
                    return
                }
                //プレビューレイヤーのframe設定
                _videoPreviewLayer.frame = imageView.bounds
                //UIImageViewで言うところのContentModeのこと
                _videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
                //Layerのbounds部分以外をマスクする
                _videoPreviewLayer.masksToBounds = true
                //imageViewのlayerとしてプレビューレイヤーを乗せる
                imageView.layer.addSublayer(_videoPreviewLayer)
                
                //セッション開始
                captureSession?.startRunning()
            } else {
                print("出力先エラー")
            }
        } else {
            print("入力元エラー")
        }
参考

2016年9月21日水曜日

Info.plistをローカライズする

注意:結論から言うとうまくいきませんでした。

iOS10、Xcode8からプライバシー関連の設定が増え、例えば画像ライブラリにアクセスするコードを書く場合に、あらかじめInfo.plistの必要な項目にユーザーに伝える使用目的を明示するメッセージを書かなければいけなくなった。
(書いたメッセージがアクセス時にアラートで表示される)

ま、そりゃいいんだけど、そこに日本語用、英語用なんて項目はないんで、Info.plist自体をローカライズして日本語用、英語用を作ることになる。
やり方としてはInfo.plistを選んで、右側のインスペクタからLocalize...ボタンを押して云々なのでまあわかるだろう。

でもそれやってビルドしようとしたら、
error: could not read data from 'パス/アプリ名-Info.plist': The file "アプリ名-Info.plist" couldn't be opened because there is no such file.
というエラーが出てビルドできなかった。
メッセージの内容としてはInfo.plistが見つかんないから開けねぇぞってことだけど、ちゃんとplistファイルはある。

調べたところ、Xcodeの以前からのバグで、パスがおかしいんだそうな。
プロジェクトのパッケージ内容を開いてproject.pbxprojを開き、中の該当部分を絶対パスから相対パスに書き直すといいとあったんで見たけど、どうも最初から相対パスになってるっぽい。(参考リンクもう一つ)
一度Localizeのチェックを外し、再度チェックすると直るとか書いてあったんだけど、試してもダメ。

それ以外の情報もないようなので、メッセージは英語と日本語両方入れておく。ダサダサ〜。
やむを得ずこうなった
後で解決方法がわかったら直します。
こんなとこで止まってリリースを遅らしちゃなんねぇ!!

2016年9月19日月曜日

プライバシー設定をplistにしとかないと落ちる

シミュレータで落ちる!

iOS10、Xcode8になってから、プライバシーに関する機能を使う際に、Info.plistにあらかじめ宣言(みたいなこと)しとかないと、シミュレータで落ちてしまうようになった。

たとえばフォトライブラリにアクセスするようなAPIを使ったら、こんなん出て落ちた。
[access] This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data.
アプリがクラッシュしたのは、プライバシーに敏感なデータにアクセスしようとしたためです。アプリのInfo.plistは、ユーザーにアプリがどのようにこのデータを使うかについて説明している文字列値で、NSPhotoLibraryUsageDescriptionキーを含まなければなりません。
だそうです。

 落ちないようにしましょう

アプリ名-Info.plistにPrivacy - から始まる項目を追加する(探すとメニューで出てくる)。
そこに、フォトライブラリにアクセスする際に表示させるAlertのメッセージを書き込んでおしまい。

そうすると最初のアクセス時に以下のように出て、無事クラッシュせずにアクセスできるようになった。
かあんたん(≧∇≦)
でも、もともとフォトライブラリにアクセスするような場合って、iOS側が確認のアラートを表示してくれてたよね。
メッセージ部分を入力しなければ、デフォルトでいつものメッセージを表示してくれるんだけど、メッセージがないと審査の段階で自動的にアップロードしたビルドが拒否されちゃう。
一応英語で書いたけど、他言語対応はどうやるのかね?
しかしめんどくさくなったねぇ。

Info.plistに設定が必要になったらしい機能の一覧はこちらのサイト。

Info.plistの候補からまとめてみた(iOS10、Xcode8時点)
  • Privacy - Bluetooth Peripheral Usage Description
    • Bluetoothを使うとき
  • Privacy - Calendars Usage Description
    • カレンダーにアクセスするとき
  • Privacy - Camera Usage Description
    • カメラを使うとき
  • Privacy - Contacts Usage Description
    • 住所録を使うとき
  • Privacy - Health Share Usage Description
    • ヘルスの機能を使うとき
  • Privacy - Health Update Usage Description
    • ヘルスのアップデートを使うとき
  • Privacy - HomeKit Usage Description
    • ホームキットを使うとき
  • Privacy - Location Always Usage Description
    • 位置情報をアプリ裏でも常に使うとき
  • Privacy - Location Usage Description
    • 位置情報を使うとき
  • Privacy - Location When In Use Usage Description
    • 場所に着いた時点(?)の位置情報を使うとき
  • Privacy - Media Library Usage Description
    • メディアライブラリを使うとき
  • Privacy - Microphone Usage Description
    • マイクを使うとき
  • Privacy - Motion Usage Description
    • モーション機能を使うとき
  • Privacy - Music Usage Description
    • ミュージックにアクセスするとき
  • Privacy - Photo Library Usage Description
    • 画像ライブラリにアクセスするとき
  • Privacy - Reminders Usage Description
    • リマインダーにアクセスするとき
  • Privacy - Siri Usage Description
    • Siriを使うとき
  • Privacy - Speech Recognition Usage Description
    • 音声認識機能を使うとき
  • Privacy - TV Provider Usage Description
    • TVプロバイダ機能(そんなのあるの?)を使うとき
  • Privacy - Video Subscriber Account Usage Description
    • ビデオ購読機能を使うとき

2016年9月18日日曜日

変なメッセージ:Xcode8、iOS10でいっぱい出た

iOS10、Xcode8にしたところ、特にシミュレータを使った時に、これまで出なかったわけのわかんないメッセージが100行以上もずらずら出てしまった。

こんなのや⇩
2016-09-19 00:34:50.662934 SkyReporter[2273:1024925] [DYMTLInitPlatform] platform initialization successful2016-09-19 00:34:50.830564 SkyReporter[2273:1024768] [MC] System group container for systemgroup.com.apple.configurationprofiles path is /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles2016-09-19 00:34:50.831165 SkyReporter[2273:1024768] [MC] Reading from public effective user settings.
2016-09-19 00:35:12.281089 SkyReporter[2273:1024768] Metal GPU Frame Capture Enabled2016-09-19 00:35:12.281607 SkyReporter[2273:1024768] Metal API Validation Enabled2016-09-19 00:35:12.355170 SkyReporter[2273:1024966] [LogMessageLogging] 6.1 <private> 

こんなの⇩
objc[1207]: Class PLBuildVersion is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/AssetsLibraryServices.framework/AssetsLibraryServices (0x120acb910) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/PhotoLibraryServices (0x1208f5210). One of the two will be used. Which one is undefined.2016-09-18 18:12:50.341636 SkyReporter[1207:83865] bundleid: com.galakuta09.SkyReporter, enable_level: 0, persist_level: 0, propagate_with_activity: 02016-09-18 18:12:50.372739 SkyReporter[1207:83865] subsystem: com.apple.siri, category: Intents, enable_level: 1, persist_level: 1, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 0, enable_private_data: 0
デバッグ用のNSLogとかが埋もれてしまって非常に困る。

メッセージが出る理由

どうやらXcode8の設定が変なために、普段は表示されない各種情報が表示されているようだ。
これを設定すればよし
XcodeのProduct / Scheme / Edit Scheme...メニュー(⌘+<)の、左側のカラムがRun、上の選択肢がArguments、で表示されるEnvironment Variablesに、
Nameを「OS_ACTIVITY_MODE」、Valueを「disable」に設定してやることでほとんど出なくなってくれる。ありがたい。

このありがたい情報はこちらのサイトから。ありがたやありがたや。

でも、今度は逆に実機確認時にNSLogとかの表示が全く出てくれなくなった。
⌘+<ですぐに表示できる設定だから、Xcode8が直ってくれるまでは切り替えて使うしかないか…。アップルのバカ!

2016年6月14日火曜日

すべてのiPhoneに対応させない

注意:結論から言うと、うまくいきませんでした。

解像度が多すぎる

アンドロイドスマホほどじゃないとは思うが、iPhoneは解像度とアスペクト比のバリエーションが多い。
執筆時現在、以下の画面サイズに対応させなければいけない。
  • RetinaのiPhone4sで640×960
  • 縦にサイズが伸びたiPhone5が640×1136
  • iPhone6では4.7インチで750×1334
  • 最新のiPhone6 Plusでは5.5インチで1242×2208

このうちiPhone4sだけは縦が短いので、うまく画面を作らないと、それ以外の画面で横がはみ出したり上下に黒い帯を表示させることになってしまう。

正直、いちいちこんないっぱい対応してられるか! と思う。
これにiPadの対応までするとなるとますますだ。(iPadはアスペクト比が一緒だからレイアウト変更などは一種類で済むが)
俺のせこいアプリなんかはiPhone4sの解像度でだけ作って、iPhone6s Plusだろうがスケーリングモードで拡大して表示してくれるとありがたい。
要はiPhoneのアプリをiPadで使う時みたいに、多少表示が粗く見えても2倍とかに拡大してくれればいいのだ。

そういう風に作れないだろうか?
さらに、それでアップルの審査をパスできるだろうか?(最低iPhone5のサイズに対応せんと怒られるのではないか?)

対応させないためには

みなさん頑張って各解像度に対応させてるのか、検索してもスケーリングモードで表示させる方法があまり出てこない。
どうやらアプリ起動時に表示されるLaunch Screen(スプラッシュスクリーンともいうのだな)をどの解像度(機種)の分をインストールするか/しないかで対応解像度を選べるらしい…ということだけ風の便りで聞いていた。


そこでLaunch ScreenにiPhone4s(3.5インチ)用のLaunchScreenだけ入れiTunesConnectにアップロードし、スクリーンショットも3.5インチ画面のみをアップロードしてみた。
しかし「審査提出」ボタンを押すと「アプリが他のサイズ用に対応してるから、それ用のスクリーンショットもアップロードしろ」と出てしまった。

チェックボックス全てOFFにしてみる

最低限のLaunch Image
XcodeのLaunchImageの画面でスクリーンショットの入ってない枠が残ってるせいかと思い、LaunchImageのインスペクタでチェックボックスを全てOFFにしてみたが、やっぱりiTunesConnectで同じエラーになる。

もしかしたらちゃんとしたやり方あるんだろうとは思うけど、近い将来サポートされなくなる3.5インチにこだわって作っててもしょうがないのであきらめることにした。
あ〜、全部の解像度対応してやりますよ! このヤロー!(TДT)(iPadを除く)

やっぱりサポートされなくなった

2016/6/14のアップルのイベントでiOS10について発表があり、iPhone4sが非対応になったようだ。
これで、若干のアスペクト比の違いはあれど全て縦長になったので(iPadを除く)、デザインはしやすくなったかな。

iPadのスケーリング表示はどうなる?

iPadってiPhoneしか対応してないアプリを使う場合、現状ではiPhone4sの解像度をスケーリング表示させてるけど、これからはどうなるんだろうな。
iPhone5の解像度をスケーリング表示するようになるの?

iPad対応版を作るんであれば、どのみち今までどおり縦長なiPhone、正方形に近いiPadの両方のデザインを作らないといけませんな。こっちはもう早くからあきらめてる。