読者です 読者をやめる 読者になる 読者になる

メモを揉め

お勉強の覚書。

swift_combination を Swift 3.0 に対応させました

プログラミング Swift Ruby

Xcode 8.0 がリリースされていたので早速インストールして Swift 3.0 を試してみた。
拙作のライブラリ swift_combination を開いてみると既存のコードを Swift 3.0 のシンタックスにコンバートするか聞かれたので、とりあえずつっこんでみる。

memowomome.hatenablog.com

下のコードは配列の長さを指定して 0 で埋めるという処理だが、微妙に API の名前が変わってたりした。

var indices = [Int](count: length, repeatedValue: 0) // before
var indices = [Int](repeating: 0, count: length)     // after

もう一箇所は for in を使ってる箇所で enumerateenumerated に変更されている。

for (i, slct) in unselected.enumerate() { // before
  ...
}
for (i, slct) in unselected.enumerated() { // after
  ...
}

細かい API の変更は全部これで直してくれるっぽい。

手動で直した箇所

ビルドしてみると以下の2点で警告が出た。

第一引数をラベル無し引数で渡す場合は明示的に指定する必要がある

Swift 3.0 では基本的に引数のラベルは省略しない事になったらしい。

let combos = combination([0, 1, 2], length: 2)      // before
let combos = combination(arr: [0, 1, 2], length: 2) // after

ライブラリはこれに合わせて全部ラベル付きで呼び出すように修正したけど、ラベルを省略したコードをそのまま動かしたい場合は、関数定義の引数ラベルの前に _ を付ければ一応オッケーらしい。

public func combination<T>(arr:[T], length:Int? = nil) -> [[T]] {   // before
  ...
}
public func combination<T>(_ arr:[T], length:Int? = nil) -> [[T]] { // after
  ...
}

関数の戻り値を使用しない場合は明示的に指定する必要がある

関数定義の方で戻り値を指定している場合、それを使わないと警告が出る。
_ = myFunc() のように _ への代入を書くか、関数定義の上に @discardableResult を付けることで警告を消すことができた。

_combination(arr: arr, length: _len){ ret.append($0) }     // 警告が出る
_ = _combination(arr: arr, length: _len){ ret.append($0) } // 警告が消える

@discardableResult // これがあれば戻り値を破棄出来る
internal func _combination<T>(arr:[T], length:Int, process:([T]) -> ()) -> [T] {
  ...
}

とりあえず @discardableResult を関数定義に付ける方向で対応することにした。

flexbox-grow, flex-shrink, flex-basis について

Flexbox HTML Tips デザイン プログラミング CSS

flex-grow CSS プロパティは、flex アイテムの flex grow factor を指定します。これは、アイテムが flex コンテナ内のスペースをどれだけ占有するかを指定します。
MDN: flex-grow

MDN の説明がシンプルすぎてよく分からなかったので、もう少し詳しく調べた内容をまとめておきます。
説明中に出てくるスクショの動作サンプルは下の記事にあります。

memowomome.hatenablog.com

この記事は上の動作サンプル記事が長くなっため分離した補足記事みたいな感じです。

flexbox の基本

flexbox の基本的な動作は以下のページが個人的には分かりやすかったのでオススメです。

flex-grow

ある flex アイテムが、 flex コンテナ内の他の flex アイテムと比較して、どのくらい大きくなろうとするかを整数値で指定する。

W3Cflex grow の説明にはこうある。

This component sets flex-grow longhand and specifies the flex grow factor, which determines how much the flex item will grow relative to the rest of the flex items in the flex container when positive free space is distributed. When omitted, it is set to 1.
W3C: flex-grow

意味としては、

この数値は、ポジティブなフリースペースが分配された時に、 flex アイテムが flex コンテナ内の他の flex アイテムと比較してどれくらい増大されるかを決定します。

ということだと思う。

flex-grow はあくまでも余ったスペースを各アイテムで分け合う。
分配されるのは領域全体ではなく、余ったスペース(以下ポジティヴなフリースペース)。
ある領域を1:2:3の比率で分割したいとして、単純に flex-grow をそれぞれ1,2,3と指定しても上手くいかない。

