20201223のSwiftに関する記事は15件です。

iOSアプリ開発でぶち当たるエラー処世術〜Xcode編〜

エラー処世術

アプリケーション開発を行なっていると必ずぶち当たるエラー.検索しても解決策が得られない.開発が滞ってしまい,やる気が削がれる.といったことは誰しもが経験することだと思います.
私自身もその一人で,毎日技術力不足を痛感させられます.この記事では,Xcodeで開発する際に効率よくエラーの解明を行い,圧倒的成長を遂げるための処世術を少し紹介したいと思います.

この記事の対象者

  • iOSアプリ開発を始めたての方(Xcodeでの)
  • LLDBって何?デバッガって?という方
  • 追加したUI部品(button, viwe...)が表示されない!どこいったの?という方

処世術1 〜LLDB〜

LLDBとはXcodeのデフォルトデバッガです.皆さんお馴染みの画面の下の方にいます.デバッガとは不具合の原因を探すお手伝いをしてくれる機能やソフトを指します.基本的にはプログラムを一時停止させたりしてエラーを見つけます.

上は,お馴染みのXcodeの画面ですが.画面下の部分がデバッガです.ここでは,14行目にブレークポイントを設定して,プログラムを一時停止させています.行番号をクリックするとブレークポイントを設定できます.解除は,ブレークポイントを掴んでコード上にドラッグするとできます.
デバッガの右の画面の

(lldb)

と書かれている部分にコマンドを入力することでエラーの原因を探ることができます.

po コマンド

いちばん使うことになるであろうコマンドです.poは変数を出力してくれるものです.
例えば,以下のように,19行目にブレークポイントを設定した場合.
スクリーンショット 2020-12-23 20.38.44.png

po lemon

と入力すると,その時点での変数の値を確認することができます.
この例だとパッとしませんがいつか役に立つと思います.

処世術2 〜UI部品どこ??〜

追加したはずのbuttonがシミュレーションを起動すると消えている..どこいったの?コードミスってる?なんて経験あると思います.これは案外簡単なミスであることがほとんどです.ただ慣れないうちは,なぜ無いのか原因を解明するのに時間がかかってしまいがちです.(その時間も成長に繋がる気もしますが,無いに越したことは無いでしょう.)
この問題の対処法のうちの1つを紹介します.

Xcodeの隠れた機能 〜ビューデバッガ〜

ビューデバッガとは,現在のビュー階層を3Dで確認することができる機能です.
シミュレーターを起動しているときに,
スクリーンショット 2020-12-23 20.42.45.png
ここをクリックすることで,確認することができます.
スクリーンショット 2020-12-23 20.45.10.png
こんな感じで.
この例だと,あまりビューデバッガに恩恵を感じませんが,大規模なアプリになる程利用する機会は多くなると思います.
この機能を使えば,追加したUI部品がどこにあるのか,他のUI部品の下に隠れていないかなどを確認することができます.
「ビューデバッガのおかげで迷子になっていたUI部品を探し出し,救出することができた!」ということが結構あります.

おわりに

「アドベントカレンダーかぁ、何書こう.」
ギリギリまでテーマが決まらず投稿が遅れてしまいました.個人的に辛いこともあり,記事を書かずに逃げ出そうかという考えもよぎりました.最初は義務感で書き始めましたが,良い気分転換になり,悩んでいたことも少し気が楽になった気がします.定期的なアウトプットって大事だなあと実感しました.(Qiitaさん,パソナテックさんありがとう!)

今回,iOSアプリ開発を始めた時に知っておきたかったことを書きました.私もまだまだ初心者なので,間違いがあれば指摘してください.よろしくお願いします.自分の成長に合わせて内容もどんどん更新していくつもりです.そして,この記事が悩みを抱えていた方の助けになれば幸いです.

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

[Cocoa][Swift]AirPlay 2 について

HomePod miniは2台でステレオ再生ができるということで購入したのだが、使ってみたところ、それはAirPlay 2を利用した場合だった。MacintoshにレコードプレーヤーをLINE入力で繋いだので、レコードがステレオで再生されることを夢見たのだが、サウンドの入力を出力に繋ぐために利用しているQuickTime PlayerはAirPlay 2に対応していないので、ステレオ再生できない。そもそも、QuickTime Playerを使う必要があるということも面倒なので、レコードプレーヤーの音声を再生するアプリを制作作するため、AirPlay 2 に対応する手順を調べた。

AVAudioSessionのroute-sharing policyに .longForm を設定する

let audioSession = AVAudioSession.sharedInstance()
try audioSession.setRouteSharingPolicy(.longForm)
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(AVAudioSessionCategoryPlayback,
                             mode: AVAudioSessionModeDefault,
                             routeSharingPolicy: .longForm )

AVRouteDetectorを使って出力先があるかどうか調べて、AVRoutePickerView で出力先を選択する。

MPRemoteCommandCenter を使って再生が制御できるようにする。再生中のメディアの情報は MPNowPlayingInfoCente から得る。

AVPlayer 又は AVQueuePlayer を利用するか、 AVSampleBufferAudioRenderer と AVSampleBufferRenderSynchronizer で再生を行う。

情報は少ないが、サンプルコードを見つけた。

【関連情報】
Getting AirPlay 2 into Your App

Integrating AirPlay for Long-Form Video Apps

Cocoa Advent Calendar 2020

Cocoa.swift

Cocoa勉強会 関東

Cocoa練習帳

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

【Swift】Optional<Wrapped>型でエラー処理を行う

エラー処理には、Optional<Wrapped>型を用いた処理の他にも、
Result<Success, Failure>型を用いた処理やdo-catch文などもあります。

エラー処理は、私自身よく理解できておらず正直なところ苦手です。

しかし、コードを書く上では絶対に必要な処理なので
備忘録がてら皆さんに共有できたらなと思います。

Optional<Wrapped>型によるエラー処理

ご存知かと思いますがOptional<Wrapped>型は、
値が存在するかどうかを表現することができます。

Optional<Wrapped>型はnilを許容するので、
値が存在する時は格納されている値を、存在しない場合はnilを返します。

エラー処理で使う場合は
値が存在することを成功・存在しないことを失敗とみなせば、
エラー処理の機構として成り立ちますよね、という話です。

これでしたら結構簡単そうです!

実装方法

例えば、エラーが発生し得る関数であれば、
その戻り値をOptional<Wrapped>型にすればエラー処理を行うことができます。

下記のサンプルコードでは、
引数のidに該当するインスタンスがあったらそのインスタンスを返しています。
なかった場合はnilが返ります。

struct User {
    var id: Int
    var name: String
}

// インスタンス化
let user1 = User(id: 1, name: "Tarou")
let user2 = User(id: 2, name: "Zirou")

// インスタンスを配列に格納
var users = [user1, user2]

// エラーが起きる可能性のある関数
// 引数のidに該当するものがなかったらnilを返す
func findUserById(id: Int) -> User? {
    for user in users {
        if user.id == id {
            return user
        }
    }
    return nil
}

// 値が存在する場合
if let user = findUserById(id: 1) {
    print(user.name)
} else {
    print("Error: User not found")
}

// 値が存在しない場合
if let user = findUserById(id: 3) {
    print(user.name)
} else {
    print("Error: User not found")
}

実行結果
Tarou
Error: User not found

Optional<Wrapped>型を処理の成否に利用している例としては、
他にも失敗可能イニシャライザなどがあります。

次のサンプルコードでは、
引数のメールアドレスを二分割できない場合は
不正なデータとみなしてインスタンス化を失敗させています。

let item = mail.split(separator: "@")のitemは、
mailを二分割した時の2つの値が格納された配列です。

struct User {
    var name: String
    var mail: String

    init?(name: String, mail: String) {
        let item = mail.split(separator: "@")
        guard item.count == 2 else {
            return nil
        }
        self.name = name
        self.mail = mail
    }
}

let user1 = User(name: "Tarou", mail: "Tarou@gmail.com")
let user2 = User(name: "Zirou", mail: "Zirou.com")

var users = [user1, user2]

for user in users {
    if let user = user {
        print("Name: \(user.name)\nMail: \(user.mail)")
    } else {
        print("Error: Invalid data")
    }
}

