はじめに
筆者は中国大手の動画共有サイト「bilibli」のUIを気に入っているのですが、bilibiliで実際に取り入れられている**「スクロールできてタッチできるメニューバー」**を実装したので共有したいと思います。
スクロール操作だけのメニューバーについての記事はよく見かけますが、タッチ操作ができるようなものはあまり見かけないので、そのような実装をしてみたい人にとっては役に立つ記事になるのではないでしょうか。
(タッチ操作を可能にすることで、スクロールメニューバーの選択バーのアニメーションやスクロールビューのタッチイベントを可能にしたりなど難易度が少し上がるからでしょうか?)
目次
- 環境
- 実行例
- 考え方
- 画面構成
- 実装について
- UIScrollViewを作成する
- UIScrollViewにUILabelを貼り付ける
- UIScrollViewのタッチイベントを受け取れるようにしタッチした際の処理をする
- UIScrollViewのスクロールイベント関数内でUIScrollViewBarについて処理をする
- ソースコード
- 終わりに
環境
- Xcode 11.2.1
- Swift5
実行例
考え方
ロジックはそこまで必要になりませんが知識の幅は必要になるかなと思います。
画面構成
画面の構成は以下のようになります。
UIView(黄)
にUIScrollView
とUIImageView
とUIView(紫)
が貼り付けられていて、UIScrollView
にUILabel
が貼り付けられています。
UILabel
がメニューバーのメニューとなっていて、UIView(紫)
がスクロールメニューバーの選択バーとなっています。
実装について
今回ちょっとしたコツがいるのは以下の3つになります。
ポイント |
---|
スクロールビューをタッチできるようにすること。 |
選択しているメニューの下に表示するバーの実装。 |
二つのスクロールビュー操作をひとつのスクロールイベント関数で処理わけする。 |
また、全体的な処理の流れは以下になります。
順番 | 処理内容 |
---|---|
1 | UIScrollView、UIView(スクロールビューのバー)を作成する |
2 | UIScrollViewにUILabelを貼り付ける |
3 | UIScrollViewのタッチイベントを受け取れるようにしタッチした際の処理をする |
4 | UIScrollViewのスクロールイベント関数内でUIScrollViewBarについて処理をする |
1.UIScrollViewを作成する
まずはUIScrollViewを作成しましょう。
private func createHeaderView() {
let displayWidth: CGFloat! = self.view.frame.width
// 上に余裕を持たせている(後々アニメーションなど追加するため)
myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
myHeaderView.alpha = 1
myHeaderView.backgroundColor = UIColor(red: 142/255, green: 237/255, blue: 220/255, alpha: 1)
myTableView.addSubview(myHeaderView)
scrollView = UIScrollView(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))
scrollView.bounces = false
scrollView.alwaysBounceHorizontal = false
scrollView.alwaysBounceVertical = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.backgroundColor = UIColor(red: 238/255, green: 142/255, blue: 160/255, alpha: 1)
makeScrollMenu(scrollView: &scrollView)
myHeaderView.addSubview(scrollView)
scrollViewBar = UIView(frame: CGRect(x: 0, y: 225, width: 70, height: 5))
scrollViewBar.backgroundColor = UIColor(red: 142/255, green: 237/255, blue: 220/255, alpha: 0.8)
myHeaderView.addSubview(scrollViewBar)
let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
if bilibili {
image.image = UIImage(named: "bili2")
} else {
image.image = UIImage(named: "bili")
}
myHeaderView.addSubview(image)
}
scrollView
ではBounceしないように設定してます。
また、スクロールバーも非表示の設定にしてます。
makeScrollMenu(scrollView: &scrollView)
でラベルを貼り付けていきます。
スクロールビューの選択バーもここで作成しています。(ScrollViewBar: UIView
)
動くヘッダービューに関してはこちらの記事では説明しません。興味のある人はこちらをご覧ください。
2.UIScrollViewにUILabelを貼り付ける
UIScrollViewにUILabelを貼り付けるのは以下のように表現されます。
func makeScrollMenu(scrollView: inout(UIScrollView)) {
let menuLabelWidth:CGFloat = 70
let titles = Data.TitleMenu
let menuLabelHeight:CGFloat = scrollView.frame.height
var X: CGFloat = 0
var count = 1
for title in titles {
let scrollViewLabel = UILabel()
scrollViewLabel.textAlignment = .center
scrollViewLabel.frame = CGRect(x:X, y:0, width:menuLabelWidth, height:menuLabelHeight)
scrollViewLabel.text = title
scrollViewLabel.isUserInteractionEnabled = true
scrollViewLabel.tag = count
scrollView.addSubview(scrollViewLabel)
X += menuLabelWidth
count += 1
scrollViewLabelArray.append(scrollViewLabel)
}
changeColorScrollViewLabel(tag: 1)
scrollView.contentSize = CGSize(width:X, height:menuLabelHeight)
}
UILabel
の高さはUIScrollView
と同じ高さに、横幅は好きな大きさに設定します。
ラベル同士を被らないように並べていくのは横幅分ずらして設置していくだけです。
また、タッチした際に識別できるようにtag
を設定していきます。
そしてタッチできるようにisUserInteractionEnabled
をtrue
にします。
changeColorScrollViewLabel(tag: Int)
で選択されているラベルの色を変えます。
scrollViewLabelArray: [UILabel]
に作成したラベルを追加していますが、それはchangeColorScrollViewLabel()
で使うためです。
3.UIScrollViewのタッチイベントを受け取れるようにしタッチした際の処理をする
UIScrollView
はそのままではタッチイベントを受け取れません。
なのでextension
でタッチイベントを受け取れるように以下のように記述します。
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.next?.touchesBegan(touches, with: event)
}
これでスクロールビューからタッチイベントを取得できるようになったので、次はタッチイベント関数での処理を記述していきましょう。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.next?.touchesBegan(touches, with: event)
for touch: AnyObject in touches {
let t: UITouch = touch as! UITouch
guard t.view is UILabel else {
return
}
switch t.view!.tag {
case 1:
print(1)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 1)
Data.setIndex(v: 0)
myTableView.reloadData()
case 2:
print(2)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 2)
Data.setIndex(v: 1)
myTableView.reloadData()
case 3:
print(3)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 3)
Data.setIndex(v: 2)
myTableView.reloadData()
case 4:
print(4)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 4)
Data.setIndex(v: 3)
myTableView.reloadData()
case 5:
print(5)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 5)
Data.setIndex(v: 4)
myTableView.reloadData()
case 6:
print(6)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 6)
Data.setIndex(v: 5)
myTableView.reloadData()
default:
break
}
}
}
はじめにguard t.view is UILabel else {return}
でUILabel
以外のタッチに対しては無視するようにしています。
状況によって適切な条件を用意してあげてください。
それ以降はSwitch文
でラベルのtag
で場合分けしています。
では、ラベルをタッチした際に行う処理を見ていきましょう。
はじめにUIView.animate()
でscrollViewBar
に対してアニメーション処理をしているのがわかりますね。
これは選択バーの位置を選択されたメニューの位置に動かすような操作をしています。
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
上のコードのように選択されたラベルのx座標からスクロールビューの現在のx座標を引いた値をスクロールビューバーのx座標にしています。
(これによりスクロールビューがどのような位置にあってもUILabel
にくっついたように表示することができます)
次に選択されたメニューの色を変えます。
コード内ではchangeColorScrollViewLabel(tag: Int)
を呼び出して色を変えています。
この関数は以下のように表現されています。
private func changeColorScrollViewLabel(tag: Int) {
for label in scrollViewLabelArray {
if label.tag == tag {
label.textColor = UIColor(red: 142/255, green: 237/255, blue: 220/255, alpha: 1)
} else {
label.textColor = .white
}
}
}
最後にTableView
で使用するデータを切り替える処理を行います。
Data.setIndex(v: Int)
でデータを切り替え、myTableView.reloadData()
で更新していますね。
Data
は構造体data
のインスタンスです。
構造体data
は以下のように表現されています。
struct data {
let TitleMenu = ["アニメ","ドラマ","映画","ニュース","漫画","生放送"]
var index = 0
mutating func setIndex(v: Int) {
index = v
}
func getTitle() -> String {
return TitleMenu[index]
}
}
4.UIScrollViewのスクロールイベント関数内でUIScrollViewBarについて処理をする
スクロールイベント関数は以下のように表現されています。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
switch scrollView {
case self.scrollView:
scrollingViewBar(scrollView: scrollView)
case self.myTableView:
scrollingMyTableView(scrollView: scrollView)
default:
break
}
}
今回このスクロールイベント関数を呼び出されるScrollView
は2つあります。
scrollView
とmyTableView
ですね。
なのでスクロールイベント関数がどっちのScrollView
によって呼び出されたものなのかを判断して処理を分けてあげないと不具合が起きてしまうことがあります。(実は筆者はここでハマりました。笑)
ではscrollView
がスクロールされた際に呼び出されている関数scrollingViewBar
を見ていきましょう。
private func scrollingViewBar(scrollView: UIScrollView) {
scrollViewBar.frame.origin.x += (lastContentOffsetX - scrollView.contentOffset.x)
lastContentOffsetX = scrollView.contentOffset.x
}
lastContentOffsetX
は前に呼び出された際のスクロールビューの位置を保存しています。
lastContentOffsetX - scrollView.contentOffset.x
でスクロール量を求めることができます。
scrollViewBar
をスクロールした分だけ横にずらせればいいので、scrollViewBar
のx座標にスクロール量を足すだけですね。
ソースコード
今まで説明したコードを抜粋して載せます。
Githubにサンプルを載せておくので参考にしてみてください。
https://github.com/Hajime-Ito/SampleScrollMenuBar
var scrollView: UIScrollView!
var scrollViewBar: UIView!
var myHeaderView: UIView!
var lastContentOffset: CGFloat = 0
var lastContentOffsetX: CGFloat = 0
var scrollViewLabelArray: [UILabel] = []
private func createHeaderView() {
let displayWidth: CGFloat! = self.view.frame.width
// 上に余裕を持たせている(後々アニメーションなど追加するため)
myHeaderView = UIView(frame: CGRect(x: 0, y: -230, width: displayWidth, height: 230))
myHeaderView.alpha = 1
myHeaderView.backgroundColor = UIColor(red: 142/255, green: 237/255, blue: 220/255, alpha: 1)
myTableView.addSubview(myHeaderView)
scrollView = UIScrollView(frame: CGRect(x: 0, y: 200, width: displayWidth, height: 30))
scrollView.bounces = false
scrollView.alwaysBounceHorizontal = false
scrollView.alwaysBounceVertical = false
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
scrollView.backgroundColor = UIColor(red: 238/255, green: 142/255, blue: 160/255, alpha: 1)
makeScrollMenu(scrollView: &scrollView)
myHeaderView.addSubview(scrollView)
scrollViewBar = UIView(frame: CGRect(x: 0, y: 225, width: 70, height: 5))
scrollViewBar.backgroundColor = UIColor(red: 142/255, green: 237/255, blue: 220/255, alpha: 0.8)
myHeaderView.addSubview(scrollViewBar)
let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
if bilibili {
image.image = UIImage(named: "bili2")
} else {
image.image = UIImage(named: "bili")
}
myHeaderView.addSubview(image)
}
private func updateHeaderView() {
let displayWidth: CGFloat! = self.view.frame.width
self.myHeaderView.subviews[2].removeFromSuperview()
let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
if bilibili {
image.image = UIImage(named: "bili2")
} else {
image.image = UIImage(named: "bili")
}
myHeaderView.addSubview(image)
}
func addHeaderViewGif() {
let displayWidth: CGFloat! = self.view.frame.width
let image = UIImageView(frame: CGRect(x: (displayWidth-100)/2, y: 100, width: 100, height: 100))
if bilibili {
image.loadGif(name: "bili2")
} else {
image.loadGif(name: "bili")
}
myHeaderView.addSubview(image)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.myHeaderView.subviews[2].removeFromSuperview()
}
}
func makeScrollMenu(scrollView: inout(UIScrollView)) {
let menuLabelWidth:CGFloat = 70
let titles = Data.TitleMenu
let menuLabelHeight:CGFloat = scrollView.frame.height
var X: CGFloat = 0
var count = 1
for title in titles {
let scrollViewLabel = UILabel()
scrollViewLabel.textAlignment = .center
scrollViewLabel.frame = CGRect(x:X, y:0, width:menuLabelWidth, height:menuLabelHeight)
scrollViewLabel.text = title
scrollViewLabel.isUserInteractionEnabled = true
scrollViewLabel.tag = count
scrollView.addSubview(scrollViewLabel)
X += menuLabelWidth
count += 1
scrollViewLabelArray.append(scrollViewLabel)
}
changeColorScrollViewLabel(tag: 1)
scrollView.contentSize = CGSize(width:X, height:menuLabelHeight)
}
private func changeColorScrollViewLabel(tag: Int) {
for label in scrollViewLabelArray {
if label.tag == tag {
label.textColor = UIColor(red: 142/255, green: 237/255, blue: 220/255, alpha: 1)
} else {
label.textColor = .white
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.next?.touchesBegan(touches, with: event)
print("touch")
for touch: AnyObject in touches {
let t: UITouch = touch as! UITouch
guard t.view is UILabel else {
return
}
switch t.view!.tag {
case 1:
print(1)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 1)
Data.setIndex(v: 0)
myTableView.reloadData()
case 2:
print(2)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 2)
Data.setIndex(v: 1)
myTableView.reloadData()
case 3:
print(3)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 3)
Data.setIndex(v: 2)
myTableView.reloadData()
case 4:
print(4)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 4)
Data.setIndex(v: 3)
myTableView.reloadData()
case 5:
print(5)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 5)
Data.setIndex(v: 4)
myTableView.reloadData()
case 6:
print(6)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [],animations: {
self.scrollViewBar.frame.origin.x = t.view!.frame.origin.x - self.scrollView.contentOffset.x
}, completion: nil)
changeColorScrollViewLabel(tag: 6)
Data.setIndex(v: 5)
myTableView.reloadData()
default:
break
}
}
}
}
private func scrollingViewBar(scrollView: UIScrollView) {
scrollViewBar.frame.origin.x += (lastContentOffsetX - scrollView.contentOffset.x)
lastContentOffsetX = scrollView.contentOffset.x
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
switch scrollView {
case self.scrollView:
scrollingViewBar(scrollView: scrollView)
case self.myTableView:
scrollingMyTableView(scrollView: scrollView)
default:
break
}
}
//MARK: --
extension UIScrollView {
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.next?.touchesBegan(touches, with: event)
}
}
終わりに
今回は動かせるヘッダーとしてスクロールできるメニューバーをくっ付けたので、普通にメニューバーを作るよりも難易度が少し高かったのかなと思います。
しかし今回実装したスクロールできてタッチで選択できるメニューバーはよく見かけるくらい使われる実装だと思うので、理解しておいて損はないはずです。
それにしても、見たものを再現するのは暇つぶしには最適だなぁと思いました。