上は width60px, 120px, 180px に指定した display: block の要素。
下は flex-grow1, 2, 3 に指定した display: flex の要素。

f:id:alluser:20160902002526p:plain f:id:alluser:20160902002402p:plain

上と下を見比べると、下の比率が1:2:3になっていないことが分かる。

下の図について細く見ていくと、余ったスペースが各アイテムに対してどのように分配されているのかが分かる。

flex コンテナの width360px
flex アイテムの widthautoflex アイテム内のコンテンツは40pxspan 要素なので、 flex アイテムの幅は40pxとなる。

flex-grow が分け合うポジティヴなフリースペースとは、 flex コンテナから flex アイテムの幅を引いた値。360px - (40px + 40px + 40px) = 240pxのことを指す。

この240pxflex-grow の値1:2:3の比率で分配し、 width に足した合わせた値が最終的な幅となる。

(40 + 1/6 * 240) + (40 + 2/6 * 240) + (40 + 3/6 * 240)
80 + 120 + 160

この例では比率が2:3:4になる。

flex-shrink

ある flex アイテムが、 flex コンテナ内の他の flex アイテムと比較して、どのくらい小さくなろうとするかを整数値で指定する。

W3Cflex-shrink の説明にはこうある。

This component sets flex-shrink longhand and specifies the flex shrink factor, which determines how much the flex item will shrink relative to the rest of the flex items in the flex container when negative free space is distributed.
W3C: flex-shrink

意味としては、

この数値は、ネガティブなフリースペースが分配された時に、 flex アイテムが flex コンテナ内の他の flex アイテムと比較してどれくらい縮小されるかを決定します。

ということだと思う。

ネガティヴなフリースペースというが分かりにくい。
flex コンテナの幅と flex アイテムの幅の差がポジティヴかネガティヴかというふうに考えると分かりやすいかもしれない。

flex-grow の時と逆の状況を考える。
flex アイテムの width160px にして、 flex コンテナからはみ出すようにしてみる。

  • flex アイテムを全て足した幅が flex コンテナより小さい
    360px - (40px + 40px + 40px) = 240px = ポジティブなフリースペース。
  • flex アイテムを全て足した幅が flex コンテナより大きい
    360px - (160px + 160px + 160px) = -120px = ネガティヴなフリースペース

flex-shrink はこのネガティヴなフリースペースをどう分配するか、ということを指定する。

下のスクショは、
上が display: block + float: leftwidth: 120px の要素。
下が flex-shrink1,2,3 に指定した display: flex の要素となっている。

f:id:alluser:20160902003856p:plain f:id:alluser:20160902003912p:plain

ネガティヴなフリースペース -120px を、 flex-shrink の値1:2:3の比率で分配すると、

(160 + 1/6 * -120) + (160 + 2/6 * -120) + (160 + 3/6 * -120)
140 + 120 + 100

flex アイテムはそれぞれ140px120px100pxとなり、比率は7:6:5となる。

flex-basis

flex-basis についてはまだ仕様が不安定な印象です。
とりあえず MDN の説明を引用。

flex-basis CSS プロパティは、flex アイテムの初期 main size である flex basis を指定します。box-sizing を使用して別に指定されていない限り、このプロパティが content-box の寸法を定義します。
MDN: flex-basis

