記録。

めも。

最近

反省

最終更新日が7/16だったので、だいぶ更新していない状態が続いてしまった。。。 7月後半から秋にかけてPJが炎上したりとだいぶ忙しかったこともあるが、約5ヶ月も空いてしまった。。。

近況報告

春から夏

どんな技術に関わってきたかという観点で、今年度の春から振り返ってみる。 春から夏終わりまではモバイルアプリ開発のPJに入っていた。

cordovaを使用したクロスプラットフォームでの開発だったが、 iOSのネイティブで開発する必要がある機能もたくさんあったため(主にカメラ)ネイティブ部分を担当して開発していた。

夏にかけてJS側(共通部分)で多くのバグが発生してしまったため、その火消しのために集中していた。 クロスプラットフォームのJS側はVue.jsで開発されていため、Vue.jsで開発(というよりはデバッグして修正、テストだったか、)

個人的にはwebフロントの開発はどのフレームワークでも結局CSSが苦手だということに行き着いてしまう。 (今後、頑張りましょう。そして早く慣れましょう)

あとは、jpgのメタデータを操作する機会があったため、その辺の仕組みなどを今度記事に書く。

夏から今

前期終わりのタイミングで、モバイルアプリ開発は無事終了し、 そのあとは、webフロントエンドの開発(Vue.js)とアプリケーションの基盤となるインフラの構築(AWS, Linux)をやっていた。

で、今に至る。

今とこれから

まずは、ブログの更新頻度をあげよう。

あとはGithubに草をもっと生やそう。

興味を持ったことへはなるべく実践しながら使えるようにしよう。

運用・保守に興味を持ったので、個人ではその辺にフォーカスして開発したりインプットしてみようと思う。

興味を持った理由や背景は今度書く。

運用・保守といっても、泥臭くサーバー内で作業をすることがしたいわけではない。 運用・保守のコストを抑えられるような技術、今までやってきたアプリ開発などの品質などを高めるためのアプローチ(設計やテストなど)に 興味・関心というよりは必要性をより感じるようになった。

ということで、最近は、Swift書きつつ、DockerやKubenetesのことについてもインプットをしている。

書くネタはたくさん溜まったと思うので、更新頻度を上げていく。

virtual boxに作成したcentOSサーバーにssh接続しようとしたら、「WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!」

centOSのサーバーを作成してssh接続しようとしたら見たことないエラーが出てきて 接続できなかったので、その時のことを対処法を含めてメモ。

どんなエラーが出たか?

以下の画像を参照

f:id:jksdaba:20180716192205p:plain

それで調査したら、、、 サーバー上のRSA鍵情報とクライアントのRSA鍵情報が異なっていたためこのようなエラーが発生してしまった。

なんでこのようなエラーが起こってしまったのか?

サーバーは作成したばかりだったので、なんで鍵情報が異なると言われたのかがよくわからなかった。

しかし、この後の対処方法でも使用するUsers/.../.ssh/known_hostsを見たらわかった。

今回作成したサーバーのIPと以前に作成したサーバー(すでに削除してしまった)のIPが同じだった。 しかし、known_hostsには同じIPでも以前作成したサーバーに接続した時の鍵情報を持っていたため 異なると言われ、エラーが出てしまった。

対処法は?

Users/.../.ssh/known_hostsに記載されている 不必要な鍵情報を消去(IPアドレスハッシュ値が記載されていると思うので、それを削除した)

参考

www.server-memo.net

viewを画面からはみ出ないように移動させたり、拡大させる

久しぶりの更新になってしまいました。。。

今日は、お題に書いてある通りです。 移動や拡大させる方法は書いてあるんですけど、実際にアプリに処理を組み込むことになったら、 はみ出ちゃいかんだろーと思いはみ出ないようにする処理を実装したんで、メモメモ。

イメージはこんな感じになります。 f:id:jksdaba:20180616171203g:plain

移動時にはみ出さないようにする

