だいたいそれでいいんじゃないの

つれつれなるkixixixixiの技術的なストック。http://reload.co.jp

こんなコードはいやだ【Objective-C】

最近うけた案件があるアプリの改修でした。
そちらのコードが御法度の詰め合わせのフルコンボみたいな記事になっていたので、
そのバッドノウハウの共有になればと思い記事にしました。 基本的には、「こうしよう!」というものでなく、「これはやめよう!」という事例を集めています。

アプリの概要
- iOS用のライフスタイルアプリ
- コードはObjectiveC

データをハードコーディングするのはやめよう

アプリケーションではマスターデータのようなデータを共通で使う場面があると思います。 例えばそのデータが100件を超えていたら、実行ファイルにハードコーディングなんてして欲しくないですよね。

[NSArray arrayWithObjects:@"10",@"72",@"109",@"11",@"90",@"8",@"9",@"13",@"14",@"15",@"6",@"120",@"50",@"7",@"74",@"75",@"76",@"77",@"95",@"79",@"81",@"78",@"36",@"83",@"80",@"16",@"5",@"107",@"88",@"84",@"85",@"98",@"87",@"18",@"106",@"17",@"86",@"119",@"82",@"108",@"100",@"3",@"118",@"124",@"39",@"2",@"1",@"96",@"99",@"41",@"42",@"4",@"40",@"73",@"19",@"110",@"20",@"21",@"22",@"23",@"37",@"91",@"92",@"93",@"94",@"112",@"113",@"114",@"115",@"116",@"35",@"38",@"117",@"89",@"52",@"54",@"55",@"33",@"51",@"122",@"97",@"105",@"24",@"25",@"26",@"28",@"104",@"43",@"44",@"71",@"27",@"47",@"49",@"48",@"121",@"53",@"123",@"101",@"102",@"103",@"29",@"45",@"46",@"30",@"31",@"32",@"2000",@"3000", @"56",@"57",@"58",@"59",@"60",@"61",@"62",@"63",@"64",@"65",@"66",@"67",@"68",@"69",@"70",nil];

そもそもなんのデータかわからなくなるような配列はよくない。

変数名はデータの意味を伝えよう

