カテゴリー
iOS Swift

View LifecycleのどのタイミングでboundsとsafeAreaInsetsの情報を得るか

override func viewDidLoad() {
  super.viewDidLoad()
  print("printing view.bounds in viewDidLoad: \(view.bounds)")
  print("printing view.safeAreaInsets in viewDidLoad: \(view.safeAreaInsets)")
}

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  print("printing view.bounds in viewWillAppear: \(view.bounds)")
  print("printing view.safeAreaInsets in viewWillAppear: \(view.safeAreaInsets)")
}

override func viewWillLayoutSubviews() {
  super.viewWillLayoutSubviews()
  print("printing view.bounds in viewWillLayoutSubviews: \(view.bounds)")
  print("printing view.safeAreaInsets in viewWillLayoutSubviews: \(view.safeAreaInsets)")
}
	
override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  print("printing view.bounds in viewWillLayoutSubviews: \(view.bounds)")
  print("printing view.safeAreaInsets in viewWillLayoutSubviews: \(view.safeAreaInsets)")
}

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  print("printing view.bounds in viewDidAppear: \(view.bounds)")
  print("printing view.safeAreaInsets in viewDidAppear: \(view.safeAreaInsets)")
}

結果

printing view.bounds in viewDidLoad: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewDidLoad: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

printing view.bounds in viewWillAppear: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewWillAppear: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

printing view.bounds in viewWillLayoutSubviews: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewWillLayoutSubviews: UIEdgeInsets(top: 48.0, left: 0.0, bottom: 34.0, right: 0.0)

printing view.bounds in viewWillLayoutSubviews: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewWillLayoutSubviews: UIEdgeInsets(top: 48.0, left: 0.0, bottom: 34.0, right: 0.0)

printing view.bounds in viewDidAppear: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewDidAppear: UIEdgeInsets(top: 48.0, left: 0.0, bottom: 34.0, right: 0.0)

カテゴリー
iOS Swift

iOSで使用可能なFontを表示する

func getFontInfo() {
  let fontFamily = UIFont.familyNames
  for i in 0..<fontFamily.count {
    let family = fontFamily[i]
    let fontName = UIFont.fontNames(forFamilyName: family)
    print("\(family) : \(fontName)")
  }
}
カテゴリー
iOS Swift

CMTimeについて

CMTime is a structure that represents time.

timescaleは1秒を何分割するか
valueはそのtimescale内でどの数値分進むか
例) value: 600, timescale: 600 の場合、600/600 で1秒を表す

カテゴリー
iOS Swift

UIView内でSwiftUIのViewを使用する

struct PlayController: View {
  var buttonSize:CGFloat = 32
  var body: some View {
    HStack(spacing: 30) {
      Button {
	
      } label: {
        Image(systemName: "stop.fill")
	  .resizable()
	  .frame(width: buttonSize, height: buttonSize)
      }
      Button {
				
      } label: {
        Image(systemName: "play.fill")
	  .resizable()
	  .frame(width: buttonSize, height: buttonSize)
      }
      Button {
			
      } label: {
        Image(systemName: "backward.end.fill")
	  .resizable()
	  .frame(width: buttonSize, height: buttonSize)
      }
      Button {
				
      } label: {
	Image(systemName: "forward.end.fill").resizable()
	  .resizable()
	  .frame(width: buttonSize, height: buttonSize)
      }
    }
  }
}
import UIKit
import SwiftUI

class ViewController: UIViewController {
	
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    view.backgroundColor = .red
    constrainControllers()
  }
	
  private func constrainControllers() {
    let controlSwiftUIView = PlayController()
    let controller = UIHostingController(rootView: controlSwiftUIView)
	    
    controller.view.translatesAutoresizingMaskIntoConstraints = false
      self.addChild(controller)
      self.view.addSubview(controller.view)
      controller.didMove(toParent: self)
      controller.view.heightAnchor.constraint(equalToConstant: 100).isActive = true
      controller.view.leadingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.leadingAnchor).isActive = true
      controller.view.trailingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.trailingAnchor).isActive = true
      controller.view.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor).isActive = true
  }
}
カテゴリー
iOS Swift

Storyboardを使わずにViewをAutoLayoutする

let constraints = [
    view.centerXAnchor.constraint(equalTo: superview.centerXAnchor),
    view.centerYAnchor.constraint(equalTo: superview.centerYAnchor),
    view.widthAnchor.constraint(equalToConstant: 100),
    view.heightAnchor.constraint(equalTo: view.widthAnchor)
]
NSLayoutConstraint.activate(constraints)
extension UIView {

    /* Constraint creation conveniences. See NSLayoutAnchor.h for details.
     */
    open var leadingAnchor: NSLayoutXAxisAnchor { get }

    open var trailingAnchor: NSLayoutXAxisAnchor { get }

    open var leftAnchor: NSLayoutXAxisAnchor { get }

    open var rightAnchor: NSLayoutXAxisAnchor { get }

    open var topAnchor: NSLayoutYAxisAnchor { get }

    open var bottomAnchor: NSLayoutYAxisAnchor { get }

    open var widthAnchor: NSLayoutDimension { get }

    open var heightAnchor: NSLayoutDimension { get }

    open var centerXAnchor: NSLayoutXAxisAnchor { get }

    open var centerYAnchor: NSLayoutYAxisAnchor { get }

    open var firstBaselineAnchor: NSLayoutYAxisAnchor { get }

    open var lastBaselineAnchor: NSLayoutYAxisAnchor { get }
}
カテゴリー
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
    }
    
}