20200924のiOSに関する記事は20件です。

Swiftで取得した画像をWKWebView内のHTMLで表示する方法

WkWebviewのコンテンツ内でSwiftで取得したPNG画像などのデータを使いたいケースがあると思います。

本記事ではWebViewJavascriptBridgeを使用した、PNG画像のSwiftとJavaScript間での通信にて表示する方法について記載します。

ローカルのHTMLをWebViewで読み込み(Swift)

まずはWKWebviewでローカルのhtmlを読み込む。

Swift
class ViewController: UIViewController {
    var webView:WKWebView?

    override func viewDidLoad() {
        super.viewDidLoad()
        // WKWebViewのViewへの追加
        self.webView = WKWebView(frame: view.frame)
        view.addSubview(self.webView!)
        let htmlPath = Bundle.main.path(forResource: "content", ofType: "html")
        let baseURL = URL.init(fileURLWithPath: htmlPath!)
        webView!.loadFileURL(baseURL, allowingReadAccessTo: baseURL)
    }
}

JavaScriptへのbridge(Swift)

WebViewJavascriptBridgeの初期化と、
registerHandlerにてJavaScript→Swiftへアクセスする受け口作成。

Swift
        self.bridge = WebViewJavascriptBridge.init(forWebView: webView)
        // WKWebView内のjavascriptからSwift内のデータを取得
        self.bridge!.registerHandler("image") { (data, callback) in
        }

JavaScript側のbridge設定(JavaScript)

読み込むhtml内のJavaScriptにてWebViewJavascriptBridgeでSwift間通信するためのテンプレートを追加後、
bridge.callHandlerにてSwift側への画像取得要求を行う。
※詳細はWebViewJavascriptBridge参照

JavaScript
        function setupWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
            window.WVJBCallbacks = [callback];
            var WVJBIframe = document.createElement('iframe');
            WVJBIframe.style.display = 'none';
            WVJBIframe.src = 'https://__bridge_loaded__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

        setupWebViewJavascriptBridge(function (bridge) {
            bridge.callHandler('image', { 'key': 'value' }, function responseCallback(responseData) {
            })
        })

Base64データへの変換(Swift)

WKWebview側でPNGを読み込むためbase64のstringへ変換してJavaScript側へ返却。

Swift
        self.bridge!.registerHandler("image") { (data, callback) in
            if let imagePath = Bundle.main.path(forResource: "PNG", ofType: "png") {
                // PNGをUIImageに
                let image = UIImage(contentsOfFile: imagePath)
                // Data型へ変換
                let data = image?.pngData()
                // PNGのデータをbase64エンコードしてWebViewで表示できるよう修正
                let base64Image = data?.base64EncodedString(options: .endLineWithLineFeed)
                callback!(base64Image)
            }
        }

base64形式のPNGデータの読み込み

Swift側から返却されたbase64形式のresponseDataは、
PNGのデータURIとなっていないため、先頭に
data:image/png;base64,
を追加してimgタグで読み込み。

JavaScript
            bridge.callHandler('image', { 'key': 'value' }, function responseCallback(responseData) {
                var html = [
                ];
                html += '<img src="data:image/png;base64,'
                html += responseData
                html += '" alt="PNG"/>'
                document.getElementById("content").innerHTML = html
            })

上記にてWKWebview上でSwiftで取得したPNGが表示される。

サンプルアプリ

GitHubにて上記ソースのサンプルアプリ公開しています。

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

【Swift5】AVSpeechSynthesizerとSFSpeechRecognizerを併用する際の注意点

概要

iOSでは標準で音声合成と音声認識の機能を利用することが可能です。
- 音声合成: AVSpeechSynthesizer
- 音声認識: SFSpeechRecognizer

音声合成と音声認識を併用するケースはそこまで多くないと思いますが、一緒に利用する場合はハマりポイントがあるのでこの記事でメモします。

想定するケース

音声認識後に何らかの文字列を音声読み上げする場合を考えます。
イメージしやすいように、音声認識をして取得した結果を読み上げるだけのアプリを想定します。

  1. 音声認識を行い、話しかけられたテキストを取得する
  2. 取得した結果を音声で読み上げる

音声認識

iOSで標準利用可能なSFSpeechRecognizerを利用して音声認識をします。
※iOS10から利用可能

公式Doc
https://developer.apple.com/documentation/speech/sfspeechrecognizer

下記のような実装で音声認識が可能です。(最低限イメージが持てるよう、かなり省略したものを載せています。実装する方は公式Docをご参照ください。)

import UIKit
import AVFoundation
import Speech

class SampleViewController: UIViewController {

    private var audioEngine: AVAudioEngine!
    private var recognitionReq: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private var recognizedText = ""
    private let synthesizer = AVSpeechSynthesizer()

    ~~~~

    private func startRec() throws {
        ~~~~

        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
        try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
        let inputNode = audioEngine.inputNode

        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 2048, format: recordingFormat) { (buffer, time) in
            recognitionReq.append(buffer)
        }
        audioEngine.prepare()
        try audioEngine.start()

        recognitionTask = recognizer.recognitionTask(with: recognitionReq, resultHandler: { (result, error) in
            if let error = error {
                ~~~
            } else {
                print(result.bestTranscription.formattedString))
            }
        })
    }

音声合成

AVSpeechSynthesizerの利用法はこちらの記事で紹介しています。
https://qiita.com/maKunugi/items/dc9da201a663c8773c8c

併用した場合のハマりポイント

音声認識直後にAVSpeechSynthesizerのspaekメソッドを使って読み上げをしようとすると下記のエラーが発生し、読み上げが行われません。

2020-09-24 20:33:33.228313+0900 xxxxxxx[10895:2365580] [AXTTSCommon] Failure starting audio queue \M-3<…>

解決方法

AVSpeechSynthesizerのspeakメソッドを利用する際は、audioSession.setCategoryを指定することで解決しました。

try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)

この問題についてハマった際、ネット上の記事を探していると同様の事象についてのやりとりがあります。
ex
https://developer.apple.com/forums/thread/88030

ただし、ネット上の情報だと、

AVAudioSession.Category.playAndRecord

を指定するとうまくいくと記述されている情報が多いですが、自分の環境だと、このCategoryを設定してしまうと読み上げの音量が極端に小さくなってしまうという挙動を確認しました。

※調べきれていないですが、OSのバージョンによって挙動が変わる可能性もあります。
※iOS13, 14では「playback」を指定することで正常に動作することを確認しました (XCode12)

音声認識後に音量が小さくなってしまう事象はこの記事でも登場しています。
https://stackoverflow.com/questions/40639660/swift-3-using-speech-recognition-and-avfoundation-together

情報が錯綜しているので不安はあるものの、同様の事象で悩まれている方の参考になれば幸いです。

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

公式YOLOv3物体検出モデルをiOSで使う手順。

アップル公式配布モデルをダウンロードして、物体検出してみます。
image.png

以下のような「人間」「自転車」「車」「バイク」。。。という80個の物体を認識して画像内の位置を教えてくれます。
スクリーンショット 2020-09-24 19.41.02.png

手順

1、Visionで実行リクエストを作成

YOLOv3.mlmodelをXcodeプロジェクトにドラッグ&ドロップして、

lazy var detectRequest:VNCoreMLRequest = {
    let model = try! VNCoreMLModel(for: YOLOv3().model)
    let request = VNCoreMLRequest(model: model, completionHandler: nil)
    request.imageCropAndScaleOption = .scaleFit
    return request
}()

2、ImageRequestHandlerで画像を渡して実行

let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])

