xyk blog

最近は iOS 開発の記事が多めです。

Swift 画像表示ライブラリ Nuke で画像取得失敗時のレスポンスステータスコードを知りたい

検証環境:
Xcode 13.4
Swift 5.6.1
Nuke 11.0.1

Swift の画像表示ライブラリである Nuke (https://github.com/kean/Nuke) で、画像取得に失敗した時のレスポンスステータスコードを知りたい状況があったのだが、Error 情報から取り出す方法がちょっと面倒だったのでメモしておく。

ImagePipeline.shared.loadImage(with: url) { result in

    if case .failure(let pipelineError) = result,
        case ImagePipeline.Error.dataLoadingFailed(let dataLoaderError) = pipelineError,
        case DataLoader.Error.statusCodeUnacceptable(let responseCode) = dataLoaderError {
        print("responseCode: \(responseCode)")
    }
}

zsh で実行に失敗したコマンドを履歴に残さない

zsh で実行に失敗したコマンドは履歴(.zsh_history)に残さないようにしたい。
以下を.zshrc に追加しておく。
precmdのタイミングでフックして、zsh の fc コマンドを利用して履歴から削除している。

autoload -Uz add-zsh-hook

remove_last_history_if_not_needed () {
  local last_status="$?"
  local HISTFILE=~/.zsh_history
  if [[ ${last_status} -ne 0 ]]; then
    fc -W
    ed -s ${HISTFILE} <<EOF >/dev/null
d
w
q
EOF
    fc -R
  fi
}

add-zsh-hook precmd remove_last_history_if_not_needed

UIViewController で画面表示時に1度のみ処理を実行する

検証環境:
Version 13.2 (13C90)
Swift 5.5.2

UIViewController で画面表示時に何か処理を1度のみ実行したい時に lazy stored property を使って簡潔に書くやり方。

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    _ = viewDidAppearOnce
}

private lazy var viewDidAppearOnce: Void = {
    // doSomething
}()

RxSwift を使っているなら

private let disposeBag = DisposeBag()

override func viewDidLoad() {
    super.viewDidLoad()

    rx.sentMessage(#selector(viewDidAppear))
        .take(1)
        .subscribe(onNext: { _ in
            // doSomething
        }).disposed(by: disposeBag)
}

Swift で月初・月末を取得する

検証環境:
Xcode Version 12.5 (12E262)
Swift 5.4

Swift で、ある月の月初・月末を取得する方法。
月初の取得は1日固定で取得するだけだが、月末の取得は月初から1ヶ月進めて1日戻すことで算出できる。

例1:2020年2月の月初と月末を取得する

let calendar = Calendar(identifier: .gregorian) // 西暦を指定
let firstDay = calendar.date(from: DateComponents(year: 2020, month: 2))! // day: 1 を指定してもよいが省略しても月初となる

/* こう書いても同じ
var comps = calendar.dateComponents([.year, .month], from: Date())
comps.year = 2020
comps.month = 2
let firstDay = calendar.date(from: comps)!
*/

let add = DateComponents(month: 1, day: -1) // 月初から1ヶ月進めて1日戻す
let lastDay = calendar.date(byAdding: add, to: firstDay)!

print("\(firstDay)") // 2020-01-31 15:00:00 +0000
print("\(lastDay)") // 2020-02-28 15:00:00 +0000

print で文字列出力すると GMT(UTC) でわかりづらいので、フォーマッタを用意して JST で文字列出力する。

let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.calendar = Calendar(identifier: .gregorian) // 西暦を指定
    // formatter.timeZone = TimeZone(identifier: "Asia/Tokyo") // システムのタイムゾーンが Asia/Tokyo でない場合は指定が必要
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
}()

print(dateFormatter.string(from: firstDay)) // 2020-02-01
print(dateFormatter.string(from: lastDay)) // 2020-02-29

例2:今月の月初と月末を取得する

let calendar = Calendar(identifier: .gregorian) // 西暦を指定
let comps = calendar.dateComponents([.year, .month], from: Date())
let firstDay = calendar.date(from: comps)!

