rbenv の ruby バージョンを更新する

2018-12-25 Ruby 2.6.0 がリリース。

早速インストールしようとしたけど、rbenv にバージョン 2.6.0 がない。
どうすればいいの、、、ってやったことメモ。

参考

以下サイトを参考にしました。
この記事は、ほぼ参考サイトの転載です。

やったこと

% cd ~/.rbenv/plugins/ruby-build
% git pull

ruby-build の README

# Via Homebrew
$ brew update && brew upgrade ruby-build

# As an rbenv plugin
$ cd "$(rbenv root)"/plugins/ruby-build && git pull

(引用) GitHub - rbenv/ruby-build: Compile and install Ruby

brew upgrade も試したけど更新されなかった。
その場合はgit pullしようってことかな。

View Controller でキーイベント処理をする

環境

前提

  • テキストボックスなど、キー入力を処理する View がないこと

サンプルコード

override func keyDown(with event: NSEvent) {
    let s = event.characters ?? "(nil)"
    print(#function + ": " + s)
}

override func viewDidAppear() {
    super.viewDidAppear()
    
    if let window = self.view.window {
        window.makeFirstResponder(self)
    }
}

説明

キー入力イベントをレスポンダチェーンで拾うには NSResponder#keyDown などのキーイベントメソッドをオーバーライドすればよい

NSViewController#keyDown をオーバーライドしてもキー入力に反応しないというブログや質問/回答を散見したが、原因はおそらく、First Responder になっていないからだと思われる (環境によるかもしれないので断定はできない)

なので、サンプルコードでは View が表示されたときに (viewDidAppear で) 明示的に NSWindow#makeFirstResponder を呼び出して、View Controller 自身を First Responder に指定している (ちなみに、makeFirstResponder を処理するのに acceptsFirstResponder -> true である必要はない)

First Responder がキーイベントを処理する Responder に移ったとき (例えば、テキストボックスが Window 内にあり、そこにフォーカスされたときなど) には View Controller のキーイベントメソッドは処理されなくなるので注意

ラベルやボタンなど、キー入力を処理しない View が First Responder であれば、レスポンダチェーンをたどり View Controller のイベントメソッドが処理されるが、テキストボックスなどは自身のイベントメソッドを処理してレスポンダチェーンを終了してしまうためだ (たぶん)

Window 内のどの View がキーイベントを処理中でも、キーイベントを検知したいというのであれば、それはレスポンダチェーンではなく (NSResponder のメソッドをオーバーライドするのではなく)、NSEvent#addLocalMonitorForEvents などを利用してイベントハンドラを追加すべきかと思う

参考

調査メモ

  • NSWindow#makeFirstResponder を呼び出さないとき、画面表示時の First Responder -> Window だった
  • acceptsFirstResponder は、タブキーなどでコントロールのフォーカスを移す際に、フォーカスが移せるかどうかの判定に使用されるものっぽい (調査不足)
  • マウスクリックイベントのレスポンダチェーンは NSWindow#firstResponder から始まるのではなく、クリックイベントの発生した座標に存在する View から始まるのではないかと思うが、詳細不明 (調査不足)

macOS アプリで画面遷移 (View Controller の切り替え)

macOS アプリ (Cocoa App) で画面遷移させるにはどうすればいいのか調べたことまとめ

環境

やりたいこと

  • Window 内のすべての UI をまるっと入れ替えたい
  • 前提として、ここでは Storyboard を利用するものとする

概要

今回目標とする画面遷移処理は、最終的に NSView の入れ替えを行えばよくて
最低限に必要な処理は以下2点

  • 表示されている View の superview に、次に表示したい View を追加
  • 表示されている View を superview から切り離す

アクションメソッド内に上記処理があれば画面遷移できる
必要に応じてアニメーションをはさめばいい

ちなみに、NSWindow.contentView を入れ替えるのではなく contentView の subview の入れ替えを行う

contentView を入れ替えてもいいと思うけど、次の理由から用途は限られるのではなかろうか (実機で試してないけど)

  • 遷移アニメーションを組み込みにくい?
  • 局所的な View の切り替えに流用しにくい?

あと、ほとんどの場合 frame の引き継ぎをすることになると思うので忘れずに

実装方法

諸々考えると Container View を利用して遷移元 View と親子関係を構築し、遷移処理は Segue を利用するのがよさそう

Container View?

実態はただの NSView で、Storyboard が embed された View Controller に対して View Controller と View の親子関係を構築するっぽい

Segue?

Storyboard で View Controller 間の遷移、表示方法などを指定するやつ
用意されているものは別ウィンドウやダイアログ用途のものしかなく View を入れ替えるものはなかった

  • Show: 別ウィンドウで表示
  • Modal: 別ウィンドウ(モーダル)で表示
  • Sheet: 上からスライドインして表示
  • Popover: 吹き出しで表示

サンプル

参考サイトのソースを Swift で書き直してみた

class SlideSegue: NSStoryboardSegue {
  override func perform() {
    // NSViewControllerの親子関係を設定
    guard
      let s = self.sourceController as? NSViewController,
      let d = self.destinationController as? NSViewController,
      let p = s.parent
    else {
      print("downcasting or unwrapping error")
      return
    }

    if (!p.children.contains(d)) {
      p.addChild(d)
    }

    // 遷移アニメーション
    NSAnimationContext.runAnimationGroup(
      { context in
        context.duration = 0.5

        var frame = s.view.frame
        frame.origin.x = frame.size.width;
        d.view.frame = frame;
        s.view.superview?.addSubview(d.view)

        let newDFrame = s.view.frame

        var newSFrame = s.view.frame
        newSFrame.origin.x = -newSFrame.size.width

        s.view.animator().frame = newSFrame
        d.view.animator().frame = newDFrame

        d.view.autoresizingMask = s.view.autoresizingMask // Storyboard で適切に設定されているなら不要かも

      }, completionHandler: {
        s.removeFromParent() // 戻る可能性があるなら不要かも
        s.view.removeFromSuperview()
      }
    )
  }
}

アニメーションを抜いてみた
(autoresizingMask の引き継ぎ処理と removeFromParent は割愛)

  override func perform() {
    // (省略)

    if (!p.children.contains(d)) {
      p.addChild(d)
    }

    // (ここから下を修正)

    d.view.frame = s.view.frame;
    s.view.superview?.addSubview(d.view)
    s.view.removeFromSuperview()
  }

NSViewController#transition で書き直してみた

  override func perform() {
    // (省略)
  
    if (!p.children.contains(d)) {
      p.addChild(d)
    }

    // (ここから下を修正)

    // 試してみたところ
    // frame の引き継ぎも transition でしてくれるっぽい
    // d.view.frame = s.view.frame;
    p.transition(from: s, to: d)
  }

superview.addSubview や removeFromSuperview は transition でやってくれる

This method adds the view in the toViewController view controller to the superview of the view in the fromViewController view controller. Likewise, this method removes the fromViewController view from the parent view controller’s view hierarchy at the appropriate time. It is important to allow this method to add and remove these views.

(引用) Apple Developer Documentation

基本的なアニメーションは options で指定できる

p.transition(from: s, to: d, options: .slideForward)

NSViewController#removeFromParent などの完了処理が必要な場合は completionHandler で処理を追加すればいい

p.transition(from: s, to: d, completionHandler: { s.removeFromParent() } )

NSViewController#transition で次のエラーが発生する場合は

[General] fromView.superview must be layer-backed for animations to take effect (setWantsLayer:YES on it or a parent of it)

wantsLayer = true をコード追加するか

s.view.superview?.wantsLayer = true // (追加)
p.transition(from: s, to: d, options: .slideForward)

または、Storyboard で Core Animation Layer を指定する

f:id:bitsandbobs:20181213141442p:plain

まとめ

  • 基本的な画面遷移 (View Controller の切り替え) は NSViewController#transition が便利
  • Storyboard を使用している場合は Custom Segue を利用するとよさげ

参考

Creating a ViewController & xib, and Loading them from Storyboard

ViewController と xib ファイルを作って Storyboard から呼び出す手順をまとめておく

!注意!これは Cocoa App (macOS アプリ) に関する記事です

環境

手順

ViewController と xib ファイルを作成する

f:id:bitsandbobs:20181210012507p:plain New File から

f:id:bitsandbobs:20181210012735p:plain macOS / Cocoa Class を選択して Next

f:id:bitsandbobs:20181210012803p:plain Subclass of は NSViewController を選択
Also create XIB file for .. のチェックオン (手間が省けるので)
クラス名を入力して Next → Create

f:id:bitsandbobs:20181210012837p:plain ViewController のソースファイルと、同名の xib ファイルが作成される

xib の編集

f:id:bitsandbobs:20181210012907p:plain File's owner の Custom Class 名に先ほど作ったクラスが設定されていることを確認しておく
その後、好きなように View にアレコレ追加していく(割愛)

Storyboard の編集

f:id:bitsandbobs:20181210013134p:plain View Controller の Custom Class 名に作ったクラスを指定する

f:id:bitsandbobs:20181210013152p:plain デフォルトで作られている View を Storyboard から削除しておく
(Storyboard に View が残っていると xib ファイルではなく、この View を優先して表示するため)

あとがき

当初 xib ファイルが読み込まれずどハマりしました。 Storyboard で ViewController を追加したときに初期作成される View を削除してなかったからですが、単純なことほど気づきにくいものですね。 この問題の原因を調べる過程でわかったことを以下にまとめておきます。

  • ViewController が View を初期化するとき(loadView がコールされるとき) nibName プロパティが nil の場合はクラス名と同名の xib ファイルをロードする(loadView() - NSViewController | Apple Developer Documentation)

    Prior to OS X v10.10, the loadView() method did not provide well-defined behavior if the nibName property’s value was nil. In macOS 10.10 and later, however, you get correct behavior without specifying a nib name as long as the nib file’s name is the same as that of the view controller.

  • ViewController が Storyboard からインスタンス化されるときは init(nibName: bundle:) ではなく init(coder:) コンストラクタがコールされる
  • Storyboard にて ViewController に View が設定されている場合は、nibName = ViewController の Object ID1 + "-view-" + View の Object ID (例: "XfG-lQ-9wD-view-Qcj-BY-oc5")となる
  • Storyboard にて ViewController に View が設定されていない場合は nibName = nil となる

ちなみに調査の過程で以下の方法を試しました。 困ったことに、これらは全て期待する動きをしたのですよね。

  • NSViewController の修正
    • viewDidLoad で NSNib#instantiate
    • loadView をオーバーライドして NSNib#instantiate
    • init(coder:) をオーバーライドして super.init(nibName: bundle:)
    • nibName プロパティをオーバーライドして nil またはクラス名を返す
  • Storyboard の修正
    • User Defined Runtime Attributes に nibName:String を追加する
    • Storyboard の ViewController から View を消す(!!!)

Runtime Attributes に nibName を追加する方法は ViewController クラスは作らないけど View は xib で外だしにしたいときとかに使えそうです。

nibName や init(coder:) をオーバーライドする方法は xib のファイル名がクラス名と異なるとき(例えば、動的にViewを切り替えるときなど)に使えるかもしれません。

参考


  1. Object ID は Identity inspector の Document に表示されている

Rails New Project

bundler + rails の環境構築メモ

前提

  • rubyおよびbundlerはインストール済みであること
  • gemのインストール先はvendor/bundleとする

手順

ディレクトリを作り移動してbundle initでGemfileを作る

% mkdir <new_project_dir>
% cd <new_project_dir>
% bundle init

Gemfileを編集してgem "rails"を加える
(コメントアウトされているので行頭の#を取り除く)

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "rails"

Railsをインストール

% bundle install --path=vendor/bundle

現在のディレクトリをRailsのプロジェクトディレクトリとして初期化
(Gemfileがコンフリクトするので--forceで上書き)

% bundle exec rails new . --skip-bundle --force

(必要に応じてGemfileを編集する)

gemをインストールする

% bundle install --path=vendor/bundle --without=production

.gitignoreにvender/bundleを追加する

# Ignore bundle-path
/vendor/bundle

gitに最初のコミットを行う

% git add .
% git commit -m 'initial commit'

Rails JSON API で CSRF トークン検証をスキップする

コントローラに以下を追加

skip_before_action :verify_authenticity_token, if: :json_request?

def json_request?
  request.format.json?
end

(参考) RailsでAPIを書いたときの雑な感想 | Yucchiy's Note

CSS リストを横並びにする

html 例

  <ul class="menu">
    <li>index</li>
    <li>images</li>
    <li>movies</li>
    <li>maps</li>
  </ul>

css

ul.menu {
  list-style: none;
}

ul.menu li {
  display: inline-block;
}

以前は float でブロック要素を並べていたけど、いまは inline-block
古いブラウザではうまくいかないかもしれないので注意

li 要素と li 要素の間に改行コードがあるので隙間ができる
この隙間を解消する方法もいろいろあるらしい