DispatchQueue.global(qos: .userInitiated).async { [self] in
    do {
       try handler.perform([detectRequest])
    } catch let error {
       print("\(error)")
}

3,結果(ラベル、信頼度、物体位置)を処理する

画像内で認識できた物体の数だけVNRecognizedObjectObservationが返ってきます。

guard let results = request.results as? [VNRecognizedObjectObservation] else { return }

一つ一つの物体について、ラベル、信頼度、物体位置が取れます。

for result in results {
    let label:String = result.labels.first!.identifier // ラベル名。「labels」の0番目(例えば”Car”の信頼度が一番高い。1番目(例えば”Truck”)の信頼度が次に高い。
    print(label)
    // "Car"

    let confidence = result.confidence // labelの信頼度
    print(confidence)
    // 0.8664

    let boundingBox = result.boundingBox // 認識された物体の境界ボックス
    print(boundingBox)
    // (0.4403754696249962, 0.3421999216079712, 0.12934787571430206, 0.38909912109375)    
    //* Core Imageと同じで右下が原点
}

?


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

iOSDC2020に参加して3日間で36のセッションを見ました

はじめに

こんにちは、ClassiのiOSアプリエンジニアの@yoko-yanです。
iOSDC2020に参加して、day0からday2までの3日間で、36のセッションを見ましたので、そのセッションを感想ともに、簡単にまとめたいと思います。
Classiは、iOSDC2020のボトルウォータースポンサーとTシャツスポンサーになっています。

以下、個人的によかったセッションを、紹介します。
どちらかというと、自分の業務や、興味の範囲で、役に立ったかという視点ですので、ご了承ください。
この他にも、参加した中で素晴らしいセッションもあり、参加してないセッションについても、興味深いものは多いんですが、一旦、上記の視点と参加したものの中ではという観点でお話させていただきます。

個人的によかったセッション

新規機能開発からモジュール分割を始めてみる by Ryo Izumi

新規機能開発からモジュール分割を始めてみる - Speaker Deck
マルチモジュール化の話
実際に、モジュール(フレームワーク)化の方法を丁寧に解説
注意点や発見したこと、メリデメなどTipsのような感じ
感想
実際に今の業務でもモジュール化した方がいいねという話はあがっているので、それの判断基準になりそうな話が聞けたのでよかった
XcodeGenは、他のセッションでもよく出てくるので、今のうちに導入は前向きに検討し、実際に導入することになったときのために、知識はもっといた方がいいなと思った

エラーアーキテクチャ設計について考える by きちえもん

エラーアーキテクチャ設計について考える/Thinking about error architecture design - Speaker Deck
エラーハンドリングを、iOSで改善した話とAndroidで改善した話
それぞれ別の話で、コードもそれぞれのコードでの紹介
感想
個人的には、1番このセッションが有用だった。今現在、まさにエラーハンドリングのリファクタをしているところなので、いろんなところに共感できつつ、点で学んでいた中途半端な知識が少し、線でつながった感じ
一部、Androidのコードで紹介していたが、分かりづらいということもなく、考え方は共通なので、逆にiOSでも同じロジックでいけそうだなとイメージ出来たのはよかった
メモ

考え方は、共通なので分かりづらいということもなく、とても分かりやすかった
以下、いくつか頭に残っているものを列挙
- Error型を、そのまま使うと、どんなエラーが起こり得るのか分かりづらい
- 結果の型を、任意のエラー型を指定できるように
- 個別のエラーを表現することができるようになった
- 任意のエラー型は、列挙型で起こり得るエラーを記述
- 固有エラーを、共通エラーと同じ上位で処理すると、全てのエラーを考慮しないといけないロジックになってしまう。
- 固有エラーは別の流れにしてViewModelで処理したほうがいい

一石二鳥: マルチモジュール化, ビルド速度快適化 by Saryong Kang

マルチモジュール化のコツとメリデメの話、bazelを用いたビルド最適化の話、テストの目的や考え方・Tipsなどの話
感想
1番、メモを撮った。モジュール編では、別のモジュール化をテーマにしたセッションをより概念的なところからの話のような感じで、モジュール化に対する理解が深まった
ビルド編では、Bazelというビルドツールを知らなかったので、構築が難しいのと、まだXcodeとの相性が悪そうだが、利用できるようになるといろいろなことが出来そう
テスト編では、テストコードを各基準や目的など、よりどんなテストを書けばいいかの理解が深まった
メモ

実践マルチモージュル編
モジュールを分ける基準
・ドメインロジックがグループに分けられる
・スコープがよく定義されている
・複数のアプリで適用可能
・アプリによって活性化される
・既存のモジュールと明確に違う
・長期間担当するチームが存在している

あとから分けるのは難しい
初期段階で分けた方がいい

スピード感と正しさを両立

ドメインごとの分離
各ドメイン3−4つのレイヤーに分離
Binding,Presentation,Data
Presentationレイヤーは、ドメインロジックと分けるメリットはあまりない

Dependency Inversion Principle 依存性逆転の原則
依存性逆転したほうが、分かりやすい
例えば、Repositoryのインターフェースを、プレゼンテーションレイヤーに書く

ビルド編
Bazel 
バックエンドからモバイルまで、幅広くビルドができるもの
Skylark Pythonのサブセット
分析段階のビルドは、LinuxでもOK、コンパイルはMacが必要

Bezelがつかわれない理由
最近のXcodeビルドが、そんなに遅くない
リモートビルドの設定の難易度が高い
Xcodeとの相性が超悪い

であれば
CIサーバーに導入を目標にする
podなどは、ビルドに埋め込む

Googleが方向性を変更、Appleは対応しない方針だったが、転換

テスト編
いいテストの条件
実際のアプリに期待している行動と一緒

目的は、行動の変化がない限り、更新しない

テスト作成の原則
テストが失敗したら、アプリも失敗しているのが理想

XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する by 佐藤剛士

XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する - Speaker Deck
PageObjectデザインパターンを導入した、UIテストの導入方法を紹介
UIテストを導入する判断基準から、実際にコードを紹介しつつ、実際に導入する手順まで、丁寧に解説
感想
実際に業務でUIテストの導入を検討したが、ログイン画面がWebだったので、うまく要素が取れず断念したが、Ask the Speakerで、まさに聞きたいことを質問してくれた人がいたので、非常に為になった。再度、UIテストの導入に挑戦してみたい
自社でもQAリソースが足りないことが多く、サービスがWebメインなのでアプリの優先度も高くないので、導入を前向きに検討するための情報を得られた
メモ

UIテストのつらさ
UIは変わりゆくもの

UITestを導入した経緯
UITestの対象、新機能?既存機能?
メルペイでは、既存
リグレッションテストは、2週間ごと
既存機能は安定していることが必要
リグレッションテスト300 自動化対象200項目 CIパス項目40

XCUITest入門
AccessibilityIdentifier
クラス名や要素名を組みあわせる命名規則
Page Object Pattern
テストと画面を分ける考え方 Appniumが提唱するパターン
変更容易性向上 UIが変わっても、Page Object を変更するのみ

Ask the Speaker
Q WebでのUIテストどうしてますか?
A メルペイでは、Webはほとんどランディングページしかないので、UIテストは遷移したかどうかくらいしかチェックしてない
Webをテストする際は、waitは、10秒〜30秒かけた方がいい。結構、読み込みに時間がかかる印象
Q パーミッションのテストはどうしてますか?
A パーミッションは、XCUIアプリケーションで取れないので、システムアラート取得するようにしてる。トークでは省いたけどスライドに追加しました。

組織構造の力学を操作して、アプリ開発プロセスを最大化させる by Masato Ishigaki

組織構造の力学を操作して、アプリ開発プロセスを最大化させる / organizational structure to maximize the development process - Speaker Deck
iOSアプリを開発する中で、生産性のバランス可視化したり意思決定プロセスの構築、開発体制の整備などを行って、新規事業開発で、市場投入までの時間を短縮した話
感想
ちょうど、今やってる業務でユーザーストーリーを作成し、その次にどう進めるかというフェーズだったので、非常に興味があり、実際にAsk the Speakerで質問もしてみて、いろいろ参考になった
iOSの話というより、もっと幅広い話になるが、個人的には前職では、そういったプロジェクトに関わることが多く、同じような課題感を持っていたので、過去にうまくいかなかったプロジェクトが、どう進めればよかったのかを改めて振り返ることが出来た
新規開発だと、機能を積み上げるインクリメント型で作りがちだが、ユーザーのストーリーにそったイッテレーティブ型で作るのが重要という話で、頭の中では分かってるが、図にすると非常に分かりやすいと思って、スムーズに理解できた
メモ

リードタイムを最大限短縮する

ユーザーストーリーマッピング
プロダクトバックログ

ナラティブフローに沿って、行ってレーティブに作る
インクリメントでは作らない

カンバンの可視化したほうがいい
WIP制限をかけてたほうがいい

1番大事なのは、生産性の可視化

Ask the Speaker
(ユーザーストーリーを作成したあとの流れについて知りたかったので)質問してみた
Q ストーリーマッピングの中で、実際にUIデザインをする時にどの程度書き起こすのかと、その後の流れで実際に開発に着手するタイミングを教えてください
A ユーザーストーリーマッピングの時点で、がっつりデザインを作っていた。各工程でグラデーションがあって、初期の段階で、デザインも開発も早い段階で着手し、出来るところから初めていく。多少の手戻りはしょうがない。ワイヤーのデザインもガッチリ作っていた。ワイヤーが何のエピックをやっているか、設計のエピックが何をやっているかを、可視化するのが大事

google/mediapipe で始めるARアプリ開発 by noppe

google/mediapipe で始めるARアプリ開発/iOSDC2020 - Speaker Deck
mediapipeで構築したMLパイプライン(Graph)とARKitを連携して、ハンドトラッカーのフレームワークを作り、バーチャルなボタンを作成する話
bezelというビルドツールを使う
Xcodeでもビルドできる「tulsi」というものがある
感想
個人開発で、KinectやLeapMotionを使ったトラッキングのサンプルコードや、AR/VRのサンプルコードで遊んでいた時期がありかつ、iOSのVisionフレームワークも触ってみたことがあったので、非常に興味があるセッションでした
内容的には、バーチャルボタンをクリックするというシンプルなものでしたが、近未来を感じさせるようなものなので、とてもワクワクした
前職で、個人でHTC VIVEとHoloLensを買った強者がいたが、HoloLensはバーチャルボタンをクリックする体験が出来たので、これがモバイルデバイスレベルで出来るようになったのを感じるのは、とても嬉しかった
Ask the Speaker
きつねのanimojiは、プライベートAPIをハックしてだしたらしい

Swiftで分かるSOLID原則 by 川口 航平

SwiftでわかるSOLID原則 iOSDC 2020 - Speaker Deck
SOLID原則とは何かという話から、SOLID原則を意識してソフトウェア開発を行えば,開発者にとって有益という話
感想
開発をしているとバグを埋め込む可能性にびくびくしながら何回も見直すことが多く、変更に強く理解しやすいシステムを作りたいというのは、常にあるので、SOLID原則を分かりやすく解説してくれたのはよかった
原則のいくつかは、見たことあるものだが、十分理解出来てない部分も多かったので、よく理解できた
特に、依存性逆転の原則については、最近、悩んでるところでもあったので、分かりやすく説明してくれたので楽しみながら学べた
メモ

SOLID原則 変更に強く理解しやすいシステム

単一責任原則
したがっていない例
FatVCがよくある例

リスコフの置換原則
依存性逆転の原則

その他の見たセッションで感想

ここに書いてないセッションの感想を、以下のページに分けて、書きましたので、こちらもどうぞ
iOSDC2020のday0に参加したセッションの内容と感想 - Qiita
iOSDC2020のday1に参加したセッションの内容と感想 - Qiita
iOSDC2020のday2に参加したセッションの内容と感想 - Qiita

見てないセッションで気になるもの

GitHub ActionsでiOSアプリをCIする個人的ベストプラクティス by uhooi | トーク | iOSDC Japan 2020 - fortee.jp
今回、2冠を取られた@uhooiさんのセッション

iOSアプリ開発のための"The Composable Architecture"がすごく良いので紹介したい by 今城 善矩 | トーク | iOSDC Japan 2020 - fortee.jp
初めてRxSwiftを学んだときにお世話になったRxSwift研究読本の著者の@yimajoさんのセッション

まとめ

今回の開催はオンラインということで、これまでの雰囲気とどう違うのかは分かりませんが、おかげで、たくさんのセッションを見ることができて、良かったです。
ただ、去年まで沖縄在住だったので、今回、東京に移住してきて、初の参加だったので、個人的にはオフラインで参加してみたかったというお気持ちです。

今回、オンラインで開催して頂いた、おかげで、見れなかったセッションも、じっくりと見ることができます。(1ヶ月間)
ニコニコ動画のプレミアム会員になると、追っかけ再生が出来るみたいなので、月額540円は、安いと思ったので、迷わず課金しました。

初めてのオンラインということで、運営スタッフは、相当大変だったろうなと思います。
運営の皆様もスピーカーの皆様も感謝!

来年は、オンラインとオフラインのハイブリッドで開催して頂けると嬉しいな。。
(ただ、さらに大変なことになるのは、想像できますね。。)

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

SwiftUIで著名アプリのUIをトレース ~Spotify編~

iOS14とXcode12がリリースされ、SF Symbols2が使えるようになったので著名なアプリのUIをトレースするのが簡単になったなぁと思いレイアウトの勉強がてらSpotifyの再生画面をトレースしてみました。

完成品

Simulator Screen Shot - iPhone 8 - 2020-09-24 at 18.53.37.png
厳密には背景がアートワークのグラデーションが薄っすらかかっていますが割愛

もしもSpotifyがライトモードだったら

Simulator Screen Shot - iPhone 8 - 2020-09-24 at 18.53.43.png

SwiftUIが自動でライトモードとダークモード対応してくれるので、こんなもしかしたらな画面も生成できました。
まぁライトモードでこれだと完全トレースとは言えないんですが。。。

コード

PlayerView.swift
import SwiftUI

struct PlayerView: View {
    @Binding var isPresent: Bool
    @State var seekValue = 0.7
    @State var isPlaying = false

    var body: some View {
        VStack(spacing: 24) {
            HStack {
                Button(action: {
                    self.isPresent = false
                }, label: {
                    Image(systemName: "chevron.down")
                        .foregroundColor(.primary)
                })
                Spacer()
                Text("834.194")
                    .font(.caption)
                    .fontWeight(.semibold)
                Spacer()
                Image(systemName: "ellipsis")
            }
            Image("artwork")
                .resizable()
                .imageScale(.large)
                .scaledToFit()
            TitleAndSeekControl(seekValue: $seekValue)
            PlayerControl()
            ActionControl()
        }
        .padding(.horizontal)
    }
}

private struct TitleAndSeekControl: View {
    @Binding var seekValue: Double
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text("忘れられないの")
                .font(.title2)
                .fontWeight(.semibold)
            Text("サカナクション")
                .foregroundColor(.secondary)
            Slider(value: $seekValue)
                .accentColor(.secondary)
            HStack {
                Text("2:31")
                Spacer()
                Text("3:43")
            }
            .foregroundColor(.secondary)
            .font(.caption)
        }
    }
}

private struct PlayerControl: View {
    var body: some View {
        HStack {
            Image(systemName: "heart")
            Spacer()
            HStack(spacing: 32) {
                Image(systemName: "backward.end.fill")
                    .font(.title2)
                Image(systemName: "play.circle.fill")
                    .font(.system(size: 48))
                Image(systemName: "forward.end.fill")
                    .font(.title2)
            }

            Spacer()
            Image(systemName: "minus.circle")
        }
        .imageScale(.large)
    }
}

private struct ActionControl: View {
    var body: some View {
        HStack {
            Image(systemName: "tv.and.hifispeaker.fill")
            Spacer()
            Image(systemName: "square.and.arrow.up")
        }
        .imageScale(.medium)
    }
}

struct PlayerView_Previews: PreviewProvider {
    @State static var isPresent: Bool = true
    static var previews: some View {
        PlayerView(isPresent: $isPresent)
    }
}

