カテゴリー
iOS Swift

任意の点がMKCoordinateRegion内にあるかを調べるには

任意の点CLLocationCoordinate2DがMapViewのMKCoordinateRegionの中にあるのかを知る方法を調べていたら、このStackOverflowの投稿に辿りつきました。

+ (BOOL)coordinate:(CLLocationCoordinate2D)coord inRegion:(MKCoordinateRegion)region
{
    CLLocationCoordinate2D center = region.center;
    MKCoordinateSpan span = region.span;

    BOOL result = YES;
    result &= cos((center.latitude - coord.latitude)*M_PI/180.0) > cos(span.latitudeDelta/2.0*M_PI/180.0);
    result &= cos((center.longitude - coord.longitude)*M_PI/180.0) > cos(span.longitudeDelta/2.0*M_PI/180.0);
    return result;
}

Swiftで書くとこのようになると思います。

func checkIfCoordinateInRegion(coordinate: CLLocationCoordinate2D, region: MKCoordinateRegion) -> Bool {
        let center = region.center
        let span = region.span
        return cos((center.latitude - coordinate.latitude)*Double.pi/180.0) > cos(span.latitudeDelta/2.0*Double.pi/180.0) && cos((center.longitude - coordinate.longitude)*Double.pi/180.0) > cos(span.longitudeDelta/2.0*Double.pi/180.0)
}

なぜcosで計算すると良いのかがパッと見分からなかったので考えてみました。

まず、地球を描きます。

The Earth
Draw imaginary latitude and longtitude.

Draw an imaginary span area
Viewing the earth from the side.
Here is cosθ of your span area.
If a point is inside the span area, its cosθ’ will always be greater than cosθ.

カテゴリー
iOS Swift

iPhoneで録画したポートレート動画の描画

iPhoneでポートレート(portrait == 縦方向に長い画像)撮影した動画は、フレームを描画すると意図した角度から90°反時計回りに回転して描画されてしまいます。その理由についてAppleのこのコードを元に勉強してみました。作成したサンプルはこちら

iPhoneで撮影した動画は全てランドスケープ(landscape == 横方向に長い画像)で保存されるようです。その動画がポートレートかランドスケープかの判定にはAVAssetTrackのpreferredTransformというプロパティを用いるということのようです。

preferredTransformはCGAffineTransformというクラスのインスタンスです。Affine Transform(アフィン変換)を理解するには行列の知識が必要なのですが、この「さつき先生」の動画レクチャーがウルトラ分かりやすいのでおすすめします。

CGAffineTransformはこのような行列です。

iPhoneで撮影したランドスケープ動画のpreferredTransformの値は以下のようになっています。これはつまり単位行列(identity matrix)です。

affine transform a: 1.0
affine transform b: 0.0
affine transform c: 0.0
affine transform d: 1.0
affine transform tx: 0.0
affine transform ty: 0.0
3×3の単位行列

単位行列はベクトルや行列にかけても、そのベクトルや行列を変化させません。これは、グラフィックスでは、元の動画を変化させない(スケール、回転、並行移動をさせない)ということです。

iPhoneで撮影したポートレート動画のpreferredTransformの値は以下のようになっていました。単位行列ではなく、aとdの値が0.0, bが1.0, cは-1.0という値になっています。またtxの値が1080(画像の縦寸法)になっています。(txが画像の縦寸法の値になっている理由がいまいちよく分かっていません、、、分かり次第記事をアップデートします)

affine transform a: 0.0
affine transform b: 1.0
affine transform c: -1.0
affine transform d: 0.0
affine transform tx: 1080.0
affine transform ty: 0.0

この行列の逆行列は以下です。bとcが入れ替わり、txとtyも入れ替わっています。

affine transform inverted a: 0.0
affine transform inverted b: -1.0
affine transform inverted c: 1.0
affine transform inverted d: 0.0
affine transform inverted tx: -0.0
affine transform inverted ty: 1080.0

このa, b, c, dの部分はつまり、θ = 270°の回転行列です。回転行列式の仕組みも、このさつき先生のハイパー分かりやすい動画レクチャーをご覧ください。さつき先生の説明にある回転行列と比べるとCGAffineTransformはcとdの位置が入れ替わっています。これはこの行列を右から掛けるか左から掛けるかの違いから来ています。

つまりこの preferredTransformの値は、逆行列を用いることで、単位行列の時はそのまま単位行列を返し、回転したい場合は回転行列を返して利用するところがミソです。

var affineTransform: CGAffineTransform {
    return self.videoTrack.preferredTransform.inverted()
}

UIImageViewには以下のようにCVPixelBuffer -> CIImage -> CGImage -> UIImage と変換させ、描画させています。

