20200726のiOSに関する記事は9件です。

[swift5]端末毎のビュー崩れを防ぐ方法(コードで指定)

はじめに

今回はiPhoneの端末サイズによってビューが崩れる現象を修正する実装方法についてアウトプットします。具体的な方法はいくつかありますが、今回はコードで要素の位置を指定する方法です。

環境

swift5
Xcode 11.6

要素の位置を指定

基本文法: 'CGRect(x:値, y:値, width:値, height:値)'

基本的にはHTMLのボックモデルと考え方が似ており、左上を基準に要素を動かします。
x軸は右に動かせば+、y軸は下に動かせば+となる。

widthとheightは指定した要素の横縦のサイズを指定する。
液晶全体の幅に合わせたい場合は、'view.frame.size.width(height)'と指定すれば画面いっぱいに要素を配置してくれる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftでインスタグラムへのシェア導線をつけてみる

はじめに

今回はアプリからインスタグラムのフィードへのシェア導線をつける実装です。詳しくはこちらに書いてあります👨‍💻

インスタグラムに遷移できるようにする

まずは、アプリからインスタグラムの Custom URL Scheme が利用できるように info.plist の LSApplicationQueriesSchemesinstagram://を追加していきましょう。

画像を保存して LocalIdentifier を取得する

    func saveImage() {
        UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
    }

    @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        let fetchOptions: PHFetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]

        let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
        if (fetchResult.firstObject != nil) {
            guard let lastAsset = fetchResult.lastObject else {
                return
            }
            let localIdentifier = lastAsset.localIdentifier
        }
    }

LocalIdentifier を使って Custom URL Scheme でインスタグラムに遷移する

    @objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
        let fetchOptions: PHFetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]

        let fetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
        if (fetchResult.firstObject != nil) {
            guard let lastAsset = fetchResult.lastObject, let urlScheme = URL(string: "instagram://library?LocalIdentifier=\(lastAsset.localIdentifier)") else {
                return
            }
            if UIApplication.shared.canOpenURL(urlScheme) {
                UIApplication.shared.open(urlScheme)
            }
        }
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS: DeviceCheckの参照、保存をJavaで実装。(アカウントBan,リセマラ防止対応など)

iOSのdevice checkで出来ること。

  • 端末から取得できるdevice tokenを元に、端末単位で2つのビット値をAppleのAPIに記録、参照できる。
  • 端末を初期化、アプリのアンインストール&インストールしても保存されたビット値はリセットされない。

どんなことに利用できるか。

リセマラ防止。

  • (初期インストール時、device checkのAPIに記録すると、再インストールされたことを判定できる。)

深刻な不正行為をするユーザーを端末単位で、アカウントbanする。

  • device checkのAPIで、アカウントbanされた端末であることを記録。
  • アプリの再インストール、端末の初期化後も、以前にアカウントbanされた端末である場合、利用できないようにする。

利用時の注意点

  • 端末が中古市場等で別の人に渡った場合でも、保持される値なので慎重な運用が必要。
  • 端末から取得したdevice tokenは時間的な有効期限があるみたい。(この部分の記憶は定かじゃないので、テストしてみてください。)アカウントbanで利用する場合は、banした後の次のアプリ起動時にdevice tokenをサーバーサイドに送って、そこですぐにdevice checkのAPIで更新するような工夫が必要。

事前準備、前提

  • 認証キーのp8ファイルを取得。
  • KEY_ID, TEAM_IDを取得
  • DeviceTokenを端末で取得し、サーバーサイドに渡す。

参考記事

DeviceCheck APIのURL

baseのAPI

開発

https://api.development.devicecheck.apple.com/v1/

本番運用時

https://api.devicecheck.apple.com/v1/

APIの機能

参照:query_two_bits

https://api.devicecheck.apple.com/v1/query_two_bits

更新:update_two_bits

https://api.devicecheck.apple.com/v1/update_two_bits

device tokenのバリデーション:validate_device_token

https://api.devicecheck.apple.com/v1/validate_device_token