それとなくStackでグルーピングされてそうな要素を個別のViewに書き出しています。
Viewモディファイアの勉強に既存のアプリのトレースはなかなかいいです。
あくまで見た目上のトレースなので実用するにはバインディングをもうちょっと整理しないといけませんが。

非デザイナーに嬉しいSF Symbols

僕は画面のオブジェクトを自分でデザインとかできないのでSF Symbolsは相当助かります。
SF Symbolsはこんなのもあるの?ってくらい追加されているので、iOSアプリ開発ではFontAwesomeからSF Symbolsに移行していくのではないでしょうか。

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

【Swift】ちょっとはMVPという設計パターンに触れてみる

なぜ書いたか

これまでSwiftを使って色々アプリを作ってきましたが、
正直設計パターンというのをほとんど意識しませんした:joy:
基本的なMVCと言われるもので作ってましたが、そろそろまずいだろということで
ちょっと設計パターンについて勉強してみました。

本記事はアウトプット用のメモとなります。

MVCとMVPの違い

  • MVC
    • Model
      • ドメインのモデルにあたる構造体を定義クラス
    • View
      • ViewControllerやTableViewCellクラスなど画面描画に関する処理を行うクラス
    • Controller
      • ViewControllerクラス ユーザーからの操作の検知やその後の画面変更を行うクラス
  • MVP
    • Model
      • 上記同様 構造体を扱うクラス
    • View
      • ViewCotrollerクラス Uikitをインポートした画面描画に関する処理をまとめたクラス
    • Presenter
      • Presenterクラス Uikitをインポートしないデータを扱うクラス

MVPにおける各コンポーネントの責務

モデル ビュー プレゼンター
DB層と通信する データをレンダリングする モデルへのクエリを実行します
適切なイベントの提起 イベントを受け取る モデルからデータをフォーマットする
非常に基本的な検証ロジック フォーマットされたデータをビューに送信します。
複雑な検証ロジック

引用:https://riptutorial.com/ja/ios/topic/9467/mvpアーキテクチャ

MVCのデメリット(一部)

MVCではViewControllerがViewとControllerを兼ねて処理している
→Step数が多くなり肥大化しやすい。

MVPによる対処法:Presenterクラスを用いてViewControllerクラスの処理をPresenterクラスに一部移動する。

MVPの恩恵

  • ViewControllerのボリュームを抑えることができる。
    • 可読性の向上
  • クラスごとの責務がわかりやすい
    • 保守性、静的テストのしやすさの向上
  • 設計パターンを知っていれば、新規参入者でもわかりやすい構造

参考サイト

iOSアプリ設計大全集 2016
わかりやすくMVCやMVP、MVVMなどについてまとめられている
設計パターンの一覧を学ぶのに有用なサイト
PEAKS(ピークス)|iOSアプリ設計パターン入門
設計パターンの細かい説明だけでなく、設計とはという概念的な部分まで書かれた書籍
ネットで無料で見ることができる
iOSをMVC,MVP,MVVM,Clean Architectureで実装してみた
各設計パターンをメリットデメリットを上げてわかりやすくまとめられている

筋肉.swiftアプリをMVPパターンで実装(しようと)してみて感じたこと
これまで自分が実装してきたMVPは中途半端だった
MVPとは について知れるサイト

https://github.com/rockname/ArchitectureSampleWithFirebase/tree/mvp
https://github.com/gdate/MVPTest
MVPの設計パターンにそったサンプルコード

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

iOSDC2020のday2に参加したセッションの内容と感想

トークセッション

「それ、自動化できますよ」: note を支えるワークフロー大全 by laprasDrum

iOS Dev Workflow Automation for note - Speaker Deck
zapierを使ってCIビルドを自動化した話

感想
結構、よくある話だが、実際に運用しているフローと組み込むまでの流れを聞けたのは参考になった
具体的な手順を紹介してくれたので、業務でも取り入れたい

XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する by 佐藤剛士

XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する - Speaker Deck
PageObjectデザインパターンを導入した、UIテストの導入方法を紹介
UIテストを導入する判断基準から、実際にコードを紹介しつつ、実際に導入する手順まで、丁寧に解説

感想
実際に業務でUIテストの導入を検討したが、ログイン画面がWebだったので、うまく要素が取れず断念したが、Ask the Speakerで、まさに聞きたいことを質問してくれた人がいたので、非常に為になった。再度、UIテストの導入に挑戦してみたい
自社でもQAリソースが足りないことが多く、サービスがWebメインなのでアプリの優先度も高くないので、導入を前向きに検討するための情報を得られた

メモ

UIテストのつらさ
UIは変わりゆくもの

UITestを導入した経緯
UITestの対象、新機能?既存機能?
メルペイでは、既存
リグレッションテストは、2週間ごと
既存機能は安定していることが必要
リグレッションテスト300 自動化対象200項目 CIパス項目40

XCUITest入門
AccessibilityIdentifier
クラス名や要素名を組みあわせる命名規則
Page Object Pattern
テストと画面を分ける考え方 Appniumが提唱するパターン
変更容易性向上 UIが変わっても、Page Object を変更するのみ

Ask the Speaker
Q WebでのUIテストどうしてますか?
A メルペイでは、Webはほとんどランディングページしかないので、UIテストは遷移したかどうかくらいしかチェックしてない
Webをテストする際は、waitは、10秒〜30秒かけた方がいい。結構、読み込みに時間がかかる印象
Q パーミッションのテストはどうしてますか?
A パーミッションは、XCUIアプリケーションで取れないので、システムアラート取得するようにしてる。トークでは省いたけどスライドに追加しました。

ここ数年間のタウンワークiOSアプリのエンジニアのチャレンジ by 石井 潤、元 亨周

大規模なサービスゆえの、いろいろな制限(制約)の中で、どのように改善していったかの話
デバッグモードの開発で手動テストの効率化をしたり、E2Eテストツール導入、クラッシュ率のモニタリングして99.9%を保つ運用、フルSwift化して品質・コストを改善したり

感想
自社の業務では、EdTechゆえいろいろな制約があるので、制約がある中で改善した話は、非常に共感出来て、その運用方法や手法は真似したい

組織構造の力学を操作して、アプリ開発プロセスを最大化させる by Masato Ishigaki

組織構造の力学を操作して、アプリ開発プロセスを最大化させる / organizational structure to maximize the development process - Speaker Deck
iOSアプリを開発する中で、生産性のバランス可視化したり意思決定プロセスの構築、開発体制の整備などを行って、新規事業開発で、市場投入までの時間を短縮した話

感想
ちょうど、今やってる業務でユーザーストーリーを作成し、その次にどう進めるかというフェーズだったので、非常に興味があり、実際にAsk the Speakerで質問もしてみて、いろいろ参考になった
iOSの話というより、もっと幅広い話になるが、個人的には前職では、そういったプロジェクトに関わることが多く、同じような課題感を持っていたので、過去にうまくいかなかったプロジェクトが、どう進めればよかったのかを改めて振り返ることが出来た
新規開発だと、機能を積み上げるインクリメント型で作りがちだが、ユーザーのストーリーにそったイッテレーティブ型で作るのが重要という話で、頭の中では分かってるが、図にすると非常に分かりやすいと思って、スムーズに理解できた

メモ

リードタイムを最大限短縮する

ユーザーストーリーマッピング
プロダクトバックログ

ナラティブフローに沿って、行ってレーティブに作る
インクリメントでは作らない

カンバンの可視化したほうがいい
WIP制限をかけてたほうがいい

1番大事なのは、生産性の可視化

Ask the Speaker
(ユーザーストーリーを作成したあとの流れについて知りたかったので)質問してみた
Q ストーリーマッピングの中で、実際にUIデザインをする時にどの程度書き起こすのかと、その後の流れで実際に開発に着手するタイミングを教えてください
A ユーザーストーリーマッピングの時点で、がっつりデザインを作っていた。各工程でグラデーションがあって、初期の段階で、デザインも開発も早い段階で着手し、出来るところから初めていく。多少の手戻りはしょうがない。ワイヤーのデザインもガッチリ作っていた。ワイヤーが何のエピックをやっているか、設計のエピックが何をやっているかを、可視化するのが大事

実践!「みてね」における自動生成活用例 by 佐藤俊輔

実践!「みてね」における自動生成活用例 - Speaker Deck
Sourcery + Stencil を用いて「テストデータ」の自動生成をしたり、「DI」まわりのコードを自動生成した話

感想
SourceryとStencilというものを初めて知った
コードの自動生成については、Webのフレームワークではよくあるので、Xcodeでも標準で出来るようになるといいなと思いつつ、将来的にはこういう自動生成が普通になる可能性は高いので、早めにキャッチアップしておいたほうがよさそう
個人的に、印象に残ったのは、「頑張りすぎない。テンプレートを見てわかりやすいように運営する」

google/mediapipe で始めるARアプリ開発 by noppe

google/mediapipe で始めるARアプリ開発/iOSDC2020 - Speaker Deck
mediapipeで構築したMLパイプライン(Graph)とARKitを連携して、ハンドトラッカーのフレームワークを作り、バーチャルなボタンを作成する話
bezelというビルドツールを使う
Xcodeでもビルドできる「tulsi」というものがある

感想
個人開発で、KinectやLeapMotionを使ったトラッキングのサンプルコードや、AR/VRのサンプルコードで遊んでいた時期がありかつ、iOSのVisionフレームワークも触ってみたことがあったので、非常に興味があるセッションでした
内容的には、バーチャルボタンをクリックするというシンプルなものでしたが、近未来を感じさせるようなものなので、とてもワクワクした
前職で、個人でHTC VIVEとHoloLensを買った強者がいたが、HoloLensはバーチャルボタンをクリックする体験が出来たので、これがモバイルデバイスレベルで出来るようになったのを感じるのは、とても嬉しかった

Ask the Speaker
きつねのanimojiは、プライベートAPIをハックしてだしたらしい

Apple Silicon への長い道 by hak

macOS/iOSデバイスのSoCの歴史を、細かに解説

感想
自身もmacOSとiOSデバイスを触り始めて、結構な年数立ってるので、興味深いセッションでした
ただ、1時間半あったので、途中、集中力が切れて、居眠りしちゃってた部分もあり、細かくは覚えてない
が、ハードウェアに興味ある方は必見


LTセッション

LLDBはアプリ開発時に使うデバッガーツールだけど、Mac用のアプリであれば自ら作成したアプリでなくともアタッチすることができるそうで、LLDBでMac用アプリのどのような情報にアクセスできるので怖いよという話

本当はこわいLLDB by みやし

本当はこわいLLDB - Speaker Deck
Obj-Cだとハックできる
Swiftで書いたコードは、型・シンボルなどの情報はほぼ残らないので、実行中に制御できないが、Objective-Cでは、さわれてしまう

感想
iOS開発に携わって長いが、LLDBに関しては、あまり詳しくなかく、デバッグツールとしてはよく使っていたけど、ハックが出来るのは知らなかったので、確かに怖いと思い、よい情報を得られたのでよかった

iOS 13における Siri Shortcuts 最小実装+α by 明渡麻衣花

iOS 13におけるSiri Shortcuts 最小実装+α - Speaker Deck
Siri Shortcutsの実装の仕方や考察、Siri Shortcutsの導入判断基準や検証する端末や検証方法などの話

感想
Siri Shortcutsについては、意外と使われているんだなと思った
具体的な実装方法を紹介してくれたので、比較的簡単に実装できそうなので、よさそうと思いつつ、シーンなどをイメージして実装しないと、利用されないどころか、逆に余計な機能になりうるなと分かった

xcrun Essentials by Yutaro Muta

xcrun Essentials - Speaker Deck
Command Line Tools Packageに付属しているxcrunの活用方法の話

感想
xcrunについては、最近ちょうど、CIで利用できるテストシュミレータの確認をするときに使って、その事例紹介だったので、内容には非常に共感でき、もっとコマンドを知りたいなと感じた

