はじめに
自作で公開していたライブラリがあったのですが、ずいぶん前に公開したライブラリなので今回リファクタリングしました。
リファクタリングをしたポイントを記事にまとめましたので、他にも良さげな書き方等あれば気軽にコメントいただけると幸いです。
今回リファクタリングしたライブラリは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
クラスの肥大化を防ぐことができたので、構成が理解しやすくなったと思います。
終わりに
今回はリファクタリングした大きなポイントをまとめました。
今後も随時更新していこうと思います。