xyk blog

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

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

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

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

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

let calendar = Calendar.current
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.current
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

Swift で少数第二位や第三位で丸め処理を行う

検証環境:
Xcode 12.4
Swift 5.3.2

Swift で少数第二位や第三位で丸め処理を行う方法について。
例えば第二位で四捨五入したいのであれば、対象の少数値にまず10を掛けてからrounded()で四捨五入し、その後に10で割ればよい。

実行例

let pi = Double(3.1415)

// 少数第1位で四捨五入する
let ret1 = pi.rounded() // 3

// 少数第2位で四捨五入する
let ret2 = (pi * 10).rounded() / 10  // 3.1

// 少数第3位で四捨五入する
let ret3 = (pi * 100).rounded() / 100 // 3.14

// 少数第4位で四捨五入する
let ret4 = (pi * 1000).rounded() / 1000 // 3.142

rounded メソッドによる丸めのルールは前回の記事を参照。

xyk.hatenablog.com

Swift の Float(CGFloat) や Double で小数点の丸め処理を行う

検証環境:
Xcode 12.4
Swift 5.3.2

Swift の浮動小数点数型である Float(CGFloat) や Double で小数点の切り捨て、切り上げ、四捨五入などの丸め処理をするには Swift3 から追加された FloatingPoint プロトコルの extension に定義されている roundedメソッドを使えばよい。
(roundメソッドもあるが、roundedが非破壊メソッドであるのに対しroundは破壊メソッドとなる)
ちなみに以前はceil(切り捨て)、floor(切り上げ)、round(四捨五入)のグローバルな関数を使っていた。

引数なし rounded

Float

func rounded() -> Float

Double

func rounded() -> Double

引数なし rounded メソッドは四捨五入される。
0.5より小さければ切り捨て、0.5より大きければ切り上げ、ちょうど0.5なら0から遠い方の数値へ丸める。

実行例

(5.2).rounded()
// 5.0
(5.5).rounded()
// 6.0
(-5.2).rounded()
// -5.0
(-5.5).rounded()
// -6.0

引数あり rounded

Float

func rounded(_ rule: FloatingPointRoundingRule) -> Float

Double

func rounded(_ rule: FloatingPointRoundingRule) -> Double

引数の enum である FloatingPointRoundingRule には

  • up
  • down
  • toNearestOrAwayFromZero
  • toNearestOrEven
  • towardZero
  • awayFromZero

の6つが定義されている。

rounded(.up)

切り上げ。大きい方の数値に丸める。

実行例

(5.2).rounded(.up)
// 6.0
(5.5).rounded(.up)
// 6.0
(-5.2).rounded(.up)
// -5.0
(-5.5).rounded(.up)
// -5.0

rounded(.down)

切り下げ。小さい方の数値に丸める。

実行例

(5.2).rounded(.down)
// 5.0
(5.5).rounded(.down)
// 5.0
(-5.2).rounded(.down)
// -6.0
(-5.5).rounded(.down)
// -6.0

rounded(.towardZero)

0に近い数値に丸める。

実行例

(5.2).rounded(.towardZero)
// 5.0
(5.5).rounded(.towardZero)
// 5.0
(-5.2).rounded(.towardZero)
// -5.0
(-5.5).rounded(.towardZero)
// -5.0

rounded(.awayFromZero)

0から遠い数値に丸める。

実行例

(5.2).rounded(.awayFromZero)
// 6.0
(5.5).rounded(.awayFromZero)
// 6.0
(-5.2).rounded(.awayFromZero)
// -6.0
(-5.5).rounded(.awayFromZero)
// -6.0

rounded(.toNearestOrAwayFromZero)

schoolbook rounding(教科書の丸め?)と呼ばれる丸め規則に従う。[参照]

最も近い許容値に丸めます。2つの値が等しく近い場合は、大きさが大きい方が選択されます。

0.5より小さければ切り捨て、0.5より大きければ切り上げ、ちょうど0.5なら0から遠い(AwayFromZero)方の数値へ丸める。

こちらは引数なしのrounded()と同じ挙動であり四捨五入となる。

実行例

(5.2).rounded(.toNearestOrAwayFromZero)
// 5.0
(5.5).rounded(.toNearestOrAwayFromZero)
// 6.0
(-5.2).rounded(.toNearestOrAwayFromZero)
// -5.0
(-5.5).rounded(.toNearestOrAwayFromZero)
// -6.0

rounded(.toNearestOrEven)

bankers rounding(銀行家の丸め?)と呼ばれる丸め規則に従う。[参照]

最も近い許容値に丸めます。2つの値が等しく近い場合は、偶数の値が選択されます。

0.5より小さければ切り捨て、0.5より大きければ切り上げ、ちょうど0.5なら偶数(Even)となる方の数値へ丸める。

実行例

(5.2).rounded(.toNearestOrEven)
// 5.0
(5.5).rounded(.toNearestOrEven)
// 6.0
(4.5).rounded(.toNearestOrEven)
// 4.0
(-5.2).rounded(.toNearestOrEven)
// -5.0
(-5.5).rounded(.toNearestOrEven)
// -6.0
(-4.5).rounded(.toNearestOrEven)
// -4.0

参考

Float
developer.apple.com

CGFloat
developer.apple.com

Double
developer.apple.com

Proposal github.com

UITabBar の特定タブの選択時の色を変える

検証環境:
Xcode 12.4
Swift 5.3.2
iOS Deployment Target 14.4

UITabBar 使用時に、特定タブ(UITabBarItem)のみ、選択時の色を変える方法について。

2つ目のタブを赤に変更
f:id:xyk:20210410130748p:plain

その他のタブはデフォルトの tint color
f:id:xyk:20210410130858p:plain

タイトル文字色はtabBarItem.setTitleTextAttributesで指定、
画像色はtabBarItem.selectedImageにセットする UIImage の withRenderingMode に .alwaysOriginal を指定することで変更できる。
UITabBarController のサブクラスを作ってその中の viewDidLoad で変更している。

サンプルコード

class TabBarController: UITabBarController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let targetIndex = 1 // 変えたいタブの場所。今回は2つ目とする。
        
        if let tabBarItems = tabBar.items, tabBarItems.indices.contains(targetIndex) {
            let tabBarItem = tabBarItems[targetIndex]
            
            // タイトル色の変更
            tabBarItem.setTitleTextAttributes([.foregroundColor: UIColor.systemRed], for: .selected)
            
            // 画像色の変更
            let image = tabBarItem.image! // 元の画像を取得
            let tintedImage = image.tinted(with: .systemRed).withRenderingMode(.alwaysOriginal) // 画像の色変更とRenderingMode指定
            tabBarItem.selectedImage = tintedImage
        }
    }
}

// 画像の色を変える UIImage Extension
extension UIImage {
    
    func tinted(with color: UIColor) -> UIImage {
        return UIGraphicsImageRenderer(size: size).image { context in
            let rect = CGRect(origin: .zero, size: size)
            draw(in: rect)
            color.setFill()
            context.fill(rect, blendMode: .sourceIn)
        }
    }
}

StoaryBoard側でタブの画像、タイトルは設定済み。
UITabBarController にこのカスタムクラスを設定する。
UIImageの色を変える方法については以下の記事で書いた。

xyk.hatenablog.com