UILabelやUITextViewなどのテキストを自動スクロールする方法
UIScrollView上のテキストを自動的にスクロールさせる
UIScrollViewやUITableViewは標準的なスクロール機能を持った非常に便利なUIツールです。
しかしAuto Layoutで制約を付けるときには、外側の制約と、内側の制約とを考える必要があるので、少しややこしくなったりします。
画面上の見た目のサイズを決めるのが外側で、UIScrollView.frameと関連づきます。
スクロールする中のコンテンツのサイズを決めるのが内側で、UIScrollView.contentSizeと関連づきます。
そして、スクロールする中のコンテンツのサイズは、テキスト量に応じて変わります。
テキスト量が外側のサイズを超えない程度であれば、スクロールはしません。
逆にテキスト量が多ければ多いほど、スクロールする長さは伸びます。
あらためて上の図のようなStoryboardの例で説明します。
下3分の2くらいの領域にUIScrollViewを置いています。
その中にUILabelを置いています。
UIScrollViewの外側のサイズは赤枠のとおりです。
それに対し、UIScrollViewの内側のサイズ(contentSize)は青枠のとおりです。 この青枠と同じサイズで黄色枠のUILabelが上に乗っている状態です。逆に言うと、黄色枠のUILabelのテキスト量に応じて、青枠のサイズが決まるということです。
ですので、今回の場合は青と黄色の上下左右が動的にぴったりになるように、Constraintsを設定していま す。
尚、制約上はこれだけでは不十分で警告がでます。青と黄色の上下左右に制約を設定と言いましたが、実際は更に幅と高さも青枠と同じという制約を加える必要があります。
コード的には以下のイメージです。
scrollView.leftAnchor.constraint(equalTo: outView.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: outView.rightAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: outView.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: outView.bottomAnchor).isActive = true
//内側は6つの制約
label.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
label.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
label.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
label.heightAnchor.constraint(equalTo: scrollView.contentLayoutGuide.heightAnchor).isActive = true
問題
今回は実際に相談のあった例で、複数行テキストを任意の時間をかけて上から下まで自動スクロールするという課題を解決したいと思います。
手を加えずにスクロールするので、映画のエンドロールのような下からテキストが登っていくようなイメージですね。
歌詞や詩を表示する際の演出にも使えそうな効果で、いろいろ応用できそうです。
テキストのスクロールとのレイアウトは、UIScrollViewとUILabelを使用した上の例でできているので、自動スクロールの制御についてみていきます。
解決方法
UIScrollViewの機能拡張
UIScrollViewをを機能拡張する方法で自動スクロールするプログラムを追加します。
Objective-cのカテゴリという機能を使って、 従来のUIScrollViewを拡張します。
(例) UIScrollView+AutoScroll
@interface UIScrollView (AutoScroll)
- (void)startScroll:(NSInteger)scrollSpeed;
- (void)stopScroll;
@end
@implemantation UIScrollView (AutoScroll)
BOOL isScrolling;
NSInteger autoScrollSpeed;
- (void)startScroll:(NSInteger)scrollSpeed {
isScrolling = YES;
autoScrollSpeed = scrollSpeed;
[self autoScroll];
}
- (void)stopScroll {
isScrolling = NO;
}
- (void)autoScroll {
//後述
}
@end
上でいうscrollSpeedは、時間ではなく移動量です。
例えば10なら、アニメーション1コマあたりの移動にY座標を10pt下げるというイメージです。
このアニメーションを繰り返し、一番下まで移動したら自動スクロールを停めます。
フローをまとめると以下のようなイメージです。
それを踏まえたautoScrollメソッドの中身は以下のようなコードです。
- (void)autoScroll {
//スクロールが停止されている場合処理しない
if (!isScrolling) {
return;
}
//ページの高さを取得
int height = (int)self.contentSize.height;
if (height == 0) {
[self stopScroll];
return;
}
//スクロール範囲を計算
int currentPageMaxScroll = height - self.frame.size.height;
int currentPageOffset = (int)self.contentOffset.y;
//スクロールが一番下まで到達した場合終了
if (currentPageOffset >= currentPageMaxScroll) {
[self stopScroll];
return;
}
//移動先の位置を算出
NSInteger targetOffset = currentPageOffset + autoScrollSpeed;
//スクロール最大値に丸め込み
if (targetOffset > currentPageMaxScroll) {
targetOffset = currentPageMaxScroll;
}
//アニメーション設定
[UIView beginAnimations:@"AutoScroll" context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationDuration:1.0];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(autoScroll)];
self.contentOffset = CGPointMake(self.contentOffset.x, targetOffset);
[UIView commitAnimations];
}
解決のひとつの例ですので、他のアプローチもあります。
あとは、自動スクロールを開始したいときに、startScrollを呼べばよいだけです。
実際に動かしてみると以下のようになります。
追記:Swiftのextensionも付け加えておきます。
class ViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var label: UILabel!
...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scrollView.startScroll(scrollSpeed: 10)
}
}
var isScrolling: Bool = false
var autoScrollSpeed: CGFloat = 10.0
extension UIScrollView {
func startScroll(scrollSpeed: CGFloat) {
isScrolling = true
autoScrollSpeed = scrollSpeed
autoScroll()
}
func stopScroll() {
isScrolling = false
}
@objc private func autoScroll() {
if !isScrolling { return }
let height = self.contentSize.height
if height == 0 {
stopScroll()
return
}
let currentPageMaxScroll = height - self.frame.size.height
let currentPageOffset = self.contentOffset.y
if currentPageOffset >= currentPageMaxScroll {
stopScroll()
return
}
var targetOffset = currentPageOffset + autoScrollSpeed
if targetOffset > currentPageMaxScroll {
targetOffset = currentPageMaxScroll
}
UIView.beginAnimations("AutoScroll", context: nil)
UIView.setAnimationCurve(.linear)
UIView.setAnimationDuration(1.0)
UIView.setAnimationDelegate(self)
UIView.setAnimationDidStop(#selector(autoScroll))
self.contentOffset = CGPoint(x: self.contentOffset.x, y: targetOffset)
UIView.commitAnimations()
}
}