実行結果
Name: Tarou
Mail: Tarou@gmail.com
Error: Invalid data

利用するタイミング

利用するタイミングとしては、値の有無だけで結果を十分に表せる場合です。

先ほどのように、idで一致するものを検索する場合や
メールアドレスの形式がおかしくないかのチェックなどの単純なものは
Optional<Wrapped>型でいいと思います!

ただ、id検索する際に、データベースに接続できなかったり
データが重複していたりなどとエラーの種類を多くすることは恐らく難しいですね・・・。

なので失敗の原因に応じてメッセージを表示したり、
復旧の処理を行ったりするのであればOptional<Wrapped>型では不十分です!

結論として、Optional<Wrapped>型は単純なケースのみにしましょう!

また、冒頭で紹介した、
Result<Success, Failure>型を用いた処理やdo-catch文ですが、
別で記事にする予定なのでぜひご覧ください!

【Swift】Result<Success, Failure>型でエラー処理を行う
【Swift】do-catch文でエラー処理を行う(その1)

最後までご覧いただきありがとうございました。

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

swiftでクリスマスツリーを作る

はじめに

メリークリスマス!
本日は24日なので、swiftのUIBezierPathを使ってクリスマスツリーを作ります。
Storyboardでクリスマスツリーの画像を貼れば簡単ですが、今回はすべてコードで書いていきたいと思います。
(少しずれている所もありますが許してください。。)

ひとまず完成図

Screen Shot 2020-12-20 at 4.01.20.png
こんな感じで作りました。
一気にクリスマスツリー作るのは大変そうだったので、
「星」・「木の部分の三角形」・「枝の部分の長方形」・「鉢の部分の長方形」に分けて作りました。

コード

class ViewController: UIViewController {
    var treeFrame: CGRect = .zero
    var starFrame: CGRect = .zero
    var roundRectFrame: CGRect = .zero

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black
        starViewSetup()
        treeViewSetup()
        branchViewSetup()
        potViewSetup()
    }

    // 星を作る
    func starViewSetup() {
        let starLayer = CAShapeLayer.init()
        let starViewWidth = CGFloat(50)
        starFrame = CGRect(x: (view.bounds.width / 2) - (starViewWidth / 2) , y: 100, width: starViewWidth, height: 50)
        starLayer.frame = starFrame
        starLayer.strokeColor = UIColor.yellow.cgColor
        starLayer.fillColor = UIColor.yellow.cgColor
        let starViewHeight = starFrame.size.height
        let starBezier = UIBezierPath()
        starBezier.move(to: CGPoint.init(x: 0, y: starViewHeight / 3))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth / 3, y: starViewHeight / 3))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth / 2, y: 0))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth * 0.66, y: starViewHeight / 3))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth, y: starViewHeight / 3))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth * 0.75, y: starViewHeight * 0.6))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth * 0.9 , y: starViewHeight))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth / 2, y: starViewHeight * 0.75))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth / 10 , y: starViewHeight))
        starBezier.addLine(to: CGPoint.init(x: starViewWidth / 4, y: starViewHeight * 0.6))
        starBezier.close()
        starLayer.path = starBezier.cgPath
        view.layer.insertSublayer(starLayer, at: 1)
    }

    // 木を作る
    func treeViewSetup() {
        let treeLayer = CAShapeLayer.init()
        treeFrame = CGRect(x: starFrame.minX - starFrame.size.height / 2, y: starFrame.minY - starFrame.height + 10, width: 100, height: 300)
        treeLayer.frame = treeFrame
        treeLayer.strokeColor = UIColor.green.cgColor
        treeLayer.fillColor = UIColor.green.cgColor
        let treeViewHeight = treeFrame.height
        let treeViewWidth = treeFrame.width
        let treeBezier = UIBezierPath()
        treeBezier.move(to: CGPoint(x: treeViewWidth / 2, y: treeViewHeight / 5))
        treeBezier.addLine(to: CGPoint(x: treeViewWidth * 1.2, y: treeViewHeight))
        treeBezier.addLine(to: CGPoint(x: 0, y:treeViewHeight))
        treeBezier.close()
        treeLayer.path = treeBezier.cgPath
        view.layer.insertSublayer(treeLayer, at: 0)
    }

    // 枝を作る
    func branchViewSetup() {
        let roundRectLayer = CAShapeLayer.init()
        roundRectFrame = CGRect.init(x: treeFrame.midX - 5, y: treeFrame.maxY - 10, width: 30, height: 100)
        roundRectLayer.frame = roundRectFrame
        roundRectLayer.strokeColor = UIColor.green.cgColor
        roundRectLayer.fillColor = UIColor.green.cgColor
        roundRectLayer.path = UIBezierPath.init(roundedRect: CGRect.init(x: 0, y: 0, width: roundRectFrame.size.width, height: roundRectFrame.size.height), cornerRadius: 8).cgPath
        view.layer.insertSublayer(roundRectLayer, at: 2)
    }

    // 鉢を作る
    func potViewSetup() {
        let potLayer = CAShapeLayer.init()
        let potFrame = CGRect.init(x: (roundRectFrame.midX) - (roundRectFrame.size.height / 2), y: roundRectFrame.height + roundRectFrame.minY - 10, width: roundRectFrame.size.height, height: 70)
        potLayer.frame = potFrame
        potLayer.strokeColor = UIColor.brown.cgColor
        potLayer.fillColor = UIColor.brown.cgColor
        potLayer.path = UIBezierPath.init(roundedRect: CGRect.init(x: 0, y: 0, width: potFrame.size.width, height: potFrame.size.height), cornerRadius: 8).cgPath
        view.layer.insertSublayer(potLayer, at: 3)
    }
}

結論・最後に

星の微調整が結構大変でしたが楽しかったです。
なので、
Storyboardは楽!!!!

次回以降は、ツリーを装飾できたりしたらいいな、、、
最後までお読みいただきありがとうございました。
良いクリスマス、年末年始を!

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

CoreImageフレームワークを使ってSwiftでしみ消し、くま消し機能を実装した話

バイトル履歴書アプリに画像の補正機能を追加するエンハンスを担当したのでやったことまとめておきます。

今回のエンハンス内容としましては画像補正機能として撮影した写真全体に対して独自で定義したフィルター処理を行うのと、目の下のくまの部分や頬のあたりのしみの部分を部分的に画像補正処理を行う機能をCoreImageフレームワークのみを使い実装しました。

写真全体に対してフィルター処理を行うケースはそんなに難しくないのでサンプルコードだけ載せておきます。

import CoreImage
...

// フィルター処理をかけたいSampleイメージを準備
var image = UIImage(named: "sample.jpg")

// UIImageからCIImageへの変換
let ciImage: CIImage? = CIImage(image: image)

// Sepiaフィルターの準備
var filter = CIFilter(name: "CISepiaTone")

// フィルターに画像を渡す
filter.setValue(ciImage, forKey: kCIInputImageKey)

// Filterをかけた画像を取得する
var filteredImage: CIImage? = filter.outputImage

セピア補正のフィルターを適用すると結果はこんな感じです。
実際にプロダクトには色々な画像補正フィルターを掛け合わせて独自フィルターを作り実装しました。

オリジナル セピア補正
filter-sample-original.png filter-sample-sepia.png

※写真はフリー素材を利用:https://www.pakutaso.com/model.html

画像の部分補正処理の流れ

続いて、画像の部分補正をする場合の処理の流れをまとめていきます。

0aac97d9-f553-44cf-97ea-c622e28b864d-1920x3172r.png

しみ消し機能にしろ、くま消し機能にしろ画像を部分的に補正するという点においては同じなのでどちらも上記の処理フローとなります。

マスク画像の作成

マスク画像の作成にはCoreImageのCIDetectionを使い顔のパーツの位置関係を取得することから始めます。顔検出において取得できる情報ですが、口の位置や目の位置、目が開いているかどうかや顔の輪郭を取得でき、それらの情報を元にマスク画像の作成をしていきます。

顔検出もCoreImageがサポートしていて下記のコードより取得できます。

let options = [CIDetectorAccuracy: CIDetectorAccuracyHigh]
var faces: [CIFeature] = []
if let image = CIImage(image: faceImage), let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: options) {
     faces = faceDetector.features(in: image)
}

