4
3

More than 5 years have passed since last update.

iOS Today Extention (ウィジェット)のサイズは固定ではない

Last updated at Posted at 2018-07-24

iOSのウィジェットのサイズについて、情報を見つけられなかったため記録を残します。
iOS9以降を対象にしていて、8以前は調べていません。

対象OS:iOS9/10/11
対象XCode: 9.4

ウィジェットのライフサイクル

Todayを表示するとき、毎回Todayビューコントローラーの最初から始まります。
iOS10以降でアプリアイコンを3DTouchで押した時に表示されるウィジェットでも同じです。

  • iOS9

    • Todayを表示
      1. awakeFromNib()
      2. beginRequest()
      3. viewDidLoad()
      4. widgetMarginInsets()
      5. widgetPerformUpdate
      6. viewWillAppear()
      7. viewDidAppear()
    • Todayを表示終了
      1. viewWillDisappear()
      2. viewDidDisappear()
      3. deinit()
  • iOS10以降

    • Todayを表示
      1. awakeFromNib()
      2. beginRequest()
      3. viewDidLoad()
      4. widgetActiveDisplayModeDidChange()
      5. widgetPerformUpdate()
      6. viewWillAppear()
      7. viewDidAppear()
    • Show more /Show lessを押した時
      1. widgetActiveDisplayModeDidChange()
    • Todayを表示終了
      1. viewWillDisappear()
      2. viewDidDisappear()
      3. deinit()

iOS9までと10以降では4ステップ目に呼ばれる関数が違っています。
deinitは即行われるわけではありませんが、だいたい表示終了後、数秒で行われるようです。
2番目のbeginRequest()はcontextを渡してきますが、iOS9ではとくに情報は入っていません。iOS10以降では、サイズの情報が得られます。この情報はビューコントローラーのextensionContextに保持され、viewDidLoad()以降のいつでも参照できます。
ウィジェットは特に迅速な動作を要求されます。表示するたびに毎回最初から作られるので、起動したらそこにある情報を使って動くよう設計しておく必要があります。

ウィジェットの画面サイズ

App Extention Programming Guide の Todayのページには、Auto Layoutを使うよう指示があります。
ウィジェットのサイズは、システムの強い制約を受けます。ウィジェットの横幅はデバイスやOS毎に固定されていて変更できません。高さは一定の制約の中で変更可能ですが、iOS9以前とiOS10以降で仕様が異なっています。

iOS9のウィジェット

ios9iPhone4sWidgetSizes.png

iOS9ではウィジェットの高さは、ウィジェットが制御できます。

高さを動的に変えなくて良い場合

ウィジェットの高さを固定できる場合は、viewDidLoadの中でビューコントローラーのpreferredContentSizeheightに入れて指定します。widthは0にします。
ビューコントローラのviewtranslatesAutoresizingMaskIntoConstraintsfalseにしないようにしてください。
preferredContentSizeで指定した高さは、最終的に制約に反映されます。translatesAutoresizingMaskIntoConstraintsfalseにしてしまうと、その制約がつくられなくなり、高さが不定になります。

TodayViewController.swift
func viewDidLoad() {
    preferredContentSize = CGSize(width:0, height:希望の高さ)
    view.translatesAutoresizingMaskIntoConstraints = true 
}

iOS9で高さを可変にしたいとき

preferredContentSizeはビューのサイズを決め打ちしてしまうので、ラベルなどのもつ本来のコンテンツサイズ(intrinsic content size)を自動的に反映させることはできません。例えばDynamic Typeに対応するなど、コンテンツサイズに応じた表示にする場合は、preferredContentSizeを指定せず、Auto Layoutに全部をまかせます。そうすると、内部サイズを反映した高さでウィジェットを作成してくれます。ただし、幅は決められているので、どのサイズになってもいいようにAuto Layoutを設計する必要があります。また、最上位ビューの高さが決まるようなレイアウトにしないと、無駄に大きいサイズでウィジェットが作られます。
このときも、translatesAutoresizingMaskIntoConstraintstrueのままにするようにしてください。

insetの高さは0がよい

