20200402のSwiftに関する記事は7件です。

AVFoundationでカメラモジュールを作る

概要

iOSのAVFoundationを用いて汎用カメラモジュールを作ります。
Vision.frameworkと組み合わせて画像認識したり,リアルタイムビデオエフェクトアプリを作る際にも利用できるように,汎用のモジュールとして作りましょう。いろいろと使いまわせて便利です。

まずはシングルトンのカメラコントローラクラスを作り,そこにAVCaptureSessionのインスタンスを持たせます。これがカメラのセッションを管理するクラスですね。
このAVCaptureSessionに,インプットとアフトプットを接続し,さらにカメラプレビューとなるAVCaptureVideoPreviewLayerを接続すれば準備完了。
AVCaptureSessionをrunしてやればカメラが起動し,AVCaptureVideoPreviewLayerにカメラの映像が表示されます。

CameraControllerの実装

汎用カメラオブジェクトとして,私はいつもCameraControllerというクラスを作っています。

CameraController.swift
class 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.swift
func 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.swift
override func viewDidAppear(_ animated:Bool){

    super.viewDidAppear(animated)

    //ここ
    CameraController.shared.startSession(self.view)
}

以上,汎用カメラクラスの基本設計でした。
カメラなど,特定のデバイスを扱うコードはできる限りシングルトンの専用クラスに書いておいた方が良いと思います。しばしばViewControllerにすべて書いた実装を見ますが,それはあまり良くないです。理由は以下。

・デバイスは1つしかないので,カメラを制御するコードが複数のインスタンスから同時にコールされないようにするため
・コードの再利用が容易(←重要)
・専用のオブジェクトにまとめておけば,どのviewControllerからでも呼びやすい

というところです。

上記コードですが,くれぐれも,これだけではまだ十分ではないのでいろいろと頑張りましょう。改善提案,間違いの指摘は歓迎です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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のインストールが必要ですが、省きます:sweat_smile:
  • 適当なフォルダにsocket.ioをインストール
npm install socket.io
  • 1秒ごとに、現在のサーバー時間をクライアントに送るプログラム
  • "from_client"のイベントで送信された情報をコンソールに出力する
websocket_server.js
var 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.swift
import 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を張る

成功すると、こんな感じ

スクリーンショット 2020-04-02 16.29.08.png

  • ちゃんとアプリでの動作がサーバに反映されている
  • disconnect、reconnectの動作が正常に動いているのがわかる

所感

  • StarscreamやSwiftWebSocketは、websocketが繋がらない場合にpollingなどをする場合自前で書かなければならないらしい
  • 今回はSocket.io-client-swiftを利用したが、これを使う場合はサーバも必ずSocket.ioを使う必要がある
  • しかし、WebSocketが繋がらなかった時に代替手段としてLong Pollingやpollingを行ってくれるので、若干楽?
  • 応用すれば様々なサービスで使えそう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIRefreshControl

MVVMでUIRefreshControlを使ってみる。

import系

podfile
import RxSwift

まず、View側にrefreshControlとViewModelのインスタンスを用意

ViewController
var viewModel: HomeViewModel = HomeViewModel()
let refreshControl = UIRefreshControl()

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    viewModel.tryRefresh(refreshControl: refreshControl,tableView:tableView)
  }

ViewModelに動作系を書く。

ViewModel
import 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も最近になって勉強を始めた状況なので、使い方変かも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIRefreshControl使ってみた

RXとMVVMでUIRefreshControlを使ってみる。

import系

podfile
import RxSwift

まず、View側にrefreshControlとViewModelのインスタンスを用意

ViewController
var viewModel: HomeViewModel = HomeViewModel()
let refreshControl = UIRefreshControl()

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    viewModel.tryRefresh(refreshControl: refreshControl,tableView:tableView)
  }

ViewModelに動作系を書く。

ViewModel
import 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で呼んであげれば完成ー!
拡張の中身なしに書いてしまいましたが、気になる方はコメント頂ければお見せしますー!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CAMetalLayerを実機でもシミュレータでも良い感じにビルドする

時々、とても奇妙なクラスに出会うことがある。
CAMetalLayerはまさにそれで、実機ではiOS8+、シミュレータではiOS13+が必要になる。
これはシミュレータでMetalが使えるようになったのがiOS13~だったからという歴史的経緯によるものだ。
ちなみに内部的には実機とシミュレータでSDKが別れているため、それぞれのSDKごとにavailableが定義されて実現している。

実機SDK
API_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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

sizeForItemAt indexPathでサイズをセットしているのにUICollectionViewCellのサイズが変わってしまう

問題

久しぶりにiOSアプリを修正していて、CollectionViewで横スクロールさせていたら、スクロール開始時にセルのサイズが変わってしまう問題が起きた。
sizeForItemAt indexPathでは固定のサイズをちゃんとセットしているのにも関わらず、debug view hierarchyでチェックしても確かにセルのサイズが変わってしまっている。。

解決方法

久しぶりの開発で、CollectionViewのEstimate sizeというプロパティにデフォルトでAutomaticがセットされていることを知らなかったのですが、これをNoneにすることで解決しました。
Estimate sizeはセルのサイズを概算してくれるようですが、Noneにしておくのが無難かもしれないですね。

stackoverflow

参照

https://stackoverflow.com/questions/56840665/why-on-xcode-11-uicollectionviewcell-changes-size-as-soon-as-you-scroll-i-alre

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでiPhoneのバッテリー残量を表示してみる

はじめに

たまたまTwitterで#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.swift
class BatteryViewModel: ObservableObject {
    ///バッテリー状態
    @Published var status: UIDevice.BatteryState = .unknown
    ///バッテリー残量
    @Published var remain = ""

終わりに

サンプルコードは、githubにアップしています。
参考にどうぞ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む