読者です 読者をやめる 読者になる 読者になる

View の Auto Layout によるアニメーションを無効にする

環境: Swift3

あるViewのSubviewのレイアウトをAuto Layoutで行った時にアニメーションしながら配置された。
この時のアニメーションは不要なので無効にする。
やり方はSubviewの配置が行われるlayoutSubviewsメソッドをオーバーライドして以下のようにアニメーションしないようにする。

方法1

UIView クラスメソッドのperform​Without​Animation:​のブロック内で実行させる。

override func layoutSubviews() {
    UIView.performWithoutAnimation {
        super.layoutSubviews()
    }
}

方法2

CATransaction.setDisableActions(true)を実行後にlayoutSubviewsを実行させる。

override func layoutSubviews() {
    CATransaction.begin()
    CATransaction.setDisableActions(true)
    super.layoutSubviews()
    CATransaction.commit()
}

角丸なUIViewに角丸な影をつける

環境: Swift3

角丸なUIViewに角丸なドロップシャドウをつけるやり方。
プレイグラウンドで確認。

f:id:xyk:20170319023445p:plain

import UIKit
import PlaygroundSupport

let baseView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
baseView.backgroundColor = UIColor(red: 255/255, green: 110/255, blue: 134/255, alpha: 1)

let shadowView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
shadowView.backgroundColor = UIColor(red: 89/255, green: 172/255, blue: 255/255, alpha: 1)
shadowView.center = baseView.center

shadowView.layer.cornerRadius = 10
shadowView.layer.masksToBounds = false

shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOpacity = 0.5 // 透明度
shadowView.layer.shadowOffset = CGSize(width: 5, height: 5) // 距離
shadowView.layer.shadowRadius = 5 // ぼかし量

// 以下、角丸パス追加とラスタライズで高速化
shadowView.layer.shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 10).cgPath
shadowView.layer.shouldRasterize = true
shadowView.layer.rasterizationScale = UIScreen.main.scale

baseView.addSubview(shadowView)

PlaygroundPage.current.liveView = baseView

CGPath の変化をアニメーションさせるサンプル

環境: Xcode8.2.1, Swift3

CGPath の変化をアニメーションさせる方法を試した。
気をつける点としては変更前と変更後のパスの数を同じにしておくこと。

f:id:xyk:20170310125452g:plain

import UIKit
import PlaygroundSupport

class SquareButton: UIControl {
    
    let pathLayer = CAShapeLayer()
    var squarePath: UIBezierPath!
    var halfMoonPath: UIBezierPath!
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.squarePath = self.squarePath(rect: frame)
        self.halfMoonPath = self.halfMoonPath(rect: frame)
        
        self.pathLayer.fillColor = UIColor.orange.cgColor
        self.pathLayer.strokeColor = UIColor.white.cgColor
        self.pathLayer.lineWidth = 4
        self.pathLayer.path = self.squarePath.cgPath
        self.layer.addSublayer(self.pathLayer)
    }
    
    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        super.endTracking(touch, with: event)
        
        self.toggle()
    }
    
    func toggle() {
        
        let anim = CABasicAnimation(keyPath: "path")
        
        if self.isSelected {
            anim.fromValue = self.halfMoonPath.cgPath
            anim.toValue = self.squarePath.cgPath
        } else {
            anim.fromValue = self.squarePath.cgPath
            anim.toValue = self.halfMoonPath.cgPath
        }
        
        anim.duration = 0.4
        anim.fillMode = kCAFillModeForwards
        anim.isRemovedOnCompletion = false
        
        self.pathLayer.add(anim, forKey: "animatePath")
        
        self.isSelected = !self.isSelected
    }
    
    func squarePath(rect: CGRect) -> UIBezierPath {
        
        let initialPoint = CGPoint(x: 0, y: 0)
        let curveStart = CGPoint(x: rect.maxX * 0.05, y: 0)
        let curveControl = CGPoint(x: rect.maxX * 0.5, y: 0)
        let curveEnd = CGPoint(x: rect.maxX * 0.95, y: 0)
        let firstCorner = CGPoint(x: rect.maxX, y: 0)
        let secondCorner = CGPoint(x: rect.maxX, y: rect.maxY)
        let thirdCorner = CGPoint(x: 0, y: rect.maxY)
        
        let myBezier = UIBezierPath()
        myBezier.move(to: initialPoint)
        myBezier.addLine(to: curveStart)
        myBezier.addQuadCurve(to: curveEnd, controlPoint: curveControl)
        myBezier.addLine(to: firstCorner)
        myBezier.addLine(to: secondCorner)
        myBezier.addLine(to: thirdCorner)
        
        myBezier.close()
        return myBezier
    }
    
    func halfMoonPath(rect: CGRect) -> UIBezierPath {
        
        let initialPoint = CGPoint(x: 0, y: 0)
        let curveStart = CGPoint(x: rect.maxX * 0.05, y: 0)
        let curveControl = CGPoint(x: rect.maxX * 0.5, y: rect.maxY * 0.6)
        let curveEnd = CGPoint(x: rect.maxX * 0.95, y: 0)
        let firstCorner = CGPoint(x: rect.maxX, y: 0)
        let secondCorner = CGPoint(x: rect.maxX, y: rect.maxY)
        let thirdCorner = CGPoint(x: 0, y: rect.maxY)
        
        let myBezier = UIBezierPath()
        myBezier.move(to: initialPoint)
        myBezier.addLine(to: curveStart)
        myBezier.addQuadCurve(to: curveEnd, controlPoint: curveControl)
        myBezier.addLine(to: firstCorner)
        myBezier.addLine(to: secondCorner)
        myBezier.addLine(to: thirdCorner)
        
        myBezier.close()
        return myBezier
    }

}