要はベースとなる widthheight を指定するということだと思う。
box-sizing の影響を受けるので、 borderpadding を含めた値を指定したい場合は box-sizingborder-box にする。
(ただし、IEでは box-sizing の指定に関するバグ有り。 Flexbugs: 7. flex-basis doesn't account for box-sizing:border-box

指定出来る値は widthheight に設定出来る値であれば何でも良い。
あるいは、 content を指定する。

content

結論から書くと、動作がブラウザによってバラバラなので、現段階では使わないほうが良さそうです(2016年9月4日)。

コンテンツサイズに応じてよしなにやってくれる的な設定なんじゃないかーぐらいのことしか分かりませんでした。

W3C の説明も

Indicates automatic sizing, based on the flex item’s content.

と一行だけです。

実際の動作サンプルについては別記事に追記があります。

メモを揉め: Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis

ブラウザ間の差異の原因になっている(2016年9月4日現在)

flex-basisChromeFirefox 、 Edge グループと Safari 、 IE11 グループとで動作が異なる。

ChromeFirefox 、 Edge はコンテンツのサイズを維持。 Safari 、 IE11 はコンテンツサイズを無視する。

こちらも詳しくは別記事に記述があります。

メモを揉め: Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis

その他の考慮すべきこと

ここまでの flex-growflex-shrink の動作は、余った、あるいは足りないスペースを flex-growflex-shrink に基いて分配するという、ある意味シンプルなルールです。
しかし、場合によっては様々な要因によって結果が予想しにくくなることがあります。

再計算が必要になる場合

フリースペースの分配が行われた後、何らかの原因によって再びネガティブスペースを作ってしまった場合、再計算が行われます。

0以下になる場合

flex コンテナの幅が 360px
flex アイテムの flex-shrink123
width400px
min-wditn0

に設定されていた場合、下のスクショの様な結果になる。

f:id:alluser:20160905002955p:plain

順を追って見ていくと、

  1. flex コンテナから flex アイテム3つの合計を引く
    360 - (400 + 400 + 400) = -840
  2. ネガティブなフリースペース -840pxflex-shrink に基いて分配する
    (400 - 1/6 * 840) + (400 - 2/6 * 840) + (400 - 3/6 * 840)
  3. 3つ目の flex アイテムが 0 を下回ってしまう
    (260) + (120) + (-20)
  4. 再び生まれたネガティブなフリースペース -20px の再分配が行われる
    (260 - 1/3 * 20) + (120 - 2/3 * 20) + (0)
  5. 最終的なサイズが決定する
    253.3333... + 106.6666... + 0

コンテンツサイズを下回る場合

ChromeFirefox 、 Edge の flex-basis の挙動により起こる現象。

flex コンテナの幅が 360px
flex アイテムの flex-grow156
flex-basis0

に設定されていた場合、下のスクショの様な結果になる。

f:id:alluser:20160905005623p:plain

順を追って見ていくと、

  1. flex コンテナから flex アイテム3つの合計を引く
    360 - (0 + 0 + 0) = 360
  2. ポジティブなフリースペース 360pxflex-grow に基いて分配する
    (0 + 1/12 * 360) + (0 + 5/12 * 360) + (0 + 6/12 * 360)
  3. 1つ目の flex アイテムがコンテンツサイズの 40px を下回ってしまう
    (30) + (150) + (180)
  4. 1つ目の 40px を除いた 320px が再びポジティブなフリースペースとして再分配される (40) + (0 + 5/11 * 320) + (180 + 6/11 * 320)
  5. 最終的なサイズが決定する
    40 + 145.4545... + 174.5454...

margin, paddingを下回る場合

marginpaddingflex-growflex-shrink によって増大も縮小もしないため、フリースペースの分配結果がこれらを下回る場合は再分配が行われる。

flex コンテナの幅が 360px
flex アイテムの flex-shrink123
width160px
左右の padding40px
min-width0

に設定されていた場合、下のスクショの様な結果になる。

f:id:alluser:20160828223132p:plain

  1. flex コンテナから flex アイテム3つの合計を引く
    360 - (240 + 240 + 240) = -360
  2. ポジティブなフリースペース 360pxflex-grow に基いて分配する
    (240 - 1/6 * 360) + (240 - 2/6 * 360) + (240 - 3/6 * 360)
  3. 3つ目の flex アイテムが左右の padding の合計 80px を下回ってしまう
    (180) + (120) + (60)
  4. 再び生まれたネガティブなフリースペース -20px の再分配が行われる
    (180 - 1/3 * 20) + (120 - 2/3 * 20) + (80)
  5. 最終的なサイズが決定する
    173.3333... + 106.6666... + 80

それでも解決しない場合

再分配しても以下の様に、

  • 0 を下回る
  • min-width を下回る
  • margin, paddingを下回る
  • コンテンツサイズを下回る

に当てはまる場合は、サイズが下回らない様にコンテナからはみ出し、分配はそこで終わりとなる。

flex コンテナの幅が 360px
flex アイテムの flex-shrink123
width0px
左右の padding70px

に設定されていた場合、下のスクショの様な結果になる。

f:id:alluser:20160906004256p:plain

左右の padding の合計 140px より縮小することが出来ないため、 flex コンテナからはみ出る。

追記(2016年9月6日)Firefoxの表記について

FireFox から Firefox に変更しました。

Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis

CSS HTML Tips デザイン プログラミング Stylus Jade JavaScript Flexbox

flexboxのプロパティの組み合わせを比較できるカタログが欲しいなーと思ったので作ってみました。
各プロパティーの値がどのように解釈されるか、ブラウザ間での違いなどが分かります。

あれもこれもと入れていたら無駄に長くなりました。

See the Pen Case Studies of Flexbox by Keita Okamoto (@all-user) on CodePen.

縦のストライプの幅が10pxflexコンテナの幅が360pxとなっていて、flexアイテムとそのコンテンツの大きさはそれぞれのラベルに表示されます。

コンテナの上には当てているスタイルの要約と最終的なサイズを割り出す計算式があります。
全ブラウザで結果が同じ場合は緑、ブラウザ間で結果に差異がある場合は赤の文字になっています。

f:id:alluser:20160828194455p:plain f:id:alluser:20160828194518p:plain

一応仕様的にはこうなるべきだろうという計算式を書いているつもりですが、解釈が間違っているかも知れません。
なんか違うなと思ったら教えていただけると助かります。

ベストプラクティス(暫定)

flexboxの各アイテムが最終的にどのサイズで描画されるのかは領域の分配ルールに様々な要素が絡んでいます。
そのため、適当にプロパティを組み合わせていくと、なかなか思い通りのサイズになりません。

スコープを絞るために話をflex-direction: rowに限定するとして、サイズの計算に影響を与えるプロパティはこれだけあります。

  • flex-shrink
  • flex-grow
  • flex-basis
  • width
  • min-width
  • max-width
  • margin
  • padding
  • box-sizing

さらにブラウザ間の誤差やflexboxの抱える様々なバグを考慮して全てをコントロールするのはかなり難しいと思われます。(この実験に関して言えば、実際にはブラウザの誤差はあるひとつの挙動に起因していたため、そこまで辛くなかった。後述)

色々試した結果、暫定的なベストプラクティスとして以下のルールを守るのが良さそうだという結論になりました。

  • flex-basisauto(初期値)
  • width0に設定する(ただしflex-direction: column + heightの時は要注意。下に追記あり)
    f:id:alluser:20160828222041p:plain
  • width0以外にしたい場合はmin-widthをそれより小さい値で設定する
    f:id:alluser:20160828223132p:plain

min-widthを設定すればflex-basisでもブラウザ間の差異は無くなりますが、あえてflex-basisを使うメリットも特に無さそうなのでwidthを使うのが良いかなと思ってます。

  • flex-basisとmin-widthを組み合わせた例
    f:id:alluser:20160828223924p:plain

あくまで厳密にサイズをコントロールしたい時のベストプラクティスです。(flexboxでテーブルみたいに縦を揃えたい、とか)
コンテンツのサイズが予測できたり、サイズに応じてよしなにやって欲しい時はwidthflex-basisともにauto(必要に応じてmax-widthmin-width)でも良いと思います。

幅の計算をするときに気をつけること

paddingmarginの扱い

paddingmarginflex-shrinkflex-growの影響を受けないようです。
常に設定された値になる必要があるので、スペースを分配した結果が設定した値を下回る場合はスペースを再分配する必要があります。

  • 左右のpadding 80pxを下回るため再分配が行われる
    f:id:alluser:20160828223132p:plain

box-sizingの影響

paddingwidthに含まれるかどうかはbox-sizingの値によって決まります。
初期値であるcontent-boxの場合は以下のようになります。

  • width: autoのためコンテンツサイズ分の40px * 3を除く240pxflex-growの値に基いて分配される
    f:id:alluser:20160828231139p:plain

  • コンテンツサイズ分の40px * 3に加え、左右のpadding 40px * 3を除く120pxflex-growの値に基いて分配される
    f:id:alluser:20160828230822p:plain

  • flexアイテムのwidth 160px * 3とコンテナサイズ360pxの差-120pxflex-shrinkに基いて分配される
    f:id:alluser:20160828234942p:plain

  • flexアイテムのwidth 160px * 3に左右のpadding 80px * 3を加えた720pxとコンテナサイズ360pxの差-360pxflex-shrinkに基いて分配される
    f:id:alluser:20160828223132p:plain

min-widthの効果

widthの設定を0以上に設定していて、その値よりコンテンツサイズが小さい場合、flex-shrinkはコンテンツサイズを下回って縮小することができなくなります。(flex-basisの場合は0に設定してもコンテンツサイズを下回るサイズにはなれないことがある。後述)

min-widthを設定することで、その値まではコンテンツサイズを下回るサイズに縮小することが出来ます。

  • コンテンツサイズの160pxを下回って縮小することが出来ない(Safari、IE11は縮小する) f:id:alluser:20160829001831p:plain

  • min-width: 0によりコンテンツサイズの160pxを下回って縮小することが出来る
    f:id:alluser:20160829001849p:plain

ブラウザ間の差異(2016年8月28日現在)

この実験に絞って言えばブラウザ間で結果に差異が出る原因は、flex-basisがコンテンツサイズを保証するかどうか、ということに集約できました。

ChromeFirefox、Edgeはコンテンツのサイズを維持。
Safari、IE11はコンテンツサイズを無視する。

コンテンツサイズを保証するというのはどういう事かというと、flex-shrinkflex-growを適用した後に、もしflexアイテムのサイズがコンテンツサイズを下回っていた場合に、そのflexアイテム以外のflexアイテムの間でネガティブなスペースを再分配し、必ずコンテンツサイズを下回らない様にします。(それでも下回る場合はそれ以上縮小しない)

  • 再分配してもコンテンツサイズを下回るのでそれ以上縮小しない例
    f:id:alluser:20160828202921p:plain

どちらが仕様的に正しいのかはわかりませんが、IEを除くとコンテンツサイズを無視するのはSafariだけなのでChromeFirefox、Edgeの動作がより最新の仕様に沿っているのかもしれません。

追記(2016年8月31日)height0指定について

flex-direction: column時にheight0を指定すると、たとえflexアイテムがflex-growによってコンテナいっぱいに広がっていたとしても、flexアイテム内の要素からはheight0に見えてしまうらしく、%指定が出来なくなるというデメリットがあることが分かりました。(Safariのみ)

heightの代わりにmin-height0にすれば大丈夫でした。

  • Safariのスクショ。左: height: 0、右: height: 100% + min-height: 0
    f:id:alluser:20160830235946p:plain
    枠線付きがflexコンテナ、濃いグレーがflexアイテム、薄いグレーがflexアイテム内のブロック要素

http://codepen.io/all-user/pen/RGbKRd

追記(2016年9月5日)flex-basiscontent について

flex-content に指定できる値 content についての調査を追加しました。

結論から書くとブラウザ間の挙動がバラバラ。
今はまだ使わないほうが良さそうです。

以下、実験で分かったこと。

ショートハンドが効かない(Chrome, Firefox, Safari, IE11)

ショートハンドで指定した flex-growflex-shrink がなぜか無効になります。(Edge は有効)
flex-grow0Chrome, Firefox, Safari, IE11)、 flex-shrink1Chrome, Firefox, Safari)と解釈されているように見えます。

ロングハンドの指定は有効でした。

  • flex-grow
    • flex-basiscontent
      flex-grow123 をショートハンドで指定
      f:id:alluser:20160905233121p:plain
    • flex-basiscontentcontentauto
      flex-grow123 をショートハンドで指定
      f:id:alluser:20160905234039p:plain
    • flex-basiscontent
      flex-grow123 をロングハンドで指定
      f:id:alluser:20160905235419p:plain
  • flex-shrink
    • flex-basiscontent
      min-width0
      flex-shrink123 をショートハンドで指定
      f:id:alluser:20160906001341p:plain
    • flex-basiscontentcontentauto
      min-width0
      flex-shrink123 をショートハンドで指定
      f:id:alluser:20160906001658p:plain
    • flex-basiscontent
      min-width0
      flex-shrink123 をロングハンドで指定
      f:id:alluser:20160906003231p:plain

追記(2016年9月6日)Firefoxの表記について

FireFox から Firefox に変更しました。