func displayFrame(_ frame: CVPixelBuffer?, withAffineTransform transform: CGAffineTransform) {
    DispatchQueue.main.async { // updating the UI needs to happen on the main thread
        if let frame = frame {            
            let ciImage = CIImage(cvPixelBuffer: frame)
                .transformed(by: transform) // applying affineTransform here, otherwise a portrait image will be displayed in landscape.
            let ciContext = CIContext(options: nil)
            guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else { return }
            let uiImage = UIImage(cgImage: cgImage)
            self.imageView.image = uiImage
        }
    }
}
カテゴリー
iOS Swift

CAShapeLayerをアニメーションさせる

CAShapeLayerのpositionをfor loop内でインクリメントし、アニメーションさせてみます。

import UIKit

class ViewController: UIViewController {
    
    var dotShapeLayer = CAShapeLayer()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        showPoint()
        movePoint()
    }

    func showPoint() {
        view.layer.addSublayer(dotShapeLayer)
        let dotRect = CGRect(x: 0, y: 0, width: 10, height: 10)
        let smallDotPath = UIBezierPath(rect: dotRect)
        dotShapeLayer.path = smallDotPath.cgPath
    }
    
    func movePoint() {
        DispatchQueue.global().async {
            for i in 0...100 {
                DispatchQueue.main.async {
                    self.dotShapeLayer.position = CGPoint(x: i * 2, y: i * 2)
                    self.view.layer.setNeedsDisplay()
                }
                sleep(1)
            }
        }
    }
}
カテゴリー
iOS Swift

CAShapeLayerに図形を描く

Core AnimationのCALayerを使って、Viewに図形を描くことが出来ます。

override func viewDidLoad() {
    super.viewDidLoad()
    let shapeLayer = CAShapeLayer() // CAShapeLayerインスタンスを作成
  view.layer.addSublayer(shapeLayer)
    print(shapeLayer.bounds) // (0.0, 0.0, 0.0, 0.0)をプリント
}

addSublayer(shapeLayer) とした時点では、ShapeLayerはサイズを持たないオブジェクトとしてview.layerのsublayerとして存在しています。サイズはないのですが、概念上黄色に着色して図にすると以下のようになります。

図を描くには、UIBezierPathクラスを使います。

let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める

shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 6
(x: 5, y: 5)から(x: 5, y: 130)を結ぶ線が引かれました。
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす

shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
二つの線がつながり、その間を.link色で塗りつぶしました。

.addQuadCurve(to: CGPoint, controlPoint: CGPoint)を使い、曲線を描き、pathを閉じます。

let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす
path.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 125, y: 5)) // 曲線を描く
path.close() // pathを閉じる

shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす
path.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 125, y: 5)) // 曲線を描く
path.close() // pathを閉じる

shapeLayer.bounds = path.bounds    // path.boundsを代入
print("shapeLayer.bounds after: \(shapeLayer.bounds)") // サイズのなかったshapeLayer.boundsにpath.boundsが設定される。shapeLayer.bounds after: (5.0, 5.0, 120.0, 125.0)
shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
shapeLayerがサイズを持ち、図形はdefault position(0.0, 0.0)へ配置されました。
let path = UIBezierPath()
path.move(to: CGPoint(x: 5, y: 5))      // 図形の起点を定める
path.addLine(to: CGPoint(x: 5, y: 130)) // 起点からどのポイントに線を引くか定める
path.addLine(to: CGPoint(x: 125, y: 130)) // さらに線を伸ばす
path.addQuadCurve(to: CGPoint(x: 5, y: 5), controlPoint: CGPoint(x: 125, y: 5)) // 曲線を描く
path.close() // pathを閉じる

shapeLayer.bounds = path.bounds    // path.boundsを代入
print("shapeLayer.bounds after: \(shapeLayer.bounds)") // サイズのなかったshapeLayer.boundsにpath.boundsが設定される。shapeLayer.bounds after: (5.0, 5.0, 120.0, 125.0)

// shapeLayerのposition.x, yをview.boundsのmidX, midYに設定
shapeLayer.position.x = view.bounds.midX 
shapeLayer.position.y = view.bounds.midY


shapeLayer.path = path.cgPath           // shapeLayerのpathにpath.cgPathを代入
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.link.cgColor // 青色に塗りつぶす
shapeLayer.lineWidth = 6
shapeLayerのポジションがview.bounds.midY, midXに設定されました。

カテゴリー
iOS Swift

UIButtonの背景色をコードで設定

UIButton.backgroundColorでボタンの背景色を設定するとボタンをプレスした時の色の反応が見えないため、UIColorからUIImageを作成し、UIButtonのバックグラウンドイメージを設定する簡単なコードを作成しました。

