LoginSignup
165
156

More than 5 years have passed since last update.

Swiftのアプリ設計時に決めることメモ

Posted at

はじめに

僕がプログラム(プロダクト)を作る際に注意していることのメモで僕自身の設計時の方針がイマイチ決まってなかったのでここにメモとして残す。
前回はRailsの場合なども入れてしまったのでまとまりが悪くなったので今回はSwiftにフォーカスを当てる。

なお、Swiftのバージョンは1.2です。
僕が次に作るアプリで気をつけようと思う事をつらつらメモっていきます。

変数の定義について

変数の定義は悩ましいのですが、Objective-cの時代に沿った命名を使います。

定数

定数は基本的にlet宣言の大文字

class API {
    // APIを使用するか?
    static let USE_FLG = true
    // class let USE_FLG = true  // この定義でも可能で好みで使えば良いと思う。僕はstaticを好む。
}

定数のみの場合はクラスではなく構造体でも定義できますがSwift1.2からstaticclassキーワードが使えるようになったので、僕はクラスで行ってます。
また、グローバル定数は定義しません。(定数の意味をクラスで定義したいため。汎用的な領域をできるだけ侵したくない。)

変数

PrivatePublicで変数を分ける必要はないのですが、Objective-c時代の流れで僕はPrivate変数には_をつけている。

class API {
    // Public変数
    var publicFlg = true
    // Private変数
    private var _privateFlg = false
}

ただ、この方法は別に推奨はしない。
やってみたところ、そこまでの恩恵がなかったため_は付けなくても良いかと思う。

変数の初期値

変数の初期値は付けれる場合には、できるだけ付けてOptional型を使わないようにする。
理由としてはnilが入る余地が無いものについてわざわざOptionalを使うとコードが煩雑化するためである。
(簡単な足し算すら面倒になるし。)

なので!を使う機会は以下の場合に限られてくる。

// init処理では入れれないが必ずデータが入る場合。
// (viewWillAppear、viewDidLoadなどで初期データを設定する場合)
var apiAccessor:ApiAccessor()!

// Xibなどの外部ツールからオブジェクトが挿入される時(後述)
@IBOutlet private weak var _nameText:UITextField!

基本的にはこの二点以外では!を使用すケースは殆ど無いはずである。

一方で?を使う機会は多く存在する。

// 親Viewを保持する。
weak var parentView:UIView?

// ApiAccessorがなんらかの事情で取得できない場合はAPI通信の処理を行わない。
weak var apiAccessor:ApiAccessor()?

// チェックボックスの初期値を設定する。ただし、値が入ってない場合はチェックボックス自体を表示しない。
// あまり良い例ではないが、こういう使い方をするとBool型で三つの分岐を作れる。
var checkBoxFlg:Bool?

?で定義するものは基本的に他のオブジェクトからオブジェクトを借りる(オーナーシップを持たない)ケースが多い。
なので、上記のようにweakと併用して使う事が多い。

letとvarの使い分け

letは改変されない事を期待している場合に使用しますが、オブジェクトの中身までは影響が及ばないためオブジェクトを定義する場合は基本的にletを使う事が多い。
(というか、定義したオブジェクトの中身を変えるケースはかなりレアケースだと思う。※Stringは別として。)

上記にもあるように、定義する際にはデータは入らないが画面を表示する際にデータが入る場合はletを使えないためvarになる。
また、Intなどの計算で使用する場合はvarを定義する。
それ以外は大体letになるので、基本的にはletで定義してどうしても変える必要があるときにvarを使うと堅牢なプログラムになる。

Swift1.2からは以下のように使うまでに必ず値が入る場合はletが使えるので、積極的にletを使っていくといいと思います。

let value:Int

if randFlg {
    value = 1
} else {
    value = 0
}

println("\(value)")

外部からオブジェクトを設定する場合

XibやStoryBoardなどからオブジェクトを挿入する場合はPrivateが効くので以下のような定義でできる限り統一する。

@IBOutlet private weak var _nameText:UITextField!

これは他のControllerから値を直接操作されてしまうとバグの元になるのと、動的ディスパッチを減らす目的のためPrivateをつける習慣があると良い。
(weakに関しても基本的にはつけたほうがいいが、Controller.Viewの外側に配置する場合はweakをつけるとデータがすぐに解放されてしまうので注意。あまりいないと思いますが・・・)

ただし、SpriteBuilderなどの外部ツールからデータを設定する場合は以下のようになる。

weak var _nameText:CCLabelTTF!

SpriteBuilerはPrivateを突破できないのでprivateは外します。
しかし、Privateである事を暗に示したいので_をつけてます。(上記にも書いているようにあまり恩恵がない・・・)

