20210513のSwiftに関する記事は6件です。

[Swift5][Combine]PublisherがFailureを出力した後もストリームを途切れさせない方法の考察

モチベーション Publisherはライフサイクル上、一度Failureを返すと出力をやめてしまいます。 しかし一度Failureを返しても上流から値を流した時に引き続き値を出力し続けて欲しい場面がありました。 今更なトピックスですが、勘違いしてハマったりしたので思考の整理のためにまとめてみました。 PublisherがFailureを返した時の動作 以下のPublisherを使ってその挙動を再現してみます。 受け取った数字が0から3の範囲なら成功 受け取った数字が0から3の範囲外なら失敗 import Combine import Foundation var cancellables = Set<AnyCancellable>() // 引数が3以内なら成功、3以内じゃなければ失敗を返すPublisher func makeLengthCheckPublisher(number: Int) -> AnyPublisher<String, LengthError> { Future<String, LengthError> { promise in if 0...3 ~= number { return promise(.success("\(number)は3以内だからOK")) } else { return promise(.failure(LengthError.outBounds(number))) } } .eraseToAnyPublisher() } (1...6).publisher // 1から6を出力 .flatMap { makeLengthCheckPublisher(number: $0) } // AnyPublisher<String, LengthError>に変換 .sink( receiveCompletion: { result in switch result { case .finished: break case let .failure(error): print(error.localizedDescription) } }, receiveValue: { message in print(message) } ) .store(in: &cancellables) enum LengthError: Error { case outBounds(Int) } extension LengthError: LocalizedError { var errorDescription: String? { switch self { case .outBounds(let number): return "\(number)は範囲外だからNG" } } } 実行するとこんな結果になります。 1は3以内だからOK 2は3以内だからOK 3は3以内だからOK 4は範囲外だからNG 1~3までは成功し、4でFailureを出力して、以降値を出力しなくなっているのが分かります。 上流のPublisherは値を出力してることの確認 次に「Failureを返したPublisherの上流では値が出力されていること」の確認をしておきます。 もしかしたら上流から途切れてるかもしれないですからね。 さっきのコードを修正して、途中で出力をprintするように修正します。 (1...6).publisher .handleEvents(receiveOutput: { print("\($0)を出力します。") }) // handleEventを追加 .flatMap { ... 実行するとこんな結果になります。 1を出力します。 1は3以内だからOK 2を出力します。 2は3以内だからOK 3を出力します。 3は3以内だからOK 4を出力します。 4は範囲外だからNG 5を出力します。 6を出力します。 4が出力されてFailureが出てからも、上流では5と6を流していたことが分かりました。 Failureの出力後も途切れないようにする 本題です。 PublisherがFailureを出力すると、以降値を出力しなくなってしまうことが分かりました。 けどFailureを出力後も上流から値が流れてきたら同じくハンドリングしたいシーンもあります。 以下のやり方があります。 1. replaceError(with:)で失敗自体出力されないようにする 2. catchで別のPublisherに変換して出力を継続する 1.replaceError(with:)で失敗自体出力されないようにする replaceError(with:)はFailureが出力される時に成功時の値として出力するoperatorです。 (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .replaceError(with: "範囲外でした。") } .sink { message in print(message) } .store(in: &cancellables) 1は3以内だからOK 2は3以内だからOK 3は3以内だからOK 範囲外でした。 範囲外でした。 範囲外でした。 このハンドリング方法の問題はfailureのassociatedValueからErrorを受け取れないことです。 失敗しても成功として握り潰せる場面以外は使わないだろうなという感じです。 2.catchで別のPublisherに変換して出力を継続する catchはErrorを引数で受け取りつつ別のPublisherに変換して出力を継続できるOperatorです。 (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .catch { error -> Just<String> in return .init(error.localizedDescription) } } .sink { message in print(message) } .store(in: &cancellables) 1は3以内だからOK 2は3以内だからOK 3は3以内だからOK 4は範囲外だからNG 5は範囲外だからNG 6は範囲外だからNG catchだとErrorを受け取れるので、Errorをハンドリングしつつ流れを継続できます。 以下のようにcatchのクロージャでErrorを使ってハンドリングしても良いし .catch { [weak self] error -> Just<String> in self?.showAlert(message) return .init(error.localizedDescription) } handleEventでcompletionとしてFailureをハンドリングしておいて、catchはPublisherへの変換だけにするのも各Operatorの責務が明確になって良さそうです。 .handleEvents(receiveCompletion: { result in guard case let .failure(error) = result else { return } self?.showAlert(message) }) .catch { error -> Just<String> in return .init(error.localizedDescription) } このハンドリングで大体対応できると思います。 catchによるハンドリングのリスク ただcatchで変換したPublisherの出力がその後も流れてしまうので、catchとreceivedValueが両方実行されて、「失敗のハンドリングをしてるのに成功のハンドリングも実行される」という挙動になるリスクがあります。 以下の例に戻って考えてみます。 (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .catch { error -> Just<String> in return .init(error.localizedDescription) } } .sink { message in print(message) } .store(in: &cancellables) 1は3以内だからOK 2は3以内だからOK 3は3以内だからOK 4は範囲外だからNG 5は範囲外だからNG 6は範囲外だからNG 上記はcatchでStringを出力するJustを作って出力を続けさせているので、エラーが起きた後もsink(receivedValue: )が呼ばれています。 エラー時と正常時でsinkを共有して問題ないなら良いのですが、エラーが起きたらsinkには流したくない場面の方が多いと思います。 こんな時はOperaterでエラー後の出力をカットすればsinkに流れるのを防げます。 カットする方法はいくつかありますが、以下の二つが使いやすいと思います。 compactMap filter compactMapによる出力の制御 compactMapを噛ませればnilの時に出力させないようにできるので、Justで返す値をOptionalにしておき、エラーならnilを出力すればOKです。 (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .map { Optional($0) } .catch { error -> Just<String?> in print("catch: \(error.errorDescription ?? "")") return .init(nil) } } .compactMap { $0 } .sink { message in print("sink: \(message)") } .store(in: &cancellables) 実行してみるとFailureからnilを出力するJustに変換した時はsinkが呼ばれていないことが分かります。 sink: 1は3以内だからOK sink: 2は3以内だからOK sink: 3は3以内だからOK catch: 4は範囲外だからNG catch: 5は範囲外だからNG catch: 6は範囲外だからNG filterによる出力の制御 filterを噛ませると指定した条件がtrueの時だけ値を出力するようにできるのでcompactMapより柔軟に出力を排除できます。 ※出力に「OK」が含まれる時だけ通すようにした場合 (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .catch { error -> Just<String> in print("catch: \(error.errorDescription ?? "")") return .init("") } } .filter { $0.contains("OK") } .sink { message in print("sink: \(message)") } .store(in: &cancellables) sink: 1は3以内だからOK sink: 2は3以内だからOK sink: 3は3以内だからOK catch: 4は範囲外だからNG catch: 5は範囲外だからNG catch: 6は範囲外だからNG 通常時と失敗時で定義したenumに出力をラップしたりするとfilterが使いやすいかもしれません。 enumの名前適当だけどこんな感じでしょうか。 func makeLengthCheckPublisher(number: Int) -> AnyPublisher<PublishKind<String>, LengthError> { Future<PublishKind<String>, LengthError> { promise in if 0...3 ~= number { return promise(.success(.next("\(number)は3以内だからOK"))) } else { return promise(.failure(LengthError.outBounds(number))) } } .eraseToAnyPublisher() } enum PublishKind<T> { case next(T) case error var isError: Bool { if case .error = self { return true } else { return false } } } (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .catch { error -> Just<PublishKind<String>> in print("catch: \(error.errorDescription ?? "")") return .init(.error) } } .filter(\.isError) .sink { message in print("sink: \(message)") } .store(in: &cancellables) いややっぱりAnyPublisherでErrorを出力してるのにさらに.errorみたいなcaseにラップするのは冗長な気もしますね。。。 ビタッとくる方法が思いつきませんでしたが、オプショナルにしてcompactMapが手取り早くていいかもしれません。 [2021/05/14 追記] Emptyを使えばcatch後の出力を握り潰せる。 コメントで@iceman5499さんに教えていただきました。 compactMapやfilterで出力を排除しなくてもEmptyを出力すれば握り潰せます。 (1...6).publisher .flatMap { makeLengthCheckPublisher(number: $0) .catch { error in Empty() } } .sink { message in print(message) } .store(in: &cancellables) 1は3以内だからOK 2は3以内だからOK 3は3以内だからOK Emptyは値を出力しないPublisherです。 Emptyにしておけばわざわざnullableにしたり条件指定しなくても良いので楽だしコードも汚れないです。 catchでfailerのハンドリングをした後は基本Emptyを返しておこうと思いました。 まとめ PublisherがFailureを出力した後もストリームを続けたい場合は replaceError(with:)で成功したことにする。 catchで別のPublisherに変換して出力させる。 catch後にrecevedValueに値を流したくない場合は オプショナルにしてnil流してからcompactMapで排除する。 filterで条件指定して排除する。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