let baseView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
baseView.backgroundColor = UIColor(red: 38.0/255, green: 151.0/255, blue: 68.0/255, alpha: 1)

let button = SquareButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
baseView.addSubview(button)
button.center = baseView.center

PlaygroundPage.current.liveView = baseView

UIScrollView で現在のページ数を取得する

環境: Xcode8.2.1, Swift3

UIScrollViewのisPagingEnabledプロパティを true にすると、ページ単位のスクロールが可能になる。
このときに現在のページ数を求める方法についてメモ。

ページングは横スクロールの場合なら UIScrollView のcontentOffset.xUIScrollView.bounds.widthの半分を超えたところでドラッグを離すと隣のページに進み、半分を超えてなければ元のページに戻る挙動になっている。
まず、その時点のcontentOffset.xからページ数を計算する Extension を追加する。

extension UIScrollView {
    var currentPage: Int {
        return Int((self.contentOffset.x + (0.5 * self.bounds.width)) / self.bounds.width) + 1
    }
}

で、今回はページングのスクロールが完全に止まったタイミングでページ数を取得する方法を考える。

スクロールが完全に止まったタイミングを検出するには前回調べた UIScrollViewDelegate のメソッドに仕掛ければよい。
基本scrollViewDidEndDeceleratingのみで良いと思う(isPagingEnabled=trueはページの区切りまで自動スクロールするので)が、この Delegate はドラッグをピタッと止めた場合は呼ばれないので、その時でも検出できるように念のためscrollViewDidEndDraggingでかつdecelerate=falseの場合にも取得するようにしておく。

// MARK: - UIScrollViewDelegate

func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !decelerate {
        print("currentPage:", scrollView.currentPage)
    }
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    print("currentPage:", scrollView.currentPage)
}

これでページ数が切り替わった(ページングのスクロールが止まった)時に1度のみページ数が表示される。

UIScrollView の keyboardDismissMode を設定してドラッグ時に自動でキーボードを閉じる

環境: Xcode8.2.1, Swift3

UIScrollView 上に UITextField / UITextView を乗せている場合はkeyboardDismissModeプロパティを設定することで自動でキーボードを閉じることができる。

Storyboardの場合

f:id:xyk:20170309155712p:plain

コードの場合

// デフォルト設定。UIScrollView をドラッグしてもキーボードは閉じない
self.scrollView.keyboardDismissMode = .none

// dismisses the keyboard when a drag begins
// UIScrollView のドラッグ開始時にキーボードを閉じる
self.scrollView.keyboardDismissMode = .onDrag

// the keyboard follows the dragging touch off screen, and may be pulled upward again to cancel the dismiss
// UIScrollView を下方向ドラッグで上にスクロールするのに合わせてキーボードを閉じる
self.scrollView.keyboardDismissMode = .interactive

Dissmiss on drag設定時は UIScrollView のドラッグを開始するとすぐキーボードが閉じられる。
これを設定しておけばよさそう。

Dissmiss interactively設定時の挙動は以下のようになる。

f:id:xyk:20170309161010g:plain

UIScrollViewDelegate について

環境: Xcode8.2.1, Swift3

UIScrollView のドラッグによるスクロール時に呼ばれる UIScrollViewDelegate の順番