プログラム

iOSアプリでdevice tokenを取得。

※シミュレーターでは取得できません。

import DeviceCheck

DCDevice.current.generateToken {
    (data, error) in
    guard let data = data else {
        return
    }

    let token = data.base64EncodedString()
    print(token)
}

サーバーサイド(Java)

取得、更新サンプル

// 取得
private void queryIOSDeviceCheckSample() {
    // この値で2つのbool値と最終更新日時を取得できる。
    Response response = postRequest(DEVELOPMENT_BASE_API_URL + "query_two_bits", "端末から取得したdevicetoken", null, null);
}

// 更新
private void updateIOSDeviceCheckSample(Boolean bit0, Boolean bit1) {
    Response response = postRequest(DEVELOPMENT_BASE_API_URL + "update_two_bits", "端末から取得したdevicetoken", null, null);
}

API呼び出し処理

  • "Authorization"ヘッダーにJWTのトークンを設定。
  • request bodyにjson形式で各パラメーターを設定。
    • device_token, transaction_id, timestamp, bit0, bit1
private static Response postRequest(String url, String deviceToken, Boolean bit0, Boolean bit1) throws IOException {
    MediaType JSON = MediaType.get("application/json; charset=utf-8");

    JSONObject jsonObject = new JSONObject();
    jsonObject.put("device_token", deviceToken);
    jsonObject.put("transaction_id", UUID.randomUUID().toString());
    jsonObject.put("timestamp", new Date().getTime());
    if (bit0 != null) {
        jsonObject.put("bit0", bit0);
    }
    if (bit1 != null) {
        jsonObject.put("bit1", bit1);
    }
    String json = jsonObject.toJSONString();

    RequestBody body = RequestBody.create(JSON, json);

    String jwt = getJWTStr();
    if (jwt == null) {
        return null;
    }
    Request request = new Request.Builder()
            .url(url)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .header("Content-Length", String.valueOf(json.length()))
            .header("Authorization", "Bearer " + jwt)
            .post(body)
            .build();
    OkHttpClient client = new OkHttpClient();
    return client.newCall(request).execute();
}

p8ファイルから、JWTトークンを取得。

プッシュ通知用ライブラリ「pushy」のソースを参考に実装:JWTトークンの取得処理

JTW(=JSON Web Token)

https://ja.wikipedia.org/wiki/JSON_Web_Token

private static String getJWTStr() {
    try {
        ECPrivateKey privateKey = getECPrivateKey(P8_SECRET_KEY_PATH);
        return Jwts.builder()
            .setHeaderParam("kid", KEY_ID)
            .setIssuer(TEAM_ID)
            .setIssuedAt(new Date())
            .signWith(privateKey, SignatureAlgorithm.ES256)
            .compact();
    } catch (Exception e) {
        return null;
    }
}