let add = DateComponents(month: 1, day: -1)
let lastDay = calendar.date(byAdding: add, to: firstDay)!

print(dateFormatter.string(from: firstDay)) // 2021-07-01
print(dateFormatter.string(from: lastDay)) // 2021-07-31

Git でブランチの派生元を間違えたときに git rebase --onto で修正する

やるたびに調べているのでメモ。

$ git rebase --onto (新しい派生元ブランチ名) (現在の派生元ブランチ名) (ブランチ名)

派生元がブランチでない場合はコミットID指定でもOK。

例: 間違えて develop から作成した feature/hoge ブランチの派生元を master に変更する

$ git rebase --onto master develop feature/hoge

おまけで --onto で指定先をミスってコミットが見えなくなってしまった場合は、reflog で消えたコミット群を探し、その末端のコミットに対して再度ブランチ化すればよい。

$ git reflog
$ git checkout (コミットID)  # 消えているコミットの末端へ移動する
$ git branch (ブランチ名)  # ブランチ化する

参考:

git でコミットが消えた場合に簡単に復帰する方法 - Qiita

表示中のマップ(MKMapView)領域内に含まれるアノテーション(MKAnnotation)を調べる

検証環境:
Xcode Version 12.5 (12E262)
Swift 5.4
iOS Deployment Target 11.0

表示中のマップ(MKMapView)領域内に配置されているアノテーション(MKAnnotation)を調べる方法について。
MKMapView.visibleMapRectで現在表示中の領域を取得し、MKMapView.annotations(in:)に表示領域の mapRect を渡すことで表示領域内にあるアノテーション配列を取得できる。
以下ではそれらを使い Extension として実装した。

extension MKMapView {
    func visibleAnnotations() -> [MKAnnotation] {
        return annotations(in: visibleMapRect).compactMap { $0 as? MKAnnotation }
    }
}

自身の現在位置アノテーション(MKUserLocation)は除外する場合

extension MKMapView {
    func visibleAnnotations() -> [MKAnnotation] {
        return annotations(in: visibleMapRect).compactMap { $0 as? MKAnnotation }.filter { !($0 is MKUserLocation) }
    }
}

使用例としてスワイプやピンチイン・ピンチアウトでマップを動かし表示表域内にアノテーションが無くなったらアノテーションの再検索を実行させたい場合。

extension ViewController: MKMapViewDelegate {

    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        if mapView.visibleAnnotations().isEmpty {
            // 検索処理を実行
        }
    }

}

参考:

visibleMapRect
https://developer.apple.com/documentation/mapkit/mkmapview/1452732-visiblemaprect

annotations(in:)
https://developer.apple.com/documentation/mapkit/mkmapview/1452279-annotations

関連:

xyk.hatenablog.com

Swift 5.3 からの Multiple Trailing Closures

Swift 5.3(SE-0279SE-0286)から追加された Multiple Trailing Closures について。

例えば、UIView.animate メソッドのような引数に複数の Closure を持つ場合の Trailing Closure は Swift5.2 までは次のように書いていた。

UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: {
    // ...
}) { _ in
    // ...
}

この従来の書き方は現行のバージョンでもコンパイルエラーにはならず使える。

Swift 5.3 からの新しいルールでは Trailing Closure を次のように書ける。

UIView.animate(withDuration: 0.3, delay: 0, options: []) {
    // ...
} completion: { _ in
    // ...
}

最初の Closure は引数ラベルを省略し、2つ目以降の Closure には引数ラベルを付ける。

ちなみに最初の Closure のラベルを省略せず書くとコンパイルエラーになる。

// compile error
UIView.animate(withDuration: 0.3, delay: 0, options: []) animations: {
    // ...
} completion: { _ in
    // ...
}

参考:
Closures — The Swift Programming Language (Swift 5.4)

iOS アプリがユニバーサルリンクから起動されたかを判定する

検証環境:
Xcode Version 12.5 (12E262)
Swift 5.4

ユニバーサルリンク(Universal Link)から iOS アプリが呼び出された場合にはAppDelegateapplication(_:continue:restorationHandler:)メソッドが呼び出される。
これはアプリが

  • 未起動状態からアプリ起動
  • バックグラウンド状態 からフォアグラウンドになる