参考にした記事はこちら

import UIKit

class ViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton()
        let buttonColor = UIColor.link
        let uiImage = createUIImageFromUIColor(color: buttonColor) // UIColorからUIImageを作成
        button.setBackgroundImage(uiImage, for: .normal)

       // ボタンにアイコンイメージを指定する場合
       button.setImage(UIImage(systemsName: "heart.fill"))
       // イメージサイズを可変にする場合
       button.imageView?.contentMode = .scaleAspectFit
       button.contentHorizontalAlignment = .fill
       button.contentVerticalAlignment = .fill
       // アイコンの色を指定
       button.tintColor = UIColor.systemPink
       // ボーダーを指定する場合
       button.layer.borderColor = UIColor.black
       button.layer.borderWidth = 1
       // ボタンにテキストを指定する場合
        button.setTitle("Button", for: .normal)
        button.setTitleColor(.white, for: .normal)
        // コーナーを丸くする場合
        button.layer.cornerRadius = 20
        button.layer.masksToBounds = true
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.widthAnchor.constraint(equalToConstant: 100).isActive = true
        button.heightAnchor.constraint(equalToConstant: 50).isActive = true
    }
    
    private func createUIImageFromUIColor(color: UIColor) -> UIImage? {
        let size = CGSize(width: 200, height: 200)
        var colorImage: UIImage?
        
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        if let context = UIGraphicsGetCurrentContext() {
            context.setFillColor(color.cgColor)
            context.fill(CGRect(origin: .zero, size: size))
            colorImage = UIGraphicsGetImageFromCurrentImageContext()
        }
        UIGraphicsEndImageContext()
        return colorImage
    }
    
}
カテゴリー
iOS Swift

Videoファイルからフレームを抽出

Vision Frameworkを使って動画解析をするための前準備として、動画ファイルから毎フレームを抽出し、UIImageViewへ描画するにはどうしたら良いのか。Appleのサンプルコードを勉強し、ミニマム実装をしてみました。

このコードのキモは、AVAssetReaderTrackOutputクラスのcopyNextSampleBuffer()メソッドを利用し、CVPixelBufferオブジェクトを得て、UIImageへ描画することです。

func nextFrame() -> CVPixelBuffer? { 
    guard let sampleBuffer = self.videoAssetReaderOutput.copyNextSampleBuffer() else {
        return nil
    }
    return CMSampleBufferGetImageBuffer(sampleBuffer)
}

while loop内で nextFrame == true を満たす場合にフレームの抽出を続け、

while true {
    guard let frame = videoReader.nextFrame() else {
        break
    }
    // Draw results
    delegate?.displayFrame(frame, withAffineTransform: videoReader.affineTransform)
    usleep(useconds_t(videoReader.frameRateInSeconds))
}

得たframe: CVPixelBufferをdelegateメソッドに送り、ViewController内のUIImageViewに描画します。

func displayFrame(_ frame: CVPixelBuffer?, withAffineTransform transform: CGAffineTransform) {
    DispatchQueue.main.async {
        if let frame = frame {
            let ciImage = CIImage(cvPixelBuffer: frame).transformed(by: transform)
            let uiImage = UIImage(ciImage: ciImage)
            self.imageView.image = uiImage
        }
    }
}
カテゴリー
iOS Swift

UIViewの理解のために

UIViewはUIを構成する基本的なビルディング・ブロックで、ひとつ以上のsubviewを持つことが出来ます。また、UIResponderのサブクラスであり、touchやその他のeventに反応することが出来たり、gesture recognizerを設定することが出来ます。

UIViewControllerのviewプロパティはRoot Viewを保持します。viewプロパティの初期値はnilです。view == nilの時にviewを呼び出すと、UIViewControllerにより自動的にloadView()メソッドが呼ばれます。

boundsとframeの違いについて

boundsは、自身のcoordinate system(0, 0)にrelativeな位置(x, y)とサイズ(width, height)で表されるrectangleです。

frameは、自身のsuperviewにrelativeな位置(x, y)とサイズ(width, height)で表されるrectangleです。

override func viewDidAppear(_ animated: Bool) {
    print(view.bounds)      // CGRect(x: y: width: height:)がプリントされる
    print(view.bounds.size) // CGSize(width: height:)がプリントされる
    print(view.frame)       // CGRect(x: y: width: height:)がプリントされる
  print(view.frame.size)  // CGSize(width: height:)がプリントされる
}
// this method happens after all the layouts has been done. このメソッドは全てのレイアウト後に呼ばれる
override func layoutSubviews() {
    super.layoutSubviews()
    let layer = sampleImageView.layer
    layer.cornerRadius = 30
}