シングルトン

シングルトン設計をすると疎結合になりにくいのでできるだけ避けるようにする。
僕が使うのは端末に保存するデータ(ゲームデータ)をシングルトン設計にしてます。
(様々な局面で扱うデータなので、グローバル領域にあったほうがコードがシンプルになる。。)

class SaveData: NSObject {
    // クリティカルなアプリでない場合はこの程度で十分。
    static let sharedInstance = SaveData()
}

// 保存するときにもこんな感じでできる。(ただしsharedInstanceは長い名前なので僕はdataなどの短いメソッド名で操作している。)
SaveData.sharedInstance().save()

dispatch_onceを使って真面目に書いてもいいけど、殆どにおいて上記のコードで十分だったりする。

シングルトンを使う局面としては大体以下のパターンになると思います。

  • どのレイヤーからもアクセスされるデータ(セーブデータ)
  • コネクションを繋げているオブジェクト(WebSocketとか)
  • クラス変数にした方が良さそうだが設定値が多いもの(APIとか)

API通信のようにコネクションを貼り直す場合はシングルトンを使わずクラス変数を使ったほうが良い場合もあります。
しかし、毎回同じ設定をするコストを考えるとその部分だけをシングルトン設計にして使い回すのはありな戦略だと僕は思います。

グローバル変数

基本的にグローバル変数はNGです。
せめてシングルトンで我慢するか、staticクラス(ほぼ同義だったりもしますが)で管理したほうが管理がしやすいと思います。

設計思想にもよりますが、積極的に使っていくものではないと思ってます。

多言語化対応

多言語化対応は小さいプロジェクトでは大したコストになりませんが、大きなプロジェクトでは結構面倒です。
僕は今まで他言語化してアプリを作ってきましたが、海外で売れることは殆どありませんでした。
(僕だけかもしれませんが・・・)

なので、他言語化については本当に必要でなければできるだけ避けたほうがいいでしょう・・・・

他言語化する場合で、XCodeのみで開発する場合の効果的な開発方法は知りません。
メッセージIDの規則性などを作って地道に行うしかないかと思ってます。

しかし、SpriteBuilderなどを使う場合はSpriteBuilderにも他言語対応する機能があるので使っていくと良いと思います。
昔はメッセージIDみたいなものは一元管理したほうが良いと思ってましたが、表示部分の他言語化とモデルレイヤーの他言語化を一元管理するとメッセージIDの管理が煩雑になる上にどのメッセージIDが何を示しているかがわかりにくくなります。
なので、レイヤーに合わせて他言語化する方法も変えたほうが良いと思います。

※余程売れる確証がない限りは、個人アプリは他言語化をしないほうが無難でしょう。
(開発速度が落ちて、開発リズムが崩れます。)

Enum

他言語化しない場合のEnumはとても便利です。
例えばゲームの人物をEnumで定義する場合に以下のように定義できます。
(僕はEnumはアッパーキャメルケースで書いてます。定数扱いしているEnumの場合は例外的にcase部分を全て大文字で定義します。)

enum UserName:String {
    case Hero = "主人公"
    case King = "王様"
}

println("\(UserName.Hero)はこう言った。")

このように定義しておけば、ゲームの人物の名前を一元管理できます。
また、既に登録されている文字列であるかを調べるときにも便利です。
例えば、ゲームで登場する名前を主人公の名前に使ってほしくない場合は以下のような方法で弾けます。

if UserName(rawValue: "入力値") == nil {
    // ゲームでは使ってない名前
} else {
    // ゲームで使ってる名前
}

ただ、このデータを保存するときには文字列になってしまうという点がありますが、、、、
(まぁ、大量に使わなければそれも大した問題ではないでしょう。)

Enumの良い点は他にもあって、Switch〜Caseと相性がいい。
SwiftでSwitchを使う場合はdefaultが必須だが、Enumに限ってdefaultが必須ではなくなる。

switch userName {
case .Hero
    // 主人公だった場合の処理
case .King
    // 王様だった場合の処理
}

このようにEnumで定義されたもの全てを記載することでdefaultが不要になる。
これの何が便利かというと、Enumを追加すると上記の構文がコンパイルエラーになってくれる点だ。
不用意にEnumに値を追加したことによって、考慮していないコードで実行時エラーが発生せず事前にコンパイルエラーで発見できるため強固なプログラムを作りやすい。

また、Enumは型が決まっていれば.HeroのようにEnum名を省略できるのも良いです。
(名前省略時はSwitch文ではコード補完されますが、if文などではコード補完されません・・・。XCode7では直るのかな・・・?)

