2016年9月11日日曜日

iOSのフォントのお話

最近フォント周りについて深く掘り下げる機会がありましたので、その際のメモを残しておこうと思います。かなり読む人置いてけぼりな中身になってますが、フォントを詳しく触り始めるとなるほどーとためになる(と思う)のでどうかご了承ください(´・_・`)

UIFontのプロパティについて

UIFontにはフォントに関する数値を表すプロパティが存在します。いろいろありますが、もっとも重要なのは以下に列挙するプロパティです。

  • pointSize
  • lineHeight
  • ascender
  • descender
  • leading
  • capHeight

以下の画像を見ると非常にわかりやすいかと思います。


参照元: https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html および http://stackoverflow.com/a/35922853

これを踏まえて各プロパティを解説すると以下のとおりになります。
  • pointSize
    • フォントのサイズを表します。
    • ほとんどすべてのケースでpointSize = ascender + descenderとなり、すなわちpointSizeは上端と下端に空白を含まない純粋なフォントの高さだけを表す数値であると言っても良いと思います。
      • 上下に空白を作らない高さでフォントを描画したい場合はこの高さを使うと良い、と言い切って間違いないと思います、ごく一部の変なフォント以外。
  • lineHeight
    • フォントの上部末端(上端空白が存在する場合はそれも)から下部末端(line gapも存在するなら含む)までの完全な高さを表します。
    • 飾り文字などではないまっとうなフォントであれば、必ずこのlineHeightの中に文字の高さが収まるようになっています。
      • 一部飾り文字のあるフォントについてはfont leadingを使って下部末端のline gapを表現していることがあります。
    • pointSizeとの最も大きな違いは、上端と下端にline gapに相当する空白を持っています。特にシステムフォントで顕著にそれが見られます。詳しくは後述。
    • 実際にフォントが画面上にレンダリングされるときはこのlineHeightがすべての基準になっているケースが多いです。
  • ascender
    • 全てのアルファベットの文字の最大高さを表します。
    • この高さまで描画されるのはごく一部の文字だけで、殆どの文字はcapHeightまでの高さしか使いません。詳細はこの後のcapHeightを参照してください。
  • descender
    • アルファベットのBaselineより下に描画される文字、例えばj p q yなどの下付き部分の高さを表します。
    • 日本語の文字でこの領域に描画される文字は私が知っている限りでは存在しません。
    • UIFontが返す値は常にマイナスになるので気をつけてください。
    • CTFontが返す値は常にプラスです。
      • めんどくさいので常にfabs()を通すといいと思います。
  • leading
    • Baselineからフォントの下部末端(line gapを含む)までの高さを表します。
    • 上記の図ですとline gapの部分だけがleadingとなっていますがヒラギノフォントで試した限りは誤りです。Baselineから下全てが対象になっているようです。
    • lineHeightに含まれている下部末端のline gapとは「全く別の値が付与されていることがあります」。
      • 例えば飾り文字のあるフォントについて、lineHeightよりもフォントの描画が大きくなるぐらい下に飾りが伸びていたとしても、leadingの値はそこまで考慮して付与してあるので、usesFontLeadingのオプションを付与すると下の行にはみでなくなる、みたいなケースが存在します。
  • capHeight
    • アルファベットの大文字の最大高さを表します。
    • 日本語の文字は(特に漢字は)基本的にすべてcapHeightの高さに描画されます。
    • capHeightより上に描画されるのは、例えばÅ(Aウムラウト)のように修飾子が付いているアルファベットや、日本語だと濁点および【】隅付き括弧のような一部の文字が、capHeightよりも上に描画されます。
特に覚えておきたいのがleadingというプロパティの働きです。UIKit, TextKit, Core TextにusesFontLeadingというような名前のオプションが存在するケースが多々ありますが(例えばNSAttributedString.boundingRect(with:options:context:)のoptionsとかですね)、これは何かというと、
usesFontLeadingが指定されている場合、lineHeightの計算の際に常にUIFont.leadingの値が使用される。従って、もしattributed stringにparagraph styleを付与してlineSpacingに0を付与していたとしても、フォントにleadingの値が付与されていたら、それ以上に行の高さが低くならず必ず行間が開いてしまう。
というオプションになります。paragraph styleでlineSpacingやparagraphSpacingを0に指定しているにもかかわらずどうしても行間がゼロにならないとか、指定するより広がってしまうみたいな状況になった場合は、このfont leadingを疑ってみてください。

ちなみにiOS 8以上については以下のとおりです。

  • UILabelは常にfont leadingを使用しません。
  • UITextFieldは未調査ですがおそらく常にfont leadingを使用しません。
  • UITextViewはデフォルトfont leadingを使用しますが、UITextView.layoutManager.usesFontLeadingの値をfalseにすることで回避可能です。
  • その他のラベルパーツ(UIButton.titleLabelなど)については完全には調査できていませんが、UIButton.titleLabelに関してのみいうとiOS 9の地点ではfont leadingが常に「使用されてしまいます」。これはおそらくiOS 6以前のUILabelの挙動がそのまま残っているためではないかと思われます。同じUILabelクラスですが中身と挙動がUILabelと異なるため気をつけてください。このようなケースは他にもあると思われます。

iOSビルトインフォントの問題点

幾つかのiOSビルトインフォントについては、先述のUIFontのプロパティの値に疑問がある数値が設定されているケースが有るようです。具体的にご紹介します。

1. ヒラギノフォントの場合

ヒラギノフォント、すなわちヒラギノ角ゴシックW3/W6、それからヒラギノ明朝W3/W6については以下の様なフォントの設定の特徴があります。
  • leadingの値がきちっと付与されています。
  • 常にpointSize == lineHeightになります。
  • おそらくバグだと思うのですが、descenderの値が本来必要な量の半分しか設定されていません。そのためヒラギノフォントを明示的に付与したラベルにg, j, y, qなどの下付き部分があるアルファベット文字を描画すると下が途中で千切れてしまいます。
    • 対処法としてはfont leadingを使用するか、または自力でdescender分だけラベルの高さを高くするか、明示的に指定してヒラギノフォントなんて使うのをやめてシステムフォントを使用してください。

2. システムフォントの場合

次にシステムフォントはどうなっているのかというと、以下の様な特徴があります。
  • 常にleadingの値がゼロになります。従ってfont leadingを使うモードで描画しても使わないモードで描画しても全く同じ結果になります。
    • iOS 8はHelvetica Neue, iOS 9以上はSan Franciscoが採用されていますが、いずれのケースでも全く同様の設定になっています。
  • pointSizeよりlineHeightが高く、ちょうどその差分の分だけ自動的にline gapとして行間が開くような設定になっています。
    • 要するに簡単にいえば、font leadingを使わないで、かつattributed stringでlineSpacingやparagraphSpacingをゼロに設定したとしても、必ず行間がちょっとだけ空いてしまいます。まずやることはないと思いますが、正確に行間ゼロを作るのは著しく困難です。

UIFontのpointSizeについて

UIFont.pointSizeについて、みなさん普段から何気なしに17とか14とか値を指定されているかと思いますが、このpointSizeに指定する値の単位って気にされていますでしょうか?このフォントのpointSizeとはPostScriptポイントないしDTPポイントと呼ばれるものらしいです。一般的に1PostScriptポイント=1/72インチとされています。

ではこの単位はiOS上で使われている論理座標系のpt単位とは全く違うものなのかというと、どうやら大昔のMacintoshのころからの名残で、macOS/iOS上の論理座標系の1pt=1PostScriptポイントとなるように定められていて、これは現在に至るまで常に維持されているということらしいです。

要するに、UIFontのpointSize=iOS上での1ptと完全に一致します。このことはlineHeightが16のフォントを画面上にレンダリングすると論理単位16pt(2倍Retinaなら32pxになる)ことからも確認ができます。便利ですね。


UIFont.leadingのiOS 8以下での不具合

かいつまんで言うと、UIFont.leadingの値は、iOS 8以下のときに常にUIFont.lineHeightと同じ値を返します。これは"font leading"という単語が使われている文脈や文化圏によって意味が異なり、font leading = line heightとなる文化圏があったので誤用されてしまった、ということらしいです。しかしながら実際のiOSでは先述の通り「font leading = BaselineからフォントのLine gapを含む下端までの距離」と定義されていますから、このような値では困ります。

対処法として、iOS 8以下の場合はCTFontを使ってleadingの値を取得してください。こちらにコードを用意しました。
https://gist.github.com/akisute/3c3d162da73abd784525c9ed7859cda2

なお、iOS 9以上はこの問題は発生しません。iOS 8なんてそろそろサポート切れるんで忘れていいかもしれませんが、念のため。

まとめ

デザイナーさんにフォント周りで行間調整がおかしいぞーとかベースラインがずれてるぞーとか突っ込まれまくっていて必死こいて修正方法を調べている時に、こんな記事を見つけました。

https://www.raizlabs.com/dev/2015/08/advanced-ios-typography/

Stop Saying "No" to Designers.
このサイトには英字フォントをBaselineではなくcapHeightの位置で上揃えにするべく奮闘した一人の漢の話が書かれています。おお、海外にも私と同じ境遇の漢が居たのです。

我々もデザイナーにNOと言わないタイポグラフィを実現させたいですね\(^o^)/

2016年6月30日木曜日

Auto Layout と Manual Layout を混載させるときに役立つ UIView.translatesAutoresizingMaskIntoConstraints プロパティの話

Auto LayoutがiOS 6で導入されてはや4年、未だによく理解していなかった挙動に UIView.translatesAutoresizingMaskIntoConstraints があります。このプロパティは自分がプログラムコード上で生成したviewをAuto Layoutするときにfalseにする必要があるものということで皆様記憶されているかと思うのですが、具体的にこのプロパティは何をやっているのかが個人的に全く謎でした。それが今日一つ謎が解けましたのでここに共有させていただきたいと思います。

UIView.translatesAutoresizingMaskIntoConstraintsの値がtrueのときとfalseのときの違いについて以下に記載します(iOS 8以上で確認しています)。

  • trueのとき
    • 対象のviewのframe、すなわちx, y, width, heightの4つの要素をview.frame, view.bounds および view.center プロパティから直接操作することが可能になります。これはAuto Layoutが導入される以前のiOSの世界と同じ状態です。この挙動をAuto Layoutとマッチさせるため、対象のviewのx, y, width, heightの4要素を指定された値に固定するようなAuto Layout Constraintsが自動的にシステムによってviewに挿入されます。この自動的に挿入されるAuto Layout Constraintsのpriorityは常に1000 (Required)になります。
  • falseのとき
    • 対象のviewのframe、すなわちx, y, width, heightの4つの要素はすべてAuto Layoutエンジンが管理するようになり、view.frame, view.bounds および view.centerの値を直接書き換えても一切無視されるようになります。Auto Layout Constraintsが設定されていない場合、viewのframeはCGRect.zeroになります。

プロパティの名前にAutoresizing Maskとか入っているのでてっきりAutoresizingの仕組みに影響している用に見えますが、実際には全く関係ありません。その証拠にAutoresizingMaskの値をどのように変化させても勝手にAuto Layout Constraintsが挿入されてしまいます。このプロパティはあくまで当該viewのframeを自動的に操作するようなAuto Layout Constraintsを挿入するか否かを決めるフラグとして覚えると良いでしょう。

さてこの挙動を覚えると何が嬉しいかと申しますと、Auto Layoutと非Auto Layoutを混載させるときに非常に役立ちます。こうすることで、特定のviewだけをframe手動操作で設定し、他のviewはAuto Layoutに任せるというような荒業が自由自在に可能になります。

具体例を見てみましょう。例えば以下の様なニュースを表示する画面を作ってみようと思います。



ここでこのnewsを表示するviewのframeは複雑なアニメーションをさせたいなどの理由で外部からマニュアルで設定したいが、viewの中身はauto layoutに任せたいというようなケースがあるかと思います。

というわけで普通にAuto Layoutで作ってみましょう。
private func commonInitialize() {
        self.translatesAutoresizingMaskIntoConstraints = true
        self.backgroundColor = UIColor.white()
        
        self.imageView = UIView()
        self.imageView.translatesAutoresizingMaskIntoConstraints = false
        self.imageView.backgroundColor = UIColor.green()
        self.addSubview(self.imageView)
        
        self.titleLabel = UILabel()
        self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
        self.titleLabel.text = "factorio alpha 0.13 has been released!"
        self.titleLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyleTitle1)
        self.titleLabel.numberOfLines = 2
        self.addSubview(self.titleLabel)
        
        self.articleLabel = UILabel()
        self.articleLabel.translatesAutoresizingMaskIntoConstraints = false
        self.articleLabel.text = "In 0.13 we have the new multiplayer matching server and server browser. This will let you find games of people online join your friends and other stuff. Server games are published to the server and clients can browse existing games. The first thing you will notice is the new multiplayer menu. When you click on 'Browse Public games' you will be asked to log in to your factorio account."
        self.articleLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyleBody)
        self.articleLabel.numberOfLines = 0
        self.addSubview(self.articleLabel)
        
        let views: [String: AnyObject] = ["imageView": self.imageView,
                                          "titleLabel": self.titleLabel,
                                          "articleLabel": self.articleLabel]
        
        self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[imageView]|", options: [], metrics: nil, views: views))
        self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[titleLabel]-10-|", options: [], metrics: nil, views: views))
        self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-10-[articleLabel]-10-|", options: [], metrics: nil, views: views))
        self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[imageView]-10-[titleLabel]-10-[articleLabel]|", options: [], metrics: nil, views: views))
        self.imageView.addConstraint(NSLayoutConstraint.init(item: self.imageView, attribute: .height, relatedBy: .equal, toItem: self.imageView, attribute: .width, multiplier: 0.66, constant: 0))
    }


しかしながらこのコードはAuto Layout Warningが発生してしまいます。

2016-06-30 23:24:12.050906 AutoLayout[1725:80607] [LayoutConstraints] Unable to simultaneously satisfy constraints.
 Probably at least one of the constraints in the following list is one you don't want. 
 Try this: 
  (1) look at each constraint and try to figure out which you don't expect; 
  (2) find the code that added the unwanted constraint or constraints and fix it. 
 (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "NSAutoresizingMaskLayoutConstraint:0x7fd60ae1db90 h=--& v=--& AutoLayout.Example2View:0x7fd60ad35b70.width ==   (active)",
    "NSLayoutConstraint:0x7fd60ac360a0 H:|-(10)-[UILabel:0x7fd60ad3c6b0'factorio alpha 0.13 has b...']   (active, names: '|':AutoLayout.Example2View:0x7fd60ad35b70 )",
    "NSLayoutConstraint:0x7fd60ac36380 H:[UILabel:0x7fd60ad3c6b0'factorio alpha 0.13 has b...']-(10)-|   (active, names: '|':AutoLayout.Example2View:0x7fd60ad35b70 )"
)

これは先程のUIView.translatesAutoresizingMaskIntoConstraintsについての説明を元にすると以下のように解釈できます。

  1. UIView.translatesAutoresizingMaskIntoConstraintsがtrueに設定されていることにより、このviewにはwidth=frame.size.widthになるようなAuto Layout Constraintsが自動的に設定されている。
  2. このviewにはさらに "H:|[imageView]|"となるようなAuto Layout Constraintsが設定されている。これはimageViewを横幅いっぱいに表示するため。
  3. しかしながらこのような設定を行うと、imageViewが親となるviewの横幅を自分の横幅に合わせて引っ張ろうとするConstraintsが定義されてしまうので、1. で自動的に設定されたConstraintsと衝突してしまう。
  4. 結果としてwarningが発生する。
これを回避してやるにはいくつか方法があります。

  1. "H:|[imageView]-(0@999)-|"のように設定することで、右側ないし下側のpriorityを999に下げる。こうすることによってUIView.translatesAutoresizingMaskIntoConstraintsによって設定されるConstraintsのpriorityが勝つためワーニングは発生しなくなる。
  2. 両側を引っ張るようにvisual formatを使って設定するのをやめて、view.x=imageView.x, view.width=imageView.widthとなるようにConstraintsを付与する。
例えば2. のケースはiOS 9以降であればNSLayoutAnchorを使って簡単に設定ができます。

self.imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
self.imageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.0).isActive = true

これでAuto LayoutとManual Layoutをより自由自在に混載させることが可能になると思います。

2016年6月19日日曜日

ゼロから始めるマインクラフトのサーバー構築の話

突然なのですが、最近いまさらながらマインクラフトに大ハマリしておりまして。きっかけは友人のマルチプレイサーバーでプレイしたことだったのですが、やっぱり皆で遊ぶゲームは面白いなと再確認している次第です。マインクラフトはリリース直後、βが取れたぐらいに遊んで面倒でやめてしまったのですが、近頃はFeed The Beastと呼ばれるオールインワンのランチャーアプリが登場しており、こいつがMODの導入や管理も全て一発でやってくれるため面倒やストレスが一切なく楽しめます。

で、しばらく遊んでいたのですが、友人が他のゲームにハマってしまったためマインクラフトのサーバーを立てなくなってしまいました。そこで私のほうで自分でマインクラフトのサーバーを立ててやろうと思い立ちまして早速今週末チャレンジしてみました。

ちなみに私インフラに関しては大の苦手で、サーバーをゼロから構築するのは初めてだったりします。大昔新卒の頃にSIerの仕事でちょっと触ったりとか、その後の仕事でちょっと触ったりとかした程度。アプリケーションなら分かるんですけど(´・_・`)