メモ

xcrun とはなにか
コマンドラインから、Xcodeの機能を実行できる
shim(コマンドラッパー)の一つ

xcrun を使う例
CIで、利用可能なテストデバイスを確認する

Swiftで分かるSOLID原則 by 川口 航平

SwiftでわかるSOLID原則 iOSDC 2020 - Speaker Deck
SOLID原則とは何かという話から、SOLID原則を意識してソフトウェア開発を行えば,開発者にとって有益という話

感想
開発をしているとバグを埋め込む可能性にびくびくしながら何回も見直すことが多く、変更に強く理解しやすいシステムを作りたいというのは、常にあるので、SOLID原則を分かりやすく解説してくれたのはよかった
原則のいくつかは、見たことあるものだが、十分理解出来てない部分も多かったので、よく理解できた
特に、依存性逆転の原則については、最近、悩んでるところでもあったので、分かりやすく説明してくれたので楽しみながら学べた

メモ

SOLID原則 変更に強く理解しやすいシステム

単一責任原則
したがっていない例
FatVCがよくある例

リスコフの置換原則
依存性逆転の原則

SwiftUIとFlutter by tamappe

SwiftUIとFlutterを比較する - Speaker Deck
とりあえず、Flutter最高という話

感想
SwiftUIとFlutter、どちらもキャッチアップしようと思って、できていないものであったので、比較しながら、Flutterのよさを確認できたのはよかった
SwiftUIとFlutterどちらを先にキャッチアップしても、損はしないな(もう一方の方の理解がしやすくなりそう)と感じた

100人以上の中高大学生にiOSアプリ開発を教えていて感じたこと by とし

三年間で100人以上の中高大学生にiOSアプリ開発を教えていて感じたこと - Speaker Deck
プログラミング初心者にプログラミングを教える上で、注意するべきこと、うまく行ったこと、うまくいかなかったこと、どんな手助けが有効なのかなど、経験を通して学んだ話

感想
前職の初期に、開発チームを0から立ち上げ、未経験者5人、経験者は自分1人という経験をしたことがあるので、非常に共感できる内容だった
その時は、あまりうまくいかなかったが、このセッションで、何が悪かったのか

メモ
伸びる子、全く伸びない子がいる

アプリが作れる→コードが書ける+アプリのことがわかっている

アプリらしさが欠如している

Feature Flagを適切に分類することでA/Bテストの運用コストを下げる by Takeshi Ihara

iOSDC20200921: Feature Flagを適切に分類することでA/Bテストの運用コストを下げる - Speaker Deck
新機能の表示の切り替えでKPIの変化を検証するためのA/Bテストを、Feature Flagという機能の有効・無効を切り替える機能で運用
それを、検証範囲や検証期間に合わせて、Release、Ops、Permission、Experimentという4分類にして管理コストを削減した運用を紹介

感想
A/Bテストが増えるたびに、Feature Flagの数を増やすと管理が大変になるので、4に限定して切り替えを行うというのは、理にかなっていて納得

CryptoKitとCoreBluetoothを利用したスマートキー開発 by saiten

CryptoKitとCoreBluetoothを利用したスマートキー開発/iOSDC2020 - Speaker Deck
リモコンキーのないクルマにArduinoとiPhoneを利用したスマートキーシステムを構築する過程で得た知見の話

感想
RxBluetoothKitというRxSwiftベースで作られたライブラリがあるので、それを使うと、簡単に実装できるのが分かった
実際のコード例があるので、実装のイメージが沸いた

メモ

セキュリティ対策はを行わないと、簡単に車のドアを操作できてしまうので注意
その署名も、CryptoKitで簡単

Apple Low-Latency HLSを使った超低遅延配信について by meteor

Apple Low-Latency HLSを使った 超低遅延配信について - Speaker Deck
複雑なAppleのLow-Latency HLSの仕組みを紹介

感想
HLSは、一度、前職で動画配信の仕組みを作るときに扱ったことがあり(β実装まででリリースなし)、仕組みは難しくて、あまり理解できてなかったが、分かりやすく紹介していた

メモ

iOS13だと、safariで使えない
iOS14だと、使える
サンプルコードあり

着信時氏名表示させたいエンジニア vs 簡単には着信時氏名表示できない電話番号 (iOS13対応版) by 栗山 徹

CallKitを使って着信表示する際の、iOSの不具合なのか、実装にてこづった後、iOS13以降での注意点の含めて紹介

感想
CallKitは使ったことがなかったので、実例と苦労話を交えて聞けたのはよかった

その他の見たセッションで感想

ここに書いてないセッションの感想を、以下のページに分けて、書きましたので、こちらもどうぞ
iOSDC2020のday0に参加したセッションの内容と感想 - Qiita
iOSDC2020のday1に参加したセッションの内容と感想 - Qiita
iOSDC2020に参加して3日間で36のセッションを見ました - Qiita

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

Could not find or use auto-linked library 'swiftObjectiveC'

  1. Right-click on Your App Name in the Project Navigator on the left, and click New File…
  2. Create a single empty Swift file
  3. Create Bridging Header and do not remove Swift file then. re-run your build.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactNativeでダークモードでもアプリを強制的にライトモードにするお話

突然ですがiOS端末がダークモードになっていたとしても、アプリはライトモードのままにして欲しいって場合ありませんか?

僕はありました。

ライトにしたい理由は、DateTimePickerライブラリ

こいつがダークモードで確認すると白色になって見えない問題が発生してしまったため。
IMG_3406.PNG

それなら端末がダークモードでもアプリ内では強制的にライトモードにすれば解決でしょ(小並感)

って感じで色々調べたらinfo.plistの中に以下の記述を書けばいいってあったので書きました。

info.plist
 <key>UIUserInterfaceStyle</key>
    <string>Light</string>

pod install して yarn react-native run iosで動作確認!
IMG_4704.png

DateTimePickerライブラリも無事に見えるようになったし解決した!

ハイハイメデタシメデタシ....

IMG_3405_censored.jpg

え????

位置情報アラートの色がライトモードになってない問題が発生.....

あ"あ"あ"あ"あ"あ"あ"あ"あ"も"お"お"お"お"お"お"お"!!!!!!!!!

...ってことでさらに調査していくとこんな感じに記述するといいよってあったので書きました。

AppDelegate.m
if (@available(iOS 13.0, *)) {
        rootView.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
    }

これでpod install して yarn react-native run iosで動作確認!

IMG_4703_censored.jpg

やったぁぁぁぁできたぁぁぁぁぁ!!!!

まとめ

  • 本来であれば紹介したどちらかの方法だけで強制的にライトモードにできるとのことだったのですが、私の場合は上手くできなかったのでどちらも当ててみました。
  • Androidの方はまだやったことがないので出来次第qiitaに投稿します。

注意

  • iOS13でダイアログを強制ライトモードにすることができるのはiPhoneXや11といったホームボタンのない端末だけのようです。iPhone8やiPhoneSe(初期型)といったホームボタン有りの端末ではライトモードになりませんでした。

  • iOS14ではiPhone8やSeといった端末でもライトモードが効きました。

参考記事

強制的にライトモードにする方法
https://stackoverflow.com/questions/58395926/how-to-force-disable-ios-dark-mode-in-react-native

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

[自分用] ボウリングにお役立ちアプリ(Swift-iOSアプリ)

Swiftでアプリを作ってみた

Swiftの練習として, Userdefault, webAPI(Qiita), スクレイピングを利用したアプリを作成してみました.
アプリの内容としては, やくに立たないアプリよりも自分の役に立つアプリを作ろうと考えて趣味のボウリングに役立つアプリを作りました.

githubのリポジトリはこちらです
https://github.com/yossi1118ubi/Bowling4

機能まとめ

  1. webAPIを使って最近のボウリングに関するブログを検索する機能
  2. 自分で作成したボウリングに関する参考になる記事をまとめたスプレットシートから内容を検索して引っ張ってくる機能
  3. 一般的なメモ機能
  4. 行きつけのボウリング上の営業時間を表示する機能

//ホームが面の写真

それぞれの機能紹介

1. webAPIを使って最近のボウリングに関するブログを検索する機能

プロボウラーなの上げている最新のブログを簡単に探すことができるといいなと思い, webAPIを使ってボウリング関連のブログを取得する機能を作りました. 本当はnoteのAPIを使って情報を取得しようと思っていたのですが, APIが公式には公開されていないとのことで仕方なく
QiitaのAPIでボウリングに関する最新の記事を取得するアプリを作りました.

結局, ボウリングに関する技術的な記事を返してくれる機能となってしまい全然嬉しくない機能となりました.
この機能を開くと初期ではボウリング関連の最新記事がtableViewに表示されます.
また, 好きなキーワードで検索をすることもできます. また, 特定のtableViewの行をタップするとSafariでその記事を見ることができるという機能も追加しました.

//ここに写真をいれる

2. 自分で作成したボウリングに関する参考になる記事をまとめたスプレットシートから内容を検索して引っ張ってくる機能

個人的にボウリングで気になった動画や記事のタイトルとURLをスプレットシートに記録していたので, その記事を検索してひっぱってきてくれる機能が欲しいと考えました.
そこで, jsonを使ってスプレットシートの情報を獲得できるようにしました.
また, そのタイトルをタップするとそのURLの記事や動画をSafariで表示することができます.

3. メモ機能

本当に一般的なメモ機能です. メモを保存書いて保存する機能, 保存したメモを見る機能, 保存したメモを削除する機能が搭載されています.
ここではUserdefaultの使い方を勉強しました.

4. 行きつけのボウリングの営業時間を表示する機能

コロナで行きつけのボウリング上の営業時間が変わることがあったので, 簡単に確認できるような機能を作りました. この機能は, 行きつけのボウリング上のwebサイトからスクレイピングで営業時間を取得して画面に表示する機能です.

この機能を通して, スクレイピングの手法を学びました.

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

Xcode11でXCFrameworkの依存関係でNo such moduleエラーが発生する

問題

XcodeのWorkspaceに、iOS Application ProjectとiOS Framework Projectがあり、それらが別のXCFrameworkに依存している構成のとき、iOS Applicationをビルドすると No such module 'XXX' のエラーとなりビルドに失敗してしまいます。

  • MySample Workspace
    • MyApp Project
    • MyFramework Project
    • OtherFramework.xcframwork MyApp MyFramework.png

解決方法

Xcode12にアップデートします。
どうやら、Xcode11のバグ(?)のようで、Xcode12では問題なくビルドできます。

参考

https://pyckamil.github.io/programming,/xcframework,/xcode/2020/05/09/everything-wrong-with-xcframeworks.html

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

【iOS】DiffableDataSourceで作ったUICollectionViewに並び替え機能を実装する

UICollectionViewに並び替え機能を実装したい。

昔ながらのやり方ならば、UICollectionViewDataSourceを使った方法がありますね。
しかし、これからUICollectionViewを作るなら、UICollectionViewDiffableDataSource を使って書きたい。

そんなときは、 UICollectionViewDragDelegateUICollectionViewDropDelegate を使いましょう。

こんな感じの並び替えUIを、シンプルなコードで実装できます。

DiffableDataSourceで作ったUICollectionViewに並び替え機能を追加しました。

環境

  • Xcode 12.0
  • iOS 14.0

教材コード

今回は、このCollectionViewをドラッグ&ドロップで並び替えできる様にします。

CompositionalLayout + DiffableDataSourceで作ったシンプルなCollectionViewのコード (クリックで開封)
import UIKit

final class ViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    private enum Section {
        case main
    }

    private struct Item: Hashable {
        let color: UIColor
        let identifier = UUID()
    }

    private var items = [
        Item(color: .red),
        Item(color: .blue),
        Item(color: .green),
        Item(color: .brown)
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        setupCollectionView()
        setupCollectionViewDataSource()
    }

    private func setupCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemGroupedBackground
        view.addSubview(collectionView)
    }

    private func createCollectionViewLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        let layout = UICollectionViewCompositionalLayout(section: section)

        return layout
    }

    private func setupCollectionViewDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, item in
            cell.backgroundColor = item.color
        }

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
            (collectionView, indexPath, item) -> UICollectionViewCell? in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