そんなに大したことはありません。 判定させるだけです。以下のように。

  //はみ出していないか判定する
        if frame.origin.x < 0 {
            frame.origin.x = 0
        }
        if frame.origin.y < 0 {
            frame.origin.y = 0
        }
        if frame.maxX > UIScreen.main.bounds.width {
            frame.origin.x = UIScreen.main.bounds.width - frame.width
        }
        if frame.maxY > UIScreen.main.bounds.height {
            frame.origin.y = UIScreen.main.bounds.height - frame.height
        }

上記の処理をtouchesMovedやtouchesEndedに組み込めば移動時にはみ出すことは無くなります。

拡大時にはみ出さないようにする

これは移動の時より若干厄介でした。

    //現在のscaleを保持する
    private var currentScale:CGFloat = 1.0
    
    //最大のscaleを保持する
    private var maxScale: CGFloat!

 ...

    @objc
    private func pinchAction(_ sender: UIPinchGestureRecognizer) {
        //アクション中にどんどん更新されるscaleを保持するための配列
        var scaleArray: [CGFloat] = []
        
        switch sender.state {
        case .began:
            break
        case .changed:
            // senderのscaleは、指を動かしていない状態が1.0となる
            // 現在の拡大率に、(scaleから1を引いたもの) / 10(補正率)を加算する
            currentScale = currentScale + (sender.scale - 1) / 10
            // ピンチ中の拡大率は0.3〜2.5倍、指を離した時の拡大率は0.5〜2.0倍とする
            // 拡大率が基準から外れる場合は、補正する
            if currentScale < 0.3 {
                currentScale = 0.3
            } else if currentScale > 2.5 {
                currentScale = 2.5
            }
            
            //拡大、縮小を適用する前に画面の境界に位置していないか確認する
            //境界に位置していた場合は、処理を終える
            guard self.frame.origin.x != 0 else {
                return
            }
            guard self.frame.origin.y != 0 else {
                return
            }
            guard self.frame.maxX != UIScreen.main.bounds.width else {
                return
            }
            guard self.frame.maxY != UIScreen.main.bounds.height else {
                return
            }
            
            //拡大、縮小
            self.transform = CGAffineTransform(scaleX: currentScale, y: currentScale)
            //適用していくscaleを配列で保持していく
            scaleArray.append(currentScale)
            
            //拡大、縮小して画面からはみ出ていないか確認する
            //はみ出ていなければ、処理を終える
            if self.frame.origin.x < 0 || self.frame.origin.y < 0 || self.frame.maxX > UIScreen.main.bounds.width || self.frame.maxY > UIScreen.main.bounds.height {
                //拡大、縮小してはみ出してしまった場合
                if maxScale == nil {
                   //はみ出る直前のscaleを取得
                   maxScale = scaleArray[scaleArray.count - 1]
                }
                //はみ出ないようにサイズを更新
                self.transform = CGAffineTransform(scaleX: maxScale, y: maxScale)
            }
            
        default:
            if currentScale < 0.5 {
                UIView.animate(withDuration: 0.2, animations: {
                    self.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
                }, completion: nil)
            }
            //アクション終了時で初期化をする
            deinitScale()
        }
    }
    
    private func deinitScale() {
        //currentScaleを初期の1.0に戻す
        //特に指が画面から離れた時に初期に戻しておかないと次、拡大・縮小する際におかしくなる
        currentScale = 1.0
        //maxScaleもいったん初期化学
        //2回目以降の拡大、縮小時中途半端なサイズが最大サイズになってしまう
        maxScale = nil
    }

まずはguard文にあるようにviewが画面の境界に位置していないか確認します。 境界に位置していたら、処理を終了させて拡大できないようにしました。

境界に位置していなければ、その後拡大させるのですが、 その際にscaleArrayと宣言していた配列にcurrentScaleを保持できるように追加しています。(この配列については後述)

拡大した時に画面からはみ出ないか再度チェックを行なっています。 拡大してはみ出てしまった時、先ほどの配列(scaleArray)からはみ出る直前のscaleを取得します。(つまり取得したscaleがそのイベントでの最大サイズになる) これを改めてviewに適用することで拡大時に画面からはみ出てしまうことを防ぐことができます。

