- 投稿日:2020-09-24T21:37:52+09:00
Swiftで取得した画像をWKWebView内のHTMLで表示する方法
WkWebviewのコンテンツ内でSwiftで取得したPNG画像などのデータを使いたいケースがあると思います。
本記事ではWebViewJavascriptBridgeを使用した、PNG画像のSwiftとJavaScript間での通信にて表示する方法について記載します。
ローカルのHTMLをWebViewで読み込み(Swift)
まずはWKWebviewでローカルのhtmlを読み込む。
Swiftclass 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へアクセスする受け口作成。Swiftself.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参照JavaScriptfunction 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側へ返却。
Swiftself.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タグで読み込み。JavaScriptbridge.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が表示される。
サンプルアプリ
- 投稿日:2020-09-24T20:01:08+09:00
公式YOLOv3物体検出モデルをiOSで使う手順。
アップル公式配布モデルをダウンロードして、物体検出してみます。
以下のような「人間」「自転車」「車」「バイク」。。。という80個の物体を認識して画像内の位置を教えてくれます。
手順
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を使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-09-24T19:12:37+09:00
SwiftUIで著名アプリのUIをトレース ~Spotify編~
iOS14とXcode12がリリースされ、SF Symbols2が使えるようになったので著名なアプリのUIをトレースするのが簡単になったなぁと思いレイアウトの勉強がてらSpotifyの再生画面をトレースしてみました。
完成品
厳密には背景がアートワークのグラデーションが薄っすらかかっていますが割愛もしもSpotifyがライトモードだったら
SwiftUIが自動でライトモードとダークモード対応してくれるので、こんなもしかしたらな画面も生成できました。
まぁライトモードでこれだと完全トレースとは言えないんですが。。。コード
PlayerView.swiftimport 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に移行していくのではないでしょうか。
- 投稿日:2020-09-24T18:25:10+09:00
【Swift】ちょっとはMVPという設計パターンに触れてみる
なぜ書いたか
これまでSwiftを使って色々アプリを作ってきましたが、
正直設計パターンというのをほとんど意識しませんした
基本的な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の設計パターンにそったサンプルコード
- 投稿日:2020-09-24T17:16:34+09:00
[自分用] ボウリングにお役立ちアプリ(Swift-iOSアプリ)
Swiftでアプリを作ってみた
Swiftの練習として, Userdefault, webAPI(Qiita), スクレイピングを利用したアプリを作成してみました.
アプリの内容としては, やくに立たないアプリよりも自分の役に立つアプリを作ろうと考えて趣味のボウリングに役立つアプリを作りました.githubのリポジトリはこちらです
https://github.com/yossi1118ubi/Bowling4機能まとめ
- webAPIを使って最近のボウリングに関するブログを検索する機能
- 自分で作成したボウリングに関する参考になる記事をまとめたスプレットシートから内容を検索して引っ張ってくる機能
- 一般的なメモ機能
- 行きつけのボウリング上の営業時間を表示する機能
//ホームが面の写真
それぞれの機能紹介
1. webAPIを使って最近のボウリングに関するブログを検索する機能
プロボウラーなの上げている最新のブログを簡単に探すことができるといいなと思い, webAPIを使ってボウリング関連のブログを取得する機能を作りました. 本当はnoteのAPIを使って情報を取得しようと思っていたのですが, APIが公式には公開されていないとのことで仕方なく
QiitaのAPIでボウリングに関する最新の記事を取得するアプリを作りました.結局, ボウリングに関する技術的な記事を返してくれる機能となってしまい全然嬉しくない機能となりました.
この機能を開くと初期ではボウリング関連の最新記事がtableViewに表示されます.
また, 好きなキーワードで検索をすることもできます. また, 特定のtableViewの行をタップするとSafariでその記事を見ることができるという機能も追加しました.//ここに写真をいれる
2. 自分で作成したボウリングに関する参考になる記事をまとめたスプレットシートから内容を検索して引っ張ってくる機能
個人的にボウリングで気になった動画や記事のタイトルとURLをスプレットシートに記録していたので, その記事を検索してひっぱってきてくれる機能が欲しいと考えました.
そこで, jsonを使ってスプレットシートの情報を獲得できるようにしました.
また, そのタイトルをタップするとそのURLの記事や動画をSafariで表示することができます.3. メモ機能
本当に一般的なメモ機能です. メモを保存書いて保存する機能, 保存したメモを見る機能, 保存したメモを削除する機能が搭載されています.
ここではUserdefaultの使い方を勉強しました.4. 行きつけのボウリングの営業時間を表示する機能
コロナで行きつけのボウリング上の営業時間が変わることがあったので, 簡単に確認できるような機能を作りました. この機能は, 行きつけのボウリング上のwebサイトからスクレイピングで営業時間を取得して画面に表示する機能です.
この機能を通して, スクレイピングの手法を学びました.
- 投稿日:2020-09-24T14:33:04+09:00
【iOS】DiffableDataSourceで作ったUICollectionViewに並び替え機能を実装する
UICollectionViewに並び替え機能を実装したい。
昔ながらのやり方ならば、UICollectionViewDataSourceを使った方法がありますね。
しかし、これからUICollectionViewを作るなら、UICollectionViewDiffableDataSource を使って書きたい。そんなときは、
UICollectionViewDragDelegate
とUICollectionViewDropDelegate
を使いましょう。こんな感じの並び替えUIを、シンプルなコードで実装できます。
環境
- 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 = true2. 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() } } }
- 投稿日:2020-09-24T14:33:04+09:00
【簡単】4つのステップでDiffableDataSourceで作ったUICollectionViewに並び替え機能を実装する
UICollectionViewに並び替え機能を実装したい。
昔ながらのやり方ならば、UICollectionViewDataSourceを使った方法がありますね。
しかし、これからUICollectionViewを作るなら、UICollectionViewDiffableDataSource を使って書きたい。そんなときは、
UICollectionViewDragDelegate
とUICollectionViewDropDelegate
を使いましょう。こんな感じの並び替えUIを、シンプルなコードで実装できます。
環境
- 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 = true2. 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() } } }
- 投稿日:2020-09-24T14:01:39+09:00
UICollectionViewLayoutを使ったときのUICollectionViewの描画サイクル
はじめに
こんにちは、iOSエンジニアの dayossi です。
家族が幸せになれるサービスを提供したいと思って、
HaloHaloという家族日記アプリをリリースしています。今回は、いまさらながら
UICollectionViewの描画タイミングの全体像について
整理したいと思います。
( 詳しいコードなどは、ほかの方の記事がわかりやすいので
最後にご紹介しております )CollectionViewのレイアウトをいじりたいけど…
CollectionViewのレイアウトを
もっといろいろカスタマイズしたいなーと思って
UICollectionViewLayoutで一からレイアウトを組んでみたら、「どのタイミングでレイアウト更新したらいいんだっけ?」
「セルに載せたUILabelとかUIImageViewのデータ更新もしたいけど、
どのタイミングでセルの作成とか更新とかしてるんだ?」と、途中からわからなくなってきたので整理しました。
UICollectionViewがCellを表示する大まかな流れ
大きな流れとしては
CollectionView全体のレイアウトを決めてから、セルを作る作業に入ること
が大事かなと思いました。まず、最初にCollectionViewが作られるときは
以下のUICollectionViewDataSource
のメソッド(濃いグレー)が呼ばれます。ここでは、
画面いっぱいに映る数 + 次に表示される 画面半分だけの数の
CollectionViewCell
( 以下 セル ) が作られます。なお、以下のメソッドはオプションなので
書かなくてもいいものです。
・CollectionViewCell.prepareForReuse()
・DataSource.prefetch()
(iOS 10から追加)この中で、レイアウトを指定するメソッドも呼ばれています。
呼ばれるタイミングは、
1.DataSource.numberOfItemsInSection
の前からです。
(薄いグレー :UICollectionViewLayout
のメソッドです)(
DataSource.willDisplay()
はオプションメソッドなので
記述しなくても動きます )注:
layoutAttributesForItem()
のタイミングは、
(初期設定時は厳密に)ハッキリとわかりませんでした。あくまで予測です。以上から
CollectionView全体のレイアウトを決めてから、セルを作る作業に入る
という流れが見えると思います。
ここからCollectionViewを下へスクロールすると、
以下のようにメソッドが呼ばれます。
(
DataSource.didEndDisplay()
はオプションメソッドなので
記述しなくても動きます )基本的な流れは変わりませんが、上に消えていったセルは
初期設定時に準備された 緑色のセル の次に格納されるのがポイントです。UICollectionViewLayoutを再度呼び出したいとき
先ほどの図のように
先に作ったセルを再利用して、スクロールした先のセルは表示されるので特に書き換える処理がなければ
消えたセルの内容がそのまま次に出てくるセルに使用されます。なので、表示したいものにあわせてレイアウトを変えたい場合は
もういちど計算しなおす必要がでてきます。再利用するセルのレイアウトを変えたいときは、
collectionView.invalidLayout()
を呼ぶ必要があります。
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のカスタムレイアウトの作り方
- 投稿日:2020-09-24T13:19:48+09:00
CIImage.cropped(to:)の結果が変なときに見てね。
例えばこの画像を
↓猫だけのCGRectで切り抜いたはずなのに、
↓UIImageViewに表示すると、一部しか表示されなかったり、全然表示されなかったりする
一旦CGImageにしてからUIImageにすると綺麗に表示されます。
原因はCore ImageとUIKitの座標系が一致しないせいらしいです。
cropped(to:)の公式ドキュメントに書いてました。let context = CIContext() let final = context.createCGImage(ciCroppedImage, from:ciCroppedImage.extent)?
Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。
- 投稿日:2020-09-24T09:55:19+09:00
[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.swiftimport CreateML import Cocoa続いて、先ほど①で作成した2つのJSONファイルを
playground/Resources
に配置し、Bundleメソッド
を使いトレーニングデータと、テストデータのURL定義します。sample_playground.swiftimport 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も同様)③ トレーニングデータとテストデータの準備
続いて、②で定義した
trainingDataFileURL
とtestingDataFileURL
を使ってMLDataTableのインスタンス
を作成します。そして、
MLDataTableのインスタンス
として定義されたtrainingDataTable
とtestingDataTable
の中身を確認するため、文字列定数status
を用意します。sample_playground.swiftimport 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) }コードを実行してみましょう!
このように文字列定数の中に、raw: 973, columns: 2
とありますが、973件のテキストでカラムは2つ
というように、.sizeメソッドで
MLDataTableの中身を確認することができます。④ テキスト分類の作成とトレーニング
③で作成した
trainingDataTable
のカラム名を指定して、MLTextClassifierのインスタンス
を作成します。sample_playground.swiftimport 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.swiftimport 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.swiftimport 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) }記述するコードは以上です。それではコードを実行してみましょう。
このように、MLDataTableのサイズ
、トレーニングの実行
、分類器の精度と評価
が表示され、最後の行でwrite(to:metadata:)メソッド
を実行して機械学習モデルが作成されていることが確認できます。最後に
テキスト分類モデル作成方法のアウトプットは以上です。
もし、不十分な説明や誤りを発見された場合はコメントでご連絡いただければ幸いです!最後までご覧いただきありがとうございました!
- 投稿日:2020-09-24T06:37:12+09:00
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 (Timeあり)
datePicker.preferredDatePickerStyle = .compact datePicker.datePickerMode = .dateAndTime
- inline (Timeなし)
datePicker.preferredDatePickerStyle = .inline datePicker.datePickerMode = .date
- inline (Timeあり)
datePicker.preferredDatePickerStyle = .inline datePicker.datePickerMode = .dateAndTimedatePickerModeを指定することでTimeありなしができます。
datePicker.preferredDatePickerStyle = .wheelsこちらは従来の物と変わりません。
Mode
datePicker.datePickerMode = .countDownTimerdatePicker.datePickerMode = .datedatePicker.datePickerMode = .dateAndTimedatePicker.datePickerMode = .timeTextFieldと組み合わせ
textFieldInputDatePicker.preferredDatePickerStyle = .automaticだいぶ見辛いですね。
バグっぽいです。textFieldInputDatePicker.preferredDatePickerStyle = .inlineこれも潰れてしまいます。
いろいろ調べましたが、バグっぽいです。
おそらく対応してないっぽいです。
stack overflowにも外国人が質問してました。textFieldInputDatePicker.preferredDatePickerStyle = .wheelswheelsが無難な気がします。
まとめ
今回は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)
- 投稿日:2020-09-24T06:37:12+09:00
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 (Timeあり)
datePicker.preferredDatePickerStyle = .compact datePicker.datePickerMode = .dateAndTime
- inline (Timeなし)
datePicker.preferredDatePickerStyle = .inline datePicker.datePickerMode = .date
- inline (Timeあり)
datePicker.preferredDatePickerStyle = .inline datePicker.datePickerMode = .dateAndTimedatePickerModeを指定することでTimeありなしができます。
datePicker.preferredDatePickerStyle = .wheelsこちらは従来の物と変わりません。
Mode
datePicker.datePickerMode = .countDownTimerdatePicker.datePickerMode = .datedatePicker.datePickerMode = .dateAndTimedatePicker.datePickerMode = .timeTextFieldと組み合わせ
textFieldInputDatePicker.preferredDatePickerStyle = .automaticだいぶ見辛いですね。
バグっぽいです。textFieldInputDatePicker.preferredDatePickerStyle = .inlineこれも潰れてしまいます。
いろいろ調べましたが、バグっぽいです。
おそらく対応してないっぽいです。
stack overflowにも外国人が質問してました。textFieldInputDatePicker.preferredDatePickerStyle = .wheelswheelsが無難な気がします。
まとめ
今回は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でも試してみます。読んでいただきましてありがとうございます。
コメント書いてくれると泣いて喜びます。