1. UICollectionViewDragDelegateとUICollectionViewDropDelegateを有効にする

まず、UICollectionViewのドラッグ&ドロップを有効にするため、以下のコードを追加します。

collectionView.dropDelegate = self
collectionView.dragDelegate = self
collectionView.dragInteractionEnabled = true

2. UICollectionViewDragDelegateのcollectionView(_:itemsForBeginning:at)でドラッグを有効にする

collectionView(_:itemsForBeginning:at) に以下のコードを足します。

extension ViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let itemIdentifier = items[indexPath.item].identifier.uuidString as NSString
        let itemProvider = NSItemProvider(object: itemIdentifier)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
    }
}

ちなみに、空の [UIDragItem]() を指定するとそのセルはドラッグできなくなります。
特定のセルをドラッグしたくない場合は、indexPathで指定してやると良いでしょう。

3. UICollectionViewDropDelegateのcollectionView(_:dropSessionDidUpdate:withDestinationIndexPath:)で、自分のアプリからのドロップだけを有効にする

今回は、外部のアプリからのドロップは受付ず、自分のアプリからのドロップだけ受け付ける様にしておきます。
localDragSessionを見て、内部からのドロップだったら並び替えをし、外部からのドロップならキャンセルする様にしておきます。

extension ViewController: UICollectionViewDropDelegate {
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if session.localDragSession != nil {
            // 内部からのドロップなら並び替えする
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        } else {
            // 外部からのドロップならキャンセルする
            return UICollectionViewDropProposal(operation: .cancel)
        }
    }
}

4. UICollectionViewDropDelegateのcollectionView(_:performDropWith:)で、データソースを更新する

あとは、遷移元と遷移先のIndexPathを取得して、それを元にデータソースを更新するだけです。

func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        switch coordinator.proposal.operation {
        case .move:
            guard
                let destinationIndexPath = coordinator.destinationIndexPath,
                let sourceIndexPath = coordinator.items.first?.sourceIndexPath
            else { return }

            // データソースを更新する
            let sourceItem = items.remove(at: sourceIndexPath.item)
            items.insert(sourceItem, at: destinationIndexPath.item)

            var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
            snapshot.appendSections([.main])
            snapshot.appendItems(items)
            dataSource.apply(snapshot, animatingDifferences: false)
        case .cancel, .forbidden, .copy:
            return
        @unknown default:
            fatalError()
        }
    }

コード全文

コード全文も載せておきます。

CompositionalLayout + DiffableDataSource + DragDelegate + DropDelegateで作った並び替え可能なUICollectionViewコード (クリックで開封)
import UIKit

final class ViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    private enum Section {
        case main
    }

    private struct Item: Hashable {
        let color: UIColor
        let identifier = UUID()
    }

    private var items = [
        Item(color: .red),
        Item(color: .blue),
        Item(color: .green),
        Item(color: .brown)
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        setupCollectionView()
        setupCollectionViewDataSource()
    }

    private func setupCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.dragDelegate = self
        collectionView.dropDelegate = self
        collectionView.dragInteractionEnabled = true
        collectionView.backgroundColor = .systemGroupedBackground
        view.addSubview(collectionView)
    }

    private func createCollectionViewLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        let layout = UICollectionViewCompositionalLayout(section: section)

        return layout
    }

    private func setupCollectionViewDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, item in
            cell.backgroundColor = item.color
        }

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
            (collectionView, indexPath, item) -> UICollectionViewCell? in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }

        loadDataSource(items: items)
    }

    private func loadDataSource(items: [Item]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

extension ViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let itemIdentifier = items[indexPath.item].identifier.uuidString as NSString
        let itemProvider = NSItemProvider(object: itemIdentifier)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
    }
}

extension ViewController: UICollectionViewDropDelegate {
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if session.localDragSession != nil {
            // 内部からのドロップなら並び替えする
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        } else {
            // 外部からのドロップならキャンセルする
            return UICollectionViewDropProposal(operation: .cancel)
        }
    }

    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        switch coordinator.proposal.operation {
        case .move:
            guard
                let destinationIndexPath = coordinator.destinationIndexPath,
                let sourceIndexPath = coordinator.items.first?.sourceIndexPath
            else { return }

            // データソースを更新する
            let sourceItem = items.remove(at: sourceIndexPath.item)
            items.insert(sourceItem, at: destinationIndexPath.item)

            loadDataSource(items: items)
        case .cancel, .forbidden, .copy:
            return
        @unknown default:
            fatalError()
        }
    }
}

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

【簡単】4つのステップでDiffableDataSourceで作ったUICollectionViewに並び替え機能を実装する

UICollectionViewに並び替え機能を実装したい。

昔ながらのやり方ならば、UICollectionViewDataSourceを使った方法がありますね。
しかし、これからUICollectionViewを作るなら、UICollectionViewDiffableDataSource を使って書きたい。

そんなときは、 UICollectionViewDragDelegateUICollectionViewDropDelegate を使いましょう。

こんな感じの並び替えUIを、シンプルなコードで実装できます。

DiffableDataSourceで作ったUICollectionViewに並び替え機能を追加しました。

環境

  • Xcode 12.0
  • iOS 14.0

教材コード

今回は、このCollectionViewをドラッグ&ドロップで並び替えできる様にします。

CompositionalLayout + DiffableDataSourceで作ったシンプルなCollectionViewのコード (クリックで開封)
import UIKit

final class ViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    private enum Section {
        case main
    }

    private struct Item: Hashable {
        let color: UIColor
        let identifier = UUID()
    }

    private var items = [
        Item(color: .red),
        Item(color: .blue),
        Item(color: .green),
        Item(color: .brown)
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        setupCollectionView()
        setupCollectionViewDataSource()
    }

    private func setupCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemGroupedBackground
        view.addSubview(collectionView)
    }

    private func createCollectionViewLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        let layout = UICollectionViewCompositionalLayout(section: section)

        return layout
    }

    private func setupCollectionViewDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, item in
            cell.backgroundColor = item.color
        }

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
            (collectionView, indexPath, item) -> UICollectionViewCell? in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }

        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

1. UICollectionViewDragDelegateとUICollectionViewDropDelegateを有効にする

まず、UICollectionViewのドラッグ&ドロップを有効にするため、以下のコードを追加します。

collectionView.dropDelegate = self
collectionView.dragDelegate = self
collectionView.dragInteractionEnabled = true

2. UICollectionViewDragDelegateのcollectionView(_:itemsForBeginning:at)でドラッグを有効にする

collectionView(_:itemsForBeginning:at) に以下のコードを足します。

ちなみに、空の [UIDragItem]() を指定するとそのセルはドラッグできなくなります。
特定のセルをドラッグしたくない場合は、indexPathで指定してやると良いでしょう。

extension ViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let itemIdentifier = items[indexPath.item].identifier.uuidString as NSString
        let itemProvider = NSItemProvider(object: itemIdentifier)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
    }
}

3. UICollectionViewDropDelegateのcollectionView(_:dropSessionDidUpdate:withDestinationIndexPath:)で、自分のアプリからのドロップだけを有効にする

今回は、外部のアプリからのドロップは受付ず、自分のアプリからのドロップだけ受け付ける様にしておきます。
localDragSessionを見て、内部からのドロップだったら並び替えをし、外部からのドロップならキャンセルする様にしておきます。

extension ViewController: UICollectionViewDropDelegate {
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if session.localDragSession != nil {
            // 内部からのドロップなら並び替えする
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        } else {
            // 外部からのドロップならキャンセルする
            return UICollectionViewDropProposal(operation: .cancel)
        }
    }
}

4. UICollectionViewDropDelegateのcollectionView(_:performDropWith:)で、データソースを更新する

あとは、遷移元と遷移先のIndexPathを取得して、それを元にデータソースを更新するだけです。

func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        switch coordinator.proposal.operation {
        case .move:
            guard
                let destinationIndexPath = coordinator.destinationIndexPath,
                let sourceIndexPath = coordinator.items.first?.sourceIndexPath
            else { return }

            // データソースを更新する
            let sourceItem = items.remove(at: sourceIndexPath.item)
            items.insert(sourceItem, at: destinationIndexPath.item)

            var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
            snapshot.appendSections([.main])
            snapshot.appendItems(items)
            dataSource.apply(snapshot, animatingDifferences: false)
        case .cancel, .forbidden, .copy:
            return
        @unknown default:
            fatalError()
        }
    }

コード全文

コード全文も載せておきます。

CompositionalLayout + DiffableDataSource + DragDelegate + DropDelegateで作った並び替え可能なUICollectionViewコード (クリックで開封)
import UIKit

final class ViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!

    private enum Section {
        case main
    }

    private struct Item: Hashable {
        let color: UIColor
        let identifier = UUID()
    }

    private var items = [
        Item(color: .red),
        Item(color: .blue),
        Item(color: .green),
        Item(color: .brown)
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        setupCollectionView()
        setupCollectionViewDataSource()
    }

    private func setupCollectionView() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.dragDelegate = self
        collectionView.dropDelegate = self
        collectionView.dragInteractionEnabled = true
        collectionView.backgroundColor = .systemGroupedBackground
        view.addSubview(collectionView)
    }

    private func createCollectionViewLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        let layout = UICollectionViewCompositionalLayout(section: section)

        return layout
    }

    private func setupCollectionViewDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, Item> { cell, indexPath, item in
            cell.backgroundColor = item.color
        }

        dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) {
            (collectionView, indexPath, item) -> UICollectionViewCell? in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
        }

        loadDataSource(items: items)
    }

    private func loadDataSource(items: [Item]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.main])
        snapshot.appendItems(items)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

extension ViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        let itemIdentifier = items[indexPath.item].identifier.uuidString as NSString
        let itemProvider = NSItemProvider(object: itemIdentifier)
        let dragItem = UIDragItem(itemProvider: itemProvider)
        return [dragItem]
    }
}

extension ViewController: UICollectionViewDropDelegate {
    func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
        if session.localDragSession != nil {
            // 内部からのドロップなら並び替えする
            return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
        } else {
            // 外部からのドロップならキャンセルする
            return UICollectionViewDropProposal(operation: .cancel)
        }
    }

    func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
        switch coordinator.proposal.operation {
        case .move:
            guard
                let destinationIndexPath = coordinator.destinationIndexPath,
                let sourceIndexPath = coordinator.items.first?.sourceIndexPath
            else { return }

            // データソースを更新する
            let sourceItem = items.remove(at: sourceIndexPath.item)
            items.insert(sourceItem, at: destinationIndexPath.item)

            loadDataSource(items: items)
        case .cancel, .forbidden, .copy:
            return
        @unknown default:
            fatalError()
        }
    }
}

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

UICollectionViewLayoutを使ったときのUICollectionViewの描画サイクル

はじめに

こんにちは、iOSエンジニアの dayossi です。

家族が幸せになれるサービスを提供したいと思って、
HaloHaloという家族日記アプリをリリースしています。

今回は、いまさらながら
UICollectionViewの描画タイミングの全体像について
整理したいと思います。
( 詳しいコードなどは、ほかの方の記事がわかりやすいので
最後にご紹介しております )

CollectionViewのレイアウトをいじりたいけど…

CollectionViewのレイアウトを
もっといろいろカスタマイズしたいなーと思って
UICollectionViewLayoutで一からレイアウトを組んでみたら、

「どのタイミングでレイアウト更新したらいいんだっけ?」
「セルに載せたUILabelとかUIImageViewのデータ更新もしたいけど、
どのタイミングでセルの作成とか更新とかしてるんだ?」

と、途中からわからなくなってきたので整理しました。