private static ECPrivateKey getECPrivateKey(String p8FilePath) throws Exception {

    final FileInputStream fileInputStream = new FileInputStream(new File(p8FilePath));
    final ECPrivateKey signingKey;
    {
        final String base64EncodedPrivateKey;
        {
            final StringBuilder privateKeyBuilder = new StringBuilder();

            final BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
            boolean haveReadHeader = false;
            boolean haveReadFooter = false;

            for (String line; (line = reader.readLine()) != null; ) {
                if (!haveReadHeader) {
                    if (line.contains("BEGIN PRIVATE KEY")) {
                        haveReadHeader = true;
                    }
                } else {
                    if (line.contains("END PRIVATE KEY")) {
                        haveReadFooter = true;
                        break;
                    } else {
                        privateKeyBuilder.append(line);
                    }
                }
            }

            if (!(haveReadHeader && haveReadFooter)) {
                throw new IOException("Could not find private key header/footer");
            }

            base64EncodedPrivateKey = privateKeyBuilder.toString();
        }

        final byte[] keyBytes = Base64.getDecoder().decode(base64EncodedPrivateKey.getBytes(StandardCharsets.US_ASCII));

        final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        final KeyFactory keyFactory = KeyFactory.getInstance("EC");

        try {
            signingKey = (ECPrivateKey) keyFactory.generatePrivate(keySpec);
        } catch (InvalidKeySpecException e) {
            throw new InvalidKeyException(e);
        }
    }
    return signingKey;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Xcode Behaviors】Xcodeのイベントに合わせて動作を設定する

開発効率を高める便利機能

Xcodeの設定にBehaviorsという項目があるのをご存知でしょうか?
この項目から、Build/Testing/Runningなどの各イベント発生時の挙動を設定することができます。

例えば次のGIFのように、RunningのPausesイベント(デバッグ中のエラー発生やブレイクポイント到達)発生時に、別のタブを開いて該当部分を表示するといったことが可能です。
xcode-behaviors.gif
デフォルトの設定では、エラーが発生するたびに編集中のファイルからエラー発生箇所に移動してしまいますが、別タブで開く設定をしておけば、タブを移動するだけで編集中の状態に戻れるので、ストレスが軽減します。

デバッグ中にエラーが発生したときやブレイクポイントに達したときに別のタブを開く

behaviors.png
Behaviors > Running > Pauses から以下の項目を設定します。

  • Show tab named "Debug" in "active window"
  • "Show" navigator "Debug navigator"
  • "Show" debugger with "Variables & Console View"
  • "Hide" inspectors

ナビゲーターに"Debug navigator"に選択することで、スタックトレースが表示されます。
デバッガーに"Variables & Console View"を選択することで、変数の内容を表示するビューとコンソールが表示されます。
ちなみにタブ名とインスペクターを隠す設定は任意です。

ビルドに失敗したときに問題箇所を表示するナビゲーターを開く

スクリーンショット 2020-07-26 20.13.58.png
Behaviors > Build > Fails から以下の項目を設定します。

  • "Show" navigator "Issue navigator"

この項目はデフォルトで設定されているべきでは?と思うのですが、なぜかデフォルトではないようです。

テストに失敗したときに問題箇所を表示するナビゲーターを開く

ビルドのときと同様です。
Behaviors > Testing > Failsから以下の項目を設定します。

  • "Show" navigator "Test navigator"

参考

公式ドキュメント(あまり情報がない)
- Customizing Your Workflow
- Configure actions for events

タブをさらに使いこなしたい方はこちらが詳しいです
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える

他の設定項目についてはこちら
Xcodeのオススメ初期セットアップ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Xcode Behaviors】Xcodeのイベントに合わせて動作を設定する - 開発効率をUPする便利機能

開発効率をUPする便利機能

Xcodeの設定にBehaviorsという項目があるのをご存知でしょうか?
この項目から、Build/Testing/Runningなどの各イベント発生時の挙動を設定することができます。

例えば次の画像のように、RunningのPausesイベント(デバッグ中のエラー発生やブレイクポイント到達)発生時に、別のタブを開いて該当部分を表示するといったことが可能です。
before-running.pngrunning-fails.png
デフォルトの設定では、エラーが発生するたびに編集中のファイルからエラー発生箇所に移動してしまいますが、別タブで開く設定をしておけば、タブを移動するだけで編集中の状態に戻れるので、ストレスが軽減します。

デバッグ中にエラーが発生したときやブレイクポイントに達したときに別のタブを開く

behaviors.png
Behaviors > Running > Pauses から以下の項目を設定します。

  • Show tab named "Debug" in "active window"
  • "Show" navigator "Debug navigator"
  • "Show" debugger with "Variables & Console View"
  • "Hide" inspectors

ナビゲーターに"Debug navigator"に選択することで、スタックトレースが表示されます。
デバッガーに"Variables & Console View"を選択することで、変数の内容を表示するビューとコンソールが表示されます。
ちなみにタブ名とインスペクターを隠す設定は任意です。

ビルドに失敗したときに問題箇所を表示するナビゲーターを開く

スクリーンショット 2020-07-26 20.13.58.png
Behaviors > Build > Fails から以下の項目を設定します。

  • "Show" navigator "Issue navigator"

この項目はデフォルトで設定されているべきでは?と思うのですが、なぜかデフォルトではないようです。

テストに失敗したときに問題箇所を表示するナビゲーターを開く

testing.png
ビルドのときと同様です。
Behaviors > Testing > Failsから以下の項目を設定します。

  • "Show" navigator "Test navigator"

参考

公式ドキュメント(あまり情報がない)
- Customizing Your Workflow
- Configure actions for events

タブをさらに使いこなしたい方はこちらが詳しいです
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える

他の設定項目についてはこちら
Xcodeのオススメ初期セットアップ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS]APIKit + Combine + SwiftUIでGitHubリポジトリ検索

