20210513のiOSに関する記事は7件です。

MacCatalystを使い方を解説

準備 Macのバージョン確認 まずMacのバージョンを確認します。 1.デスクトップ画面左上のAppleマークをクリック 2.「このMacについて」をクリック 3.開いたウィンドウでMacのバージョンを確認 ※Macのバージョンが10.15以降でなければMacCatalystで作ったアプリが動きません。 Xcodeのバージョン確認 次は、Xcodeのバージョン確認です。 もしXcodeがインストールされていなければApp Storeからダウンロードしてください。 1.Xcode.appを起動する 2.デスクトップ画面左上の「Xcode」をクリック 3.「About Xcode」をクリック 4.開いたウィンドウでXcodeのバージョンを確認 ! ※Xcodeのバージョンは11.0以降でなければMacCatalystが使用できません。 MacCatalystを使ってみる では、実際にMacCatalystを使ってアプリをMac対応させていきます。 使用した環境 開発環境  バージョン   MacOS   10.15.7   Xcode   11.3.1   MacCatalystの機能を有効にする 1.MacCatalystを使いたいアプリをXcodeで開く 2.プロジェクト設定の [TARGETS > General > Deployment Info > Mac] にチェック 3.アラートが出てくるので「Enable」をクリック ! 4.ビルド先がMy Macに設定されているのを確認して ▶ をクリックしてビルド 5.「Build Succeeded」が表示され、アプリが正常に動けば成功です      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MacCatalystで注意するべき3つのこと

iPad対応アプリしか移植できない MacCatalystでアプリをMacに移植するには、iPad対応が必須です。 iPhone対応アプリのMac移植をすることもできますが、その場合もiPad対応が必要になります。 移植元アプリの対応OS MacCatalystが使えるか iPhone & iPad 〇 iPhoneのみ ✕ iPadのみ 〇 デバイス間の仕様の違い MacとiPadでは様々な点で違いがあります。 移植元のアプリによっては以下の違いに注意する必要があります。 操作方法の違い Macではキーボード&マウス、iPadではタッチパネルのようにMacとiPadでは操作方法が違います。 MacCatalystでは自動でキーボード&マウス操作に対応してくれるので、基本的に気にする必要はないですが、特殊な操作方法を実装している場合等は注意が必要かもしれません。 内蔵カメラ・マイク・センサー等の有無 Macの場合、カメラやマイクを内蔵していないものもあります。 その場合でも外部接続でカメラやマイクは使えますが、ジャイロセンサーをはじめとした各種センサーはMacに非対応なので、iPadのセンサーを使った機能はMacでは実現できないものと考えた方がいいでしょう。 アプリ画面のサイズ iPadではアプリ画面がスクリーンのサイズで固定されますが、Macの場合はアプリ画面のサイズを自由に変更することができます。そのため、アプリ画面のレイアウトについて調整が必要になってくる場合もあります。 旧バージョンだと使用不可 開発する場合、開発環境はXcodeのバージョン11.0以降が必要で、アプリを動かすだけでも、macOS 10.15以降が必要になります。 アプリを旧バージョンのMacにも対応させたい場合は注意が必要です。     MacCatalystの注意点を解説しました。 次回はいよいよMacCatalystの使い方を説明していきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】Push通知用証明書とプロビジョニングプロファイルの作成(2021年版)

