- 投稿日:2021-05-16T13:58:58+09:00
Core Hapticsを用いて触覚フィードバック機能つきのメトロノームアプリを作ってみる
Core Hapticsは、iOS 13から追加された振動と音声を統合的に制御するフレームワークです。Core Hapticsを用いることで振動機能のついたメトロノームを作成することができます。 1. はじめに 1.1 本記事で作成するメトロノーム 本記事では以下の機能がついた簡単なメトロノームを作成します。なお、UIの配置にはSwiftUIを用います。 BPMを表示する機能 ボタンによりBPMを1ずつ増やす機能 設定したBPMのテンポで音を繰り返し鳴らす機能 音に合わせてiPhone本体を振動させる機能 1.2 本記事のソースコード 本記事のソースコードは、https://github.com/Ossamoon/sample-haptics に掲載してあります。 1.3 Core Hapticsについて この記事では、Core Hapticsフレームワークの基本的な使い方は省略します。基本的な使い方を学びたい方は、Qiitaの記事『Core Haptics - カスタムハプティックパターンの作成と再生』および、Appleの公式ドキュメントを参考にしてください。 1.4 動作環境 Xcode 12.5 Swift 5.4 2. アプリの作成 2.1 プロジェクトの作成 Xcodeで新規プロジェクトを作成します。SwiftUIが使えるようInterfaceにはSwiftUIを指定し、Life CycleにはSwiftUI Appを指定します。 プロジェクトを作成するとContentView.swiftファイルが自動生成されます。このファイル内のContentView構造体は2.4 Viewの作成にて書き直します。 2.2 音源の取り込み メトロノームの音として利用するための外部音源をXcodeに取り込みます。今回はフリーの効果音を配布されている効果音ラボさんの音源を利用させていただきます。 ボタン・システム音のページに移動し、"決定、ボタン押下2"の音源をダウンロードしてください。 ダウンロードした音源はXcode上にドラッグアンドドロップすることでXcodeに取り込むことができます。場所はどこでもいいですが、ContentView.swiftと同じ階層が分かりやすいでしょう。後で使いやすいように、音源のファイル名をsound.mp3に変更しておきましょう。 2.3 Controllerの作成 振動機能を制御するコントローラーを記述していきます。ContentView.swiftと同じ階層に新規ファイルを作成し、ファイル名をHapticController.swiftとしてください。 HapticController.swiftを開き、以下のコードを記述します。 HapticController.swift import Foundation import CoreHaptics import AVFoundation class HapticController { // メトロノームのパラメーター var bpm: Double = 120.0 // AudioSession private var audioSession: AVAudioSession // 音声データに関わるパラメータ private let audioResorceNames = "sound" private var audioURL: URL? private var audioResorceID: CHHapticAudioResourceID? // HapticEngine private var engine: CHHapticEngine! // 端末がCore Hapticsに対応しているか private var supportsHaptics: Bool = false // HapticPatternPlayer private var player: CHHapticAdvancedPatternPlayer? // HapticEventのパラメーター private let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.4) private let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0) private var hapticDuration: TimeInterval = TimeInterval(0.08) // AudioEventのパラメーター private let audioVolume = CHHapticEventParameter(parameterID: .audioVolume, value: 1.0) private var audioDuration: TimeInterval { TimeInterval(60.0 / bpm) } init(){ // AudioSessionの設定 audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playback) try audioSession.setActive(true) } catch { print("Failed to set and activate audio session category.") } // 端末がCore Hapticsに対応しているかを調べる let hapticCapability = CHHapticEngine.capabilitiesForHardware() supportsHaptics = hapticCapability.supportsHaptics // 外部音源の取り込み if let path = Bundle.main.path(forResource: audioResorceNames, ofType: "mp3") { audioURL = URL(fileURLWithPath: path) } else { print("Error: Failed to find audioURL") } createAndStartHapticEngine() //この関数は下で定義 } // Engineの作成と開始 private func createAndStartHapticEngine() { // 端末の対応を確認 guard supportsHaptics else { print("This device does not support CoreHaptics") return } // AudioSessionを渡してEngineを作成 do { engine = try CHHapticEngine(audioSession: audioSession) } catch let error { fatalError("Engine Creation Error: \(error)") } // Engineをスタート do { try engine.start() } catch let error { print("Engin Start Error: \(error)") } } // メトロノームを再生 func play() { // 端末の対応を確認 guard supportsHaptics else { return } do { // Engineをスタート try engine.start() // HapticPatternを作成 let pattern = try createPattern() //この関数は下で定義 // Playerを作成(Advacedの方を利用していることに注意) player = try engine.makeAdvancedPlayer(with: pattern) player!.loopEnabled = true // 再生 try player!.start(atTime: CHHapticTimeImmediate) } catch let error { print("Haptic Playback Error: \(error)") } } // メトロノームを停止 func stop(){ // 端末の対応を確認 guard supportsHaptics else { return } // 停止 engine.stop() } // HapticPatternの作成 private func createPattern() throws -> CHHapticPattern { do { var eventList: [CHHapticEvent] = [] // AudioResorceIDを取得 audioResorceID = try self.engine.registerAudioResource(audioURL!) // eventListにHapticEventを加えていく eventList.append(CHHapticEvent(audioResourceID: audioResorceID!, parameters: [audioVolume], relativeTime: 0, duration: self.audioDuration)) eventList.append(CHHapticEvent(eventType: .hapticTransient, parameters: [sharpness, intensity], relativeTime: 0)) eventList.append(CHHapticEvent(eventType: .hapticContinuous, parameters: [sharpness, intensity], relativeTime: 0, duration: self.hapticDuration)) // HapticPatternを生成し返す let pattern = try CHHapticPattern(events: eventList, parameters: []) return pattern } catch let error { throw error } } } いくつかのポイントを詳しく説明します。 外部音源を利用するにはEngineにAudioSessionを渡す Core Hapticsで外部音源を利用するには、 AudioSessionのインスタンスを取得し、アクティブにする CHHapticEngine(audioSession: audioSession)のように、Engineのインスタンスを取得する際にオーディオセッションを渡す 外部音源のパスを取得し、AudioResorceIDを取得する AudioResorceIDを渡したHapticEventを生成し、それをHapticPatternに組み込む といった手順が必要になります。EngineはAudioSessionを渡さなくてもインスタンスが取得できるのですが、その場合は外部音源が再生できなかったり、音量が端末本体の音量と連動しなかったりします。 メトロノームアプリにTimerは使えない 一定の時間感覚で繰り返し処理を行う場合、一般的にはTimerが使用されます。しかしながら、Timerの時間間隔は厳密でなく、メトロノームアプリに採用してしまうとテンポの揺らぎを感じてしまいます。(少なくとも筆者は違和感を感じました) そこで、Core Hapticsに内蔵されたループ機能を用います。ループの長さはHapticPatternやHapticEventの長さに準拠するので、それらを調節することによってループの長さを調節できます。 ループ機能を使うにはmakeAdvancedPlayerメソッドでPlayer作成する HapticPatternPlayerを作るためのメソッドはmakePlayer(with:)とmakeAdvancedPlayer(with:)の2種類が用意されていますが、ループ機能を用いる時はAdvancedの方が必須となります。Advancedは、再生速度を変えたいときなどにも用いられます。 2.4 Viewの作成 ContentView.swiftファイル内のContentView構造体を以下のように書き直します。 ContentView.swift import SwiftUI struct ContentView: View { // UI表示に必要なパラメーター @State private var bpm: Int = 60 @State private var isPlaying: Bool = false // コントローラーのインスタンス private var hapticController = HapticController() var body: some View { VStack { Text("BPM") .font(.system(size: 20)) // BPMを表示 Text(String(bpm)) .font(.system(size: 68)) // "+"ボタン: タップするとBPMを1増やす Button(action: { self.bpm += 1 }, label: { Image(systemName: "plus.circle") .resizable() .scaledToFit() .frame(width: 60.0, height: 60.0) }) .disabled(isPlaying) // 再生・停止ボタン if isPlaying == false { Button(action: { hapticController.bpm = Double(self.bpm) hapticController.play() isPlaying = true }) { Text("Start") .font(.system(size: 60)) } } else { Button(action: { hapticController.stop() isPlaying = false }) { Text("Stop") .font(.system(size: 60)) } } } } } プレビューでUIの見た目を確認すると1.1 本記事で作成するメトロノームで紹介したスクリーンショットのような見た目になっていると思います。 2.5 実機で確認 Core HapticsをSimulatorでテストすることはできません。よって、想像通りのものができているか確認するためには、実機テストが必要不可欠になります。 お手元にiPhone 8以降の端末があれば、ぜひBuildして実機テストを行ってみてください。指定のBPMで音が鳴ると同時に端末が振動していれば完成です。 3. より深く知りたい方へ 筆者が作成したメトロノームアプリがApp Storeに公開されています。ソースコードも筆者のGitHubにて公開しているので、まずはApp Storeでダウンロードして使い心地を試した後、ソースコードをじっくり読んでみることをお勧めします。
- 投稿日:2021-05-16T11:34:43+09:00
FlutterでAdMobを表示(iOS/Android)
はじめに 今回はAdMobの公式パッケージを使って広告を表示できるようにします。 更新履歴 2021.5.16 初回投稿 環境 macOS Big Sur(11.2.3) Flutter (Channel stable, 2.0.6, on macOS 11.2.3 20D91 darwin-x64, locale ja-JP) Android toolchain - develop for Android devices (Android SDK version 30.0.3) Xcode - develop for iOS and macOS Chrome - develop for the web Android Studio (version 4.1) VS Code (version 1.56.0) 参考にしたサイト flutterアプリでfirebase_admobからgoogle_mobile_adsに切り替えてみた - Qiita package google_mobile_ads | Flutter Package やりたいこと iOS/AndroidでAdMobの広告を表示させたい。 WebではAdMob表示できないので一旦、無視 AdMobテスト用ID アプリID Android: ca-app-pub-3940256099942544~3347511713 iOS: ca-app-pub-3940256099942544~1458002511 広告ユニットID Android: ca-app-pub-3940256099942544/6300978111 iOS: ca-app-pub-3940256099942544/2934735716 なお、admobのプラグインにて、定数定義がありますので、 FirebaseAdMob.testAppIdやBannerAd.testAdUnitIdのように利用することも可能なようです。(試してません) 実装手順 Flutterでプロジェクト作成 null safetyにするため、pubspec.yamlのsdkを以下のように変更 pubspec.yaml environment: sdk: ">=2.12.0 <3.0.0" パッケージをpubspec.yamlに追加してインストール pubspec.yaml google_mobile_ads: ^0.12.2 iOSの設定 iOS/Runner/info.plistに以下を追加 info.plist <key>GADApplicationIdentifier</key> <string>ca-app-pub-3940256099942544~1458002511</string> 「ca-app-pub-3940256099942544~1458002511」はテスト用アプリIDとなりますので、本番の際はこちらを自分のアプリのアプリIDに変更してください。 Androidの設定 最小SDKバージョンの変更 android/app/build.gradle minSdkVersion 19 //19以上に変更 android/app/src/main/AndroidManifest.xmlに以下を追加 AndroidManifest.xml <meta-data android:name="flutterEmbedding" android:value="2" /> <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3940256099942544~3347511713" /> 「ca-app-pub-3940256099942544~3347511713」はテスト用アプリIDとなりますので、本番の際はこちらを自分のアプリのアプリIDに変更してください。 5.管理クラスの作成(参考サイトのソースコードのままですthx) ad_banner.dart import 'package:flutter/material.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'dart:io'; class AdBanner extends StatelessWidget { const AdBanner({ required this.size, }); final AdSize size; @override Widget build(BuildContext context) { final banner = BannerAd( size: size, adUnitId: BannerAd.testAdUnitId, listener: AdListener( onAdLoaded: (Ad ad) => print('Ad loaded.'), onAdFailedToLoad: (Ad ad, LoadAdError error) { print('Ad failed to load: $error'); }, onAdOpened: (Ad ad) => print('Ad opened.'), onAdClosed: (Ad ad) => print('Ad closed.'), onApplicationExit: (Ad ad) => print('Left application.'), ), request: AdRequest()) ..load(); return Container( width: banner.size.width.toDouble(), height: banner.size.height.toDouble(), child: AdWidget(ad: banner)); } // 広告ID static String get bannerAdUnitId { if (Platform.isAndroid) { return "ca-app-pub-3940256099942544/6300978111"; } else if (Platform.isIOS) { return "ca-app-pub-3940256099942544/2934735716"; } else { //どちらでもない場合は、テスト用を返す return BannerAd.testAdUnitId; } } } 6.実装 main.dart import 'package:flutter/material.dart'; import 'ad_banner.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); MobileAds.instance.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Admob Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('AdMob Demo'), ), body: Center( child: AdBanner(size: AdSize.banner), ), ); } } 7.動作確認 広告のサイズ AdSize width height banner 320 50 largeBanner 320 100 mediumRectangle 300 250 fullBanner 468 60 leaderboard 728 90 以上
- 投稿日:2021-05-16T04:05:44+09:00
[Flutter] Gitリポジトリからダウンロードしたプログラムでエラー
はじめに Windowsで作成したFlutterアプリをGitリポジトリを通してMacにダウンロード。 それを起動させようとしたらエラーが出てきた。 Firebase関連のプラグインのバージョンを変えたりして、試行錯誤していたら余計にハマった。 バージョンがあってないやら、ファイルがみつからないやら、、、、、 あまりにも適当にやりすぎてエラーを再現できないが、備忘録としてエラー回避できた方法をいくつか残す。 エラー内容 Error output from CocoaPods: ↳ [!] Automatically assigning platform `iOS` with version `9.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`. Exception: Error running pod install 対策① 1.ダウンロードしたプログラムファイルのプログラム名/ios/Runner.xcworkspaceをxcodeで開く 2.赤枠で囲んだ部分のバージョンをiOS 13.0に変更 3.xcodeを保存して閉じる 4.flutter run 対策② 1.flutter clean 2.flutter build 対策③ 1.flutter clean 2.flutter pub get 3.cd ios 4.pod install 5.pod repo update 6.pod update 7.cd .. 8.flutter run 対策④ 1.対策①の3までやる 2.対策③を全部やる 参考URL https://github.com/FirebaseExtended/flutterfire/issues/1979 https://dolphinetech.com/flutter/error-running-pod-install/ https://fluttercorner.com/solved-error-could-not-find-included-file-generated-xcconfig-in-search-paths-in-target-runner/ 最後に エラーが出るとどうしてもあわわなる、、、 落ち着いてエラー内容をちゃんと読めば何とかなるさ! 最初からMacで作ればいいじゃない
- 投稿日:2021-05-16T03:48:33+09:00
週刊 DICOMデコーダーを創る (3) シークエンス解読
SQ(シークエンス)の構造 DICOM規格日本語訳のPS3.5 p39-を読み解いていきます 図のように,SQのVRが来ると,その次の2byteは予約済み(普通は0x0000がはいっている)で,関係が無いので飛ばします. 続く4byteにデータ長が入っています. データ長は2種類あり, 種類 データ長のバイト列 概要 明示的長さ 0x00 01 13 50 (例) このように値が入っている場合はデータ長を示す 未定義長さ 0xFF FF FF FF データ長が明示されていない.この場合は,項目を区切るタグを目印によむ このような未定議長さの場合には,区切りタグを基に読む必要があります. タグ 意味 概要 FFFE, E0000 項目の開始 項目の開始に必要なタグ FFFE, E00D 項目の区切り 項目の長さが明示されている場合は省略される FFFE, E0DD SQの終わり シークエンスの長さが明示されている場合は省略される 今回のファイルで最初にSQが登場するのは,アドレス0x21C(540)からです. この部分を図示すると次の通りとなります. 実装 コーディング DicomData.swift func analyzeData(){ currentPosition = 128 + 4 // シークエンス用 var hierarchy = 0 while currentPosition < dicomData.count { let position = currentPosition let group = readUInt16() let element = readUInt16() // SQ用の特殊タグをチェックする if (group == 0xFFFE && element == 0xE000){ // 項目開始 let length = readUInt32() if length == 0xFFFFFFFF{ printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: "", length: length) hierarchy += 1 continue }else{ // TODO: 明示長さの場合は,指定の部位まで読んだら階層を下げる必要がある printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: "", length: length) currentPosition += Int(length) continue } }else if (group == 0xFFFE && element == 0xE00D){ // 項目終わり let length = readUInt32() // 0xFFFFFFFF printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: "", length: length) hierarchy -= 1 continue }else if (group == 0xFFFE && element == 0xE0DD){ // シークエンスの終わり let length = readUInt32() // 0xFFFFFFFF printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: "", length: length) hierarchy -= 1 continue } let vr = readChar(length: 2) if ["OB", "OW", "OF", "UT"].contains(vr){ // VRの続きの2byteは意味をなさないので飛ばす currentPosition += 2 let length = readUInt32() if length == 0xFFFFFFFF{ printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: vr, length: length) hierarchy += 1 }else{ printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: vr, length: length) currentPosition += Int(length) } }else if vr == "SQ"{ // SQでは,データ長さが0xFFFFFFFFの場合に注意 currentPosition += 2 let length = readUInt32() if length == 0xFFFFFFFF{ // 未定義長さ printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: vr, length: length) hierarchy += 1 }else{ // 明示長さ currentPosition += Int(length) printLog(hierarchy: hierarchy, position: position, group: group, element: element, vr: vr, length: length) } }else{ let length = readUInt16() print("\(String(repeating: " ", count: hierarchy))0x\(String(position, radix: 16).uppercased())(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length: \(UInt32( length))") currentPosition += Int( length) } } } (省略 readUInt16, readUInt32, readCharは変更なし) func printLog(hierarchy: Int, position: Int, group: UInt16, element:UInt16, vr:String, length:UInt32){ if length == 0xFFFFFFFF{ print("\(String(repeating: " ", count: hierarchy))0x\(String(position, radix: 16).uppercased())(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length:") }else{ print("\(String(repeating: " ", count: hierarchy))0x\(String(position, radix: 16).uppercased())(\(position)), tag: (\(String(format: "%04x", group)), \(String(format: "%04x", element))), VR: \(vr), Length: \(length)") } } 解読の流れ タグ(Group, Element)をよむ 特殊なタグの場合は個別処理 FFFE, E000 : 項目の始まりなので,階層を上げる FFFE, E00D : 項目の終わりなので,階層を下げる FFFE, E0DD : シークエンスの終わりなので,階層を下げる 上記の特殊タグの場合は続くVRは無いので,continueでループの最初に戻る VRを読む "OB", "OW", "OF", "UT"の場合は画像データが入っていたりする.いまはまだ読まないので,後日実装 SQの場合はシークエンスの始まり.長さは明示されている場合もあるし未定義の場合もある現時点では明示されている場合は,その長さ分すっ飛ばして解読を続けている 他のVRは2byteの長さを読む 実行結果 dicom file loaded 0x84(132), tag: (0002, 0000), VR: UL, Length: 4 0x90(144), tag: (0002, 0001), VR: OB, Length: 2 0x9E(158), tag: (0002, 0002), VR: UI, Length: 28 0xC2(194), tag: (0002, 0003), VR: UI, Length: 46 0xF8(248), tag: (0002, 0010), VR: UI, Length: 22 0x116(278), tag: (0002, 0012), VR: UI, Length: 8 0x126(294), tag: (0008, 0008), VR: CS, Length: 38 0x154(340), tag: (0008, 0016), VR: UI, Length: 28 0x178(376), tag: (0008, 0018), VR: UI, Length: 46 0x1AE(430), tag: (0008, 0020), VR: DA, Length: 8 0x1BE(446), tag: (0008, 0030), VR: TM, Length: 6 0x1CC(460), tag: (0008, 0050), VR: SH, Length: 0 0x1D4(468), tag: (0008, 0060), VR: CS, Length: 2 0x1DE(478), tag: (0008, 0070), VR: LO, Length: 0 0x1E6(486), tag: (0008, 0080), VR: LO, Length: 0 0x1EE(494), tag: (0008, 0081), VR: ST, Length: 0 0x1F6(502), tag: (0008, 0090), VR: PN, Length: 0 0x1FE(510), tag: (0008, 1030), VR: LO, Length: 0 0x206(518), tag: (0008, 1050), VR: PN, Length: 0 0x20E(526), tag: (0008, 2110), VR: CS, Length: 2 0x218(536), tag: (0008, 2112), VR: SQ, Length: 0x224(548), tag: (fffe, e000), VR: , Length: 0x22C(556), tag: (0008, 1150), VR: UI, Length: 28 0x250(592), tag: (0008, 1155), VR: UI, Length: 46 0x286(646), tag: (fffe, e00d), VR: , Length: 0 0x28E(654), tag: (fffe, e0dd), VR: , Length: 0 0x296(662), tag: (0009, 0010), VR: LO, Length: 14 0x2AC(684), tag: (0009, 1002), VR: OB, Length: 904 0x640(1600), tag: (0009, 1003), VR: OB, Length: 528 0x85C(2140), tag: (0009, 1005), VR: OB, Length: 24 0x880(2176), tag: (0010, 0010), VR: PN, Length: 12 0x894(2196), tag: (0010, 0020), VR: LO, Length: 8 0x8A4(2212), tag: (0010, 0030), VR: DA, Length: 8 0x8B4(2228), tag: (0010, 0040), VR: CS, Length: 2 0x8BE(2238), tag: (0018, 0060), VR: DS, Length: 0 0x8C6(2246), tag: (0018, 1063), VR: DS, Length: 2 0x8D0(2256), tag: (0018, 1152), VR: IS, Length: 0 0x8D8(2264), tag: (0018, 1155), VR: CS, Length: 2 0x8E2(2274), tag: (0018, 1500), VR: CS, Length: 0 0x8EA(2282), tag: (0018, 1510), VR: DS, Length: 4 0x8F6(2294), tag: (0018, 1511), VR: DS, Length: 2 0x900(2304), tag: (0019, 0010), VR: LO, Length: 16 0x918(2328), tag: (0019, 1030), VR: UL, Length: 4 0x924(2340), tag: (0020, 000d), VR: UI, Length: 54 0x962(2402), tag: (0020, 000e), VR: UI, Length: 54 0x9A0(2464), tag: (0020, 0010), VR: SH, Length: 0 0x9A8(2472), tag: (0020, 0011), VR: IS, Length: 2 0x9B2(2482), tag: (0020, 0013), VR: IS, Length: 0 0x9BA(2490), tag: (0020, 0020), VR: CS, Length: 0 0x9C2(2498), tag: (0021, 0010), VR: LO, Length: 16 0x9DA(2522), tag: (0021, 1013), VR: IS, Length: 2 0x9E4(2532), tag: (0028, 0002), VR: US, Length: 2 0x9EE(2542), tag: (0028, 0004), VR: CS, Length: 12 0xA02(2562), tag: (0028, 0008), VR: IS, Length: 2 0xA0C(2572), tag: (0028, 0009), VR: AT, Length: 4 0xA18(2584), tag: (0028, 0010), VR: US, Length: 2 0xA22(2594), tag: (0028, 0011), VR: US, Length: 2 0xA2C(2604), tag: (0028, 0100), VR: US, Length: 2 0xA36(2614), tag: (0028, 0101), VR: US, Length: 2 0xA40(2624), tag: (0028, 0102), VR: US, Length: 2 0xA4A(2634), tag: (0028, 0103), VR: US, Length: 2 0xA54(2644), tag: (0028, 1040), VR: CS, Length: 4 0xA60(2656), tag: (0028, 1090), VR: CS, Length: 4 0xA6C(2668), tag: (0028, 6040), VR: US, Length: 6 0xA7A(2682), tag: (0028, 6100), VR: SQ, Length: 0xA86(2694), tag: (fffe, e000), VR: , Length: 0xA8E(2702), tag: (0028, 6101), VR: CS, Length: 4 0xA9A(2714), tag: (0028, 6110), VR: US, Length: 2 0xAA4(2724), tag: (fffe, e00d), VR: , Length: 0 0xAAC(2732), tag: (fffe, e0dd), VR: , Length: 0 0xAB4(2740), tag: (0029, 0010), VR: LO, Length: 16 0xACC(2764), tag: (0029, 1000), VR: SQ, Length: 0xAD8(2776), tag: (fffe, e000), VR: , Length: 0xAE0(2784), tag: (0029, 0010), VR: LO, Length: 16 0xAF8(2808), tag: (0029, 1001), VR: US, Length: 4 0xB04(2820), tag: (0029, 1002), VR: US, Length: 50 0xB3E(2878), tag: (0029, 1003), VR: FL, Length: 4 0xB4A(2890), tag: (fffe, e00d), VR: , Length: 0 0xB52(2898), tag: (fffe, e0dd), VR: , Length: 0 0xB5A(2906), tag: (5000, 0005), VR: US, Length: 2 0xB64(2916), tag: (5000, 0010), VR: US, Length: 2 0xB6E(2926), tag: (5000, 0020), VR: CS, Length: 4 0xB7A(2938), tag: (5000, 0030), VR: SH, Length: 10 0xB8C(2956), tag: (5000, 0103), VR: US, Length: 2 0xB96(2966), tag: (5000, 0104), VR: US, Length: 0 0xB9E(2974), tag: (5000, 0105), VR: US, Length: 0 0xBA6(2982), tag: (5000, 0106), VR: US, Length: 0 0xBAE(2990), tag: (5000, 0110), VR: US, Length: 4 0xBBA(3002), tag: (5000, 0112), VR: US, Length: 2 0xBC4(3012), tag: (5000, 0114), VR: US, Length: 2 0xBCE(3022), tag: (5000, 3000), VR: OW, Length: 7680 0x29DA(10714), tag: (7fe0, 0010), VR: OB, Length: 0x29E6(10726), tag: (fffe, e000), VR: , Length: 384 0x2B6E(11118), tag: (fffe, e000), VR: , Length: 17912 0x716E(29038), tag: (fffe, e000), VR: , Length: 18680 0xBA6E(47726), tag: (fffe, e000), VR: , Length: 18644 0x1034A(66378), tag: (fffe, e000), VR: , Length: 17820 0x148EE(84206), tag: (fffe, e000), VR: , Length: 16848 0x18AC6(101062), tag: (fffe, e000), VR: , Length: 16308 0x1CA82(117378), tag: (fffe, e000), VR: , Length: 16020 0x2091E(133406), tag: (fffe, e000), VR: , Length: 16088 0x247FE(149502), tag: (fffe, e000), VR: , Length: 16280 0x2879E(165790), tag: (fffe, e000), VR: , Length: 16408 0x2C7BE(182206), tag: (fffe, e000), VR: , Length: 16568 0x3087E(198782), tag: (fffe, e000), VR: , Length: 16556 0x34932(215346), tag: (fffe, e000), VR: , Length: 16604 0x38A16(231958), tag: (fffe, e000), VR: , Length: 16536 0x3CAB6(248502), tag: (fffe, e000), VR: , Length: 16628 0x40BB2(265138), tag: (fffe, e000), VR: , Length: 16428 0x44BE6(281574), tag: (fffe, e000), VR: , Length: 16328 0x48BB6(297910), tag: (fffe, e000), VR: , Length: 16456 0x4CC06(314374), tag: (fffe, e000), VR: , Length: 16468 0x50C62(330850), tag: (fffe, e000), VR: , Length: 16528 0x54CFA(347386), tag: (fffe, e000), VR: , Length: 16576 0x58DC2(363970), tag: (fffe, e000), VR: , Length: 16648 0x5CED2(380626), tag: (fffe, e000), VR: , Length: 16684 0x61006(397318), tag: (fffe, e000), VR: , Length: 16788 0x651A2(414114), tag: (fffe, e000), VR: , Length: 16852 0x6937E(430974), tag: (fffe, e000), VR: , Length: 16960 0x6D5C6(447942), tag: (fffe, e000), VR: , Length: 17164 0x718DA(465114), tag: (fffe, e000), VR: , Length: 17376 0x75CC2(482498), tag: (fffe, e000), VR: , Length: 17428 0x7A0DE(499934), tag: (fffe, e000), VR: , Length: 17392 0x7E4D6(517334), tag: (fffe, e000), VR: , Length: 17240 0x82836(534582), tag: (fffe, e000), VR: , Length: 17216 0x86B7E(551806), tag: (fffe, e000), VR: , Length: 17272 0x8AEFE(569086), tag: (fffe, e000), VR: , Length: 17416 0x8F30E(586510), tag: (fffe, e000), VR: , Length: 17480 0x9375E(603998), tag: (fffe, e000), VR: , Length: 17568 0x97C06(621574), tag: (fffe, e000), VR: , Length: 17572 0x9C0B2(639154), tag: (fffe, e000), VR: , Length: 17676 0xA05C6(656838), tag: (fffe, e000), VR: , Length: 17612 0xA4A9A(674458), tag: (fffe, e000), VR: , Length: 17760 0xA9002(692226), tag: (fffe, e000), VR: , Length: 17716 0xAD53E(709950), tag: (fffe, e000), VR: , Length: 17800 0xB1ACE(727758), tag: (fffe, e000), VR: , Length: 17952 0xB60F6(745718), tag: (fffe, e000), VR: , Length: 17916 0xBA6FA(763642), tag: (fffe, e000), VR: , Length: 17984 0xBED42(781634), tag: (fffe, e000), VR: , Length: 18120 0xC3412(799762), tag: (fffe, e000), VR: , Length: 18112 0xC7ADA(817882), tag: (fffe, e000), VR: , Length: 18008 0xCC13A(835898), tag: (fffe, e000), VR: , Length: 17984 0xD0782(853890), tag: (fffe, e000), VR: , Length: 17908 0xD4D7E(871806), tag: (fffe, e000), VR: , Length: 17960 0xD93AE(889774), tag: (fffe, e000), VR: , Length: 18112 0xDDA76(907894), tag: (fffe, e000), VR: , Length: 18256 0xE21CE(926158), tag: (fffe, e000), VR: , Length: 18240 0xE6916(944406), tag: (fffe, e000), VR: , Length: 18168 0xEB016(962582), tag: (fffe, e000), VR: , Length: 18092 0xEF6CA(980682), tag: (fffe, e000), VR: , Length: 18052 0xF3D56(998742), tag: (fffe, e000), VR: , Length: 18008 0xF83B6(1016758), tag: (fffe, e000), VR: , Length: 18048 0xFCA3E(1034814), tag: (fffe, e000), VR: , Length: 17976 0x10107E(1052798), tag: (fffe, e000), VR: , Length: 18056 0x10570E(1070862), tag: (fffe, e000), VR: , Length: 18040 0x109D8E(1088910), tag: (fffe, e000), VR: , Length: 18068 0x10E42A(1106986), tag: (fffe, e000), VR: , Length: 17984 0x112A72(1124978), tag: (fffe, e000), VR: , Length: 18012 0x1170D6(1142998), tag: (fffe, e000), VR: , Length: 18024 0x11B746(1161030), tag: (fffe, e000), VR: , Length: 18048 0x11FDCE(1179086), tag: (fffe, e000), VR: , Length: 17940 0x1243EA(1197034), tag: (fffe, e000), VR: , Length: 17984 0x128A32(1215026), tag: (fffe, e000), VR: , Length: 17952 0x12D05A(1232986), tag: (fffe, e000), VR: , Length: 18048 0x1316E2(1251042), tag: (fffe, e000), VR: , Length: 17996 0x135D36(1269046), tag: (fffe, e000), VR: , Length: 18040 0x13A3B6(1287094), tag: (fffe, e000), VR: , Length: 18048 0x13EA3E(1305150), tag: (fffe, e000), VR: , Length: 17936 0x143056(1323094), tag: (fffe, e000), VR: , Length: 18020 0x1476C2(1341122), tag: (fffe, e000), VR: , Length: 18112 0x14BD8A(1359242), tag: (fffe, e000), VR: , Length: 18200 0x1504AA(1377450), tag: (fffe, e000), VR: , Length: 18128 0x154B82(1395586), tag: (fffe, e000), VR: , Length: 18164 0x15927E(1413758), tag: (fffe, e000), VR: , Length: 18192 0x15D996(1431958), tag: (fffe, e000), VR: , Length: 18108 0x16205A(1450074), tag: (fffe, e000), VR: , Length: 18088 0x16670A(1468170), tag: (fffe, e000), VR: , Length: 18116 0x16ADD6(1486294), tag: (fffe, e000), VR: , Length: 18032 0x16F44E(1504334), tag: (fffe, e000), VR: , Length: 17976 0x173A8E(1522318), tag: (fffe, e000), VR: , Length: 18096 0x178146(1540422), tag: (fffe, e000), VR: , Length: 18016 0x17C7AE(1558446), tag: (fffe, e000), VR: , Length: 18032 0x180E26(1576486), tag: (fffe, e000), VR: , Length: 17996 0x18547A(1594490), tag: (fffe, e000), VR: , Length: 17964 0x189AAE(1612462), tag: (fffe, e000), VR: , Length: 17992 0x18E0FE(1630462), tag: (fffe, e000), VR: , Length: 17996 0x192752(1648466), tag: (fffe, e000), VR: , Length: 17952 0x196D7A(1666426), tag: (fffe, e000), VR: , Length: 17972 0x19B3B6(1684406), tag: (fffe, e000), VR: , Length: 17976 0x19F9F6(1702390), tag: (fffe, e0dd), VR: , Length: 0 このように,DICOMファイルの終端までエラーなく読むことができました. 0x29DA(10714), tag: (7fe0, 0010)に格納されている情報が画像データとなっています. 次回 次回以降,各tagに入っているデータをチェックしていきましょう なお,今回のコードでは,SQや項目タグの長さが明示されている場合にはうまく読み取れないかもしれません. 次回以降も,初回に記載したDICOMデータファイルで解読を進めます.
- 投稿日:2021-05-16T03:26:13+09:00
【Swift】サロンでRxSwiftを教えてもらったのでまとめてみた
はじめに 随時更新予定 今回は、オンラインサロンでRxSwiftを教えてもらっているので、その中で自分の学びになったことを簡潔にまとめてみたいと思います。 学んだこと ・どれがBLなのかを考える ・Result型で値を返すことができる enum CalculateError: Error { case numberToDivideIsZero } func calculate(num1: Double, num2: Double) -> Result<Double, CalculateError> { if num2.isZero { return .failure(.numberToDivideIsZero) } return .success(num1 / num2) } let num1 = 10.0 let num2 = 0.0 switch calculate(num1: num1, num2: num2) { case .success(let result): print(result) case .failure(let error): print(error) } ・メインスレッドで実行したいものには、Driverを使う(イベントをながすものはRelay) ・Driverをbindするときは、drive(onNext:)を使う ・Relayはaccept()でイベント通知する relay.accept("text") ・Alertなどはイベントとして、enumを用意する enum Event { case showAlert(String) } ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ ・ おわりに おわりです。
- 投稿日:2021-05-16T01:09:13+09:00
【Swift】MVPを簡単にまとめてみる
はじめに 今回は、MVPアーキテクチャについて簡単にまとめていきたいと思います。 MVPとは 特徴 ・Model, View, Presenterにより分割されるGUIアーキテクチャ ・データ同期する方法に、PassiveViewとSupervisingControllerの二つがある フロー同期とオブザーバー同期 まず、PassiveViewとSupervisingControllerを理解する前に、フロー同期とオブザーバー同期の説明をします。 フロー同期 上位モジュールのデータを下位モジュールに適宜セットして同期する同期方法 つまり、手続き的な同期方法。 メリット ・画面遷移間でデータの受け渡しなどでデータフローを追いやすい →上位モジュール(遷移先)が下位モジュール(遷移元)の参照を持つことができるため、有効 デメリット ・共通したデータを参照しているすべての箇所の参照を持っておかなければいけなくなるので、参照の管理が煩雑になりやすい。 オブザーバー同期 下位モジュール(監視元)が上位モジュール(監視先)からObserverパターンを使い送られてくるイベントを受け取ってデータを同期させる同期方法。 つまり、宣言的な同期方法である。 メリット ・共通した監視先を持つ複数の箇所でデータ同期をしやすい ・上位モジュールが下位モジュールの参照をもつ必要がなく、かつコンポーネント間の距離に関係なくデータ同期ができる。(互いに疎結合) デメリット ・データ変更のたびに同期処理が実行されるため、いつデータ同期が行われるのかが追いづらい。 Model MVC, MVVMなどもModelと同じ。 View View+ViewController Presenter ViewとModelの仲介役 プレゼンテーションロジックを担う 1Viewにつき、1Presenter作成することが多い PresenterからはViewをweak参照することが多い PassiveView Presenter→Viewでフロー同期を使う MVPのフロー同期例 myNameLabel.text = "REON" tableView.reloadData() Viewはすべてのユーザー入力イベントをPresenterに渡す。Presenterは入力に応じてプレゼンテーションロジックを処理して、Viewに対して手続き的な描画処理を出す。 ViewはPresenterの指示によってのみ描画処理を行い、Viewを基準とした描画処理は行わない。 SupervisingController フロー同期とオブザーバー同期の両方を使う Presenter→View間をフロー同期 Model→View間をオブザーバー同期する 監視元であるViewが監視先であるModelからNotificationCenterなどで送られるイベントを受け取って描画処理を行うことが例として挙げられる。 Viewは簡単なプレゼンテーションロジックを持つ。 Presenterは複雑なプレゼンテーションロジックを持つ。 ModelはPresenterによって呼び出され、Viewに対して描画に必要なデータをイベント通知によって受け渡しする。 SupervisingControllerはModel→View間にオブザーバーパターン同期を用いることで、処理を簡潔にかける。 おわりに MVPはPassiveViewかSupervisingControllerのどちらを使うのかがミソですね。