iOS13に出てきたSwiftUI・Combineと、ライブラリのAPIKitを使い、通信部分の実装手法を紹介したいと思います。
今回は、GitHub APIを使用します。

実装のポイント

  • できるだけシンプル:冗長なコードはなるべく書かない
  • 保守性がある:読みやすいコード
  • 再利用性がある:別の異なるAPIでも少しの変更で利用が可能

開発環境・使用するライブラリなど

開発環境

  • OS:10.15.6
  • Xcode:10.6
  • iOS:iOS13以降(iOS14では確認していません)

使用するライブラリ

APIKit
今回は、APIKitを拡張し、CombineのPublisherでAPIを叩いた結果を取得します。

使用するフレームワーク

  • SwiftUI
  • Combine

また、Swift標準のDecodableを使い、JSONをパースします。

使用する考え方

  • リポジトリパターン:APIやDBにアクセスするための定義を抽象化

実装

全体像

APIKit.png

  • ContentView:UI表示
  • GitHubSearchModel:ロジックを管理するクラス
  • GitHubRepository:APIへのアクセス方法を定義

API基本部分

API部分のクラス相関図は以下のようになります。

APIKit-2.jpg

GitHubRepository.swiftについては、中身は以下のようになっています。

GitHubRepository.swift
import Foundation
import APIKit

class GitHubRepository {

    //GitHubレスポンス用のデコーダー
    static let decoder: GitHubDecoder = .init()

    //リポジトリを検索
    struct SearchRepositories: GitHubRequestProtocol {
        //https://developer.github.com/v3/search

        //検索クエリ
        let query: String

        let method: HTTPMethod = .get
        let path: String = "/search/repositories"
        var decoder: JSONDecoder {
            return GitHubRepository.decoder
        }

        var params: [String: Any] {
            return [
                "q": query
            ]
        }

        typealias Response = SearchResponse

    }

    private init() { }
}

GitHubRepositoryはリポジトリなので、APIアクセスに必要な情報だけを定義(抽象化)しています。
SearchResponseはDecodableに準拠しています。
GitHubRequestProtocol.swiftGitHubDecoder.swiftAPIDataParser.swiftはソースをご確認ください。

ここまでで、GitHub APIを叩くための準備ができました。

Modelの実装

GitHub APIを使い、情報を取ってくるロジック(モデル)を実装します。
今回はViewとしてSwiftUIを使用するので、ロジックはObservableObjectを継承したクラスにします。

GitHubSearchModel.swift
/// 検索モデル
class GitHubSearchModel: ObservableObject {

    @Published var items: [SearchItem] = [] //検索結果
・・・・
    /// 検索を行う
    func search() {
        debugPrint("search")

        // 1)APIリクエストを作成
        let request = GitHubRepository.SearchRepositories(query: searchText)

        // 2)CombineのPublisherを作成し、通信処理を行う
        self.requestCancellable = request.publisher //Publisherに変換(この時点で通信処理を行なっている)
            .receive(on: DispatchQueue.main) //メインスレッドで受け取る
            .sink(receiveCompletion: { result in
・・・・
            }, receiveValue: { [weak self] response in
                // 3)結果をitemsに保存
                self?.items = response.items
            })
        // 4)itemsの更新はCombineを通して通知される

    }

1) APIリクエストを前述のリポジトリから作成します。
2) 通信処理を行います。
3) 結果をitemsに格納します
4) itemsの変更はCombineによりUIに通知されます。

