20200629のiOSに関する記事は6件です。

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

App Clipsをざっくり理解する

WWDC20で発表された、App Clipsをざっくり理解するために要点をまとめています。

仕様・特徴

  • ユーザーはアプリをインストールする必要なく、ネイティブアプリの一部を使用できる
  • App Clipによって処理されるURLを使用する(ユニバーサルリンクに似ている)
  • NFCタグ、QRコード、Smart App Banner、iMessageのリンク、Mapsから開くことができる
  • App Clip URLはApp Store Connectを使用して登録する
  • アプリとApp Clipの両方を含むビルドをApp Store Connectに配信すると、App Clip Configurationセクションが表示される
  • 異なるエクスペリエンスには個別のURLを使用する
  • URLはprefixの一致に基づいているため、すべての可能なApp Clip URLを登録する必要はない(クエリストリングやパスパラメータで柔軟に出来る)
  • アプリがインストールされている場合アプリが優先、アプリがインストールされていない場合は、App Clipがダウンロードされる
  • 使用されない場合、iOSはApp Clipとそのデータを削除する

制限・注意点

  • App Clipは追加機能であり、App Clip単体では提供できない
  • App Clipでのインタラクションは、迅速かつ集中的に行う必要がある
  • XcodeでApp Clip用のターゲットを作成する必要がある
  • App Clipは10メガバイト未満にする必要がある
  • App Clipからの機密情報へのアクセスは制限されている
  • タイトル、サブタイトルの長さ、画像サイズ、アスペクト比、およびフォーマットの要件がある
  • ユーザーが一貫した体験を得られるように、App Clipにはアプリと同じ名前とアイコンを使用する

その他出来ること

  • App Clipからアプリへデータ移行するには、App Groupを利用する
  • SKOverlayを使用してアプリをインストールするよう促すことができる
  • XcodeでApp ClipのURL処理コードをテストするには、テストURLを指定できる
  • App Clipsは、カメラ、マイク、およびBluetoothの許可を要求することができる
  • App Clipの通知では、毎回の起動後、最大8時間まで許可を得ることができる

関連リンク

https://developer.apple.com/videos/play/wwdc2020/10174
https://developer.apple.com/videos/play/wwdc2020/10146
https://developer.apple.com/videos/play/wwdc2020/10120
https://developer.apple.com/videos/play/wwdc2020/10172
https://developer.apple.com/app-clips/
https://developer.apple.com/design/human-interface-guidelines/app-clips/overview/
https://developer.apple.com/documentation/app_clips/developing_a_great_app_clip
https://developer.apple.com/documentation/app_clips/creating_an_app_clip
https://developer.apple.com/documentation/swiftui/fruta_building_a_feature-rich_app_with_swiftui

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

CocoPodsで外部のライブラリの導入方法

多くのプロジュエクトが外部のライブラリを使っています。
Githubからプロジェクトをダウンロードして、ライブラリの導入したり、インストールしたりする必要です。

必要のライブラリの導入方法

1.ターミナルでpodバージョンを確認
pod --version
バージョンが1.7.5より古い、またはpodがインストールしていない場合は下記のコマンドで更新またインストールします。

gem sources --remove https://rubygems.org/
gem sources --add https://gems.ruby-china.com/
//pod install
sudo gem install cocoapods -n /usr/local/bin
sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
//update pod local library
pod setup

2.プロジェクト関連のライブラリを追加します。
プロジュエクトが必要なラブラリの定義はPodfileで記載します。

//cd 実際のxiaozhiboのフォルダのパス 例えば: 
cd ~/iOs/XiaoZhiBo
// Podfileから必要なラブラリをインストール
pod install

参考資料

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

CocoaPodsで外部のライブラリの導入方法

多くのプロジュエクトが外部のライブラリを使っています。
Githubからプロジェクトをダウンロードして、ライブラリの導入したり、インストールしたりする必要です。

必要のライブラリの導入方法

1.ターミナルでpodバージョンを確認
pod --version
バージョンが1.7.5より古い、またはpodがインストールしていない場合は下記のコマンドで更新またインストールします。

gem sources --remove https://rubygems.org/
gem sources --add https://gems.ruby-china.com/
//pod install
sudo gem install cocoapods -n /usr/local/bin
sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
//update pod local library
pod setup

2.プロジェクト関連のライブラリを追加します。
プロジュエクトが必要なラブラリの定義はPodfileで記載します。

//cd 実際のxiaozhiboのフォルダのパス 例えば: 
cd ~/iOs/XiaoZhiBo
// Podfileから必要なラブラリをインストール
pod install

参考資料

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

Flutterで画像、カスタムフォントを使用する方法

今回はFlutterアプリで画像とカスタムフォントを使用する方法を記述します。

プロジェクトに画像とフォントを追加する

  1. プロジェクト直下にassetsフォルダを作成し、その直下にimagesfontsフォルダを作成する。
  2. 使用したい画像とフォントを各フォルダに格納する。

スクリーンショット 2020-06-29 4.11.29.png

pubspec.yamlにパスを記述する

Flutterで画像やフォントを使用するには、pubspec.yamlにパスを記述する必要があります。

pubspec.yaml
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  assets:
    - assets/images/
    - assets/fonts/

# An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware.

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  fonts:
    - family: OpenSansCondensed
      fonts:
        - asset: assets/fonts/OpenSansCondensed-Light.ttf
          weight: 300
        - asset: assets/fonts/OpenSansCondensed-Bold.ttf
          weight: 700

フォントファイルは太さによって複数に分かれていることが多いので、それぞれにweightを定義します。
pubspec.yamlに記述する際、インデントが揃っていないと、エラーになったり、上手く適用されないなどがあるので、注意が必要です。

画像を表示する

実際にアプリ上にプロジェクトに追加した画像を表示してみます。

aseetsの画像を表示する

main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Demo'),
        ),
        body: Center(
          child: Image.asset('assets/images/sample.png'),
        ),
      ),
    );
  }
}

ビルドすると以下のように画像を表示させることができました。
Simulator Screen Shot - iPhone 11 Pro - 2020-06-28 at 21.53.33.png

ネットワーク上の画像を表示させる

Image.network('イメージのURL')

画像のURLを記述することで、URL先の画像を表示させることもできます。

カスタムフォントを使用する

カスタムフォントを適用する方法は主に2種類あります。

アプリ全体のデフォルトに設定する

MaterialApp()themeに指定してあげることで、デフォルトのフォントに設定されます。

main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(fontFamily: 'OpenSansCondensed'),
      home: SamplePage(),
    );
  }
}

個別に指定する

特定のTextにのみ適用させたい場合は、以下のようにTextStylefontFamilyに指定します。
fontWeightpubspec.yamlに記述したweightを指定することで、それぞれのフォントファイルを使用することができます。

main.dart
Text(
          'custom font',
          style: TextStyle(
            fontFamily: 'OpenSansCondensed',
            fontWeight: FontWeight.w300,
            fontSize: 40,
          ),
        ),

Simulator Screen Shot - iPhone 11 Pro - 2020-06-29 at 04.16.40.png

おまけ

フォントファイルをわざわざ用意しなくとも、google_fontsを使用することができるFlutterのパッケージがありますので、そちらを利用してみるのもいいかもしれません。
https://pub.dev/packages/google_fonts

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