20210502のSwiftに関する記事は13件です。

PHPでpostできるAPIを作ろう!

はじめに Swiftを勉強し始めてからSwiftでの仕事をしたいとこだわり続けてきたんですけど、API作るためにPHPの勉強したらPHPちょっと楽しいかもと思っています。 今回は簡単なPOSTできるAPIを作ろう思います。 サーバーサイドはPHP、クライアントサイドはSwiftを使います。 PHPのコード 説明を挟みながら少しずつコードを加えていく書き方でいきます。 api.php if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 処理 } こちらはリクエストがPOSTかどうかの判定です。 GETなどの他のリクエストの処理をしたい場合は 'POST'の部分を変えてください。 api.php $response = array(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (isset($_POST['post_message']) === false) { $response['error'] = true; $response['message'] = "エラーのメッセージ"; } else { // nullじゃないとき } } isset()は値がnullかどうかを調べてくれる関数です。 nullならfalse,そうでないならtrueを返してくれます。 今回の場合ポストで送信されてきたpost_messageというキーの連想配列の値がnullの時にレスポンスでエラーを出すようにしています。 以下のコードはnullじゃないときというコメントの下に書いています。 api.php $post_message = $_POST['post_message']; //データベースに接続して保存したり... $response['error'] = false; $response['message'] = "成功のメッセージ"; //スコープの一番外に print json_encode($response); $_POST["キー"]でpostで送信した値を取り出せます。 php側はこんな感じです。 実際は正規表現などを用いてバリデーションチェック等を挟むことになると思います。 Swiftのコード PostViewController.swift class PostViewController: UIViewController { viewdidLoad() { super.viewdidLoad() let url = URL(string: "apiのURL")! let request = NSMutableURLRequest(url: url) //httpMethodの設定 //ここをPOSTにしておくとpostできる(当たり前) request.httpMethod = "POST" //渡したい値をセットする //文字列だけど=の左の文字列がPHPの連想配列のキーになる。 //渡したい値が複数ある場合は&で繋げます。(web開発している人なら当たり前にわかる) let postParams = "post_message=swiftからのpost" request.httpBody = postParams.data(using: .utf8) let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in if error != nil { print("error is \(error!)") return } do { let myJSON = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary if let parseJSON = myJSON { var msg: String! msg = parseJSON["message"] as? String print(msg!) } } catch { print(error) } } task.resume() } } 今回はpostのAPIについての記事なのでswiftの説明はコメントのみとさせていただきます。 これでビルドしてみるとログに$response['message']で設定した文字列が表示されていると思います。 実際にちゃんと値が渡せているか確認するなら $response['error'] = false; $response['message'] = "post_message is ".$post_message ; で確認できると思います。 まとめ swiftを使う仕事をしたいと思いスキルを上げるために勉強を始めたphpですが割と楽しくなってきてswiftにこだわる気持ちが薄れてきました。 あと、全く関係ない話ですが最近スペースキーの反応が悪いので掃除したんですが直らなくてモヤモヤしてます。 今回は以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Auto Layout priority swift label編

■UILabelの動き color 文字色変わる Font 文字のでかさ 太さ等を変える Aligment 文字の位置 左揃え 中央 右揃え ■Lines 重要 1 なら 1行 2 なら 2行 3 なら 3行 文字数が多ければLinesの数値によって行が変わる。 ■しかし0なら下記のように決められた幅に自動で文字が入る動きをする。 ■priorty(優先度) priorityとは制約が持つ優先度の値の事です。 とりあえず1から1000の数値を持っております。 数字が高いほど優先度が高くなります。 比べるのはAutoLayOutが持つpriorityとcontentが持つpriority AutoLayOutのデフォルトの数値は1000になり, 一番優先度高くなります。 (AutoLayOutが1000Contentが1000の時AutoLayOutが勝利する。) ■この事を踏まえてまずこちらをご覧ください。 ■余白 例:UIlabelを指定してます。 文字以外の緑が大きく広がっているこれが余白がある状態 ■潰れ 文字長くて収まらない時...となる。 この余白と潰れを自在に操れたりもできます。 ■Contentのpriority 例:top,bottom,leeling,tralingの制約をつけた状態のUILabel(content)を指定 右メニューの下らへん Content Hugging Priority(余白) Content Compression Resistance Priority(潰れ) の2つに注目 互いにHorizontal(横) Vertical(縦) の値が存在する。 ■AutoLayOutのpriorityをつける。 @750がついてるのがpriorityを変更した場合 ついてない制約はpriorityがデフォルトで最強の1000がついてる。 制約を選び右端のメニューからいじれる ■上記の二つのpriorityを比べる ■Content Hugging Priority(余白なくす) 余白をなくす君 下記のような余白があるLabelがあるとする。 ■余白なくしてみる。 例:AutoLayoutで定めたpriorityよりContent Hugging Priorityのvertical(縦)のpriorityが高い AutoLayout bottomの制約のpriorityが750 負け Content Hugging verticalのPriorityが1000 勝利 縦の余白なくす君の勝利 結果:縦の余白をなくす事ができる。 例:逆にAutoLayOutのbottomのpriorityをいじらなければデフォルトで1000なので AutoLayout bottomの制約のpriorityが1000 圧倒的勝利 Content Hugging Priorityのverticalが1000 負け AutoLayOutのデフォルトの勝利 結果:余白ができる。 (お互い1000ならAutoLayOutが優先される。) つまりAutoLayOutのみで制約すればこのように広がる制約をつけている。 でも余白いらんなと思ったらpriorityを使用して余白を消す事ができる! 制約の右側のpriorityとHorizontalをいじれば横の余白も消せる。 無駄な制約を減らせる。 ■Content Compression Resistance Priority(潰れなくす) 潰れなくする君 下記のようなこういったlabelがあるとする。 Linesを1行にした場合文字が多ければ潰れる。 ■例:潰れなくしてみる。 trailing(右)のAutoLayoutのpriorityを750 負け Content Compression Resistance PriorityのHorizontalのpriorityが1000 勝利 潰れなくす君の勝利 結果:潰れなくなるので下記のように横に飛び出す。 例:潰れる tariling(右)のAutoLayOutがpriorityがデフォルト 圧倒的勝利 Content Compression Resistance PriorityのHorizontalのpriorityが1000 負け AotolayOutのデフォルトの勝利 結果:最初のように潰れる trailingのpriorityをなんもいじってないからデフォルトの最強の優先度になって デフォルトで潰れが発生している。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

こんな書き方もできるSwift

init class Sample { static let sample:Sample = .init() private init() {} } ここではSampleクラスの中で、そのインスタンスを内部にあるsampleに代入している。その際、.init()がイニシャライザとして使われており、ドットの前は self = Sample を指す。動作の上では.init()で十分で、private initはなかったとしても問題ない。ただし、ここではスコープを限定するためにinitに対してprivateという属性をつけている。 普段インスタンスを別クラスからしか作った試しがなく、自身をクラス内の変数にインスタンスとして代入するというのはみたことがありませんでした。またinitメソッドも.init()のようには作ったことがなく、スペースを取って別物のように扱っていたので、インスタンスを作る際に.init()で済ませられるのはとても新鮮に感じられました。 追記していきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift5.4 変更点まとめ

2021/04/26にSwift5.4のリリースがあったのでその変更点をまとめました。 Swiftのアップデート一覧 Support for multiple variadic parameters in functions, subscripts and initializers (SE-0284): 複数の可変長引のサポート Extend implicit member syntax (SE-0287): 暗黙のメンバー構文の拡張 Result builders (SE-0289) Local functions supporting overloading: ローカル関数におけるオーバーロードのサポート Property wrappers for local variables: ローカル変数におけるプロパティラッパーのサポート Support for multiple variadic parameters in functions, subscripts and initializers (SE-0284) 複数の可変長引数のサポート 今まで可変長引数は1つしか指定できなかったが複数指定できるようになった func foo(_ a: Int..., b: Double...) { } struct Bar { subscript(a: Int..., b b: Int...) -> [Int] { a + b } init(a: String..., b: Float...) { } } Extend implicit member syntax (SE-0287) 暗黙のメンバー構文の拡張 メンバーアクセスのチェーンをサポートし以下のように記述できるようになった。 let milky: UIColor = .white.withAlphaComponent(0.5) let milky2: UIColor = .init(named: "white")!.withAlphaComponent(0.5) let milkyChance: UIColor? = .init(named: "white")?.withAlphaComponent(0.5) Result builders (SE-0289) 新しい構造体を @resultBuilder で定義することで、新しい結果ビルダーを定義することができる。定義するメソッドとプロパティは static にする必要がある。 以下の結果ビルダーを使えば、その以下のように文字列を結合できる。 @resultBuilder struct StringBuilder { static func buildBlock(_ strings: String...) -> String { strings.joined(separator: "\n") } } 値を定義する際には、buildBlock メソッドを直接使用する必要があった。 そのため、各String要素にはコンマを入れなければならなかった。 let stringBlock = StringBuilder.buildBlock( "Tokyo", "Kanagawa", "Saitama", "Chiba" ) 代わりに関数の中で@StringBuilderを使えば、カンマを使わずに同じことができる。 @StringBuilder func makeSentence() -> String { "Tokyo" "Kanagawa" "Saitama" "Chiba" } Local functions supporting overloading ローカル関数におけるオーバーロードのサポート 以下のように記述できるようになった。 func outer(x: Int, y: String) { func doIt(_: Int) {} func doIt(_: String) {} doIt(x) // calls the first 'doIt(_:)' with an Int value doIt(y) // calls the second 'doIt(_:)' with a String value } Property wrappers for local variables ローカル変数におけるプロパティラッパーのサポート 以下のように記述できるようになった。 @propertyWrapper struct Wrapper<T> { var wrappedValue: T } func test() { @Wrapper var value = 10 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サンプルアプリレベルのZoomSDKをSwiftUIで実装するときに詰まったポイントN個

ひょんなことからZoomSDK使ったミーティングアプリをSwiftUIで実装するという素晴らしくチャレンジングな開発に関われたので忘備録も兼ねていろいろ書いていこうと思います。 今回はとりあえず動くものをと言うことで、Zoom client SDKのSwiftサンプルと同じレベルくらいに持っていきます。 つまりポイント①:ライブラリが一元管理できない Zoomさんの公式ドキュメントでは、FrameworkをXcodeに直入れする方法を丁寧に教えてくれています。 CocoaPodsで入れさせてくれ。。 同じことを思った先人がいたのか、方法はありました。他人のリポジトリを晒していいのか不安なので、気になる人は「zoomSDK CocoaPos」あたりで検索かけてみてください。多分一番上にきます。 つまりポイント②: unrecognized selector sent to instance まずは動作確認ということで、UIKitで組まれているclientSDKのサンプルにあるViewController.swiftを適当にリネーム&レイアウトをXibで書き換えてUIViewControllerRepresentableに噛ませて動かしてみました。結果はタイトル通りです。 どうやらデフォルトのZoomミーティング画面を表示するタイミングでエラーになっているらしい。 Life CycleにUIKit App Delegateを選んでいたので、SceneDelegateが悪そう?と当たりをつけていくと案の定消したら動作しました。こちらの記事が大変参考になりました。 https://qiita.com/koogawa/items/3472398afd4d809148b0 つまりポイント③:TextFieldを2つ含めたAlartがSwiftUIだけで組めない clientSDKのサンプルとさよならするために上記のViewController.swiftに実装されていた内容をSwiftUIで実装し直します。しかしここでもまた問題が。 ↓のようなミーティングIDとパスワードを入力させるAlartを出したかったのですが、そもそもTextField1つですら現時点で出すことができないという悲しみ。。 こちらの記事がTextField1つの時の実装方法を書かれていたので、参考にさせていただきました。 @Binding var text: String を配列にして、UIAlertControllerにforEachで配列分addTextFieldしてあげたりしたら複数並べられます。 https://www.yururiwork.net/%E3%80%90swiftui%E3%80%91textfield%E4%BB%98%E3%81%8Dalert%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B/ 最後に 公式のリファレンスが難読性高かったり、実装例が全然見つからない難しさはありますが、走りきったらいい感じにハイになれそうです。次回はZoom色を削るためにUIのカスタマイズを行いたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】VIPERを勉強してみたPart1

はじめに VIPERを勉強したので、基本的なことをまとめていこうと思います。 GitHub VIPERとは クリーンアーキテクチャーをiOS向けにしたシステムアーキテクチャのこと。 View, Interactor, Presenter, Entity, Routerの頭文字からとった。 システムアーキテクチャーとは、今までのGUIアーキテクチャー(MVC, MVVM, MVP)のように、Viewとその他みたいな考え方ではなく、画面遷移やAPI通信、データ保存などを考慮した設計のこと。 それぞれの役割 View: ViewとViewController Interactor: API通信担当 Presenter: 自分以外の中継役 Entity: データそのもの Router: 画面遷移担当 特徴 ・徹底的な疎結合 →Entity以外全てprotocolで繋ぐ ・Presenterは内部で状態をもたない →いつどのような入力に対しても必ず同じ出力になる。(Entityの違いはある) ・PresenterのメソッドはViewで起きたものに依存した名前にする →viewDidLoad, buttonDidTappedなど ・ViewとRouter以外はimport UIKitだめ、絶対 ・Interactorはデータを返すだけに徹する →API通信、端末内保存、メソッドで計算しただけなど関係なく、最後はデータを返すだけ。 他のモジュールからはどのようにデータを返したのかわからなくする。 データの返し方はRxSwift, protocol, closureなど、なんでもいい ・Entityに処理を書かない →純粋にデータを保持した型 処理の流れ 1.Routerdえ画面を生成し、DI(依存性注入)させる 2.生成された画面を表示 3.ViewからイベントをPresenterに知らせる →ライフサイクル、ボタンタップ... 4.PresenterはViewから送られてきたイベントの内容に合わせて以下のような処理をする ・Viewに対して画面の更新依頼する →Viewは依頼された通りに画面を更新する ・Interactorに対してデータの取得依頼をする →Interactorは依頼されたデータの取得が完了したらPresenterに通知する ・Routerに対して画面遷移の依頼をする →Routerは依頼された画面へ遷移する 命名 View ・画面の更新 ラベルの文字変更 UITableViewのreload など ・Presenterへのイベント通知担当 ライフサイクル ボタンのタップ、セルのタップ など GitHubSearchViewController import UIKit protocol GitHubSearchView: AnyObject { func initView() func startLoading() func finishLoading() func reloadTableView(items: [GitHubSearchEntity]) } final class GitHubSearchViewController: UIViewController { @IBOutlet private weak var textField: UITextField! @IBOutlet private weak var searchButton: UIButton! @IBOutlet private weak var indicator: UIActivityIndicatorView! @IBOutlet private weak var tableView: UITableView! // presenterへのアクセスはprotocolを介して行う private var presenter: GitHubSearchPresentation! override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self tableView.register(GitHubSearchTableViewCell.nib, forCellReuseIdentifier: GitHubSearchTableViewCell.identifier) searchButton.addTarget(self, action: #selector(searchButtonDidTapped), for: .touchUpInside) // presenterにイベントを通知 presenter.viewDidLoad() } func inject(presenter: GitHubSearchPresentation) { self.presenter = presenter } } // MARK: - @objc func @objc private extension GitHubSearchViewController { func searchButtonDidTapped() { // presenterにイベントを通知 presenter.searchButtonDidTapped(word: textField.text) } } // MARK: - GitHubSearchView extension GitHubSearchViewController: GitHubSearchView { func initView() { DispatchQueue.main.async { self.tableView.isHidden = true self.indicator.isHidden = true } } func startLoading() { DispatchQueue.main.async { self.tableView.isHidden = true self.indicator.isHidden = false } } func finishLoading() { DispatchQueue.main.async { self.tableView.isHidden = false self.indicator.isHidden = true } } func reloadTableView(items: [GitHubSearchEntity]) { DispatchQueue.main.async { self.tableView.reloadData() } } } // MARK: - UITableViewDelegate extension GitHubSearchViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) // presenterにイベントを通知 presenter.selectItem(indexPath: indexPath) } } // MARK: - UITableViewDataSource extension GitHubSearchViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // presenterにイベントを通知 return presenter.getSearchedItems().count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: GitHubSearchTableViewCell.identifier, for: indexPath) as! GitHubSearchTableViewCell // presenterにイベントを通知 let item = presenter.getSearchedItems()[indexPath.row] cell.configure(gitHubSearch: item) return cell } } Interactor ・ビジネスロジック担当(Utility) ・Presenterから依頼されたビジネスロジックを実装し、結果を返す。 delegate, closure, RxSwift... ・import UIKitだめ、絶対 UIを気にしない GitHubSearchInteractor import Foundation protocol GitHubSearchUsecase { func get(parameters: GitHubSearchParameters, handler: ResultHandler<[GitHubSearchEntity]>?) func getSearchedItems() -> [GitHubSearchEntity] } // 他のアーキテクチャーでいうUtilityの役割も持つ final class GitHubSearchInteractor { private var searchedItems: [GitHubSearchEntity] init() { searchedItems = [] } } // MARK: - GitHubSearchUsecase extension GitHubSearchInteractor: GitHubSearchUsecase { func get(parameters: GitHubSearchParameters, handler: ResultHandler<[GitHubSearch]>? = nil) { guard parameters.validation else { handler?(.failure(.error)) return } guard let url = URL(string: "https://api.github.com/search/repositories?\(parameters.queryParameter)") else { handler?(.failure(.invalidUrl)) return } let task = URLSession.shared.dataTask(with: url) { data, _, _ in guard let data = data, let gitHubResponse = try? JSONDecoder().decode(GitHubSearchEntityResponse.self, from: data), let items = gitHubResponse.items else { handler?(.failure(.error)) return } self.searchedItems = items handler?(.success(items)) } task.resume() } func getSearchedItems() -> [GitHubSearchEntity] { return searchedItems } } Presenter ・Viewから受け取ったイベントを元に別クラスに依頼 Viewに対して画面更新を依頼 Interactorに対してデータの取得を依頼 Routerに対して画面遷移を依頼 ・Presenterが提供するメソッド名はViewのメソッド名と同じ viewDidLoad, buttonDidTapped... ・Presenterに状態を持たせない ・import UIKitだめ、絶対 UIを気にしない GitHubSearchPresenter import Foundation protocol GitHubSearchPresentation: AnyObject { func viewDidLoad() func searchButtonDidTapped(word: String?) func selectItem(indexPath: IndexPath) func getSearchedItems() -> [GitHubSearchEntity] } // 他との部品以外はパラメータを持たない // 他との中継役にだけに徹する final class GitHubSearchPresenter { // view, interactor, routerへのアクセスはprotocolを介して行う // 循環参照しないようにviewだけweak private weak var view: GitHubSearchView? private var interactor: GitHubSearchUsecase private var router: GitHubSearchWireframe init(view: GitHubSearchView, interactor: GitHubSearchUsecase, router: GitHubSearchWireframe) { self.view = view self.interactor = interactor self.router = router } } // MARK: - GithubSearchPresentation extension GitHubSearchPresenter: GitHubSearchPresentation { func viewDidLoad() { view?.initView() } func searchButtonDidTapped(word: String?) { let parameters = GitHubSearchParameters(searchWord: word) view?.startLoading() interactor.get(parameters: parameters) { [weak self] result in guard let self = self else { return } self.view?.finishLoading() switch result { case .success(let items): self.view?.reloadTableView(items: items) case .failure(let error): self.router.showAlert(error: error) } } } func selectItem(indexPath: IndexPath) { let gitHubSearchEntity = interactor.getSearchedItems()[indexPath.row] let initParameters: WebUsecaseInitParameters = .init(entity: gitHubSearchEntity) router.showWeb(initParameters: initParameters) } func getSearchedItems() -> [GitHubSearchEntity] { return interactor.getSearchedItems() } } Entity ・データ構造そのもの ・ロジックを持たせない ・import UIKitだめ、絶対 UIを気にしない //対応がわかりやすいように置き換え typealias GitHubSearchEntityResponse = GitHubResponse typealias GitHubSearchEntity = GitHubSearch typealias GitHubSearchntityError = GitHubError import Foundation struct GitHubResponse: Codable { let items: [GitHubSearch]? } struct GitHubSearch: Codable { let id: Int let name: String private let fullName: String var urlString: String { "https://github.com/\(fullName)" } enum CodingKeys: String, CodingKey { case id case name case fullName = "full_name" } } Router ・画面遷移 ・依存性注入(後述) ・VIPERの肝 VIPERでは画面遷移の処理をRouterで行うことにより、Viewの責務を減らせて可読性の向上が望める GitHubSearchRouter import UIKit protocol GitHubSearchWireframe { func showWeb(initParameters: WebUsecaseInitParameters) func showAlert(error: Error) } final class GitHubSearchRouter { private unowned let viewController: UIViewController private init(viewController: UIViewController) { self.viewController = viewController } // Routerが画面遷移を担当しているので、ここに書く static func assembleModules() -> UIViewController { let view = UIStoryboard.gitHubSearch.instantiateInitialViewController() as! GitHubSearchViewController let interactor = GitHubSearchInteractor() let router = GitHubSearchRouter(viewController: view) // presenterが中継役なので、全てと繋げる let presenter = GitHubSearchPresenter(view: view, interactor: interactor, router: router) // viewからpresenterに通知する必要があるため繋ぐ // viewとpresenterは互いが互いを知っている view.inject(presenter: presenter) return view } } // MARK: - GitHubSearchWireframe extension GitHubSearchRouter: GitHubSearchWireframe { func showWeb(initParameters: WebUsecaseInitParameters) { let next = WebRouter.assembleModules(initParameters: initParameters) viewController.show(next: next) } func showAlert(error: Error) { print(error.localizedDescription) } } おわりに その他の処理はGitHubをご覧ください。 次回は依存性注入とクリーンアーキテクチャのDomain層とData層についてまとめたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift:UISwipeGestureRecognizer備忘録

バージョン ・Xcode11.4.1 ・Swift5 概要 imageView上のスワイプを検知して、ラベルに方向を表示させる。 今回は上のみ検知。 コード test.swift import UIKit class ViewController: UIViewController { @IBOutlet weak var testLabel: UILabel! @IBOutlet weak var animationView: UIImageView! override func viewDidLoad() {   super.viewDidLoad() //UISwipeGestureRecognizerのインスタンス作成 let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipeLabel(sender:)))   //UISwipeGestureRecognizerで検知する方向を設定 swipeRecognizer.direction = .up //isUserInteractionEnabledをtrueにしないととエラー出ないけれど、animationViewをスワイプしても認識されない animationView.isUserInteractionEnabled = true //uiimageにスワイプの感知を追加 animationView.addGestureRecognizer(swipeRecognizer) } //スワイプされたときに動く関数 @objc func swipeLabel(sender:UISwipeGestureRecognizer) { testLabel.text = "上" print("up") } } 参考にさせて頂いた記事 UITapGestureRecognizer が反応しない 【Swift】Swipe Gesture Recognizerの使い方。上下左右のスワイプを検知する。 UISwipeGestureRecognizerの罠と左右スワイプの実装
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIStoryBoardにinstantiateInitialViewControllerメソッドをextensionで実装する

画面遷移時などでStoryBoardが切り替わる場合、initial、つまり最初に表示させるViewContorllerを設定しなければなりません。実行時エラーが生じる訳ではありませんが、これがなされていないと背景が真っ黒のUIWindowだけが呼び出されてしまい無意味です。インスタンスメソッドのinstantiateInitialViewControllerは、画面遷移時などStoryBoardをイニシャライズし、またinitial設定を行います。 このメソッドはStoryBoardの切り替えの際に必要となるケースが多いため、記述するシーンは少なくありません。今までは必要に応じて調べて対応していましたが、最近になってextensionで呼び出しやすいように実装されている方法を知りました。これでなにができるのかというと、その都度必要な場合にメソッドを記述する必要がなくなり、また予測で呼び出しやすくなります。1箇所にまとめられることで、コードも把握しやすくなります。 extensionの使い方としても、とても参考になりました。 ここではinstantiateInitialViewControllerメソッドの基礎的な理解を含めてまとめています。ここではextensionとプロパティ周りの用語の理解は行わず、必要と考えられる程度の記述に留めています。 実装方法 extension UIStoryboard { static var sampleViewController: SampleViewController { UIStoryboard.init(name: "Sample", bundle: nil).instantiateInitialViewController() as! SampleViewController } } ここではextensionでUISroryboardクラスで呼び出せるようにしています。なぜUIStoryboardクラスなのかというと.instantiateInitialViewController()がすでに実装されているメソッドであるからです。extensionといっても、そのクラスに実装されていないメソッドを載せても利用できません。あるいは利用するなら実装し直す必要があります。もちろん、それでは本末転倒です。ちなみに、.initの前のUIStoryboardはselfとして記載しても同じです。 呼び出し方 //返り値はViewContorller UIStoryboard.sampleViewController コンピューテッドかつスタティックプロパティとして変数を宣言しており、クラスに紐づけられているため、呼び出し方は基本的には変わらないはずです。storyboardが増えてくると、その都度記述しなければならなかったメソッドもextensionでまとめると、とても綺麗なコードになります。 (と言ってワイは参考にしているだけなんやが) 各定義 instantiateInitialViewController Instance Method UIStoryboard > instantiateInitialViewController func instantiateInitialViewController() -> UIViewController? UIStoryboardクラスに属します。メソッドの名称から分かるように、返り値はUIViewControllerです。インスタンスメソッドはインスタンスがなければ利用できないため、記述例にあるようにinitでイニシャライズを行っています。 UIStoryboard.init(name:bundle:) class UIStoryboard : NSObject { init(name: String, bundle storyboardBundleOrNil: Bundle?) } ドキュメントにはCreates and returns a storyboard object for the specified resource file.とあるように、特定のファイルからStoryboardのオブジェクトを作成し返します。その際のパラメータは次のとおりです。 1 Parameters name Storyboardのファイル名を、String型として記載して指定します。ファイルの拡張子名 .storyboard は不要です。例えば、Sample.storyboardであれば、"Sample"と記述すればよいわけです。 storyboardBundleOrNil storyboardファイルやその関連したファイルを含むbundleを指定します。名称からわかるように、Storyboardのbundleか、もしくはnilを指定する形となっています。nilを許容しなければならないため、Bundle?とオプショナルになっているんですね。nilである場合は、現在のアプリケーションのmain bundleとして映るようです。 // Get the app's main bundle let mainBundle = Bundle.main 例示されているように、Bundle.mainとして利用することができるようです。と調べて書いてきましたが、そもそもbundleとはなんでしょうか。 そもそもBundleとは? Google先生で調べてみると、製品をひとまとめにすること、付属していることを指してバンドルと言うのだそうですが、これではなんのことだろうかって感じです。 AppleのドキュメントはA representation of the code and resources stored in a bundle directory on disk.となっていて、ディスクにあるバンドルディレクトリの中に保持されているコードとリソースを表現したものとされていて、これもよくわからない。 ただもう少し追っていくと、By using a bundle object, you can access a bundle's resources without knowing the structure of the bundle.と書かれていて、バンドルの中の構造を知らなくても、オブジェクトを通して中身を利用することができるよんって書かれています。 つまり、実装したコードやリソースはバンドルの中にまとめられていて、それをオブジェクトを通じてアクセスできるわけです。拙い経験ですが、外部ライブラリなど使うとバンドルの利用機会は増えてくるように思われます。 実際にどのような時に利用するのかというと一般的なパターンとしては ・Create a bundle object for the intended bundle directory.  意図されているバンドルディレクトリのオブジェクトを作る場合 ・Use the methods of the bundle object to locate or load the needed resource.  必要なソースの位置を指定したり読み込むためのbundleオブジェクトのメソッドを使用する場合 ・Use other system APIs to interact with the resource.  特定のソースと通信するために他のシステムのAPIを叩く場合 となっています。 2 Return Value 返り値はstoryboardのオブジェクトです。マッチする名前のファイルが存在しない場合は、: Could not find a storyboard named 'XXXXXX' in bundle....としてエラーが表示されます。 as! -ViewController 型のダウンキャストを行なっています。冒頭のinstantiateInitialViewControllerメソッドの定義を見てみるとこのようになっています。 func instantiateInitialViewController() -> UIViewController? 返り値はUIViewControllerクラスのオプショナルであるため、ダウンキャストを行なっているものと考えます。素人考えではinitが成功するのであれば、なんとなく返り値は確定されているのでオプショナルにならないんじゃないかと思うのですが、どうなんでしょうか。 ここまでが具体的な実装の中身となります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOSのSwiftアプリをPKG化、DMG化してみよう

我のPCスペック macOS Catalina uname -v # Darwin Kernel Version 19.6.0....RELEASE_X86_64 PKG化 XcodeでSwiftアプリプロジェクトを作成し、リリースビルドまで行う。(DerivedDataをプロジェクトディレクトリ内に出力できる様に設定しておくと楽) プロジェクトフォルダ直下にbuildフォルダを作成し、DerivedData/[プロジェクト名]/Build/Products/Release/にあるappファイルをコピー プロジェクトフォルダ直下で、下記コマンドを実行し、.plistファイルを作成 pkgbuild --analyze --root ./build [任意の名前 ].plist 生成されたplistを開き、BundlesRelocatableを<false/>にする。[1] プロジェクトフォルダ直下で、下記コマンドを実行し、.pkgファイルを生成。(公開用pkgではないらしい) pkgbuild --root ./build --component-plist [3.で生成した.plistファイル ] --identifier [アプリの識別子(ex: jp.co.hogeo) ] --version 0 --install-location /Applications [任意の名前 ].pkg プロジェクトフォルダ直下で、下記コマンドを実行し、Distribution.xmlファイルを生成。 productbuild --synthesize --package [「5.」のPKGファイル ] Distribution.xml Distribution.xmlを開き、<pkg-ref>...</pkg-ref>の下に下記を追記。 <title>[任意のタイトル]</title> (もし、よくある企業ロゴをインストーラーに表記させたい場合は、上記に加え、下記を追記する。pngファイルは、Resourcesフォルダをプロジェクトフォルダ直下に配置し、その中においておく。) <background file="hoge-logo.png" alignment="bottomleft" mime-type="image/png"/> プロジェクトフォルダ直下で、下記コマンドを実行し、公開用PKGを生成する。 productbuild --distribution [「6.」のxmlファイル ] --resources [xmlファイルで追記したpngファイル入れたフォルダ ] --package-path . ./[任意の名前].pkg Developer ID証明書の署名(いる人は) productsign --sign "Developer ID Installer: xxxx Inc." [「8.」のPKGファイル] [任意の名前].pkg DMG化 プロジェクトフォルダ直下にdmg-resourceフォルダを作成し、その中にREAD_ME.txtや上記で作成した署名PKG(又は未署名PKG)を入れる。 プロジェクトフォルダ直下で、下記コマンドを実行し、DMGファイルを生成する。 hdiutil create -srcfolder [「1.」のフォルダ] -fs HFS+ -format UDZO -volname [dmgをマウントした時にシステム側で出る名前] [任意の名前].dmg 最終フォルダ構成 [test-hello-world_ProjectFolder] | |-- test-hello-world.xcodeproj |-- test-hello-world/ |-- test-hello-worldTests/ |-- test-hello-worldUITests/ |-- DerivedData/ |-- build/ |-- Resources/ |-- dmg-resources/ |-- TestHelloWorld.plist |-- Distribution.xml |-- TestHelloWorld.pkg |-- Test-Hello-World-Release.pkg |-- test_hello_world.dmg 参考文献 [1]pkgbuildでインストーラ(.pkg)作ったけどあれ上手くアプリがインストールされないなーって時にはBundleIsRelocatableがfalseになっているかチェックしましょう Mac package installer installs the folder “Contents” in /Applications instead of the bundle Mac用Installerの作り方 macOSのインストーラーを作成するには hdiutilの使い方 Developer IDで署名したインストーラーパッケージを作る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase初心者] Cloud Firestoreは公式動画が最強なのでまとめてみた [データ構造編]

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase初心者] Cloud Firestore公式動画が最強なのでまとめてみた [データ構造編]

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase初心者] Cloud Firestore公式動画をまとめてみた [データ構造編]

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Firebase] Cloud Firestoreのデータ構造について動画の内容をまとめてみた

はじめに Google Cloud Firestoreについて調べていたところ公式Youtubeの説明が一番詳しかったので翻訳しつつまとめてみました。 今回はデータベースの構造についてのみの記事です。 執筆:2021年5月2日 基本的なデータ構造 まずはCloud Firestoreの基本的なデータ構造について説明します。 元の動画はこちら Firestoreではまずコレクションがあり、その中にドキュメントを複数持つことができます。 コレクションを複数作ることもでき、ドキュメント内にコレクションを作ることもできます。これをサブコレクションと言います。(画像のReviewsがサブコレクション) サブコレクション内のドキュメントのサブコレクションを作成することもでき、データは最大100レベルまでネストできます。 ドキュメント内のデータ構造は以下の通りで、各項目はフィールドと呼ばれます。 json型のようなmapとarray(配列)も保持できます。 公式ドキュメント https://firebase.google.com/docs/firestore/manage-data/structure-data Cloud Firestoreの6つのルール Firestoreには大きく分けて6つのルールがあり、それを理解した上でデータ構造を決める必要があります。 変更された点もありますが、動画に沿って変更前と変更後の説明をしています。 元の動画はこちら↓ ① ドキュメントの制限 1つのドキュメントに格納できるデータは1MBまで 300ページの本の文章を格納した場合770KB 写真などの格納は難しい フィールドは20,000まで 全てのフィールドにインデックスが発行される仕組みになっているためmapもフィールドにカウントされるので注意 // map: フィールド数4 address: { city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 } // フィールド数3 city: "Tokyo" region: "chiyoda-ku" zipcode: 1111111 同じドキュメントへの書き込みは1秒間に1回まで 複数人が同時に1つのドキュメントへ書き込みをしようとすると失敗する 異なるドキュメントへ同時に書き込むのはOK ② ドキュメントは部分的に取得することができない 例えば以下のようなコレクションで本のタイトルだけ取得したい時、同じドキュメント内に本文(contents)が入っているので同時にそれも取得してしまいます。 アプリのパフォーマンス低下 メモリ消費がすごいことになる 電池消費もすごいことになる といったことになってしまい、ユーザーがアンインストールしてしまう原因になるので注意が必要です。 セキュリティルールも同様にドキュメントを部分的に保護することができません。 同じドキュメント内のフィールドで別々のルールを設けたい場合別ドキュメントとして保存する必要があります。 ③ クエリは深い階層のデータまで取得することができない あるドキュメントを取得した時、そのドキュメントのサブコレクションを取得することはありません。 これは余計な読み込みをしない点でメモリ消費の節約にもなる一方、 そのサブコレクションを読み込みたい場合更にクエリを発行しなければいけないためコードが煩雑になる危険を孕んでいます。 ②の例では本のcontentsはチャプターごとのサブコレクションを作って読み込むのが良さそうです。 ここで登場人物もサブコレクション化している場合を考えてみます。 本の詳細を表示するページで登場人物を表示しないのであれば1回の読み込みで足りますが、 登場人物を一緒に表示させたいと思った場合、登場人物のドキュメントの数の回数分読み込まなくてはいけなくなります。 ④ 使用料金は読み込みと書き込みの回数で決まる ③のルールと関連してきますが、サブコレクションが何重にもなっているとその分読み込み回数が増え、料金が増えてしまいます。 なので、一緒に表示させたいデータは基本的に同じドキュメントに入れるのが良いとされています。 しかし、サブコレクションにしたほうが良い場合もあるため費用と効果のバランスを検討しながらデータ構造を決定する必要があります。 ⑤ クエリは1つのコレクション内のドキュメントしか探すことができない → クエリは複数のコレクションにまたがってドキュメントを探すことができる 例えば上記の例で「"GreatExpectations"のPから始まるキャラクターを探す」というのは以下のようにできます。 //"GreatExpectations"のPから始まるキャラクターを探す collection("dickens_books/great_exp/characters") .where(name >= "P") .where(name < "Q") 当初クエリは1つのコレクション内でしかドキュメントを探すことができず、「全ての本の中からキャラクターの名前がOliverの本を探す」というのは複数のコレクションにまたがって探す必要があるためできませんでした。 が、2019年に改善され、今は以下のコードでできるようになっています。 //全ての本の中からキャラクターの名前がOliverの本を探す collectionGroup("characters") .where("name", "==", "Oliver") 注意点 CollectionGroupクエリの実行にはインデックスの設定が必要 一度クエリを実行するとコンソールにURLが表示されるため、そこから簡単に登録できる Firebase Consoleから自分で設定することも可能 インデックスは最大200まで登録可能 同じ名前のコレクション全てでクエリを実行してしまうので命名には気をつけること 上記の例だと全ての "characters" コレクションでクエリを実行している 公式ドキュメント https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query インデックスについてはこちら https://firebase.google.com/docs/firestore/query-data/indexing ではCollectionGroupクエリが使える以前はどうやって「全ての本の中からキャラクターの名前がOliverの本を探す」ことをしていたのかというと2通りのやり方があります。 1. Collection内にmapを保持する方法 //全ての本の中からキャラクターの名前がOliverの本を探す collection("dickens_Books") .where("characters.Oliver", ">=", "") 「occupationがsocialiteのキャラクターを探す」「名前順に並び替える」というようなクエリは実行不可能であり、検索オプションが限られている Map内fieldの値をキーとするのは間違えやすくエラーが起きやすい ドキュメント内の不必要な情報も一緒に取ってきてしまう という理由で推奨されていません。 2. charactersコレクションをtop-levelのコレクションにする //全ての本の中からキャラクターの名前がOliverの本を探す collection("characters") .where("name", "==", "Oliver") //occupationがsocialiteのキャラクターを探す collection("characters") .where("occupation", "==", "Socialite") //"GreatExpectations"の登場人物を探して名前順に並び替える //2つ以上のクエリを実行するにはインデックスの設定が必要 collection("characters") .where("book", "==", "great_exp").orderBy("name") 幅広い検索が容易にできるためこちらの方法が推奨されていました。 top levelのコレクションから検索する場合検索量が増えてパフォーマンスが悪くなるのではないかと考える人もいると思いますが、サブコレクションを検索する場合と速度にほとんど差はないそうです。 結局どれを使えば良いのか 現時点(2021年5月2日時点)では、以下がどの方法を使うかを決めるガイドラインだと言っています。 ドキュメントの中のあるフィールドについて検索したい場合コレクションに追加する サブコレクションでもtop-levelのコレクションでも可能 サブコレクションごとに検索することが多く、たまにcollectionGroupクエリを使いたい時はサブコレクション 全てのドキュメントにまたがって検索することが多く、サブコレクションごとに検索する場合が少ない時はtop-levelのコレクション 親ドキュメントを読み込んだ際に必ずそのデータが必要になる時はmapを使う 簡単なフローチャートを作成してみました。 ⑥ Arrayの挙動が異質 Firestoreでは同じデータに複数の端末からアクセスすることが可能であり、 インデックス操作でデータを書き換えることが容易なArrayは現在どのフィールドで何の書き込みがされているのか把握することが難しく、思った通りの結果が得られない可能性があります。 例えばAさんが配列の2番目に書き込みをしようとしている時に、Bさんは配列の2番目の要素を削除しようとしており、更にCさんは配列の一番最初にデータを挿入しようとしている場合、それぞれで違う結果が得られてしまいクラッシュしてしまいます。 そのため以下のような配列操作や検索方法は使えません。 my_array[2] = "tackle" my_array.deleteAt(2) my_array.insertAt(0, "hello") collection.where(my_array[2] == "larry") じゃあArrayはどのように使えばいいのかというと、flagの管理用として使用します。 以下は"drama"というキーワードを持つ本を検索しています。 カテゴリ分けの際に使えそうです。 collection("dickens_books").where("keywords", "array-contains", "drama") とはいえ、現在はインデックスに関係なく配列にデータを追加する、削除するarrayUnionとarrayRemoveが追加されたので異なる結果になりクラッシュするということは無くなったと言っています。 詳細はこちら https://firebase.google.com/docs/firestore/manage-data/add-data まとめ Firestoreデータ構造の決め方 動画の最後でデータ構造を決めるガイドラインについて触れていました。 同時に使用するデータは全て同じドキュメント内に保存する 丁度いいサイズで格納する 例として1つの画面表示のために2つの異なるデータベースから30ドキュメントを取得する必要がある設計は細かすぎると言っていました 一部を検索する必要があるもの、今後データが増えていく可能性があるものはコレクションにする 逆にそのデータから親ドキュメントの中身を検索したい場合はmapを親ドキュメントに格納する 住所や位置情報など関連が深いデータのまとまりもmapを使うと良い フィールドで保存する場合よりもキーワードのコンフリクトを避けることができる flagを管理したいときはArrayを使う また動画の続きを引き続き記事にしていきたいと思います。 次はレビュー機能、お気に入り機能付きのレビューアプリのデータ構造を考えていくという実践的な内容です。 間違っている箇所などありましたらコメントでお知らせください。 参考になった記事 【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ Cloud Firestore公式Youtube
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む