- 投稿日:2021-03-02T23:30:52+09:00
【Xcode12】Libraryを常時表示させる方法
■はじめに
・この記事ではXcode12.4を使用しています。■方法
Libraryを常時表示するには、
『optionキーを押しながらLibraryボタンクリック』です。連続してObjectsを追加する際などに有用です。
- 投稿日:2021-03-02T21:55:07+09:00
firebaseを使って環境分けを設定する
firebaseを利用した環境分けをする際、てこづったのでここにまとめておく。
※今回はrelease、debugの2つに分ける。
※podやfirebaseのセットアップについては省く①まずfirebaseでアカウントを作り、プロジェクトを2つ作成する。(release用、debug用)
②両方のプロジェクトからGoogleService-Info.plistを取得する
③Xcodeのプロジェクトでフォルダをふたつ作り、分ける(debugのほうは名前を変える)
④環境分けするためのファイルを追加する
FirebaseUtil.swiftimport Foundation import FirebaseCore final class FirebaseUtil { static func setup() { #if DEBUG let name = "d_GoogleService-Info" #else let name = "GoogleService-Info" #endif let filePath = Bundle.main.path(forResource: name, ofType: "plist") if let options = FirebaseOptions(contentsOfFile: filePath!) { FirebaseApp.configure(options: options) } else { assertionFailure("Could'nt load config file") } } }⑤環境が分かれたか確認するクラッシュ用のViewControllerを追加する
CrashliticsViewController.swiftimport UIKit class CrashliticsViewController: UIViewController { @IBAction func tapCrash(_ sender: Any) { fatalError() } }※Storyboard上のボタンと繋いで、タップするとクラッシュします。
⑥TARGETのEditSchemeを開き、releaseかdebugで分けるだけ
⑦クラッシュの確認方法で少してこづったので、わかりやすい記事はこちら
⑧それぞれビルドし、クラッシュさせる
- 投稿日:2021-03-02T20:37:30+09:00
略語(頭字語、固有名詞)のSwiftyな命名規則
Swift標準APIのURL構造体の名前に疑問符が付いたので調べてみた。
Swift公式の命名規則としてAPI Design Guidelinesがあるが、ここでは略語に関して「標準的な略語以外は省略しないで書いてね」ぐらいしか書かれていない。よって、一般的な規則である「型とプロトコルの名前はUpperCamelCase、それ以外はlowerCamelCaseで書いてね」に沿ってUrl
になるはずだが、なぜかURL
になっているのだ。調べてみた結果帰納的に以下の規則で結論付けたのでまとめる。標準的な略語の命名規則
- 単語の最初の文字の種類に揃える
- ただし、IPv4とIPv6のvは必ず小文字
例
URL
CommandLineAPIHost
url
absoluteURL
IPv6Address
ipv6Settings証拠
- URLの型名とか初期化子とかメンバーとか
- XMLDTDの型名とかプロパティーとか
- UTTypeのSystem Declared Types
- WebKit JSのパッケージ名とかクラス名とか
- Identifiable.ID
例外
下三つに関しては表記可能な文字で代替するようだ。C++の表記揺れも見られるので統一して欲しい。
所感
IPv6が全てを狂わせた。おそらく"IP"で一つの単語として原則に従わせ、"v6"は可読性重視で固定にしているのだと思う。
先頭に来ないiOSやmacOSはどうなるんだろう?上記の規則だとIOS、MACOSだが。。。
- 投稿日:2021-03-02T20:05:40+09:00
TodoAPPでRxSwift入門[part3]
概要
最近RxSwiftを勉強し始めて現在理解していることを備忘録として残せたらいいなと思い記事にします。
そもそもRxSwiftのRxとはRx(Reactive X)とは、「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリです。
Rxを導入するメリットは、「値の変化を検知できる」「非同期の処理を簡潔に書ける」ということに尽きると思います。 値の変化というのは変数値の変化やUIの変化も含まれます。 例えばボタンをタッチする、という動作もボタンのステータスが変わったと捉えることができRxを使って記述することができます。とのことです。
詳しくは以下のサイトを参照してください。
入門!RxSwift
RxSwiftについてようやく理解できてきたのでまとめることにした(1)今回はPart3になります。
前回の記事はこちら
TodoAPPでRxSwift入門[part2]AddTodoViewController
前回の予告通りViewControllerの中身をみていきましょう。
AddTodoViewController.swiftimport UIKit import RxSwift import RxCocoa class AddTodoViewController: UIViewController { @IBOutlet weak var titleField: UITextField! @IBOutlet weak var detailView: UITextView! @IBOutlet weak var joinButton: UIButton! @IBOutlet weak var showTodoListButton: UIButton! let disposeBag = DisposeBag() private var viewModel: AddTodoViewPresentable! override func viewDidLoad() { super.viewDidLoad() viewModel = AddTodoViewModel(input: ( titleText: titleField.rx.text.orEmpty.asDriver(), detailText: detailView.rx.text.orEmpty.asDriver() ), storeManager: StoreManager.shared) setupViews() setupBinding() } } private extension AddTodoViewController { private func setupViews() { titleField.borderStyle = .none } private func setupBinding() { viewModel.output.isValid .drive(joinButton.rx.isEnabled) .disposed(by: disposeBag) viewModel.output.isValid.drive(onNext: { [weak self] isValid in self?.joinButton.backgroundColor = isValid ? .init(red: 200/255, green: 200/255, blue: 255/255, alpha: 1) : .lightGray }).disposed(by: disposeBag) titleField.rx.controlEvent(.editingDidBegin).asDriver().drive(onNext: { [weak self] in self?.firstResponderAnimate() }).disposed(by: disposeBag) titleField.rx.controlEvent(.editingDidEnd).asDriver().drive(onNext: { [weak self] in self?.resignFirstResponderAnimate() }).disposed(by: disposeBag) detailView.rx.didBeginEditing.asDriver().drive(onNext: { [weak self] in self?.firstResponderAnimate() }).disposed(by: disposeBag) detailView.rx.didEndEditing.asDriver().drive(onNext: { [weak self] in self?.resignFirstResponderAnimate() }).disposed(by: disposeBag) joinButton.rx.tap.subscribe(onNext: { [weak self] in guard let title = self?.titleField.text, let detail = self?.detailView.text else { return } self?.viewModel.insertTodoToFireStore(title: title, detail: detail) self?.titleField.text = "" self?.detailView.text = "" }).disposed(by: disposeBag) showTodoListButton.rx.tap.subscribe(onNext: { [weak self] in let viewController = TodoListViewController.instantiate() let navigationController = UINavigationController(rootViewController: viewController) navigationController.modalPresentationStyle = .fullScreen self?.present(navigationController, animated: true) }).disposed(by: disposeBag) } private func firstResponderAnimate() { let width = view.frame.size.width UIView.animate(withDuration: 0.3) { [weak self] in self?.view.transform = CGAffineTransform(translationX: 0, y: -width / 4) } } private func resignFirstResponderAnimate() { UIView.animate(withDuration: 0.3) { [weak self] in self?.view.transform = .identity } } }まず
ViewModel
を初期化しているところからいきます。
titleText
,detailText
にそれぞれの入力するUIを紐付けます。
orEmpty
はアンラップ的な役割になります。Input = ( text: Driver<String>, () )があったとして
String
を指定しているのに実際流れてくるのはString?
なのでorEmpty
がないとエラーになると思います。次に
setupBinding
の中をみていきます。
drive
はbind(to: )
のようなものです。
viewModel
のisValid
の値とjoinButton.rx.isEnabled
を紐付けています。
これによって入力の文字数が足りていなかったりした時にボタンが押せないようにしたりできます。次に
controlEvent
ですがこれは入力モードになった時にonNext
で画面を少し上にあげるというような処理になります。
textView
も同様ですが、controlEvent
ではなく直接どうなったかの状態を指定する形?になっています。ボタンのところはタップした時の処理ですね。
タップしたらFirestoreにデータを保存しています。まとめ
今回の記事を書いて思ったことは
FatViewController
を避けるためのMVVMなのに割とFatになってしまったなという感じです。
Input
でボタンがタップされたかどうかをobserveしてデータ保存の処理とかを完全にViewModelに任せてしまいたいですがまだまだ勉強が足りないですね。
また画面遷移はcoordinatorパターン
ですると画面遷移の処理もViewModelで検知して画面遷移するというのもできそうな気がするのですが、MVVMのそれぞれの責務を考えたときそれはViewの仕事なのかな?とも思います。
難しいところですが精進します。次回は保存したTodoを表示します。
多分次回で終わりにします。改善できるところや指摘がございましたらよろしくお願いします。
次はこちら。
TodoAPPでRxSwift入門[part4]
- 投稿日:2021-03-02T19:03:06+09:00
【Swift】MVPでYoutubeの動画検索アプリを開発してみた
はじめに
MVPについて詳しく説明している記事がありますので、お時間がある方はぜひそちらもご覧ください。
また、今回は下記の書籍を参考にYoutubeの動画検索アプリを作成しました。
とても読みやすい本でしたのでぜひご覧ください。今回のサンプルアプリのコードはGitHubにて共有しています。
筆者は独学で学習中のエンジニアでして、ツッコミどころ満載のコードを書くかもしれませんが、
その時は指摘していただけますと幸いです。Passive Viewによるサンプルアプリの実装
今回は、Passive Viewでアプリを実装しようと思います。
アプリの内容としては、一つ目のViewでキーワードを入力し検索をかけると、
二つ目の画面に遷移し、Youtubeの該当する動画一覧を表示するアプリを作成したいと思います。完成形
コードの解説
まず必要なものをインストールします。
今回は
Alamofire
とSDWebImage
です。
ということでPodfileに下記を追記してpod install
を実行します。pod "Alamofire" pod "SDWebImage"今回のサンプルアプリを作成するにあたり、
ファイルの構造などは書籍のものを参考に作成したいと思います。書籍では、一つの画面につき一つのフォルダを作成し、その中にStoryboard、Model、View、Presenterを作成しています。
今回は、動画検索画面(SearchVideo)と動画一覧画面(VideoList)を作成します。Main.storyboardからSearchVideo.storyboardに変更したので、
Info.plistの値を一部変更しなければ表示されません。下記の二箇所を自分で決めたStoryboardの名前に変更してください。
また、各画面のUIは次のようになります。
SearchVideoViewController(Navigation Controllerに変更)
VideoListViewController(tableViewのみでセルはカスタムセルを使用)
とりあえずUIはこのようにします。
各View全てAutoLayoutで制約を付けています。次にコードを記述していきます。
MVPを意識したコードの記述、難しかったです・・・。
まずはSearchVideo関連のファイルから進めていきます。
各オブジェクトをIBOutletなどで紐付けると下記のようになります。
また、Delegateなど別のプロトコルに準拠させる場合はextensionで追加しています。
(そっちの方が見やすい気がします!)ここから先の説明ですが、書籍の内容と個人的な解釈で記載しているので、
間違ったことを行っていたらすみません!!!
private var presenter: SearchVideoPresenterInput!
について。
SearchVideoPresenter
には
Input時に使うSearchVideoPresenterInput
プロトコルと、
Output時に使うSearchVideoPresenterOutput
プロトコルを定義しています。つまり、Inputプロトコルには、ViewControllerからPresenterに値を渡したい時に必要な処理を、
OutputプロトコルにはPresenterからViewControllerに値を渡したい時に必要な処理を記述しています。値を渡す時にはInputプロトコルに定義されているメソッドを使うので、
private var presenter: SearchVideoPresenterInput!
のように宣言し値を渡せるようにします。ですが、この時点では初期化をしていない状態なので、
viewDidLoad内のinject(presenter: presenter)
で初期化しています。この時、
let presenter = SearchVideoPresenter(view: self)
で
ViewControllerの情報をPresenterに与えておきます。SearchVideoViewControllerimport UIKit class SearchVideoViewController: UIViewController { // 検索用のフィールド @IBOutlet weak var searchTextField: UITextField! private var presenter: SearchVideoPresenterInput! func inject(presenter: SearchVideoPresenterInput) { self.presenter = presenter } override func viewDidLoad() { super.viewDidLoad() let presenter = SearchVideoPresenter(view: self) inject(presenter: presenter) searchTextField.delegate = self } // 検索ボタンが押された時の処理 @IBAction func pushSearchButton(_ sender: Any) { } } extension SearchVideoViewController: UITextFieldDelegate { // キーボードが閉じられたら実行される func textFieldDidEndEditing(_ textField: UITextField) { } // return を押したらキーボードを閉じる func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } // キーボード意外をタッチしたらキーボードを閉じる override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { searchTextField.resignFirstResponder() } }次に、一旦Presenterを記述していきます。
先ほど記述したように2つのプロトコルを定義してます。
プロトコルの中身はコメントの通りです。Viewで何かの処理を行うたびに
SearchVideoPresenterInput
のメソッドが動きます。
そしてViewの画面を変更する際にはSearchVideoPresenterOutput
のメソッドが動きます。ここら辺のプロトコルに関してですが、
デリゲートなどに関してある程度理解していないとごちゃごちゃになりそうなので、
わからなかったらデリゲートについて調べてください...。SearchVideoPresenterimport Foundation // SearchVideoViewControllerからのInputに必要な処理を記述 protocol SearchVideoPresenterInput { // 入力された文字を格納するための変数 var inputText: String {get set} // inputTextに文字を代入するメソッド func setTextToInputText(text: String?) // 検索ボタンを押した時の処理 func pushSearchButton() } // SearchVideoViewControllerへのOutputに必要な処理を記述 protocol SearchVideoPresenterOutput { // 画面遷移をさせるメソッド func transitionToVideoList(searchText: String) } final class SearchVideoPresenter: SearchVideoPresenterInput { private var view: SearchVideoPresenterOutput! var inputText: String = "" init(view: SearchVideoViewController) { self.view = view } func pushSearchButton() { view.transitionToVideoList(searchText: inputText) } func setTextToInputText(text: String?) { guard let inputText = text else { self.inputText = "" return } self.inputText = inputText } }MVPは基本的に下記の流れになります。
Viewでユーザーからの操作を受け付ける。
-> 「操作されましたよ」とPresenterに伝える。
-> 必要に応じてModelで計算等を行い、結果を返してもらう
-> Viewに「〇〇してください」と指示を出す。Viewは操作を受け付けるだけなので、
検索ボタンが押されたら、検索欄に〇〇と入力されているから画面遷移しながら〇〇の値を渡す。
と言った流れはよろしくないと思います。なので、もっと早い段階で入力情報をPresenterで保管しておき、
画面遷移のタイミングでPresenterからViewに入力情報と画面遷移の指示を出す流れにする必要があります。Presenterである程度プロトコルとその中身を定義したので
ViewControllerに戻って処理を記述していきます。追記した箇所のみ記載しています。
SearchVideoViewControllerimport UIKit class SearchVideoViewController: UIViewController { // 検索ボタンが押された時の処理 @IBAction func pushSearchButton(_ sender: Any) { presenter.pushSearchButton() } } extension SearchVideoViewController: UITextFieldDelegate { // キーボードが閉じられたら実行される func textFieldDidEndEditing(_ textField: UITextField) { // PresenterのinputTextに値を代入 presenter.setTextToInputText(text: textField.text) } } extension SearchVideoViewController: SearchVideoPresenterOutput { // 画面遷移の処理 func transitionToVideoList(searchText: String) { let videoListVC = UIStoryboard( name: "VideoList", bundle: nil) .instantiateViewController(withIdentifier: "VideoList") as! VideoListViewController videoListVC.inputText = presenter.inputText navigationController?.pushViewController(videoListVC, animated: true) } }最終的な流れとしては下記のようになります。
文字が入力される
-> キーボードが閉じられた時にtextFieldDidEndEditing(textField:)
実行
-> presenterにsetTextToInputText(text:)
を実行してもらう
->setTextToInputText(text:)
実行
-> presenterのinputTextに入力情報が格納される検索ボタンが押される
-> IBActionのpushSearchButton(sender:)
が実行される
-> presenterにpushSearchButton()
を実行してもらう
->pushSearchButton()
実行
-> viewにtransitionToVideoList(searchText:)
を実行してもらう
->transitionToVideoList(searchText:)
実行
-> presenterの値を渡してから画面遷移行ったり来たりして大変です(笑)
ただ、これこそがMVPの流れなのかな?とも思います。
次に画面遷移先での処理です。
画面遷移する際に入力情報の値だけ渡しているので、
それを元にYoutubeで検索し情報を取得します。処理は先ほどの流れのような処理なので一部説明は省きます。
先ほどと同じようにviewDidLoad()でpresenteとModelの初期化を行います。
また、tableViewを使うのでdelegate = self
を忘れずにNavigationControllerで画面を戻ることができるので、
再度VideoListViewControllerが開かれた時に再度APIを取得できるように
viewWillAppear()に記述しました。この画面では操作は受け付けていないので、
画面が読み込まれた時にreloadData(url:, key:, text:)
が
実行されるだけになります。VideoListViewControllerimport UIKit class VideoListViewController: UIViewController { @IBOutlet weak var videoListTableView: UITableView! // youtubeのAPI取得のためのURLとKEY private let url = "https://www.googleapis.com/youtube/v3/search" private let key = "AIzaSyDqWqDc0lL633AinIHA9JkeVEvLR1kz1KU&part=snippet" var inputText: String = "" private var presenter: VideoListPresenterInput! func inject(presenter: VideoListPresenter) { self.presenter = presenter } override func viewDidLoad() { super.viewDidLoad() let model = VideoListModel() let presenter = VideoListPresenter(view: self, model: model) inject(presenter: presenter) videoListTableView.delegate = self videoListTableView.dataSource = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 画面読み込みと同時にAPIデータ取得 presenter.reloadData(url: url, key: key, text: inputText) } } extension VideoListViewController: VideoListPresenterOutput { func reloadTableView() { videoListTableView.reloadData() } } extension VideoListViewController: UITableViewDelegate { // セルがタップされた時の処理などを記述 } extension VideoListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let data = presenter.data else { return 0 } return data.items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { videoListTableView.register(UINib(nibName: "VideoDetailCell", bundle: nil), forCellReuseIdentifier: "VideoDetailCell") let cell = videoListTableView.dequeueReusableCell(withIdentifier: "VideoDetailCell") as! VideoDetailCell if let data = presenter.data { cell.configre(data: data.items[indexPath.row].snippet) } return cell } }Viewが読み込まれた時に
reloadData()
が実行され、
reloadData()内では、ModelのgetYoutubeData()
が実行されます。getYoutubeData()はクロージャになっており、情報取得後にクロージャの内容が実行されます。
なので、取得した情報を変数dataに入れてViewにreloadTableView()
を実行するよう指示します。VideoListPresenterimport Foundation import Alamofire protocol VideoListPresenterInput { // Viewが表示されるたびに呼ばれるメソッド func reloadData(url: String, key: String, text: String) var data: VideoModel? {get set} } protocol VideoListPresenterOutput { func reloadTableView() } final class VideoListPresenter: VideoListPresenterInput { var data: VideoModel? private var view: VideoListPresenterOutput! private var model: VideoListModelInput! init(view: VideoListViewController, model: VideoListModel) { self.view = view self.model = model } func reloadData(url: String, key: String, text: String) { model.getYoutubeData(url: url, key: key, text: text) { (data) in self.data = data self.view.reloadTableView() } } }Modelのコードは下記のようになります。
getYoutubeData()メソッドはもらったURLと入力の情報、Keyを元にデータを取得しています。
データの取得にはAlamofireが必須なので必ずインストールしましょう。
「Youtubeのデータを格納するモデル」ですが、取得したJSONのデータを格納するためのクラスになります。ここら辺の説明はMVPとは関係ないので省略します。
VideoListModelimport Foundation import Alamofire // Youtubeのデータを格納するモデル final class VideoModel: Decodable { let kind: String let items: [Item] } final class Item: Decodable { let snippet: Snippet } final class Snippet: Decodable { let publishedAt: String let channelId: String let title: String let description: String let thumbnails: Thumbnail } final class Thumbnail: Decodable { let medium: ThumbnailsInfo let high: ThumbnailsInfo } final class ThumbnailsInfo: Decodable { let url: String let width: Int? let height: Int? } // ここまでYoutubeのデータを格納するモデル protocol VideoListModelInput { func getYoutubeData(url: String, key: String, text: String, completion: @escaping (_ data: VideoModel) -> Void) } protocol VideoListModelOutput { func resultAPIData(data: VideoModel) } final class VideoListModel: VideoListModelInput { // youtubeのデータを取得する func getYoutubeData(url: String, key: String, text: String, completion: @escaping (VideoModel) -> Void) { let urlString = "\(url)?q=\(text)&key=\(key)" AF.request(urlString).responseJSON { (response) in do { guard let data = response.data else { return } let decode = JSONDecoder() let video = try decode.decode(VideoModel.self, from: data) completion(video) } catch { print("変換に失敗しました。\n【エラー内容】", error) } } } }情報を取得したらPresenterのクロージャ内の処理を実行。
その処理はtableViewのリロードになっているのでtableViewが更新される。
という流れになっています。ちなみにカスタムセルのコードは下記のようになります。
SDWebImageでサムネの画像を取得しています。VideoDetailCellimport UIKit import SDWebImage class VideoDetailCell: UITableViewCell { @IBOutlet weak var thumbnail: UIImageView! @IBOutlet weak var channelIcon: UIImageView! @IBOutlet weak var title: UILabel! @IBOutlet weak var date: UILabel! override func awakeFromNib() { super.awakeFromNib() } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } // セルにデータを入れていく func configre(data: Snippet) { thumbnail.sd_setImage(with: URL(string: data.thumbnails.medium.url), completed: nil) title.text = data.title date.text = data.publishedAt } }さいごに
今回は簡単な処理だったのでMVPの良さが出ているかわかりませんが、
個人的には結構面白いなと思いました。慣れれば処理をどこに書くか迷わないでしょうし、
どこに処理を書いたかがわかりやすくなりそうです。また、処理の流れなども掴みやすくなるかも?と思いました。
そもそも記述方法があっていなかったらすみません・・・。
MVVMなども理解すれば便利そうなので、
MVPでしっかりとコードが書けるようになったら覚えてみます!以上、最後までご覧いただきありがとうございました。
- 投稿日:2021-03-02T19:02:24+09:00
【Swift】MVPアーキテクチャについて色々調べてみた
はじめに
iOSアプリ設計パターン入門という書籍を参考に記事を作成しました。
とても読みやすい本でしたのでぜひご覧ください。通常3,500円で電子が3,200円になります。
MVPアーキテクチャ
Swiftで使われるアーキテクチャは、MVCやMVP、MVVMなどさまざまですが、
今回はMVPについて解説していきます。MVPは画面の描画処理とプレゼンテーションロジックとを分離するアーキテクチャになり、
Passive View
とSupervising Controller
の二つのパターンが存在します。
この二つのパターンについては後ほど解説します。
MVPは、Model・View・Presenterの頭文字をとってMVPと言われています。MVPと似ているアーキテクチャとしてあげられるMVCですが、
MVCとMVPのどちらがいいかと思う方もいると思います。その時次第で変わるのかなと素人目線では思うのですが、
書籍を読む限り、テストを行ったり作業分担を行う際にはMVPを使用するというイメージで良いのではないかなと思います。というのも、MVPの目的はテストを容易に行えるようにすること、作業分担をしやすくすることだからです。
データの同期方法
MVPを語る上で大切になってくるのがデータの同期方法です。
MVPでは、主に二つの同期方法をとっています。
それはフロー同期とオブザーバー同期です。フロー同期
フロー同期は、上位レイヤーのデータを下位レイヤーに都度セットしてデータを同期する手続き的な同期方法です。
フロー同期のメリットとしてあげられるのは、
データの流れを掴みやすいことになります。なぜかというと、フロー同期は画面遷移の際の
push
やpop
の時にデータを同期させるからです。
第三者が見ても、画面遷移のコード近辺を見ればどのデータを渡しているかが容易に判断できます。ですが、逆にデメリットもあります。
例えば、画面A -> 画面B -> 画面Cと遷移する際に、
画面Aと画面Cでは同じデータを使いたい場合があるとします。この際、画面Aから画面Cに値を渡すことができず、画面Bを経由しなければなりません。
画面Bではそのデータを使う予定はないので、必要のないデータを保持することになります。これは、管理が煩雑する可能性があるのであまりよろしくないと思います。
オブザーバー同期
オブザーバー同期は、監視元である下位レイヤーが監視先である上位レイヤーから
Observerを使って送られるイベント通知を受け取ってデータを同期させる、宣言的な同期方法です。オブザーバー同期のメリットとしてあげられるのは、
共通した監視先を持つ複数の箇所で、データを同期しやすいことです。複数のタブや階層が離れている画面から共通の画面領域を監視しているので、
同期箇所で他の画面の参照を持つ必要がありません。Twitterの「いいね」機能を例にすると分かりやすいかもしれません。
ユーザAがユーザBのツイートをいいねした時の処理を例にします。ユーザAの画面
・ユーザBツイートのハートマークが赤になる
・ユーザAのプロフィールのいいね欄に追加されるユーザBの画面
・いいねされましたの通知がくる
・自分のツイートのいいねが1つ増えるユーザBをフォローしている人の画面
・ユーザBのツイートのいいねの数が1つ増えるユーザAやユーザBの内容はおそらく別タブで行われているので、このような時に有効かと思います。
(もし間違っていたらすみません・・・!)オブザーバー同期のデメリットとしては、
データが同期されるたびに変更処理を行うためいつデータが同期されるかが追いづらくなることです。フロー同期の際は、画面遷移の時に同期されると分かっているので判断は容易ですが、
フロー同期の場合はそのように判断できないことがデメリットです。MVPにおけるデータの同期方法
先ほど、MVPには二つのパターンがあります。と記載しましたがそれについて少し説明したいと思います。
Passive View
とSupervising Controller
ですね。Passive Viewは、Presenter -> View間に先ほど説明したフロー同期をします。
Supervising Controllerは二つの同期方法を使います。
Presenter -> View間はフロー同期し、Model -> View間はオブザーバー同期します。単純な私は、これなら全部Supervising Controllerでいいじゃないか!と思ってしまいますが、
実際はそうでもないらしいです・・・。(設計の世界は奥が深い!!)MVPの構造
Passive ViewとSupervising Controllerはそれぞれ違ったデータ同期方法ですが、
共通する事項も多く存在するのでそれについて記載していきます。MVPには、Model / View / Presenter の3つのコンポーネントが存在します。
それぞれの共通事項は下記のようになります。
共通事項
Model
Modelは、UIに関係しない純粋なロジックやデータを持ちます。
MVPにおけるModelはMVCやMVVMと同じ役割を果たしています。Modelが扱う領域の具体例は WebAPI やデータベースへの
アクセス、BLE デバイス制御や、会員ステータスごとの商品の割引率の計算などが存在するらしいです。また、Modelの特徴として、ViewやPresenterに依存していないので、
Modelのみでもビルドが可能であるという特徴もあげられます。View
Viewは、ユーザの操作受付と、画面表示を担当するコンポーネントです
Viewは、タップやスワイプなどの操作を受け付けて、
Presenterに処理を委譲したりModelの処理を呼びたしたりします。Modelに変更が発生したら、何らかの方法でViewが更新されるわけですが、
その時の更新方法の違いがPassive ViewとSupervising Controllerの違いになります。Presenter
Presenterは、ViewとModelの仲介役です。
Modelはアプリのビジネスロジックを知っていますが、
それが画面上にどのように表示されるかを知っているべきではありません。Viewをシンプルにするために、複雑なロジックを持たせたくはないが、
Modelに画面表示に関わるロジックを持たせたくない場合に使用するらしいです。Presenterは、たいてい1つのViewにつき1つ作成します。
Passive View
Passive ViewはViewを完全に受け身にするパターンで
各コンポーネントのデータのやりとりはフロー同期によって実現されます。Passive Viewの利点は、Viewに描画処理の実装のみをも持たせるようにし、描画指示をPresenterに任せることで、
プレゼンテーションロジックのテストが行いやすくなる点だそうです。なので、Passivi Viewにおける各コンポーネントの役割は下記のようになります。
コンポーネント 役割 Model Presenterからのみアクセスされ、Viewとは直接の関わりを持たない View Presenterからの描画指示に従うだけで、完全な受け身な立ち位置 Presenter 全てのプレゼンテーションロジックを受け持つ Supervising Controller
Supervising Controllerは、フロー同期とオブザーバー同期の両方を使うパターンです。
ViewはPresenterとはフロー同期で、Modelとはオブザーバー同期でデータのやりとりを行います。Supervising Controllerは、基本的にViewに対する入力イベントはPresenterに渡し、
必要に応じてViewに対して描画指示を出します。Supervising Controllerの面白いところは、
Viewは簡単なプレゼンテーションロジックを持ちPresenterは複雑なプレゼンテーションロジックを持つというところです。簡単な処理を行う時にわざわざPresenterを通すと冗長なので簡単な処理の場合は直接Modelからアクションを行います。
Supervising Controllerにおける各コンポーネントの役割は下記のようになります。
コンポーネント 役割 Model Presenterからのみアクセスされ、必要に応じてViewに対してイベントを通知する View PresenterとModelの双方から描画指示を受け、簡単なプレゼンテーションロジックを持つ Presenter 複雑なプレゼンテーションロジックを担う MVPの構造まとめ
Passive Viewの流れと、Supervising Controllerの流れは下記のようになります。
Passive View
View -> Presenter -> Model -> Presenter -> ViewSupervising Controller
・簡単な処理
View -> Presenter -> Model -> View
・複雑な処理
View -> Presenter -> Model -> Presenter -> Viewでは、どちらのパターンを使用すればいいかというところですが、
書籍では迷ったらPassive Viewを使うようにと進めています。理由としては、Supervising Controllerの特徴の一つである
「Viewが持つ簡単なプレゼンテーションロジック」が関係してきます。まず、この簡単の基準を開発者間で統一することが難しいです。
開発者にはプロもいればそうでない人もいるので当たり前のことですね。その他にも、オブザーバー同期によるModelとViewの接続に使う、適切なイベントの粒度の設計が難しい。
という点もあげられていました。私自身が経験したことはないのでこちらに関しては深く語れませんが、
確かに大規模な開発になると進むにつれボロが出てくる可能性はありそうです。これらのことから、とりあえずSupervising ControllerではなくPassive Viewでの開発をメインにすればいいかと思います。
設計に携わるエンジニアの方々は本当に尊敬します・・・。
さいごに
今まで私は、MVCで色々開発してきましたが、
テストは切っても切り離せないことですし、チームで作業を行うにあたり、作業分担も大事になってきます。なので、これからはMVPで開発する癖をつけていこうかなと思います。
MVPの中にも二つのパターンがありましたが、
私はとりあえずPassivi Viewをメインに学習を進めていこうと思います。
みなさんも一緒にがんばりましょう!!この記事で学んだことと、書籍の内容をもとに
サンプルアプリを作成してみましたので下記の記事も合わせてご覧になっていただけると幸いです。・【Swift】MVPでYoutubeの動画検索アプリを開発してみた
また、今回参考にした書籍はこちらになります。
設計パターンについての書籍の中でも一番分かりやすい気がしましたのでぜひご覧ください。以上、最後までご覧いただきありがとうございました。
- 投稿日:2021-03-02T18:26:58+09:00
SwiftUIでリスト表示する際に画像が黒く塗り潰される
SwiftUIでリストを作成した際に画像が黒く塗りつぶされてしまったので対応方法の覚書です。
以下の様な画像と名称のみのセルを持ったシンプルなリストを定義します。
struct ContentView: View { var body: some View { VStack { List { ForEach(0..<2, id: \.self) { index in Button(action: { print("タップ") }) { SpotCell() } } } } } } struct SpotCell: View { var body: some View { HStack(alignment: .top) { Spacer().frame(width: 20) VStack(alignment: .leading) { HStack(alignment: .top) { VStack { Spacer().frame(height: 16) Image(uiImage: UIImage(named: "otaru")!) .resizable() .scaledToFill() .frame(width: 60, height: 60) .clipped() .cornerRadius(4.0) Spacer().frame(height: 16) } VStack(alignment: .leading) { Spacer().frame(height: 16) Text("小樽運河").font(.subheadline) Spacer() } } } Spacer() } } }こちらiOS14以上で実行すると問題なく画像が表示されますが、iOS13の実機で実行すると以下の様に画像が黒く塗り潰されてしまいました。
iOS13以下だとImageを定義する際に.renderingMode(.original)を付けなればならない様です。
以下の様にresizableの後ろにつけることによって画像が正常に表示されました。
※リストの外に画像を配置した場合は.renderingModeを付けなくても正常に表示されるのですが、リストの中に画像を配置すると上のように黒く塗り潰されてしまう様です。Image(uiImage: UIImage(named: "otaru")!) .resizable() .renderingMode(.original) .scaledToFill() .frame(width: 60, height: 60) .clipped() .cornerRadius(4.0)
- 投稿日:2021-03-02T18:13:01+09:00
[Swift] TableViewを下スワイプして画面を更新する(UIRefreshControll)
現在開発中のアプリでTwitterのタイムラインのような、Viewを下スワイプして中身を更新する処理を追加したいと思い調査して実装。
ヒットした記事でなかなか実装出来ず多少詰まったため、記事を書くことにしました。
この記事は筆者の半ば備忘録的な記事です。UIRefreshControllとは
画面を下に引っ張ってデータを更新するUI部品。
TwitterやYouTubeなどで皆さんが一度は見たことがある、あのクルクルです。
以下、Appleが提供するドキュメントから一部抜粋。UIRefreshControlはUITableViewやCollectionViewを含むUIScrollViewに接続できる標準的なコントロールです。
英語でじっくり読みたいって方は一番下のリンクから飛んでみてください。
要は、スクロールビューならどれでも使えるよというやつです。実行環境
Xcode 12.4
Swift 5.3.2実装方法
そんな難しくはなく、すぐ実装出来ます。
@IBOutlet weak var tableView: UITableView! //storyboardのTableViewをアウトレット接続 override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self //TableViewを使うなら必須の処理 tableView.delegate = self //TableViewを使うなら必須の処理 configureRefreshControl() //この関数を実行することで更新処理がスタート } //〜中略〜 func configureRefreshControl () { //RefreshControlを追加する処理 tableView.refreshControl = UIRefreshControl() tableView.refreshControl?.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged) } @objc func handleRefreshControl() { /* 更新したい処理をここに記入(データの受け取りなど) */ //上記の処理が終了したら下記が実行されます。 DispatchQueue.main.async { self.tableView.reloadData() //TableViewの中身を更新する場合はここでリロード処理 self.tableView.refreshControl?.endRefreshing() //これを必ず記載すること } }以上です。
コメントアウトで解説は書いているのでそちらを参考にしていただけたらと思います。注意点としては.endRefreshing()を必ず書いてくださいということくらいです。
最後に
比較的簡単に実装出来る機能ですが、汎用性の高いUIなのかなと思います。
ぜひ、実装してみてより良いアプリ開発ライフを送っていきましょう。ではまた。
参考
- 投稿日:2021-03-02T18:13:01+09:00
[Swift] TableViewを下スワイプして画面を更新する(UIRefreshControl)
現在開発中のアプリでTwitterのタイムラインのような、Viewを下スワイプして中身を更新する処理を追加したいと思い調査して実装。
ヒットした記事でなかなか実装出来ず多少詰まったため、記事を書くことにしました。
この記事は筆者の半ば備忘録的な記事です。UIRefreshControlとは
画面を下に引っ張ってデータを更新するUI部品。
TwitterやYouTubeなどで皆さんが一度は見たことがある、あのクルクルです。
以下、Appleが提供するドキュメントから一部抜粋。UIRefreshControlはUITableViewやCollectionViewを含むUIScrollViewに接続できる標準的なコントロールです。
英語でじっくり読みたいって方は一番下のリンクから飛んでみてください。
要は、スクロールビューならどれでも使えるよというやつです。実行環境
Xcode 12.4
Swift 5.3.2実装方法
そんな難しくはなく、すぐ実装出来ます。
@IBOutlet weak var tableView: UITableView! //storyboardのTableViewをアウトレット接続 override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self //TableViewを使うなら必須の処理 tableView.delegate = self //TableViewを使うなら必須の処理 configureRefreshControl() //この関数を実行することで更新処理がスタート } //〜中略〜 func configureRefreshControl () { //RefreshControlを追加する処理 tableView.refreshControl = UIRefreshControl() tableView.refreshControl?.addTarget(self, action: #selector(handleRefreshControl), for: .valueChanged) } @objc func handleRefreshControl() { /* 更新したい処理をここに記入(データの受け取りなど) */ //上記の処理が終了したら下記が実行されます。 DispatchQueue.main.async { self.tableView.reloadData() //TableViewの中身を更新する場合はここでリロード処理 self.tableView.refreshControl?.endRefreshing() //これを必ず記載すること } }以上です。
コメントアウトで解説は書いているのでそちらを参考にしていただけたらと思います。注意点としては.endRefreshing()を必ず書いてくださいということくらいです。
最後に
比較的簡単に実装出来る機能ですが、汎用性の高いUIなのかなと思います。
ぜひ、実装してみてより良いアプリ開発ライフを送っていきましょう。ではまた。
参考
- 投稿日:2021-03-02T15:16:24+09:00
シングルトンパターンを使う時の条件とは
はじめに
タイトルにも書いてあるように、シングルトンパターンを使う時の条件を備忘録としてまとめます。
シングルトンパターンとは
デザインパターン(ソフトウェア開発における設計パターン)の一つであり、
シングルトン
を使って定義します。
そして、シングルトンパターンはインスタンスが1個しか生成されないことを保証したい時
に使います。シングルトンの代表例として、
UserDefaults
やUIApplication
などがあります。シングルトンの書き方
swiftでのシングルトンの書き方は、
クラス
に以下のコードを記述するだけです。class Hogehoge { // 初回アクセスのタイミングで初期化 static let shared = Hogehoge() // 外部からのインスタンス生成を禁止する private init() {} } // このようにして呼び出すことができる Hogehoge.shared因みにですが、
struct
に対してシングルトンパターンは用いることが出来ません。
Copy on Write (CoW)
によって暗黙的にコピーを発生させてしまう
からです。struct Fugafuga { var str = "original" static let shared = Fugafuga() } var fugafuga = Fugafuga.shared // ここでCoWによってコピーが発生し、Fugafuga.sharedとは別物になってしまう fugafuga.str = "copy" print(fugafuga.str) // copy print(Fugafuga.shared.str) // originalこのようにコピーが発生してしまえば、
インスタンスが1個
では無くなってしまい
シングルトンパターンの要件を満たせないからです。使う時の条件とは
先ほど、シングルトンパターンを使用する場面は
インスタンスが1個しか生成されないことを保証したい時
と書きましたが...自分にはあまりピンと来ません。じゃあ、そういう場面とは?という感じで結局、どこで使用するか分かりません。
自分はあらゆる場所から呼べる便利さ
から何となく使っている感じでした。・便利だが、危険でもある
しかし、
あらゆる場所から呼べる便利さ
ですが危険性もあります。swiftの機能の大部分は、
オブジェクト指向
という考え方で設計されています。この設計方法には3つの原則があり、その中の1つ「
カプセル化
」がシングルトンパターンと真逆の考え方
なんですね。・カプセル化とシングルトンパターン
カプセル化を意識すると、
意図していない場所で値が好きな様に書き換えられてしまうことを防ぐ
ことができます。例えば、このようなコードがあったとします。
class Hogehoge { var str = "Hello World!" }外部で
Hogehogeクラスのインスタンス
を作成した場合、今のままだとクラス内に宣言されているstrプロパティ
にアクセス出来てしまいます。つまり、下記のように外部から好きな値に書き換えることが可能になります。
class Hogehoge { var str = "Hello World!" } let hogehoge = Hogehoge() print(hogehoge.str) // Hello World! hogehoge.str = "Goodbye World!" print(hogehoge.str) // Goodbye World!では、カプセル化を意識して先ほどのコードを改修してみましょう。
class Hogehoge { private var str = "Hello World!" } let hogehoge = Hogehoge() print(hogehoge.str) // エラー hogehoge.str = "Goodbye World!" // エラー print(hogehoge.str) // エラー
private
をvarの前に付けました。こうする事によって、
外部からアクセスしようとしてもエラーになるので外部から値を好きな様に書き換えられてしまう
ことが無くなりました。しかし、
シングルトンパターン
は色んな場所で呼び出せてしまうので思わぬ場面で大切なデータを書き換えてしまった
等の危険性があります。なので、
適した場面で使用しなくてはいけません
。シングルトンパターンの正しい使い方
Appleドキュメントで書かれていました。(やはり、公式ドキュメントを読むのが良き)
「シングルトンを使用して、グローバルにアクセス可能なクラスの共有インスタンスを提供します。サウンド効果を再生するオーディオチャンネルや、HTTPリクエストを行うネットワークマネージャのように、アプリ全体で共有されているリソースやサービスへの統一されたアクセスポイントを提供する方法として、独自のシングルトンを作成することができます。」
(引用: Managing a Shared Resource Using a Singleton | Apple Developer Document)上記のドキュメントを読んで自分なりに解釈する
例えば、ログイン機能をアプリに実装しようと思い、
新規登録画面(以下、画面A)
とログイン画面(以下、画面B)
を作ろうと思いました。1.早速、実装しようとするも画面によってログインに関するインスタンスを生成するのは面倒だし、無駄にメモリを消費してしまう等の問題が...。
↓
2.なら
ログイン機能を管理するクラス
を作った方がいいな。↓
3.シングルトンパターンを
使用しない
場合。ログイン機能を管理するクラスのインスタンス
を画面A・Bそれぞれに対して生成しなくてはならないし、更にそれぞれで何か変更を加えた際に不整合が起こる可能性があり、管理しにくい
。シングルトンパターンを
使用する
場合。そこで、アプリ内でログイン機能を管理するクラスのインスタンスは1個のみ
を保証するシングルトンパターンを使用すれば、ログイン機能を管理するクラス
が統一されているので不整合が起きにくく、更にはソースコードの可読性が向上し、管理しやすくなる
。ということでしょうか?
おわりに
まだまだ、まとめた今でもシングルトンパターンを使えるか不安ですが
とりあえず知識は深まりました。因みに自分の現在開発しているアプリでは、先ほどの
ログイン機能を管理するクラス
の他にAPI通信を行うクラス
もシングルトンパターンを使用しています。メンターによると、API通信を行うクラスはシングルトンパターンにするのが主流だそうです。
参考記事
【Swift】Singleton パターンってどういう時に使うの?
Swift におけるシングルトン・staticメソッドとの付き合い方
- 投稿日:2021-03-02T13:14:42+09:00
【SwiftUI】sheetメソッドを使ってビューをモーダル表示にする
先日リリースした私のアプリに使用した技術の解説です。
私のアプリはこちら。
sheetメソッドとは
ビューをモーダル表示させるメソッド。下から出てきて画面の一番手前に表示される。
実際の動作
「わかった!」ボタンを押すと下から答え合わせのビューが出てきて、画面の一番手前に表示されているのがわかるかと思います。
また、このシートは上下に動かすことができ、一定の位置まで下げて指を離すと閉じるようになっています。基本の書き方
sheetメソッド.sheet(isPresented: ブール値, content: { 表示させたいビュー })sheetメソッドはさまざまな引数を取りますが、一番シンプルな書き方はこのようになります。
isPresented
がtrueになったとき、content
の{ }内のビューが表示されます。ソースコード
上のサンプルアプリのコードの一部です。
ContentView.swiftimport SwiftUI struct ContentView: View { @State var answer = "" @State var moveAnswer = false //シートを出すためのブール型の変数 var body: some View { VStack{ Text("なぞなぞ") .font(.title) .padding() Text("パンはパンでも白黒のかわいいパンは\nぱ〜んだ?") TextField("こたえを書いてね", text: $answer) .frame(width: 200) .padding() .border(Color.black) // シートを出すトリガーになるボタン Button(action: { moveAnswer = true //シートを出すためのアクション }, label: { Text("わかった!") .padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(10) }) //moveAnswerがtrueになったときに出てくるシート .sheet(isPresented: $moveAnswer, content: { Answer(answer: answer, moveAnswer: $moveAnswer) }) } } }こちらのコードを部分ごとにわけて解説したいと思います。
シートを出すための仕組み
まず、ContentViewのプロパティとしてブール型の変数を用意します。
@State var moveAnswer = false //シートを出すためのブール型の変数アプリ起動時はシートが出ていない状態にしたいため、初期値にfalseを入れています。逆に言えば、初期値をtrueにすればアプリ起動時にシートを出すことができます。
次は、シートを出すトリガーになるボタンです。
// シートを出すトリガーになるボタン Button(action: { moveAnswer = true //シートを出すためのアクション }, label: { Text("わかった!") .padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(10) })ボタンをタップするとmoveAnswerにtrueが代入されます。
そして、sheetメソッドを見ていきます。
.sheet(isPresented: $moveAnswer, content: { Answer(answer: answer, moveAnswer: $moveAnswer) })ボタンのアクションによってmoveAnswerにtrueが入ると
isPresented
がtrueになり、content
のAnswerビューが表示されます。今回、AnswerビューはAnswer.swiftという別のファイルを作って書きました。そちらのビューも見てみましょう。
今回シートに表示したビュー
Answer.swiftimport SwiftUI struct Answer: View { let answer: String //前の画面のなぞなぞの答え @Binding var moveAnswer: Bool //前の画面のmoveAnswerを反映させるための変数 var body: some View { VStack{ //なぞなぞの答えが正解かどうかで表示を変える if answer == "パンダ" { Text("せいかい!!\nすごい!天才だ!") .font(.title) Image("correct") }else{ Text("ちがうよ。もういちどもんだいをよく読んでみよう。") .font(.title) Image("fault") } //シートを閉じるためのトリガーになるボタン Button(action: { moveAnswer = false //シートを閉じるためのアクション }, label: { Text("もどる") .font(.title) .padding() }) } } }動画内でもやっていたように、わざわざボタンを作らなくてもシートを閉じる操作はできますが、ボタンがあった方が便利な場面もあるかと思います。このボタンの作り方について解説したいと思います。
シートを閉じるためのボタンの作り方
シートを閉じるためには、ContentViewのmoveAnswerがfalseになる必要があります。そのため、まずはContentViewのmoveAnswerをAnswerビューに反映させます。
@Binding var moveAnswer: Bool //前の画面のmoveAnswerを反映させるための変数
@Binding
をつけることで、このmoveAnswerに入れたブール値の状態を共有しています。つまり、AnswerのmoveAnswerがfalseになると、ContentViewのmoveAnswerもfalseに変わるようにしています。そしてこちらがシートを閉じるためのトリガーとなるボタンです。
//シートを閉じるためのトリガーになるボタン Button(action: { moveAnswer = false //シートを閉じるためのアクション }, label: { Text("もどる") .font(.title) .padding() })このボタンを押すことでmoveAnswerにfalseが入り、シートを閉じることができます。
- 投稿日:2021-03-02T11:13:36+09:00
[Swift5]UITextViewのキーボードをタップで閉じる方法(シンプル)
- 投稿日:2021-03-02T10:09:38+09:00
iOS 14 / watchOS 7 HealthKit 変更点
iOS 14 / watchOS 7 から HealthKit に症状を記録するデータタイプや、歩行に関するより詳細なデータタイプが追加されるなどの変更がありました。
ECG 関連のアップデートが中心な感じですが、2021.1 の watchOS 7.3 アップデートで日本でも ECG が使えるようになったのでまとめてみました。
心拍に関するデータタイプ HKElectrocardiogramType の追加
wathOS 7.3 (iOS 14.4) より、日本でも Apple Watch で心電図(ECG)が使えるようになりました。アメリカので二年遅れてついに日本で使えるようになりました。Apple Watch S4,5,6 で使用可能です。
不規則な心拍(心房細動(AFib)の兆候がある不規則な心拍リズム)の通知を受け取ることができるようになり、心電図 Apple Watch アプリを使うと、心房細動、洞調律、低心拍数、高心拍数、判定不能のいずれかに分類することができるようになっています。
HKElectrocardiogramType
この有無が記録されている HKElectrocardiogram をチェックすることが iOS14/watchOS7 SDK からできるようになりました。
let healthStore = HKHealthStore() let healthTypes = Set([ HKCategoryType.electrocardiogramType() ]) healthStore.requestAuthorization(toShare: nil, read: healthTypes) { (success, error) in // ... }https://developer.apple.com/documentation/healthkit/hkelectrocardiogram?changes=latest_major
HKElectrocardiogram を取得するには HKSampleQuery で取得します。
let ecgType = HKObjectType.electrocardiogramType() let ecgQuery = HKSampleQuery(sampleType: ecgType, predicate: nil, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { (query, samples, error) in if let error = error { fatalError(error.localizedDescription) } guard let ecgSamples = samples as? [HKElectrocardiogram] else { fatalError(String(describing: samples)) } for sample in ecgSamples { print(sample) } } healthStore.execute(ecgQuery)HKElectrocardiogramQuery に HKElectrocardiogram オブジェクトを渡して詳細な測定値を取得できます。
let voltageQuery = HKElectrocardiogramQuery(ecgSample) { (query, result) in switch(result) { case .measurement(let measurement): if let voltageQuantity = measurement.quantity(for: .appleWatchSimilarToLeadI) { print(voltageQuantity) } case .done: print("done") case .error(let error): fatalError(error.localizedDescription) } } healthStore.execute(voltageQuery)-4.39537 mcV 1.91129 mcV 8.0672 mcV 14.0773 mcV ... 省略HealthKit で症状を記録することができるデータタイプの追加
iOS 13.6 / watchOS 7 以降から症状についての記録のためのデータタイプが追加されています。
iOS 14 でもさらに追加され、「寒気」「鼻水が出る」「咳」「腰の痛み」「物忘れ」「頭痛」「胸の痛み」「ニキビ」など、ほか含め合計 39 種類の症状が発生したことを記録する HealthKit 対応のログアプリなどが作れるようになりました。
全て HKCategoryTypeIdentifier のデータタイプになっているので、他の HKCategoryType のサンプルのように取り扱うことができるようになっています。
https://developer.apple.com/documentation/healthkit/data_types/symptom_type_identifiers
歩行速度や歩幅、階段の昇降速度のサンプルデータ型が追加
歩くことに関するいくつかのサンプルデータが追加されました。全て HKQuantityType 型のサンプルデータになります。
6 分間歩行の推定データが取得できるようになりました。
static let sixMinuteWalkTestDistance: HKQuantityTypeIdentifier6 分間歩行とは何か、近畿中央呼吸器センターさんのホームページに記載されている内容を引用します。
6 分間歩行試験とは、6 分間平地を歩いていただくことによって、肺や心臓の病気が日常生活の労作にどの程度障害を及ぼしているのか調べるための検査です。https://kcmc.hosp.go.jp/shinryo/hokou.html
歩行の速度や歩幅を示すデータ、さらに歩幅が左右で違う割合だとか、歩いている時に両足が地面についている瞬間の割合だとか、歩くことに関する複雑なデータが取得できるようになりました。
static let walkingSpeed: HKQuantityTypeIdentifier static let walkingStepLength: HKQuantityTypeIdentifier static let walkingAsymmetryPercentage: HKQuantityTypeIdentifier static let walkingDoubleSupportPercentage: HKQuantityTypeIdentifier階段登り下りの速度のデータが取得できるようになりました。
static let stairAscentSpeed: HKQuantityTypeIdentifier static let stairDescentSpeed: HKQuantityTypeIdentifier「手洗いした」データタイプが追加
watchOS 7 / Apple Watch S4,5,6 で、手洗いをマイクとモーションセンサーを使って自動で検知するようになりました。そのログデータを取り出すことができるようになります。
static let handwashingEvent: HKCategoryTypeIdentifierHKSampleQuery で他のデータと同じようにサンプルを取り出すことができます。
SwiftUI で昨日今日の handwashingEvent を取得する例。他の HKCategoryType のデータタイプも同様の書き方で取得することができます。
import SwiftUI import HealthKit var healthStore: HKHealthStore! struct ContentView: View { var body: some View { Button(action: { checkHealthKit() }, label: { Text("Button") }) } func checkHealthKit() { healthStore = HKHealthStore() let healthTypes = Set([ HKCategoryType.categoryType(forIdentifier: .handwashingEvent)! ]) healthStore.requestAuthorization(toShare: nil, read: healthTypes) { (success, error) in fetchHandWashing() } } func fetchHandWashing() { let now = Date() let calendar = Calendar.current let yesterday = calendar.date(byAdding: .day, value: -1, to: calendar.startOfDay(for: now)) let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: now, options: []) let sortDescriptor = [NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: true)] let washingEvent = HKCategoryType.categoryType(forIdentifier: .handwashingEvent)! let query = HKSampleQuery(sampleType: washingEvent, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: sortDescriptor) { (query, results, error) in guard error == nil else { print("error"); return } let format = DateFormatter() format.dateFormat = "yyyy-MM-dd HH:mm:ss" format.timeZone = TimeZone(identifier: "Asia/Tokyo") print("\(format.string(from: yesterday!)) - \(format.string(from: now)) result ?:") if let tmpResults = results as? [HKCategorySample] { tmpResults.forEach { (sample) in print(sample) } } } healthStore.execute(query) } }ヘッドホンから有害とされる音量を受けた時の記録のデータタイプの追加
ヘッドホンからの音、うるさすぎるよ!っていうものです。
static let headphoneAudioExposureEvent: HKCategoryTypeIdentifierこれに合わせて、環境音がうるさすぎるの時のデータタイプ名はカテゴリを正しく表記する名称に変更になりました。
~~static let audioExposureEvent: HKCategoryTypeIdentifier~~ ↓ static let environmentalAudioExposureEvent: HKCategoryTypeIdentifierワークアウトタイプの追加
HKWorkoutActivityTypeCardioDance HKWorkoutActivityTypeSocialDance HKWorkoutActivityTypePickleball HKWorkoutActivityTypeCooldownCardio Dance は YouTube とかで検索するといっぱい出てきますね。
Social Dance はフォークダンスなどパートナーとか何人かでやるダンス。
Pickle Ball っていう新しいアメリカで流行り始めている?スポーツが追加されたようです。歴史的には 50 年ぐらいあるらしい。 https://www.playpickleball.com
Cooldown は運動前後のクールダウン的な運動やストレッチを明示的にするための定数ですね。Apple Fitness+ サービス(日本では提供されていませんが)のメニューに対応した定数が今後も増えそうな気がします。
(iOS 14.3 / watchOS 7.3 以降)妊娠出産に関するヘルスケアデータタイプの追加
妊娠出産関連のカテゴリタイプが追加されたので、それに対応するデータタイプが追加されています。
static let contraceptive: HKCategoryTypeIdentifier // 避妊薬 static let lactation: HKCategoryTypeIdentifier // 授乳 static let pregnancy: HKCategoryTypeIdentifier // 妊娠(iOS 14.3/watchOS 7.2 以降) HKSample.hashUndeterminedDuration の追加
var hasUndeterminedDuration: Bool { get }サンプルの endDate プロパティが distantFuture の場合 true になります。
distantFuture とは「遠い未来の日付」で、イベントの終了待ちをするまでのテンポラリ値として入れておくなどの使い方をしたりします。
print(Date.distantFuture) // 4001-01-01 00:00:00 +0000おわり
HealthKit を解説している拙著「HealthKit Book for Beginners」にも本記事を追加しましてアップデート(v3.0)しましたので、購入していただいた方は PDF 版再ダウンロードおねがいしますー。(製本版は v1.0 のままです)
- 投稿日:2021-03-02T06:22:12+09:00
【iOS】LiDARセンサーを用いたサンプルコード集を公開しました!
iOSのLiDARセンサーを用いたサンプルコード集をGitHubに公開したので紹介します。
本記事の執筆時点でGitHubで91スターを頂いています!TokyoYoshida/ExampleOfiOSLiDAR
?よさそうだと思った方はスターをお願いします⭐例えば、こんなサンプルが入っています。
The GitHub repository of sample code using iOS LiDAR sensors, including 3D scanning, got 40 stars in a day!
— TokyoYoshida (@jugemjugemjugem) February 19, 2021
iOSのLiDARセンサーを用いたサンプルコードのGitHubリポジトリが1日で40スターも取った!
The repository is here.https://t.co/FHASFV4Zfs#iOS #LiDAR #AR #ARKit pic.twitter.com/lfCq1T9yBgiPad Pro/iPhone 12 ProにLiDARが搭載されたとき、いつかは現実世界をスキャンしてみたいと思っていたかたもいらっしゃると思いますが、このサンプルを使えば簡単に実現できてしまいます。
ビルド方法
XCodeでExampleOfiOSLiDAR.xcodeprojを開いてビルドして、iPhone 12 Pro/Pro MAXかLiDARセンサーを搭載したiPad Proにインストールします。
サンプル一覧
Depth Map
深度情報を画像にしたものです。スクリーンショットは静止画ですがリアルタイムに動作します。
Confidence Map
深度情報の信頼度を画像にしたものです。リアルタイムに動作します。
Collision
現実世界に、仮想的なオブジェクトがぶつかったときの判定をします。
Export
現実世界のオブジェクトをスキャンして、.objファイルにエクスポートします。
エクスポートしたファイルはXcode,Macのプレビューなどで閲覧したり、Blenderで編集できます。Scan with Texture
現実世界のオブジェクトをカラースキャンします。こちらは、サンプルアプリ内でのみ閲覧できます。
最後に
LiDARセンサーはなかなかおもしろい技術なので、これからも追っていきたいと思っています。作品やサンプルができたら、Twitterで発信していきますのでフォローをお願いします?
https://twitter.com/jugemjugemjugemNote こちらはiOS開発、AR、機械学習などについては発信しています。
https://note.com/tokyoyoshida
- 投稿日:2021-03-02T01:25:59+09:00
[ARKit]水平面の検出
水平面を検出するためのコンフィグレーションを設定
現実世界の水平面を検出するためにの設定をします。
sceneViewの型はARSCNViewクラスです。デフォルトでは、平面検出は行われません。必要な機能はプログラマーが各自設定する必要があります。let configuration=ARWorldTrackingConfiguration() //水平面の時は.horizontalで垂直面の時は.verticalになります。 configuration.planeDetection = .horizontal sceneView.session.run(configuration)検出した平面をどのように扱うのか?
ARSessionDelegateを処理したいクラスに批准させて、そのクラスをARSceneView.session.delegateに代入して、デリゲートメソッドを利用します。
sceneView.session.delegate=selfここで、selfを代入させたので、このクラス(Viewcontroller)上でデリゲートメソッドを利用できます。
//アンカーがセッションに追加されたときに呼ばれる func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { } //アンカーが更新されたときに呼ばれる func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) { }アンカーとは検出物体の位置や向きを表すクラスです。
ARanchorクラスはプロパティとしてindentifierとtransformを持っています。
indentifierはそのアンカーの名前です。人間でも名前で他人と区別するように、アンカーにも名前をつけて他のアンカーと区別をします。つまり、アンカーを一意に定めるものです。transformは、位置や大きさを表すプロパティで4×4行列です。
ここで、anchorのままでは使いにくいので,サブクラスであるARPlaneAnchorにキャストします。//アンカーがセッションに追加されたときに呼ばれる func session(_ session: ARSession, didAdd anchors: [ARAnchor]) { guard let planeAnchors=anchor as? [ARPlaneAnchor] else {return} }ARPlaneanchorクラスは,centerプロパティとextentプロパティを持ちます。
centerプロパティは平面アンカーの中心の位置で、extent(simd_float3型)は,平面の大きさを表しますが、平面なのでyの値は0になります。
検出した平面に色を塗りたいときこのメソッドは使いません。
AR機能はセッション単位で管理しています。
上のメソッドでは、平面に限らず、seceViewにアンカーが追加されたり、更新されたりするときに呼ばれだけで検出した平面のアンカーの位置や大きさを所得して、色を変えたりするのは別のメソッドで行います。平面検出したときに呼ばれるメソッド
ARSCNViewDelegateプロトコルのデリゲートメソッドを使います。
//平面アンカーのノードがシーンに追加されたときに呼ばれる func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { // アンカーを平面アンカーにキャストする guard let planeAnchor=anchor as? ARPlaneAnchor else {return} // アンカーから平面形状(geometry)の平面を作成 let geometry=SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z)) // 平面に黄色を付ける geometry.materials.first?.diffuse.contents=UIColor.yellow.withAlphaComponent(0.8) // 平面からノードを作成する。 let planeNode=SCNNode(geometry: geometry) // なんか知らんけど、平面がx-y平面上に作られるので,平面をx軸(1,0,0)を軸に90°回す planeNode.transform=SCNMatrix4MakeRotation(-Float.pi/2, 1, 0, 0) // メインスレッドでnodeに追加する DispatchQueue.main.async(execute:{ node.addChildNode(planeNode) }) }nodeは平面アンカーに対応したnodeです。
- 投稿日:2021-03-02T00:16:41+09:00
Swiftでカメラキャプチャーするための最小コード
準備
Info.plist
に以下の設定を追加。<key>NSCameraUsageDescription</key> <string>[カメラ使用に関する説明]</string>コード
import UIKit import AVFoundation class CaptureViewController: UIViewController { private let previewLayer: AVCaptureVideoPreviewLayer = { let previewLayer = AVCaptureVideoPreviewLayer() previewLayer.videoGravity = .resizeAspectFill return previewLayer }() init() { super.init(nibName: nil, bundle: nil) let session = AVCaptureSession() previewLayer.session = session if let device = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: device), session.canAddInput(input) { session.addInput(input) } let output = AVCaptureVideoDataOutput() output.setSampleBufferDelegate(self, queue: .global()) if session.canAddOutput(output) { session.addOutput(output) output.connection(with: .video)?.videoOrientation = .portrait } session.startRunning() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.layer.addSublayer(previewLayer) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() previewLayer.frame = CGRect(origin: .zero, size: view.bounds.size) } } extension CaptureViewController: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // sampleBufferを処理する } }環境
Xcode 12.2
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8) Target: x86_64-apple-darwin19.6.0