UI

ContentView
こちらをご確認ください。

実際に使ってみた

apikit.gif

ソースコード

今回のソースコードはこちらに置いてあります。
https://github.com/usk-lab/APIKitCombine

再利用性について

今回はGitHub APIを使用しました。
GitHubRepository, GitHubRequestProtocol, GitHubDecoderを少し変えることで他のAPIサービスでも利用できます。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超簡単にViewの背景をぼかす方法【iOS開発】

背景をぼかしたい!!

背景をぼかしたいなーと思った時にはUIVisualEffectViewを使うと思うんですけど、ぼかす処理を毎回書くのがめんどくさい!!
というわけで超簡単に背景ぼかしができるextensionを作りましょう

extensionを書く!

新しいファイルを作ってこれを書きましょう!

addVisualEffect.swift
import UIKit

extension UIView {
    func addVisualEffect() {
        //スクリーンサイズ取得!
        let width = UIScreen.main.bounds.size.width
        let height = UIScreen.main.bounds.size.height

        //ブラーエフェクト生成!
        let blurEffect = UIBlurEffect(style: .light)

        //ブラーエフェクトからエフェクトビューを生成!
        let visualEffectView = UIVisualEffectView(effect: blurEffect)

        //エフェクトビューのサイズ指定!
        visualEffectView.frame = CGRect(x: 0, y: 0, width: width, height: height)

        //もとのViewにaddSubView!!
        self.addSubview(visualEffectView)

        //重ね順を一番下に!!
        self.sendSubviewToBack(visualEffectView)
    }
}

ポイントはサイズをしっかり合わせるところと重ね順を一番下にすることですかね!

あ、そういえばブラーエフェクトには種類があるんですよ
(ここに詳しく書いてありました! -> https://dev.classmethod.jp/references/ios8-uivisualeffectview/
extraLightとlightとdartの3種類です!
だから引数を指定してあげればもっと使い勝手良くなる気がします
僕はlightしか使わないのでこれでいきます!

実際に使う!

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.addVisualEffect()
    }

たったこれだけで背景をぼかすことができるんです!!便利!!

最後に

メモ程度の記事に最後までつきあってくれてありがとうございます
是非使ってみてください!!

環境:Xcode11.6(11E708)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】VectorArithmetic を自作して AnimatablePair の型パラメータ地獄から解脱する

はじめに

モチベーション

SwiftUI でアニメーション可能なシェイプを作成する場合は animatableData を実装する必要があります。

アニメーション可能なパラメータが1つの場合は次のような実装をします。

public struct HogeShape: Shape {
    public var foo: CGFloat

    public var animatableData: CGFloat {
        get { foo }
        set { foo = newValue }
    }

    public func path(in rect: CGRect) -> Path {
        // foo をパラメータにした Path を作る
    }
}

animatableData の型は必ずしも CGFloat である必要はなく、VectorArithmetic に適合していれば OK です。次の型が既知で適合しています。

  • CGFloat
  • Double
  • Float
  • AnimatablePair
  • EmptyAnimatableData

AnimatablePair を利用することで、次のようにパラメータを2つ指定することができます。

public struct FugaShape: Shape {
    public var foo: CGFloat
    public var bar: CGFloat

    public var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get {
            AnimatablePair(foo, bar)
        }
        set {
            foo = newValue.first
            bar = newValue.second
        }
    }

    public func path(in rect: CGRect) -> Path {
        // foo, bar をパラメータにした Path を作る
    }
}

AnimatablePair の型パラメータには、任意の VectorArithmetic を指定することが可能なので、型パラメータに AnimatablePair を指定することができます。

これを利用してパラメータを3つにすることができます。

public struct PiyoShape: Shape {
    public var foo: CGFloat
    public var bar: CGFloat
    public var baz: CGFloat