前置きはこのぐらいにして早速やってみましょう。

ホスティングサービスを選定

パッと思いつくところだとさくらVPSとAWSが有名ですが、今回は友人のおすすめでConoHaというVPSホスティングサービスを選んでみました。ほとんどさくらVPSと大差ないように見えたのですが、ConoHaのほうがさくらVPSと違い初期費用がかからないらしいです。新しいもの好きですしせっかくなのでConoHaを選択。

サーバーのスペック等は以下のようにしました。CentOS 7.2に3コア2GBメモリで月1750円。マインクラフトのサーバであればこのぐらいの性能で大丈夫なはず。


とりあえずサーバーを起動してsshまで

構築したらすぐにインスタンスが起動します。ConoHaでは自動的に固定Public IPが1インスタンスに割り当てられるようで、この点固定IPを用意したりするのが何かと面倒なAWSに比べて楽でした。管理画面もすっきりまとまっていて使いやすく好印象です。キーペアもその場で同時にすぐ作成してくれて手間がかかりません。

生成したキーペアをローカルの ./ssh 以下に保存して ./ssh/config を適当に設定してログイン。root@を付け忘れてssh接続要求に失敗するみたいなよくあるお約束のミスをしましたが、特に問題なくログインできました。

ログインに成功したのを確認してから手持ちのドメイン (akisute.com) のサブドメインを用意してDNSを設定してやります。疎通して問題なし。