小学4年生の算数の宿題をSwiftを使って片付ける

みなさん、算数を楽しく学んでいますか? 算数は、パズルのような楽しさがありますが、それをプログラミングで解こうとすると、さらにパズルみたいで楽しいです。 問題 A君が今持っているお金は1234円です。 A君は、友だちのB君に、「今,お金いくら持ってる?」と聞かれたので、だいたいの金額で答えることにしました。 Aくんは、 はんぱな数を数えるのがめんどうです。ざっくりした数で答えてください。 百の位で切り捨てて,千の位までにした場合 十の位で切り捨てて,百の位までにした場合 一の位で切り捨てて,十の位までにした場合 答え 百の位で切り捨てて,千の位までにした場合 ⇒ 「1000円」 十の位で切り捨てて,百の位までにした場合 ⇒ 「1200円」 一の位で切り捨てて,十の位までにした場合 ⇒ 「1230円」 プログラミングで解決する この小学4年生の最初に学ぶ算数の問題を、MacのPlaygroundを使って、対話型アプリケーションにしてみましょう。 show("こんにちは") let name = ask("なまえ") show("どうも " + name + "くん" ) show( name + "くん" + "今、いくらお金持っている?") let number = askForNumber( name + "くん、あなたのお財布の中のお金を数えて、数字を入れてください") show("はんぱな数を数えるのがめんどうです。何桁で回答しますか?") var digits = askForChoice("何桁で回答しますか", strings: [ "百の位で切り捨てて,千の位で答える", "十の位で切り捨てて,百の位で答える", "一の位で切り捨てて,十の位で答える", "デバッグ用"] ) if digits == "百の位で切り捨てて,千の位で答える"{ var DoubleNum = ceil(Double(number/1000)) DoubleNum *= 1000 let intNum = Int(DoubleNum) let strNum = String("\(intNum)") show("ざっくり " + strNum + "円くらいだよ" ) }else if digits == "十の位で切り捨てて,百の位で答える"{ var DoubleNum = Double(Int(number)/100) DoubleNum *= 100 let intNum = Int(DoubleNum) let strNum = String("\(intNum)") show("ざっくり " + strNum + "円くらいだよ" ) }else if digits == "一の位で切り捨てて,十の位で答える"{ var DoubleNum = Double(Int(number)/10) DoubleNum *= 10 let intNum = Int(DoubleNum) let strNum = String("\(intNum)") show("ざっくり " + strNum + "円くらいだよ" ) }else{ show("DoubleNumは小数点を含む数字なので、整数でも小数点がつきます") show("整数を切り上げる関数はないので、一旦整数を小数点以下の数字にして、計算します") var DoubleNum = Double(Int(number)/100) show(DoubleNum + 0) show("intNumを戻します") DoubleNum *= 100 show(DoubleNum + 0) show("intNumは数字なので、足すことができます") let intNum = Int(DoubleNum) show(intNum + 0) show("strNumは文字なので、くっつけることができます") let strNum = String("\(intNum)") show(strNum + "") show("ざっくり " + strNum + "円くらいだよ" ) } さて、ここには、小学4年生が習う算数の初歩があるとともに、プログラミング初心者の基本が隠されていました。 定数と変数 Swiftでは、「型推論」といって宣言時に設定された値を元に自動で型を推測して設定されます。 PHPなどと違い型が動的に変わる「動的型付け」ではなく、あくまで「静的片付け」なので、宣言時に設定された型以外の値を入れようとするとエラーになります。 定数の宣言 letを使います let 定数名 = 値 //型を指定しない場合 //例 let num = 1 let str = "あいうえお" let 定数名:型 = 値 //型を指定する場合 //例 let num:Int = 1 let str:String = "あいうえお" 変数の宣言方法 varを使います var 変数名 = 値 //型を指定しない場合 //例 var num = 1 var str = "あいうえお" var 変数名:型 = 値 //型を指定する場合 //例 var num:Int = 1 var str:String = "あいうえお" 型変換 型というのは、例えば文字という型や、数字という型です。具体的に言えば、文字の1は"1"+"1"をすると、11という文字になりますが、数字の1+1は1+1=2と計算されて2という数字になります。 今回の、「切り捨て」のメインの計算は、以下になります。 var DoubleNum = Double(Int(number)/1000) DoubleNum *= 1000 let intNum = Int(DoubleNum) let strNum = String("\(intNum)") show("ざっくり " + strNum + "円くらいだよ" ) ここでは、数字で計算して、文字で出力する必要があるので、型変換をしました。 実際に、数字がどうなるかは、show で中身のデータを見ながら確認してみましょう (実際に+している0や””に文字を入れてみましょう) デバッグ用のプログラム show("DoubleNumは小数点を含む数字なので、整数でも小数点がつきます") show("整数を切り上げる関数はないので、一旦整数を小数点以下の数字にして、計算します") var DoubleNum = Double(Int(number)/100) show(DoubleNum + 0) show("intNumを戻します") DoubleNum *= 100 show(DoubleNum + 0) show("intNumは数字なので、足すことができます") let intNum = Int(DoubleNum) show(intNum + 0) show("strNumは文字なので、くっつけることができます") let strNum = String("\(intNum)") show(strNum + "") show("ざっくり " + strNum + "円くらいだよ" ) このような形で、文字と数字を扱うためには、型変換が必要になります。 型変換方法 型変換の例を以下に示します。 Int → String var intNum: Int = 11 var str: String = String(intNum) show(str + "という文字です") 出力結果: 11という文字です Int → Double var intNum: Int = 37 var doubleNum: Double = Double(intNum) show(doubleNum) 出力結果: 37.0 Double → String var doubleNum: Double = 1.234 var str: String = String("\(doubleNum)") show(str + "という文字です") 出力結果: 1.234という文字です Double → Int var doubleNum: Double = 1.234 var intNum: Int = Int(doubleNum) show(intNum) 出力結果: 1 String → Double var str: String = "10.1" var doubleNum: Double = atof(str) show(doubleNum + 1.1) 出力結果: 11.2 String → Int var str : String = "1234" var num : Int = Int(str)! show(num + 1) 出力結果: 1235 文字列の比較とIf-elseによる条件分岐 文字列を比較する場合は、if文用います。 elseと組み合わせることで、条件分岐を組み上げることができます。 //ここの文字を変えてみてください let string = "C" if string == "A" { show("Aです") } else if string == "B"{ show("Bです") }else{ show("Cです") } 小学生の問題をプログラミングで解くと、とっても勉強になりますね。
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Swift で Unity の macOS の Native Plugin を作る