ビューコントローラーにwidgetMarginInsets(forProposedMarginInsets:)を実装すると、widgetの表示の余白を制御することができます。デフォルトでは、ウィジェットをインデントして右によせ、下にも余白をとります。機種毎のOS9でのウィジェットのinset値はこうなっていました。

機種 画面サイズ 横幅 inset上 inset左 inset下 inset右
iPhone4s 320x480 272 0.0 48.0 39.0 0.0
iPhone5等 320x568 272 0.0 48.0 39.0 0.0
iPhone6等 375x667 336 0.0 48.0 39.0 0.0
iPhone6Plus等 414x736 375 0.0 52.0 39.0 0.0

この関数では高さ0を返しておく事をお勧めします。というのは、一番最初にウィジェットを表示するときだけ、ウィジェットの高さがpreferredContentSize.height + inset.top + inset.bottomになってしまう 不具合 現象があるので、サイズが変化してしまうからです。次にサイズを変えるきっかけがあると、ウィジェットの高さは正しくpreferredContentSize.heightになります。

TodayViewController.swift
func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
    return = UIEdgeInsets(top: 0, left: defaultMarginInsets.left, bottom: 0, right: defaultMarginInsets.right)
}

ウィジェットは即時応答性を高めるために、前に表示した画像のキャプチャを一旦表示して、新しいサイズにアニメーションしながら変化させられるよう仕組みが用意されているのですが、その部分に間違ったサイズを指示してくるタイミングがあるようです。一番最初だけは高さ0で始まり、それからウィジェットのサイズに変化するよう指示がきます。このとき、viewWillTransition()にはウィジェットのサイズで指示が来ますが、実際のビューは、insetの高さを足した大きさで作られてしまいます。
insetを0にして影響がないようにするのが一番簡単な回避策だと思います。
固定の高さでよければ、大きさが変わっても問題ないようなレイアウトにすることもできます。高さをオートレイアウトで決める場合は難しいでしょう。

iOS9のウィジェットの高さの決め方

iOS9ではOSが指定してくるウィジェットのサイズを調べる手段がなく、画面サイズを元に自分で適当に決める必要があります。App Extention Programming Guideには、スクロールしなければならないほど大きいサイズを指定しないように記載があります。
iOS10以降で仕様が変わってしまっているので、そちらに合わせて決めるのも現実的な方法だと思います。

iOS9のウィジェットの文字色

最新のヒューマンインターフェースガイドラインでは暗い文字を推奨していますが、iOS9では背景が暗いので、白い文字のほうが良いです。昔のヒューマンインターフェースガイドラインには、おそらく白くするように指示があったんでしょうね。

iOS10以降のウィジェット

iOS10以降は大きく仕様が変わって、幅だけでなく高さも固定になりました。
ウィジェットのサイズを2種類(.compact.expanded)定義できるようになり、「表示を増やす」/「表示を減らす」ボタンでユーザーが切り替えられます。.compactへの対応は必須で、.expandedは任意です。
.expandedは制限内で任意の高さに変更できますが、.compactはOSが決めた値に固定されます。
preferredContentSizeの高さ指定は.expandedでは有効ですが、.compactでは無視されます。
widgetMarginInsets()はもはや呼ばれません。
その代わり、ビューコントローラーのextensionContextに大きさの情報が入るようになりました。
extentionContext.widgetActiveDisplayModeで現在のサイズを取得でき、
extentionContext.widgetLargestAvailableDisplayModeで、.expandedを使うかどうかを指定できます。
また、大きさを切り替える時、ビューコントローラーのwidgetActiveDisplayModeDidChange()が呼ばれます。この関数で.expandedになるときに、preferredContentSizeに高さを指定します。高さの範囲は.compact.expandedのそれぞれの最大サイズの間です。widthは0でもかまいません。
widgetが自分でどちらのサイズにするかを選ぶことはできないようです。
(iOS11ではNewsが最初から.expandedであるように思えますが、やり方がわかりませんでした)

使い方も変わり、Search画面にも出せるようになり、3D Touchでアプリアイコンを押した時にもウィジェットが表示されるように拡張されています。

高さ固定という誤解

