- 投稿日:2020-04-02T22:22:11+09:00
AVFoundationでカメラモジュールを作る
概要
iOSのAVFoundationを用いて汎用カメラモジュールを作ります。
Vision.frameworkと組み合わせて画像認識したり,リアルタイムビデオエフェクトアプリを作る際にも利用できるように,汎用のモジュールとして作りましょう。いろいろと使いまわせて便利です。まずはシングルトンのカメラコントローラクラスを作り,そこにAVCaptureSessionのインスタンスを持たせます。これがカメラのセッションを管理するクラスですね。
このAVCaptureSessionに,インプットとアフトプットを接続し,さらにカメラプレビューとなるAVCaptureVideoPreviewLayerを接続すれば準備完了。
AVCaptureSessionをrunしてやればカメラが起動し,AVCaptureVideoPreviewLayerにカメラの映像が表示されます。CameraControllerの実装
汎用カメラオブジェクトとして,私はいつもCameraControllerというクラスを作っています。
CameraController.swiftclass CameraController: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVCapturePhotoCaptureDelegate { // MARK: - lifecycle static let shared = CameraController() private override init() { } }Swiftでのシングルトンの実装方法もいくつかあるようですが,シンプルに。このクラスはSampleBufferDelegateとPhotoCaptureDelegateを採用していますが,必要な機能によっては他にも採用するプロトコルはあり得ます。
また,カメラの起動に必要なものは,プロパティとして宣言しておきます。
CameraController.swift// MARK: - properties let captureSession:AVCaptureSession = AVCaptureSession() let videoOutput:AVCaptureVideoDataOutput = AVCaptureVideoDataOutput() let photoOutput:AVCapturePhotoOutput = AVCapturePhotoOutput() let previewLayer = AVCaptureVideoPreviewLayer() var preview:UIView!最後のpreviewだけ,宣言時にはnilです。
このクラス,例えば他のviewControllerなどから簡単に呼べるように,カメラをスタートするメソッドとストップするメソッドを持たせます。カメラ起動中の映像を表示するために,スタートメソッドにはプレビューを表示するUIViewをパラメータに持たせます。スタートメソッドで受け取ったviewを,プロパティのpreviewにセットするわけですね。次に,カメラをスタートするメソッド,ストップするメソッドを追加します。スタートメソッド内でcapture sessionやinput, outputを設定します。
CameraController.swiftfunc startSession(preview:UIView){ //カメラプレビュー self.preview = preview self.previewLayer.setSessionWithNoConnection(self.captureSession) self.previewLayer.frame = preview.bounds self.previewLayer.videoGravity = .resizeAspectFill preview.layer.addSublayer(self.previewLayer) self.captureSession.beginConfiguration() // input guard let videoDevice = AVCaptureDevice.default(for: .video) else { return } do { let deviceInput = try AVCaptureDeviceInput(device: videoDevice) if self.captureSession.canAddInput(deviceInput){ self.captureSession.addInput(deviceInput) } else{ print("input error") } } catch { } // output if self.captureSession.canAddOutput(self.photoOutput){ self.captureSession.addOutput(self.photoOutput) self.photoOutput.isHighResolutionCaptureEnabled = true self.photoOutput.isLivePhotoCaptureEnabled = self.photoOutput.isLivePhotoCaptureSupported } else{ print("photo output error!") } if self.captureSession.canAddOutput(self.videoOutput){ self.videoOutput.setSampleBufferDelegate(self, queue: self.sampleBufferQueue) self.captureSession.addOutput(self.videoOutput) } else{ print("video output error!") } self.captureSession.commitConfiguration() self.captureSession.startRunning() }本来はマルチスレッドを活用して,UIに関わる部分以外はバックグラウンドのqueueで行いましょう。エラーハンドリングも適切に。このコードでは割愛してます。
続いてカメラをストップするメソッドを書きたいところですが,長くなるのでこれも割愛。基本的には,sessionをstopしてinputとoutputを外しておけばOKです。
最後に,必要なデリゲートプロトコルを実装。ここでは使用頻度が高いと思われる2つだけ書いておきます。メソッドの中身は,必要に応じて頑張って書きましょう。
CameraController.swift// MARK: - capture photo delegate // 写真撮影後に呼ばれる func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?){ } // MARK: - sample buffer delegate // 1フレームごとに呼ばれる func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { }これがCameraControllerの実装は終わり(実際にはもっと書かなきゃいけませんよ!)。viewControllerからカメラを起動する場合には,例えば以下のようにします。
ViewController.swiftoverride func viewDidAppear(_ animated:Bool){ super.viewDidAppear(animated) //ここ CameraController.shared.startSession(self.view) }以上,汎用カメラクラスの基本設計でした。
カメラなど,特定のデバイスを扱うコードはできる限りシングルトンの専用クラスに書いておいた方が良いと思います。しばしばViewControllerにすべて書いた実装を見ますが,それはあまり良くないです。理由は以下。・デバイスは1つしかないので,カメラを制御するコードが複数のインスタンスから同時にコールされないようにするため
・コードの再利用が容易(←重要)
・専用のオブジェクトにまとめておけば,どのviewControllerからでも呼びやすいというところです。
上記コードですが,くれぐれも,これだけではまだ十分ではないのでいろいろと頑張りましょう。改善提案,間違いの指摘は歓迎です。
- 投稿日:2020-04-02T16:37:10+09:00
【swift】swift+node.jsでWebSocket
WebSocketとは...
- 双方向通信を低コストで行うための仕組みのこと
- 平たく言えばチャットとか、常に通信が開いたままの状態でサーバとやりとりするような通信方式の事
ws://
とwss://
という通信スキーマを使っている。httpとhttpsのようなもの- wsの挙動としては、まずhttpでハンドシェイクを行い、その後にwsにプロトコルを変更して通信する流れ
- もっと詳しくは以下の記事を参照
- https://qiita.com/chihiro/items/9d280704c6eff8603389
利用するライブラリ
- StarscreamやSwiftWebSocket、Socket.io-client-swift 等色々ある
- サーバ側がnode.jsの場合、基本的に
SocketIO
を使う模様- そのため、node.jsとのやりとりが簡単にできるSocket.io-client-swiftを使う
- 参考)https://github.com/socketio/socket.io-client-swift
実装
1.node.jsでサーバ側のプログラムを作成
- 今回サーバーがnode.jsなのでnodeのインストールが必要ですが、省きます
- 適当なフォルダにsocket.ioをインストール
npm install socket.io
- 1秒ごとに、現在のサーバー時間をクライアントに送るプログラム
- "from_client"のイベントで送信された情報をコンソールに出力する
websocket_server.jsvar http = require("http"); var server = http.createServer(function(req,res) { res.write("Hello World!!"); res.end(); }); // socket.ioの準備 var io = require('socket.io')(server); // クライアント接続時の処理 io.on('connection', function(socket) { console.log("client connected!!") // クライアント切断時の処理 socket.on('disconnect', function() { console.log("client disconnected!!") }); // クライアントからの受信を受ける (socket.on) socket.on("from_client", function(obj){ console.log(obj) }); }); // とりあえず一定間隔でサーバ時刻を"全"クライアントに送る (io.emit) var send_servertime = function() { var now = new Date(); io.emit("from_server", now.toLocaleString()); console.log(now.toLocaleString()); setTimeout(send_servertime, 1000) }; send_servertime(); server.listen(8080);
- 以下のコマンドを実行する
node websocket_server.js
- 成功すれば毎秒ログが表示されます
2. アプリ側を実装
- Podfileに以下を記載し、
pod install
でSocketIOをinstallするpod 'Socket.IO-Client-Swift', '~> 15.2.0'
- 以下コーディング内容
TestViewController.swiftimport UIKit import SocketIO class TestViewController: UIViewController,UITableViewDataSource, UITableViewDelegate{ let manager = SocketManager(socketURL: URL(string:"http://localhost:8080/")!, config: [.log(true), .compress]) var socket : SocketIOClient! var dataList :NSMutableArray! = [] @IBOutlet weak var testTableView: UITableView! @IBAction func tapButtonAction(_ sender: Any) { socket.emit("from_client", "button pushed!!") } @IBAction func reconnectButtonAction(_ sender: Any) { socket.connect() } @IBAction func desconnectButtonAction(_ sender: Any) { socket.disconnect() } override func viewDidLoad() { super.viewDidLoad() testTableView.delegate = self testTableView.dataSource = self socket = manager.defaultSocket socket.on(clientEvent: .connect){ data, ack in print("socket connected!") } socket.on(clientEvent: .disconnect){data, ack in print("socket disconnected!") } socket.on("from_server"){data, ack in if let message = data as? [String]{ print(message[0]) self.dataList.insert(message[0],at: 0) self.testTableView.reloadData() } } socket.connect() } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataList.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "Cell") cell.textLabel?.text = dataList[indexPath.row] as? String; return cell } }
- localhostに繋ぎに行くため、ATSを許可しておく事
- アプリを開いた段階でlocalhostとの通信が実行される
TapButton
を押すと、from_client
にデータが乗ってサーバー側へ送られる。websocket_server.js
の以下の部分がクライアントからの情報を受け取る// クライアントからの受信を受ける (socket.on) socket.on("from_client", function(obj){ console.log(obj) });
disconnectButton
を押すとsocketをdisconnectするreconnectButton
を押すと再度connectionを張る成功すると、こんな感じ
- ちゃんとアプリでの動作がサーバに反映されている
- disconnect、reconnectの動作が正常に動いているのがわかる
所感
- StarscreamやSwiftWebSocketは、websocketが繋がらない場合にpollingなどをする場合自前で書かなければならないらしい
- 今回はSocket.io-client-swiftを利用したが、これを使う場合はサーバも必ずSocket.ioを使う必要がある
- しかし、WebSocketが繋がらなかった時に代替手段としてLong Pollingやpollingを行ってくれるので、若干楽?
- 応用すれば様々なサービスで使えそう
- 投稿日:2020-04-02T14:45:08+09:00
UIRefreshControl
MVVMでUIRefreshControlを使ってみる。
import系
podfileimport RxSwiftまず、View側にrefreshControlとViewModelのインスタンスを用意
ViewControllervar viewModel: HomeViewModel = HomeViewModel() let refreshControl = UIRefreshControl() override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewModel.tryRefresh(refreshControl: refreshControl,tableView:tableView) }ViewModelに動作系を書く。
ViewModelimport RxSwift 〜略〜 private var disposeBag = DisposeBag() func tryRefresh(refreshControl: UIRefreshControl,tableView: UITableView){ refreshControl.rx.controlEvent(.valueChanged) .subscribe(onNext: {[weak self]_ in //動かしたいfunctionを呼ぶ。(ネストが嫌ならカスタムしてください。) { refreshControl.endRefreshing() tableView.reloadData() } }).disposed(by: disposeBag) }これで期待通り動くと思います。
refreshControlはtableで使うよなあ、と思ってこんな感じになったのですが、ご指摘あればコメントください。
MVVMも最近になって勉強を始めた状況なので、使い方変かも。
- 投稿日:2020-04-02T14:45:08+09:00
UIRefreshControl使ってみた
RXとMVVMでUIRefreshControlを使ってみる。
import系
podfileimport RxSwiftまず、View側にrefreshControlとViewModelのインスタンスを用意
ViewControllervar viewModel: HomeViewModel = HomeViewModel() let refreshControl = UIRefreshControl() override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) viewModel.tryRefresh(refreshControl: refreshControl,tableView:tableView) }ViewModelに動作系を書く。
ViewModelimport RxSwift //〜略〜 private var disposeBag = DisposeBag() func tryRefresh(refreshControl: UIRefreshControl,tableView: UITableView){ refreshControl.rx.controlEvent(.valueChanged) .subscribe(onNext: {[weak self]_ in //動かしたいfunctionを呼ぶ。(ネストが嫌ならカスタムしてください。) { refreshControl.endRefreshing() tableView.reloadData() } }).disposed(by: disposeBag) }これで期待通り動くと思います。
refreshControlはtableで使うよなあ、と思ってこんな感じになったのですが、ご指摘あればコメントください。
MVVMも最近になって勉強を始めた状況なので、使い方変かも。refleshControlカスタムしたいかたは、ViewControllerに以下のように書いてみてくださいね!
ViewController///refreshControl ///- Returns:refreshControlerCSS func CSSrefleshController(){ // refreshControl.tintColor = .white ←指定したい色。extensionで色つくってるので、以下のようになっています。 // refreshControl.attributedTitle = NSAttributedString(string: "入れたいコメント")←これも拡張してるので、以下のようになっている。 refreshControl.tintColor = Color.main refreshControl.attributedTitle = NSAttributedString(string: "Updating".localizedString() ?? "update") tableView.addSubview(refreshControl) }上記をdidroadで呼んであげれば完成ー!
拡張の中身なしに書いてしまいましたが、気になる方はコメント頂ければお見せしますー!
- 投稿日:2020-04-02T12:48:53+09:00
CAMetalLayerを実機でもシミュレータでも良い感じにビルドする
時々、とても奇妙なクラスに出会うことがある。
CAMetalLayerはまさにそれで、実機ではiOS8+、シミュレータではiOS13+が必要になる。
これはシミュレータでMetalが使えるようになったのがiOS13~だったからという歴史的経緯によるものだ。
ちなみに内部的には実機とシミュレータでSDKが別れているため、それぞれのSDKごとにavailableが定義されて実現している。実機SDKAPI_AVAILABLE(macos(10.11), ios(8.0), watchos(2.0), tvos(9.0)) @interface CAMetalLayer : CALayerシミュレータSDK@available(iOS 13.0, *) open class CAMetalLayer : CALayer {さて、問題はアプリで
CAMetalLayer
を利用するときで、iOS12未満もサポートしているアプリだと次のような挙動になる。
実機 シミュレータ iOS12 ○ コンパイルエラー iOS13 ○ ○ この挙動をどう見るか難しいところだが、シミュレータ相手であればさっとコンパイルだけ通したいこともあると思う。
そんなときはpublic protocol CAMetalLayerInterface: class { var pixelFormat: MTLPixelFormat { get set } var framebufferOnly: Bool { get set } var presentsWithTransaction: Bool { get set } func nextDrawable() -> CAMetalDrawable? } #if targetEnvironment(simulator) @available(iOS 13, *) extension CAMetalLayer: CAMetalLayerInterface {} #else @available(iOS 12, *) extension CAMetalLayer: CAMetalLayerInterface {} #endif open class AnimationView: UIView { typealias LayerClass = CAMetalLayerInterface & CALayer override open class var layerClass: Swift.AnyClass { #if targetEnvironment(simulator) if #available(iOS 13, *) { return CAMetalLayer.self } else { preconditionFailure("Not support simurator older than iOS12.") } #else return CAMetalLayer.self #endif } private var gpuLayer: LayerClass { self.layer as! LayerClass } }こんな感じで型を消しつつ、iOS12 & Simulator以外のときに型を入れてあげるのが良い気がする。
#if canImport(QuartzCore.CAMetalLayer)とか
#if targetEnvironment(simulator) && available(iOS 13, *)とか出来ればいいのになーって思ってしまった。レアケースだけど…
https://developer.apple.com/documentation/quartzcore/cametallayer
- 投稿日:2020-04-02T12:44:15+09:00
sizeForItemAt indexPathでサイズをセットしているのにUICollectionViewCellのサイズが変わってしまう
問題
久しぶりにiOSアプリを修正していて、CollectionViewで横スクロールさせていたら、スクロール開始時にセルのサイズが変わってしまう問題が起きた。
sizeForItemAt indexPath
では固定のサイズをちゃんとセットしているのにも関わらず、debug view hierarchyでチェックしても確かにセルのサイズが変わってしまっている。。解決方法
久しぶりの開発で、CollectionViewの
Estimate size
というプロパティにデフォルトでAutomatic
がセットされていることを知らなかったのですが、これをNone
にすることで解決しました。
Estimate sizeはセルのサイズを概算してくれるようですが、Noneにしておくのが無難かもしれないですね。参照
- 投稿日:2020-04-02T00:47:04+09:00
SwiftUIでiPhoneのバッテリー残量を表示してみる
はじめに
たまたまTwitterで#swiftuiを検索していたら見つけたので、ちょっと作ってみました。
今のバッテリー残量にどうやってアクセスするんだろう…
— ふってぃプログラミング初心者SwiftUI (@Futty_99) March 31, 2020
UIKitのサンプルはたくさんあるのにSwiftUIはとても少ないのが少し大変ですね?#SwiftUI#プログラミング初心者#駆け出しエンジニアと繋がりたいバッテリー残量を取るには
バッテリー残量を取るには、下記のコードで取得できます。
BatteryViewModel.swift//バッテリー監視を開始する UIDevice.current.isBatteryMonitoringEnabled = true //変化通知登録しただけなので初回は更新する必要がある remain = String(format: "%0.1f", UIDevice.current.batteryLevel * 100) status = UIDevice.current.batteryStateその後の変化通知を
NotificationCenter
で受け取れます。BatteryViewModel.swift//バッテリーレベル変化通知を受け取れるようにする NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelChanged(notification:)), name: UIDevice.batteryLevelDidChangeNotification, object: nil) //バッテリー状態編か通知を受け取れるようにする NotificationCenter.default.addObserver(self, selector: #selector(batteryStateChanged(notification:)), name: UIDevice.batteryStateDidChangeNotification, object: nil)バッテリー残量をViewに反映する
ObservableObjectで反映します。
BatteryViewModel.swiftclass BatteryViewModel: ObservableObject { ///バッテリー状態 @Published var status: UIDevice.BatteryState = .unknown ///バッテリー残量 @Published var remain = ""終わりに
サンプルコードは、githubにアップしています。
参考にどうぞ。