オープンソースこねこね

Webプログラミングなどについてあれこれ。

Codeから遷移したUIViewControllerにstoryboard上でナビゲーションバーを表示する

storyboad上のSegueで画面遷移をつないでいった場合、UINavigationControllerに含まれるViewControllerは自動でナビゲーションバーが表示されて、そこにタイトルとかバーボタンをとかを配置することができるのだけど、コードから遷移させた場合storyboad上ではどことも繋がっていないViewControllerとして表現されてしまって、実際はUINavigationControllerの中にいてナビゲーションバーとかがあるのにそれが表示されていない、ということが起こる。

そんなViewControllerの場合以下のようにしてやればstoryboard上にナビゲーションバーを表示でき、IB上でタイトルやボタンを配置することができます。

  • storyboard上でViewControllerを選択する。
  • Attributes inspectorでSimulated Metrics設定のTop BarTranslucent Navigation Barに設定する
  • storyboard上にナビゲーションバーの領域があらわれる
  • ナビゲーションバーの領域にNavigation Itemドラッグアンドドロップして配置する

以上。

UITextFieldやUITextViewでキーボードの外をタップしたらキーボードを閉じる

すでにいろいろやり方がネット上に書かれているが、自分の中では以下の方法に落ち着いたのでメモっておく。

まず、以下の記事で紹介されているようなFirstResponderを取得するメソッドをUIViewにカテゴリとして事前に実装しておく。

FirstResponderを探せ - Kazzzの日記

#import "UIView+UIUtil.h"
@implementation UIView (UIUtil)
- (UIView *)findFirstResponder
{
    if ([self isFirstResponder]) 
    {
        return self;
    }
    for (UIView *subView in [self subviews]) 
    {
        if ([subView isFirstResponder])
        {
            return subView;
        }
        if ([subView findFirstResponder])
        {
            return [subView findFirstResponder];
        }
    }
    return nil;
}
@end

あとはViewController上で以下のように実装する。

# XXHogeViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    // タップを検知するためのGestureRecognizer。デリゲートで処理するように設定
    UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] init];
    gestureRecognizer.delegate = self;
    gestureRecognizer.cancelsTouchesInView = NO;
    [self.view addGestureRecognizer:gestureRecognizer];
}

// タップ時のデリゲートメソッド
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    UIView *touchedView = touch.view;
    UIView *firstResponderView = [self.view findFirstResponder];
    
    if (touchedView != firstResponderView) {
        // 現在編集中のViewと違う場所がタップされたときのみキーボードを閉じる
        [self.view endEditing:YES];
    }
    return YES;
}

PHPでcronのように定期実行をするプログラムを書いた

kohkimakimoto/workerphp · GitHub

自分で使う分には最低限、必要な機能を実装できたので紹介します。以下のようなPHPファイルを記述してPHPコマンドラインから実行するとプロセスが起動しっぱなしになりcront_timeで指定したスケジュールでジョブを定期実行してくれます。

<?php
require_once __DIR__.'/vendor/autoload.php';

$worker = new \Kohkimakimoto\Worker\Worker();

// job for every minute.
$worker->job("hello", ['cron_time' => '* * * * *', 'command' => function(){
    echo "Hello world\n";
}]);

// job runs a shell command.
$worker->job("uptime", ['cron_time' => '10 * * * *', 'command' => "uptime"]);

$worker->start();

ジョブはクロージャPHPのコードとして書くか、シェルのコマンドを文字列で直接指定することができます。インストールはcomposer installするだけですが、その他詳しい情報はリポジトリのREADMEを参照してください。内部的にジョブはプロセスをforkして実行させているので、PHPpcntlエクステンションがインストールされている必要があります。linuxならyumとかでインストールできると思います。

cronが普通に使える環境ならまあそれを使えばいいんですが、個人の用途でherokuの無料枠dynoを使って定期実行処理をしたかったので、こんなの作りました。

Web API

cronにない拡張機能として、簡易なWeb APIを提供するHTTPサーバを組み込んでいます。