    public var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>> {
        get {
            .init(foo, .init(bar, baz))
        }
        set {
            foo = newValue.first
            bar = newValue.second.first
            baz = newValue.second.second
        }
    }

    public func path(in rect: CGRect) -> Path {
        // foo, bar, baz をパラメータにした Path を作る
    }
}

AnimatablePair の型パラメータに AnimatablePair を設定していくことで、いくらでもアニメーション可能なプロパティを追加することができますが、型パラメータがネストされていくので可読性が落ちていきます。パラメータを5つにすると次のようになります。

var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>>>> {
        get {
            .init(prop0, .init(prop1, .init(prop2, .init(prop3, prop4))))
        }
        set {
            prop0 = newValue.first
            prop1 = newValue.second.first
            prop2 = newValue.second.second.first
            prop3 = newValue.second.second.second.first
            prop4 = newValue.second.second.second.second
        }
    }

プロパティが5つあることが、現実の解決すべき問題として多いか少ないかについては何とも言えないところですが、単純なシェイプでも柔軟にアニメーションをつけようとすると、プロパティの数はそれなりに必要になってくると思います。

たとえば、角を丸くした長方形のシェイプで考えてみます。
角丸の半径に加えて、各四隅の角丸の有無を自由に制御できるようにすると、単純なシェイプでも5つのプロパティが必要になります。
これらが自由に制御できれば、ビューのマスク処理にアニメーションをつけることも容易になります。

本記事では、VectorArithmetic に適合した AnimatableValues を自作することで、上記のネストを次のようなシンプルな実装にします。

public var animatableData: AnimatableValues {
    get {
        .init(prop0, prop1, prop2, prop3, prop4)
    }
    set {
        let values: [CGFloat] = newValue.values()
        prop0 = values[0]
        prop1 = values[1]
        prop2 = values[2]
        prop3 = values[3]
        prop4 = values[4]
    }
}

環境

  • Xcode 11.6 (11E708)
  • Swift 5.2.4

VectorArithmetic を実装する

アニメーション可能なプロパティをフラットに複数保持するために Array で値を管理します。

public struct AnimatableValues: VectorArithmetic {
    private var values: [Double]
}

あとは、VectorArithmetic に適合するために必要な実装をします。

必要最低限の実装

AnimatableValuesVectorArithmetic に適合するためには、ベクトル空間の公理系を満たす必要があり、以下のように実装します。

zero

static var zero: Self { get }

ゼロ元を返す必要があります。ゼロ元はどのような AnimatableValues と加算してもプロパティを変更しないように実装します。

+ 演算子

static func + (lhs: Self, rhs: Self) -> Self

各要素ごとに加算したオブジェクトを返す必要があります。

- 演算子

static func - (lhs: Self, rhs: Self) -> Self

各要素ごとに減算したオブジェクトを返す必要があります。

scale(by:)

mutating func scale(by rhs: Double)

各要素ごとに rhs 倍したオブジェクトを返す必要があります。

magnitudeSquared

var magnitudeSquared: Double { get }

各要素の二乗した和を返します。

実装例

上記を満たすような実装例です。

固定長のリストであれば .zero[0, 0, ..., 0] のような実装が望ましいところですが、Array で保持している都合で実現できないので、空のリスト [] で代替しておき、+, - 演算を実行するタイミングでゼロ埋めしているのが実装のポイントです。これで公理系を満たすことができます。

import SwiftUI
import enum Accelerate.vDSP

public struct AnimatableValues: VectorArithmetic, Hashable {
    private var values: [Double]

    public init(values: [Double]) {
        self.values = values
    }

    public init<F: BinaryFloatingPoint>(values: [F]) {
        self.init(values: values.map(Double.init))
    }

    public init<F: BinaryFloatingPoint>(_ values: F...) {
        self.init(values: values.map(Double.init))
    }

    public func values<F: BinaryFloatingPoint>(_ type: F.Type = F.self) -> [F] {
        values.map(F.init)
    }
}

