【Swift】UITableViewで下に引っ張ると拡大するヘッダーを実装してみた。

絶賛ブログ強化月間中です。Reoです。

 

今回はSwiftでUITableViewを用いて、下に引っ張ると拡大するヘッダーを実装してみました。

わりといろんなところでよく見るやつな気がします。

下に引っ張るとみょいーってなるやつです。

とりあえずGitHubにあげておきました。

uruly/ZoomableTableHeader: Zoomalbe TableView Header

 

UITableViewのサブクラスを用意する

さて、最近このやり方でいいのか少し悩んでいますが、とりあえず良いことにして。

UITableviewのサブクラスを作成します。

import UIKit

class TableView: UITableView {
    
    let cellIdentifier = "cell"

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect, style: UITableViewStyle) {
        super.init(frame: frame, style: style)
        self.delegate = self
        self.dataSource = self
        self.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
        
    }
    
    convenience init(frame: CGRect) {
        self.init(frame: frame, style: .plain)
    }
}

extension TableView: UITableViewDelegate {

}

extension TableView: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 30
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
        cell.textLabel?.text = "\(indexPath.row)番目"
        
        return cell
    }
    
}

とりあえずこれで適当なtableViewが作成されたのでViewControllerに設置しておきます。

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let statusBarHeight = UIApplication.shared.statusBarFrame.maxY
        let tableView = TableView(frame: CGRect(x:0,y:-statusBarHeight,
                                                width:self.view.frame.width,
                                                height: self.view.frame.height + statusBarHeight))
        self.view.addSubview(tableView)
        
    }

}

 

このステータスバーのやつが潜り込むように色々設定してみたけどうまくいかないので、暫定処理でステータスバー分下げて配置しています(´・ω・`)

 

とりあえずこれでただのTableViewができました。

ヘッダーを作ろう!

ほいでは、次にヘッダーを作ります。

UITableViewHeaderFooterViewのサブクラスを用意します。

import UIKit

class ZoomableTableHeaderView: UITableViewHeaderFooterView {

    private var imageView:UIImageView!
    private var label:UILabel!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        
        // imageViewを設置
        imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        imageView.backgroundColor = UIColor.clear
        self.contentView.addSubview(imageView)
        
        // ラベルを設置
        label = UILabel()
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
        label.textColor = UIColor.white
        self.contentView.addSubview(label)
        
    }
    
    func setImage(_ image: UIImage) {
        self.imageView.image = image
    }
    
    func setLabel(_ text: String,frame:CGRect) {
        self.label.frame = frame
        self.label.text = text
    }
    
}

 

とりあえずヘッダーに画像とラベルを配置したものです。

TableViewの方に

class TableView: UITableView {
    
    let cellIdentifier = "cell"
    let headIdentifier = "imageHeader"
    let headerHeight:CGFloat = 240  // headerの高さ

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect, style: UITableViewStyle) {
        super.init(frame: frame, style: style)
        self.delegate = self
        self.dataSource = self
        self.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
        self.register(ZoomableTableHeaderView.self, forHeaderFooterViewReuseIdentifier: headIdentifier)
        
    }
    
    convenience init(frame: CGRect) {
        self.init(frame: frame, style: .plain)
    }
}

extension TableView: UITableViewDelegate {
    //ヘッダー
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        if section == 0 {
            return headerHeight
        }else {
            return 0
        }
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headIdentifier)
        if let header = header as? ZoomableTableHeaderView {
            let labelHeight:CGFloat = 50
            header.changeFrame(frame: CGRect(x:0,y:0,width:self.frame.width,height:headerHeight))
            header.setImage(UIImage(named:"zoomable.png")!)
            header.setLabel("ラベル名",
                          frame: CGRect(x:0,
                                        y:headerHeight - labelHeight,
                                        width:self.frame.width,
                                        height:labelHeight))
        }
        
        return header
    }
}

ラインを引いた部分とUITableViewDelegate内を追記しました。

このtableView(_:viewForHeaderInSection:)内で改めてimageViewの高さを設定してやらないと高さが反映されないので、ここで設定しています。

なんか違うアプローチがある気がするんですけどねえ(´ε`;)

imageViewに高さを設定するために

ZoomableTableHeaderViewに

    func changeFrame(frame:CGRect) {
        self.frame = frame
        self.imageView.frame = frame
    }

を追記しました。

 

ここまででこんなのができてるはず。

とりあえず下準備はできました。

 

ヘッダーを拡大、移動させよう!

さてようやく本題。

