20200924のSwiftに関する記事は12件です。

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で続きを読む

公式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で続きを読む

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で続きを読む

[自分用] ボウリングにお役立ちアプリ(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で続きを読む

【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で続きを読む

[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の挙動について(UIKit)

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でも試してみます。

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

SwiftUI版も書きました。
iOS14でのDatePickerの挙動について(SwiftUI)

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む