UIKit - AutoLayoutを利用した高さ可変のTableHeaderViewをつくる

表題通りUITableViewのHeaderに高さ可変のHeaderViewをつくります。

TableHeaderViewの中にラベルか何かを表示させていて、
その文字列をレスポンスから引っ張ってきたりして設定するために、
高さを変えなければならない。そんな人向けの記事です。

まずTableHeaderViewをつくりましょう。

当たり前ですが、xibでもコードオンリーでもいいのでHeaderViewをつくりましょう。
その際、下辺が大きさに応じて変化させることができるような制約(Constraint)をつけましょう。 下はかんたんな例。 カスタムビューの最下端にラベルのボトムとの制約をつけています。

class MyTableHeaderView: UIView {

    var label: UILabel! = {

    }()

   override func updateConstraints() {
        super.updateConstraints()
        
        labelDescription.bottomAnchor.constraintEqualToAnchor(bottomView.bottomAnchor, constant: -16).active = true
    } 
}

UITableViewに設定しよう

以下の様な感じで設定してください。 ぼくは以下の処理をself.view.frameの大きさが決まった、viewDidLayoutSubviewsに書いています。
viewDidLayoutSubViewsは複数回呼ばれるので注意。

let headerView = MyTableHeaderView()
headerView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: デフォルトの高さ)

self.tableView.tableHeaderView = MyTableHeaderView()

高さ可変の処理

たとえば、ヘッダーに大量の文字列を入れなければならないとしましょう。
そのタイミングで以下のような処理を書きます。

if let tableHeaderView = self.tableView.tableHeaderView as? MyTableHeaderView {
    // 大量のテキストを設定
    tableHeaderView.label.text = "大量のテキスト"

    // テキスト入れた状態でAutolayoutによる高さの算出
    tableHeaderView.setNeedsLayout()
    tableHeaderView.layoutIfNeeded()
    let size = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
                
    // 高さの再設定
    tableHeaderView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)

    // 再代入
    self?.tableView.tableHeaderView = tableHeaderView
}

おそらくこの方法でできるはずです。ではでは

参考: iOS7・iOS8の処理分岐なし!UITableViewのCellの高さをAutolayoutで自動計算する方法 - 株式会社エウレカ

UIKit - topLayoutGuide, bottomLayoutGuideとは

実はあんまり分かっていなかったので確認。
だいぶはしょって説明すると、
topLayoutGuideは、ステータスバーやナビゲーションバーなどの上部のUIを考慮してレイアウトするもので、 bottomLayoutGuideは、タブバーなどの下部のUIを考慮してレイアウトするものですね。

下のように書くと、ナビゲーションバーやステータスバー、タブバーに
かぶらないないようにレイアウトしてくれます。

var btn: UIButton!
var label: UILabel!

override func updateViewConstraints() {
        
        btn.topAnchor.constraintEqualToAnchor(self.topLayoutGuide.bottomAnchor, constant: 0).active = true
        btn.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: 100).active = true
        
        label.bottomAnchor.constraintEqualToAnchor(self.bottomLayoutGuide.topAnchor, constant: 0).active = true
        label.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: 100).active = true
        
        super.updateViewConstraints()
    }

Swift - didSetでgetter,setterがシンプルに

willSet, didSetをあまり使っていなかったが、使ってみたら便利だった話。

「currentIndexはgetできる」
「currentIndexはsetすると、その数字を内部で保持し、あるViewの色が変わる」
というようなものを、willSet, didSetを知らない間こんなふうに書いていました。

private var _currentIndex: Int = 0

var currentIndex: Int {
    get {
         return _currentIndex
    }
    set {
         _currentIndex = newValue
         view.backgroundColor = _currentIndex > 5 ? UIColor().redColor() : UIColor().blueColor()
    }
}

ただこの書き方だと、currentIndexの役割をもったプロパティが2つできてしまい、
なんか心がもやもやします(恋かな?)

したらば、下の書き方にすると非常にシンプルに書くことができます。

var currentIndex: Int {
    didSet {
         view.backgroundColor = currentIndex > 5 ? UIColor().redColor() : UIColor().blueColor()        
    }
}

willSetはまだ使う機会ないのですが、さぞや活躍してくれることでしょう。むふふ

Swift - クロージャを利用して初期化をすっきりする

viewDidLoadが肥大化するのは、可読性が悪くなるのでもう嫌だ。
そしてStoryboardを使うのはもっと嫌だ。
ということで僕も最近では流行りに習って、クロージャを利用してプロパティを初期化しています。
下のような感じ。

class ViewController: UIViewController {
    
    private var didSetupConstraints = false
    