のどちらでも呼び出される。

func application(_ application: UIApplication,
                  continue userActivity: NSUserActivity,
                  restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
        let webpageURL = userActivity.webpageURL {
        print("webpageURL: \(webpageURL)")
    }
    
    return true
}

アプリが未起動状態でユニバーサルリンクからアプリが起動した場合は、上記に書いたAppDelegateapplication(_:continue:restorationHandler:)メソッドの呼び出しに加えてAppDelegateapplication(_:didFinishLaunchingWithOptions:)メソッドの引数 launchOptions にも NSUserActivity パラメータが入っているので、それをチェックすることでアプリがユニバーサルリンクから起動されたかを判定することができる。

func application(_ application: UIApplication, 
                  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    if let userActivityDict = launchOptions?[.userActivityDictionary] as? [String: Any] {
        if let userActivity = userActivityDict["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity {
            if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
                let webpageURL = userActivity.webpageURL {
                NSLog("webpageURL: \(webpageURL)")
            }
        }
    }
    
    return true
}

呼び出し順は

  1. application(_:didFinishLaunchingWithOptions:)
  2. application(_:continue:restorationHandler:)

になる。

ちなみに NSUserActivity はユニバーサルリンク以外にも、Siriショートカットや、Spotlight検索でも使われるが、上記と同様な方法で判定できる。

App内課金の審査が「審査待ち」から進まない

App内課金の審査が審査待ち状態から進まなかった時の話。
アプリの審査については最近は1~2日で終わる。
今回は新規のアプリというわけではなく、追加で消耗型のApp内課金を追加したのだが、「審査待ち」状態のまま、何の音沙汰もなく1週間ほど過ぎてしまった。
さすがに遅すぎるのはと思ったので以下の問い合わせフォームから、AppReviewのお問い合わせというメニューを選択して審査を進めてほしいという旨のメッセージを送った。

https://developer.apple.com/contact/app-store/?topic=status

すると翌日、「承認済み」のステータスに変わった。
とりあえずよかったが、2021年現在でも審査見落としが発生するのはさすがになんとかしてほしいところ。

f:id:xyk:20210327111144p:plain

f:id:xyk:20210327111158p:plain

f:id:xyk:20210327111254p:plain

審査待ち f:id:xyk:20210327111309p:plain

審査完了 f:id:xyk:20210327111318p:plain

git でリモート追跡ブランチを解除する

検証環境:
git version 2.23.0

git で特定のブランチが既に何かのリモートブランチを追跡している状態で、その追跡をやめる方法、そして再度追跡させる方法についてメモ。

リモートブランチの追跡を解除する

以下コマンドでリモートブランチの追跡が解除されて、ローカルのみのブランチとなる。

$ git branch --unset-upstream <branch>

例:

# 解除前の状態確認
$ git branch -vv
* feature/hoge fefad7b5 [origin/feature/hoge] wip

# 解除
$ git branch --unset-upstream feature/hoge

# 解除後の状態確認
$ git branch -vv
* feature/hoge fefad7b5 wip

リモートブランチの追跡を開始する

以下コマンドでローカルブランチをアップストリームブランチにリンクさせてリモートブランチの追跡が開始される。

$ git branch --set-upstream-to <upstream>

# または
$ git branch -u <upstream>

例:

# 追跡追加
$ git branch --set-upstream-to origin/feature/hoge
Branch 'feature/hoge' set up to track remote branch 'feature/hoge' from 'origin'.

# 追加後の状態確認
$ git branch -vv
* feature/hoge fefad7b5 [origin/feature/hoge] wip

初回プッシュ時にリモート追跡ブランチの追加も同時に行う

$ git push --set-upstream-to origin <your-local-branch>

# または
$ git push -u origin <your-local-branch>

例:

# リモートブランチ名はローカルブランチ名と同名になる
$ git push -u origin feature/fuga
...
Branch 'feature/fuga' set up to track remote branch 'feature/fuga' from 'origin'.

# 明示的にリモートブランチ名を指定する場合
$ git push -u origin feature/fuga:feature/fuga