といってもコード貼るだけなんですけども(´;ω;`)

TableView.swiftに追記します。

extension TableView: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        // ヘッダーを取得
        guard let header = self.headerView(forSection: 0) as? ZoomableTableHeaderView else {
            return
        }
        
        var insetTop:CGFloat = 0
        if #available(iOS 11.0, *) {
            insetTop = scrollView.safeAreaInsets.top        // iPhoneX用のsafeArea分
        }
        let labelHeight:CGFloat = 60                        // 固定するラベルの高さ
        let minSize:CGFloat = headerHeight - labelHeight    // スクロール後ヘッダーを固定するサイズ
        let offsetY = scrollView.contentOffset.y + insetTop // safeAreaを含んだoffsetY
        
        if offsetY < 0 {
            // 下に引っ張ると画像が拡大
            let offsetY = offsetY * -1
            let newY = offsetY * -1
            let newHeight = offsetY + self.headerHeight
            header.changeFrame(frame: CGRect(x:0,y:newY,width:header.frame.width,height:newHeight))
        }else if offsetY <= minSize {
            //スクロールされる
            let newY = offsetY * -1
            header.changeFrameMin(frame: CGRect(x:0,y:newY,width:header.frame.width,height:header.frame.height))
        }else {
            // 固定される
            header.changeFrameMin(frame: CGRect(x:0,y:-minSize,width:header.frame.width,height:header.frame.height))
        }
        
    }
    
}

スクロール中に呼び出せれるscrollViewDidScroll(_:)内で、スクロール量に合わせて、ヘッダーの位置情報を更新します。

 

offsetY < 0 のときは、テーブルビューが下に引っ張られている時です。

この時引っ張られている分だけ、headerの高さとheader上のimageViewの高さを変更します。

実際にtransformで拡大しているわけではありませんが、みょーんと拡大されるはずです。

これは、ZoomableTableHeaderView内のimageViewにimageView.contentMode = .scaleAspectFillが設定されているためです。

 

offsetY <= minSizeのときはテーブルビューのスクロールと合わせてヘッダーのy座標を変えてあげています。

elseのときはヘッダーを固定しています。

この2つの場合はchangeFrameMin(frame:)というのを呼び出しています。これを、ZoomableTableHeaderViewに追記します。

class ZoomableTableHeaderView: UITableViewHeaderFooterView {

    // 略

    func changeFrameMin(frame:CGRect) {
        self.frame = frame
        self.contentView.frame = frame
    }

}

これでとりあえず拡大処理はできました。こんな感じの動きになっているはずです。

スクロールした時にHeaderのところになんか残っちゃってるんですよね。

最後にこれを解消します。

 

HeaderViewにBackgroundViewを設定しよう

透過するときにわりとよく使う気がする方法です。 ZoomableTableHeaderViewで

override init(reuseIdentifier: String?) {
    super.init(reuseIdentifier: reuseIdentifier)
    
    self.backgroundView = UIView()
    self.backgroundView?.backgroundColor = UIColor.clear
    // 略
}

こうすると、透過はできます。
が、これだと透明になっただけで実際には透明なレイヤーが一枚挟まれている状態で、重なっているセルがタップできません。

なので、少し面倒ですがUIViewのサブクラスを作成します。

import UIKit

class PassThroughView: UIView {

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews {
            if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                return true
            }
        }
        return false
    }

}

これは、透過部分はすり抜けてタップできるviewです。

これを先ほどのbackgroundViewに設定します。

override init(reuseIdentifier: String?) {
    super.init(reuseIdentifier: reuseIdentifier)
    
    self.backgroundView = PassThroughView()
    self.backgroundView?.backgroundColor = UIColor.clear
    // 略
}

これでタップできるようになりました。

 

全体コードはGitHubにて

全体コードはGitHubにあげておきました。uruly/ZoomableTableHeader: Zoomalbe TableView Header

 

今回の記事は、1ヶ月ほど前に実際のアプリで実装したものを抜き出して紹介したものです。

正直1ヶ月前のコードで(しかも今回それぶりのSwift)で、これってなんか違うアプローチの方がイクナイ?って思いながら書いていました。

TableViewのHeaderにする必要はない気もしますし、拡大縮小は普通にtransformでやってもいいのではって気もします。

座標を変えるときに面倒だからってframe全部を書き換えているのもどうなんだろうって思います。

 

動き的には割といい感じで動いている気もするんですけどね。

でも他アプリで見る実装とは何かが違う気がしています。いや、実際はわからんですけど。

 

今回細かい部分の説明を全くしてないですが、自分はこんな感じでこんなものを作ってみたよ!っていう紹介でした。いつもそんな感じですね!!!.・゚・(ノд`)゚・.

 

ではでは、何か少しでも参考になる部分があれば嬉しいです。

Swift
コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS
more