ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

ファイル内の文字列を置換して上書き保存

便利でたまに使いたくなるのですが、すぐ忘れてしまうので備忘録です。 Perlワンライナーを使用します。

環境

OS version

macOS 10.13.5 (High Sierra)

Perl version

$ perl -v

This is perl 5, version 26, subversion 2 (v5.26.2) built for darwin-thread-multi-2level
...

単一ファイルの置換

$ perl -p -i -e 's/XXX/YYY/g' FilePath

FilePathが示すファイル中の'XXX'を'YYY'に置換する。

各オプションについては以下:

-p

入力ファイルの各行を、後述する-eオプションのスクリプトで処理して出力する。このオプションを追加することでsedコマンドのように扱える。

-i

入力ファイルに処理結果を上書きする。間違えた処理を上書きしてしまうと戻せないため、元ファイルはバックアップ推奨。

-e

実行するスクリプトを指定する。

複数ファイルの一括置換

単一ファイルの置換をfindコマンドで複数ファイルに適用するだけ。

$ find . -type f -exec perl -p -i -e 's/XXX/YYY/g' {} \;

参考

https://www.perl.com/pub/2004/08/09/commandline.html/

PushでPop(風アニメーション)する

UINavigationControllerを使用したPush遷移の際のアニメーションをPop風(左から右へ)にする必要があったので、実現方法についてメモしておきます。

検証環境

  • Xcode9.2
  • iOS11 Simulator

大まかな流れ

  1. Animatorの作成(UIViewControllerAnimatedTransitioningプロトコルの実装)
  2. UINavigationControllerDelegateプロトコルを実装し、1.で作成したAnimatorオブジェクトを返す
  3. Swipe Backの有効化

Animatorの作成

UIViewControllerAnimatedTransitioningプロトコルを実装したAnimatorオブジェクトを作成します。

今回は以下の2メソッドを実装しました。

  • transitionDuration(using:)

    アニメーションの実行時間を返します。

  • animateTransition(using:)

    このメソッド内に実際のアニメーション処理を実装します。引数のtransitionContextにViewController等の遷移に必要な情報が渡されます。

    1. transitionContextから遷移元、遷移先のViewを取得
    2. transitionContext.containerViewに遷移先(to)のViewを追加(addSubview)
    3. アニメーション処理(UIViewアニメーションやCoreAnimationなど)
    4. アニメーション終了を通知(transitionContext.completeTransition(true))
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.viewController(forKey: .from)?.view,
            let toView = transitionContext.viewController(forKey: .to)?.view else
        {
            transitionContext.completeTransition(true)
            return
        }
        
        transitionContext.containerView.addSubview(toView)
        
        toView.frame = CGRect(x: fromView.frame.origin.x - fromView.frame.size.width,
                              y: fromView.frame.origin.y,
                              width: toView.frame.width,
                              height: toView.frame.height)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0,
                       options: UIViewAnimationOptions.init(rawValue: 0),
                       animations: {
                        fromView.frame = CGRect(x: fromView.frame.origin.x + fromView.frame.size.width,
                                                y: fromView.frame.origin.y,
                                                width: fromView.frame.size.width,
                                                height: fromView.frame.size.height)
                        toView.frame = CGRect(x: toView.frame.origin.x + fromView.frame.size.width,
                                              y: toView.frame.origin.y,
                                              width: toView.frame.size.width,
                                              height: toView.frame.size.height)
        },
                       completion: { _ in
                        transitionContext.completeTransition(true)
        })
    }
}

UINavigationControllerDelegateの実装

以下のメソッドを実装します。

  • navigationController(_:animationControllerFor:from:to:)

このメソッドから先ほど作成したAnimatorを返すことで、遷移アニメーションが切り替わります。

画面遷移の度に呼び出されてしまいますが、operationをチェックすることでPushによる遷移なのか、Popによる遷移なのかを判定出来ます。今回はPushのアニメーションのみを変更したいため、.pushの場合のみAnimatorを返し、その他の場合はnilを返してデフォルトのアニメーションになるようにしています。

class PopNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }

}

extension PopNavigationController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationControllerOperation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
    {
        if operation == .push {
            return PopAnimator()
        }
        return nil  // nilを返した場合、デフォルトにアニメーションになる
    }
}

f:id:kurozu10344:20171215023447g:plain

Swipe Backの有効化

UINavigationControllerDelegatenavigationController(_:animationControllerFor:from:to:)を実装すると、デフォルトのSwipe Backが無効化されてしまうようです(たとえnilを返した場合でも)。どうやらScreen Edge Swipeのイベントが発生しなくなる模様。

無効化して欲しくない場合、回避策としてUINavigationControllerinteractivePopGestureRecognizer.delegateに独自のUIGestureRecognizerDelegate実装クラスをセットし、gestureRecognizerShouldBeginメソッドでtrueを返すようにするとScreen Edge Swipeイベントが発生するようになり、Swipe Backが再び行えるようになりました。こんな対処で良いのかなという感じですが、今のところ問題は出ていません。特定の画面では無効化したい場合、適宜falseを返せば無効化されます。

class PopNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
        interactivePopGestureRecognizer?.delegate = self
    }

}

extension PopNavigationController: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return true // falseを返した場合、Swipe Backは発生しない
    }
}

サンプルコード

GitHubにサンプルコードを置いています。

PushLikePop