UICollectionViewがCellを表示する大まかな流れ

大きな流れとしては
CollectionView全体のレイアウトを決めてから、セルを作る作業に入ること
が大事かなと思いました。

まず、最初にCollectionViewが作られるときは
以下の UICollectionViewDataSource のメソッド(濃いグレー)が呼ばれます。

ここでは、
画面いっぱいに映る数 + 次に表示される 画面半分だけの数の
CollectionViewCell( 以下 セル ) が作られます。

スクリーンショット 2020-09-23 20.18.17.png

なお、以下のメソッドはオプションなので
書かなくてもいいものです。
CollectionViewCell.prepareForReuse()
DataSource.prefetch() (iOS 10から追加)

この中で、レイアウトを指定するメソッドも呼ばれています。

呼ばれるタイミングは、
1. DataSource.numberOfItemsInSection の前からです。
(薄いグレー : UICollectionViewLayout のメソッドです)

スクリーンショット 2020-09-23 20.16.21.png

( DataSource.willDisplay() はオプションメソッドなので
記述しなくても動きます )

注:layoutAttributesForItem()のタイミングは、
  (初期設定時は厳密に)ハッキリとわかりませんでした。あくまで予測です。

以上から
CollectionView全体のレイアウトを決めてから、セルを作る作業に入る
という流れが見えると思います。


ここからCollectionViewを下へスクロールすると、
以下のようにメソッドが呼ばれます。
スクリーンショット 2020-09-23 20.46.29.png

DataSource.didEndDisplay() はオプションメソッドなので
記述しなくても動きます )

基本的な流れは変わりませんが、上に消えていったセルは
初期設定時に準備された 緑色のセル の次に格納されるのがポイントです。

UICollectionViewLayoutを再度呼び出したいとき

先ほどの図のように
先に作ったセルを再利用して、スクロールした先のセルは表示されるので

特に書き換える処理がなければ
消えたセルの内容がそのまま次に出てくるセルに使用されます。

スクリーンショット 2020-09-23 20.46.29.png

なので、表示したいものにあわせてレイアウトを変えたい場合は
もういちど計算しなおす必要がでてきます。

再利用するセルのレイアウトを変えたいときは、
collectionView.invalidLayout()を呼ぶ必要があります。

スクリーンショット 2020-09-23 20.46.42.png

DataSource.prepareForReuse()の中で
collectionView.invalidLayout()を呼ぶと、
次にセルを再利用する際に、レイアウトをスムーズに変更できると思います。

また、レイアウト・UILabelなどのプロパティUI表示も変更したい場合は
DataSource.prepareForReuse()の中で、更新対象のプロパティにnilを代入したあと

任意のタイミングで
collectionView.reloadData()
collectionView.reloadSections( )
のようなリロード処理を呼び出すと、まるっと対応可能です。

ただ、collectionView.reloadData()よりも
collectionView.reloadSections()
必要な部分だけ更新するやり方のほうが
メモリに優しい印象です。

終わりに

UICollectionViewのレイアウトは、iOS13から使用可能な
Compositional Layoutを使えば、
レイアウトのカスタマイズは結構カンタンにできます。
(私もコレに任せて、HaloHaloを作成しました)

基本的なところを改めて学び直してみると
OSバージョンの壁を超えて、より多くの方に
より使いやすいUIを考えられると思いました。

まだまだ未熟者なので、
「ここはこう考えたほうがいいよ!」など
温かいご指摘などを頂けますと幸いです。

最後までお読みいただき、ありがとうございます。

参考記事

ちょっと長いですが、はじめに見たほうが良かったです…!!
A Tour of UICollectionView (WWDC 2018)
What's New in UICollectionView in iOS 10 (WWDC 2016)

具体的に考えていく際に、とても参考になりました!!
[iOSDC 2016] iOS10のCollectionViewからライフサイクルが変わったので遊んでみた
【Swift】CollectionViewを再理解する
CollectionViewのカスタムレイアウトを作ってみた
UICollectionView の Layout で悩んだら
UICollectionViewのカスタムレイアウトの作り方

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

CIImage.cropped(to:)の結果が変なときに見てね。

例えばこの画像を
nekocyan458A4183_TP_V4.jpg
↓猫だけのCGRectで切り抜いたはずなのに、
スクリーンショット 2020-09-24 13.09.26.png
↓UIImageViewに表示すると、一部しか表示されなかったり、全然表示されなかったりする
スクリーンショット 2020-09-24 13.11.33.png

一旦CGImageにしてからUIImageにすると綺麗に表示されます。
原因はCore ImageとUIKitの座標系が一致しないせいらしいです。
cropped(to:)の公式ドキュメントに書いてました。

let context = CIContext()
let final = context.createCGImage(ciCroppedImage, from:ciCroppedImage.extent)

?


Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

iOSDC2020のday1に参加したセッションの内容と感想

トークセッション

4年間運用されて表示速度が低下した詳細画面を改善する過程で得た知見 by marty-suzuki

iOSDC Japan 2020 Day 1 Track B 10:50 - Speaker Deck
表示が遅くなった画面を、どのように計測して改善していったかの過程を実際のコードを交えて紹介
遅延初期化時のリコメンドを遅延ロードとか
画面回転のリロードとか

感想
前職はSNSで表示速度とかを、かなり気にして実装してたので、共感やなるほどと思うことが多く、自分も知ってることも知らないことも分かりやすく解説してたので、個人的には非常に面白かった

400種類のアプリを毎日ビルドする自動化の技術 by Kishikawa Katsumi

400種類のアプリを毎日ビルドする自動化の技術 - Speaker Deck
ヤプリは、クライアントごとに、デベロッパーが存在するので、それを手動で管理していると大変なので、自動化したという話

感想
実際に、多数のデベロッパーを管理するというのは、中々ないが、自動化のフローや考え方は非常に勉強になった
現在の業務では、多数でデベロッパー管理というのはないが、前職だとそういうサービスがあった(実際に携わってはいない)ので、今後のビジネス展開として十分あり得そうなので、何かしらここで学んだものを取り入れたいなと思った

Flutter移行の苦労と、乗り越えた先に得られたもの by 桐山圭祐

Add-to-appを使用して既存のプロジェクトにFlutterを段階的移行した話

感想
業務的には、1からFlutterを使うというより、既存のプロジェクトに追加するというのが、現実的だと思うので、具体的な方法を見ることが出来たのはよかった
SWiftUIも同じ宣言的UIなので、SwiftUIを見据えて、Flutterを導入するというものありな気がした

新規機能開発からモジュール分割を始めてみる by Ryo Izumi

新規機能開発からモジュール分割を始めてみる - Speaker Deck
マルチモジュール化の話
実際に、モジュール(フレームワーク)化の方法を丁寧に解説
注意点や発見したこと、メリデメなどTipsのような感じ

感想
実際に今の業務でもモジュール化した方がいいねという話はあがっているので、それの判断基準になりそうな話が聞けたのでよかった
XcodeGenは、他のセッションでもよく出てくるので、今のうちに導入は前向きに検討し、実際に導入することになったときのために、知識はもっといた方がいいなと思った

エラーアーキテクチャ設計について考える by きちえもん

エラーアーキテクチャ設計について考える/Thinking about error architecture design - Speaker Deck
エラーハンドリングを、iOSで改善した話とAndroidで改善した話
それぞれ別の話で、コードもそれぞれのコードでの紹介

感想
個人的には、1番このセッションが有用だった。今現在、まさにエラーハンドリングのリファクタをしているところなので、いろんなところに共感できつつ、点で学んでいた中途半端な知識が少し、線でつながった感じ
一部、Androidのコードで紹介していたが、分かりづらいということもなく、考え方は共通なので、逆にiOSでも同じロジックでいけそうだなとイメージ出来たのはよかった

メモ

考え方は、共通なので分かりづらいということもなく、とても分かりやすかった
以下、いくつか頭に残っているものを列挙
- Error型を、そのまま使うと、どんなエラーが起こり得るのか分かりづらい
- 結果の型を、任意のエラー型を指定できるように
- 個別のエラーを表現することができるようになった
- 任意のエラー型は、列挙型で起こり得るエラーを記述
- 固有エラーを、共通エラーと同じ上位で処理すると、全てのエラーを考慮しないといけないロジックになってしまう。
- 固有エラーは別の流れにしてViewModelで処理したほうがいい

一石二鳥: マルチモジュール化, ビルド速度快適化 by Saryong Kang

マルチモジュール化のコツとメリデメの話、bazelを用いたビルド最適化の話、テストの目的や考え方・Tipsなどの話

感想
1番、メモを撮った。モジュール編では、別のモジュール化をテーマにしたセッションをより概念的なところからの話のような感じで、モジュール化に対する理解が深まった
ビルド編では、Bazelというビルドツールを知らなかったので、構築が難しいのと、まだXcodeとの相性が悪そうだが、利用できるようになるといろいろなことが出来そう
テスト編では、テストコードを各基準や目的など、よりどんなテストを書けばいいかの理解が深まった

メモ

実践マルチモージュル編
モジュールを分ける基準
・ドメインロジックがグループに分けられる
・スコープがよく定義されている
・複数のアプリで適用可能
・アプリによって活性化される
・既存のモジュールと明確に違う
・長期間担当するチームが存在している

あとから分けるのは難しい
初期段階で分けた方がいい

スピード感と正しさを両立

ドメインごとの分離
各ドメイン3−4つのレイヤーに分離
Binding,Presentation,Data
Presentationレイヤーは、ドメインロジックと分けるメリットはあまりない

Dependency Inversion Principle 依存性逆転の原則
依存性逆転したほうが、分かりやすい
例えば、Repositoryのインターフェースを、プレゼンテーションレイヤーに書く

ビルド編
Bazel 
バックエンドからモバイルまで、幅広くビルドができるもの
Skylark Pythonのサブセット
分析段階のビルドは、LinuxでもOK、コンパイルはMacが必要

Bezelがつかわれない理由
最近のXcodeビルドが、そんなに遅くない
リモートビルドの設定の難易度が高い
Xcodeとの相性が超悪い

であれば
CIサーバーに導入を目標にする
podなどは、ビルドに埋め込む

Googleが方向性を変更、Appleは対応しない方針だったが、転換

テスト編
いいテストの条件
実際のアプリに期待している行動と一緒

目的は、行動の変化がない限り、更新しない

テスト作成の原則
テストが失敗したら、アプリも失敗しているのが理想

効率よくUIKitからSwiftUIへ移行する by josh

SwiftUIに移行したほうがいい理由やしない方がいいケース、SwiftUIに移行する上で大事なこと、SwiftUIに移行した時のメリットなどの話

感想
実際のコードの例を紹介というより、どういったケースや考え方で、SwiftUIへ移行した方がいいのかという内容だったので、逆に、SwiftUIの情報が少ない自分には、ちょうどよかった。SwiftUIの導入の判断基準としては役に立ちそう

メモ

SwiftUIに移行した方が良い理由
UIKitはどんどん複雑になっている。今後はSwiftUIが主流になる

SwiftUIに移行しない理由
安定しない、バグが多い
パフォーマンスによっては、SwiftUIの方が悪い場合がある
ビジネスの優先順位

SwiftUIに移行する上で大事なこと
Objective-CからSwiftに移行しておく
コードもSwiftらしいコードにしておく
FRPに触れておく

SwiftUIに移行した時のメリット
AutoLayoutに対応
SafeAreaの概念への対応
Storyboardがスリムになる
ダークモードへの対応
宣言的なUIKitのAPIを使える

SwiftUIにマッチするアーキテクチャ
Redux
TCA
MVVM

SwiftUIをどの画面から実装するのがいいか
二つのアプローチ
AppDelegateやTabBarControllerなど、上位のものを対応するのは、大変。下位のVCに依存している可能性がある

Tips
同じ画面で混ぜすぎないこと→UIKitもSwiftUIどちらも概念が違う。画面単位で実装する方がいい