nginxを立てる

サーバーの情報などをウェブに公開したいので、nginxを利用してstatic htmlファイルをホスティングできるようにします。nginxのCentOS 7.2に対するインストール自体はこちらのページに従えばすぐにできます。
http://nginx.org/en/linux_packages.html
後は以下のビギナーズガイドに従って設定を行えば良いのですが、基本的には最初から全て必要そうな設定は用意されているので、/usr/share/nginx/html以下のindex.htmlを適当に編集してやるだけでオッケーです。
http://nginx.org/en/docs/beginners_guide.html

firewalldを設定する

しかしながらnginxを立ち上げただけでは外からアクセスしてもつながりません。これはCentOS(に限らないですが、ほとんどのLinuxのディストリビューションについて)は何らかのファイアウォールがデフォルトで入っているためです。一般的にはiptablesが使用されているのですが、CentOS 7ではiptablesではなくfirewalldというやつが使われているらしいので、そちらを設定することにしました。
http://urashita.com/archives/963
http://www.unix-power.net/centos7/firewalld.html

httpサービスをpublicゾーンの設定に追加するだけで簡単に外からアクセスできるようになります。iptablesより簡単でいいですね。

Javaをインストールする

マインクラフトはJavaで動作するので最新版のJavaが必要です。Javaには大きくOpenJDKとOracle Javaがありますが、個人的にはOracle Javaのほうが速そうなのでそちらを使うことにしました。以下のページで紹介されているとおりにすれば簡単にインストールできるかと思います。
https://www.digitalocean.com/community/tutorials/how-to-install-java-on-centos-and-fedora