マスク画像の作成には検出した顔のパーツの位置情報を使い、数値モデル化したしみの位置を計算してCIRadialGradientというCIFilterを使います。こちらのFilterですが、ガウシアンを位置情報を入力することで生成できるフィルターとなっており、しみ消し用のマスクに関しては計6点のガウシアン波形を重ね合わせマスク画像を作成しております。ガウシアン波形の合成にはCISourceOverCompositingと呼ばれるCIFilterを使用しています。

let maskCircles: [MaskCircle] = [
   /// ガウシアンの位置情報など...
]
for (index, circle) in maskCircles.enumerated() {
   guard let gaussian = CIFilter(name: "CIRadialGradient") else { return nil }
   gaussian.setValue(CIVector(cgPoint: circle.center), forKey: kCIInputCenterKey)
   gaussian.setValue(circle.innerRadius, forKey: "inputRadius0")
   gaussian.setValue(circle.outerRadius, forKey: "inputRadius1")
   gaussian.setValue(CIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0), forKey: "inputColor0")
   gaussian.setValue(CIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.0), forKey: "inputColor1")

   if index == 0 {
      prev = gaussian
   } else {
      shimi = CIFilter(name: "CISourceOverCompositing")
      if let prevMask = prev?.outputImage, let gaussianMask = gaussian.outputImage {
         shimi?.setValue(gaussianMask, forKey: kCIInputImageKey)
         shimi?.setValue(prevMask, forKey: kCIInputBackgroundImageKey)
      }
      prev = shimi
   }
}

下記の画像がオリジナル画像とマスク画像をオリジナル画像に重ね合わせている図です。顔のしみがある部分を狙ってガウス波形の重ね合わせでマスク画像を作成しています。

オリジナル画像 しみ消し用のマスク画像
スクリーンショット 2020-12-22 18.09.20.png スクリーンショット 2020-12-22 18.09.51.png

画像の合成

続きまして作成したしみ消し用のマスク画像を元に画像合成をしてしみ消し機能を実装していきます。マスク画像を使った部分的な合成にはCIBlendWithMaskフィルターを使います。

CIBlendWithMaskフィルターではマスク画像とオリジナル画像とマスク部分だけ適応したい画像補正をオリジナル画像にかけた画像を用意して合成処理を行います。

let blend = CIFilter(name: "CIBlendWithMask") else { return nil }

/// ブラー効果をかけた画像の設定
blend.setValue(blurImage, forKey: kCIInputImageKey)
/// オリジナル画像の設定
blend.setValue(originalImage, forKey: kCIInputBackgroundImageKey)
/// マスク画像の設定
blend.setValue(mask, forKey: kCIInputMaskImageKey)

/// 合成した画像の取得
blend.outputImage
オリジナル画像 しみ消し用のマスク画像 Blur画像
スクリーンショット 2020-12-22 18.09.20.png スクリーンショット 2020-12-22 18.09.51.png スクリーンショット 2020-12-22 18.10.00.png

結果

部分的に画像補正を行ってしみ消し機能を実装してみた結果はこんな感じです!しみの部分だけ部分的にぼかし効果が適用されてしみが目立たなくなっているのがわかるかと思います。

オリジナル画像 しみ消し処理後の画像
スクリーンショット 2020-12-22 18.32.27.png スクリーンショット 2020-12-22 18.32.41.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】Qiita 週間LGTM数ランキング【自動更新】

他のタグ

AWS Android Docker Git Go iOS Java JavaScript Linux Node.js PHP Python Rails React Ruby Swift TypeScript Vim Vue.js

集計について

  • 期間: 2020-12-17 ~ 2020-12-24
  • 条件: ストック数が 2 以上の記事

ソースコード:

LGTM 数ランキング

1 位: 新卒iOSエンジニアがコードレビューを受けて気づいたポイント4選

iOS Swift コードレビュー

89 LGTM
@yayamochi さん ( 2020-12-19 22:59 に投稿 )

2 位: PokéAPIを利用したPokedexにUIアニメーションを導入してみた

アニメーション SpriteKit Swift pokemon

20 LGTM
@haguhoms さん ( 2020-12-24 11:16 に投稿 )

3 位: ARKit 4のLiDAR Depth API

iOS Swift Depth ARKit Lidar

13 LGTM
@shu223 さん ( 2020-12-23 11:45 に投稿 )

4 位: iOS14 で始める CarPlay

iPhone iOS CarPlay Swift Swift5

13 LGTM
@crexista さん ( 2020-12-18 17:30 に投稿 )

5 位: SwiftLintの設定ファイルをサーバーに配置して共有する方法

iOS Swift SwiftLint

11 LGTM
@uhooi さん ( 2020-12-17 23:02 に投稿 )

6 位: iOSアプリ上にストリーミングサーバーを構築する

iOS avfoundation Swift ServerSideSwift

10 LGTM
@rb-de0 さん ( 2020-12-20 12:00 に投稿 )

7 位: [Swift] SwiftUIの不思議な機能を実現する変数展開"()"について調べた

iOS Swift SwiftUI

8 LGTM
@En3_HCl さん ( 2020-12-20 00:15 に投稿 )

8 位: 【iOS】CIFilterを駆使して、ネガフィルムの写真をネガポジ変換する

iOS CIFilter Metal Swift CIKernel

7 LGTM
@MYamate_jp さん ( 2020-12-18 01:13 に投稿 )

9 位: SpriteKit Particle Fileを使ってクリスマスカード作ってみた

SpriteKit Swift

6 LGTM
@Umiii さん ( 2020-12-22 19:36 に投稿 )

10 位: Cocoa MVCがおFatになりやすいワケ

iOS Swift

6 LGTM
@k_awoki さん ( 2020-12-21 23:46 に投稿 )

11 位: 【Swift】MessageKitを解剖する

Swift

6 LGTM
@Sossiii さん ( 2020-12-19 16:50 に投稿 )

12 位: SwiftUIで作るプログレス

iOS Swift SwiftUI

6 LGTM
@Masataka-n さん ( 2020-12-18 15:00 に投稿 )

13 位: サブする iOS で取り組んでいる DI(Dependency Injection)可能な実装について

Xcode iOS テスト DI Swift

5 LGTM
@branch10480 さん ( 2020-12-24 01:43 に投稿 )

14 位: iOS用ライブラリ作成者向けSwift Package Managerのリソース周りTips

iOS UIKit Swift SwiftPM

5 LGTM
@kazuhiro4949 さん ( 2020-12-17 21:16 に投稿 )

15 位: ARKit 4でのFace Trackingの改善

iOS AR Swift Depth ARKit

4 LGTM
@shu223 さん ( 2020-12-24 17:23 に投稿 )

16 位: SwiftUI 使って Watch Complication を実装してみる!

Swift AppleWatch watchOS Complication SwiftUI

3 LGTM
@MilanistaDev さん ( 2020-12-24 01:28 に投稿 )

17 位: 【Swift】アプリ内にデータを保存する UserDefaults を使う

初心者 Swift

3 LGTM
@antk さん ( 2020-12-22 18:25 に投稿 )

18 位: 【Swift5】レビュー機能のUIを実装する

iPhone レビュー iPhone開発 Swift Swift5

3 LGTM
@tomotaka_s さん ( 2020-12-21 23:48 に投稿 )

19 位: VisionでHand Pose Detection 手のトラッキング

iOS MachineLearning Swift VisionFramework handtracking

3 LGTM
@john-rocky さん ( 2020-12-19 23:40 に投稿 )

20 位: 【Xcode&Swift】ちょっと手こずるエラーまとめ

Xcode エラー error 初心者 Swift

3 LGTM
@Erica_pon さん ( 2020-12-18 10:22 に投稿 )

21 位: ビットボードの紹介~少しのオセロ実装を添えて~

Swift 初投稿 bitboard

3 LGTM
@watergreat31 さん ( 2020-12-18 09:04 に投稿 )

22 位: iOSアプリ開発でぶち当たるエラー処世術〜Xcode編〜

Xcode iOS LLDB デバッグ Swift

2 LGTM
@okamoto441 さん ( 2020-12-23 21:06 に投稿 )

23 位: 【Swift】型の設計指針について

