20210503のSwiftに関する記事は16件です。

MVPでGitHubのAPIを叩いてTableViewに結果を表示するアプリを作ってみた <復習用>

はじめに タイトルにある通り、今回はMVPでGitHubのAPIを叩いて結果を表示するアプリを作りました。 MVPはレガシーになってきており、アーキテクチャの主流はMVVMらしいですが、そこを学ぶ前段階としてまずはMVP!ということで。 復習がてらに、部分的に載せて解説していきます! ちなみに、参考にしたのは名著「iOSアプリ設計パターン入門」です! MVPの大まかな構図 ①ViewからUI操作を受けて、Presenterへ通知。 ②PresenterがModelを操作。 ③PresenterがViewに受け取ったModelの結果を渡す。 これをイメージとして持っておくと理解がしやすいです。 View まずはViewから。 Viewは描画処理のみを担当します。いつViewが変更されるかなどは知りません。また、PresenterにUI操作の通知を行います。 カスタムCellは省きます。 MVPSearchController.swift import UIKit final class MVPSearchController: UIViewController { @IBOutlet private weak var indicator: UIActivityIndicatorView! @IBOutlet private weak var tableView: UITableView! { didSet { tableView.register(UINib(nibName: "RepoCell", bundle: nil), forCellReuseIdentifier: "RepoCell") tableView.dataSource = self tableView.delegate = self } } @IBOutlet private weak var searchText: UITextField! override func viewDidLoad() { super.viewDidLoad() tableView.isHidden = true indicator.isHidden = true } // Presenterのインスタンスを保持 private var presenter: GitHubPresenterInput! // Routerに繋いでもらう処理がある func inject(presenter: GitHubPresenterInput) { // Presenterと繋がる self.presenter = presenter } // 検索ボタンタップ @IBAction func search(_ sender: Any) { guard let searchText = searchText.text else { return } // Presenterに通知 presenter.search(with: searchText) } } extension MVPSearchController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // Presenterに通知 presenter.selected(index: indexPath.row) } } extension MVPSearchController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { // Presenterに通知 return presenter.numberOfItems } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "RepoCell") as? RepoCell else { return UITableViewCell() } let repositoly = presenter.getRepo(index: indexPath.row) cell.configure(repositoly: repositoly) return cell } } extension MVPSearchController: GitHubPresenterOutput { func upDataRepsitory(_ repository: [Repository]) { DispatchQueue.main.async { self.tableView.reloadData() } } func upData(load: Bool) { DispatchQueue.main.async { self.tableView.isHidden = load self.indicator.isHidden = !load } } func get(error: Error) { print(error.localizedDescription) } func showWeb(Repositoly: Repository) { DispatchQueue.main.async { Router.shared.showWeb(from: self, repositoly: Repositoly) } } } UI操作をPresenterに通知して結果を受け取っています。先ほどの構図通りですね。 また、ここでのポイントは具体的な描画処理をProtocolとして切り分けている所ですね。こうして責務を切り分けることで、テスト容易性が担保されていきます。 GitHubPresenterOutputの処理が呼ばれるタイミングはロジックを担当するPresenterの役割なので、Viewが知る必要はないため、中身だけ実装しておきます。 Presenter 続いて、Presenterです。 Presenterはロジック部分を担当します。Viewからの通知を受けて、Modelの情報を返すのが主な役割です。 GitHubPresenter.swift import Foundation // アクションを受け入れる用のプロトコル protocol GitHubPresenterInput { var numberOfItems: Int { get } func getRepo(index: Int) -> Repository func selected(index: Int) func search(with text: String?) } // アクションを加える用のプロトコル protocol GitHubPresenterOutput: AnyObject { func upDataRepsitory(_ repository: [Repository]) func upData(load: Bool) func get(error: Error) func showWeb(Repositoly: Repository) } class GitHubPresenter { // アクションを加えるプロトコルに準拠したインスタンスを保持(Viewを保持) private weak var output: GitHubPresenterOutput! // API通信用に準拠したインスタンスを取得 private var api: GitHubAPIProtocol // モデルを保持 private var repositoly: [Repository] init(output: GitHubPresenterOutput, api: GitHubAPIProtocol = GitHubAPI.shared, repositoly: [Repository] = []) { self.output = output self.api = api self.repositoly = repositoly } } // PresenterはInputを受けてOutputするイメージ(外から通知を受けて外に通知を出す) // outputが具体的に何をしてるかは知らなくても良い extension GitHubPresenter: GitHubPresenterInput { // 数を数えて!という通知に対して var numberOfItems: Int { return repositoly.count } // 指定されたindexのレポジトリを教えて!という通知に対して func getRepo(index: Int) -> Repository { return repositoly[index] } // 指定されたindexのレポジトリが選択された!という通知に対して // webを開く通知をする func selected(index: Int) { self.output.showWeb(Repositoly: repositoly[index]) } // 指定されたtextをもとに検索して! func search(with text: String?) { guard let text = text, !text.isEmpty else { return } // ロード中を通知 self.output.upData(load: true) self.api.getRepository(searchWord: text) { [weak self] result in // ロード終了を通知 self?.output.upData(load: false) // モデルを操作してViewに結果を渡す switch result { case .success(let repositoly): self?.repositoly = repositoly self?.output.upDataRepsitory(repositoly) case .failure(let error): self?.output.get(error: error) } } } } さきほどの構図通り、PresenterはViewを変数outputとして保持し、Modelを変数repositolyとして保持していますね。ViewがPresenterに対して通知するときに呼び出されるGitHubPresenterInputプロトコルのメソッドに応じて、Viewに対して描画指示を出しています。 output(View)が行う具体的な処理内容の記述はViewのファイルに記述してます。 Model 続いてはModelですね。 Repository.swift import Foundation struct Item: Codable { let items: [Repository]? } struct Repository: Codable { let fullName: String var urlStr: String { return "https://github.com/\(fullName)" } enum CodingKeys: String, CodingKey { case fullName = "full_name" } } Modelにはロジックは持たせていません。そのかわり、API通信用のプロトコルに準拠させた新たなクラスを作り、そこに通信用の処理を記述します。Modelは値の受け渡し専用みたいな感じです。 そうすることで責務の切り分けができます。 通信用のクラス 続いてはAPI通信用のシングルトンのクラスですね。 GitHubAPI.swift import Foundation enum GitHubError: Error { case error } // 実際に検索処理をするプロトコル protocol GitHubAPIProtocol { func getRepository(searchWord: String, completion: ((Result<[Repository], GitHubError>) -> Void)?) } final class GitHubAPI: GitHubAPIProtocol { static let shared = GitHubAPI() private init() {} func getRepository(searchWord: String, completion: ((Result<[Repository], GitHubError>) -> Void)?) { guard searchWord.count >= 0 else { completion?(.failure(.error)) return } guard let url = URL(string: "https://api.github.com/search/repositories?q=\(searchWord)&sort=stars") else { return } // API通信 let task = URLSession.shared.dataTask(with: url) { (data, response, error) in guard let data = data else { return } guard let repositolies = try? JSONDecoder().decode(Item.self, from: data) else { return } guard let repositoly = repositolies.items else { return } if error != nil { completion?(.failure(.error)) return } completion?(.success(repositoly)) } // 検索処理実行 task.resume() } } GitHubAPIProtocolに準拠させて、デリゲートメソッドの中身はGitHubAPI内に記述しています。「iOSアプリ設計パターン入門」ではModel内にこの処理を記述しています。その方が先ほどの構図通りですが、責務を切り分けるために、シングルトンクラスを用意しました。 Presenterでデータを取得したいときは、GitHubAPIクラスのgetRepository()を呼んで、取得します。構図通りですね。 Router 続いては、画面遷移の処理をまとめるRouterクラスですね。 Router.swift final class Router { static let shared = Router() private init() {} private var window: UIWindow? // MVPSearchViewControllerを表示 func showRoot(window: UIWindow) { guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? MVPSearchController else { return } // vcとpresenterを参照させ合う(presenterは弱参照でvcを保持) let presenter = GitHubPresenter(output: vc) vc.inject(presenter: presenter) let nav = UINavigationController(rootViewController: vc) window.rootViewController = nav window.makeKeyAndVisible() self.window = window } // 次の画面へ遷移 func show(from: UIViewController, to: UIViewController) { if let nav = from.navigationController { nav.pushViewController(to, animated: true) } else { from.present(to, animated: true, completion: nil) } } // show()を使ってWebViewControllerに画面遷移 func showWeb(from: UIViewController, repositoly: Repository) { guard let webVc = UIStoryboard(name: "Web", bundle: nil).instantiateInitialViewController() as? WebViewController else { return } let presenter = WebPresenter(output: webVc, repositoly: repositoly) webVc.inject(presenter: presenter) show(from: from, to: webVc) } } AppDelegateでshowRoot()を呼んで、MVPSearchViewControllerを表示します。 showRoot()では、MVPSearchViewControllerのインスタンスを生成し、inject()を呼んで, ViewであるMVPSearchViewControllerとPresenterを繋ぎます。そして、ナビゲーションコントローラーから遷移してMVPSearchViewControllerが表示されるという処理が記述されてますね。 showWeb()では、WebViewControllerのインスタンスを生成し、inject()を呼んで、ViewであるWebViewControllerとWebPresenterを繋ぎます。そして、画面遷移するメソッドであるshow()を使ってWebViewControllerに遷移するという処理が記述されてますね。 View(2) 続いては、WebViewControllerですね。ここではレポジトリのURLを叩いてWebサイトを表示します。 WebViewController.swift import UIKit import WebKit final class WebViewController: UIViewController { @IBOutlet private weak var webView: UIWebView! // presenterのインスタンスを保持 private var presenter: WebPresenter! override func viewDidLoad() { super.viewDidLoad() // viewDidLoaded()の中身をVCが知る必要はない presenter.viewDidLoaded() } func inject(presenter: WebPresenter) { self.presenter = presenter } } extension WebViewController: WebPresenterOutput { func load(request: URLRequest) { DispatchQueue.main.async { self.webView.loadRequest(request) } } } PresenterにviewDidLoaded()で通知して、処理を仰ぎます。そうすると、Presenterはload()を呼んで、Webサイトを表示します。 Presenter(2) 続いては、WebPresenterクラスですね。 WebPresenter.swift import Foundation // アクションを受け入れるプロトコル protocol WebPresenterInput { func viewDidLoaded() } // アクションを加えるプロトコル protocol WebPresenterOutput: AnyObject { func load(request: URLRequest) } final class WebPresenter { private weak var output: WebPresenterOutput! private var repositoly: Repository init(output: WebPresenterOutput, repositoly: Repository) { self.output = output self.repositoly = repositoly } } extension WebPresenter: WebPresenterInput { func viewDidLoaded() { guard let url = URL(string: repositoly.urlStr) else { return } output.load(request: URLRequest(url: url)) } } viewDidloaded()の中身はここで記述してあげます。ViewはviewDidloaded()を使って通知するだけなので中身を知る必要がありませんでした。 まとめ 以上で、MVPでGitHubのSearchAPIを叩いてレポジトリを表示するアプリをMVPで作る過程は終了です。 「iOSアプリ設計パターン入門」を主に参考にさせてもらいました。 とてもいい復習になりました!ありがとうございました〜^o^ 参考 『iOSアプリ設計パターン 入門』 第6章 MVP 関義隆、史翔新、田中賢治、松館大輝、鈴木大貴、杉上洋平、加藤寛人
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面遷移 まとめ swift

■Navigation Controllerの画面遷移 オレンジの画面にNavigation Controllerを使用すると上部にbarが出る。 オレンジの画面にButtonを置く Buttonを押すと緑の画面に遷移する方法 1 オレンジ画面のButtonを選択 Controlを押しながら青い線を伸ばし緑の画面で離す。 Action Segue(画面の現れ方)を選択する。 Show(左からスライドで遷移) Present Modally(下からニョキっと遷移) この二つが一般的 ■show選択時 左から画面がスライドで遷移 緑の画面左上に<Backが現れる。(Navigation Controllerの画面遷移の時のみ) <Backを押すと画面がオレンジに戻る。 ■Present Modally 下からニョキっと遷移(画面が重なってるみたい) <Back ボタンはついてこない作るしかない 前の画面が微かに見えるのでこれを見えないようにできる。 オレンジの画面と緑の画面の真ん中の矢印を選択 右メニューの Presentationを Full Screenにする。 じゃあ見えなりフルスクリーンで画面遷移する。 2Present Modallyの画面の戻り方 ■showではBACKボタンがデフォルトで出る。 ■Present ModallyBACKボタンがない。 Present Modallyでの画面の戻り方を紹介します。 戻り方(コードを使う) 準備 緑の画面のViewControllerを作り紐づける。 仮にNextViewControllerとする。 緑の画面にもNavigation Controllerを使用し右上にBarItemをつける。(Done) Doneボタンを押して戻る。 ① dismissを使う NextViewControllerに DoneボタンをActionボタンとして紐づける。 その中に dismiss(animated: true, completion: nil) これで戻れるが どっちかっていうと緑の画面を閉じてる動きです。 3任意のタイミングで画面遷移したい コードを使う 1ではButtonを選択してControlを押してひっぱていた 今回は引っ張る場所が違う オレンジ画面の上部青い場所(ViewControllerからControlを押して引っ張る) 緑の画面で離し同様にshowかpresent Modallyを選択 違うのはこの時点では何をしたら画面遷移するか決めていない。 ■準備 間の青い矢印を選択(segue) 右メニューのidentifier の名前を決める今回はsegueとする。 ■コード準備 オレンジの画面と紐づいているViewControlleを開く ButtonをOutletでViewController.swiftと紐づける。 Buttonを押すと画面遷移ようにコードに書く performSegue(withIdentifier:"さっきつけたIdentifierの名前", sender:nil) これでButtonを押すと画面遷移する。 4UnwindSegueを使用した画面遷移 ■こんな時に使える 2つ以上前の画面に戻る。 画面遷移した時に何かしたい値を渡して表示したり オレンジ画面と紐づいている viewController.swiftに下記のコードを書く @IBAction func メソッド名(segue:UIStoryboardSegue){ } メソッド名を例えばexitとする。 緑の画面のDoneを選択Controlを押しながら青い線を引っ張り 緑画面上部右のExitに持っていく さっき作ったexitメソッドに繋がっている。 exitWithSegueを選択 これでとりあえず画面が戻る。 Next View Controllerに Unwind segue to "exitWithSegue" と出ていれば成功。 5UnwindSegueの応用 値を渡して画面遷移 ■やりたいこと 緑画面のtextFieldに書いたtextを 緑画面のButtonを押したら オレンジ画面のLabelに表示する。 ■準備 下記のようにItemを画面に準備する。 ■オレンジ画面のViewController.swiftに下記のコードを足す class ViewController: UIViewController { @IBOutlet var label: UILabel! @IBAction func aaa(segue: UIStoryboardSegue) { } } ■緑画面上部左を選択 2つ右のExitまでControlを押しながら青い線を引っ張って離す。 先ほど作ったaaaメソッドを選択 ■UnwindSegue aaaWithSegueの右メニューidentifierに名前をつける。 仮名(back) これで3で使用した下記コードを使用すれば 好きなタイミング場所で画面遷移できる。 performSegue(withIdentifier:"back", sender:nil) ■Buttonを押すと 変数ContentにtextFieldに書いてあった文字を入れ込んで 画面遷移する。 ■オレンジ画面ののコード @IBAction func aaa(segue:UIStoryboardSegue){ //画面先をオプショナルバインディングをして定数に if let nextVC = segue.source as? NextViewController {      //nextVCの変数Contentをラベルに表示 label.text = nextVC.content } } ■これで一連の流れができます。 緑画面のtextFieldに文字書く オレンジ画面のラベルに表示 6 画面遷移時に値を渡す方法 その他 ■AppDelegate.swift に変数を保存できる。 変数を他classで使う コンピューテッドプロパティで変数に入れ込む あとはオプショナルバインディングをして変数を呼び出せる。 private var appDelegate: AppDelegate? { UIApplication.shared.delegate as? AppDelegate } ■segueが実行される前に勝手に発動するメソッド override func prepare(for segue: UIStoryboardSegue, sender: Any?) { //segueのidentifierが"segue"なら実行 if segue.identifier == "segue" { // 遷移先ViewCntrollerの取得 let nextVC = segue.destination as! NextViewController // nextVCの変数に現在の画面のlabelのtextを入れ込む等 nextVC.Content = label.text! } } 7 UITabBarController どんな時に使える? 画面同士が並列に並ぶ時 親子関係ではない画面遷移 例:音楽アプリでジャンル別で画面が存在する等 準備 始めのViewContorollerを消す +マークから TabBarControllerを選択して持ってくる。 ■次に左のTabBarControllerを選択し右メニューの is initial View Controllerにチェックマーク 一度右側縦に並んだ2つの画面を消し +マークから新たなUIViewControllerで二つの画面を出す。 そこにTab BarControllerからControlで青い線を引っ張り画面遷移したい画面を選ぶ 今回はshowやPresentModallyではなく Relationship Segueの中の ViewControllersをお互い選択 あとは画面遷移したい画面に新たなClassのファイルを各々作りお互い紐づければ完成 ビルドすると初めの画面は緑の画面になり下のitemButtonで画面を切り替えれる。 以上まとめです。 何かあればまた更新します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】VIPERとは

はじめに 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をご覧ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIWindowのmakeKeyAndVisualとisHiddenの違いについて

makeKeyAndVisualとは func makeKeyAndVisible() This is a convenience method to show the current window and position it in front of all other windows at the same level or lower. If you only want to show the window, change its isHidden property to false. 現在のウィンドウを表示させ、同じレベルかそれ以下の他のすべてのウィンドウの前に位置させるメソッド。ウィンドウを表示するだけの場合は、isHiddenプロパティをfalseに変更するとされています。この場合はwindowとして表示させるのみで、位置に関しては関係しません。 isHiddenとは var isHidden: Bool { get set } このプロパティの値をtrueに設定すると、レシーバーが非表示になり、falseに設定すると、レシーバーが表示されます。既定値は false です。ウィンドウの対応するビューを非表示にすると、そのビューの次の有効なキービューが新しい最初のレシーバーになります。 これらを確かめてみたのが次です。画面が表示された後に、Dispatch.main.asyncメソッドで3秒後に戻しています。KeyWindowに表示されているのは、ボタンを押した直後の画面です。 keyWindowとは var keyWindow: UIWindow? { get } This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message. このプロパティは、windows配列の中で、直近にmakeKeyAndVisible()メッセージが送られたUIWindowオブジェクトを保持します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIWindowのmakeKeyAndVisibleとisHiddenの違いについて

makeKeyAndVisibleとは func makeKeyAndVisible() This is a convenience method to show the current window and position it in front of all other windows at the same level or lower. If you only want to show the window, change its isHidden property to false. 現在のウィンドウを表示させ、同じレベルかそれ以下の他のすべてのウィンドウの前に位置させるメソッド。ウィンドウを表示するだけの場合は、isHiddenプロパティをfalseに変更するとされています。この場合はwindowとして表示させるのみで、位置に関しては関係しません。 isHiddenとは var isHidden: Bool { get set } このプロパティの値をtrueに設定すると、レシーバーが非表示になり、falseに設定すると、レシーバーが表示されます。既定値は false です。ウィンドウの対応するビューを非表示にすると、そのビューの次の有効なキービューが新しい最初のレシーバーになります。 これらを確かめてみたのが次です。画面が表示された後に、Dispatch.main.asyncメソッドで3秒後に戻しています。KeyWindowに表示されているのは、ボタンを押した直後の画面です。 keyWindowとは var keyWindow: UIWindow? { get } This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message. このプロパティは、windows配列の中で、直近にmakeKeyAndVisible()メッセージが送られたUIWindowオブジェクトを保持します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】構造体とは??

構造体とは? 値を保持するためのプロパティや関連する機能のメソッドなどをまとめて定義しておいて、必要な時にその struct のインスタンスを生成して使うことができます。 classと同じような働きをしていますが、structは継承やデイニシャライズができないなどの違いがあります。 定義方法 struct 構造体名 { プロパティ宣言 イニシャライザ定義 メソッド定義 } サンプルコード //structを定義 struct Student { //プロパティ宣言 var name:String var age:Int var points:[Int] //メソッドを定義 func ave() -> Float { var sum = 0 for i in 0...2 { sum += points[i] } return Float(sum) / 3 } //メソッドを定義 func printInfo() -> Void { let ave = self.ave() let phrase = "氏名は\(name)です\n試験の平均点は\(ave)です。" print(phrase) } } //インスタンス化時に引数に値を置くことで、イニシャライザで初期値を与える(今回はメンバーワイズイニシャライザを使用) var stu = Student(name: "田中", age: 15, points:[70, 79, 85]) //printInfo関数を実行 stu.printInfo()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIScrollView swift

■UIScrollView 名の通り画面をスクロールさせる奴 基本的にアプリをつくる際sclolViewを敷いていれば急な対応に柔軟に対応できる! 例:画面幅いっぱいの中に急に新しい項目を追加したい時にスクロールで画面幅を広げれば一応画面内に 項目を追加できるので一時的に対応はできる。 その後デザイン修正などが行われる。 ■UIScrollViewが持つパラメーター パラメーターと言われれば何の事か想像しづらいが UIScrollViewが持っている領域の事である。 ■ContentLayoutGuide スクロール領域 画面をスクロールできる領域の事である。 まあ領域展開みたいな物ww ■FrameLayoutGuide UIScrollViewの領域 実際のUIScrollViewの範囲の事 これを踏まえて作っていく 1 ■画面にUIScrollViewを貼り四角ぞれぞれ0の制約をつける。 エラーでるけど無視でOK! スクロール領域決まってないけど決まってない範囲にどやって制約つけるねんって言ってる! 2 ■UIScrollViewの中にViewを入れる。 とりあえず何も考えず中にViewを入れよう! このViewの名前はContentViewなどにしておこう! 3 ■中に入れたContentViewの四角をContentLayOutGuideと合わせよう! ほなContentViewがスクロール領域になる。 Comand押しながらContentViewとContentLayOutGuide二つを選択して 右下らへんのAdd New Alignment Constraintsを選択 画像のように Leading Edges Trailing Edges Top Edges Bottom Edges にチェックマークをつけAdd 4 Constraintsを押す。 4 ■ContentViewのwidthとheightを決める。 最終的にContentViewのwidthとheightが全体の画面をスクロールできる領域になる! 例:必ずしもではないがContentViewのwidthとheightをFrameLayoutGuideに合わせて作ることもできる。 ContentViewを選択controlを押して青い線を伸ばしつつFrameLayoutGuideを選択 EqualWidth EqualHeight を選択する。 ContentViewの横幅と縦をFrameLayoutGuideに合わせるよ! そして制約の 横幅 ContentView.Width = FrameLayoutGuide の右メニューのMultiliperの数値を変更すれば横のスクロール領域が増える。 Multiliper1なら1画面分の横幅 Multiliper2なら2画面分の横幅 縦幅 ContentView.height = FrameLayoutGuide の右メニューのMultiliperの数値を変更すれば縦のスクロール領域が増える。 Multiliper1なら1画面分の縦幅 Multiliper2なら2画面分の縦幅 もちろんただ単にContntViewのwidthとheightのに数値を与1画面のでかさを越えればその分スクロール領域になる。 ■実際の応用編 StackViewを使用 上記の1の工程を終える。 ScrollViewの中に二つViewを入れる。 Page1 Pag2とする。 このPage1とPage2をStackViewに入れ込む。 2 stackViewに制約をつける。 top,trailing,bottomはScrollViewに合わせる。 leadingのみFrameLayoutGuideに合わせる。 widthをContentLayOutGuideに合わせる。 3 stackViewの中身 Page1 Page2 のそれぞれの幅をFrameLayOutGuideと合わせる。 EqualWidth EqualHeightを使う。 4 Page1Page2の二つのviewのheightとwidthの幅を変更 Page1.width = 0.57971 x Frame Layout Guide.width Page1.height = 0.156479 x Frame Layout Guide.height Page2.width = 0.57971 x Frame Layout Guide.width Page2.height = 0.156479 x Frame Layout Guide.height のそれぞれの制約の右メニューのMultiliperの数値を変更1にした場合。 FrameLayOutGuideの1画面分のデカさの縦幅横幅2枚分がstackViewに入ることになり スクロールできるようになりエラーが消える。 5 ■StackViewの場合 右メニューAxisを変更するだけで横縦とスクロール領域を変更できる。 Vertical(縦) Horizontal(横) Horizontalにすると 以上UIScrollViewの作り方でした!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UISclorView swift

■UISclolView 名の通り画面をスクロールさせる奴 基本的にアプリをつくる際sclolViewを敷いていれば急な対応に柔軟に対応できる! 例:画面幅いっぱいの中に急に新しい項目を追加したい時にスクロールで画面幅を広げれば一応画面内に 項目を追加できるので一時的に対応はできる。 その後デザイン修正などが行われる。 ■UISclolViewが持つパラメーター パラメーターと言われれば何の事か想像しづらいが UIScrolViewが持っている領域の事である。 ■ContentLayoutGuide スクロール領域 画面をスクロールできる領域の事である。 まあ領域展開みたいな物ww ■FrameLayoutGuide UIScrolViewの領域 実際のUIScrolViewの範囲の事 これを踏まえて作っていく 1 ■画面にUISclolViewを貼り四角ぞれぞれ0の制約をつける。 エラーでるけど無視でOK! スクロール領域決まってないけど決まってない範囲にどやって制約つけるねんって言ってる! 2 ■UIScrolviewの中にViewを入れる。 とりあえず何も考えず中にViewを入れよう! このViewの名前はContentViewなどにしておこう! 3 ■中に入れたContentViewの四角をContentLayOutGuideと合わせよう! ほなContentViewがスクロール領域になる。 Comand押しながらContentViewとContentLayOutGuide二つを選択して 右下らへんのAdd New Alignment Constraintsを選択 画像のように Leading Edges Trailing Edges Top Edges Bottom Edges にチェックマークをつけAdd 4 Constraintsを押す。 4 ■ContentViewのwidthとheightを決める。 最終的にContentViewのwidthとheightが全体の画面をスクロールできる領域になる! 例:必ずしもではないがontentViewのwidthとheightをFrameLayoutGuideに合わせて作ることもできる。 ContentViewを選択controlを押して青い線を伸ばしつつFrameLayoutGuideを選択 EqualWidth EqualHeight を選択する。 ContentViewの横幅と縦をFrameLayoutGuideに合わせるよ! そして制約の 横幅 ContentView.Width = FrameLayoutGuide の右メニューのMultiliperの数値を変更すれば横のスクロール領域が増える。 Multiliper1なら1画面分の横幅 Multiliper2なら2画面分の横幅 縦幅 ContentView.height = FrameLayoutGuide の右メニューのMultiliperの数値を変更すれば縦のスクロール領域が増える。 Multiliper1なら1画面分の縦幅 Multiliper2なら2画面分の縦幅 もちろんただ単にContntViewのwidthとheightのに数値を与1画面のでかさを越えればその分スクロール領域になる。 ■実際の応用編 StackViewを使用 上記の1の工程を終える。 SclolViewの中に二つViewを入れる。 Page1 Pag2とする。 このPage1とPage2をStackViewに入れ込む。 2 stackViewに制約をつける。 top,trailing,bottomはScrollViewに合わせる。 leadingのみFrameLayoutGuideに合わせる。 widthをContentLayOutGuideに合わせる。 3 stackViewの中身 Page1 Page2 のそれぞれの幅をFrameLayOutGuideと合わせる。 EqualWidth EqualHeightを使う。 4 Page1Page2の二つのviewのheightとwidthの幅を変更 Page1.width = 0.57971 x Frame Layout Guide.width Page1.height = 0.156479 x Frame Layout Guide.heifht Page2.width = 0.57971 x Frame Layout Guide.width Page2.height = 0.156479 x Frame Layout Guide.height のそれぞれの制約の右メニューのMultiliperの数値を変更1にした場合。 FrameLayOutGuideの1画面分のデカさの縦幅横幅2枚分がstackViewに入ることになり スクロールできるようになりエラーが消える。 5 ■stackViewの場合 右メニューAxisを変更するだけで横縦とスクロール領域を変更できる。 Vertical(縦) Horizontal(横) Horizontalにすると 以上SclolViewの作り方でした!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エラー対処】【ゆめみiOS研修】Swift Unit Test -> Could not cast value of type 'xxx' to 'xxxTests'

株式会社ゆめみ iOS研修 システムエンジニアとして働いているものコードを書く機会が少ないため、プライベートでコードを書く習慣を身につけたいと思い見つけた新人育成用カリキュラム。 Session8でのテストコード作成時に以下のエラーで躓いたため備忘録として記載します。 エラーログ Could not cast value of type 'yumemi.ViewController' (0x108f463f8) to 'yumemiTests.ViewController' テストコードでUIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()よりViewControllerを生成しようとしたが、実行時にエラーとなる。 ログの内容的にTargetの呼び出し先が関係していそう。 バージョン ツール バージョン Xcode 12.4 Swift 5 テストコード ViewControllerTests.swift import XCTest @testable import yumemi class ViewControllerTests: XCTestCase { var viewController: ViewController! override func setUpWithError() throws { viewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() _ = viewController.view } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testExample() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() throws { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } } 解決方法 テスト対象のViewControllerを参照するとTarget MembershipにTest Target(今回はyumemiTests)が含まれていたため、チェックを外したらエラーは解消され、テストコードからViewControllerを参照できるようになった。 その他 解決はしたものの理由は分かっていないため、ご存知の方がいればご教授頂けますと嬉しいです。 まだ綺麗にコードを書けていませんが奮闘中です。 yumemi個人演習用リポジトリ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

intrinsicContentSizeなるものを知った話

きっかけ  先日ViewをUIStackViewで並べて挙動を確認しようと思ったら思い通りに動きませんでした。UIStackViewには縦か横かの方向を決めるaxisプロパティ、どこに揃えて並べるかを決めるalignmentプロパティ、どのように並べるかを決めるdistributionプロパティがあるのですが、axis、alignmentはともかくdistributuonが少し難解。「fillとfillProportinalliyってどうちがうん?」と思いサンプルアプリを作ってみたことがきっかけになりました。 UI部品はそれぞれ大きさを持っている UI 部品はその中身によって大きさが異なります。UILabelであれば、中身のtextが長くなるにつれて、それを表示するためのUIViewも大きくなります。 let あ : UILabel = { let label = UILabel() label.text = "あ" return label }() let あい : UILabel = { let label = UILabel() label.text = "あい" return label }() let あいう : UILabel = { let label = UILabel() label.text = "あいう" return label }() print(あ.intrinsicContentSize)//(16.0) print(あい.intrinsicContentSize)//(31.5,20.0) print(あいう.intrinsicContentSize)//(47.5,20.0) あ、あい、あいうとしてUILabelを作成しそれぞれのintrinsicContentSizeを表示させると、高さは変わりませんが幅は一文字あたり約16ptずつ大きくなっていることがわかります。これらはtextという中身によって定まるもの(intrinsic: 形 固有の、本質的な)なので、UILabelのframeを大きくしても変化することはありません。 ここまで書いたことはUILabelがUIViewをサブクラスに持っていることを考えればすごく当たり前のことかもしれませんが。。。 UIViewのintrinsicContentSizeは? UIViewはどうなっているのでしょうか。 let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) print(view.intrinsicContentSize)//(-1.0,-1.0) (-1,-1)と決まっているようです。 intrinsicContentSizeを変更する intrinsicContentSizeをoverrideします。 class View: UIView { override var intrinsicContentSize: CGSize { return CGSize(width: 100, height: 100) } } print(View().intrinsicContentSize)//(100.0,100.0) intrinsicContentSizeを自由に定める initに引数を加えてintrinsicContentSizeを自由に決められるようにしてみました。 class View: UIView { let width: CGFloat let height: CGFloat init(frame: CGRect,size: CGSize) { self.width = size.width self.height = size.height super.init(frame: frame) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var intrinsicContentSize: CGSize { return CGSize(width: width, height: height) } } print(CGSize(width: 10, height: 10))//(10.0,10.0) 今のところは使い所が不明ですが。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swiftで楽天レシピをAPI通信で実装してみた。

今回はオリジナルアプリケーションを制作する上でAPI通信で実装してみたかったので今回備忘録的に記述していきます。 今回使用するAPIは楽天です。 その中の料理レシピのAPIがあったのでそれを使って通信してみました。 とりあえず途中までのコードです。 今回はMVCモデルで楽天APIから最初はレシピ画像とレシピタイトルを持ってきます。 json形式でデータを持ってきます。 APIのURL https://webservice.rakuten.co.jp/explorer/api/Recipe/CategoryRanking/ ViewController.swift import UIKit import Alamofire import SwiftyJSON import SDWebImage class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource { @IBOutlet weak var tableView: UITableView! var userName = String() override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self jsonAnaly() } var contentArray = [Contents]() func jsonAnaly() { let url = "https://app.rakuten.co.jp/services/api/Recipe/CategoryRanking/20170426?format=json&applicationId=1014272479943576132" AF.request(url, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON { (response) in switch response.result{ case .success: let json:JSON = JSON(response.data as Any) for i in 0...json.count{ let foodImageUrl = json["result"][i]["foodImageUrl"].string let recipeTitle = json["result"][i]["recipeTitle"].string var contentModel = Contents(foodImageUrl:foodImageUrl , recipeTitle:recipeTitle) self.contentArray.append(contentModel) } self.tableView.reloadData() case .failure(let error): print(error) } } } 構造体を使って引数などを持ってきています。 ImageModel.swift import Foundation struct Contents:Codable { var foodImageUrl:String? var recipeTitle:String? } まだ、勉強を初めて2ヶ月ほどなのでいまいちfor文を回すことやAPIのパラメーターの概念がわからなかったので苦労しました。 func jsonAnaly() { let url = "https://app.rakuten.co.jp/services/api/Recipe/CategoryRanking/20170426?format=json&applicationId=1014272479943576132" ここで楽天レシピのAPIのURLを持ってきています。 通信で持ってきたデータをfor文で回します。 for i in 0...json.count{ let foodImageUrl = json["result"][i]["foodImageUrl"].string let recipeTitle = json["result"][i]["recipeTitle"].string var contentModel = Contents(foodImageUrl:foodImageUrl , recipeTitle:recipeTitle) self.contentArray.append(contentModel) } 楽天APIのデータ { "result": [ "result"は配列になっている { "foodImageUrl": "result"の[i]番目の"foodImageUrl" "https://image.space.rakuten.co.jp/d/strg/ctrl/3/34d4ce95b8674c8fb6c8f08b5115464a9f180c31.17.2.3.2.jpg", "recipeDescription": "小鉢がもう1品ほしいなっていう時に簡単でオススメです。", "recipePublishday": "2011/08/22 19:04:07", "shop": 0, "pickup": 1, "recipeId": 1200002403, "nickname": "JIMA88", "smallImageUrl": "https://image.space.rakuten.co.jp/d/strg/ctrl/3/34d4ce95b8674c8fb6c8f08b5115464a9f180c31.17.2.3.2.jpg?thum=55", "recipeMaterial": [ "きゅうり", "ごま油", "すりごま", "鶏ガラスープのもと", "ビニール袋" ], "recipeIndication": "5分以内", "recipeCost": "100円以下", "rank": "1", "recipeUrl": "https://recipe.rakuten.co.jp/recipe/1200002403/", "mediumImageUrl": "https://image.space.rakuten.co.jp/d/strg/ctrl/3/34d4ce95b8674c8fb6c8f08b5115464a9f180c31.17.2.3.2.jpg?thum=54", "recipeTitle": "1分で!うまうま胡麻キュウリ" }, let foodImageUrl = json["result"][i]["foodImageUrl"].string "result"の[i]番目のイメージをlet foodImageUrlに入れる。 ここまでがかなり苦労しました。 今回はここまでです。 続きはまた、近日中に公開します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【swift】get、setってなんのこと?

はじめに 他の人のコード見てるときに、get setって書いてあって何やねんって思ったので、備忘録的な感じで残しておきます。 こちらのコードを見てみてください。 ex protocol priceAndTax { var taxIncludedPrice: Double { get set } } パット見ただのプロパティの定義に見えますが、{ }という変な記述があります。 これはコンピューテッドプロパティといって、値を自分で持たない変数のことで、プロトコルのプロパティはこれしか宣言できません。ちなみに、var num: Int = 1のように自分で値を持つストアドプロパティとよく比較されます。 解説 ここからは、このコンピューテッドプロパティの挙動をItemクラスにプロトコルを準拠させることで確認していきます。 class Item: priceAndTax { var taxExcludedPrice: Double = 100 var taxIncludedPrice: Double { get { return taxExcludedPrice * 1.08 } set { self.taxExcludedPrice = newValue / 1.08 } } } ItemクラスにpriceAndTaxを準拠させるとこんな感じになります。taxExcludedPrice、taxIncludedPriceはそれぞれ税抜価格、税込価格のことです。 taxExcludedPriceやget{ },set{ }の中身はあとから付け足しました。 そしてこのgetの部分はゲッターといい、taxIncludedPriceの値を取得すると呼ばれます。 対してsetの部分はセッターといい、taxIncludedPriceに値を代入すると呼ばれます。 では、実際にゲッターとセッターを呼んで、具体的にその挙動を見ていきましょう。 let snack = Item() snack.taxIncludedPrice = 216 // taxIncludedPriceのsetが呼ばれる print(snack.taxExcludedPrice) // taxIncludedPriceのセッターで、新しく値がセットされてるはず print(snack.taxIncludedPrice) // taxIncludedPriceのgetが呼ばれる 一行ずつ解説していきます。 一行目:Itemクラスでsnackインスタンスを作成しているだけです。 二行目:taxIncludedPriceに値を代入しているので、setメソッドが呼ばれています。newValueといういきなり出てきた変数がありますが、これはセットされる新しい値が自動的に格納される変数のことで、自分で定義する必要はありません。なのでここではnewValueには216が格納されています。そしてnewValue / 1.08で216円の税抜価格、200円がtaxExcludedPriceに格納されます。taxIncludedPrice自体には値が格納されていないということに注意してください!!! 三行目:先程200が格納されたストアドプロパティにアクセスしているので、そのまま200が出力されます。 四行目:print関数でsnack.taxIncludedPriceでtaxIncludedPriceの値を取得しているので、getメソッドが呼ばれます。taxExcludedPriceには200が格納されているので、それの税込価格216が出力されます。ここでも。taxIncludedPrice自体の値を取得しているわけではないということに注意してください。 変数に値が格納されてないのに値が取得できるなんてちょっと不思議ですね〜 最後に これがコンピューテッドプロパティの挙動でした。snackの値段を変更する事を考えたとき、税抜価格と税込価格両方に対してストアドを利用すると二つの変数それぞれに代入しないといけませんが、今回のようにコンピューテッドを利用すれば一行で同時に更新できるので、一方の値を更新し忘れるというミスが事前に防げそうですね。 おまけ プロトコルに定義するコンピューテッドプロパティは、getが必須でsetは任意らしいです。なのでこう書いても大丈夫です。 protocol priceAndTax { var taxIncludedPrice: Double { get } } そうするとtaxIncludedPriceはこう記述できます。 var taxIncludedPrice: Double { get { return taxExcludedPrice * 1.08 } } さらにgetのみの定義だと、getを省略して記述することも可能です。 var taxIncludedPrice: Double { return taxExcludedPrice * 1.08 } さらにさらに一行で値の返却のみをする場合、関数と同じく暗黙のreturnが利用可能なので、 var taxIncludedPrice: Double { taxExcludedPrice * 1.08 } こんなに短くなりました 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Main.storyboardを利用しない場合に注意しなければならない設定

Main.storyboardを使わない場合、各種設定を変更しないとMain.storyboardが見つからないよというエラーが生じる。初心者にはこの辺りの設定が理解できておらずエラーに対処してもわからなさが残るため、いくつかポイントとなる部分だけまとめています。 AppDelegate 通常、新規にプロジェクトを立ち上げた際には、AppDelegate.swiftには次のようにAppDelegateクラスが記述されている。AppDelegate内に直接Main.storyboardの記述がなされているわけでないが、SceneDelegateの絡みで削除しておくべき箇所がある。 import UIKit @main //エントリーポイントとなるattributes:属性 class AppDelegate: UIResponder, UIApplicationDelegate { //UIResponderはクラス //UIApplicationDelegateはプロトコル           // アプリの起動時に呼ばれる func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // アプリケーションの起動後にカスタマイズするためのオーバーライドポイントです。 return true } // MARK: UISceneSession Lifecycle // シーンの起動時に呼ばれる func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // 新しいシーンセッションが作成されるときに呼び出されます。 // このメソッドを使用して、新しいシーンを作成するための構成を選択します。 return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) //SceneConfigurationはinitialViewを指定する //設定はinfo.plistにあり、初期状態では"Default Configuration" (=Main.storyboard) } // シーンの終了時に呼ばれる func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // ユーザーがシーンセッションを破棄したときに呼び出されます。 // アプリケーションが実行されていないときにセッションが破棄された場合は、application:didFinishLaunchingWithOptionsの直後にこれが呼ばれます。 // 廃棄されたシーンに固有のリソースは戻らないので、このメソッドを使って解放してください。 } } application(_:didFinishLaunchingWithOptions:) Tells the delegate that the launch process is almost done and the app is almost ready to run. と書いてあるように、一番はじめのメソッドは起動のプロセスがほぼ終了しているもしくはアプリを走らせるのがほぼ準備できていることをデリゲートに伝える。ここまではいいのだけど、他のメソッドがわかりにくい。この辺りは、一部記載が現在のバージョンにそぐわないものの、こちらの記事がとてもわかりやすくまとめられている。 application(_:configurationForConnecting:options:) Retrieves the configuration data for UIKit to use when creating a new scene. 新しい画面を作る際にUIKitが使う設定データを保持するメソッドとなります。Sceneの種類や管理するためのオブジェクト、最初に表示するViewControllerを含んだStoryboardの情報を扱います。そのため、Main.storyboardを単純に削除すると、設定上のエラーが生じます。初期状態では"Default Configuration"=Main.storyboardであるからです。これはinfo.plistに設定されています。必ず実装しなければならないメソッドではなく、Main.storyboardを使わない場合はinfo.plistと合わせて外しても問題ない要素です。 この辺りは下記のまとめ記事がわかりやすいです。 application(_:didDiscardSceneSessions:) Tells the delegate that the user closed one or more of the app's scenes from the app switcher. ドキュメントに書かれている通り、シーンが1つ以上閉じられたことをデリゲートに伝えるメソッドです。ここで書かれているapp switcherはアプリの切り替えに使う機能ですね。Appleのサイトにわかりやすい例がGIFで載せられています。 SceneDelegate こちらもプロジェクトを新規に立ち上げた際に自動的に組み込まれるファイルです。 class SceneDelegate: UIResponder, UIWindowSceneDelegate { //UIRespodeはクラス //UIWindowSceneDelegateはプロトコル var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // オプションで UIWindow `window` を設定し、提供された UIWindowScene `scene` にアタッチするには、このメソッドを使用します。 // ストーリーボードを使用している場合、`window`プロパティは自動的に初期化され、シーンにアタッチされます。 // このデリゲートは、接続するシーンやセッションが新しいことを意味しません(代わりに `application:configurationForConnectingSceneSession` を参照してください)。 guard let _ = (scene as? UIWindowScene) else { return } } func sceneDidDisconnect(_ scene: UIScene) { // シーンがシステムから解放されるときに呼び出されます。 // これは、シーンがバックグラウンドに入った直後や、セッションが破棄されたときに発生します。 // 次にシーンが接続したときに再作成可能な、このシーンに関連するすべてのリソースを解放します。 // そのセッションは必ずしも破棄されたわけではないので、シーンは後で再接続するかもしれません (代わりに `application:didDiscardSceneSessions` を参照してください)。 } func sceneDidBecomeActive(_ scene: UIScene) { // シーンが非アクティブな状態からアクティブな状態になったときに呼び出されます。 // このメソッドを使用して、シーンが非アクティブだったときに一時停止していた(またはまだ開始していない)タスクを再開します。 } func sceneWillResignActive(_ scene: UIScene) { // シーンがアクティブな状態から非アクティブな状態に移行するときに呼び出されます。 // これは一時的な中断(例:電話の着信)によって起こる可能性があります。 } func sceneWillEnterForeground(_ scene: UIScene) { // シーンが背景から前景に移行するときに呼び出されます。 // 背景に入るときに行った変更を元に戻すには、このメソッドを使います。 } func sceneDidEnterBackground(_ scene: UIScene) { // シーンが前景から背景に移行する際に呼び出されます。 // このメソッドを使用して、データを保存し、共有リソースを解放し、シーン固有の状態情報を十分に保存します。 // シーンを現在の状態に戻すために十分なシーン固有の状態情報を保存します。 } } info.plist info.plistは実行ファイルの構成情報を記述されており、Sceneの各種設定なども記載されているため、プロジェクト内のstoryboardを削除してもinfo.plistが残る場合に、エラーが生じる可能性があります。 特にmain.storyboardを削除した場合は、info.plistの項目のうち関係するものを削除しておく必要があります。探し方としては上記の記事を参考に検索かけるのが手っ取り早いです。これはあくまでHowtoですので、下記にinfo.plistについてまとめていきます。 これがinfo.plistの初期設定画面となります。Main.storyboardに関わる点としては Application Scene Manifest -Scene Configuration --Application Session Role ---Item 0(Default Configuration) ----Configuration Name ----Storyboard Name という階層のConfiguration NameとStoryboard Nameです。これらはAppDelegateのメソッドであるapplication(_:didFinishLaunchingWithOptions:)で指定される Default Configuration であり、そのDefaultの設定がStoryboardではMainというわけです。 これらはMain.storyboardを必要としないのであれば消してしまっても、コードで起動時画面を設定していけば問題ありません。逆に残した状態であると正しく設定しないとエラーが生じてしまいます。試したことありませんが、正しく設定すれば、AppDelegateでログイン有無による起動時画面の切り替えも行えるかもしれません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【FizzBuzz】PythonとSwiftの比較

普段はPythonを専門にしていますが、最近ちょっと飽きてきて気分転換にSwiftの勉強を始めたので、PythonとSwiftの違いについて記事にしました。 企業の選考課題で有名な「FizzBuzz」のアルゴリズムをそれぞれの言語で実装してみましょう。 ※FizzBuzzとは?3の倍数の時にFizz!、5の倍数の時にBuzz!、3と5の倍数の時にFizzBuzz!と出力させるアルゴリズム 1.Pythonで実装する場合 python for i in range(1, 51): if i % 15 == 0: print("FizzBuzz!") elif i % 3 == 0: print("Fizz!") elif i % 5 == 0: print("Buzz!") else: print(i) 2.Swiftで実装する場合 swift for i in 1...50 { if i % 15 == 0 { print("FizzBuzz!") }else if i % 3 == 0 { print("Fizz!") { }else if i % 5 == 0 { print("Buzz!") }else { print(i) } } 3.総評 Pythonでは、範囲を指定するのに「range関数(range(最小値, 最大値+1)」を使用しますが、Swiftでは「最小値...最大値」で範囲を指定できます。 比較演算子等は共通しています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Facemotion3dにおけるUaaLについて

最近、Facemotion3dというiOSアプリをリリースしました。 これは、iOSでフェイストラッキングが出来るアプリです。 このアプリの中には「UaaL」と呼ばれている技術が使われています。 目次 1.UaaLとは 2.UaaLが使用されているアプリ 3.SceneKitとの併用 4.SceneKitの面倒くささ 5.アプリ起動直後ではない方法でUaaLを使用する 6.UaaL実装においてうまく行かなかった点 7.Facemotion3dでUaaLを実装した理由 1. UaaLとは UaaLとは「Unity as a Library」の略で、その名の通りUnityをライブラリとして使うものです。 大元の記述をSwiftというiOSネイティブの言語で記述し、Unityをライブラリ(framework)として使用することで、UI部分はSwiftを使いつつ、3DCGの描画にはUnityを使うというようなSwiftとUnityの良い所取りが出来ます。 Unityでスクロール画面を作るよりも、Swiftでのスクロール速度の方が早かったりする利点があります。また、ARKitを使用する場合、「純正Unityだと30FPSしか出ないのに対して、Swiftだと60FPSを出せる...」という話をTwitter上で見かけたりしたので、本当かは分かりませんがそういう利点があります。 2. UaaLが使用されているアプリ 偉大な先人としてnoppeさんのvearや、Realityなどで使用されています。 そういった記事を参考にさせて頂いてFacemotion3dにもUaaLを使っています。 以下の記事などを参考にさせて頂きました。 https://qiita.com/noppefoxwolf/items/b43d8554142e69c2ada6 https://qiita.com/tkyaji/items/7dbd56b41b6ac3e72635 https://forum.unity.com/threads/integration-unity-as-a-library-in-native-ios-app.685219/ 3. SceneKitとの併用 Facemotion3dでは、Apple標準のビューポートを使用する「SceneKit」とUnityを使用する「UaaL」の併用ということをやっています。 これはどうしてこうなったかというと開発していく流れの中で偶然こうなったという感じです。元々、SceneKitでアプリを作っていて、SceneKitでも十分見栄えのするモデルをモデラーさんが作ってくれました。途中からUaaLについても調べ始めたので後付けでUaaLを載せました。 正確な比較はしたことないので分かりませんが、恐らくメモリの消費量であったり電池の消費量などの面ではUnityを起動しない方が長時間使用する場合、パフォーマンス的にはApple標準のSceneKitを使う方が良いのではないかという気がします。 4. SceneKitを使う上での面倒くささ SceneKitにも利点はありますが、Apple標準のビューポートは機能が少なく、プログラミングする上でのネット上の情報が少ないので3DCG部分について記述する上では圧倒的にUnity(UaaL)で記述する方が楽です。 また、UnityではVRMなどを使うとプログラマーからしてみると簡単にデータをアプリ内に読み込めます。 しかし、SceneKitを使う場合、MayaなどのDCCツールでデザイナーからデータを受け取り、それを良い感じに原点付近に来るようにデータを配置して、ピボット情報を修正し、スケール情報を整え、オブジェクトの命名規則をちゃんとチェックしたりした上で、DAE_FBXなどという聞きなれない拡張子でデータをエクスポートする必要があります。 それだけでなく、DAE_FBXを使うだけではデータをアプリ内に読み込めないので、DAE_FBXをテキストファイルで開き、内部の記述をちょっと書き換える必要があります。 これだけでも面倒くさいのですが、MacのXCodeにDAE_FBXを読み込んだ後に.scnというファイル拡張子に変換し、全てのテクスチャパスを手動で張り替えるという面倒くさい手順が必要でした。 モデル変更などでデザイナーさんが頻繁にデータを更新するので、この作業を何度か繰り返す必要があります。 ここら辺の作業は、今後、AppleとPixarが共同で開発しているUSDZという拡張子が台頭することで簡略化されるのだと思われます。 5. アプリ起動直後ではない方法でUaaLを使用する 参考にさせて頂いたこちらの記事では、アプリ起動直後にUnityが起動する方法が紹介されています。 Facemotion3dでも、前回アプリ終了時にUnityをロードしたまま終了していればアプリ起動直後にUnityが起動する仕組みになっていますが、基本はUaaLではなくSceneKitが起動するようになっています。 前置きが長くなりましたが、ここから実際にFacemotion3d内に記述されているコードを示します。基本的にはnoppeさんの記事の内容を拡張しながら場当たり的に書いていった上に、Swift歴も浅いので関数名の汚さや、記述の汚さが目立つかもしれません...。「適当に書いてたらなんか動いた...!」という類のコードであり、何ら参考にならないかもしれません。 AppDelegate.swift import UnityFramework @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var application: UIApplication? var launchOptions: [UIApplication.LaunchOptionsKey : Any]? var firstLaunchUnity = true var isUnityRunning = false func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) // see notes below for the meaning of Atomic / Non-Atomic SwiftyStoreKit.completeTransactions(atomically: true) { purchases in for purchase in purchases { switch purchase.transaction.transactionState { case .purchased, .restored: if purchase.needsFinishTransaction { // Deliver content from server, then: SwiftyStoreKit.finishTransaction(purchase.transaction) } // Unlock content case .failed, .purchasing, .deferred: break // do nothing @unknown default: print("unknown") } } } self.application = application self.launchOptions = launchOptions let storyboard = UIStoryboard(name: "ViewController", bundle: nil) window?.rootViewController = storyboard.instantiateInitialViewController() window?.makeKeyAndVisible() return true } lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "FACEMOJO for DAZ") container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container }() func saveContext () { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } catch { let nserror = error as NSError fatalError("Unresolved error \(nserror), \(nserror.userInfo)") } } } func firstStartUnity() { Unity.shared.secondInit() if let app = self.application { Unity.shared.application(app, didFinishLaunchingWithOptions: launchOptions) window?.makeKeyAndVisible() } } } class Unity: NSObject, UnityFrameworkListener, NativeCallsProtocol { static let shared = Unity() var unityFramework: UnityFramework //グローバル変数 var myVar = GlobalVar.shared override init() { let bundlePath = Bundle.main.bundlePath let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework" let bundle = Bundle(path: frameworkPath)! if !bundle.isLoaded { bundle.load() } let frameworkClass = bundle.principalClass as! UnityFramework.Type let framework = frameworkClass.getInstance()! if framework.appController() == nil { var header = _mh_execute_header framework.setExecuteHeader(&header) } unityFramework = framework super.init() }   //1度UnityをUnloadした後に読み込む場合に対応 func secondInit() { if !unityIsInitialized() { let bundlePath = Bundle.main.bundlePath let frameworkPath = bundlePath + "/Frameworks/UnityFramework.framework" let bundle = Bundle(path: frameworkPath)! if !bundle.isLoaded { bundle.load() } let frameworkClass = bundle.principalClass as! UnityFramework.Type let framework = frameworkClass.getInstance()! if framework.appController() == nil { var header = _mh_execute_header framework.setExecuteHeader(&header) } unityFramework = framework } } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { unityFramework.register(self) FrameworkLibAPI.registerAPIforNativeCalls(self) unityFramework.setDataBundleId("com.unity3d.framework") unityFramework.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: launchOptions) } private func unityIsInitialized() -> Bool { return ( unityFramework.appController() != nil ) } func unloadUnity() { if unityIsInitialized() { unityFramework.unloadApplication() if let appDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.firstLaunchUnity = true } } } //Unity側からSwiftに対して送られてくる関数-関数名はUaaLサンプルの初期設定のまま使用 func showHostMainWindow( _ color: String!) { //Unityを読み込み終わったことを示すフラグ self.myVar.UnityLoadFlag = true } func sendUnityMessage(objectName:String,functionName:String,message:String) { unityFramework.sendMessageToGO(withName: objectName, functionName: functionName, message: message) } var view: UIView { unityFramework.appController()!.rootView! } } class GlobalVar { private init() {} static let shared = GlobalVar() var UnityLoadFlag = false var unityFirstLoadFlag = true } ViewController.swift //グローバル変数 var myVar = GlobalVar.shared //Unityを起動する関数,適当なところで呼び出す func startUnity() { //GUI周りの表示切り替え DispatchQueue.main.async { if self.myVar.unityFirstLoadFlag == true { //Unityロゴを隠さないようにするための処理 //UaaLを使うとUnityロゴを隠せてしまうが、それを避ける処理 //self.tappedFunction() } //ロード時のくるくる開始 //self.activityIndicator.startAnimating() //self.activityIndicator.isHidden = false } //loadingUnity = true var waitTimeAfterStopUnity = 0.0 if self.myVar.UnityLoadFlag == true { waitTimeAfterStopUnity = 0.5 } stopUnity() DispatchQueue.main.async {        //SceneKitを非表示にする //self.faceView.isHidden = true } DispatchQueue.main.asyncAfter(deadline: .now() + waitTimeAfterStopUnity) { if let appDelegate = UIApplication.shared.delegate as? AppDelegate { appDelegate.firstStartUnity() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { DispatchQueue.main.async { let unityView:UIView = Unity.shared.view unityView.isHidden = false self.view?.insertSubview(unityView, at: 0)              //ロード時のくるくる停止 //self.stop_activityIndicator() } /*        //SceneKitのタッチが反応しないようにする(画面回転用) self.faceView.isUserInteractionEnabled = false        //self.viewのタッチが反応するようにする(画面回転用) self.view.isUserInteractionEnabled = true self.view.isMultipleTouchEnabled = true self.unityView?.isUserInteractionEnabled = true self.unityView?.isMultipleTouchEnabled = true */ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { if self.myVar.unityFirstLoadFlag == true { self.myVar.unityFirstLoadFlag = false } } } } } } //Unityをアンロードする関数,適当なところで呼び出す func stopUnity() { if self.myVar.UnityLoadFlag == true { DispatchQueue.main.async { //Bool型をStringで記述するというあほな記述、無視してください;; self.myVar.UnityLoadFlag = false let unityView = Unity.shared.view unityView.removeFromSuperview() Unity.shared.unloadUnity() unityView.isHidden = true      //SceneKitを非表示にする //self.faceView.isHidden = true } } } 6. UaaL実装においてうまく行かなかった点 1.AVAudioSessionやAVAudioRecorderなどを使った録音、再生周りがうまく動作しなくなった SceneKitのままだとAVAudioSessionなどがうまく機能しているのですが、UaaLを起動した途端に録音ができなくなりました。理由は不明です。 Unityの使用中だけでなく、Unityをアンロードした後でも同様に録音できないようです。 2.2本指ドラッグを反応させる方法が分からなかった Unityの画面のズームをさせるために、 self.viewに対してPinchGestureRecognizerを使用しています。 そして、Unity画面の回転のために、以下の記事を参考に1本指の画面ドラッグをself.viewに実装しています。 https://i-app-tec.com/ios/image-drag.html 上記の記事には、画面の2本指ドラッグについても書かれており、SceneKitでは2本指ドラッグが正常に反映されたのですが、UaaLになると2本指ドラッグによるカメラ移動が反応しないという問題が回避できませんでした。原因は不明です。 7. Facemotion3dでUaaLを実装した理由 単に、これは知っておかないとまずいやつだ、という感覚がありました。 また、既にVRMをiOS上に読み込む機能を持つフェイストラッキングアプリがいくつかあるのに、何でUaaLを使ってVRMを読み込む機能をFacemotion3dに実装しようと思ったかの理由を書いておこうと思います。 私がFacemotion3dの前身であるiFacialMocapというアプリを作った時に、1番最初に感じたことは、「周りがVRMを動かすものを作っている中で、自分のアプリだけVRMを動かすためのアプリではない」という点でした。 そんな中でVRMを動かす機能を自分のアプリにも実装してしまうと、アプリ間の差別化が出来ず、色んな方面と衝突してしまうのではないかという懸念が常にありました。しかし、VRMを動かすアプリを作れる人はいつでも非VRMを動かすアプリだって作れるので危機感とかもあったわけです。そんな中で、アプリの差別化をできないにしても、どうすれば「世の中にとって新規性のあるもの」を作れるかということは考えていました。 新規性の案の1つとして、自分のアプリは非VRMを動かせるわけなので、非VRMの動的ロードなども出来るのではないか...と考えたりしていました。実際に、TriLibというUnityの有料アセットを使用すればFBXを動的ロードの実装ができます。 私は、TriLibを動作テストをして、実際に使ってみました。FBXの動的ロードに成功し、ある程度動かせるレベルまで行けました。基本的に、軸情報とボーン等のオブジェクト名の情報さえあればFBXは動かせるわけなので、各ソフトウェアの軸情報をコピーしてUnity用に変換し、読み込むということを試してみたりしていました。FBXデータだと必ずしも原点付近にデータがあるとも限らないため、メッシュのバウンディングボックスを調べて、オブジェクトがカメラ内に収まるように移動させるようなことも試してみたりしました。 しかし、実装したものの、TriLibの完成度の問題なのか、自分の知識レベルの低さの問題なのか、いくつかの点でつまづいたわけです。 まず、TriLibは現時点でFBXを読み込むとメモリが変な食い方するのか何なのか、動きがのっそりしたおかしな挙動になります。また、テクスチャのロードなどがFBXのバージョンによってはうまく行かないなどの問題がありました。TriLibeによるFBXのロード時間は、VRMのロード時間より遥かに長く、10~20秒くらいかかった気がします。(2020年時点) そんなこんなで、FBXの動的ロードは置き去りになり、VRMの読み込み機能だけが残ったという背景があります。 FBXの動的ロードを実装したところで、将来的にはVRMやFBXではなく、Apple標準のUSDZがシェアを伸ばしてそちらが覇権を握ったりするかもしれません。その辺の考えについては、以下の記事に書いてみました。 ドワンゴが作るプラットフォームや規格について思うこと https://note.com/pekochun/n/n8c5a8f115645 こういうのも書きました。 モーションキャプチャアプリを作っていて色々思うこと https://note.com/pekochun/n/nf16643bd9f68
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プライバシーポリシー

プライバシーポリシー 池口大雅.(以下,「私」といいます。)は,本アプリ、ウェブサイト上で提供するサービス(以下,「本サービス」といいます。)におけるプライバシー情報の取扱いについて,以下のとおりプライバシーポリシー(以下,「本ポリシー」といいます。)を定めます。 第1条(プライバシー情報) プライバシー情報のうち「個人情報」とは,個人情報保護法にいう「個人情報」を指すものとし,生存する個人に関する情報であって,当該情報に含まれる氏名,生年月日,住所,電話番号,連絡先その他の記述等により特定の個人を識別できる情報を指します。 プライバシー情報のうち「履歴情報および特性情報」とは,上記に定める「個人情報」以外のものをいい,ご利用いただいたサービスやご購入いただいた商品,ご覧になったページや広告の履歴,ユーザーが検索された検索キーワード,ご利用日時,ご利用の方法,ご利用環境,郵便番号や性別,職業,年齢,ユーザーのIPアドレス,クッキー情報,位置情報,端末の個体識別情報などを指します。 第2条(プライバシー情報の収集方法) 私は,ユーザーが利用登録をする際に氏名,生年月日,住所,電話番号,メールアドレス,銀行口座番号,クレジットカード番号,運転免許証番号などの個人情報をお尋ねすることがあります。また,ユーザーと提携先などとの間でなされたユーザーの個人情報を含む取引記録や,決済に関する情報をプライバシーポリシー 池口大雅.(以下,「私」といいます。)は,本アプリ、ウェブサイト上で提供するサービス(以下,「本サービス」といいます。)におけるプライバシー情報の取扱いについて,以下のとおりプライバシーポリシー(以下,「本ポリシー」といいます。)を定めます。 第1条(プライバシー情報) プライバシー情報のうち「個人情報」とは,個人情報保護法にいう「個人情報」を指すものとし,生存する個人に関する情報であって,当該情報に含まれる氏名,生年月日,住所,電話番号,連絡先その他の記述等により特定の個人を識別できる情報を指します。 プライバシー情報のうち「履歴情報および特性情報」とは,上記に定める「個人情報」以外のものをいい,ご利用いただいたサービスやご購入いただいた商品,ご覧になったページや広告の履歴,ユーザーが検索された検索キーワード,ご利用日時,ご利用の方法,ご利用環境,郵便番号や性別,職業,年齢,ユーザーのIPアドレス,クッキー情報,位置情報,端末の個体識別情報などを指します。 第2条(プライバシー情報の収集方法) 私は,ユーザーが利用登録をする際に氏名,生年月日,住所,電話番号,メールアドレス,銀行口座番号,クレジットカード番号,運転免許証番号などの個人情報をお尋ねすることがあります。また,ユーザーと提携先などとの間でなされたユーザーの個人情報を含む取引記録や,決済に関する情報を私の提携先(情報提供元,広告主,広告配信先などを含みます。以下,「提携先」といいます。)などから収集することがあります。 私は,ユーザーについて,利用したサービスやソフトウエア,購入した商品,閲覧したページや広告の履歴,検索した検索キーワード,利用日時,利用方法,利用環境(携帯端末を通じてご利用の場合の当該端末の通信状態,利用に際しての各種設定情報なども含みます),IPアドレス,クッキー情報,位置情報,端末の個体識別情報などの履歴情報および特性情報を,ユーザーが私や提携先のサービスを利用しまたはページを閲覧する際に収集します。 第3条(個人情報を収集・利用する目的) 私が個人情報を収集・利用する目的は,以下のとおりです。 (1)ユーザーに自分の登録情報の閲覧や修正,利用状況の閲覧を行っていただくために,氏名,住所,連絡先,支払方法などの登録情報,利用されたサービスや購入された商品,およびそれらの代金などに関する情報を表示する目的 (2)ユーザーにお知らせや連絡をするためにメールアドレスを利用する場合やユーザーに商品を送付したり必要に応じて連絡したりするため,氏名や住所などの連絡先情報を利用する目的 (3)ユーザーの本人確認を行うために,氏名,生年月日,住所,電話番号,銀行口座番号,クレジットカード番号,運転免許証番号,配達証明付き郵便の到達結果などの情報を利用する目的 (4)ユーザーに代金を請求するために,購入された商品名や数量,利用されたサービスの種類や期間,回数,請求金額,氏名,住所,銀行口座番号やクレジットカード番号などの支払に関する情報などを利用する目的 (5)ユーザーが簡便にデータを入力できるようにするために,私に登録されている情報を入力画面に表示させたり,ユーザーのご指示に基づいて他のサービスなど(提携先が提供するものも含みます)に転送したりする目的 (6)代金の支払を遅滞したり第三者に損害を発生させたりするなど,本サービスの利用規約に違反したユーザーや,不正・不当な目的でサービスを利用しようとするユーザーの利用をお断りするために,利用態様,氏名や住所など個人を特定するための情報を利用する目的 (7)ユーザーからのお問い合わせに対応するために,お問い合わせ内容や代金の請求に関する情報など私がユーザーに対してサービスを提供するにあたって必要となる情報や,ユーザーのサービス利用状況,連絡先情報などを利用する目的 (8)上記の利用目的に付随する目的 第4条(個人情報の第三者提供) 私は,次に掲げる場合を除いて,あらかじめユーザーの同意を得ることなく,第三者に個人情報を提供することはありません。ただし,個人情報保護法その他の法令で認められる場合を除きます。 (1)法令に基づく場合 (2)人の生命,身体または財産の保護のために必要がある場合であって,本人の同意を得ることが困難であるとき (3)公衆衛生の向上または児童の健全な育成の推進のために特に必要がある場合であって,本人の同意を得ることが困難であるとき (4)国の機関もしくは地方公共団体またはその委託を受けた者が法令の定める事務を遂行することに対して協力する必要がある場合であって,本人の同意を得ることにより当該事務の遂行に支障を及ぼすおそれがあるとき (5)予め次の事項を告知あるいは公表をしている場合 利用目的に第三者への提供を含むこと 第三者に提供されるデータの項目 第三者への提供の手段または方法 本人の求めに応じて個人情報の第三者への提供を停止すること 前項の定めにかかわらず,次に掲げる場合は第三者には該当しないものとします。 (1)私が利用目的の達成に必要な範囲内において個人情報の取扱いの全部または一部を委託する場合 (2)合併その他の事由による事業の承継に伴って個人情報が提供される場合 (3)個人情報を特定の者との間で共同して利用する場合であって,その旨並びに共同して利用される個人情報の項目,共同して利用する者の範囲,利用する者の利用目的および当該個人情報の管理について責任を有する者の氏名または名称について,あらかじめ本人に通知し,または本人が容易に知り得る状態に置いているとき 第5条(個人情報の開示) 私は,本人から個人情報の開示を求められたときは,本人に対し,遅滞なくこれを開示します。ただし,開示することにより次のいずれかに該当する場合は,その全部または一部を開示しないこともあり,開示しない決定をした場合には,その旨を遅滞なく通知します。なお,個人情報の開示に際しては,1件あたり1,000円の手数料を申し受けます。 (1)本人または第三者の生命,身体,財産その他の権利利益を害するおそれがある場合 (2)私の業務の適正な実施に著しい支障を及ぼすおそれがある場合 (3)その他法令に違反することとなる場合 前項の定めにかかわらず,履歴情報および特性情報などの個人情報以外の情報については,原則として開示いたしません。 第6条(個人情報の訂正および削除) ユーザーは,私が保有する自己の個人情報が誤った情報である場合には,私が定める手続きにより,私に対して個人情報の訂正または削除を請求することができます。 私は,ユーザーから前項の請求を受けてその請求に応じる必要があると判断した場合には,遅滞なく,当該個人情報の訂正または削除を行い,これをユーザーに通知します。 第7条(個人情報の利用停止等) 私は,本人から,個人情報が,利用目的の範囲を超えて取り扱われているという理由,または不正の手段により取得されたものであるという理由により,その利用の停止または消去(以下,「利用停止等」といいます。)を求められた場合には,遅滞なく必要な調査を行い,その結果に基づき,個人情報の利用停止等を行い,その旨本人に通知します。ただし,個人情報の利用停止等に多額の費用を有する場合その他利用停止等を行うことが困難な場合であって,本人の権利利益を保護するために必要なこれに代わるべき措置をとれる場合は,この代替策を講じます。 第8条(プライバシーポリシーの変更) 本ポリシーの内容は,ユーザーに通知することなく,変更することができるものとします。 私が別途定める場合を除いて,変更後のプライバシーポリシーは,本ウェブサイトに掲載したときから効力を生じるものとします。 以上の提携先(情報提供元,広告主,広告配信先などを含みます。以下,「提携先」といいます。)などから収集することがあります。 私は,ユーザーについて,利用したサービスやソフトウエア,購入した商品,閲覧したページや広告の履歴,検索した検索キーワード,利用日時,利用方法,利用環境(携帯端末を通じてご利用の場合の当該端末の通信状態,利用に際しての各種設定情報なども含みます),IPアドレス,クッキー情報,位置情報,端末の個体識別情報などの履歴情報および特性情報を,ユーザーが私や提携先のサービスを利用しまたはページを閲覧する際に収集します。 第3条(個人情報を収集・利用する目的) 私が個人情報を収集・利用する目的は,以下のとおりです。 (1)ユーザーに自分の登録情報の閲覧や修正,利用状況の閲覧を行っていただくために,氏名,住所,連絡先,支払方法などの登録情報,利用されたサービスや購入された商品,およびそれらの代金などに関する情報を表示する目的 (2)ユーザーにお知らせや連絡をするためにメールアドレスを利用する場合やユーザーに商品を送付したり必要に応じて連絡したりするため,氏名や住所などの連絡先情報を利用する目的 (3)ユーザーの本人確認を行うために,氏名,生年月日,住所,電話番号,銀行口座番号,クレジットカード番号,運転免許証番号,配達証明付き郵便の到達結果などの情報を利用する目的 (4)ユーザーに代金を請求するために,購入された商品名や数量,利用されたサービスの種類や期間,回数,請求金額,氏名,住所,銀行口座番号やクレジットカード番号などの支払に関する情報などを利用する目的 (5)ユーザーが簡便にデータを入力できるようにするために,私に登録されている情報を入力画面に表示させたり,ユーザーのご指示に基づいて他のサービスなど(提携先が提供するものも含みます)に転送したりする目的 (6)代金の支払を遅滞したり第三者に損害を発生させたりするなど,本サービスの利用規約に違反したユーザーや,不正・不当な目的でサービスを利用しようとするユーザーの利用をお断りするために,利用態様,氏名や住所など個人を特定するための情報を利用する目的 (7)ユーザーからのお問い合わせに対応するために,お問い合わせ内容や代金の請求に関する情報など私がユーザーに対してサービスを提供するにあたって必要となる情報や,ユーザーのサービス利用状況,連絡先情報などを利用する目的 (8)上記の利用目的に付随する目的 第4条(個人情報の第三者提供) 私は,次に掲げる場合を除いて,あらかじめユーザーの同意を得ることなく,第三者に個人情報を提供することはありません。ただし,個人情報保護法その他の法令で認められる場合を除きます。 (1)法令に基づく場合 (2)人の生命,身体または財産の保護のために必要がある場合であって,本人の同意を得ることが困難であるとき (3)公衆衛生の向上または児童の健全な育成の推進のために特に必要がある場合であって,本人の同意を得ることが困難であるとき (4)国の機関もしくは地方公共団体またはその委託を受けた者が法令の定める事務を遂行することに対して協力する必要がある場合であって,本人の同意を得ることにより当該事務の遂行に支障を及ぼすおそれがあるとき (5)予め次の事項を告知あるいは公表をしている場合 利用目的に第三者への提供を含むこと 第三者に提供されるデータの項目 第三者への提供の手段または方法 本人の求めに応じて個人情報の第三者への提供を停止すること 前項の定めにかかわらず,次に掲げる場合は第三者には該当しないものとします。 (1)私が利用目的の達成に必要な範囲内において個人情報の取扱いの全部または一部を委託する場合 (2)合併その他の事由による事業の承継に伴って個人情報が提供される場合 (3)個人情報を特定の者との間で共同して利用する場合であって,その旨並びに共同して利用される個人情報の項目,共同して利用する者の範囲,利用する者の利用目的および当該個人情報の管理について責任を有する者の氏名または名称について,あらかじめ本人に通知し,または本人が容易に知り得る状態に置いているとき 第5条(個人情報の開示) 私は,本人から個人情報の開示を求められたときは,本人に対し,遅滞なくこれを開示します。ただし,開示することにより次のいずれかに該当する場合は,その全部または一部を開示しないこともあり,開示しない決定をした場合には,その旨を遅滞なく通知します。なお,個人情報の開示に際しては,1件あたり1,000円の手数料を申し受けます。 (1)本人または第三者の生命,身体,財産その他の権利利益を害するおそれがある場合 (2)私の業務の適正な実施に著しい支障を及ぼすおそれがある場合 (3)その他法令に違反することとなる場合 前項の定めにかかわらず,履歴情報および特性情報などの個人情報以外の情報については,原則として開示いたしません。 第6条(個人情報の訂正および削除) ユーザーは,私が保有する自己の個人情報が誤った情報である場合には,私が定める手続きにより,私に対して個人情報の訂正または削除を請求することができます。 私は,ユーザーから前項の請求を受けてその請求に応じる必要があると判断した場合には,遅滞なく,当該個人情報の訂正または削除を行い,これをユーザーに通知します。 第7条(個人情報の利用停止等) 私は,本人から,個人情報が,利用目的の範囲を超えて取り扱われているという理由,または不正の手段により取得されたものであるという理由により,その利用の停止または消去(以下,「利用停止等」といいます。)を求められた場合には,遅滞なく必要な調査を行い,その結果に基づき,個人情報の利用停止等を行い,その旨本人に通知します。ただし,個人情報の利用停止等に多額の費用を有する場合その他利用停止等を行うことが困難な場合であって,本人の権利利益を保護するために必要なこれに代わるべき措置をとれる場合は,この代替策を講じます。 第8条(プライバシーポリシーの変更) 本ポリシーの内容は,ユーザーに通知することなく,変更することができるものとします。 私が別途定める場合を除いて,変更後のプライバシーポリシーは,本ウェブサイトに掲載したときから効力を生じるものとします。 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む