はじめに
自作で公開していたライブラリがあったのですが、ずいぶん前に公開したライブラリなので今回リファクタリングしました。
リファクタリングをしたポイントを記事にまとめましたので、他にも良さげな書き方等あれば気軽にコメントいただけると幸いです。
今回リファクタリングしたライブラリはUI/UX系アニメーションのライブラリです。
SYBlinkAnimationKit
CocoaControls
参考にしたスタイルガイドなど
適切なアクセス修飾子をつける
- 
Public外部からアクセス可能
- 
Internalモジュール内(ライブラリ内)のみアクセス可能
- 
Privateファイル内のみアクセス可能
ライブラリは特にアクセス修飾子を意識する必要があります。
外部からアクセスできるクラス、定数、変数、関数はpublic修飾子、ライブラリのファイル間で利用するものはinternal修飾子(自動的に付与されるので基本的にinternal修飾子はつけない)。それ以外はprivate修飾子をつけるようにしています。
通常の開発ではpublic修飾子はあまり使わず、private修飾子をつけるかつけないかになってくると思いますが、意図しない変数や関数の呼び出しはバグの温床です。
ファイル(クラス)間でのインタフェースはパッと見てわかるように、基本的にはprivate修飾子をつけ、外部からのインタフェースの役割をするもののみprivate修飾子をつけないようにするべきだと思います。
継承を許容しないClassは常にfinalをつける
Before
public class SYButton: UIButton {
...
After
public final class SYButton: UIButton {
...
継承を許容させないためにclassにつけるfinalですが、継承を許容させてる意図がわかりやすいように、基本的にclassにはfinalをつけ、継承を許容させてるclassのみfinalをつけないというスタイルにしました。
今回では継承して使用することを想定としたSYTableViewCellクラス以外はfinalをつけました。
githubのスタイルガイドでは、基本的にclassにfinalをつけることを推奨しています。
条件分岐の処理
Before
switch animationType {
case .border:
    syLayer.animationType = .border
case .borderWithShadow:
    syLayer.animationType = .borderWithShadow
case .background:
    syLayer.animationType = .background
case .ripple:
    syLayer.animationType = .ripple
case .text:
    syLayer.animationType = .text
}
After
syLayer.animationType = {
            switch animationType {
            case .border:
                return .border
            case .borderWithShadow:
                return .borderWithShadow
            case .background:
                return .background
            case .ripple:
                return .ripple
            case .text:
                return .text
            }
}()
条件分岐の処理を関数型っぽく書き直しました。条件によっていろいろな処理や挙動を行うものならbeforeのままでいいと思いますが、このように関数型っぽく書いたほうが、条件によってsyLayer.animationTypeになにかを代入するということが一目見て分かりやすくなると思います。この書き方ならsyLayer.animationType =というコピペ作業もいらないですね。優秀なエンジニアはコピペ作業をしないものです。
Extensionで実装を分割する(// MARK: - を使用する)
// MARK: - Private Methods -
private extension SYButton {
    private func setup() {
    ...
今回のライブラリでは、private methodsが多くなりがちなのでextensionでprivateな関数を分割しています。またdelegateなどの処理をextensionで分割すると処理を一箇所にまとめることができるのでわかりやすいかと思います。
e.g.
// MARK: - UITableViewDataSource -
extension UIViewController: UITableViewDataSource {
...
// MARK: - UITableViewDelegate -
extension UIViewController: UITableViewDelegate {
// MARK: -
関数ジャンプを使ってる人も多いと思いますが、// MARK: -を使用することで、その場所にジャンプすることができます。実装が大きくなった時には相当便利になるので処理のまとまりごとに// MARK: -を使うようにしています。
Swift実装のstruct initializersを使う
Before
let rect = CGRectMake(0, 0, 100, 100)
let point = CGPointMake(0, 0)
let size = CGSizeMake(100, 100)
let height = CGRectGetHeight(frame)
let width = CGRectGetWidth(frame)
After
let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let size = CGSize(width: 100, height: 100)
let point = CGPoint(x: 0, y: 0)
let height = frame.height
let width = frame.width
SwiftからはCGRectやCGSizeなどの構造体が定義され、便利なメソッドがいくつか追加されました。Swift3からはCGRectMakeなどのObjective-Cの実装は使えなくなるので、SwiftからはCGRectなどのstruct initializersを使うことが推奨されています。
e.g.
let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let maxX = frame.maxX
let maxY = frame.maxY
let rect = CGRect.zero
let size = CGSize.zero
let point = CGPoint.zero
参考
- CGRectMake , CGPointMake, CGSizeMake, CGRectZero, CGPointZero is unavailable in Swift
- Swift: CGRect, CGSize & CGPoint
必要な時以外は、ClearColorは使用しない
このライブラリではアニメーションする初期の色をいくつか設定しているのですが、透明色UIColor.clearColor()を使用する場合は透過処理が入るため、描画パフォーマンスは通常の色と比較して落ちます。本当に必要な時以外はむやみにUIColor.clearColor()を使用しない方がいいと思います。
定数を定義する
Before
private var animationBorderColor = UIColor(red: 54/255, green: 215/255, blue: 183/255, alpha: 1)
private var animationDuration: CFTimeInterval = 1.5
After
struct AnimationDefaultColor {
    static let border     = UIColor(red: 54/255, green: 215/255, blue: 183/255, alpha: 1)
    static let background = UIColor(red: 248/255, green: 148/255, blue: 6/255, alpha: 1)
    static let text       = UIColor(red: 214/255, green: 69/255, blue: 65/255, alpha: 1)
    static let ripple     = UIColor(red: 171/255, green: 183/255, blue: 183/255, alpha: 1)
}
private var animationBorderColor = AnimationDefaultColor.border
//必要に応じてアクセスコントロールをprivateにする
private struct AnimationConstants {
        static let borderWidth: CGFloat = 1
        static let defaultDuration: CFTimeInterval = 1.5
        static let rippleDiameterRatio: CGFloat = 0.7
        ...
}
private var animationDuration: CFTimeInterval = AnimationConstants.defaultDuration
マジックナンバーを扱う場合は後から見て意図がわかりにくかったり、同じ値を多用してる時に変更しにくいなどのデメリットがあるかと思います。
このように構造体で定数を定義することにより、名前空間を汚染しにくくまとまった定数を構造体に収納することができます。
enumとextensionを使用して初期化処理を便利にする
Before
let borderColorAnimtion = CABasicAnimation(keyPath: "borderColor")
let backgroundColorAnimtion = CABasicAnimation(keyPath: "backgroundColor")
After
extension CABasicAnimation {
    convenience init(type: AnimationKeyType) {
        self.init(keyPath: type.rawValue)
    }
}
enum AnimationType: String {
        case borderColor
        case backgroundColor
        ..
//使用時
let borderColorAnimtion = CABasicAnimation(type: .borderColor)
let backgroundColorAnimtion = CABasicAnimation(type: .backgroundColor)
CABasicAnimationはkeyPathにStringでアニメーションを指定して使用します。
今回はAnimationTypeというenumを定義しextensionで初期化処理を追加することによって、enumのケースでアニメーションを指定するようにしました。
AnimationTypeに定義することで今回使用しているアニメーションの種類をまとめて管理できることと、補完でアクセスできるので楽にCABasicAnimationを定義することができます。
enumで管理しているので、ケースごとによる挙動もここで管理できます。extensionのアクセスレベルはinternalなので、実際にライブラリを使用する側にも影響はありません。
protocolを使用する
ケース1 共通化
private func setTextLayer() {
        guard let font = titleLabel?.font, text = currentTitle else {
            return
        }
        var attributes = [String: AnyObject]()
        attributes[NSFontAttributeName] = font
        
        let size  = text.sizeWithAttributes(attributes)
        let x     = ( self.frame.width - size.width ) / 2
        let y     = ( self.frame.height - size.height ) / 2
        let frame = CGRect(origin: CGPoint(x: x, y: y), size: CGSize(width: size.width, height: size.height + layer.borderWidth))
        textLayer.font            = font
        textLayer.string          = text
        textLayer.fontSize        = font.pointSize
        textLayer.foregroundColor = textColor.CGColor
        textLayer.contentsScale   = UIScreen.mainScreen().scale
        textLayer.frame           = frame
        textLayer.alignmentMode   = kCAAlignmentCenter
    }
テキストのアニメーション処理のため、TextLayerの処理が書かれたクラスがいくつかあり、似たような処理がいくつかのクラスに書かれていました。これはDryの原則にも反していますし、どこかで共通化したいですよね。
今回はこれをprotocol実装で共通化しました。
protocol TextConvertible {
    var textLayer: CATextLayer { get set }
    
    func configureTextLayer(text: String?, font: UIFont?, textColor: UIColor)
}
extension TextConvertible where Self: UIView {
    func configureTextLayer(text: String?, font: UIFont?, textColor: UIColor) {
        guard let text = text, font = font else { return }
        
        var attributes = [String: AnyObject]()
        attributes[NSFontAttributeName] = font
        
        let size  = text.sizeWithAttributes(attributes)
        let x     = ( self.frame.width - size.width ) / 2
        let y     = ( self.frame.height - size.height ) / 2
        let frame = CGRect(x: x, y: y, width: size.width, height: size.height + layer.borderWidth)
        
        textLayer.font            = font
        textLayer.string          = text
        textLayer.fontSize        = font.pointSize
        textLayer.foregroundColor = textColor.CGColor
        textLayer.contentsScale   = UIScreen.mainScreen().scale
        textLayer.frame           = frame
        textLayer.alignmentMode   = kCAAlignmentCenter
    }
}
//使用時
class SYButton: UIButton, TextConvertible {
...
func resetTextLayer() {
configureTextLayer(currentTitle, font: titleLabel?.font, textColor: textColor)
...
protocol実装にすることで複数のクラスでこの処理を共通化することができます。
ケース2 処理の分割化
今回SYLayerというクラスに、CALayerの管理とアニメーションの処理の二つが実装されていたためクラスが肥大化していました。
原則として1つのクラスに1つの役割というものがあると思いますが、ここもどうにかしたいと思っていたのでprotocolを利用して処理の分割化を図りました。
Before
class SYLayer {
...
// CALayerを管理する処理
func setLayer() {
...
func resizeLayer() {
...
// アニメーションの処理
func startAnimation() {
...
func stopAnimation() {
...
After
// CALayerを管理するクラス Animator protocolに準拠
class SYLayer: Animator {
...
func setLayer() {
...
func resizeLayer() {
...
// アニメーションを管理するprotocol
protocol Animatable {
..
extension Animatable {
func startAnimation() {
..
func stopAnimation() {
...
//使用時
class SYLabel {
..
let layer = SYLayer(self.layer)
layer.startAnimation()
SYLayerクラスのアニメーションの処理をAnimatable protocolに任せることで、処理が分割されSYLayerクラスの肥大化を防ぐことができたので、構成が理解しやすくなったと思います。
終わりに
今回はリファクタリングした大きなポイントをまとめました。
今後も随時更新していこうと思います。