はじめに スマホで普段何気なく目にしているPush通知。 実はPush通知を利用するためにiOSもAndroidも事前準備が必要です。 テストでもPush通知の確認が必要なことがたくさんありますよね。 お客さんによってはコチラで準備をする必要があります。 そんな時に困らないように、Push通知を利用するために準備しなくてはならないものを書いてみます。 今回は、ややこしいiOS版の準備について書いてみます。 準備するもの ●Mac ※1 ●Apple Developer Programの登録(有料です) ※1 Mac OS 10.15以上推奨 作成の流れ 1.CSRファイルと開発用証明書の作成 2.AppIDの作成 3.プロビジョニングプロファイルの作成 4.APNs用の証明書作成 文字で書くのは簡単ですね(´・ω・`) では、実際に作成してみましょう。 CSRファイルと開発用証明書の作成 1.キーチェーンアクセスを起動 2.画面左上の「キーチェーンアクセス」というメニューから 認証局に証明書を要求 を選択 3.選択すると「証明書アシスタント」というのが開きます ━━━━━━━━━━━━━━ ●ユーザのメールアドレス: ご自身のメアドを入れましょう ●通称: デフォルトのまま。会社名とかAppleとか入れておきましょう(空欄だとエラーになるかも…) ●CAのメールアドレス: 空欄にしておいてください ●要求の処理: ディスクに保存を選択 鍵ペア情報にチェック ━━━━━━━━━━━━━━ 4.続けるを押す 下記のダイアログが表示されますが、デフォルトのまま進めます。  ※保存先を変更したい場合は「場所」を変更する 保存ボタンを押下すると、下記の「鍵ペア」を指定する画面に切り替わります。 コチラもデフォルトのままでOKです。  ※下記画像が恐らくデフォルト これで「CSRファイル」という開発用証明書を作成するために必要なファイルが作成できました。 CSRファイルって? 認証局に対して申込した本人です!というのを証明するためのファイル…とでも思ってください(´・ω・`) さて、次の工程にいきます。 5. Apple Developer Programにログイン 6.「Certificates, Identifiers & Profiles」を選択 7.「Certificates」を選択 8.「+」ボタンを押して「Create a New Certificate」を確認する ここまでは簡単ですね。 9.「iOS App Development」を選択して右上のContinueを押す 10.「Choose File」を押して「開発用証明書の作成で作成したCSRファイル」を参照する 11. 次画面でDownloadボタンを押して「cerファイル」を保存する これで証明書を作れました! ダウンロードしたファイルは、あとで使用するので大事に保管しておいてください。 AppIDの作成 ここが結構山場です(´・ω・`) 1. Apple Developer Programのホーム画面に戻る 2.「Identifiers」を選択して「+」ボタンを押す 3.「Register a New Identifier」から「AppID」を選択 4.「APP」を選択 5.「Description/Bundle ID」の指定 ●Description: アプリの名称?概要?みたいな捉え方で良いかなと。Pushtestとかアプリ名称入れておきましょう。 ●Bundle ID: とても重要です(´・ω・`) ビルド時にも設定するモノになるので、必ず何を設定したか覚えておきましょう。 6.「Capabilities」の中から「Push Notifications」を探してチェックを入れる  ※デフォルトはチェックが入っていません これにチェックを入れることで「Push通知を使います!」と設定をしていると思ってください。 作ったあとにやることは? 「Push Notifications」にチェックがきちんと入っているか確認する。 これ超重要です。。。 私、何度か入れ忘れやらかしてます(´・ω・`) コレやり直しになると結構めんどくさいので、必ず見直しすることをオススメします。 プロビジョニングプロファイルの作成 ここまで来たらあと一歩か二歩です ほんとめんどくさいですよね...。 1. Apple Developer Programのホーム画面に戻る 2.「Profiles」を選択して「+」ボタンを押す 3.「Register a New Provisioning Profiles」から「iOS App Development」を選択 これですね(´・ω・`) 4.「Generate a Provisioning Profile」で「AppID」を指定する 選択する際は英語の羅列を見て判断するより、AppID作成時に指定したDescriptionを頼りに選択すると良いです。 5.「Generate a Provisioning Profile」で事前に作成しておいた”開発用証明書”を選択 6. Provisioning Profile名を任意に入力して「Generate」を押す 7. Downloadして保存しておく これでプロビジョニングプロファイルの作成も完了! ダウンロードしたファイルは大事にデスクトップにでも置いておきましょう。 あと一歩です。 APNs用の証明書作成 1. Apple Developer Programのホーム画面に戻る 2.「Certificates」を選択して「+」ボタンを押す 3.「Service」の「Apple Push Notification service SSL (Sandbox)」を選択 4.「Create a New Certificate」で事前に作成した”CSRファイル”を選択 5. 次画面で「Download」を押してcerファイルをダウンロードしてデスクトップに保存 6. デスクトップのcerファイルをダブルクリック 7. キーチェーンアクセスが開く! これが最後の工程です。 8. キーチェーンアクセスに追加されていることを確認して該当のファイルを右クリック 9. 「~~~~を書き出す」を選択 10.「証明書」の名前を任意に入力して、ファイルの保存先を選択して「保存」を押す  ※フォーマットは変更しないでください 11. 次に証明書のパスワードを設定する画面が出るので任意に設定 「補足」 ここでパスワードを設定するとアプリのビルド時にパスワードが必要になります。 パスワード無しでも良い場合は空欄のまま進めてください。 12. システムからパスワードの入力要求があるので対応 これは現在Macにログオンしているユーザーのパスワードを入れてください。 証明書とは一切関係無いパスワードになります。 13. 指定の場所に保存された「p12ファイル」を確認 これでビルドする時に必要な「p12ファイルとプロビジョニングプロファイル」の作成が完了しました! 最後に作成したp12ファイルと、途中でダウンロードしたプロビジョニングプロファイルを使用して アプリをビルドしてみてください。 きっとPush通知が飛んでくると…思います(´・ω・`) 以上がiOSのPush通知用証明書とプロビジョニングプロファイルの作成方法になります! さいごに Push通知が飛んでこない… ビルドに失敗する… こんなことがよく発生します。 ●Push Notificationsにチェックが入っているか再確認 これ…ホントによくやります。 事前に確認しましょう! ●作った順番を間違えている 記事の順番通りに作成するのが基本です。 元から証明書など作成しているのであればSKIP出来ますが 最初のうちは全て新規に作成することをオススメします。 ●ビルド時にエラーになる Xcodeに表示されたエラー内容にもよりますが、 やっぱり「Push Notifications」のチェック入れ忘れが多いです。 証明書とプロビジョニングプロファイルが紐づいてないといったエラーも考えられますので、 ビルドした人にどんなエラーが出たのか確認しましょう。 他にも原因は考えられますが、大体は上記の3つが多いです。 あと最後になりますが、、、コレが一番重要かもしれませんw 作り方とか画面とかが勝手に変わる可能性があります(Appleの気まぐれですw) 大きく文言とかは変わらないと思うので、きちんと自分が作っているものを理解して対応しましょう! We look forward to your visit. 株式会社GENZではソフトウェアのテストサービス業務を行っています。 キャリア採用も行っておりますので、ご興味がある方は以下をご確認ください! 株式会社GENZ - スマホアプリ検証 株式会社GENZ - 採用情報
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS]写真へのアクセスレベル「追加のみ」「閲覧・追加両方」を1アプリ内に混在させる時どうなるか

基本事項 iOSアプリの写真(カメラロールなど)へのアクセス許諾についての基本は以下をご覧ください WWDC2020での許諾周りのアップデートまとめ 簡単にまとめると以下のようになります。 iOS13以前 写真へのアクセス権限は一種類(閲覧・追加両方)しかなく、その許諾ダイアログは二択のダイアログである。 許諾の状態をチェックする時はPHAuthorizationStatusを使う。 ユーザーに許諾を得る時はrequestAuthorization(_:)を使う。 許諾ダイアログの文言はNSPhotoLibraryUsageDescriptionが適用される。 iOS14 iOS14では写真へのアクセス権限として「追加のみ」の権限と「閲覧・追加両方」の権限がある。 「追加のみ」の権限を得る時の許諾ダイアログはやはり二択のダイアログとなる。例えば、画像保存機能の時はこれを使う事になる。 許諾の状態をチェックする時はauthorizationStatus(for: .addOnly)を使う。 ユーザーに許諾を得る時はrequestAuthorization(for: .addOnly)を使う。 許諾ダイアログの文言はNSPhotoLibraryAddUsageDescriptionが適用される。 「閲覧・追加両方」の権限を得る時は、三択のダイアログとなる。例えば、画像アップロード機能の時はこれを使うことになる。 許諾の状態をチェックする時はauthorizationStatus(for: .readWrite)を使う。 ユーザーに許諾を得る時はrequestAuthorization(for: .readWrite)を使う。 許諾ダイアログの文言は、iOS13以下と同じでNSPhotoLibraryUsageDescriptionが適用される。 iOS14で「追加のみ」「閲覧・追加両方」を1アプリ内に混在させられるが、するとどうなるか 例えば、1アプリ内に画像保存機能(追加のみ)と画像アップロード機能(閲覧・追加両方)が両方ある場合など。 許諾文言の整理 許諾文言はinfo.plistにNSPhotoLibraryUsageDescriptionとNSPhotoLibraryAddUsageDescriptionの二つの固定文言を書いていくことになる。 info.plistの内容をiOSのバージョンが13以下か14以降かによって動的に変えることはできない(はず)なので、全状況をカバーできる文言を入れていくしかない。※もし変える方法を知ってる場合は教えてください・・・ 以下のように表で整理するとわかるが、NSPhotoLibraryUsageDescriptionは画像アップロード機能と画像保存機能両方の時に表示されうるので、両方の機能についての説明を入れていかなければならないと考えられる。 一方、NSPhotoLibraryAddUsageDescription はiOS14で画像保存機能を使う時に表示される可能性があるだけであるから、画像保存機能についての説明さえ入っていればいいと考えられる。 画像アップロード機能(閲覧・追加両方) 画像保存機能(追加のみ) iOS13以前 NSPhotoLibraryUsageDescription NSPhotoLibraryUsageDescription iOS14 NSPhotoLibraryUsageDescription NSPhotoLibraryAddUsageDescription iPhoneの「設定」アプリ > 「写真」はどのような設定となるか ケース1 最初に画像保存機能を使用 -> その後画像アップロード機能を使用 画像保存機能を使う時、requestAuthorization(for: .addOnly)でユーザーに許諾を得る。許諾が得られた場合、iPhoneの「設定」アプリ > 該当アプリ > Photosを見てみると、以下のように追加のみか不許可かの二択になる。 その後、画像アップロード機能を使う時、requestAuthorization(for: .readWrite)でユーザーに許諾を得ることになる。iPhoneの「設定」アプリ > 該当アプリ > Photosを見てみると、以下のように「追加のみ」「特定の写真のみ」「全部の写真」「不許可」の四択となる。 ケース2 最初に画像アップロード機能を使用 -> その後画像保存機能を使用 最初requestAuthorization(for: .readWrite)でユーザーに許諾を得ることになる。許諾を得られると、設定アプリでは「特定の写真のみ」「全部の写真」「不許可」の三択となる。 その後、画像保存機能を使う時は、すでに閲覧・追加両方の権限が得られていることから、改めてユーザーに許諾を得る必要はないから、そのまま画像保存機能が使えることになる。 最終的に、順番によって「設定」の「写真」の選択肢が3つと4つで異なってくることになり、少し気持ち悪いが・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MacCatalystを使っているアプリを紹介!あなたも使っているかも

Twitter 言わずと知れた大手SNSアプリ。 一時期Mac版のアプリが廃止されていましたが、 MacCatalystアプリとして復活しました。 「MacもTwitterも使ってる!」という人は一度使ってみては? Twitter Apple Developper Apple関連の記事や動画を見ることができるアプリです。 同じ画面でアプリ開発をしながら情報収集もできるので、 Mac開発者からするとありがたいアプリです。 Apple Developer その他MacCatalystアプリ ・GoodNotes 5 メモ・PDF管理 ・Rosetta Stone 言語学習 ・LookUp 英語辞典 ・Jira Cloud プロジェクト管理 ・Dice by PCalc ダイスゲーム ・HabitMinder 健康習慣の管理 ・Firey Feeds フィードリーダー ・Zoho Sign 電子署名 ・Zoho Books 会計 ・Post-it 付箋アプリ ・King’s Corner トランプゲーム ・Triplt 旅行計画管理 参考: https://apps.apple.com/jp/story/id1479230371   MacCatalystアプリを紹介しました。 ほかにもMacCatalystを使ったアプリはまだまだあります。 あなたが何気なく使っているアプリも実はMacCatalystアプリだったり...? 次回はMacCatalystの注意点を解説します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】画面遷移で責務分離する

はじめに 今回は、ViewControllerで画面遷移をおこなうときに責務を分離して、変更に強くなるようなコードを紹介したいと思います。 以下のようなアプリを例として作ってみたいと思います。 GitHub 共通部分 以下のような階層にしました User struct User { let name: String let job: String } TaskListTableViewCell final class TaskListTableViewCell: UITableViewCell { @IBOutlet private weak var nameLabel: UILabel! @IBOutlet private weak var jobLabel: UILabel! var onTapEvent: (() -> Void)? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { onTapEvent?() } } // MARK: - setup extension TaskListTableViewCell { func setup(user: User, onTapEvent: (() -> Void)?) { self.onTapEvent = onTapEvent nameLabel.text = user.name jobLabel.text = user.job } } TaskListTableViewCell.xib TaskList.storyboard AddTask.storyboard UserInfo.storyboard 細かな制約は今回の趣旨と関係がないので、GitHubをクローンして確認していただくか、適当に制約はつけておいてください! 変更前 TaskListViewController final class TaskListViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private var users = [User]() override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.register(UINib(nibName: "TaskListTableViewCell", bundle: nil), forCellReuseIdentifier: "TaskListTableViewCell") tableView.tableFooterView = UIView() } @IBAction private func addButtonDidTapped(_ sender: Any) { let addTaskVC = UIStoryboard(name: "AddTask", bundle: nil).instantiateViewController(identifier: "AddTaskViewController") as! AddTaskViewController addTaskVC.onTapEvent = { user in guard let user = user else { return } self.users.append(user) self.tableView.reloadData() } addTaskVC.modalPresentationStyle = .fullScreen present(addTaskVC, animated: true, completion: nil) } } // MARK: - UITableViewDataSource extension TaskListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return users.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "TaskListTableViewCell") as! TaskListTableViewCell let user = users[indexPath.row] cell.setup(user: user) { [weak self] in self?.presentUserInfoVC(user: user) } return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 100 } private func presentUserInfoVC(user: User) { let userInfoVC = UIStoryboard(name: "UserInfo", bundle: nil).instantiateViewController(identifier: "UserInfoViewController") as! UserInfoViewController userInfoVC.user = user userInfoVC.modalPresentationStyle = .fullScreen present(userInfoVC, animated: true, completion: nil) } } AddTaskViewController final class AddTaskViewController: UIViewController { @IBOutlet private weak var nameTextField: UITextField! @IBOutlet private weak var jobTextField: UITextField! @IBOutlet private weak var backButton: UIButton! var onTapEvent: ((User?) -> Void)? @IBAction private func backButtonDidTapped(_ sender: Any) { guard let nameText = nameTextField.text, let jobText = jobTextField.text else { return } let user = (nameText.isEmpty || jobText.isEmpty) ? nil : User(name: nameText, job: jobText) onTapEvent?(user) dismiss(animated: true, completion: nil) } } UserInfoViewController final class UserInfoViewController: UIViewController { @IBOutlet private weak var nameLabel: UILabel! @IBOutlet private weak var jobLabel: UILabel! var user: User? override func viewDidLoad() { super.viewDidLoad() nameLabel.text = user?.name jobLabel.text = user?.job } @IBAction private func backButtonDidTapped(_ sender: Any) { dismiss(animated: true, completion: nil) } } 変更後 TaskListViewController final class TaskListViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private var users = [User]() override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.register(TaskListTableViewCell.nib, forCellReuseIdentifier: TaskListTableViewCell.identifier) tableView.tableFooterView = UIView() } @IBAction private func addButtonDidTapped(_ sender: Any) { let addTaskVC = AddTaskViewController.instantiate { user in guard let user = user else { return } self.users.append(user) self.tableView.reloadData() } addTaskVC.modalPresentationStyle = .fullScreen present(addTaskVC, animated: true, completion: nil) } } // MARK: - UITableViewDataSource extension TaskListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return users.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TaskListTableViewCell.identifier) as! TaskListTableViewCell let user = users[indexPath.row] cell.setup(user: user) { [weak self] in self?.presentUserInfoVC(user: user) } return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 100 } private func presentUserInfoVC(user: User) { let userInfoVC = UserInfoViewController.instantiate(user: user) userInfoVC.modalPresentationStyle = .fullScreen present(userInfoVC, animated: true, completion: nil) } } AddTaskViewController final class AddTaskViewController: UIViewController { @IBOutlet private weak var nameTextField: UITextField! @IBOutlet private weak var jobTextField: UITextField! @IBOutlet private weak var backButton: UIButton! private var onTapEvent: ((User?) -> Void)? @IBAction private func backButtonDidTapped(_ sender: Any) { guard let nameText = nameTextField.text, let jobText = jobTextField.text else { return } let user = (nameText.isEmpty || jobText.isEmpty) ? nil : User(name: nameText, job: jobText) onTapEvent?(user) dismiss(animated: true, completion: nil) } static func instantiate(onTapEvent: @escaping (User?) -> Void) -> AddTaskViewController { let addTaskVC = UIStoryboard.addTask.instantiateViewController( identifier: AddTaskViewController.identifier ) as! AddTaskViewController addTaskVC.onTapEvent = onTapEvent return addTaskVC } } private extension UIStoryboard { static var addTask: UIStoryboard { UIStoryboard(name: "AddTask", bundle: nil) } } UserInfoViewController final class UserInfoViewController: UIViewController { @IBOutlet private weak var nameLabel: UILabel! @IBOutlet private weak var jobLabel: UILabel! private var user: User? override func viewDidLoad() { super.viewDidLoad() nameLabel.text = user?.name jobLabel.text = user?.job } @IBAction private func backButtonDidTapped(_ sender: Any) { dismiss(animated: true, completion: nil) } static func instantiate(user: User) -> UserInfoViewController { let userInfoVC = UIStoryboard.userInfo.instantiateViewController(identifier: UserInfoViewController.identifier) as! UserInfoViewController userInfoVC.user = user return userInfoVC } } private extension UIStoryboard { static var userInfo: UIStoryboard { UIStoryboard(name: "UserInfo", bundle: nil) } } 解説 1番のポイントは、遷移元で遷移先の変更に応じて変更をする必要がなくなったところです。 例えば、変更後のコードは、TaskListViewController(遷移元)からAddTaskViewController(遷移先)に遷移しますが、AddTaskViewControllerに何かTaskListViewControllerから渡したいものが増えたとしても、AddTaskViewControllerを変更するだけですみます。TaskListViewControllerは余分な情報を持ちすぎなくてすみます。TaskListViεwControllerは、AddTaskViewControllerに遷移さえできればいいので、AddTaskViewControllerに何を渡すのかに関しては関心がありません。どうでもいいことです。なので、その情報はAddTaskViewControllerに直接持たせました。さらに、こうすることで、AddTaskViewControllerのonTapEvent変数はprivateをつけることができます。 以下のコードに関しては、AddTaskViewControllerでしか使わないことが保証されていることと、AddTaskViewControllerでしか必要がない情報なので、private extensionとすることでこのファイル内でしか扱えないようにしました。そもそも、extension UIStoryboardを作るかどうかは、好みなので、そこはお任せします。 private extension UIStoryboard { static var addTask: UIStoryboard { UIStoryboard(name: "AddTask", bundle: nil) } } 遷移先がUserInfoViewControllerの場合も同様です。 さらに、ハードコーディングをしないことも重要ですが、今回の趣旨と関係ないので、説明は省きます。以下の記事とGitHubなどを参考に、UITableViewCellのidなどをハードコーディングしないようにしてみてください! おわりに この記事がいいなと思っていただけたら、LGTMとストックをお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

週刊 DICOMデコーダーを創る (2)

本日はtag, VR, lengthを読み取ってみる 需要の程は不明ですが,知識の整理も兼ねて今日も作っていきます. 今日はtag, VR, 値長さを読み取るところまでやってみます. 前回はViewController.swiftにまとめていましたが,本格的に読み取り始めることにするので,新たにDicomDataクラスを作成します. DicomData.swift class DicomData{ var dicomData : Data! init(withData data:Data) { dicomData = data // dicomデータかチェック // ヘッダは128バイトのファイルプリアンブル // 続く4バイトにプレフィックスが続く if dicomData.count < 132{ print("file size error") return } if dicomData.getStringWithRange(start: 128, length: 4) != "DICM"{ print("file type error") return } print("dicom file loaded") } } データ要素の読み取り VR一覧 PS3.5 p23に一覧が記載されています. 各VRには対応するデータ内容がどのような値型で格納されているかを示しています. DICOM規格ではかなり厳密に区別されていますが,実際には値として入っているのは 数字・文字列・バイト列(バイナリ)だけです. VR 内容 備考 AE 文字列 AS 年齢を表す文字列 AT tag, elementを表す16bit符号なし整数 CS 文字列 DA 日付を表す文字列 DS 固定小数点か浮動小数点を表す文字列 DT 日時を示す文字列 FL 32bit浮動小数点数 FD 64bit浮動小数点数 IS 整数列 整数を表す文字列 LO 文字列 LT 文字列 OB byte列 ここにdataが入る * OF 浮動小数点ワードの列 * OW その他のワード列 dataが入る * PN 人名が入る文字列 SH 文字列 SL 32bit符号付き整数 SQ 項目のシーケンス * SS 16bit符号付き整数 ST 文字列 TM 時間を表す文字列 UI 文字列 UL 32bit符号なし整数 UN 内容が不明のバイト列 * US 16bit符号なし整数 UT 文字列 * 各データ要素は, group element VR 値長さ 値 1byte 1byte 2byte 2byte = UInt16 値長さで指定された分だけ で構成されているが,*をつけたOB, OW, OF, SQ, UT, UNは group element VR 予約領域(使用しない) 値長さ 値 1byte 1byte 2byte 2byte 4byte = UInt32 値長さで指定された分だけ で構成される点に注意が必要です. VRに続く2byteに値長さは格納されておらず,2byteあけて4byte分使って値が格納されています. これはdicomの各tagに対応するデータは日時であったり人名であったり,基本的に小さなデータを格納しているが,例えばOBやOWには16bit高解像度CTの画素値が入ったりするので,その容量を示すためには2byte長で表すことができる最大数(=16bit符号なし整数 max 65,535)となるので,データとして60KB程度のものしか格納できなくなります. そのため,OBやOWなど画素を収納する部分は4byteでデータ容量を表すことができ,4,294,967,295byte = 4GB程度のデータが格納可能となります. 関数作成 新たにDicomDataクラスを作ったので,そちらに各読み取り関数を実装していきます. 今回実装するのは, 指定した長さ分のバイト列→文字列へasciiテーブルで変換するクラス 16bit符号なし整数 32bit符号なし整数 現在読み取りしているファイルアドレスを移動させるseek関数 です. DicomData.swift var currentPosition : Int = 0 func readUInt16() -> UInt16{ let data = dicomData[currentPosition ... currentPosition+1].map{$0} currentPosition += 2 return UInt16(data[1]) << 8 + UInt16(data[0]) } func readUInt32() -> UInt32{ let data = dicomData[currentPosition ... currentPosition+3].map{$0} currentPosition += 4 return UInt32(UInt16(data[3])) << 24 + UInt32(UInt16(data[2])) << 16 + UInt32(UInt16(data[1])) << 8 + UInt32(UInt16(data[0])) } func readChar(length:Int) -> String{ let pos = currentPosition currentPosition += 2 return dicomData[pos...pos+length-1].map{String(Unicode.Scalar($0))}.joined() } func seek(offset : Int){ currentPosition += offset } readChar関数は前回と同様にString(Unicode.Scalar($0))で実装します. readUInt16 readUInt32はWindowsの.NET frameworkには存在するのですが,swiftでは見当たりませんでした(間違っていたらすいません). bit演算のシフトを用いて実装します. 例えば 16 A2 15 DAのバイナリが順に並んでいたとき,little endianであれば4byteとったときに DA 15 A2 16 = 3658850838と読み取る必要があります それぞれをbitで表すと DA(=218) 1101 1011 15(= 21) 0001 0101 A2(=162) 1010 0010 16(= 22) 0001 0110 です. これを左にそれぞれ24bit, 16bit, 8bitずらすことで 1101 1011 0000 0000 0000 0000 0000 0000 0000 0000 0001 0101 0000 0000 0000 0000 0000 0000 0000 0000 1010 0010 0000 0000 0000 0000 0000 0000 0000 0000 0001 0110 として,これらを足し合わせて(bit演算の四則演算は癖があるので確認してください) (今回は単純に + でいいですが) 1101 1010 0001 0101 1010 0010 0001 0110 = 0xDA15A216 とすることで得られます. 読み取り func analyzeData(){ currentPosition = 128 + 4 while currentPosition <= 1950 { let position = currentPosition let group = readUInt16() let element = readUInt16() let vr = readChar(length: 2) if ["OB", "OW", "OF", "SQ", "UT"].contains(vr){ // VRの続きの2byteは意味をなさないので飛ばす currentPosition += 2 let length = readUInt32() print("Address: 0x\(String(position, radix: 16))(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length: \(length)") currentPosition += Int(length) }else{ let length = readUInt16() print("Address: 0x\(String(position, radix: 16))(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length: \(length)") currentPosition += Int( length) } } } 今回は試しに1950バイト目まで読み取ってみます 格納されたデータの内容は読み飛ばして,tag, VR, lengthのみ読んでいます 出力は Address: 0x84(132), tag: (0002, 0000), VR: UL, Length: 4 Address: 0x90(144), tag: (0002, 0001), VR: OB, Length: 2 Address: 0x9e(158), tag: (0002, 0002), VR: UI, Length: 28 Address: 0xc2(194), tag: (0002, 0003), VR: UI, Length: 46 Address: 0xf8(248), tag: (0002, 0010), VR: UI, Length: 22 Address: 0x116(278), tag: (0002, 0012), VR: UI, Length: 8 Address: 0x126(294), tag: (0008, 0008), VR: CS, Length: 38 Address: 0x154(340), tag: (0008, 0016), VR: UI, Length: 28 Address: 0x178(376), tag: (0008, 0018), VR: UI, Length: 46 Address: 0x1ae(430), tag: (0008, 0020), VR: DA, Length: 8 Address: 0x1be(446), tag: (0008, 0030), VR: TM, Length: 6 Address: 0x1cc(460), tag: (0008, 0050), VR: SH, Length: 0 Address: 0x1d4(468), tag: (0008, 0060), VR: CS, Length: 2 Address: 0x1de(478), tag: (0008, 0070), VR: LO, Length: 0 Address: 0x1e6(486), tag: (0008, 0080), VR: LO, Length: 0 Address: 0x1ee(494), tag: (0008, 0081), VR: ST, Length: 0 Address: 0x1f6(502), tag: (0008, 0090), VR: PN, Length: 0 Address: 0x1fe(510), tag: (0008, 1030), VR: LO, Length: 0 Address: 0x206(518), tag: (0008, 1050), VR: PN, Length: 0 Address: 0x20e(526), tag: (0008, 2110), VR: CS, Length: 2 Address: 0x218(536), tag: (0008, 2112), VR: SQ, Length: 4294967295 です. 先程示したとおり,VRがOB, OW, OF, SQ, UTは挙動が違うので分けています. 現時点ではアドレスとVR,データ長が表示されているだけで,各tagが何を示すのかはわかりませんが,これは対応表があります. またそのうち実装しますが,今見えている中で大事なのは (0002, 0010)のUIが入っているデータで,UIなので文字列が入っています. 内容は,1.2.840.10008.1.2.4.50という文字列で,これも対応表がありますが, JPEG Baseline (Process 1) で圧縮転送していることを示しています. ここを読み取って,どのような方法で最終の画像データをデコードするのかを決めることになります. データ 転送形式 1.2.840.10008.1.2 Implicit VR Little Endian Default Transfer Syntax 1.2.840.10008.1.2.1 Explicit VR Little Endian Transfer Syntax 1.2.840.10008.1.2.1.99 Deflated Explicit VR Little Endian 1.2.840.10008.1.2.2 Explicit VR Big Endian 1.2.840.10008.1.2.4.50 JPEG Baseline (Process 1) 1.2.840.10008.1.2.4.70 JPEG Lossless, Non-Hierarchical, First-Order Prediction (Process 14 [Selection Value 1]) 1.2.840.10008.1.2.4.80 JPEG-LS Lossless Image Compression 1.2.840.10008.1.2.4.81 JPEG-LS Lossy (Near-Lossless) Image Compression 1.2.840.10008.1.2.4.90 JPEG 2000 Image Compression (Lossless Only) 1.2.840.10008.1.2.4.91 JPEG 2000 Image Compression 1.2.840.10008.1.2.4.100 MPEG2 Main Profile 1.2.840.10008.1.2.5 RLE Lossless 本来はすべての伝送方法に対応してデコードできるようにする必要がありますが, 現時点では上記から一部を実装する予定です. 今回のログですが,最初のほうは順調に読めていますが,最後のVRがSQの部分で変になってしまっています. このSQが曲者で,正しく実装するのはなかなか難しそうなのですが,データを階層化して入れ子のようにまとめるためのVRです. 今後どのように実装していくかはまだ悩み中ですが,次回はSQを関して実装していこうと思います. ではまた.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む