6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Swift5]ビリビリ動画にあるようなスクロールできてタッチできるメニューバー

Last updated at Posted at 2020-07-25

はじめに

筆者は中国大手の動画共有サイト「bilibli」のUIを気に入っているのですが、bilibiliで実際に取り入れられている**「スクロールできてタッチできるメニューバー」**を実装したので共有したいと思います。

スクロール操作だけのメニューバーについての記事はよく見かけますが、タッチ操作ができるようなものはあまり見かけないので、そのような実装をしてみたい人にとっては役に立つ記事になるのではないでしょうか。
(タッチ操作を可能にすることで、スクロールメニューバーの選択バーのアニメーションスクロールビューのタッチイベントを可能にしたりなど難易度が少し上がるからでしょうか?)

目次

  • 環境
  • 実行例
  • 考え方
    • 画面構成
    • 実装について
      • UIScrollViewを作成する
      • UIScrollViewにUILabelを貼り付ける
      • UIScrollViewのタッチイベントを受け取れるようにしタッチした際の処理をする
      • UIScrollViewのスクロールイベント関数内でUIScrollViewBarについて処理をする
  • ソースコード
  • 終わりに

環境

  • Xcode 11.2.1
  • Swift5

実行例

samplescrollview.gif

考え方

ロジックはそこまで必要になりませんが知識の幅は必要になるかなと思います。

画面構成

画面の構成は以下のようになります。

Diagram.png

UIView(黄)UIScrollViewUIImageViewUIView(紫)が貼り付けられていて、UIScrollViewUILabelが貼り付けられています。

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を設定していきます。
そしてタッチできるようにisUserInteractionEnabledtrueにします。

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にくっついたように表示することができます)

これは以下の図を見ると理解しやすいと思います。
Untitled Diagram (1).png

次に選択されたメニューの色を変えます。
コード内では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
        }
    }

今回このスクロールイベント関数を呼び出されるScrollView2つあります。
scrollViewmyTableViewですね。
なのでスクロールイベント関数がどっちの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座標にスクロール量を足すだけですね。

参考までにそれを説明した図を再掲しておきます。
Untitled Diagram (1).png

ソースコード

今まで説明したコードを抜粋して載せます。
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)
    }
}

終わりに

今回は動かせるヘッダーとしてスクロールできるメニューバーをくっ付けたので、普通にメニューバーを作るよりも難易度が少し高かったのかなと思います。
しかし今回実装したスクロールできてタッチで選択できるメニューバーはよく見かけるくらい使われる実装だと思うので、理解しておいて損はないはずです。

それにしても、見たものを再現するのは暇つぶしには最適だなぁと思いました。

6
6
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
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?