最後にdefaultの部分に記載されているのですが、currentScaleとmaxScaleを初期化させています。 currentScaleを初期化していないと 例えば、次に拡大しようとした時、前の拡大アクションで残っていたcurrentScaleを引き継いでしまい急に画面からはみ出るように拡大してしまったりします。

maxScaleも初期化していないと 前の拡大アクションの最大scaleを引き継ぎ、拡大しようとしているのに引き継いだscaleが適用されてしまい、中途半端なサイズまでしか拡大できなくなってしまいます。 これは例えば、拡大したけれども、viewの位置的にすぐに境界にぶつかってしまった時、大きいscaleではないのにmaxScaleとして保持されます。 これが引き継がれると、その中途半端なscaleが適用されて「拡大しているのに拡大できない!」みたいな事態になってしまいます。 (これって自分が書いたコードが悪いのかも。。。)

なので、次のアクションのために初期化しました。

ということで以上です。

https://github.com/2takaanthony85/gestureSample

AVFoundationを使用して撮影した写真のExif情報を取得したり、追加してみる(その2)

前回、書き込む方の記事を書かなかったので、今から書きます。

とりあえず、コードを貼ります。

import UIKit
import AVFoundation

class ViewController: UIViewController, CameraButtonDelegate, AVCapturePhotoCaptureDelegate {

  //viewDidLoadなど省略しています
    
    func takePicture() {
        let settings = AVCapturePhotoSettings()
        self.output.capturePhoto(with: settings, delegate: self)
    }
    
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            let exifSample = ExifSample()
            //exif読み込み
            exifSample.read(imageData)
            //exif書き込み
            exifSample.write(imageData)
        }
    }
import UIKit
import ImageIO
import MobileCoreServices

class ExifSample: NSObject {

 //書き込み    
    func write(_ data: Data) {
        let ciImage = CIImage(data: data)
        if let ci = ciImage {
            //exifの生成
            let exif = NSMutableDictionary()
            exif.setObject("take a picture!!", forKey: kCGImagePropertyExifUserComment as! NSCopying)
            //メタデータの生成
            let metaData = NSMutableDictionary()
            metaData.setObject(exif, forKey: kCGImagePropertyExifDictionary as! NSCopying)
            //ciimageをcgimageに変換
            let context = CIContext(options: nil)
            let cgImage = context.createCGImage(ci, from: ci.extent)
            //書き込まれる画像データと書き込む情報の保存先の生成
            let imageData = NSMutableData()
            let dest: CGImageDestination? =  CGImageDestinationCreateWithData(imageData, kUTTypeJPEG, 1, nil)
            
            if let wrappedDest = dest, let wrappedCgImage = cgImage {
                //書き込み
                //新しく書き込まれた画像データはimageDataに保存される
                CGImageDestinationAddImage(wrappedDest, wrappedCgImage, metaData)
                CGImageDestinationFinalize(wrappedDest)
                
                //書き込み確認
                confirm(imageData)
            } else {
                print("wrappedDest is nil")
            }
        } else {
            print("ci is nil")
        }
    }
    
 //書き込み確認
    func confirm(_ newData: NSMutableData) {
        
        let newString = newData.base64EncodedString(options: .lineLength64Characters)
        let decode = Data(base64Encoded: newString, options: .ignoreUnknownCharacters)
        
        if let wrappedData = decode {
            let newCiImage = CIImage(data: wrappedData)
            if let wrappedNewCi = newCiImage {
                print(wrappedNewCi.properties)
            } else {
                print("wrappedCi is nil")
            }
        }
    }
}
 

ということで、ViewController側のphotoOutputで撮影した画像データ(imageData)を渡してExifSampleのwriteで書き込みを行なっています。

書き込み時にはImageI/Oフレームワーク で提供されているCGImageDestinationなどを使いました。