高さ固定といっても、単純に固定ではありません。Dynamic Typeに合わせて変化してしまいます。
(Dynamic Typeについては、iOSのDynamic Typeについて に書きました。)
web上の多くの情報は、単にwidgetの高さは110(または120)固定であるとしていますが、正しくありません。
Dynamic Typeは、ユーザーがいつでもフォントサイズを変化させることができる機能です。ウィジェットの大きさは、これに合わせて、たとえあなたのアプリがDynamic Typeに対応していなくても、動的に変化します。
変化は.compactの大きさだけでなく、.expandedの方にも影響し、maxSizeが変化します。
以下に調査結果を示します。

iOS10.3のウィジェットサイズ:
iOS10widget.png

機種 画面 横幅 モード 1 2 3 4 5 6 7 8 9 10 11 12
iPhone4s 320x468 304 .compact 95 100 105 110 120 130 140 170 200 240 280 325
.expanded 532 520 504 528 528 520 504 476 480 480 448 520
iPhone5/SE 320x568 304 .compact 95 100 105 110 120 130 140 170 200 240 280 325
.expanded 532 520 504 528 528 520 504 476 480 480 448 520
iPhone6等 375x667 359 .compact 95 100 105 110 120 130 140 170 200 240 280 325
.expanded 608 600 630 616 624 624 616 612 560 576 560 520
iPhone6Plus等(縦) 414x736 398 .compact 95 100 105 110 120 130 140 170 200 240 280 325
.expanded 684 680 672 660 672 676 672 680 640 672 672 650
iPhone6Plus等(横) 736x414 420 .compact 95 100 105 110 120 130 140 170 200 240 280 325
.expanded 684 342 378 352 336 364 336 340 320 288 336 260

表の1〜12はDynamic Typeで指定したの文字の大きさで、表内の数値がそのときの高さです。
1が最小でデフォルトは4、8以上はアクセシビリティで「さらに大きな文字」スイッチをオンにすることで選べます。
.compact行の数値はウィジェットの固定高さで、.expanded行は、その時のmaxSize.heightの値を示しています。
.expandedの最大高さが微妙に変化する理由はよくわかりませんが、調べた限り毎回同じ結果になりました。
また、iPhone6Plusなどの5.5インチiPhoneの横表示は、高さも幅も縦表示と違う設定になっています。
.expandedという名前なのに、.compactより小さいサイズになるところがあったりしますが、おそらくこれは不具合です。このケースでは、ボタンを押しても高さが低い方に寄って変化しなくなるという変な動きをします。以下のコードでは、それぞれのモードのmaxSizeにすることができました。

TodayViewController.swift
@available(iOSApplicationExtension 10.0, *)
func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) {
    switch activeDisplayMode {
        case .compact:
            preferredContentSize = CGSize(width: 0, height: 0)
        case .expanded:
            preferredContentSize = maxSize
        }
    }
}

(iPadは調べていませんが、さらにひどい混乱があるかもしれません。なお、widgetは常にiPadにも対応が必須です・・・)

iOS11.4のウィジェットサイズ:
ios11iPhone5WidgetSizes.png

機種 画面 横幅 モード 1 2 3 4 5 6 7 8 9 10 11 12
iPhone5/SE 320x568 304 .compact 95 100 105 110 120 130 145 170 200 200 200 200
.expanded 456 440 462 440 480 520 522 680 720 640 640 560
iPhone6等 375x667 359 .compact 95 100 105 110 120 130 145 170 200 200 200 200
.expanded 532 560 546 528 576 624 696 816 880 880 800 800
iPhone6Plus等(縦) 414x736 398 .compact 95 100 105 110 120 130 145 170 200 200 200 200
.expanded 608 600 630 616 672 676 754 884 1040 1040 1040 880
iPhone6Plus等(横) 736x414 398 .compact 95 100 105 110 120 130 145 170 200 200 200 200
.expanded 608 600 630 616 672 676 754 884 1040 1040 1040 880
iPhoneX 375x812 359 .compact 95 100 105 110 120 130 145 170 200 200 200 200
.expanded 684 680 672 660 720 780 812 1020 1120 1120 1040 960

