20210506のSwiftに関する記事は14件です。

MVPアーキテクチャについて

はじめに 今までは、ずっとMVCを採用してアプリを開発してきましたが MVC脱却を図るため、今回はMVPに関することを備忘録として残しておきます。 まず、MVPを知る前にMVCの流れを復習していきましょう。 MVC API通信の例で簡単に説明すると、 まず、Viewからユーザーのアクションを検知し、そのアクションをControllerに伝えます。 そして、Controllerは必要なModelを取得するためにAPIを叩きます。 その後、APIから必要なModelが返され、Controllerで保持されます。 ControllerからViewにModelを渡して そのModelからViewを更新することによって画面が更新されます。 なぜMVCから脱却したいのか? MVCだとFatViewControllerになりやすいからです。 iOSDC 2017 前夜祭で「節子、それViewControllerやない…、FatViewControllerや…。」というタイトルで登壇しました! 余談ですが、上記の記事もMVPについて書かれてました! MVP 先ほどと同じようにAPI通信の例で説明すると、 まず、Viewからユーザーのアクションを検知します。 そして、Controllerにアクションを伝えてPresenterに知らせます。 Presenterは必要なModelを取得するためにAPIを叩きます。 その後、APIから必要なModelが返され、Presenterで保持されます。 PresenterからControllerにModelを渡して そのModelをViewに渡します。 最終的に、ModelからViewを更新することによって画面が更新されます。 Presenterについて Presenterは、今までMVCでViewControllerが行なってきたことのほとんどを担っています。 APIを叩いたり、Modelを保持したり等ですね。 そのような責務を引き剥がすことによってFatViewControllerを回避しています。 では実際に、コードで確認していきましょう。 実装コード 今回は、Github APIを使用したアプリを例に説明していきます。 Presenter まずは、Presenterのコードです。 Presenterの特徴として、 ・UIがどうなっているかは考慮しない ・UIKitをインポートしない ・Xcodeじゃなくてもコードが書ける などが挙げられます。 iOS特有のUIKit、SwiftUIなどに依存しないので極端にいうとメモアプリでも書けます。 MVPSearchPresenter.swift import Foundation protocol MVPSearchPresenterInput { var numberOfItems: Int { get } func item(index: Int) -> GithubModel func search(param: String?) func didSelect(index: Int) } protocol MVPSearchPresenterOutput: AnyObject { func update(loading: Bool) func update(githubModels: [GithubModel]) func validation(error: ParameterValidationError) func get(error: Error) func showWeb(url: URL) } final class MVPSearchPresenter { private weak var output: MVPSearchPresenterOutput! private var api: GithubAPIProtocol! private var githubModels: [GithubModel] init(output: MVPSearchPresenterOutput, api: GithubAPIProtocol = GithubAPI.shared) { self.output = output self.api = api self.githubModels = [] } } extension MVPSearchPresenter: MVPSearchPresenterInput { var numberOfItems: Int { githubModels.count } func item(index: Int) -> GithubModel { githubModels[index] } func search(param: String?) { if let validationError = ParameterValidationError(param: param) { output.validation(error: validationError) return } guard let searchText = param else { return } output.update(loading: true) self.api.get(searchText: searchText) {[weak self] (result) in guard let self = self else{ return } switch result { case .success(let githubModels): self.output.update(loading: false) if githubModels.isEmpty { self.output.get(error: AppError.emptyApiResponce.error) return } self.githubModels = githubModels self.output.update(githubModels: githubModels) case .failure(let error): self.output.update(loading: false) self.output.get(error: error) } } } func didSelect(index: Int) { guard let githubUrl = URL(string: githubModels[index].urlStr) else { output.get(error: AppError.getApiData.error) return } output.showWeb(url: githubUrl) } } ではコードを細かく説明していきます。 ・コード説明① まず、ViewControllerとPresenterを繋げないといけません。 PresenterがViewControllerからの入力を受け取り その結果をViewControllerに出力しないといけないからです。 MVPでは依存性を低くするためにprotocolによって繋げます。 以下が実際にprotocolを定義している部分です↓ // 入力に関するプロトコル // ViewControllerから送られてくる protocol MVPSearchPresenterInput { var numberOfItems: Int { get } func item(index: Int) -> GithubModel func search(param: String?) func didSelect(index: Int) } // 出力に関するプロトコル // ViewControllerに結果を渡す protocol MVPSearchPresenterOutput: AnyObject { func update(loading: Bool) func update(githubModels: [GithubModel]) func validation(error: ParameterValidationError) func get(error: Error) func showWeb(url: URL) } ・コード説明② Presenterは、Modelを内部で保持します。 更にoutputプロパティを定義し、init時にViewControllerと繋げれるようにしています。 ここで注意すべき点は、ViewControllerとPresenterがお互いに参照し合うので weakキーワードを付けて循環参照にならないようにしないといけません。 クロージャの中に書く[weak self]についてまとめてみた final class MVPSearchPresenter { // ViewControllerとPresenterが参照し合い循環参照が起きるためweakキーワードを付ける // このoutputがViewControllerのこと private weak var output: MVPSearchPresenterOutput! private var api: GithubAPIProtocol! // Modelを保持する private var githubModels: [GithubModel] init(output: MVPSearchPresenterOutput, api: GithubAPIProtocol = GithubAPI.shared) { self.output = output self.api = api self.githubModels = [] } } ・コード説明③ MVPSearchPresenterInputプロトコルを準拠してViewControllerからの入力を処理します。 Presenterとしては、何でか分からないけど とりあえず入力が来たから処理して出力したって感じです。 Presenterにとっては、検索バーがタップされたとか画面が遷移したとかは関係なく知らなくても良いのです。 更に、入力・出力する相手も関係ないのでテストがしやすくデザインを決める前にも開発が可能です。 extension MVPSearchPresenter: MVPSearchPresenterInput { var numberOfItems: Int { githubModels.count } func item(index: Int) -> GithubModel { githubModels[index] } func search(param: String?) { if let validationError = ParameterValidationError(param: param) { // ViewControllerに任せる output.validation(error: validationError) return } guard let searchText = param else { return } // ViewControllerに任せる output.update(loading: true) // API通信 self.api.get(searchText: searchText) {[weak self] (result) in guard let self = self else{ return } switch result { case .success(let githubModels): // ViewControllerに任せる self.output.update(loading: false) if githubModels.isEmpty { // ViewControllerに任せる self.output.get(error: AppError.emptyApiResponce.error) return } self.githubModels = githubModels // ViewControllerに任せる self.output.update(githubModels: githubModels) case .failure(let error): // ViewControllerに任せる self.output.update(loading: false) self.output.get(error: error) } } } func didSelect(index: Int) { guard let githubUrl = URL(string: githubModels[index].urlStr) else { // ViewControllerに任せる output.get(error: AppError.getApiData.error) return } // ViewControllerに任せる output.showWeb(url: githubUrl) } } ViewController では、ViewControllerのコードです。 ViewControllerの特徴として、 ・Viewに関すること以外は書かない ・ifやfor等といった制御構文が入らない などが挙げられます。 MVPSearchViewController.swift import UIKit final class MVPSearchViewController: UIViewController { @IBOutlet weak private var tableView: UITableView! { didSet { tableView.register(UINib(nibName: TableViewCell.className, bundle: nil), forCellReuseIdentifier: TableViewCell.className) } } @IBOutlet weak private var indicator: UIActivityIndicatorView! private var searchBar = UISearchBar() private var input: MVPSearchPresenterInput! func inject(input: MVPSearchPresenterInput) { self.input = input } override func viewDidLoad() { super.viewDidLoad() self.navigationItem.titleView = searchBar searchBar.delegate = self } } extension MVPSearchViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { input.search(param: searchBar.text) searchBar.resignFirstResponder() } } extension MVPSearchViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { input.numberOfItems } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.className, for: indexPath) as! TableViewCell let githubModel = input.item(index: indexPath.item) cell.configure(githubModel: githubModel) return cell } } extension MVPSearchViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) input.didSelect(index: indexPath.row) } } extension MVPSearchViewController: MVPSearchPresenterOutput { func update(loading: Bool) { indicator.animation(isStart: loading) } func update(githubModels: [GithubModel]) { DispatchQueue.main.async { self.searchBar.text = "" self.searchBar.resignFirstResponder() self.tableView.reloadData() } } func validation(error: ParameterValidationError) { Alert.okAlert(vc: self, title: error.message, message: "") } func get(error: Error) { Alert.okAlert(vc: self, title: error.localizedDescription, message: "") } func showWeb(url: URL) { Router.showWeb(url: url, from: self) } } では細かく説明していきます。 ・コード説明① まず、Presenterと繋げるためにinputプロパティとinjectメソッドを用意します。 この部分で外部からPresenterを繋げます。 // このinputがpresenterのこと private var input: MVPSearchPresenterInput! // ここで外部からPresenterを繋げる func inject(input: MVPSearchPresenterInput) { self.input = input } ・コード説明② 今回は、キーボードの検索ボタンを押した時にAPI通信を行い その結果をTableViewに表示するといったアプリとなっています。 なので検索ボタンを押した時にPresenterに知らせないといけません。 extension MVPSearchViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { // Presenterに知らせる input.search(param: searchBar.text) searchBar.resignFirstResponder() } } ・コード説明③ Presenterからの結果をViewControllerが受け取らないといけません。 なので、MVPSearchPresenterOutputプロトコルを準拠して 以下のように結果を受け取れるようにします。 extension MVPSearchViewController: MVPSearchPresenterOutput { func update(loading: Bool) { // インディケータを回すかどうかを決めている indicator.animation(isStart: loading) } func update(githubModels: [GithubModel]) { // TableViewの更新など DispatchQueue.main.async { self.searchBar.text = "" self.searchBar.resignFirstResponder() self.tableView.reloadData() } } func validation(error: ParameterValidationError) { // アラート表示 Alert.okAlert(vc: self, title: error.message, message: "") } func get(error: Error) { Alert.okAlert(vc: self, title: error.localizedDescription, message: "") } func showWeb(url: URL) { // 画面遷移する Router.showWeb(url: url, from: self) } } PresenterとViewControllerを繋げる PresenterとViewControllerを繋げる準備はしましたが、このままだとまだ繋がっていません。 ではどこで繋げていくかというと、画面遷移に関するクラスにて繋げています。 画面遷移に関係あるコードの記述を別ファイルに分けて実装する Router.swift final class Router { static func showMVPSearch(from: UIViewController) { let mvpSearchVC = UIStoryboard.mvpSearchViewController // ここでPresenterとViewControllerを繋げている let presenter = MVPSearchPresenter(output: mvpSearchVC) mvpSearchVC.inject(input: presenter) from.show(next: mvpSearchVC) } } MVPを採用してみて 単純にViewControllerの記述量が減ったのが良きですね。 今回のサンプルアプリでも、その恩恵を得られたので 大規模なアプリだと、もっと効果を感じれそうです。 PresenterはViewがどうなろうと関係ないのでアプリの仕様が分かっていれば すぐに開発できるメリットもありますし、何ならXcodeにも依存していません。 後は、入力・出力先に依存していないのでテストしやすいです。 何か不明な不具合があった時にでも、Presenterは入力・出力先を変えてテスト可能です。 おわりに ソースコードはこちらにあげてます。 何かあれば、コメントして下さると有り難いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メモ : pod installがうまくいかない