if userName == .King {  // ←ここの.Kingがコード補完されない。userNameの型は決まってるから補完されてもいいはずなのに。
    // 王様の処理
} else if userName == .Hero {
    // 主人公の処理
}

また、上記のコードはたまにコンパイルエラーになります。(※2015/8/5現在)

if userName == UserName.King {  // ←ここだけ型省略で書くとエラーになるケースがある。(型推論されない時がある・・・?)
    // 王様の処理
} else if userName == .Hero {   // ←こっちは平気。
    // 主人公の処理
}

どうやら他のクラスで定義してるEnumを使うときに発生しやすいみたいです。

lazy

僕はlazyを使うほど困ったことがないので使ってません。
また、lazyを付けなくてもある程度はSwiftが判断してlazyにしてくれるみたいなので今のとこをは使わずに行こうかと思います。

ただ、関数型言語っぽくSwiftを使う場合はlazyでコストカットできそうですね。

データ保存

データ保存はCoreDataNSUserDefaultsが代表的な方法だと思います。
それ以外にもファイル保存や機密性の高い情報はキーチェーンで保存するなどもあります。
(KVS系のLevelDBなどを使うことも頑張ればできます・・・。今のプロジェクトはLevelDBでデータ管理をしてる・・・。マイナーですよねw)

で、一長一短がありますが僕はNSUserDefaults一択だと思ってます。
NSUserDefaultsはNSCodingインターフェースと組み合わせるとオブジェクト自身をシリアライズできるので、保存データを以下のような構成にしていると一括で保存して復元できるメリットがあります。

DataCenter
  ∟Users(Array)
    ∟User(NSCodingインターフェースを実装したオブジェクト)
    ∟User
  ∟Items(Array)
    ∟Item
    ∟Item

このような構成でクラス設計をすればDataCenterのインスタンスを保存すれば、全てのデータが保存されます。
また、復元も一発でできるのでとても楽に実装ができます。

必要に応じてNSKeyedArchiverを使って、AES256で暗号化することもできます。
s1140227さんのセーブデータを暗号化し「端末への保存」や「PHP連携」を行う。が参考になると思います。

CoreDataSQLでデータへアクセスできるというメリットがあります。
なので、データ量が多いアプリを使う場合はこちらを選択することになりますが・・・
アプリのアップデートでスキーマが変わる場合などの対応とテストが難しいため個人で扱うには少し敷居が高いでしょう。
僕は調べてサンプルコードまで作って確認した程度ですが、それでも結構面倒くさかったです。

なので、データの保存はNSUserDefaultsで保存する方がいいかと思います。

通知系(関数渡し、Delegate、NSNotificationCenter)

通知系の処理というのは、何らかの処理が終わった場合に通知する処理を指します。
主な局面は以下の場合です。

  • APIコールを行った後のレスポンス時に呼ばれる処理
  • ポップアップ画面を出した後に選択された結果を受け取る処理
  • ボタンを押された場合の処理
  • 一方的にサーバーから通知を受け取った場合の処理(プッシュ通知やWebsocketなどで)

このような場合に非同期で呼び出してほしい関数を渡す必要があります。
その時に行う手法は大きく分けて以下の三つがあります。
それぞれの特色と使う機会について記載しておきます。

設計時にはどのケースでは何を使うかをある程度定義しておく必要があります。
統一なく以下の処理が乱立するとコードが読みにくくなります。
※Promiseなどを使うのも選択肢の一つですが、ここではライブラリに関しては言及しません。

関数渡し

関数自体を以下のように受け渡します。

func api(callback:(result:Result)->()) {
    // API呼び出し処理
    ...
    // 非同期で結果を返す。
    callback(.OK)
}

// API呼び出し
api() { result in
    // 結果を受けとった後の処理
}

この方法は比較的コード量が少なく、僕がメインで使う方法です。
コードも直感的なので、おすすめです。
しかし、API呼び出し後の処理で使っている変数は全てキャプチャーされるのでweakを意識したコードを書かなければなりません。

コールバックされるケースが少ない場合に有効ですが、以下のような様々なケースで結果を受け取る場合には不向きです。

  • 成功した場合
  • 失敗した場合
  • キャンセルされた場合
  • Aのボタンを押された場合
  • データ構造が変わった場合

などのように、委託元で様々なケースで通知を出す場合はDelegateの方が便利です。

Delegate

Delegateは以下のように受け渡します。

// プロトコル(インターフェース)を事前に決めておく必要がある。
protocol ApiDelegate {
    func callback(result:Result) -> ()
}