Swift で Unity の macOS の Native Plugin を作る Unity は Native Plugin を作成することで、Unity から提供されていない、OS 固有の機能を使用することができます 例えば maxOS では、Bluetooth の利用などが出来るようになります こちらの記事では、Swift だけで、Unity の iOS の Native Plugin を紹介しています 上記の記事の iOS の Native Plugin のプロジェクトを拡張して、macOS 向けの Native Plugin を作成します iOS 実機向けと、macOS 向けの Native Plugin を同時に開発し、Editor での動作確認を実現することで、開発効率の向上を狙っています 今回は、iOS 向けに作成したプロジェクトをベースに、数字の文字列を整数型に変換する swiftPmPlugin_toNumber 関数を持つ macOS で利用可能な Native Pugin を作成します リポジトリはこちら↓です macOS の bundle をビルドする スタート時の workspace の様子 Swift で作った SwiftPM のライブラリと、動作確認用の Native のアプリターゲットを含むプロジェクトがあります アプリターゲットは今回は必須ではありません Bundle のターゲットを作成する macOS-Plugin project を作成しました project は何でも良いです File > New > Target から Bundle を選んで作成します ここでは、MacOsSwiftPmPlugin と命名しました Package のリンク bundle のターゲットを選択し、general から Frameworks に、SwiftPM のライブラリを追加します Header のリンク MacOsSwiftPmPlugin 内に PrefixHeader.pch を作成します @import {{Swift で作ったパッケージ名}}; を記述します #ifndef PrefixHeader_pch #define PrefixHeader_pch // Include any system framework and library headers here that should be included in all compilation units. // You will also need to set the Prefix Header build setting of one or more of your targets to reference this file. @import SwiftPmPlugin; // <- 追加 #endif /* PrefixHeader_pch */ ビルドをすると Bundle が出力されます nm コマンドを利用すると、swiftPmPlugin_toNumber のシンボルが含まれることを確認できます $ nm /path/to/bundle/MacOsSwiftPmPlugin.bundle/Contents/MacOS/MacOsSwiftPmPlugin | grep swiftPmPlugin_toNumber 0000000000001fe0 T _swiftPmPlugin_toNumber Unity に組み込む サンプルプロジェクトの Unity の Plugins/macOS/ に作成した MacOsSwiftPmPlugin.bundle を配置しました macOS の場合は、DllImport 時に、bundle 名を指定する必要があるため、#if で分けました using System; using System.Runtime.InteropServices; using UnityEngine; public class Cube : MonoBehaviour { #if UNITY_EDITOR_OSX [DllImport("MacOsSwiftPmPlugin")] #elif UNITY_IOS [DllImport("__Internal")] #endif private static extern long swiftPmPlugin_toNumber(string stringNumber); void Update() { Debug.Log("number: " + swiftPmPlugin_toNumber("30")); } } Unity Editor から 実行してエラーが出なければ、Native Plugin の作成成功です さいごに macOS の場合、一度 DllImport すると、Unity Editor を終了するまで保持されます bundle の差し替えだけだと処理が変わらないので注意してください ライブラリの開発時は、iOS 向けのライブラリと同様に、Native 向けのターゲットを Xcode で作成して、デバッグをすると効率的だと思います
  • このエントリーをはてなブックマークに追加
  • 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で続きを読む