クラス Swift 構造体 設計指針

2 LGTM
@onishiApp さん ( 2020-12-20 14:36 に投稿 )

24 位: The Composable ArchitectureでPullbackは何をしているのか

iOS Swift TheComposableArchitecture

2 LGTM
@redryerye さん ( 2020-12-19 23:47 に投稿 )

25 位: 【Swift】プロトコルの概念と定義方法

Protocol プロトコル Swift インターフェース

2 LGTM
@onishiApp さん ( 2020-12-18 17:16 に投稿 )

26 位: WKWebViewでSPAなサイトの遷移を監視する方法

iOS SPA Swift WKWebView

2 LGTM
@yo1106 さん ( 2020-12-17 23:39 に投稿 )

27 位: 【Swift】非同期処理「GCD」について

非同期処理 GCD Swift QoS DispatchQueue

1 LGTM
@onishiApp さん ( 2020-12-22 16:53 に投稿 )

28 位: [iOS] iPhone12 miniでStatusBarの高さ取得が期待した動作をしないのと対策

UI Xcode iOS Swift

1 LGTM
@t_osawa_009 さん ( 2020-12-22 12:11 に投稿 )

29 位: 【Swift】非同期処理ってなにもの?

非同期処理 GCD Swift Thread OperationQueue

1 LGTM
@onishiApp さん ( 2020-12-22 11:02 に投稿 )

30 位: Storyboardを使わずにカウントアプリを作ってみる

Xcode iOS Swift Swift5

1 LGTM
@tanaka-tt さん ( 2020-12-21 06:18 に投稿 )

31 位: NotificationCenterを用いて、iOSアプリのライフサイクルイベントを検知する

iOS Swift RxSwift

1 LGTM
@yutosa3 さん ( 2020-12-18 19:33 に投稿 )

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

【Swift】Qiita 週間 LGTM 数ランキング【自動更新】

他のタグ

AWS Android Docker Git Go iOS Java JavaScript Linux Node.js PHP Python Rails React Ruby Swift TypeScript Vim Vue.js

集計について

  • 期間: 2020-12-17 ~ 2020-12-24
  • 条件: ストック数が 2 以上の記事

ソースコード:

LGTM 数ランキング

1 位: 新卒iOSエンジニアがコードレビューを受けて気づいたポイント4選

iOS Swift コードレビュー

89 LGTM
@yayamochi さん ( 2020-12-19 22:59 に投稿 )

2 位: PokéAPIを利用したPokedexにUIアニメーションを導入してみた

アニメーション SpriteKit Swift pokemon

20 LGTM
@haguhoms さん ( 2020-12-24 11:16 に投稿 )

3 位: ARKit 4のLiDAR Depth API

iOS Swift Depth ARKit Lidar

13 LGTM
@shu223 さん ( 2020-12-23 11:45 に投稿 )

4 位: iOS14 で始める CarPlay

iPhone iOS CarPlay Swift Swift5

13 LGTM
@crexista さん ( 2020-12-18 17:30 に投稿 )

5 位: SwiftLintの設定ファイルをサーバーに配置して共有する方法

iOS Swift SwiftLint

11 LGTM
@uhooi さん ( 2020-12-17 23:02 に投稿 )

6 位: iOSアプリ上にストリーミングサーバーを構築する

iOS avfoundation Swift ServerSideSwift

10 LGTM
@rb-de0 さん ( 2020-12-20 12:00 に投稿 )

7 位: [Swift] SwiftUIの不思議な機能を実現する変数展開"()"について調べた

iOS Swift SwiftUI

8 LGTM
@En3_HCl さん ( 2020-12-20 00:15 に投稿 )

8 位: 【iOS】CIFilterを駆使して、ネガフィルムの写真をネガポジ変換する

iOS CIFilter Metal Swift CIKernel

7 LGTM
@MYamate_jp さん ( 2020-12-18 01:13 に投稿 )

9 位: SpriteKit Particle Fileを使ってクリスマスカード作ってみた

SpriteKit Swift

6 LGTM
@Umiii さん ( 2020-12-22 19:36 に投稿 )

10 位: Cocoa MVCがおFatになりやすいワケ

iOS Swift

6 LGTM
@k_awoki さん ( 2020-12-21 23:46 に投稿 )

11 位: 【Swift】MessageKitを解剖する

Swift

6 LGTM
@Sossiii さん ( 2020-12-19 16:50 に投稿 )

12 位: SwiftUIで作るプログレス

iOS Swift SwiftUI

6 LGTM
@Masataka-n さん ( 2020-12-18 15:00 に投稿 )

13 位: ARKit 4でのFace Trackingの改善

iOS AR Swift Depth ARKit

5 LGTM
@shu223 さん ( 2020-12-24 17:23 に投稿 )

14 位: サブする iOS で取り組んでいる DI(Dependency Injection)可能な実装について

Xcode iOS テスト DI Swift

5 LGTM
@branch10480 さん ( 2020-12-24 01:43 に投稿 )

15 位: iOS用ライブラリ作成者向けSwift Package Managerのリソース周りTips

iOS UIKit Swift SwiftPM

5 LGTM
@kazuhiro4949 さん ( 2020-12-17 21:16 に投稿 )

16 位: SwiftUI 使って Watch Complication を実装してみる!

Swift AppleWatch watchOS Complication SwiftUI

3 LGTM
@MilanistaDev さん ( 2020-12-24 01:28 に投稿 )

17 位: 【Swift5】レビュー機能のUIを実装する

iPhone レビュー iPhone開発 Swift Swift5

3 LGTM
@tomotaka_s さん ( 2020-12-21 23:48 に投稿 )

18 位: VisionでHand Pose Detection 手のトラッキング

iOS MachineLearning Swift VisionFramework handtracking

3 LGTM
@john-rocky さん ( 2020-12-19 23:40 に投稿 )

19 位: 【Xcode&Swift】ちょっと手こずるエラーまとめ

Xcode エラー error 初心者 Swift

3 LGTM
@Erica_pon さん ( 2020-12-18 10:22 に投稿 )

20 位: ビットボードの紹介~少しのオセロ実装を添えて~

Swift 初投稿 bitboard

3 LGTM
@watergreat31 さん ( 2020-12-18 09:04 に投稿 )

21 位: 【Swift】アプリ内にデータを保存する UserDefaults を使う

初心者 Swift

2 LGTM
@antk さん ( 2020-12-22 18:25 に投稿 )

22 位: 【Swift】型の設計指針について

クラス Swift 構造体 設計指針

2 LGTM
@onishiApp さん ( 2020-12-20 14:36 に投稿 )

23 位: The Composable ArchitectureでPullbackは何をしているのか

iOS Swift TheComposableArchitecture

2 LGTM
@redryerye さん ( 2020-12-19 23:47 に投稿 )

24 位: 【Swift】プロトコルの概念と定義方法

Protocol プロトコル Swift インターフェース

2 LGTM
@onishiApp さん ( 2020-12-18 17:16 に投稿 )

25 位: WKWebViewでSPAなサイトの遷移を監視する方法

iOS SPA Swift WKWebView

2 LGTM
@yo1106 さん ( 2020-12-17 23:39 に投稿 )

26 位: iOSアプリ開発でぶち当たるエラー処世術〜Xcode編〜

Xcode iOS LLDB デバッグ Swift

1 LGTM
@okamoto441 さん ( 2020-12-23 21:06 に投稿 )

27 位: 【Swift】非同期処理「GCD」について

非同期処理 GCD Swift QoS DispatchQueue

1 LGTM
@onishiApp さん ( 2020-12-22 16:53 に投稿 )

28 位: [iOS] iPhone12 miniでStatusBarの高さ取得が期待した動作をしないのと対策

UI Xcode iOS Swift

1 LGTM
@t_osawa_009 さん ( 2020-12-22 12:11 に投稿 )

29 位: 【Swift】非同期処理ってなにもの?

非同期処理 GCD Swift Thread OperationQueue

1 LGTM
@onishiApp さん ( 2020-12-22 11:02 に投稿 )

30 位: Storyboardを使わずにカウントアプリを作ってみる

Xcode iOS Swift Swift5

1 LGTM
@tanaka-tt さん ( 2020-12-21 06:18 に投稿 )