LTセッション

Catalystに対応したアプリをリリースするまでのリジェクト集 by かっくん

Catalystに対応したアプリをリリースするまでのリジェクト集 #iosdc #a #lt/iosdc_2020_lt - Speaker Deck
既存のiOSアプリをCatalyst化してリリースする際の苦労話
iOSとMacアプリでは、審査の基準が違うようで、単純にCatalyst化しただけではダメらしい

感想
Catalystについては、あまり情報がなかったので、これを機に知れたのはよかった
業務でもビジネス展開によっては、ありえるので、キャッチアップしておく必要がありそう

In App Purchaseのこれからの在り方を考える by Yuki Yamamoto

最近、トレンドになっているIn App Purchaseの件
自社(hey)とフォートナイトの話を交えて、今後のありかたを訴えるような内容でした

感想
個人的には、Appleとフォートナイトの件は、気になっていたので、似たような事例の話を聞けてよかった
今後は、アップルがガイドラインを変更したように、バグ修正以外の違反では即時リジェクトしないと言っている(実際、どんな運用になるかは分からないが)ので、今後の審査の変化はキャッチアップしていく必要がある

iOS Custom Keyboards でできること/できないこと/やってはいけないこと by Kyome

iOS Custom Keyboardsでできること/できないこと/やってはいけないこと / iOSDC Japan 2020 LT - Speaker Deck
iOSカスタムキーボード開発の話
実際の開発例をコード共に紹介
できること/できないことの他に、変わったキーボードも作れるよ

感想
デザインが未来的なキーボードや、罫線のみのキーボードや、楽器のキーボードなど、かなり自由に作れるんだなと分かって、実際に開発してみたい

DroidKaigiの公式アプリで始めるiOSアプリのOSSコミッターへの道 by 遠藤拓弥

DroidKaigiの公式アプリで始める_iOSアプリOSSコミッターへの道 - Speaker Deck
OOSコミッターをやってみたという話
最初のしくじりから反省し、こうすればいいよという内容

感想
OSSコミットは、やってみたいなと思うので、軽い気持ちでPR出したらダメだなという気づきと、やってみたいという気持ちになった

Apple Pencilと左利き対応 by ああうえ

Apple Pencilと左利き対応 - Speaker Deck
利き手を意識したUI設計の思考過程を紹介
HIGに基づいて検討し、実際に試行してもらったデータに基づき、決めていった

感想
実際の業務でも、アプリを片手で使うか、両手で使うかのような議論が出たこともあったので、単に机上の空論で決めるのではなく、実際の試行結果から、データに基づき決めていくのが重要だなと共感

macOS仮想カメラ「テロップカム」実装方法とその先 by 服部 智

macOS仮想カメラ「テロップカム」 実装方法とその先 - Speaker Deck
仮想カメラの実装の仕方を、実際のコードを元に紹介

感想
仮想カメラは、VRやARに興味を持っていたときに、いろいろ触っていたので、面白かった。ただ、以前、触っていたのは、別の言語とライブラリだったので、iOSでの実装例が見れてよかった

文字列をコピーできるスクリーンショットを作る by えんどう

文字列をコピーできるPDFを作るにはどうすればいいかの話

感想
あまり、、内容覚えてないな。。集中力が切れてたかも。汗

あなたのアプリ、✨リブランディング✨できますか? by monoqlo

あなたのアプリ、✨リブランディング✨できますか? / iosdc2020 - Speaker Deck
リブランディングを伴う複数回に及ぶリニューアルの話

感想
あまり、、内容覚えてないな。。集中力が切れてたかも。汗

iOSアプリを譲渡!?失敗は許されない一発勝負!予想外に立ち塞がる様々な罠に挑んだストーリー? by じんむ

iOSアプリを譲渡!? 失敗は許されない一発勝負! 予想外に立ち塞がる 様々な罠に挑んだストーリー / ios app transfer - Speaker Deck
iOSアプリを譲渡申請したときの、苦労話

感想
アプリ譲渡に関しては、前職で過去に検討したことがあって、実際に譲渡申請はやらなかったが、だいぶ大変そうだなというのは、感じていたので、実際の苦労話を交えて、注意するポイントなどが聞けたので、自分の知識の一つとしていい話が聞けた
スピーカーの話し方が、絵本を読んでくれてるような話し方で、とても面白かった

その他の見たセッションで感想

ここに書いてないセッションの感想を、以下のページに分けて、書きましたので、こちらもどうぞ
iOSDC2020のday0に参加したセッションの内容と感想 - Qiita
iOSDC2020のday2に参加したセッションの内容と感想 - Qiita
iOSDC2020に参加して3日間で36のセッションを見ました - Qiita

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

iOSDC2020のday0に参加したセッションの内容と感想

トークセッション

SourceKit LSPをブラウザでコードを読むために活用する by Kishikawa Katsumi

SourceKit-LSPをブラウザでコードを読むために活用する - Speaker Deck
SourceKit-LSP というSwiftのコードを読むためのツールの紹介
VScodeでの使い方から、ブラウザで使う方法まで
感想
Appleがこういうツールを出しているのを始めて知った
ブラウザでコードが読めるというのは、今後役に立ちそう

今日から分かるAVAudioEngineの全て by meteor

今日から分かる AVAudioEngineの全て - Speaker Deck
AVAudioEngineを使ってのいろいろなTipsを紹介
音をいろいろミックスしたりなど、音をカスタムする
感想
スピーカーのLive感があって、聞きやすく分かりやすかった
業務では活用の機会がなさそうだが、個人的には

HomeKit 2020 by 所友太

HomeKit 2020 - Speaker Deck
HomeKitを実際のコードを元に実装の方法を解説
感想
コロナ禍でスマートホーム化をいろいろとやってたので、非常に興味があり、HomeKitを使うと、こんなに簡単に実装できちゃうんだなと感じた

J2ObjC を使って Java 資産を iOS 開発で使ってみた by うるし

iOSDC2020: J2ObjCを使ってJava資産 をiOS開発で使ってみた - Speaker Deck
J2ObjCを使って、共通ロジックをライブラリ化
感想
Swiftじゃなくて、Objectieve-Cへ変換されるので、今後のメンテナンスが厳しそうだけど、iOS/Androidのロジックの共通化としてよさそう
自然に共通ロジックを使う前提のソースコードになるので、必然的に可読性はあがりそう

その他の見たセッションで感想

ここに書いてないセッションの感想を、以下のページに分けて、書きましたので、こちらもどうぞ
iOSDC2020のday1に参加したセッションの内容と感想 - Qiita
iOSDC2020のday2に参加したセッションの内容と感想 - Qiita
iOSDC2020に参加して3日間で36のセッションを見ました - Qiita

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

Unity iOSの内部フォルダの保存、閲覧する方法

はじめに

UnityでiOSの「フォルダ」アプリに保存したい場合の方法について記述します。実装方法よりXcodeの設定方法がメインとなります。

実装について

UnityのApplication.persistentDataPathを使用します。
永続的なデータを保存する場所として用意されているパス先です。

一点気をつけたいのが、
iCloudバックアップ領域に保存されるので、容量が大きい場合審査に落ちる場合があります。基本的にはiCloudにはバックアップしないという設定をUnityメソッドで呼び出します。SetNoBackupFlagのパス先はバックアップ対象外となります。
余程大きくなるなら大人しくサーバにあげましょう。あくまで内部フォルダです。

using UnityEngine;
using System;
using System.IO;

public class save : MonoBehaviour
{
    private string _savePath;

    void Start()
    {
        //iCloudバックアップ不要設定
        UnityEngine.iOS.Device.SetNoBackupFlag(Application.persistentDataPath);
        //iOS   : /var/mobile/Containers/Data/Application/<guid>/Documents/Product名/hoge/
        //MacOS : /Users/user名/Library/Application Support/DefaultCompany/Product名/hoge/
        _savePath = Application.persistentDataPath + "/hoge/";
        Debug.Log(_savePath);
        Directory.CreateDirectory(_savePath);

        using (File.Create(_savePath + "test.txt")) {
            //今回は生成のみですが、記述、読込など
        }
    }
}

iOS側では「ファイル」アプリに保存されます。(Xcode設定するまでは見えません)
MacOS側では通常の状態では「ライブラリ」から隠しフォルダ設定になっているので「Command」+「Shift」+ 「.」のショートカットキーで閲覧可能になります。

Xcodeの設定について

展開したXcodeの設定画面で「Info」の中の「Custom iOS Target Properties」に
「Application supports iTunes file Sharing」
「Supports opening documents in place」
を追加してYESに設定します。

スクリーンショット 2020-09-24 11.18.18.png

項目の追加方法は、、、
+や-で追加と削除が行えます。
スクリーンショット 2020-09-24 11.24.32.png

まとめ

取り敢えずの保存はこれでできますが、保存方法はきちんと考えてユーザー側から見えていいデータなのか編集されても問題ないデータかなどを踏まえてお使いください。
データ確認時はいくらでも使って下さい。

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

[CoreML]CreateMLを用いたテキスト分類モデルの作成方法

投稿のポイント

今回Appleが提供している CreateMLを用いて、positive or negative を判別するテキスト分類モデルを作成したのでアウトプットします。

画像分類モデルに関する情報はある程度発見できたのですが、テキスト分類モデルに関する情報はあまり見つかりませんでしたので参考にしていただければと思います。

備考) この記事のコード記述箇所のファイル名の拡張子に.playground.swiftとありますが、実際のファイル名は.playgroundです。コードの可読性を高めるため(コードに色を付けたいため)にQiita上だけ.swiftを付けさせていています。ご了承ください。

目次

① テキストデータの作成
② CreateMLとテキストデータのインポート
③ トレーニングデータとテストデータの準備
④ テキスト分類の作成とトレーニング
⑤ 分類器の精度と評価
⑥ テキスト分類モデルをCoreMLとして保存

① テキストデータの作成

まずJSONまたはCSV形式でテキストデータを収集します。(今回はJSON)
内容はtextキー = テキストlabelキー = positive or negativeで、データテーブルを作成します。作成例は以下の通り。

sentiment_analysis_training.json
[
  {
    "text": "木村文乃 テレ朝連ドラで初主演",
    "label": "positive"
  },
  {
    "text": "Zeebra、不倫報道について謝罪",
    "label": "negative"
  },
  {
    "text": "レイズ・筒香が4号、5番DHで出場",
    "label": "positive"
  }
]
sentiment_analysis_test.json
[
  {
      "text": "Netflix 日本会員500万人突破",
      "label": "positive"
  },
  {
      "text": "買い物中の12歳も拘束 香港",
      "label": "negative"
  },
  {
      "text": "再び感染者増 活気失うハワイ",
      "label": "negative"
  }
]

機械学習の正確な評価を得ようとすると大量のデータが必要です。
今回はトレーニングデータ約1000件、テストデータ約50件用意しており、テストデータがトレーニングデータの約5%です。この比率が最も健全な比率だそうです。

なお、トレーニングデータとテストデータのテキストは全く別の内容で準備する必要があります。

② CreateMLとテキストデータのインポート

①のJSONファイルを作成したらいよいよ実装に入ります。

機械学習モデルはXcodeのplaygroundで作成します。
Xcodeを開いてFile/New/Playground/を新規で開き、macOS/Blank/Nextでファイル名を設定してプロジェクトを立ち上げます。

playgrondのデフォルト画面が表示されたら一度全てのデフォルトコードを削除して、import CreateML と import Cocoaをインポートします。

sample_playground.swift
import CreateML
import Cocoa

続いて、先ほど①で作成した2つのJSONファイルをplayground/Resourcesに配置し、Bundleメソッドを使いトレーニングデータと、テストデータのURL定義します。

sample_playground.swift
import CreateML
import Cocoa

