20200629のSwiftに関する記事は4件です。

キャプチャリストにおいて複数の変数に対して 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

となります。

おわり

メモリリークの原因になるので気をつけたいですね?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

手を動かして理解するCocoa MVCパターン

cover

はじめに

本記事では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の橋渡し役となりアプリ全体をコントロールする。

グループ 40.png
ここで非常に重要なポイントはControllerがViewとModelの橋渡し役となることで、ViewとModelが何かに依存せずに完全に独立しているところです。
こうすることで、ViewとModelは様々なところで再利用することができるようになります。
逆にControllerは特定のViewとModelに強く依存しており、Cocoa MVCは
Controllerの再利用性を犠牲にViewとModelの再利用性を極限まで高めた設計と言えます。

そして実際にユーザーの入力〜画面の描画までの全体の処理の流れは以下のようになります。

グループ 43.png

  1. [Controller] ユーザーの入力を受け付ける。
  2. [Controller] Modelに処理を依頼する。
  3. [Model] データを処理する。
  4. [Model] データの処理結果を購読しているControllerに通知する
  5. [Controller] Modelの変更を検知する。
  6. [Controller] Viewに描画を指示する。
  7. [View] 画面に描画を行う。

ではここままででざっくりイメージがつかめたと思うので、次項から実際に手を動かして実装していきましょう!

手を動かして実装する

今回作成するアプリは+ボタン、-ボタンで数字を増減させることができるシンプルなカウンターアプリです。

ezgif.com-crop.gif

こちらをCocoa MVCパターンを使って実装していきましょう。
ゼロからプロジェクト作成をしていただいても構いませんし、初期の状態と完成形をこちらに用意しておいたので、こちらをCloneしてJP/Starterから始めても構いません。
https://github.com/kazuooooo/CocoaMVCFromScratch

CounterModelを実装する

まずはMVCのM、ModelにあたるCounterModelを実装していきましょう。
前述の通りデータの保持、処理を行うのがModelの役割なので、

  • 今数値がいくらなのか保持する(データの保持)
  • 数字を増やす/減らす(データの処理)
  • Modelを監視しているコントローラーに変更を通知する

が必要です。

CounterModel.swift
import 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.swift
import Foundation
import UIKit

class CounterView: UIView {

    @IBOutlet weak var countLabel: UILabel!
    public func render(count: Int){
        countLabel.text = String(count)
    }
}

続いて実際の画面をStoryBoardで作成してください。(Starterを使っている場合は事前に作成してあります。)
Main_storyboard.png

カウントのLabelはIBOutletで接続し、親のViewにCounterViewを設定します。
Main_storyboard.png
Main_storyboard.png

CounterViewControllerを実装する

最後にMVCのC、CounterViewControllerを実装します。

ControllerではViewとModelの橋渡しをするために

  • Modelの変更を監視する
  • ユーザーの入力を受け付けて、Modelに処理を依頼する
  • Modelの変更を検知して、Viewに描画処理を依頼する

を行う必要があります。
実装は以下のようになります。

CounterViewController
import 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クラスの設定を忘れないようにしましょう。
Main_storyboard.png
Main_storyboard.png

さて以上で実装は完了です。
一度Runをして+/-ボタンが正しく動くか確認してみてください!

再度パターンに当てはめて考えてみる

実装は完了しましたが、写経しただけ感があってまだどこかしっくりきていませんよね?
最後にもう一度全体像をコードを見ながら確認していきましょう。
そうすることで理解がグッと深まるはずです。

依存関係を見てみる

まずは依存関係について先ほど見たこちらの図を見ながらコードをもう一度確認してみましょう。

グループ 40.png

Controllerのコードを見てみると、ViewとModelの参照及び、変更の監視がされていることがわかります。

CounterViewController
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
        )
    }
...
}

またViewとModelはControllerは処理を受け付けるだけで完全に独立しており、再利用可能なこともコードを見てチェックしておいてください。

処理の流れをチェックする

最後に+ボタンを押した時を例に全体の処理の流れも図とコードを見ながらチェックしていきます。

1.入力を受け付ける

1.png
まずはControllerが+ボタンの入力を受け付けます。

CounterViewController
// 入力を受け付ける
@IBAction func OnPlusButtonTapped(_ sender: Any) {
    ...
}

2. Modelに処理を依頼する

2.png
入力を受けたらControllerはModelに処理を依頼します。

CounterViewController
@IBAction func OnPlusButtonTapped(_ sender: Any) {
    // 2. Modelに処理を依頼する
    counterModel.countUp()
}

3. データを処理する

3.png

Modelはデータを処理します。
今回の場合はcountUpが呼び出されているので自身のcountをインクリメントします。

CounterModel
class CounterModel {
    ...
    // データを処理する
    func countUp(){ count += 1 }
}

4. 変更を通知する

4.png

ModelはNotificationCenterを通じて購読者に変更を通知します。

CounterModel
class CounterModel {
    ...
    internal var count: Int = 0 {
        didSet {
            // Modelを監視しているコントローラーに変更を通知する
            notificationCenter.post(
                name: .init(rawValue: CounterModel.notificationName),
                object: count
            )
        }
    }
    ...
}

5. Modelの変更を検知する

5.png

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.描画指示

6.png
ControllerはViewに描画を指示します。

@objc func handleCountChange(_ notification: Notification) {
    if let _ = notification.object as? Int {
        // Viewに描画処理を依頼する
        counterView.render(count: counterModel.count)
    }
}

7.描画処理を行う

7.png

Controllerからの描画指示を受けてViewは描画処理を行います。

CounterView
class CounterView: UIView {

    // 描画処理を行う
    public func render(count: Int){
        countLabel.text = String(count)
    }
}

終わりに

いかがだったでしょうか?
ソフトウェアアーキテクチャーパターンは初めはとっつきにくいですが、一度理解してしまえばエンジニアにとって非常に強力な武器になってくれます。
今回のCocoaMVCのようにきっちり理解できていなくて雰囲気で使っているなーというところは一度腰を据えて自分で手を動かしてみることで理解が一気に深まるのでオススメです!

[参考]
iOSアプリ設計パターン入門
Model-View-Controller

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NavigationControllerの下の色を変える方法

NavigationContronContllerの下(TabBar)の色を変えるのに苦労したので記録を残します。

NavigationContronContllerの下(TabBar)はデフォルトで白と青
IMG_CC1004CB8DBE-1.jpeg

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)//上の色(デフォルトで青)

    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SwiftUI]正規表現によるネイティブUIでのマルチメディア

実現するもの

全体のイメージ

multimedia.gif

詳しく見ましょう

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.swift
enum RegexPattern: String {
    case image = "^<image>.*</image>$"
    case caption = "^<caption>.*</caption>$"
}

"^<image>.*</image>$"というパターンは、
"<image>"で始まり、"</image>"で終わることを意味しています。
この部分を書き換えれば、記述方法をカスタマイズできます。

一致判断とタグ削除

RegularExpression.swift
import 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.swift
import 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へ)をご覧ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む