- 投稿日:2020-06-29T19:52:15+09:00
キャプチャリストにおいて複数の変数に対して weak, unowned キーワードを付ける際の注意点
今回はトレイリングクロージャにおけるキャプチャリストについての気づきをシェアします!
小ネタですが、意識しないと気づかずメモリリークを起こす原因になるような注意点です!本題: 弱参照のつもりが、強参照に!?
ネットにあるコードを読んでいると、たまに、メモリリーク解消を目的とした弱参照を宣言するために、以下のような実装を見かけます。
alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self, alertController] _ in
[weak 変数1, 変数2]
のような実装になっていますね。
コードを読むと、どうやら変数1
変数2
ともにweak
属性にして弱参照にしたいようです。
が、この実装では変数2
が強参照になります。もう一度申し上げます。
変数2
は強参照です。対策
じゃあ、どうするか??
[weak 変数1, weak 変数2]
とそれぞれの変数の前にweak
を宣言することで実現可能です。
先程のUIAlertAction
のトレイリングクロージャの例だと、alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self, weak alertController] _ inとなります。
おわり
メモリリークの原因になるので気をつけたいですね?
- 投稿日:2020-06-29T16:00:20+09:00
手を動かして理解するCocoa MVCパターン
はじめに
本記事ではiOSアプリ実装では最もよく用いられているであろうCocoa MVCパターンについて解説します。
巷ではクリーンアーキテクチャーはじめ様々なソフトウェアアーキテクチャーが出てきていますが、Swiftでアプリケーションを構築する上で余程大規模にならない限りは、Apple自体が推していることもあり、このCocoa MVCパターンを使うのが自分の経験上最もいいのではないかなと思います。ソフトウェアアーキテクチャーは本などで読むとふむふむなるほどと思うのですが、いざ実装しようとしてみると難しいことが多いです。
そこで今回は実際に非常に単純なアプリケーションを実装しながら手を動かして理解するような構成にしています。
では早速始めていきましょう!Cococa MVCパターンについてのイメージを掴む
何も知らない状態で手を動かしてもあまり学習効率が良くないので、先にCocoa MVCのイメージを掴んでおきましょう。
実際手を動かした後もう一度復習するので、この時点ではこんなもんだなーくらいの理解で構いません。MVCパターンとはModel-View-Controller Patternの略で、その名の通りアプリケーション内のコードをModel、View、Controllerの3つの役割に分割します。
Model
Modelはデータの保持及び処理を担当します。
具体的には通信処理や計算処理、ローカルストレージに対する保存処理など、後述のView、Controller以外全てがModelに含まれます。RailsなどのWebアプリのフレームワークにおけるSQLデータベース上のテーブルを表現するModelとは意味が異なるので注意が必要です。
またModelは保持しているデータが変更されたら、そのModelを購読しているControllerに変更を通知します。View
Viewは画面の描画処理を行います。
再利用性を高めるためにも極力ロジックは含めずに、入力を受けてそれをそのまま描画するような設計にすることが重要です。Controller
ControllerはModelとViewの参照を保持し、Modelについては変更を監視します。
Controllerはユーザーの入力を受けつけ、Modelに処理を依頼し、Modelが変更されたのを検知してViewの描画を更新します。これらをまとめると以下のようになります。
名前 役割 Model データ保持、処理を行う View 画面描画を行う Controller ユーザーの入力を受け付ける。ModelとControllerの橋渡し役となりアプリ全体をコントロールする。
ここで非常に重要なポイントはControllerがViewとModelの橋渡し役となることで、ViewとModelが何かに依存せずに完全に独立しているところです。
こうすることで、ViewとModelは様々なところで再利用することができるようになります。
逆にControllerは特定のViewとModelに強く依存しており、Cocoa MVCは
Controllerの再利用性を犠牲にViewとModelの再利用性を極限まで高めた設計と言えます。そして実際にユーザーの入力〜画面の描画までの全体の処理の流れは以下のようになります。
- [Controller] ユーザーの入力を受け付ける。
- [Controller] Modelに処理を依頼する。
- [Model] データを処理する。
- [Model] データの処理結果を購読しているControllerに通知する
- [Controller] Modelの変更を検知する。
- [Controller] Viewに描画を指示する。
- [View] 画面に描画を行う。
ではここままででざっくりイメージがつかめたと思うので、次項から実際に手を動かして実装していきましょう!
手を動かして実装する
今回作成するアプリは+ボタン、-ボタンで数字を増減させることができるシンプルなカウンターアプリです。
こちらをCocoa MVCパターンを使って実装していきましょう。
ゼロからプロジェクト作成をしていただいても構いませんし、初期の状態と完成形をこちらに用意しておいたので、こちらをCloneしてJP/Starterから始めても構いません。
https://github.com/kazuooooo/CocoaMVCFromScratchCounterModelを実装する
まずはMVCのM、ModelにあたるCounterModelを実装していきましょう。
前述の通りデータの保持、処理を行うのがModelの役割なので、
- 今数値がいくらなのか保持する(データの保持)
- 数字を増やす/減らす(データの処理)
- Modelを監視しているコントローラーに変更を通知する
が必要です。
CounterModel.swiftimport Foundation class CounterModel { static let notificationName = "CounterModelChanged" let notificationCenter = NotificationCenter() // 今数値がいくらなのか保持する(データの保持) internal var count: Int = 0 { didSet { // Modelを監視しているコントローラーに変更を通知する notificationCenter.post( name: .init(rawValue: CounterModel.notificationName), object: count ) } } // 今数値がいくらなのか保持する(データの保持) func countUp(){ count += 1 } func countDown(){ count -= 1 } }CounterViewを実装する
続いてMVCのVにあたるCounterViewを実装します。
Viewの役割は描画処理を行うことです。
CountViewはrenderというメソッドを通してcountLabelに描画処理を行います。CounterView.swiftimport Foundation import UIKit class CounterView: UIView { @IBOutlet weak var countLabel: UILabel! public func render(count: Int){ countLabel.text = String(count) } }続いて実際の画面をStoryBoardで作成してください。(Starterを使っている場合は事前に作成してあります。)
カウントのLabelはIBOutletで接続し、親のViewにCounterViewを設定します。
CounterViewControllerを実装する
最後にMVCのC、CounterViewControllerを実装します。
ControllerではViewとModelの橋渡しをするために
- Modelの変更を監視する
- ユーザーの入力を受け付けて、Modelに処理を依頼する
- Modelの変更を検知して、Viewに描画処理を依頼する
を行う必要があります。
実装は以下のようになります。CounterViewControllerimport UIKit class CounterViewController: UIViewController { // ViewとModelの参照を保持する @IBOutlet var counterView: CounterView! private(set) lazy var counterModel = CounterModel() override func viewDidLoad() { super.viewDidLoad() // Modelの変更を監視する counterModel.notificationCenter.addObserver( self, selector: #selector(self.handleCountChange(_:)), name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil ) } // 変更を検知する @objc func handleCountChange(_ notification: Notification) { if let count = notification.object as? Int { // Viewに描画処理を依頼する counterView.render(count: count) } } // 入力を受け付ける @IBAction func OnPlusButtonTapped(_ sender: Any) { // Modelに処理を依頼する counterModel.countUp() } @IBAction func OnMinusButtonTapped(_ sender: Any) { counterModel.countDown() } }こちらもボタンのIBActionへの紐付け、ViewControllerクラスの設定を忘れないようにしましょう。
さて以上で実装は完了です。
一度Runをして+/-ボタンが正しく動くか確認してみてください!再度パターンに当てはめて考えてみる
実装は完了しましたが、写経しただけ感があってまだどこかしっくりきていませんよね?
最後にもう一度全体像をコードを見ながら確認していきましょう。
そうすることで理解がグッと深まるはずです。依存関係を見てみる
まずは依存関係について先ほど見たこちらの図を見ながらコードをもう一度確認してみましょう。
Controllerのコードを見てみると、ViewとModelの参照及び、変更の監視がされていることがわかります。
CounterViewControllerclass CounterViewController: UIViewController { // ViewとModelの参照を保持する @IBOutlet var counterView: CounterView! private(set) lazy var counterModel = CounterModel() override func viewDidLoad() { super.viewDidLoad() // Modelの変更を監視する counterModel.notificationCenter.addObserver( self, selector: #selector(self.handleCountChange(_:)), name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil ) } ... }またViewとModelはControllerは処理を受け付けるだけで完全に独立しており、再利用可能なこともコードを見てチェックしておいてください。
処理の流れをチェックする
最後に+ボタンを押した時を例に全体の処理の流れも図とコードを見ながらチェックしていきます。
1.入力を受け付ける
CounterViewController// 入力を受け付ける @IBAction func OnPlusButtonTapped(_ sender: Any) { ... }2. Modelに処理を依頼する
入力を受けたらControllerはModelに処理を依頼します。CounterViewController@IBAction func OnPlusButtonTapped(_ sender: Any) { // 2. Modelに処理を依頼する counterModel.countUp() }3. データを処理する
Modelはデータを処理します。
今回の場合はcountUpが呼び出されているので自身のcountをインクリメントします。CounterModelclass CounterModel { ... // データを処理する func countUp(){ count += 1 } }4. 変更を通知する
ModelはNotificationCenterを通じて購読者に変更を通知します。
CounterModelclass CounterModel { ... internal var count: Int = 0 { didSet { // Modelを監視しているコントローラーに変更を通知する notificationCenter.post( name: .init(rawValue: CounterModel.notificationName), object: count ) } } ... }5. Modelの変更を検知する
ControllerはModelのNotificationCenterに登録しておいたObserverからModelの変更を検知します。
class CounterViewController: UIViewController { override func viewDidLoad() { ... // Modelの変更を監視する counterModel.notificationCenter.addObserver( self, selector: #selector(self.handleCountChange(_:)), name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil ) } ... // 変更を検知する @objc func handleCountChange(_ notification: Notification) { ... } }6.描画指示
@objc func handleCountChange(_ notification: Notification) { if let _ = notification.object as? Int { // Viewに描画処理を依頼する counterView.render(count: counterModel.count) } }7.描画処理を行う
Controllerからの描画指示を受けてViewは描画処理を行います。
CounterViewclass CounterView: UIView { // 描画処理を行う public func render(count: Int){ countLabel.text = String(count) } }終わりに
いかがだったでしょうか?
ソフトウェアアーキテクチャーパターンは初めはとっつきにくいですが、一度理解してしまえばエンジニアにとって非常に強力な武器になってくれます。
今回のCocoaMVCのようにきっちり理解できていなくて雰囲気で使っているなーというところは一度腰を据えて自分で手を動かしてみることで理解が一気に深まるのでオススメです!
- 投稿日:2020-06-29T13:03:17+09:00
NavigationControllerの下の色を変える方法
NavigationContronContllerの下(TabBar)の色を変えるのに苦労したので記録を残します。
NavigationContronContllerの下(TabBar)はデフォルトで白と青
import UIKit class TabBarViewController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() UITabBar.appearance().barTintColor = UIColor(red: 174/255, green: 238/255, blue: 226/255, alpha: 1)//下の色(デフォルトで白) UITabBar.appearance().tintColor = UIColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1)//上の色(デフォルトで青) } }
- 投稿日:2020-06-29T00:04:41+09:00
[SwiftUI]正規表現によるネイティブUIでのマルチメディア
実現するもの
全体のイメージ
詳しく見ましょう
speciesData.json"detailText": [ "コガタペンギンは保護色があり、青い背中は上から見れば海と同じに見え、白い腹は下の捕食者と獲物を迷惑させることができます。したがって、夜に波から上陸するとき、海岸に打ち寄せる白い波が突然立ち上がって、コガタペンギンの群れになったように見えます。", "これはまさにおとぎ話のようで、英語では「フェアリーペンギン(Fairy Penguin)」とも呼ばれています。", "<image>02</image>", "<caption>夜に帰ってくるコガタペンキンの群れ。画像:Phillipislandtourism / Wikimedia Commons</caption>", "コガタペンギンの平均寿命は6〜7年くらいですが、飼育下では20年を超えることもあります。", ]上記のコードで生成されたのはこちら⬇️
このように、文字列(String)だけのデータをもとに、
コンテンツ 記述 本文(Text) 本文 画像(Image) <image>画像番号</image> 画像の説明文(Text) <caption>説明文</caption> が混在しているマルチメディアのコンテンツをネイティブUIで自動生成します。
ここは例示なので、コンテンツの種類と記述方法は簡単にカスタマイズできます。
実装方法
正規表現で記述を認識
正規表現に詳しくなくても大丈夫です。
まず、ある文字列が指定したパターンと一致しているか、を教えてくれるものとして覚えましょう。例えば、"<image>"で始まり、"</image>"で終わる文字列であるか、ということを判断してくれます。
パターンの定義
RegularExpression.swiftenum RegexPattern: String { case image = "^<image>.*</image>$" case caption = "^<caption>.*</caption>$" }"^<image>.*</image>$"というパターンは、
"<image>"で始まり、"</image>"で終わることを意味しています。
この部分を書き換えれば、記述方法をカスタマイズできます。一致判断とタグ削除
RegularExpression.swiftimport Foundation extension String { //正規表現の一致判断 func match(pattern:RegexPattern) -> Bool { let pattern = pattern.rawValue let regex = try! NSRegularExpression(pattern:pattern) return regex.firstMatch(in:self, range:NSRange(self.startIndex..., in:self)) != nil } //両端のタグの削除 func strip() -> String { let lstrip = self.replacingOccurrences(of:"^<\\w+>", with:"", options:NSString.CompareOptions.regularExpression, range:self.range(of:self)) return lstrip.replacingOccurrences(of:"</\\w+>$", with:"", options:NSString.CompareOptions.regularExpression, range:lstrip.range(of:lstrip)) } }ここでは、呼び出しやすくするために、String型のメソッドとして実装します。
ネイティブUIを生成
SpeciesDetail.swiftimport SwiftUI struct SpeciesDetail: View { var species: Species var body: some View { ScrollView { VStack { //文字列ごと処理 ForEach(self.species.detailText, id: \.self) { text -> AnyView in //imageのパターンと一致した場合 if text.match(pattern: .image) { return AnyView( //SwiftUIのImageを利用し、画像コンテンツの表示をカスタマイズ Image("\(self.species.imageName)_\(text.strip())") .resizable() .scaledToFit() ) //captionのパターンと一致した場合 } else if text.match(pattern: .caption){ return AnyView( //SwiftUIのTextを利用し、画像の説明文の表示をカスタマイズ Text(text.strip()) .foregroundColor(Color.gray) .font(.custom("Baskerville", size: 16)) ) //どのパターンとも一致しない場合 } else { return AnyView( //SwiftUIのTextを利用し、本文の表示をカスタマイズ Text(" " + text) .padding(.top, 18) .font(.custom("Baskerville", size: 20)) .lineSpacing(12) .fixedSize(horizontal: false, vertical: true) ) } } } .padding() } } } struct SpeciesDetail_Previews: PreviewProvider { static var previews: some View { SpeciesDetail(species: speciesData[0]) } }フルコード
以上は自分のアプリの一部として実装しています。
詳しくはこちら(GitHubへ)をご覧ください。