// MARK: - UIScrollViewDelegate

// any offset changes
// スクロール中は常に呼ばれる
func scrollViewDidScroll(_ scrollView: UIScrollView) {

}

// 1. called on start of dragging (may require some time and or distance to move)
// ドラッグ開始時
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {

}

// ここから下はドラッグ状態から指を離した後に呼ばれる

// 2. called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
// ドラッグの終わりの始まり
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

}

// 3. called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
// ドラッグの終わり
// decelerate が true ならまだ減速しながらスクロール中、false ならスクロールは止まっている。
// ドラッグをピタッと止めて、慣性なしでドラッグを終えた場合に decelerate = false になる。  
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {

}

// 4. called on finger up as we are moving
// 減速開始時 -> ★呼ばれない場合あり
// 3 で decelerate が true であれば呼ばれ、false であれば呼ばれない。
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {

}

// 5. called when scroll view grinds to a halt
// 減速終了時 -> ★呼ばれない場合あり
// 4 と同様に 3 で decelerate が true であれば呼ばれ、false であれば呼ばれない。
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

}

UIScrollView をプログラムでスクロールさせた場合に呼ばれる UIScrollDelegate の順番

例えば以下のようなコードでアニメーション付きでスクロールさせた場合

scrollView.setContentOffset(CGPoint(x: 320, y: 0), animated: true)

この場合は上記2~5のドラッグ系 Delegate は呼ばれずscrollViewDidScrollscrollViewDidEndScrollingAnimationのみ呼ばれる。

// MARK: - UIScrollViewDelegate

func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {

}

順番は以下のようになる。

scrollViewDidScroll
...
...
...
scrollViewDidScroll
scrollViewDidEndScrollingAnimation

ちなみに以下のようにアニメーションなしでスクロールさせた場合は

scrollView.setContentOffset(CGPoint(x: 320, y: 0), animated: false)

scrollViewDidEndScrollingAnimationは呼ばれずscrollViewDidScrollが1度だけ呼ばれる。

scrollViewDidScroll

UIScrollView の Bounce 設定について

環境: Xcode8.2.1, Swift3

UIScrollView の Bounce 設定のパラメータが3つあるが、これらの違いについて調べた。

Storyboard
以下がデフォルトの設定
f:id:xyk:20170308131140p:plain

コード

// UIScrollView

// default YES. if YES, bounces past edge of content and back again
open var bounces: Bool 

// default NO. if YES and bounces is YES, even if content is smaller than bounds, allow drag vertically
open var alwaysBounceVertical: Bool 

// default NO. if YES and bounces is YES, even if content is smaller than bounds, allow drag horizontally
open var alwaysBounceHorizontal: Bool 

組み合わせ1(デフォルト)

parameter value
Bounces true
Bounce Horizontally false
Bounce Vertically false

UIScrollView に追加したビューのcontentSizescrollView.frame.sizeより大きい場合のみ、バウンスが発生する。
つまり
scrollView.frame.width < contentSize.widthであれば、横スクロール時に画面端でバウンス発生、
scrollView.frame.height < contentSize.heightであれば、縦スクロール時に画面端でバウンス発生する。
scrollView.frame.width >= contentSize.widthまたは
scrollView.frame.height >= contentSize.heightであればバウンスは発生しない。

組み合わせ2

parameter value
Bounces true
Bounce Horizontally true
Bounce Vertically false

横方向スクロール時に必ずバウンスが発生する。
contentSize.widthscrollView.frame.widthより小さくてもバウンスする。

組み合わせ3

parameter value
Bounces true
Bounce Horizontally false
Bounce Vertically true

縦方向スクロール時に必ずバウンスが発生する。
contentSize.heightscrollView.frame.heightより小さくてもバウンスする。

組み合わせ4

parameter value
Bounces true
Bounce Horizontally true
Bounce Vertically true

縦方向、縦方向ともにスクロール時に必ずバウンスが発生する。
contentSizescrollView.frame.sizeより小さくてもバウンスする。

残りの組み合わせ

parameter value
Bounces false
Bounce Horizontally false
Bounce Vertically false
parameter value
Bounces false
Bounce Horizontally true
Bounce Vertically false
parameter value
Bounces false
Bounce Horizontally false
Bounce Vertically true

縦方向、縦方向ともにスクロール時にバウンスしない。
つまり、Bouncesを false にするとBounce HorizontallyBounce Verticallyの設定にかかわらずバウンスが発生しなくなる。