[iOS]UIStackViewのアニメーションが変!


世間話

今更UIStackViewと戯れているんですが、ちょいちょいわけわからん動きをしますね。
UITableViewと格闘して大嫌いになった人達がUIStackViewを絶賛していましたが、彼らは大丈夫だったんでしょうか。こっちも結構曲者ですよ・・・

Hide/Showアニメーションが変

UIStackViewの良いところは、なんと言ってもisHiddenでViewの表示/非表示が簡単にできるところですよね。
iOSでもAndroidのgoneが使いたいんだ!と何度思ったことが。

ですが、表示/非表示のAnimationをしようとしたところ、変な動きになりました。

// コードのイメージ
UIView.animate(withDuration: 0.3) {
    self.stackView.arrangedSubviews[index].isHidden.toggle()
    self.stackView.layoutIfNeeded()
}

一番上と一番下、お前らなんなん!?
ここらへん、ググっても全然出てきませんでした。

ちなみに条件は

  • Alignment Fill
  • Distribution Fill
  • Spacing 0
  • 各Viewにheightのconstraint

ああ、え、そういうこと?

半透明にしたらわかりました。

例えば3番目のViewをhiddenにしようとすると、4番目以降のViewが上にせり上がってきて、4番目のViewが3番目のViewに達したあとで3番目のViewが見えなくなります。
何で?(殺意)
これで最下部のViewの動きに合点がいきます。
最上部のViewの動きは何かルールが違っている気がしますが。。

意図した動きにするにはどうすればいいか?

この動きは非常に厄介です。
例えばViewが2つのStackViewを作って、上部をタイトル情報、下部に詳細情報にして、タップしたら開くとか、そういう基本的な動きすらアニメーションができなくなります。

そうだ、ClipToBoundsだ!

天啓を得てUIStackViewのClipToBoundsをtrueにしてみました。
何の成果も得られませんでした。
何で?(殺意)

解1:親Viewを作って、親ViewをClipToBoundsにする

上下左右をAutoLayoutでつないでやります。
これでいけました。

解2:高さのconstraintのPriorityを999にする

ちょっと隠れ方の挙動が違いますがこれでもいけました。
何で?(殺意)

すいませんが、これまだ理解できていません。
1000ならダメで、999ならいけるんです。

別の条件でも試してみる

DistributionをFillEqualyにしてみる①

UIStackView自体の高さを縛りました。

予想通りですが若干キモいですね、使い所が限られそう?

DistributionをFillEqualyにしてみる②

今度は一番上のViewの高さを縛ってみました

良さそうな動きをしますが、基準となる一番上のViewをhiddenにしたところ壊れてしまいました。
どのように高さを設定するか考える必要が出てきそうです。
確実に表示するViewがあればいいんですが。

Spacingがついていたらどうなる?①

Spacingをつけて、各Viewの高さを縛りました。

ああっ!そうなっちゃいますよね。
これはいけません。

Spacingがついていたらどうなる?②

UIStackViewの高さを縛ってみました。

これはちょっと使えそうですね。

Spacingがついていたらどうなる?③

動いてから消えるのが良くないので、消してから動かしてみましょう。
isHiddenは使えないので透明度をいじっています。

手品っぽくなりました。これはギリギリよさそうです。

UITableViewと連携する

やっぱり同じセルが増えてくるとTableViewに頼らざるを得なくなってきます。
UITableViewCellにUIStackViewを入れて、アニメーションしてみましょう。
AutoLayoutは上下左右をひっつけます。

何で?(殺意)

ちなみにアニメーションは

tableView.beginUpdates()
tableView.endUpdate()

こうか

UIView.animate(withDuration: 1.0) {
    tableView.beginUpdates()
    tableView.endUpdate()
}

こうです

さっきは、動いてから消えましたよね。
何で今回は消えてから動くんですか????

5分ぐらい考えてみましたがわかりません。保留です。

self sizingを諦める

しょうがないので、UITableView.automaticDimensionを諦めてみます。

UIStackViewとCellを、上左右のみ設定して、UIStackViewの高さのコントロールは上でやったとおりにします。
そしてCellの開閉アニメーションはオーソドックスにheightを変更して行います。

できました。

しかしself sizing好きなので、これには私も憤慨です。
特に今の高さではなくて、1個のViewをhidden/showしたあとの高さを求めるのがだるくてしょうがないです。

一応作りましたけど。スマートではないですね。

func expectedHeight(when viewOfIndex:Int, isHidden:Bool) -> CGFloat {
    let height = arrangedSubviews.filter { !$0.isHidden }.reduce(0, { $0 + $1.frame.height })
    if isHidden && !arrangedSubviews[viewOfIndex].isHidden {
        return height - arrangedSubviews[viewOfIndex].frame.height
    }
    if !isHidden && arrangedSubviews[viewOfIndex].isHidden {
        return height + arrangedSubviews[viewOfIndex].frame.height
    }
    return height
}

おわりに

内部でAutoLayoutがどうなってるのか見えにくいので、わからないことが多すぎます。
一個一個知識や経験として仲良くなっていくしか無い気がします。

闇が深い・・・

間違いや備考などあったら後ほど追加します。

次回

UIStackViewってちょいちょい言うこと聞かないですよね。
それで「私の本当に欲しかったStackView」を作ったのでその話をします。
→ 何か使ってみるとイマイチパッとしなかったのでやめます

よりリッチなものは既にライブラリが存在します。すばらしいです、よく作りますねこんなの。
ただちょっとヘビー?

StackViewController
https://github.com/seedco/StackViewController
AloeStackView
https://github.com/airbnb/AloeStackView