//ここから新しい記述
guard let trainingDataFileURL = Bundle.main.url(forResource: "sentiment_analysis_training", withExtension: "json"),
      let testingDataFileURL  = Bundle.main.url(forResource: "sentiment_analysis_test", withExtension: "json") 
  else {
    fatalError("Error! Couldn't load resource files")
}

解説) guard文を使用しBundle.mainからJSONファイルをロードします。forResourceにJSONファイルのファイル名を、withExtensionに拡張子を記述し、trainingDataFileURLに代入。(testingDataFileURLも同様)

③ トレーニングデータとテストデータの準備

続いて、②で定義したtrainingDataFileURLtestingDataFileURLを使ってMLDataTableのインスタンスを作成します。

そして、MLDataTableのインスタンスとして定義されたtrainingDataTabletestingDataTableの中身を確認するため、文字列定数statusを用意します。

sample_playground.swift
import CreateML
import Cocoa

guard let trainingDataFileURL = Bundle.main.url(forResource: "sentiment_analysis_training", withExtension: "json"),
      let testingDataFileURL  = Bundle.main.url(forResource: "sentiment_analysis_test", withExtension: "json") 
  else {
    fatalError("Error! Couldn't load resource files")
}

//ここから新しい記述
do {
    let trainingDataTable = try MLDataTable(contentsOf: trainingDataFileURL)
    let testingDataTable  = try MLDataTable(contentsOf: testingDataFileURL)

    let stats = """
    ===================================================
    Entries used of training: \(trainingDataTable.size)
    Entries used of testing : \(testingDataTable.size)
"""
    print(stats)
} catch {
    print(error.localizedDescription)
}

コードを実行してみましょう!
image.png
このように文字列定数の中に、raw: 973, columns: 2とありますが、973件のテキストでカラムは2つというように、.sizeメソッドでMLDataTableの中身を確認することができます。

④ テキスト分類の作成とトレーニング

③で作成したtrainingDataTableのカラム名を指定して、MLTextClassifierのインスタンスを作成します。

sample_playground.swift
import CreateML
import Cocoa

guard let trainingDataFileURL = Bundle.main.url(forResource: "sentiment_analysis_training", withExtension: "json"),
      let testingDataFileURL  = Bundle.main.url(forResource: "sentiment_analysis_test", withExtension: "json") else {
    fatalError("Error! Couldn't load resource files")
}

do {
    let trainingDataTable = try MLDataTable(contentsOf: trainingDataFileURL)
    let testingDataTable  = try MLDataTable(contentsOf: testingDataFileURL)

    let stats = """
    ===================================================
    Entries used of training: \(trainingDataTable.size)
    Entries used of testing : \(testingDataTable.size)
"""
    print(stats)

    //ここから新しい記述
    let sentimentClassifier = try MLTextClassifier(trainingData: trainingDataTable, textColumn: "text", labelColumn: "label")
} catch {
    print(error.localizedDescription)
}

⑤ 分類器の精度と評価

④でトレーニングを終えたら、次は③で作成したtestingDataTable(テストデータのMLDataTableインスタンス)を使ってトレーニングしたモデルのパフォーマンスを評価します。

テスト用のデータテーブルをevaluation(on:)メソッドに渡すと、MLClassifierMetricsインスタンスが返されます。

▼以下公式ドキュメント参照

トレーニング中、Create MLはトレーニングデータのごく一部を別にしておき、これを使ってトレーニングフェーズ中にモデルの学習状況をバリデート(検証)します。バリデーションデータによって、トレーニングプロセスは、モデルがトレーニングを受けていないサンプルでのパフォーマンスを測定できます。バリデーションの精度に応じて、トレーニングアルゴリズムは、モデル内の値を調整したり、精度が十分であればトレーニングプロセスを終了したりします。分割はランダムに行われるため、モデルをトレーニングするたびに異なる結果になる場合があります。

トレーニングデータとバリデーションデータでモデルがどれだけ正確に実行したかを確認するには、モデルのtrainingMetricsプロパティclassificationErrorプロパティvalidationMetricsプロパティを使用します。

次に、文字列定数を作成し、トレーニング精度バリデーション精度評価精度、を可視化できるように実装します。

sample_playground.swift
import CreateML
import Cocoa

guard let trainingDataFileURL = Bundle.main.url(forResource: "sentiment_analysis_training", withExtension: "json"),
      let testingDataFileURL  = Bundle.main.url(forResource: "sentiment_analysis_test", withExtension: "json") else {
    fatalError("Error! Couldn't load resource files")
}

do {
    let trainingDataTable = try MLDataTable(contentsOf: trainingDataFileURL)
    let testingDataTable  = try MLDataTable(contentsOf: testingDataFileURL)

    let stats = """
    ===================================================
    Entries used of training: \(trainingDataTable.size)
    Entries used of testing : \(testingDataTable.size)
"""
    print(stats)

    let sentimentClassifier = try MLTextClassifier(trainingData: trainingDataTable, textColumn: "text", labelColumn: "label")

    //ここから新しい記述
    let evaluationMetrics = sentimentClassifier.evaluation(on: testingDataTable, textColumn: "text", labelColumn: "label")

    let trainingAccuracy   = (1.0 - sentimentClassifier.trainingMetrics.classificationError) * 100
    let validationAccuracy = (1.0 - sentimentClassifier.validationMetrics.classificationError) * 100
    let evaluationAccuracy = (1.0 - evaluationMetrics.classificationError) * 100

    let message = """
    ==========================================================
    Training   accuracy(トレーニング精度) : \(trainingAccuracy)
    Varidation accuracy(バリデーション精度): \(validationAccuracy)
    Evaluation accuracy(評価精度)     : \(evaluationAccuracy)
"""
    print(message)
} catch {
    print(error.localizedDescription)
}

⑥ テキスト分類モデルをCoreMLとして保存

モデルのパフォーマンスが十分になったら、Appで使えるように保存します。
まず、モデル作成後に配置するディレクトリのpathのインスタンスURL型でを作成してください。

次にwrite(to:metadata:)メソッドを使用してCoreMLモデルファイル(SentimentClassiifer.mlmodel)をディスクに書き込みます。なお、作者、バージョン、説明など、モデルに関する情報があれば、MLModelMetadataのインスタンスを作成してから。

sample_playground.swift
import CreateML
import Cocoa

guard let trainingDataFileURL = Bundle.main.url(forResource: "sentiment_analysis_training", withExtension: "json"),
      let testingDataFileURL  = Bundle.main.url(forResource: "sentiment_analysis_test", withExtension: "json") else {
    fatalError("Error! Couldn't load resource files")
}

do {
    let trainingDataTable = try MLDataTable(contentsOf: trainingDataFileURL)
    let testingDataTable  = try MLDataTable(contentsOf: testingDataFileURL)

    let stats = """
    ===================================================
    Entries used of training: \(trainingDataTable.size)
    Entries used of testing : \(testingDataTable.size)
"""
    print(stats)

    let sentimentClassifier = try MLTextClassifier(trainingData: trainingDataTable, textColumn: "text", labelColumn: "label")

    let evaluationMetrics = sentimentClassifier.evaluation(on: testingDataTable, textColumn: "text", labelColumn: "label")

    let trainingAccuracy   = (1.0 - sentimentClassifier.trainingMetrics.classificationError) * 100
    let validationAccuracy = (1.0 - sentimentClassifier.validationMetrics.classificationError) * 100
    let evaluationAccuracy = (1.0 - evaluationMetrics.classificationError) * 100

    let message = """
    ==========================================================
    Training   accuracy(トレーニング精度) : \(trainingAccuracy)
    Varidation accuracy(バリデーション精度): \(validationAccuracy)
    Evaluation accuracy(評価精度)     : \(evaluationAccuracy)
"""

    print(message)

    //ここから新しい記述
    let modelFileURL = URL(fileURLWithPath: "モデル作成後配置するPathを記述")

    let metadate = MLModelMetadata(author: "作成者の名前",
    shortDescription: "モデルの説明",
    version: "モデルバージョン(初回なら1.0でOK)")

    try sentimentClassifier.write(to: modelFileURL, metadata: metadate)

} catch {
    print(error.localizedDescription)
}

記述するコードは以上です。それではコードを実行してみましょう。
image.png
このように、MLDataTableのサイズトレーニングの実行分類器の精度と評価が表示され、最後の行でwrite(to:metadata:)メソッドを実行して機械学習モデルが作成されていることが確認できます。

最後に

テキスト分類モデル作成方法のアウトプットは以上です。
もし、不十分な説明や誤りを発見された場合はコメントでご連絡いただければ幸いです!

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

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

iOS14でのUIDatePickerの挙動について

iOS14でのUIDatePickerの挙動について

こんにちは。
今回は業務でiOS14対応を任されまして、
UIDatePickerについてを改修するにあたりいろいろ調べたので、メモ的に残しておきます。
誰かの役に立てば幸いです。

環境

macOS Catalina 10.15.6
Xcode Version 12.0
Simulator Version 12.0 iOS14 iPhone 11pro

何が変わった?

iOS13以下では、DatePickerには UIDatePickerStyleに

.inline

がありませんでしたが、iOS14で新たに追加されました。

public enum UIDatePickerStyle : Int {
    case automatic = 0
    case wheels = 1
    case compact = 2

    @available(iOS 14.0, *)
    case inline = 3
}

ざっと見る限り他の追加や変更などはなさそうです。

挙動

iOS14だと挙動がかなり変わったようなので、実際に動かして確認してみました。

Style

⚠︎automaticは省略します

  • compact (Timeなし)
@IBOutlet weak var datePicker: UIDatePicker!

 /// ...
datePicker.preferredDatePickerStyle = .compact
datePicker.datePickerMode = .date

compact.gif

  • compact (Timeあり)
datePicker.preferredDatePickerStyle = .compact
datePicker.datePickerMode = .dateAndTime

compact.gif

  • inline (Timeなし)
datePicker.preferredDatePickerStyle = .inline
datePicker.datePickerMode = .date

inline.png

  • inline (Timeあり)
datePicker.preferredDatePickerStyle = .inline
datePicker.datePickerMode = .dateAndTime

inline.png

datePickerModeを指定することでTimeありなしができます。

datePicker.preferredDatePickerStyle = .wheels

inline.png

こちらは従来の物と変わりません。

Mode

datePicker.datePickerMode = .countDownTimer

inline.png

datePicker.datePickerMode = .date

inline.png
タップするとカレンダーがポップアップされます

datePicker.datePickerMode = .dateAndTime

inline.png

inline.gif

datePicker.datePickerMode = .time

time.gif

TextFieldと組み合わせ

textFieldInputDatePicker.preferredDatePickerStyle = .automatic

time.gif

だいぶ見辛いですね。
バグっぽいです。

textFieldInputDatePicker.preferredDatePickerStyle = .inline

time.gif

これも潰れてしまいます。
いろいろ調べましたが、バグっぽいです。
おそらく対応してないっぽいです。
stack overflowにも外国人が質問してました。

textFieldInputDatePicker.preferredDatePickerStyle = .wheels

time.gif

wheelsが無難な気がします。

まとめ

今回はiOS14で変更されたUIDatePickerについていろいろ調べでみました。
iOSで調べましたが、Mac Catalyst 14.0でも同じです。
ちなみにサイズも任意に変更できます。
Apple公式文章から

You should integrate date pickers in your layout using Auto Layout. Although date pickers can be resized, they should be used at their intrinsic content size.

日付ピッカーは自動レイアウトを使用してレイアウトに統合する必要があります。日付ピッカーはサイズを変更することができますが、本来のコンテンツサイズで使用する必要があります。

とのことなので、おそらくKeyboardサイズにぶち込むと圧縮されてしまうのだと思います。
今後どのようになるのかわかりませんが、組み合わせ次第では今までとは違った使い方できるな〜と思いました。
次回はSwiftUIでも試してみます。

読んでいただきましてありがとうございます。
コメント書いてくれると泣いて喜びます。

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