$worker->httpServer->listen();

のように書くと、8080ポートを開きます。ポートを変更したいときは

$worker->httpServer->listen(8888, 'localhost');

のようにもかけます。HTTPサーバが立ち上がったら、

$ curl -XGET http://localhost:8080/{ジョブ名}

でジョブの情報を取得

$ curl -XPOST http://localhost:8080/{ジョブ名}

でジョブを実行することができます。また$worker->httpServer->setAPIKey()を使えば、文字列一致だけで判定する簡易な認証機能をつけられます。 ちなみに、私は以下の様な設定でherokuのPHPスタック上で動かしています。

<?php
require_once __DIR__.'/vendor/autoload.php';
date_default_timezone_set('Asia/Tokyo');

use Kohkimakimoto\Worker\Worker;

$worker = new Worker();
// herokuはhttpのportを動的に割り当てるので環境変数から取得する
$worker->httpServer->listen(getenv('PORT'));
// apiにアクセスするときのキー
$worker->httpServer->setAPIKey('vjdioaewdg49q3tg...');
// 5分毎にメモリ使用量などをログに出力する設定
$worker->stats->on();

$worker->job("hoge", ['cron_time' => '* * * * *', 'max_processes' => 1, 'command' => function(){
    // ...
}]);

$worker->job("foo", ['cron_time' => '* * * * *', 'max_processes' => 1, 'command' => function(){
    // ...
}]);

// 30分に一回dynoをwebからアクセスしてスリープさせないようにする。
$worker->job("wakeup_dyno", ['cron_time' => '*/30 * * * *', 'max_processes' => 1, 'command' => function(){
    file_get_contents("https://my_worker_xxx.herokuapp.com/?apiKey=vjdioaewdg49q3tg...");
}]);

$worker->start();

とりあえず、1日動かしっぱなしにしてみたが問題はなさそうです。

UIPageViewControllerをUINavigationControllerにいれたらナビゲーションバー部分に潜り込まなくなった件