31 位: NotificationCenterを用いて、iOSアプリのライフサイクルイベントを検知する

iOS Swift RxSwift

1 LGTM
@yutosa3 さん ( 2020-12-18 19:33 に投稿 )

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

iOS: SFTPでファイルアップロード

  • Swift 5
  • Xcode Version 12.3
  • NMSSH

iOSアプリ開発にて、SFTPでサーバーにファイルをアップロードする仕様でした。
今回使用したライブラリはNMSSH。これ以外、見つけられませんでした。
- https://github.com/NMSSH/NMSSH

CocoaPodのインストール

まずはここでつまづきました。
参照)https://qiita.com/spring_i/items/181bc3c05142d1f80d93

$ sudo gem install -v1.8.4 cocoapods -n /usr/local/bin
$ pod setup

MNSSHのインストール

Xcodeのプロジェクトは作成済みとしています。

$ cd [プロジェクト名]
$ pod init

Podfileが生成されますので、それを編集。

Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'SampleProject' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for GSApp
  pod 'NMSSH'
end

インストールコマンド

$ pod install

ライブラリインポート

プロジェクトを、生成された[プロジェクト名].xcworkspaceをダブルクリックして起動する。

対象のファイルにて、ライブラリインポート

ViewController.swift
imprt NMSSH

アップロードするダミーファイル作成

アップロードするためのダミーファイル作成

ViewController.swift
        do {
            let fileManager = FileManager.default
            let doc = try fileManager.url(for: .documentDirectory,
                                          in: .userDomainMask,
                                          appropriateFor: nil, create: false)
            let path = doc.appendingPathComponent("muyFile.txt")
            let data = "Hello, world".data(using: .utf8)
            fileManager.createFile(atPath: path.path, contents: data, attributes: nil)
        } catch {
            print(error)
        }

アップロード

ViewController.swift
            let host = "[IPアドレス]"
            let username = "[ユーザー名]"
            let password = "[パスワード]"
            let remotePath = "[送信先のファイルパス]"
            let session = NMSSHSession.connect(toHost: host, withUsername: username)
            if (session.isConnected) {
                session.authenticate(byPassword: password)
                if (session.isAuthorized) {
                    session.channel.uploadFile(path.path, to: remotePath)
                }
            }
            session.disconnect()

サンプルソースコード

onClickをボタンと結びつけています。

ViewController.swift
//
//  ViewController.swift
//  SampleProject
//
//  Created by SankoSC on 2020/12/21.
//

import UIKit
import NMSSH

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func onClick(_ sender: Any) {
        sftp()
    }

    func sftp() {
        do {
            let fileManager = FileManager.default
            let doc = try fileManager.url(for: .documentDirectory,
                                          in: .userDomainMask,
                                          appropriateFor: nil, create: false)
            let path = doc.appendingPathComponent("muyFile.txt")
            let data = "Hello, world".data(using: .utf8)
            fileManager.createFile(atPath: path.path, contents: data, attributes: nil)

            let host = "[IPアドレス]"
            let username = "[ユーザー名]"
            let password = "[パスワード]"
            let remotePath = "[送信先のファイルパス]"
            let session = NMSSHSession.connect(toHost: host, withUsername: username)
            if (session.isConnected) {
                session.authenticate(byPassword: password)
                if (session.isAuthorized) {
                    session.channel.uploadFile(path.path, to: remotePath)
                }
            }
            session.disconnect()
        } catch {
            print(error)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift CombineにてFutureで実行している非同期処理をsinkしているのに途中でキャンセルされる

Combineを使用する際にハマってしまったので、備忘録として残します。

問題

Futureのインスタンスにsinkしてクロージャ(サブスクライバ)を設定し、非同期処理が完了した際にsinkにて設定したクロージャの処理を行いたいのだが、完了する前にSubscribeがキャンセルされてしまう。

func start() {
        Future<Void, Error>{ promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 5.0){
                promise(.success(()))
            }
        }
        .handleEvents(receiveSubscription: { _ in
            print("receiveSubscription")
        }, receiveOutput: {
            print("receiveOutput")
        }, receiveCompletion: { _ in
            print("receiveCompletion")
        }, receiveCancel: {
            print("receiveCancel")
        }, receiveRequest: { _ in
            print("receiveRequest")
        })
        // クロージャが実行されない
        .sink(receiveCompletion: { completion in
            print("completion:\(completion)")
        }, receiveValue: {

        })
    }
デバッグコンソール
receiveSubscription
receiveRequest
receiveCancel

解決方法

sinkメソッドの返り値を変数に保持するか、storeメソッドを使用して保持する必要がある。

func start() {
        cancellable = Future<Void, Error>{ promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 5.0){
                promise(.success(()))
            }
        }
        .handleEvents(receiveSubscription: { _ in
            print("receiveSubscription")
        }, receiveOutput: {
            print("receiveoutput")
        }, receiveCompletion: { _ in
            print("receiveCompletion")
        }, receiveCancel: {
            print("receiveCancel")
        }, receiveRequest: { _ in
            print("receiveRequest")
        })
        .sink(receiveCompletion: { completion in
            print("completion:\(completion)")
        }, receiveValue: {

        })
    }
デバッグコンソール
receiveSubscription
receiveRequest
receiveoutput
receiveCompletion
completion:finished

原因

Future内に実装した処理が完了(promise()がコールされる)前にsinkから返却されたAnyCancellableが破棄されてしまうとキャンセルされてしまうみたい。
公式のリファレンスを読むと「Return Value」の部分にひっそりと書かれていた。

https://developer.apple.com/documentation/combine/future/sink(receivecompletion:receivevalue:)

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

iOSショートカットアプリを作って鍵をNFCで素早く開ける

スマートロックをもっとスマートに解錠したい。

玄関の前まで帰ってきたときに

携帯を取り出す → (マスクを外す) → 顔認証を解除 → アプリを探す → アプリを開く → 鍵を開ける

というプロセスを踏む必要があるのですが、「アプリを探す → アプリを開く」の2工程をNFCタグを使って削減するアプリを作ります。

特にこのマスク環境下では1工程でも減らしたい。

スマートロックにはGPSで近づくと自動で鍵を開ける設定などもあるのですが、常にGPSをチェックするため電池を激しく消耗し、チェック間隔より手動で開けた方が早いことも多く、
そもそも内廊下のマンションでは電波なんて届かないので使えません。

公式アプリではiOSショートカットに対応していないので、自分で対応するアプリを作り、
そこから公式アプリへリダイレクトさせてしまいます。

NFCタグ

10枚 NFCステッカー NTAG 213 NFCタグ/ 25 mm(1インチ)黒 円形

シールタイプで黒の目立たないものを選びました。
これを玄関部分に貼り付けるのですが、大抵の玄関の扉は金属製だと思うのですが、金属に貼り付けると反応が鈍くなるので注意!
私はインターホンの近くの壁部分に貼り付けました。

一応、マンションの玄関や廊下部分ですが普通は共用部という扱いですのでご注意ください。

iOSアプリ作成

特にView部分は必要ないのですがSwift UIで作っています。

別アプリを開く

まず公式アプリへリダイレクトさせるアプリを作成していきます。
私の場合bitlockを使用しているためlunchBitlockというアプリ名にしました。
画面を表示した瞬間に公式アプリを開くようにしています。

import SwiftUI

@main
struct lunchBitlockApp: App {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    lunchApp()
                }
                .onChange(of: scenePhase) { _ in
                    lunchApp()
                }
        }
    }

    func lunchApp() -> Void {
        if let url = URL(string: "app.jp.co.bitkey.bitlock:") {
            UIApplication.shared.open(url)
        }
    }
}

アプリ個別のURLスキームは公開されていないため調べるのが難しいのですが、
Androidアプリも同名を使っていることが多いので、Google PlayストアのURLに書かれているパッケージ名から推察できたりします。

ショートカット対応させる

iOSショートカットXcodeのメニューから

File → New → File → SiriKit Intent Definition File

でショートカット対応させるためのファイルを作成します。

作ったIntent DefinitionファイルにCustom Intentsを追加します。
特に設定は不要ですが、わかりやすいようにタイトルを設定しておきましょう。