    private let collectionView: UICollectionView = {
        
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 75, height: 50) // セルの大きさ
        layout.minimumLineSpacing = 0 // 垂直方向のアイテム同士の間隔
        layout.minimumInteritemSpacing = 5 // 水平方向のアイテム同士の間隔
        layout.headerReferenceSize = CGSize.zero // ヘッダーのサイズ
        layout.footerReferenceSize = CGSize.zero // フッターのサイズ
        layout.sectionInset = UIEdgeInsetsZero // セクションの上下左右の間隔

        let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
        collectionView.registerClass(MyCollectionCell.self, forCellWithReuseIdentifier: "MyCollectionCell")
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        return collectionView
    }()

    // MARK: - UIViewController
    
    override func loadView() {
        self.view = collectionView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.delegate = self
        collectionView.dataSource = self
    }
    
    override func updateViewConstraints() {
        
        func setupConstraints() {
            collectionView.topAnchor.constraintEqualToAnchor(topLayoutGuide.topAnchor).active = true
            collectionView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor).active = true
            collectionView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor).active = true
            collectionView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor).active = true
        }
        
        if !didSetupConstraints {
            setupConstraints()
            didSetupConstraints = true
        }
        
        super.updateViewConstraints()
    }
}

参考:

iOSアプリケーションでコードベースのレイアウトを積極利用する - クックパッド開発者ブログ

Swift - UIFlowLayoutに関するおさらい

UICollectionViewはよく使うんですが、
毎度毎度UIFlowLayoutクラスの「この値ってなんだっけ?」状態になるので書きだした。

layout.itemSize// セルの大きさ
layout.minimumLineSpacing // 垂直方向のアイテム同士の間隔の最低値
layout.minimumInteritemSpacing // 水平方向のアイテム同士の間隔の最低値
layout.headerReferenceSize // ヘッダーのサイズ
layout.footerReferenceSize // フッターのサイズ
layout.sectionInset// セクションの上下左右の間隔

当たり前だが、UICollectionFlowLayoutはUICollectionViewのサイズに対して、
溢れないようにCellを敷き詰めるためのクラスなので、
minimumInteritemSpacingに設定した値が、そのままレイアウトに適用されるわけではない。

Swift - AdMob広告を貼ってみた。

アプリにAdMob(Google)広告を貼る方法をまとめてみた。

1. 広告IDを取得する

以下リンクから、申し込み > アカウント開設の流れで、
広告IDを取得します。 www.google.co.jp

2. CocoapodsでSDKをプロジェクトに追加

podfileに、以下を記述しGoogle Mobile Ads SDKを追加する。

pod 'GoogleMobileAds'

注) 私の環境で上記の手順でSDKを追加したところ、
import GoogleMobileAds の箇所で「No such module 'GoogleMobileAds'」とエラーが出た。

いろいろと見た結果、
BuidSettings > Search Paths > Framework Search Paths に、
「 ${PODS_ROOT}/Google-Mobile-Ads-SDK/Frameworks 」
と追加したところ問題なくビルドできるようになった。

3. GADBannerViewを表示する

GADBannerViewをドキュメントに従って表示させるだけです。
私は下のような適当なプロトコルを用意して表示しています。

import GoogleMobileAds

protocol AdShowable {}

extension AdShowable where Self: UIViewController {
    
    func getAdView(adSize: GADAdSize) -> GADBannerView {
        let bannerView = GADBannerView(adSize: adSize)
        bannerView.rootViewController = self
        bannerView.adUnitID = "ca-app-pub-XXX"
        
        let request = GADRequest()
        request.testDevices = [kGADSimulatorID]
        bannerView.loadRequest(GADRequest())
        
        return bannerView
    }
}

Swift - パンで指についてくるビューをつくる

案外、パン(PCでいうドラッグ)でViewがついてくるような処理の書き方を思い出せなかったので、
備忘録的にメモ。

import UIKit

class ViewController: UIViewController, UIGestureRecognizerDelegate {
    
    var myView: UIView!
    var myView2: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        myView.backgroundColor = UIColor.blackColor()
        myView.center = view.center
        view.addSubview(myView)
        addGestureRecognizer(myView)
        
        myView2 = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        myView2.backgroundColor = UIColor.redColor()
        myView2.center = view.center
        view.addSubview(myView2)
        addGestureRecognizer(myView2)
    }
    
    func addGestureRecognizer(target: UIView) {
        let panGestureRec = UIPanGestureRecognizer(target: self, action: #selector(self.panGestureReceived(_:)))
        target.addGestureRecognizer(panGestureRec)
    }
    
    func panGestureReceived(sender: UIPanGestureRecognizer) {
        guard let target = sender.view else {
            return
        }
        
        // translationInViewが返す値は、パンが始まってからの蓄積された値となる
        let p = sender.translationInView(self.view)
        
        // 移動先座標
        let moved = CGPoint(x: target.center.x + p.x, y: target.center.y + p.y)
        target.center = moved
        
        // リセット
        sender.setTranslation(CGPoint.zero, inView: self.view)
    }
}