書き込みたい情報はNSMutableDictionary型の変数にキーと値をセットしています。 最初のexifは「"{Exif}"」キーの下の階層にあるので、最終的には「"{Exif}"」キーの値としてセットして書き込まなければなりません。 ということで上の階層の辞書としてmetaData(こいつも NSMutableDictionary型)でキーにkCGImagePropertyExifDictionaryを指定し、先ほどのexifをセットしています。

撮影した画像データはCGImageに変換して扱っています。

CGImageDestinationCreateWithDataでCGDestinationオブジェクトを作っています。 NSMutableData型のimageDataが書き込む画像データを入れるところです。(書き込み場所の指定)
kUTTypeJPEGで書き込み後のデータタイプを指定、
1は画像ファイル中の画像数(今回は撮影した画像なので1枚)、
optionsは何かあればということですが、特にないのでnilです。

CGImageDestinationAddImageでimageDataに画像データとexif情報が追加されます。

CGImageDestinationFinalize(wrappedDest)で実際に書き込みが行われて、がっちゃんこって感じです。

元々のCGImageが更新されるようなイメージを描いていたのですが、実際には新しいものができる感じっぽいです。(だからimageDataがあると思っている。)

ちなみにCGDestinationっていうのはCGImageDestinationCreateWithDataであったようにどこに書き込むのかの場所を決めたり、 CGImageDestinationAddImage、CGImageDestinationFinalizeのように実際にデータを書き込んだりすることができるオブジェクトです。

(この辺は、ドキュメントやブログを参考に解釈したので、間違っていたらコメントをください。。。)

今回はデータオブジェクトに書き込みをしましたが、URLとかでも行けるっぽいです。



今回のブログの参考資料

アプリ開発ブログ(仮): 画像をメタデータ付きで読み書き

CGImageDestination | Apple Developer Documentation

CGImageDestinationCreateWithData(_:_:_:_:) - Image I/O | Apple Developer Documentation


最後にgithubにコードがあるのでご参考までに。

https://github.com/2takaanthony85/ReadAndWriteExif

AVFoundationを使用して撮影した写真のExif情報を取得したり、追加してみる

昨日に引き続き、連投です。

今回はAVFoundationを使用したカメラで撮影した写真のExif情報を取得したり、追加したりする方法だったりを
書きたいと思います。

本題に入る前にExifについて


ExifはExchangeable image file formatと呼ばれる写真にメタデータを埋め込むための画像ファイル形式です。
撮影した画像に、例えば、撮影日時、何のデバイスを用いて撮影されたのか、解像度などなど、、、etcが撮影時に付加されてたり、自身で情報を追加することもできます。
データ管理や写真の保存にこの情報が使用されています。」


もう1つ本題に入る前になんで、AVFoundation?

→あまり記事がなかったからです。ほとんどの記事がUIImagePickerControllerを使用した解説、xcodeプロジェクトに既存の.jpg画像をぶち込んでExif情報を扱っていたりで実際に自身が求めていたAVFoundationを使用した際の方法がなかったからです。(特に情報を書き込む方法の方であんまりなかったと思っています。)
→もう1つは古い ALAssetsLibraryフレームワークを使用した記事が多かったからです。非推奨のフレームワークを使用するわけにはいかないので、今できる方法を知りたかったからです。


ってことで、本題。

まずはExif情報を取得してみようーー

Exif情報を取得するには撮影した画像データをもとにCIImageを生成します。
CIImageのpropertiesでその画像データのExif情報をすべて取得できます。

実際に取得したデータは以下の感じです。

f:id:jksdaba:20180504071358p:plain

f:id:jksdaba:20180504071410p:plain

こんな感じでコンソールに出力されます。
JSONみたいな感じですねー

取得するときのコードは以下の感じです。(AVCapureSessionなどのカメラ起動時のコードは省略しています。)

extension CameraViewController: CameraButtonDelegate, AVCapturePhotoCaptureDelegate {
    