minecraft-serverを立てる

いよいよ本題のminecraft-serverです。これについてはFTBのmodpackであればFTBのクライアントからDownload Serverボタンを押すだけでzip形式で最初から用意されたサーバーセットアップ一式がダウンロードできますので、ダウンロードURLをコピーしておいてサーバー側からwgetして取得しunzipするだけで準備完了です。
http://www.feed-the-beast.com/servers

unzipして展開した中身のServerStart.batをServerStart.shにして中身を適当に書き換えてやり(主にJVMの起動オプションからヒープに割り当てる量を調整するなど)実行すれば簡単にサーバが立ち上がります。ただし普通に立ち上げるとシェルを専有されてしまうので、screenなど経由でServerStart.shを実行するようにすればいいでしょう。
http://www.minecraftforum.net/forums/support/server-support/server-administration/1897274-how-to-use-screen-on-your-linux-minecraft-server

あとはサーバーコマンドで自分をopにでも追加しておけば良いと思います。起動したサーバーのコマンドラインから /op 自分の名前 とコマンドを実行すればopを付与できます。その他必要に応じてminecraft-serverの設定ファイルを操作したりgameruleを調整すれば良いと思います。

最後に先ほど設定したfirewalldにminecraftサービスを追加して、外からアクセスできるようにすればおしまいです。minecraftはhttpと違ってデフォルトでサービスが用意されていないので、自分でサービスを追加する必要があります。サービスの追加は/etc/firewalld/servicesにxml設定ファイルを追加すればよいです。サンプル代わりにデフォルトのサービスのxml設定ファイルが/usr/lib/firewalld/services以下に配置されているので適当なものをコピーして改造して使えばよいです。minecraftはtcp/udpの25565がデフォルトポートなのでここを開くように設定してfirewalldを再起動すれば外から繋がるようになるかと思います。

その他の設定

ログローテーションを設定しておかないとディスクがログで溢れて死ぬかと思ったんですが、最初からnginxについてはログローテーションが設定されているため気にしなくてOKです。minecraft-serverについてはどうなるかわかりませんので、必要に応じてlogrotateを設定して対応したほうが良いと思います。
https://genchan.net/server/4907

まとめ

いろいろ調べながらで時間がかかりましたが、3時間ほどでゼロ知識の状態からでもマインクラフトのマルチプレイサーバーを立てることができました。現在のところ無事快適に遊べています\(^o^)/

こういう自分の趣味嗜好のために技術を勉強すると非常に捗るのでオススメですね。みなさんもゲームを楽しみながら技術を勉強して一石二鳥になりませんか\(^o^)/