Dynamic Type対応アプリとは何なのかぁ〜。
簡単に言えば、ユーザーが設定で「文字ちょい大きめ、ちょい小さめ」ってやると、それに対応して表示文字の大きさを変更してくれるアプリのことっす。
↓実機だと設定アプリの画面表示と明るさ→文字サイズを変更で変更
↓シミュレータだと設定アプリの一般→アクセシビリティ→Large Textで変更
ちなみに、シミュレータの初期設定は英語だけど、実機と同じやり方で日本語に切り替えることもできる。
↓シミュレータでもやり方は同じ
まあ、とにかく、アプリをDynamic Typeに対応させるためには、Dynamic Type用に用意されてるスタイル(ヘッドライン用とか、キャプション用とか)をUILabelのfontプロパティに指定する必要があるんですよ。
class ViewController: UIViewController {
・・・
// 各UILabelに設定する文字スタイルを配列で持つ
let textStyles = [
UIFontTextStyle.headline,
UIFontTextStyle.subheadline,
UIFontTextStyle.body,
UIFontTextStyle.footnote,
UIFontTextStyle.caption1,
UIFontTextStyle.caption2
]
override func viewDidLoad() {
super.viewDidLoad()
// 各UILabelのy座標だけ変化させる
var y:CGFloat = 40
for textStyle in self.textStyles {
let label = UILabel(frame:
CGRect(x: 100, y: y, width: 200, height: 60))
label.font = UIFont.preferredFont(forTextStyle: textStyle)
サンプル:
http://tetera.jp/xcc/book-sample/dynamictype.zip
fontプロパティに指定するのはUIFontオブジェクトっす。
UIFont
こいつは文字の形状(明朝体とかゴシック体とかが、ヒラギノ、Osakaといった名前で分類されてる)や、大きさを表現したオブジェクトで、Dynamic Type用には、UIFontのクラスオブジェクトにpreferredFontメッセージを送って取り出したものを使います。
UIFont.preferredFont(forTextStyle: 指定するスタイル)
引数forTextStyle:にはUIFontTextStyleで定義されている、ヘッドラインとか、キャプション用のスタイルを指定するですが、サンプルではtextStylesというUIFontTextStyleの配列をプロパティとして用意し、これをviewDidLoadメソッドでforループを使って取り出し使っています。
for textStyle in self.textStyles { ・・・ }
こいつは配列用のforループで、こう書くことでself.textStylesの要素を順にtextStyleに取り出すようになります。
for index in 0..<self.textStyles.count { let textStyle = self.textStyles[index] ・・・ }
と書いても、同じことができるんですが、先の書き方の方がスッキリするので使っています。ちなみにループ範囲の指定に使ってる「..<」は未満の指定です。
これで0からtextStyles配列の要素数未満の間となります。配列のインディックスは0から始まるんで、末尾の要素のインディックスは要素数-1にしないとダメだからです。
0...3 0、1、2、3のループとなる
0..<3 0、1、2のループとなる
とにかく、こうすると、起動時にユーザー指定に応じた大きさの文字が表示されるわけですな。
ただし、これをやっただけでは初回起動時にしか対応しません。
ホームボタンで一旦アプリを中断して設定アプリで文字サイズを設定し直しても、それには対応しないんですよ。
これに対応するには通知を使います。
通知
こいつは文字どおり、通知を受けとる機能です。
アプリには、起動時に用意される通知センタってオブジェクトが居て、こいつに「これこれのイベント発生時に通知を受けたい」と登録すると、指定したイベント発生時に通知がもらえるようになってるんですよ。
そいつに、今回なら「ユーザーが設定アプリで文字サイズを変更」というイベント発生を通知してもらうようにします。
通知は、指定したオブジェクトの指定したメソッドを、通知センタから呼び出してもらうことで実現します。
そのため登録時に、通知を受け取るオブジェクトと呼び出してもらうメソッドの指定が必要なんですが…
このメソッドは、名前はなんでもいいんだけど、引数が決まってて
func メソッド名(_ notification:Notification) {・・・}
というメソッドにする必要があります。
引数のところの _ (アンダースコア)は何かというと、実を言うと本来、メソッドや関数の引数の定義は
と書くのではなく
と書くのが正式で、関数を呼び出したりメッセージを送ったりする側は、このラベル名を書くことになっているんですな。
例)
定義:
func example(arg a:Int) {・・・}
呼び出し・送信側:
example(arg:1)
じゃ、今までのは何だったのかというと、引数の略記法でして、ラベル名を書かないことで、ラベル名と引数名は同じとみなすことになるんですよ。
例)
定義:
func example(a:Int) {・・・} ラベル名も、仮引数名も同じという意味
呼び出し・送信側:
example(a:1)
というわけで、今回の引数
(_ notification:Notification)
は
ラベル名 _
仮引数名 notification
型 Notification
という意味になります。
で _ は特別で、呼び出し側はラベル名を付けないという指定になるんですよ。
例)
定義:
func example(_ a:Int) {・・・} ラベル名は付けないという意味
呼び出し・送信側:
example(1)
で、このメソッドを通知センタオブジェクトに登録するわけです。
override func viewDidLoad() { super.viewDidLoad() ・・・ NotificationCenter.default.addObserver(self, selector:#selector(ViewController.categoryDidChange(_:)), name:NSNotification.Name.UIContentSizeCategoryDidChange, object:nil) } func categoryDidChange(_ notification:Notification) { ・・・ }
通知センタオブジェクトはNotificationCenterクラスオブジェクトのdefaultプロパティに設定されてるので、そいつに登録用のaddObserverメッセージを送ってます。
最初の引数が、通知を受け取るオブジェクトで、今回ならViewController自身なのでself、その次のselector:には呼び出してもらうメソッドの指定。
ここで使ってる#selectorてのは、コンパイラ司令というやつで、Swift言語で書かれたプログラムをiPhoneが理解できるものに変換するツール(コンパイラって言います)への命令です。これで ( ) 内に書かれたメソッドを引数として渡せる形にして渡せという命令になってます。
#selector(メソッドの指定)
で、この書き方は次のようにメソッドの定義に基づいて書くようになってて
引数はラベル名に : を付けて表現します。型は書きません。
で、その次のname:は、イベントの指定。
今回なら「ユーザーが設定アプリで文字サイズを変更」というイベントで、こいつはNSNotificationに定義されてます。
NSNotification.Name.UIContentSizeCategoryDidChange
で、最後のobject:は、このイベントを発生させたオブジェクトを指定するためのもので、nullだと、どんなオブジェクトでも指定したイベントを発生させれば通知しろってことになります。
ここに特定のオブジェクトを指定した場合、そのオブジェクト以外が指定したイベントを発生させても通知は来なくなります。
今回は、どんなオブジェクトでもOKなんでnullとしてます。
これで、ようやく、設定アプリで文字の大きさが変更されると、指定したメソッドが呼び出されるようになるんですな。
んでもって、呼び出された時に、再度フォントを指定してやると、大きさが変わるって仕組みです。
そのためにサンプルでは、スタイルとUILabelを配列で用意してます。
class ViewController: UIViewController { var labels = [UILabel]() ・・・ let textStyles = [ ・・・ override func viewDidLoad() { ・・・ for textStyle in self.textStyles { ・・・ self.labels.append(label) ・・・ func categoryDidChange(_ notification:Notification) { for (index, label) in self.labels.enumerated() { label.font = UIFont.preferredFont( forTextStyle: textStyles[index]) } }
最初に登場する
var labels = [UILabel]()
てのは、空のUILabel配列の宣言。
この配列にappendメッセージを送って、作成したUILabelを追加してます。
self.labels.append(label)
でもって、categoryDidChangeメソッドでは、このlabelsからインディックスとUILabelを一組として取り出してUILabelのfontプロパティを再設定してる。
ここで出てくる
for (index, label) in self.labels.enumerated() {
というループは、配列にenumeratedメッセージを送ることで、インディックスとUILabelを一組として順に取り出すもんで、やってることは
var index = 0 for label in self.labels { ・・・ index += 1 }
と同じです。
(index, label)はタプル(tuple:組)と呼ばれるもので、Swiftでは、こんな風に ( ) で囲み、 , (カンマ)で区切ることで複数の値を、一組にして受け渡せるようになってます。
もちろん送る方、受ける方でタプルを使うことを定義する必要があるし、タプル内の値の順はその定義に従います。enumeratedメッセージを使ったforループでは(インディックス, 要素)の順です。
ちなみに、この通知登録はViewControllerが消える時には、解除しないといけません。なのでdeinitで解除してます。
deinitは自分が消えて無くなる前に呼び出される特殊なメソッドで、通知解除のタイミングとして丁度いい。通知センタに引数に自分自身を指定し、removeObserverメッセージを送ることで、自分を受け手とした登録が全て解除になります。
deinit { NotificationCenter.default.removeObserver(self) }
もっとも、次回のバージョンからはこいつは必要なく…(気になる人はWWDCビデオみましょう)
とにかくこれでDynamic Type対応アプリになったんだけど、見ての通り、UILabelの初期設定だと1行表示なんですよ。
表示できない文字後半部分は「…」で省略されます。
こいつを、複数行表示にしたい場合はUILabelのnumberOfLinesプロパティに、最大行数を指定します。
label.numberOfLines = 4
そうすることで、その指定行までは改行で残りを表示しようとしてくれます。
まあ、それでもUILabelの矩形(背景色を臼灰色にしてるのでわかると思う)を超えることはできないんですがね。
そんなの嫌じゃい。
文字はいつでも全部表示させるようにするんじゃいい。
↓こんな感じでな
そんな人はUILabelの矩形を調整することになります。
で、その場合に使うと便利なのがAutoLayout機能。
というところで、以下次回。