このファイルを追加するだけでiOSショートカットへの対応は完了です。
ショートカットアプリのアクション部分にアプリが表示されるようになります。

オートメーション設定

ショートカットアプリのオートメーション設定から新しく追加します。

「いつ」に反応させたいNFCタグを登録し、「行う(アクション)」に作ったアプリを登録するとこんな感じのオートメーションができあがります。

この状態でNFCタグにiPhoneをかざすと自動でアプリが開かれるようになって便利。

おわり

疲れてヘトヘトになって帰ってきたときに「アプリを探す」という工程が面倒だったり、別アプリを開いてるときにサクッとアプリが切り替えられるので割と便利です。

私の場合自宅WiFiに接続したときにこのアプリを開くオートメーションも登録しています。
たまにこっちの方が早く反応することもありますが、WiFi切り替え時などに誤爆します。

公式さん対応してください。。。

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

[Xcode 12] [Swift] guard節で複数条件を判定する時の推奨コーディングスタイル

Xcode 12でguard節の自動フォーマットが変わりました。
Xcode 12以降でのコーディングスタイルについての考察です。

Xcode 11以前の自動フォーマットと、私個人のコーディングスタイル

func foo() {
    // Xcode 11の自動フォーマット。letの位置がズレていて読みづらい…
    guard let hoge = hoge,
        let fuga = fuga else {
            print("guarded")
            return
    }
    print("\(hoge) - \(fuga)")
}

func bar() {
    // 上だと読みづらいので、Xcode 11ではこう書いてました。
    guard
        let hoge = hoge,
        let fuga = fuga
        else {
            print("guarded")
            return
    }
    print("\(hoge) - \(fuga)")
}

Xcode 12以降の自動フォーマット

func foo() {
    // Xcode 12の自動フォーマット。letの位置が合って読みやすくなりました。多分これが推奨のスタイル。
    guard let hoge = hoge,
          let fuga = fuga else {
        print("guarded")
        return
    }
    print("\(hoge) - \(fuga)")
}

func bar() {
    // Xcode 11以前のスタイルだと、elseの位置が変わりました。
    // 別にこれでも悪くはないけど、Xcodeが上の自動フォーマットをしてくれるなら、このスタイルで書く必要性はなくなりました。
    guard
        let hoge = hoge,
        let fuga = fuga
    else {
        print("guarded")
        return
    }
    print("\(hoge) - \(fuga)")
}

Xcode 12のリリースノート(英語)では、前者 func foo() のスタイルが例示されていますので、Xcode 12以降では前者のスタイルで書くのが良さそうです。

番外:

func foo() {
    // returnするだけならこれでも良いかと思われます。
    guard let hoge = hoge else { return }
    guard let fuga = fuga else { return }
    print("\(hoge) - \(fuga)")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DateFormatter(short, medium, long)

DateFormatter
.dateStyle
.timeStyle

それぞれの出力結果

short.swift
let dateFormatter = DateFormatter()

dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
print(dateFormatter.string(from: Date()))

// 12/23/20, 1:42 PM

medium.swift
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .medium
print(dateFormatter.string(from: Date()))

// Dec 23, 2020 at 1:42:57 PM
short.swift
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
print(dateFormatter.string(from: Date()))

// December 23, 2020 at 1:42:57 PM GMT+9

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

モデルをオプショナル型で宣言する

はじめに

MVCアーキテクチャについて勉強していたらインスタンス変数をオプショナル型で定義しているコードがありました。その理由と挙動について書きます。コメント歓迎してます。

モデルクラスの用意

class Model {
    func model() {
        print("model")
    }
}

modelというメソッドを定義しています。想定としては、コントローラークラスから参照され、用いられます。

コントローラークラスの用意

インスタンス変数modelModel?型で定義しています。
methodOfModelModelクラスのmodelメソッドを行うメソッドです。

class Controller {
    var model: Model? {
        didSet {
            print("変更された")
        }
    }
    func methodOfModel() {
        model?.model()
    }
}

挙動

let controller = Controller()
print("modelは\(controller.model)")//modelはnil
controller.methodOfModel()//この時点では実行されない
controller.model = Model() //変更された
print("modelは\(controller.model)")//modelはOptional(Page_Contents.Model)
controller.methodOfModel()//model

1行目ではControllerをインスタンス化しています。
2行目ではcontroller.modelがnilであることを確認しています。
3行目ではcontroller.methodが実行されないことを確認しています。
4行目ではcontroller.modelModel()を入れています。ここでmodelは値が変更されたのでdidSetが実行されます。
5行目では2行目と同じことが書かれています。が、modelがnilでないことが確認できます。
6行目も3行目と同じことが書かれています。が、Modelクラスのmodelが実行されます。

なぜモデルをオプショナル型で宣言するのか

ここからは書いている人の予想という色が強いことをはじめに宣言しておきます。

let model = Model()

としてしまうとクラスとクラスの結合が強くなってしまいます。クラス同士を疎結合にするために、外からインスタンス化を行わせているというのが狙いの一つではないでしょうか。

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

ARKit 4のLiDAR Depth API

iOSデバイスで初めてLiDARスキャナを搭載したiPad Proの発売と同時にリリースされたARKit 3.5では、LiDARを利用してreconstractionした3Dメッシュは得られたが、その計算に用いられているはずのデプス(深度)データにはアクセスできなかった

rectangle_large_type_2_8e10a807ae6bf848e5d9f98228be7602.png

その後発表されたARKit 4 / iOS 14(/ iPadOS 14)でようやく、LiDARで計測したデプスを取得できるようになった。従来のデプスと区別するため、「シーンデプス(Scene Depth)」とも呼ばれているようだ。

LiDARデプスの取得方法

LiDAR由来のデプスデータは、ARWorldTrackingConfiguration利用時、frameSemanticsプロパティに.sceneDepthオプションを指定することで取得可能になる。

let session = ARSession()
let configuration = ARWorldTrackingConfiguration()

// sceneDepthオプションが利用可能かをチェック
if type(of: configuration).supportsFrameSemantics(.sceneDepth) {
   // Activate sceneDepth
   configuration.frameSemantics = .sceneDepth
}
session.run(configuration)

ARFrameに新たに追加されたsceneDepthプロパティより、LiDARで計測したデプスデータを取得できる。

func session(_ session: ARSession, didUpdate frame: ARFrame) {
   guard let depthData = frame.sceneDepth else { return }
   // Use depth data
}

利用可能なコンフィギュレーション

ARConfigurationプロトコルに定義されているframeSemanticsプロパティにsceneDepthを設定するので、API的には利用可能ではあるが、APIリファレンスのsceneDepthの項にある記述を読む限り、ワールドトラッキング以外のコンフィギュレーションでは利用できないっぽい。

https://developer.apple.com/documentation/arkit/arconfiguration/framesemantics/3516902-scenedepth

If you enable this option on a world-tracking configuration's frameSemantics, ARKit includes depth information for regions of the current frame's capturedImage. The world-tracking configuration exposes depth information in the sceneDepth property that it updates every frame.

ハードウェア的な制約

同じくsceneDepthのリファレンスページより。

ARKit supports scene depth only on LiDAR-capable devices

チップがA12以上とかの条件はないようだ。(LiDARが載る→必然的に高性能チップとなるからだろうか)

従来のデプスデータとの違い

これまでも(特定の条件下で)ARKit利用時にデプスデータにアクセスすることは可能だった。ARFramecapturedDepthDataプロパティ、estimatedDepthが従来からあったデプスAPI。今回追加されたデプスAPIと何が違うのか。

デプスデータの種類と取得条件

まずもちろん、デプスデータの種類が違う。そしてそれに伴い、取得できる条件も違う。capturedDepthDataはTrue-Depthカメラ由来のデプスデータで、フェイストラッキング利用時のみ取得できる。

estimatedDepthframeSemanticspersonSegmentationWithDepthオプションを指定した場合に取得できるもので、デュアルカメラやTrue-Depthカメラ由来のデプスではなく、そのプロパティ名からわかるとおり推定デプスデータである。要A12チップ以上。

sceneDepthは上述した通り、frameSemanticssceneDepthを指定した場合に取得でき、ARコンフィギュレーションとしてはワールドトラッキングのみ、デバイスとしてLiDARを搭載している必要がある。

フレームレート/生成アルゴリズムの違い

capturedDepthDataはTrue-Depthカメラ由来のデプスデータであるため、ARFrameのカラーデータ(capturedImageプロパティ)よりも更新頻度が低く、デプスを用いたエフェクトなどをかけていると、被写体(フェイストラッキングなので自分)が速く動いた場合に追従できないことがあった。

一方でestimatedDepthは機械学習ベースで推定されるもので、A12以降の高性能なチップを条件としているため、カメラフレームと同等のフレームレートで更新される。

sceneDepthもLiDARで取得した深度データと広角カメラから取得したカラーデータをベースに機械学習アルゴリズムを利用して生成される。60fpsで動作する、つまりこちらもARFrameが得られるたびに更新されている。

(WWDC 2020の"Explore ARKit 4"セッションより)

The colored RGB image from the wide-angle camera and the depth ratings from the LiDAR scanner are fused together using advanced machine learning algorithms to create a dense depth map that is exposed through the API.
(広角カメラからのカラーRGB画像とLiDARスキャナからの深度定格が、高度な機械学習アルゴリズムを使用して融合され、APIを通じて公開される高密度の深度マップが作成されます。)

This operation runs at 60 times per second with the depth map available on every AR frame.
(この操作は毎秒60回実行され、ARフレームごとに深度マップが利用可能になります。)

型の違い

capturedDepthDataはAVDepthData型、estimatedDepthはCVPixelBuffer型、そしてsceneDepthARDepthData型で得られる。

ARDepthData

上述したとおりsceneDepthプロパティから得られるLiDAR由来のデプスデータはARDepthData型で得られる。こちらはiOS 14で追加された新クラス。

https://developer.apple.com/documentation/arkit/ardepthdata?changes=latest_major

これをうまく利用した公式サンプルが出ているので、詳しい解説はそのサンプルを読みつつ別記事で書く。

LiDARの精度

WWDC 2020ではないが、ARKit 3.5リリース時に、"Advanced Scene Understanding in AR"というTech Talkが公開されており、その中でLiDARの精度について言及があった。

The new iPad Pro comes equipped with a LiDAR Scanner. This is used to determine distance by measuring at nanosecond speeds how long it takes for light to reach an object in front of you and reflect back. This is effective up to five meters away and operates both indoors and outdoors. 
(新しいiPad ProにはLiDARスキャナーが搭載されています。これは、目の前の物体に光が到達して反射して戻ってくるまでの時間をナノ秒の速度で測定することで距離を判断するために使用されます。これは5メートル先まで有効で、屋内でも屋外でも使用できます。)

LiDARで何メートル先まで計測できるのか、という質問は何度かされたが、こういうのは公式には言及されない印象だったので、「サンプルで実際に試してみるといいですよ」という回答をしていた。目安にしろ貴重な公式情報ではあるのでメモ。

personSegmentationWithDepthとシーンデプス

ARConfigurationframeSemanticsプロパティにpersonSegmentationWithDepthオプションを指定している場合、シーンデプスを取得可能なデバイスであれば、自動的にシーンデプスを得られるらしい。

let session = ARSession()
let configuration = ARWorldTrackingConfiguration()

// Set required frame semantics
let semantics: ARConfiguration.FrameSemantics = .personSegmentationWithDepth

// Check if configuration and device supports the required semantics
if type(of: configuration).supportsFrameSemantics(semantics) {
   // Activate .personSegmentationWithDepth
   configuration.frameSemantics = semantics
}
session.run(configuration)

しかも、追加の電力コストもかからないとのこと。

Additionally if you have an AR app that uses people occlusion feature, and then search the personSegmentationWithDepth frameSemantic, then you will automatically get sceneDepth on devices that support the sceneDepth frameSemantic with no additional power cost to your application.

smoothedSceneDepth

上述したsceneDepthはWWDC 2020開催時点で(iOS 14 beta 1時点で)既に公開されていたが、iOS 14 beta 5で突如smoothedSceneDepthなるAPIが追加された。

https://twitter.com/shu223/status/1295968108352479232

このとき(2020年8月)ドキュメントをすぐに見に行ったが詳細は何も書かれてなかった。

今見に行くとちゃんと説明が書かれており、sceneDepthとの違いも明記されている。DeepL利用の日本語訳を併記してここにまとめておく。

smoothedSceneDepth: ARConfiguration.FrameSemantics

ARConfiguration.FrameSemanticsの型プロパティ

https://developer.apple.com/documentation/arkit/arconfiguration/framesemantics/3674208-smoothedscenedepth

An option that provides the distance from the device to real-world objects, averaged across several frames.
 (デバイスから実世界のオブジェクトまでの距離を提供するオプションで、複数のフレームで平均化されます。)

Declaration

static var smoothedSceneDepth: ARConfiguration.FrameSemantics { get }

Discussion

Enable this option on a world-tracking configuration (ARWorldTrackingConfiguration) to instruct ARKit to provide your app with the distance between the user’s device and the real-world objects pictured in the frame's capturedImage. ARKit samples this distance using the LiDAR scanner and provides the results through the smoothedSceneDepth property on the session’s currentFrame.
(ワールドトラッキング設定(ARWorldTrackingConfiguration)でこのオプションを有効にすると、ユーザーのデバイスとフレームのキャプチャ画像に写っている現実世界のオブジェクトとの距離をアプリに提供するようにARKitに指示します。ARKit は、LiDAR スキャナーを使用してこの距離をサンプリングし、セッションの currentFrame の smoothedSceneDepth プロパティを通して結果を提供します。)

LiDARスキャナーを使用することがここで明記されている。

To minimize the difference in LiDAR readings across frames, ARKit processes the data as an average. The averaged readings reduce flickering to create a smoother motion effect when depicting objects with depth, as demonstrated in Creating a Fog Effect Using Scene Depth. Alternatively, to access a discrete LiDAR reading at the instant the framework creates the current frame, use sceneDepth.
(フレーム間の LiDAR 読み取り値の差を最小化するために、ARKit はデータを平均として処理します。平均化された読み取り値は、「シーン深度を使用したフォグ効果の作成」で実証されているように、奥行きのあるオブジェクトを描写する際にフリッカーを低減し、より滑らかなモーション効果を生み出します。また、フレームワークが現在のフレームを作成した瞬間に個別の LiDAR 読み値にアクセスするには、sceneDepth を使用します。)

ここが一番重要。sceneDepthとの違いが明記されており、メリットや使い分けについても書かれている。

ARKit supports scene depth only on LiDAR-capable devices, so call supportsFrameSemantics(:) to ensure device support before attempting to enable scene depth.
(ARKit は、LiDAR 対応デバイスでのみシーン深度をサポートしているので、シーン深度を有効にする前に supportsFrameSemantics(
:) を呼び出してデバイスのサポートを確認してください。)

smoothedSceneDepth: ARDepthData

ARFrameのsmoothedSceneDepthプロパティ

https://developer.apple.com/documentation/arkit/arframe/3674209-smoothedscenedepth

An average of distance measurements between a device's rear camera and real-world objects that creates smoother visuals in an AR experience.
(デバイスの背面カメラと実世界のオブジェクトの間の距離測定の平均値で、AR体験でよりスムーズなビジュアルを作成します。)

Declaration

var smoothedSceneDepth: ARDepthData? { get }

Discussion

This property describes the distance between a device's camera and objects or areas in the real world, including ARKit’s confidence in the estimated distance. This is similar to sceneDepth except that the framework smoothes the depth data over time to lessen its frame-to-frame delta.
(このプロパティは、デバイスのカメラと実世界のオブジェクトやエリアとの距離を記述します。これは sceneDepth と似ていますが、フレームワークがフレーム間の差分を減らすために深度データを時間の経過とともに滑らかにします。)

This property is nil by default. Add the smoothedSceneDepth frame semantic to your configuration’s frameSemantics to instruct the framework to populate this value with ARDepthData captured by the LiDAR scanner.
(このプロパティはデフォルトでは nil です。フレームワークに、LiDAR スキャナによってキャプチャされた ARDepthData でこの値を入力するように指示するために、設定の frameSemantics に smoothedSceneDepth フレームセマンティックを追加します。)

Call supportsFrameSemantics(:) on your app’s configuration to support smoothed scene depth on select devices and configurations.
(アプリの設定で supportsFrameSemantics(
:) を呼び出して、選択したデバイスと設定で平滑化されたシーン深度をサポートします。)

関連

ARKitの本、iOSデプスの本を書いています。

実践ARKit - BOOTH
Depth in Depth - iOSデプス詳解 - BOOTH

アドベントカレンダー

本記事はiOSアドベントカレンダー20日目の記事です。2020年12月23日現在、エントリーされていた方が記事をアップされていなかったので、その方に確認の上記事を代理投稿させていただきました。

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

【Swift】非同期処理「Operation」について

Operation、OperationQueueクラス

非同期処理に関して、以前GCDの記事を投稿しましたが、
GCDはコアライブラリのlibdispatchで実装されています。

同じくコアライブラリのFoundationにも非同期処理を実現するクラスである、
OperationクラスとOperationQueueクラスがあります。

Operationクラスは、実行されるタスクとその情報をカプセル化したものです。

このOperationクラスのインスタンスがキューに入れられて順次実行されます。
この時にキューの役割を果たすのがOperationQueueクラスになります。

実行方法

タスクの定義

GCDの時はタスクはクロージャ内に書いていましたが、
Operationを使う時は、Operationクラスのサブクラスとして定義します。

クラスとして表すことによって少し手間ですがメリットもあり、
扱いやすいインターフェースを提供することができます。

なお、処理内容はmain( )メソッドの中に記述します。

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class SomeOperation: Operation {
    let value: Int
    init(value: Int) {
        self.value = value
    }

    override func main() {
        // スレッドを1秒止める
        Thread.sleep(forTimeInterval: 1)
        print(value)
    }
}

これでタスクを定義することができたので、
次にこのタスクをキューに入れる必要があります。

ですが、キューに入れる前にキューを作らなければいけません。

キューの生成

タスクを実行するキューとなるOperationQueueクラスのインスタンスを作成します。

OperationQueueクラスは引数なしのイニシャライザでインスタンス化できます。
インスタンス化後にプロパティ経由で各種設定を行います。

import Fountation

let queue = OperationQueue()

・nameプロパティ
nameプロパティを設定するとキューに名前をつけることができます。
この値には逆順DNS形式を使用することが一般的です。

・maxConcurrentOperationCountプロパティ
特定のキューが同時に実行できるタスク数はシステムの状況から適切に判断されますが、
maxConcurrentOperationCountプロパティを使うことでその数を指定することができます。

当たり前ですが、大きな数を設定しても高速になる訳ではなく、
むしろ遅くなる可能性がありますのでご注意ください。

・QualityOfService型
GCDにはQoSで実行優先度を決めることができましたが、
それのOperationQueueクラスバージョンがこれになります。

QualityOfService型の値の役割はGCDのQoSと同様です。

import Foundation

let queue = OperationQueue()
queue.name = "com.example.my_operation_queue"
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInitiated

キューとタスクは作ることができたので、
キューにタスクを追加する必要があります。

キューへのタスクの追加

タスクの追加方法は、OperationQueueクラスのaddOperation(_:)メソッドを使用します。
引数にはOperationクラスの値を渡します。

また、OperationQueueクラスには複数のOperationクラスを渡すためのメソッドである、
addOperations(_:waitUntilFinished:)も用意されています。

第一引数の型は[Operation]型になります。

次のサンプルコードでは、
SampleOperation型の値を5個作成しキューに追加しています。

第二引数のwaitUntilFinishedにtrueを与えると、
全てのタスクの実行が終わるまで呼び出したスレッドをブロックします。

今回はfalseを与えているので、タスクの終了を待たずにそのまま次の処理を実行します。

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class SampleOperation: Operation {
    let value: Int
    init(value: Int) {
        self.value = value
    }

    override func main() {
        Thread.sleep(forTimeInterval: 1)
        print("value: \(value)")
    }
}

let queue = OperationQueue()
queue.name = "com.example.my_operation_queue"
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInitiated

var operations: [SampleOperation] = []

for i in 0..<5 {
    operations.append(SampleOperation(value: i))
}

queue.addOperations(operations, waitUntilFinished: false)
print("OperationQueueにタスクを追加しました。")

実行結果
OperationQueueにタスクを追加しました
value: 0
value: 1
value: 2
value: 3
value: 4

maxConcurrentOperationCount = 2のように指定しているため、
1秒おきにタスクを2つずつ実行していきます。

Thread.sleep(forTimeInterval: 1)をコメントアウトした場合は
下記のような実行結果になります。

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class SampleOperation: Operation {
    let value: Int
    init(value: Int) {
        self.value = value
    }

    override func main() {
//        Thread.sleep(forTimeInterval: 1)
        print("value: \(value)")
    }
}

let queue = OperationQueue()
queue.name = "com.example.my_operation_queue"
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInitiated

var operations: [SampleOperation] = []

for i in 0..<5 {
    operations.append(SampleOperation(value: i))
}

queue.addOperations(operations, waitUntilFinished: false)
print("OperationQueueにタスクを追加しました。")

実行結果
value: 0
value: 1
OperationQueueにタスクを追加しました
value: 2
value: 3
value: 4

タスクのキャンセル

Operationクラスにはタスクをキャンセルする機能を持つcancel()メソッドがあります。

次のサンプルコードでは、先ほどのコードにcansel( )メソッドを追加しています。
operations[2].cancel()により value: 2が存在しないのがわかります。

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class SampleOperation: Operation {
    let value: Int
    init(value: Int) {
        self.value = value
    }

    override func main() {
        print("value: \(value)")
    }
}

let queue = OperationQueue()
queue.name = "com.example.my_operation_queue"
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .userInitiated

var operations: [SampleOperation] = []

for i in 0..<5 {
    operations.append(SampleOperation(value: i))
}

queue.addOperations(operations, waitUntilFinished: false)
operations[2].cancel()

実行結果
value: 0
value: 1
value: 3
value: 4

タスクの依存関係の設定

Operationクラスでは複数のタスク間での依存関係を設定することができます。

依存関係を定義することにより、
あるタスクが終了してからでないとこのタスクを実行しないよ!といった処理ができます。

あるタスクに対して、それよりも先に実行されるべきタスクを指定するには、
OperationクラスのaddDependency(_:)メソッドを使用します。

引数には先に実行されるべきタスクを入れます。

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class FirstOperation: Operation {
    override func main() {
        print("This is FirstOperation")
    }
}

class SecendOperation: Operation {
    override func main() {
        print("This is SecondOperation")
    }
}


let queue = OperationQueue()
queue.name = "com.example.my_operation_queue"
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .userInteractive

var operations: [Operation] = []

operations.append(FirstOperation())
operations.append(SecendOperation())

operations[0].addDependency(operations[1])
queue.addOperations(operations, waitUntilFinished: false)

実行結果
This is SecondOperation
This is FirstOperation

FirstOperation( )が先に実行されるはずでしたが、
operations[0].addDependency(operations[1])の部分で
依存関係を定義しているのでSecondOperation( )から実行されました。

使うタイミング

複数の非同期処理を実装する

Operation、OperationQueueクラスは、
非同期処理をオブジェクト指向で抽象化したものです。

単純なスレッドの切り替えの他に、
タスクの依存関係やキャンセル機能などが備わっているため、
GCDよりも複雑な処理に向いています。

また、Operation、OperationQueueクラスは、内部でGCDを利用しているので
GCDで定義できることはOperation、OperationQueueクラスでも定義することができます。

GCDはタスクをクロージャで表すことができ、
キューを生成しなくてもグローバルキューを利用できます。

Operation、OperationQueueクラスはタスクの定義やキューの生成が必要ですが、
その分複雑な処理を行うことができます。

したがって、単純な非同期処理はGCDで定義し、
キャンセルや依存関係の定義はOperation、OperationQueueクラスで定義しましょう!

他にも非同期処理にはThreadクラスがありますが、
使う機会はほぼないらしいので紹介はしません。

もし私が使う機会があったらその時に共有できたらなと思います。

以上、最後までご覧いただきありがとうございました。

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