wavioarray = [NSArray arrayWithObjects:@"2",@"2", ...

略称なのかもわかんないし、そもそも単語の区切りはしっかりしよう。

NSArray *allarray;

意味広すぎるのにグローバル変数にするのはなしでしょ...

NSMutableArray *e1quantityarray;

変数名に数字を使わないでほしい。

NSInteger selectnumeronumber;

何語なのか...

キャメルケースにしよう

クラス名やメソッド名などはちゃんと単語の区切りはわかるようにしよう。
ObjectiveCやJavaなどはキャメルケースだし、rubyperlメソッドならスネークで、クラスはキャメル。 ObjectiveCでかいてるんだから、クラス名は先頭大文字で、メンバは先頭小文字。

class inputcaloryviewController

やめて。

コードのコピペはやめよう

自分の書いたコードだとしても他のファイルなどにコピーペーストするのはやめよう。
実行ファイルに書いたマスターデータを複数のファイルに転載するのはおかしい...
コピペミスしている部分があるし、まさにコピペの弊害を自分でひきこおこさないでほしい...
ロジックでもハードコードでも、3箇所以上かいてるならどこかにまとめよう。
抽象化まではしないでも共通化しよう。 10回もコピペするのは本当に良くない。

変数に意味をもたせよう

インスタンスメンバとして作ったものに違うデータをいれるのはやめよう。
touchonnumeber とかいう変数に対象のもののデータを入れてたら、わかんなくなる。

文字列は文字列。数字は数字として扱おう

数字のデータをStringにして代入し、取り出すたびに毎回キャストするのはやめよう。

データは意味でまとめよう

例えば食べ物の名前と、そのカロリーというデータがあったとしてそれを2つの配列で扱うのではなく、

wacaloryarray = [NSArrat arraywithObjects:@"10", @"20", ...
wanamearray = [NSArrat arraywithObjects:@"ごはん", @"味噌汁", ...

食べ物自体でまとめよう。

foods = @[
  @[@"id": @1, @"name": @"ご飯", @"calory": @10],
  @[@"id": @2, @"name": @"味噌汁", @"calory": @20],
  ...

数が合わなくなるとかそういうミスもないし、意味でまとまっているから確認しやすいはず。
オブジェクトにした方がもっといい。

Warningは極力へらそう

4000個もwarningがあったらデバッグができたものではない。

バグを残したままにしないでおこう

うん。お願いします。

ObjectiveCならではのやつ

下記のようなものもあったけど、今度機会あれば。

  • リテラルにしよう
  • タグで状態を管理しない
  • AppDelegateは値の受け渡しに使うのはおかしい
  • Modelをつくろう

Xcode7(6?)とかでビルドするとiPhone6(+)対応していないアプリがScreenがiPhone4sサイズになってしまう問題

あまり想定されない事象ですが、はまったので。

結構むかしに作成されたアプリの保守をした際に、表題のように4sサイズで表示されしまう問題に直面した。
具体的に言うとiPhone6などで表示すると、解像度が 640 * 960(320 * 480)で表示されて余白となる上下は黒く塗りつぶされている。
そのアプリはAutoLayoutなどに対応してもいなかったので、UIImageViewなどを全て修正するのはきびしい...
そもそもどうしてiPhone4sのサイズになってしまうのか...
LaunchImageが設定されていなかったので、設定すると設定した解像度はビルドするとそのサイズで表示できる!
このことをいかして、iPhone5のサイズだけLaunchImageを設定すると iPhone6などでもアスペクトが同じなので 640 * 1136で表示できた。 最適化されていない表示ですが、工数は削減できそう。

以上。

AU Wi-Fi マルチデバイスにつながらなくなった!

2016/2/1より、いつも利用していたAU Wi-Fiに接続できなくなった。

www.au.kddi.com

こちらの変更のため、Wi2のネットワークへのログイン方式がかわった模様。
ブラウザだけでログインということは、いつもWi2 IDを入力してログインするということか。
便利じゃない気がするけども、LTEフラットプランだったので無料のまま利用できてよかった。

登録は簡単でPCがWebにつながる環境で手元にAU携帯があればすぐ登録できる。
IDの末尾に @aumd がつくのがなんだかゆるせない...

でもつながってよかった!

Alamofireでsession(cookie)を永続化する

Bridge-Headerを使えば簡単にObjective-Cのコードを使えるのでずっとAFNetworkingをつかってました。
だけど、今更だがSwiftだけにしてみようと思ってAlamofireを使ってみた。
(単にGAがいらない案件がきたってわけではないよ...w)

HTTPリクエストを投げる際はいつもHelper作っているので今回もその方針でAlamofireを叩きにいくHelperメソッドをつくりました。

      func callAPI(
        path path:String,
        method:Alamofire.Method,
        params:[String: AnyObject]?,
        notificationName:Notice,
        success:SuccessCallbackBlock,
        failure:FailureCallbackBlock
        )
    {
        
        Alamofire.request(
            method,
            APIHost + path,
            parameters: params
            )
            .responseJSON { request, response, result in
                
                if response?.statusCode >= 400
                {
                    failure(error: result.error)
                    self.sendNotification(notificationName, success: false, responseObject: nil)
                    
                }
                else
                {
                    self.sendNotification(notificationName, success: true, responseObject: success(response: result.value))
                }
        }
    }

このHelperを作っておいて

        callAPI(
            path: "/login.json",
            method: .GET,
            params: nil,
            notificationName: .Login,
            success: { (response) -> (AnyObject?) in
                
                if let my = response as? NSDictionary
                {
                    // do something
                }
                
                return nil
            },
            failure: { (error) -> () in
                // do something
            }
        )

こういった感じでAPIをたたく。
NSNocificationを使って通信結果のハンドリングしているのでsendnotificationメソッドやNoticeのenumは別で作ってあります。
APIHostはいわずもがな。 ブロックの型は以下のようにつくった。

public typealias SuccessCallbackBlock = (response:AnyObject?) -> (AnyObject?)
public typealias FailureCallbackBlock = (error: ErrorType?) -> ()

これでとりあえずできて、AFNetworkingよりswiftっぽくて使いやすいなと思ったけどアプリを再起動するとsessionが切れてしまうのがいただけない。cookieをどこにも保存してないからなー。
ということでHelperをいじってCookieを保存する機構を作ってSessionを永続化してみた。

    var manager : Alamofire.Manager?
    func configureManager() -> Alamofire.Manager?
    {
        if manager == nil
        {
            if let cookiesData:NSData = NSUserDefaults.standardUserDefaults().objectForKey(CookieKey) as? NSData
            {
                for cookie:NSHTTPCookie in NSKeyedUnarchiver.unarchiveObjectWithData(cookiesData) as! [NSHTTPCookie]
                {
                    NSHTTPCookieStorage.sharedHTTPCookieStorage().setCookie(cookie)
                }
            }
            
            let cfg = NSURLSessionConfiguration.defaultSessionConfiguration()
            cfg.HTTPCookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage()
            manager = Alamofire.Manager(configuration: cfg)
        }
        return manager
    }

        
    func callBasicAPI(
        path path:String,
        method:Alamofire.Method,
        params:[String: AnyObject]?,
        notificationName:Notice,
        success:SuccessCallbackBlock,
        failure:FailureCallbackBlock
        )
    {
        
        configureManager()?.request(
            method,
            APIHost + path,
            parameters: params
            )
            .responseJSON { request, response, result in
                
                let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(response?.allHeaderFields as! [String:String], forURL: response!.URL!)
                for var i = 0; i < cookies.count; i++ 
                {
                    NSHTTPCookieStorage.sharedHTTPCookieStorage().setCookie(cookies[i])
                }
                
                let cookiesData:NSData = NSKeyedArchiver.archivedDataWithRootObject(NSHTTPCookieStorage.sharedHTTPCookieStorage().cookies!)
                NSUserDefaults.standardUserDefaults().setObject(cookiesData, forKey: CookieKey)
                
                if response?.statusCode >= 400
                {
                    failure(error: result.error)
                    self.sendNotification(notificationName, success: false, responseObject: nil)
                    
                }
                else
                {
                    self.sendNotification(notificationName, success: true, responseObject: success(response: result.value))
                }
        }
    }

Helperでシングルトンっぽいmanagerをつくり、そこで NSUserDefaults に保存しておいたCookieを引っ張ってきて当てる。
通信の成功時に都度、Hederに付与されたCookieNSUserDefaults に保存。
これでsessionは永続化できました。
Helper作っておくと、モデル等からAPI叩きにいくときは意識しないでいいので楽でいいです。

以上。

YAPC::Asia 2015にいってきました

タイトル通り。会場のビッグサイトの下のカフェで書いてる。

10回目で最後のYAPC。自分は最後の3回しか参加してないけど、Perlだけでなく様々な言語・様々なレイヤーの話がわいわいされててエンジニアのお祭りって感じですごくたのしかった。
トークいくつか聞いたけど、おそらくスピーカーのひとのが資料あげてるのでそちらを参照するとおもしろいよ!
HTTP2の話はすごく面白かったので一度、かるく導入してみたいと思った!

すごい大規模だしとてもおもしろいカンファレンスなので終わってしまうのはかなしいけども、これも区切りなのかなと無理やり納得してる。また違うかたちでこんな雰囲気のカンファレンスが開催されると嬉しいです。

カンファレンスとか勉強会に言った後はすごくモチベがあがるのでそのままコーディングするのがいいなーと思っていたので速攻でカフェでコーディングをしようと思っていますw
モチベーションは5分しか持たないとかもいうので、はやく始めないとですね。

以上!

UIAlertView/UIAlertControllerは極力使わないほうがいいと思うはなし

タイトルがほんのすこし釣りくさいですが、タイトル通り。

個人的な意見ですが、iOSのUIとしてUIAlertView/UIAlertController(以下Alert)を極力つかわないと思っています。
なぜかというと、
- ユーザの動きを強制的に止めてしまう
- 基本的には警告表示するためのものなのでユーザには圧迫感がある

いろんな意見がありそうですが、自分があまり多用するのはよくないと思ってできるだけ違う手段を使うようにしてます。
これやだなーと思ってしまうのが、 保存しました。 完了しました。 などの表示にAlertを使っている例。
使い勝手がよく開発者としてはつい使ってしまいますが、完了などの通知のためにユーザの動きを止めてわざわざタップを増やすUIは不健全な気がして。
公式でGrowlみたいなUIを用意してくれればいいんですが、ないのでわざわざ実装するとなると億劫になってしまいますよね。

github.com

こんなので軽く表示させるくらいが完了通知とかだといいと思います。
もちろん、本当に一回止まってもらって警告を出すときはAlertをだすほうがいいと思います。
諸説ありそうなので、ただの提議だということで。

以上。

iOS強制アップデートの機構について

強制アップデートを実装しておいた方が、今後機能拡張した際に助かるはず!
ということで実装したのですが、わざわざそれのためにエンドポイントを作るのは微妙だし毎回確認してたら無駄なオーバーヘッドが増えるなと思います。
なので、考えた手段は、
- HTTPヘッダーにバージョン番号を記したヘッダーを追加
- UserAgentにバージョン番号を付加
どっちもクライアントでする作業はとってもらくで2〜3行くらいでよさそう。
AFSessionManegerでheaderを追加するならこれだけ。

let manager :AFHTTPSessionManager = AFHTTPSessionManager()
let serializer:AFJSONRequestSerializer = AFJSONRequestSerializer()
if let ver: AnyObject = NSBundle.mainBundle().objectForInfoDictionaryKey("CFBundleVersion")
{
    manager.requestSerializer.setValue("\(ver)", forHTTPHeaderField: "App-Version")
}

サーバー側で毎回わかりやすいstatus codeを返してやればいけそう。
railsだと before_action

def check_version!
    version = request.headers['App-Version']
    if version && version.to_i < 2
        render json: { error: 'Need Update' }, status: 303
    end
end

303でいいのかわからないけど、とりあえずつかわなそうな See Other をつかってみる。503とかのがいいのかな。
それでクライアント側ではFailureのcallbackでひろってやれば、あとはUIAlertなどを飛ばせば、大丈夫なはず。

if let response = operation.response as? NSHTTPURLResponse
{
    if response.statusCode == 303
    {
        // do something
        return
    }
}

以上。