iOS8でUIPageViewControllerトランジションスタイルをUIPageViewControllerTransitionStyleScrollにしたときのみ発生する。。。マジなんなのこれ。。。しかも一度でも画面をタップすると、潜り込む(正常な)位置に移動するという。挙動が奇妙すぎてStack Overflowでも見つからないし。iOS8のバグを疑ってしまう。ヾ(`Д´)ノ"

f:id:kohkimakimoto:20141203064908g:plain

このサンプルはXcodePage-Based Applicationが作った雛形のプロジェクトを元に、RootViewControllerをNavigationControllerにいれる、のとUIPageViewControllerの初期化処理を以下のようにUIPageViewControllerTransitionStyleScrollを指定するように変えたもの。

self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];

表示されるViewのframeなどを調べてみてもx=0,y=0となっていて変なところはない。それでViewの階層を見てみたらこんな感じになっている。 なんか下にズレてる。。。

f:id:kohkimakimoto:20141203065549p:plain

_UIQueuingScrollViewというViewの位置がy=-64.0という謎な状態になっていて、その子となるViewがその分だけ下にずれている。 しかもこれviewWillAppear:などのフェーズでは確認できず、viewDidAppear:まで到達して初めてこの状態になる。

というわけで以下のようなワークアラウンドを入れた。

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)) {
        for (UIView *v in self.pageViewController.view.subviews){
            if ([v isKindOfClass:[UIScrollView class]]) {
                UIScrollView *scv = (UIScrollView *)v;
                CGPoint offset = scv.contentOffset;
                if (offset.y < 0) {
                    offset.y = 0;
                    scv.contentOffset = offset;
                }
            }
        }
    }
}

iOS8の場合、UIPageViewControllerUIScrollViewをとってきてマイナス値なら0に書き換えるということをやっています。これでなんとか対応できた。

追記

コメント欄でアドバイスをいただいて「UIPageViewControllerのAdjust Scroll View Insetsのチェックを外す」で対応できることがわかりました。

Goの開発環境

最近Go言語をいじっているので、現時点での開発環境についてまとめておく。ツールを先に列挙しておくと以下のものを使っている。

エディタ

Sublime Text3を使っている。普段PHPを書くのに使っていて慣れているからであって最適だとは思ってはいない(静的型言語はIDEを使う方がいいとは思っているのだが、ショートカットキーを新たに覚えたりするのがつらいので使い続けている)。

これにGo用のプラグインGo​Sublimeをインストールしている。

また関数定義などにジャンプするためにctagsも使っている。ctags自体はhomebrewでインストールして、SublimeにはCtagsプラグインを入れている。標準だとctagsはGoに対応していないのだが

go - ctag database for golang - Stack Overflow

にあるように、~/.ctags

--langdef=Go
--langmap=Go:.go
--regex-Go=/func([ \t]+\([^)]+\))?[ \t]+([a-zA-Z0-9_]+)/\2/d,func/
--regex-Go=/var[ \t]+([a-zA-Z_][a-zA-Z0-9_]+)/\1/d,var/
--regex-Go=/type[ \t]+([a-zA-Z_][a-zA-Z0-9_]+)/\1/d,type/

と書き込んでおけば、関数定義元に(ある程度)ジャンプできるようなる。完璧にとはいかないが、今のところはこれでいいかと。

GOPATHとディレクトリ構造

ghqを使ったローカルリポジトリの統一的・効率的な管理について - delirious thoughts

基本は上記の記事の通り。GOPATH=$HOMEに設定してghqでGo以外のリポジトリも含めて~/src配下で一括管理する。

ただ、これだとctagsでタグを生成する際に全てのリポジトリを対象にしなくてはならず、タグ生成処理がものすごく重くなってしまい使いものにならなかった。やはり依存パッケージは個々のリポジトリ配下にほしい。

いろいろ試行錯誤した後、次のようにdirenvを使う方法に落ちついた。direnvはカレントディレクトリごとに環境変数を自動で切り替えてくれるツールで、以下の記事が参考になる。

direnvを使おう - Qiita

まず

brew install direnv

direnvをインストールし、zshの場合は~/.zshrcに以下を記述しておく。

export EDITOR=vim
eval "$(direnv hook zsh)"

さて、ホームのsrcディレクトリは以下のようになっている。

src
├── github.com
│   ├── laravel
│   │   ├── framework
│   │   └── laravel
│   ├── kohkimakimoto
│   │   └── altax
│   ├── php
│   │   └── php-src
│   └── piranha
│       └── gostatic

ここでGo言語の場合はリポジトリのルートに.envrcを作成して

export GOPATH=$(pwd)/_vendor/:$GOPATH

と書いておく。これでGo言語のリポジトリに移動した時のみGOPATHがそのリポジトリ配下の_vendorディレクトリにも設定される。go getで外部パッケージをインストールするとき_vendoer配下にインストールされるようになるし、go build時もここのパッケージを参照するようになる。ctagsのタグ生成もリポジトリルートで行えば依存のあるパッケージをだけを対象にタグを生成できる。

参考にしたサイトは以下。

ghqによるGoのプロジェクト管理 & プロジェクト毎で外部モジュール管理 - Qiita

これからGoを始める人のためのTips集 #golang - The Wacul Blog

Gomで依存パッケージを一括で_vendorにダウンロード

_vendorに依存パッケージを配置できるようになったのはいいのだが、依存物をいちいち手作業でgo getするのは非効率なので一括でダウンロードできるようにGo言語版のBundlerなやつであるGomを使っている。

mattn/gom · GitHub

以下のコマンドを叩くとgo buildを実行してインストールされていないパッケージをエラーメッセージから抽出してGomfileに吐き出す。

go build 2>&1 | awk '/cannot find package/{print "gom " $5}' >> Gomfile

これでGomfileができたら

gom install

として全ての依存パッケージを_vendor配下にダウンロードする。これでGoのプロジェクトがビルドできる\(^o^)/