    func takePicture() {
        let settings = AVCapturePhotoSettings()
        output.capturePhoto(with: settings, delegate: self)
    }
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            
            let ciImage = CIImage(data: imageData)
            print(ciImage?.properties)


 //以下省略


CIImageのpropertiesプロパティが画像に含まれているメタデータを持っていますので、それをprintで出力してみました。


特定のExif情報を取得するときは以下の感じで取得できます。

extension CameraViewController: CameraButtonDelegate, AVCapturePhotoCaptureDelegate {
    
    override func viewDidAppear(_ animated: Bool) {
        captureButton.delegate = self
    }
    
    func takePicture() {
        print("delegate")
        let settings = AVCapturePhotoSettings()
        output.capturePhoto(with: settings, delegate: self)
    }
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            
            let ciImage = CIImage(data: imageData)
            //print(ciImage?.properties)
            
            if let wrappedCiImage = ciImage {
                //全取得
                print(wrappedCiImage.properties)
                print("-----------------------")
                
                let metaData = wrappedCiImage.properties
                
                let exifData = metaData[kCGImagePropertyExifDictionary as String] as? [String: Any]
                
                if let wrappedExifData = exifData {
                    //トップのキー("{Exif}")にある情報をすべて取得
                    print(wrappedExifData)
                    print("-----------------------")
                    
                    if let pixelX = wrappedExifData[kCGImagePropertyExifPixelXDimension as String] as? Int {
                        
                        //Exifキーの下の階層にいるPixelXDimensionを取得
                        print(pixelX)
                        print("-----------------------")
                    }
                }
            }
            //以下省略

最初に載せたciImage.propertiesが[String: Any]?型だったため、アンラップしてます。その他の特定の辞書についてもアンラップしてから出力させてみました。

上記のコードでのコンソールはこんな感じです。
※print(wrappedExifData)とprint(pixelX)のみのコンソール結果を載せています

f:id:jksdaba:20180504074457p:plain


kCGImagePropertyExifDictionaryはExifを使用するためにある辞書です。
これは実際に出力されたキーの名前と1対1の関係になっています。

出力された時のキー名は以下のように対応しています、、、
kCGImagePropertyExifDictionary - "{Exif}"
kCGImagePropertyExifPixelXDimension - "PixelXDimension"

kCGImagePropertyExifDictionaryなどはImageI/Oフレームワークでグローバルに提供されています。
EXIF Dictionary Keys | Apple Developer Documentation



ちょっと長くなったし、疲れたので、、、
次回Exifに値を追加してみたいと思います。

npmモジュールを公開しました(まだまだ課題はありますが、、、)

こんにちは。

今日は個人的に作成したnpmモジュールを一応公開したので、それを書こうと思います。
一応と付けているのは後半で書きますが、課題(やるべきこと)が残っているからです。


まず、作成したnpmモジュールはcordovaのプラグインです。

https://github.com/2takaanthony85/cordova-plugin-swift-camera

cordova-plugin-swift-camera - npm


npmモジュールとは言っても全然javascriptは書いてません。笑

cordovaのカメラプラグインをswiftで作ったのは作成自体はほとんどswiftのネイティブ部分につぎ込みました。


これを作成した理由、経緯としては、

cordovaを仕事でいじることになり、、、

・カメラ機能が必要だった
・そのカメラ機能をswiftで実装しなければならなかった

ということから当初はプラグインすでに転がっていないかなーと思って
githubやnpmをあさっていましたが、、、、


ありませんでした!!!。。。。


ということで自作することになり、、、
npmとかpackage.jsonとか何?というところから始まりました。

てんやわんやしながらもなんとかcordovaのプロジェクトでカメラを使用できるところまではいきました。
そしたら、自身の中でこんな思いが生まれました。

「もしまたカメラ使うことになったら、、、流用できるようにしといた方がいいじゃん。」
「いっそのこと公開しちゃえばいいんじゃね??」

と思い、まずは基本機能だけでもと思い、npmモジュール公開に向け始動しました。



とは言っても最低限での公開だったので、意外とすぐにできてしまいました。

3分でできるnpmモジュール - Qiita

3時間でできるnpmモジュール - Qiita

初めてのnpm パッケージ公開 - Qiita

上記の記事を参考にさせていただきました。ありがとうございます。


ただ、今回「一応」とか「課題」があると書かせていただきましたが、
まだ、使ってもらうには早いのではと思っています。
(ただ、公開までの流れを体験してみたかったので、、、公開しました。。。)

理由としては

・機能が本当に最低限(ズーム、フラッシュ、バックとフロントの切り替え、クオリティの切り替えがまだ実装されていない)

・テストを本格的にやっていないまま公開した
(ちょこっと動作確認しただけなので、テストしてCIにのせるということをしていません、、、)

テスト〜継続的CIについてはまだ、ペーぺーの身、全然知らなかったためこれから勉強して次のバージョンアップの際に
クリアできればと思ってます!


とりあえず、unit testとbitriseやtravisCIのテスト関連の勉強の必要性を実感した
はじめてのnpmモジュール公開でした。

cordovaのプロジェクトを作成する(最下層からの環境構築)

仕事でcordovaを使用することになりました。
(本当は全部ネイティブがいい!!)

それで、cordovaを使用するにあたって環境構築をしたんで、
その時のまとめとして手順をこの記事に書こうと思います。


まず、cordovaを使用するには(プロジェクトを作成する、cordovaコマンドを使用できるようにする)
npmが入っていないとできません。

npmはと言うと、、、当然node.jsがインストールされているはずです。

そしてMacであればnode.jsのバージョン管理を簡単にできるnodebrewが入っていることが個人的には望ましいです。

じゃあ、nodebrewはどうやって入れるの?と言われたらインストールするためにhomebrewが入っていることが前提です。

で、、、homebrewを入れるにはそもそもXcodeコマンドラインデベロッパ・ツールが入っていないといけません。


「。。。」

「けっこうめんどう。。。」


でも!!

今の話を逆から辿れば、購入しばかりのMacでもすぐにcordovaを使用できるようになります。

つまり、、、

Xcodeコマンドラインデベロッパ・ツールを入れる

nodebrew入れる

node.js入れる(node.js入れれば、npmも入る)

cordova入れる

これで、安心して使えるってことです。

これはnode.jsを入れるところまでで言えば、
cordovaに限らず、Reactなり、Vue.jsなり、、、jsを書くときに共通するものだと思うので、
「今後jsを書こうと思ってるんだ!」という人もやっといて損はないはず。


ということで詳しく手順を書いていきたいと思います。


1. App StoreXcodeをダウンロードしてインストールしましょう
(5GB以上あるからインストール完了まで、1,2時間くらいかかると思います)

2. ターミナルを開いて、コマンドラインデベロッパ・ツールをインストールしましょう

xcode-select --install

3. homebrewをインストールしましょう
とりあえず、公式サイトのスクリプト貼っつけて実行だ!

・公式サイト
brew.sh

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


4. nodebrewをインストールしましょう

brew install nodebrew

インストールが完了したら環境変数も設定しましょう

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
source ~/.bash_profile

5. node.jsをインストールしましょう

nodebrew install-binary stable

stableは安定版のnode.jsのバージョンです。
最新版をインストールしたい場合は

nodebrew install-binary latest

その他にも

nodebrew install-binary <指定したバージョンの番号>

でもインストールできます。
バージョンについては

nodebrew ls-remote

でズラッとバージョンの一覧が表示されるので、そこから任意で選んだバージョンをインストールしてください。

6. node.jsを使用することを宣言しましょう

nodebrew use <さっきインストールしバージョン>

一応、宣言できたのか確認しましょう

nodebrew ls

current :   <さっきコマンドに入力したバージョン>

のように表示されればOK

7. node.jsが使えるかnpmが使えるか確認しましょう

node -v
npm -v

でどっちもバージョンが返って来ればOK


ということで一旦ここで終わります。
次回にcordovaのインストールだったり、プロジェクト作成だったりを書きます!