2020年モデルのMacBookAirにCocoaPodsをインストールして、Firebase連携しようと思ったものの、pod installが失敗する。 iOSのバージョン? 手順にミスが有った? わからなくて散々ネットサーフィンしたが、こちらの記事で速攻解決した。 ので、メモとして残しておく。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】ボタンをラベルのように見せたい(storyboardから、コードから)

2021/05/06投稿 はじめに Swiftで遊んでるだけの者なので細かいところはわかりません。初心者向けです。 動機 ナビゲーションバーの右にラベルを置きたかったのですが、調べた感じだと左右にはボタンしか置けなさそうだったので(違ったら教えてください)、ボタンをラベルのようにしようと思いました。 しかしただボタンを選択不可にするだけだとデフォルトで色が薄くなってしまうのでこれを直します。 ※ボタンの宣言やコードとの紐付け(storyboardの場合)は省略します。 環境 macOS Big Sur 11.3 Xcode 12.5 Swift 5.4 ボタンを選択不可に storyboardの場合 storyboardの場合は、ボタンなら Attributes inspector の State から、ナビゲーションバーのアイテムなら Attributes inspector の Bar Item から Enabled のチェックを外すだけで大丈夫です。 ------折りたたみ文ここまで------ コードで書く場合 通常の UIButton クラスや UIBarButtonItem クラスには、どちらもisEnabledという選択可能かどうかのBool型の変数が用意されているのでこれをfalse(選択不可)にします。 viewDidLoad()内などで、 ViewController.swift ボタン名.isEnabled = false と書くだけです。 ------折りたたみ文ここまで------ ボタンの色を直す これで選択不可になったのでラベルのようなものにはなりましたが、テキスト色が薄くなっていると思います。次はこれを改善して完全にラベルにしか見えないようにしていきます。 storybardの場合(ボタンのみ) ボタンだけはstoryboardで色を変えられたので載せますがあまりお勧めはしません(理由は後述)。 ③での State config を変えると、それぞれの状態の時のボタンの設定を出来ます。 disabled にすればボタンが押せな時の装飾を出来ますが、すでに Enable のチェックを外していれば default で disabled 状態なので default でも disabled でも構いません。 ④での text color が default だと、文字が薄くなってしまうので、それ以外に設定します。 ※この時 black color を選ぶと、表示が自動的に default に戻されますが、実際にシミュレータを起動してみるとちゃんと不透明の黒になります。 storyboardってこういうとこありますよね。。なので非推奨です。。 ------折りたたみ文ここまで------ ※UIBarButtonItem のstoryboardの場合の直し方は見つからなかった(用意されてない?)のでコードで書いていきます コードで書く場合 UIButton、UIBarButtonItem の細かい装飾にはそれぞれsetAttributedTitle、setTitleTextAttributesというメソッドを用います。 UIButton の場合 試しにボタンのsetAttributedTitleを記述してみます。 そうすると引数にNSAttributedString?とUIControl.Stateを入れろと言われます。 NSAttributedString テキストのフォントのサイズ、種類、色、行間などを指定できるデータ型で、表示するテキスト(String)と装飾内容([NSAttributedString.Key:Any])を指定します。 [NSAttributedString.Key:Any]はKeyと値を入れるDictionary型(連想配列)で、文字色や文字サイズを格納するキーを持っています。ここで文字色を指定します。 title: NSAttributedString?の欄に、NSAttributedString(などと打てば選択肢に(string: ,attributes: )が出てくると思うのでそれを選んでください。 stringは表示するテキストなのでそのまま記入します。 attributesは直接書くと非常に見辛いのであらかじめ定数で用意しておくのが一般的だと思います。 文字色を指定する場合は.foregroundColorを使います。他に使えるキーは適宜調べてみてください。 参考: ・ NSAttributedString.Key(apple公式) ・ 【初心者向け】NSAttributedStringをcustomKeyで自在に操る話【Swift 4】 UIControl.State ボタンがどの状態(通常時なのか選択不可時なのか、など)の時にこのメソッドを実行するかを決められます。ここで選択不可時にメソッドが実行されるようにします。 for: UIControl.Stateの欄に.を入れると選択肢が出ると思うので適宜必要なものを入れてください。 通常時の値は.normal、ボタンが使えない時の値は.disabledです。 以上でボタンがラベルのように選択不可かつ不透明になるのが確認出来ると思います。 ------折りたたみ文ここまで------ UIBarButtonItem の場合 UIButton の方を見ていただければほぼ同じなのでわかると思います。 まずは試しにバーボタンアイテムのsetTitleTextAttributesを記述してみます。 すると[NSAttributedString.Key:Any]?とUIControl.Stateを打てと言われます。 それぞれの簡単な説明は UIButton の場合 の方で書いたのでそちらを参照してください(NSAttributedString.Key、UIControl.State)。 UIButtonではボタンのタイトル(string)も必要でしたが、UIBarButtonItemでは装飾内容([NSAttributedString.Key:Any])だけで良いのでテキストはstoryboard上で記入するかコードでアイテム生成時に記入してください。 UIButton同様、attributesを指定し、UIControl.State を.disabledにして終わりです。 以上でバーボタンアイテムがラベルのように選択不可かつ不透明になるのが確認出来ると思います。 ------折りたたみ文ここまで------ ------折りたたみ文ここまで------ 参考文献 NSAttributedString.Key(apple公式) 【初心者向け】NSAttributedStringをcustomKeyで自在に操る話【Swift 4】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIStoryboard

■UIStoryboard プロパティメソッド //どのUIstoryboardなのか? init(name: String, bundle storyboardBundleOrNil: Bundle?) //例 Mainって言うUIStoryboardやで let storyboard = UIStoryboard(name: "Main", bundle: nil) //どのUIviewContorollerなのか? open func instantiateViewController(withIdentifier identifier: String) //例: Main.storyboardのStoryboardID:CodeってつけたUIViewControllerやで let CodeView = storyboard.instantiateViewController(withIdentifier: "Code") //どのUIviewContorollerなのか? open func instantiateInitialViewController() -> UIViewController? //例: MainStoryboardのFirstViewControllerやで let CodeView = storyboard.instantiateViewController() as! FirstViewController ■縮めてまとめて書く UIStoryboard.init(name: "Main", bundle: nil).instantiateViewController() as! FirstViewController
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Staticプロパティ swift

■staticプロパティ(タイププロパティ)とは 同じクラスから生成された複数のインスタンスで同じ定数を使えるようにするもの。 ■インスタンスとは class Abc { var name = "" static sInt = 0 } //下記のabcにあたる部分 クラスAbcをインスタンス化してインスタンスabcが誕生してる。 var abc = Abc() var abc2 = Abc() //これで同じクラスから生成された複数のインスタンスが生まれた //abcとabc2の事ね これを踏まえて下記を見ていきましょう! class Abc { var name = "名前なし" static sInt = 0 func aaa() { print("\(name)は\(Abc.sInt)歳です。") } } var abc = Abc() var abc2 = Abc() abc.name = "佐藤健" Abc.sInt = 100 abc.aaa() //結果: 佐藤健は100歳です。 abc2.aaa() //結果: 名前なしは100歳です。 なぜこうなる? ■インスタンスプロパティはインスタンスを呼んで値を代入する。 abc.name = "佐藤健" その場しのぎ 今回の場合はabcのインスタンス内でのみ変更されている。 ■staticプロパティは直接型(ここではAbc)を呼んで値を代入している。 Abc.sInt = 100 というような書き方になる。 直接型を呼んで代入してるので abcのインスタンスがなんや関係なく とりあえずclassAbcの中のstaticプロパティそのものを変更してる。 これを踏まえると abc2のインスタンスでaaaメソッドを呼んだ時 以前にstaticプロパティのそのものが100に変えられていたので 結果: 名前なしは100歳です。 と言う結果となった。 つまり 同じクラスから生成された複数のインスタンスで同じ定数を使えるようになった。 ■これを応用してちょっとだけ本格的にstaticを使う。 準備 このような画面を作ったとっする。 Buttonを押せばオレンジの画面から青い画面に遷移する。 ■class Akunを準備する。 class Akun { var name = "名前なし" static var sInt = 0 } オレンジ上のラベルをnameLabelとして紐づける。 オレンジ下のラベルをintLabelとして紐づける。 下記のようにコードを書く 青い画面のコード 青い上のラベルをnameLabelとして紐づける。 青い下のラベルをintLabelとして紐づける。 下記のようにコードを書く ■結果 オレンジ画面 nameLabelは佐藤健 intLabelは100 Buttonを押して青い画面に遷移 nameLabelは名前なし intLabelはstaticプロパティ使ってて 以前に100に直接値変えられているので100になった。 同じクラスから生成された複数のインスタンスで同じ定数を使えている。 これがstaticプロパティ!!!!!!!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI で ToolBarItem の色を変更する

実装法 以下のように直接指定してもデフォルト色から変わってくれない ToolbarItem(placement: .navigationBarLeading){ Button(action: {} ) { Image(systemName: "gearshape") }.accentColor(.red) } NavigationView に対して accentColor を指定するといける NavigationView { // 省略 // } .accentColor(.red) 実装例 import SwiftUI struct HomeView: View { var body: some View { NavigationView { Text("Hello") .navigationBarTitleDisplayMode(.inline) .navigationTitle("タイトル") .toolbar { ToolbarItem(placement: .navigationBarTrailing){ Button(action: {} ) { Image(systemName: "gearshape") } } } } .accentColor(.red) } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI で ToolbarItem の色を変更する

実装法 以下のように直接指定してもデフォルト色から変わってくれない ToolbarItem(placement: .navigationBarLeading){ Button(action: {} ) { Image(systemName: "gearshape") }.accentColor(.red) } NavigationView に対して accentColor を指定するといける NavigationView { // 省略 // } .accentColor(.red) 実装例 import SwiftUI struct HomeView: View { var body: some View { NavigationView { Text("Hello") .navigationBarTitleDisplayMode(.inline) .navigationTitle("タイトル") .toolbar { ToolbarItem(placement: .navigationBarTrailing){ Button(action: {} ) { Image(systemName: "gearshape") } } } } .accentColor(.red) } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AutoLayoutの基礎の基礎の基礎

AutoLayoutとは AutoLayouとは、制約を用いたレイアウト方法です。 制約とは 位置やサイズの決める概念です。 こんなの勉強しなくても、 UIViewクラスのインスタンスプロパティのframeで位置やサイズが決めるられるやん!! って思ったあなたはこの記事を最後まで読むことが決まりました。 (私もそう思ってました。) 話を戻します。 制約は、同一の階層にあるビューや親子関係にあるビューの関係を定義できます。 (ちなみに、frameはスーパービューの座標における位置を定義できます。) 同一の階層にあるビューの関係を定義できるということは、少ないコードで画面サイズの影響を押さえながらレイアウトすることが可能になります。 (frameを用いても画面サイズの影響を抑えることができます。デバイスに応じて画面サイズを取得してif文で条件分岐してframeを毎回決めるということをすれば.......気の遠くなるお話ですね) AutoLayoutエンジン 私ちは、viewに対して制約式を与えることになります。 与えた制約式の連立方程式を解くことでレイアウトを決定します。これを行なっているのがAutoLayoutエンジンです。 (名前めっちゃかっこいい) では具体的に、制約式とはなんなのか?見ていきましょう! 制約式 下の図で、青枠で囲まれているのがViewBlueでオレンジ色のviewがViewOrangeだとしましょう。 viewOrangeのスーパービューがViewBlueです。 制約式は、4種類になります。 1.viewOrange.top=viewBlue.top 2.viewOrange.left=viewBlue.left 3.viewOrange.right=viewBlue.right 4.viewOrange.bottom=0.6 × viewBlue.bottom 制約式を一般化するとy=ax+bになります。 (具体的に、連立方程式を効率的に解くアルゴリズムは、難しくて僕には理解できなかった。) (AutoLayouでわからないことがあったらこの基本部分に戻ってくると理解できるかも。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITabBarControllerの子要素ViewController同士を戯れさせてみた。[Swift5]

はじめに UITabBarControllerをコードで書いていて、子要素のViewControllerから別のViewControllerにアクセスしたいと思い、少し詰まりました。 試したこと Chapter1 Chapter2 参考文献 Chapter1 HogeViewController.swift guard let viewControllers = self.tabBarController?.viewControllers, let sample = viewControllers[1] as? SampleViewController else { return } これだと二行目の条件分岐が引っかかりました。 Chapter2 Answer 実はUITabBarControllerのViewControllersそれぞれにさらにchildrenというプロパティがありました。 HogeViewController.swift for vc in viewControllers { print("vc \(vc.children)") } 結果 vc [<プロジェクト名.SampleController: 0x7fad4581d800>] vc [<プロジェクト名.HogeViewController: 0x7fad47865800>] ということで HogeViewController.swift if let child = viewControllers[1].children.first as? SampleViewController { //これでSampleViewControllerの値をいじる。 } 他のViewControllerの値を外からいじるのは良くないのかもしれませんが。 参考文献 なし 終わりに 実務未経験ですが、もがき楽しんでおります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift 配列 転置行列 n×mからm×nに変換

コピペ用 二次元配列の各要素から、同じ位置にあるものを集めて新しい配列を作る [[あ, い, う, え, お], [か, き, く, け, こ], [さ, し, す, せ, そ]] -> [[あ, か, さ], [い, き, し], [う, く, す], [え, け, せ], [お, こ, そ]]] Extensions.swift // 転置行列 extension Array where Element: RandomAccessCollection, Element.Index == Int { func transpose() -> [[Element.Element]] { return self.isEmpty ? [] : (0...(self.first!.endIndex - 1)).map { i -> [Element.Element] in self.map { $0[i] } } } } 使い方 配列に対して .transpose() を使う Sample.swift let a = [ [1, 2, 3], [4, 5, 6] ].transpose() // a: [ // [1, 4], // [2, 5], // [3, 6] // ] let b = [ ["あ", "い"], ["か", "き"], ["さ", "し"] ].transpose() // b: [ // ["あ", "か", "さ"], // ["い", "き", "し"] // ] さいごに 細かくいじらない汎用的なコードは可読性よりもコンパクトさを優先してます Thanks @ensan_hcl さん: 最適化されたコードの提供
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift 配列 転置行列 m×nからn×mに変換

コピペ用 二次元配列の各要素から、同じ位置にあるものを集めて新しい配列を作る [[あ, い, う, え, お], [か, き, く, け, こ], [さ, し, す, せ, そ]] -> [[あ, か, さ], [い, き, し], [う, く, す], [え, け, せ], [お, こ, そ]]] Extensions.swift // 転置行列 extension Array where Element: RandomAccessCollection, Element.Index == Int { func transpose() -> [[Element.Element]] { return self.isEmpty ? [] : (0...(self.first!.endIndex - 1)).map { i -> [Element.Element] in self.map { $0[i] } } } } 使い方 配列に対して .transpose() を使う Sample.swift let a = [ [1, 2, 3], [4, 5, 6] ].transpose() // a: [ // [1, 4], // [2, 5], // [3, 6] // ] let b = [ ["あ", "い"], ["か", "き"], ["さ", "し"] ].transpose() // b: [ // ["あ", "か", "さ"], // ["い", "き", "し"] // ] さいごに 細かくいじらない汎用的なコードは可読性よりもコンパクトさを優先してます Thanks @ensan_hcl : 汎用化されたコードの提供
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

XCode 便利なショートカットキー

⌘(コマンド)+ Shift + Y デバックエリアの表示/非表示 ⌘(コマンド)+ 0 ナビゲーションの表示/非表示 やればわかりますが、かなり使います
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftの文字列や配列の拡張まとめ

できること スペース埋め、ゼロ埋め、左寄せ "ABC" -> "__ABC" "ABC" -> "00ABC" "ABC" -> "ABC__" n文字ごとに区切る "ABCDEFG" -> ["AB", "CD", "EF", "G"] 重複する要素ががあるかないか ["A", "B", "C"] -> false ["A", "A", "B"] -> true 配列を区切って二次元配列にする ["A", "B", "C", "D", "E", "F", "G"] -> [["A", "B"], ["C", "D"], ["E", "F"], ["G"]] 要素数チェックをしながらオプショナル型で取り出す ["A", "B", "C"][at: 2] -> "C" (Optional) ["A", "B", "C"][at: 3] -> nil コピペ用 それぞれ独立しているので、欲しいやつだけコピペしていってね Extension.swift // 1. スペース埋め、ゼロ埋め、左寄せ extension String { func filled(to count: Int, with pad: String = " ", isleftJustified: Bool = false) -> String { let main = String(self.prefix(count)) let pads = String(repeating: pad, count: count - main.count) return isleftJustified ? (main + pads) : (pads + main) } } // 2. n文字ごとに区切る extension String { func split(by count: Int) -> [String] { let arrayValue = Array(self) if arrayValue.isEmpty { return [] } var parent: [[Element]] = [] for i in 1 ... arrayValue.count { if (i % count == 0) { parent.append(arrayValue[(i - count)...(i - 1)].map{$0}) } } if arrayValue.count % count != 0 { parent.append(arrayValue.suffix(arrayValue.count % count)) } return parent.map({String($0)}) } } // 3. 重複する要素ががあるかないか extension Array where Element: Hashable { var hasDuplicated: Bool { return Set(self).count != self.count } } // 4. 配列を区切って二次元配列にする extension Array { func split(by count:Int) -> [[Element]] { if self.isEmpty { return [] } var parent: [[Element]] = [] for i in 1 ... self.count { if (i % count == 0) { parent.append(self[(i - count)...(i - 1)].map{$0}) } } if self.count % count != 0 { parent.append(self.suffix(self.count % count)) } return parent } } // 5. 要素数チェックをしながらオプショナル型で取り出す extension Array { /// Optional<Elemnt> として取り出す subscript (at index: Index) -> Element? { return indices.contains(index) ? self[index] : nil } } 使い方 Sample.swift // 1. スペース埋め、ゼロ埋め、左寄せ let a = "ABC".filled(to: 5) let b = "ABC".filled(to: 5, with: "0") let c = "ABC".filled(to: 5, isleftJustified: true) let d = "ABC".filled(to: 5, with: "0", isleftJustified: true) // a: " ABC" // b: "00ABC" // c: "ABC " // d: "ABC00" // 2. n文字ごとに区切る let e = "ABCDEFG".split(by: 2) // e: ["AB", "CD", "EF", "G"] // 3. 重複する要素ががあるかないか let f = ["A", "B", "C"].hasDuplicated let g = ["A", "A", "C"].hasDuplicated // f: false // g: true // 4. 配列を区切って二次元配列にする let h = ["A", "B", "C", "D", "E", "F", "G"].split(by: 2) // [["A", "B"], ["C", "D"], ["E", "F"], ["G"]] // 5. 要素数チェックをしながらオプショナル型で取り出す let i = ["A", "B", "C"][at: 2] let j = ["A", "B", "C"][at: 3] // i: "C" (Optional) // j: nil さいごに どれも調べたら出てきそうですが、一箇所にまとめておきたかったので。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】初めての非同期処理♡

この記事は? 最近iOSアプリ開発を始めた者です。現在は練習用として電話帳アプリを作成しています。このアプリには以下のような画面で電話帳に友人の情報を登録できます。「郵便番号を入力」と書いているフォームには、郵便番号を入力して検索ボタンを押下すると、postmanAPIが一致する住所を取得し、その結果を「住所を入力」と書かれたTextFileldに表示します。 このpostmanAPIと接続する部分は外部APIと接続するという事になるので、非同期処理が必要です。この記事では、非同期処理の方法について今回私が学んだ事をまとめます。内容としてはかなり基本的な内容になります。 Swiftでの非同期処理における登場人物 DispatchQueue(ディスパッチキュー) 参考:【Swift 3】非同期処理・DispatchQueueについてのメモ DispatchQueueは、一つ以上のタスクを管理するクラスで、独立したスレッドにて、登録されたタスクを実行していくものです。 タスクは、大きく分けて同期処理(.sync())または非同期処理(.async(), .asyncAfter())のどちらかで登録します。 closure(クロージャ) クロージャとはスコープ内の変数や定数を保持したひとまとまりの処理のことである よく分かりません。。。 結局、実際の処理を見ないとこの辺は理解するのが難しい。。。 実際の処理を見てみよう swiftUIで画面のレイアウトを作っています。また、アーキテクチャはMVVMです。今回の場合だと、以下のような構成になっています View...swiftUIでレイアウトを定義したファイル Model...構造体が書かれたファイル ViewModel...非同期処理でAPIと接続を行うファイル 今回はViewとViewModelのコードを見る事で、swiftでの非同期処理の動きを簡単に追っていきます。 こちらがViewのコードです。(説明に関係のある部分のコードだけ表示しています) ProfileRegisterView.swift @ObservedObject var profileRegisterVM: ProfileRegisterViewModel = ProfileRegisterViewModel() @State var address = "" @State var postNum = "" ... // 郵便番号入力フォーム TextField("郵便番号を入力", text: $postNum) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.leading) // 検索ボタン Button(action:{ // ボタンをタップした時の処理 profileRegisterVM.request(postNum: postNum, completion: { address = profileRegisterVM.changedString }) }) { Text("検索") .foregroundColor(Color(.systemGreen)) .frame(height: 20) .padding(10) ... } ... // 住所入力フォーム TextField("住所を入力", text: $address, onCommit: { self.validateLength() }) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.horizontal) ... こちらがViewModelのコードです ProfileRegisterViewModel.swift import Foundation import SwiftUI class ProfileRegisterViewModel: ObservableObject { @Published var changedString = "" var responseValue: ViewHelper? func request(postNum: String, completion: @escaping () -> Void) { // APIに接続 let zipcode = postNum guard let url = ResponseHelper.createUrl(zipcode: zipcode) else { return } let request = URLRequest(url: url) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } do { // デコードする let requestResults: RequestResults = try JSONDecoder().decode(RequestResults.self, from: data) // checkErrorTypeを初期化 self.responseValue = ResponseType.init(rawValue: requestResults.status)? .checkErrorType(requestResults: requestResults) // UIに変更を加える DispatchQueue.main.async { self.changedString = (self.responseValue?.string)! completion() } } catch let error { print(error) } } task.resume() } } コードの解説 順を追って見ていきます。 まず、ViewModelが更新されたら、再びViewが更新(再描画)される必要があるので、ViewModelがViewに監視されている状態にする必要があります。 Viewで監視したいViewModelを、@ObservedObjectを使ってViewで宣言します。 @ObservedObject var profileRegisterVM: ProfileRegisterViewModel = ProfileRegisterViewModel() また、ViewModelではObservableObjectを継承させます。 class ProfileRegisterViewModel: ObservableObject { ... 今回は、郵便番号入力フォームに郵便番号を入力する→検索ボタン押下→postmanAPIを通して郵便番号と一致する住所を取得→取得した結果を住所入力フォームに表示を実装したいです。これを分解すると以下のようになります。 郵便番号入力フォームに郵便番号を入力する→検索ボタン押下...普通にswiftUiを使ってViewに実装 postmanAPIを通して郵便番号と一致する住所を取得...ViewModelとModelに実装 取得した結果を住所入力フォームに表示...クロージャに実装 現在の私の解釈ですが、外部と接続した後にしたい処理はクロージャに書いておくのが良いのだと思います。「外部との接続が終わったらその結果を用いてこれをするんだっ!」という処理をViewでクロージャの中に書いておいて、「よし、外部との接続が終わったぞ!」というタイミングの時にViewModelでそのクロージャを呼び出す。。という流れかなと思ってます。 検索ボタンを押下した後で、結果を住所入力のテキストフィールドに表示したいので、クロージャは検索ボタンのactionの中に書きます。Buttonのactionに実装する事で、ボタン押下時に実行されます。 // 検索ボタン Button(action:{ // ボタンをタップした時の処理 profileRegisterVM.request(postNum: postNum, completion: { address = profileRegisterVM.changedString }) }) closureはcompletionの部分です。 completion: { address = profileRegisterVM.changedString } このclosureはViewModelのrequest()というメソッドの引数という形で呼び出されているので、このような書き方になります。 // ボタンをタップした時の処理 profileRegisterVM.request(postNum: postNum, completion: { address = profileRegisterVM.changedString }) 実際にViewModelのrequest()の引数としてどのように呼び出されているか見てみます。 func request(postNum: String, completion: @escaping () -> Void) { 引数には、郵便番号入力フォームに入力された郵便番号とクロージャが入っています。 TextFieldはこのように宣言すると、 TextField("郵便番号を入力", text: $postNum) postNumで、入力された値を受け取れます。異なるファイルだとしてもこの形で受け取れます。swiftUI便利すぎますねぇ。。 ViewModelでの処理はざっくり以下のような感じです func request(postNum: String, completion: @escaping () -> Void) { // 外部APIに接続 : : // postmanAPIの結果を受け取る self.responseValue = ResponseType.init(rawValue: requestResults.status)? .checkErrorType(requestResults: requestResults) // UIに変更を加える DispatchQueue.main.async { self.changedString = (self.responseValue?.string)! completion() } } 簡単に説明すると、DispatchQueue.main.async{}の中でUIに関わる処理をして、その外ではAPI接続に関する処理を書きます。completion()はクロージャの事です。クロージャでは表示する文字列を変更する処理を書いてますよね。つまりUIを変更する処理なので、DispatchQueue.main.async{}の中で呼び出します。request()の引数に入れてるので、ViewModelのファイルからでも呼び出せるのです。 ちなみに、クロージャではaddressという変数の中身を書き換えてますよね。addressは住所入力を行うテキストフィールドのtextに指定された変数です。 TextField("住所を入力", text: $address, onCommit: { self.validateLength() }) 先ほど、テキストフィールドに入力された値は(この場合は)addressで受け取れると説明しましたが、同時にこのaddressに適当な文字列を代入すると、代入した文字列がフォームに表示されるようになります。 DispatchQueue.main.async{}の中には、クロージャを呼び出す以外にも行っている処理があります。この部分です。 self.changedString = (self.responseValue?.string)! このchangedStringがViewModelの冒頭でこのように宣言されてます。 @Published var changedString = "" @Publishedをつけて宣言することによって、Viewとバインディングできるようになります。詳しく順を追って見てみるとこんな感じです〜。 // ===== ViewModel ===== // Viewとバインディングできるように@Publishedで宣言 @Published var changedString = "" // UIを更新する処理を書く DispatchQueue.main.async { // apiと接続した結果(取得してきた住所)をchangedStringに格納 self.changedString = (self.responseValue?.string)! // クロージャを実行 completion() } // ======= View ======= // クロージャ completion: { // ViewModelで更新したchangedStringをaddressに代入することで // 住所を入力するTextFieldに取得した住所の結果を表示する address = profileRegisterVM.changedString } これによって、郵便番号入力フォームに郵便番号を入力し、検索ボタンを押下すると、住所入力フォームに住所が表示されます!!こんな感じ!! 最後に まだまだ初心者なもんで、いろいろ解釈とか使うべき用語の選択とか間違っていると思います。間違ってるところはご指摘頂きたいです!!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む