// レシーバーではプロトコルを実装しておく必要がある。
class Controller: ApiDelegate {
    // APIを呼び出す
    func main() {
        let api = API()
        // ApiDelegateを実装しているクラスを受け渡す。
        api.delegate = self
        api.call()
    }

    func callback(result:Result) {
        // 結果を受け取った時の処理
    }
}

// API呼び出し側ではdelegateを受け取る必要がある。
class Api {
    // よほどの理由がない限りはweakを必ず付けること
    weak var delegate:ApiDelegate?

    // API呼び出し処理
    func call() {
        // 非同期で結果を返します。
        self.delegate?(.OK)
    }
}

見てわかる通り、関数渡しに比べて定義するものが多くコード量が増える。
しかし、一方でweakを意識するのはdelegate:ApiDelegate?の部分だけなので安全性は高い。

関数渡しのところで書いたように、様々なケースに対応した通知処理を行う場合はdelegateは向いている。
(UITableなどはその典型的な例だろう。)

    // よほどの理由がない限りはweakを必ず付けること
    weak var delegate:ApiDelegate?

この部分でweakを使っているのは、APIの処理をしている間にControllerインスタンスが解放されてしまう可能性があるためである。
上記にも書いたようにここだけ守ればメモリリークを防げるというのは大きなメリットである。

しかし、一方でこれだけ仰々しいコードが散乱するとコードの可読性が落ちてしまうのは目に見えているのでdelegateを使う場合と関数渡しを使う場合は明確に定義していたほうがいいでしょう。
僕は基本的にdelegateは使わない。
delegateを使うと密結合な状態が発生しやすいため、できる限り関数渡しを行うようにしている。

※注意
delegateモデルが必ず密結合になるというわけではないです。
ただ、関係しているクラスだけをみて実装を進めてしまうと「気づいたら密結合になってた!」みたいな状況に陥りやすいと思ってます。

NSNotificationCenter

NSNotificationCenterは以下のように受け渡しを行います。

class Controller {
    // 初期化処理
    init() {
        // コールバック先を指定します。
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "callback:", name: "apiCall", object: nil)
    }

    // APIを呼び出す
    func main() {
        // コールバック先は既に設定済み
        Api.call()
    }

    func callback(notification: NSNotification?) {
        // 結果を受け取った時の処理
        let result = notification.userInfo
    }

    deinit() {
        // 登録したコールバックを削除します。(これを行わないとコールバック処理がどんどん積まれていってしまう。)
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
}

// API呼び出しクラス
class Api {
    // API呼び出し処理
    class func call() {
        // 非同期で結果を返します。
        NSNotificationCenter.defaultCenter().postNotificationName("apiCall", object: nil, userInfo: ["result": Result.OK])
    }
}

NSNotificationCenterは上記の二つとは性質が大きく違います。
関数渡しDelegateも本質は同じで関連性があるクラスに対してしか通知を送れません。
しかし、NSNotificationCenterは登録している名前が同じもの全てに対して通知を送ることができます。
なので、まったく関連性がないクラスでも通知を受け取れるという点が特色です。

NSNotificationCenterの機構を考えると、しっかり管理しないと一気にコードが煩雑化します。
なので、この機構を使う場合は定義場所をドキュメント化するかnameをenumで管理してコメントを手厚くするなどの工夫が必要です。

便利ですが僕は滅多に使いません。
管理が煩雑になる機構は個人レベルのアプリでは死活問題になるからです。
また、解放漏れをしてしまうと同じメソッドが二回呼ばれたりする悲劇に見舞われる可能性もあります。
使用する際には重々注意して使いましょう!

疎結合にコードを書く

最後になりますが、クラス設計をするときには定義した以上の事は行わないクラス設計が重要です。
僕がよくやるのは、定義したクラスで処理を書いていると「ここにコードを書くと楽だからコードを書いちゃおう」って感じでコードを書いて使い回しができないクラスになってるときがあります。

なので、はじめに以下の事を決めてからクラス設計を行うと良いと思います。

  • そのクラスの責務は何か?
  • どの局面で使用するものなのか?
  • 汎用的に使うのか?使い捨てをするのか?(ただ、使い捨てクラスのつもりでも後で使いまわしたいという時もあるので慎重に!)
  • 汎用的に使うなら、汎用的に使う部分はどこか?
  • クラスのインプットデータとアプトップトデータは何か?(既定処理の場合はこれがない場合もある。)
  • クラス内でシングルトンクラス(スタティイク領域)にアクセスして良いかどうか?

疎結合で再利用性が多いクラスが増えれば増えるほど生産性が高まるので、これ以外にも色々な工夫を凝らすといいと思います。

165
156
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
165
156