iOS11では5.5インチiPhoneの横表示のウィジェットサイズは、縦表示と同じに統一されています。
iOS10のように.compact.expandedが逆転するケースはもうなく、.expandedの最大サイズには画面より大きいサイズを指定できるようになりました。
なお、.expandedでのウィジェットの大きさは、ウィジェットのタイトル部分の大きさに影響されます。タイトルが2行になると、その分ウィジェット本体の高さが減って、全体の大きさが変わらないように調整されます。例えば上記の表で、iPhoneXでは、サイズ10より11の方が最大高さが減っていますが、これは10まではタイトルが1行に収まっていたのが11で2行になった結果です。必ず上記の値になるわけではないのでご注意ください。

iOS11のウィジェットのフォントサイズ

iOS11では、TodayExtentionの内部からUIFont.preferredFont()を呼ぶと、通常のアプリとは異なるサイズのフォントを返してきます。大きすぎるフォントを使わないように配慮されているようです。

iPhone5/SE系:

スタイル 1 2 3 4 5 6 7 8 9 10 11 12
.body 14 15 16 17 19 21 23 28 33 33 33 33
.callout 13 14 15 16 18 20 22 26 32 32 32 32
.caption1 11 11 11 12 14 16 18 22 26 26 26 26
.caption2 11 11 11 11 13 15 17 20 24 24 24 24
.footnote 12 12 12 13 15 17 19 23 27 27 27 27
.headline 14 15 16 17 19 21 23 28 33 33 33 33
.subheadline 12 13 14 15 17 19 21 25 30 30 30 30
.title1 25 26 26 26 27 28 30 36 41 41 41 41
.title2 19 20 20 20 21 22 24 29 35 35 35 35
.title3 19 20 20 20 21 22 24 29 35 35 35 35
.largeTitle 31 32 32 32 33 34 35 42 46 46 46 46

iPhone6系/6Plus系/X:

スタイル 1 2 3 4 5 6 7 8 9 10 11 12
.body 14 15 16 17 19 21 23 28 33 33 33 33
.callout 13 14 15 16 18 20 22 26 32 32 32 32
.caption1 11 11 11 12 14 16 18 22 26 26 26 26
.caption2 11 11 11 11 13 15 17 20 24 24 24 24
.footnote 12 12 12 13 15 17 19 23 27 27 27 27
.headline 14 15 16 17 19 21 23 28 33 33 33 33
.subheadline 12 13 14 15 17 19 21 25 30 30 30 30
.title1 25 26 27 28 30 32 34 38 43 43 43 43
.title2 19 20 21 22 24 26 28 34 39 39 39 39
.title3 17 18 19 20 22 24 26 31 37 37 37 37
.largeTitle 31 32 33 34 36 38 40 44 48 48 48 48

対応例

私の場合は、ウィジェットに置くのは、短い説明文とボタン(タップでアプリを起動)という単純な内容だったので、サイズが大きくなるのを防ぎたいと思って詳細を調べたのですが、結局、できない事がわかりました。
仕方がないので上記の画像のように、.compactにのみ対応し、せめてボタンがはみ出さないようにしました。みっともないですが、仕方がありません。

その他

iOS9でのDynamic Type対応について

Today Extensionでは、iOS9であっても、NSNotification.Name.UIContentSizeCategoryDidChangeによる文字サイズの変更通知の取得は必要ありません。設定アプリへ行って、Todayビューへ戻ってきた時にまた最初から実行されるので、結果的に、新しい設定を反映した画面を作ることになります。

終わりに

ウィジェットの大きさがDynamic Typeと連動する事についての記述は他にみたことがなく、仕方なく記事を書くことにしました。たまたま私が調べた範囲ではそうだったという以上の根拠はありませんが、実際に、このように動いていると思います。
Appleの公式アプリや、Googleのアプリは対応しているので、当たり前の仕様なのかもしれません。
本気で対応すると、かなり厄介な仕様になっていると思いますが、ウィジェットのサイズは強制的に適用されてしまい、逃げられません。サイズが変わっても問題がないようにみなさんのアプリもご確認ください。

アクセシビリティに対応したアプリが増える事を望みます。
誤解などあれば、ご教示頂けると記事を書いた甲斐があります。遠慮なくお願いいたします。

参考文献

App Extention Programming Guide - Today
iOS Human Interface Guideline - Widgets

4
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3