// MARK: - VectorArithmetic
public extension AnimatableValues {
    mutating func scale(by rhs: Double) {
        values = vDSP.multiply(rhs, values)
    }

    var magnitudeSquared: Double {
        vDSP.sum(vDSP.multiply(values, values))
    }
}

// MARK: - AdditiveArithmetic
public extension AnimatableValues {
    static var zero: Self {
        .init(values: [])
    }

    static func + (lhs: Self, rhs: Self) -> Self {
        .init(values: operate(vDSP.add, lhs, rhs))
    }

    static func - (lhs: Self, rhs: Self) -> Self {
        .init(values: operate(vDSP.subtract, lhs, rhs))
    }

    private static func operate(_ operation: ([Double], [Double]) -> [Double], _ lhs: Self, _ rhs: Self) -> [Double] {
        let count = max(lhs.values.count, rhs.values.count)
        let lhs = lhs.values + Array(repeating: 0, count: count - lhs.values.count)
        let rhs = rhs.values + Array(repeating: 0, count: count - rhs.values.count)
        return operation(lhs, rhs)
    }
}

この実装によって、animatableData は次のように簡潔に記述できるようになります。

public var animatableData: AnimatableValues {
    get {
        .init(prop0, prop1, prop2, prop3, prop4)
    }
    set {
        let values: [CGFloat] = newValue.values()
        prop0 = values[0]
        prop1 = values[1]
        prop2 = values[2]
        prop3 = values[3]
        prop4 = values[4]
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dateを扱うときは12時間表記設定を考慮しよう。

要約

  • カスタムフォーマットの日時文字列からDate変換時にnilが返却される。
  • シミュレータでは発生しない、実機で発生する
  • 国内のみのサービスでもDateFormatterにはLocaleは必ず設定し利用する。
  • 可能であれば実機利用のテストのタイミングに組み込む

現象

文字列からDate変換するときに、端末の設定が12時間表記になっていると必ずnilが発生。
シミュレータでは発生しないので、忘れがち。

問題になるパターン

変換文字列 24時間 12時間 シミュレータ
時刻を含む文字列 成功 失敗(nil) 成功
時刻を含まない文字列 成功 成功 成功

参考 問題になるコード

let dateFormmater = DateFormatter()
dateFormmater.dateFormat = "yyyy-MM-dd HH:mm:ss"

/// 12時間表記の際に date == nil となる.
let date = dateFormmater.date(from: "2020-03-02 10:00:00")

対処方法

Formatter設定時Localeを設定する

実機でもdateFormatterにはLocaleが設定されているものの、localeを改めて設定する必要がある。

let dateFormmater = DateFormatter()
dateFormmater.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormmater.locale = Locale(identifier: "en_US_POSIX")

/// 12時間表記の際でもnilとならない。
let date = dateFormmater.date(from: "2020-03-02 10:00:00")

忘れがちなのでExtenstionにしてしまう.

状況によって固定値も含めてしまうとよい。

public extension DateFormatter {

    static var standard: DateFormatter {
        let standard = DateFormatter()
        standard.dateFormat = "yyyy-MM-dd HH:mm:ss"
        standard.locale = Locale(identifier: "en_US_POSIX")
        return standard
    }
}

コードで強力に制約をするのであれば、DateFormatterをwrapするのが良さそう。

おまけ

海外対応時のメッセージの受信時刻表示で必要なこと

  • 時刻データのタイムゾーンとクライアントのタイムゾーンを考慮する
  • 時刻表記のフォーマットを考慮する(ここでは触れていない)
let dateFormmater = DateFormatter()
dateFormmater.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormmater.locale = Locale(identifier: "en_US_POSIX")
/// 時刻データのタイムゾーンでDateへ変換.
dateFormatter.timeZone = TimeZone(abbreviation: "JST")
let date = dateFormmater.date(from: "2020-03-02 10:00:00")

/// 端末のタイムゾーンで文字列に変換.
dateFormatter.timeZone = TimeZone.current
let dateString = dateFormatter.string(from: date!)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む