- 投稿日:2020-07-26T22:11:44+09:00
[swift5]端末毎のビュー崩れを防ぐ方法(コードで指定)
はじめに
今回はiPhoneの端末サイズによってビューが崩れる現象を修正する実装方法についてアウトプットします。具体的な方法はいくつかありますが、今回はコードで要素の位置を指定する方法です。
環境
swift5
Xcode 11.6要素の位置を指定
基本文法: 'CGRect(x:値, y:値, width:値, height:値)'
基本的にはHTMLのボックモデルと考え方が似ており、左上を基準に要素を動かします。
x軸は右に動かせば+、y軸は下に動かせば+となる。widthとheightは指定した要素の横縦のサイズを指定する。
液晶全体の幅に合わせたい場合は、'view.frame.size.width(height)'と指定すれば画面いっぱいに要素を配置してくれる。
- 投稿日:2020-07-26T21:57:50+09:00
Swiftでインスタグラムへのシェア導線をつけてみる
はじめに
今回はアプリからインスタグラムのフィードへのシェア導線をつける実装です。詳しくはこちらに書いてあります??
インスタグラムに遷移できるようにする
まずは、アプリからインスタグラムの Custom URL Scheme が利用できるように info.plist の
LSApplicationQueriesSchemes
にinstagram://
を追加していきましょう。画像を保存して 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) } } }
- 投稿日:2020-07-26T20:28:01+09:00
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を端末で取得し、サーバーサイドに渡す。
参考記事
iOS11で追加されたDeviceCheckについて
apple 公式:
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トークンの取得処理
- pushyという、iOSのプッシュ通知用のライブラリがあり、 p8ファイルからJWTトークンを取得する処理がある。
- その部分を参考にして実装。 https://github.com/jchambers/pushy/blob/master/pushy/src/main/java/com/eatthepath/pushy/apns/auth/ApnsSigningKey.java#L124-L170
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; }
- 投稿日:2020-07-26T20:25:35+09:00
【Xcode Behaviors】Xcodeのイベントに合わせて動作を設定する
開発効率を高める便利機能
Xcodeの設定にBehaviorsという項目があるのをご存知でしょうか?
この項目から、Build/Testing/Runningなどの各イベント発生時の挙動を設定することができます。例えば次のGIFのように、RunningのPausesイベント(デバッグ中のエラー発生やブレイクポイント到達)発生時に、別のタブを開いて該当部分を表示するといったことが可能です。
デフォルトの設定では、エラーが発生するたびに編集中のファイルからエラー発生箇所に移動してしまいますが、別タブで開く設定をしておけば、タブを移動するだけで編集中の状態に戻れるので、ストレスが軽減します。デバッグ中にエラーが発生したときやブレイクポイントに達したときに別のタブを開く
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"を選択することで、変数の内容を表示するビューとコンソールが表示されます。
ちなみにタブ名とインスペクターを隠す設定は任意です。ビルドに失敗したときに問題箇所を表示するナビゲーターを開く
Behaviors > Build > Fails から以下の項目を設定します。
- "Show" navigator "Issue navigator"
この項目はデフォルトで設定されているべきでは?と思うのですが、なぜかデフォルトではないようです。
テストに失敗したときに問題箇所を表示するナビゲーターを開く
ビルドのときと同様です。
Behaviors > Testing > Failsから以下の項目を設定します。
- "Show" navigator "Test navigator"
参考
公式ドキュメント(あまり情報がない)
- Customizing Your Workflow
- Configure actions for eventsタブをさらに使いこなしたい方はこちらが詳しいです
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える他の設定項目についてはこちら
Xcodeのオススメ初期セットアップ
- 投稿日:2020-07-26T20:25:35+09:00
【Xcode Behaviors】Xcodeのイベントに合わせて動作を設定する - 開発効率をUPする便利機能
開発効率をUPする便利機能
Xcodeの設定にBehaviorsという項目があるのをご存知でしょうか?
この項目から、Build/Testing/Runningなどの各イベント発生時の挙動を設定することができます。例えば次の画像のように、RunningのPausesイベント(デバッグ中のエラー発生やブレイクポイント到達)発生時に、別のタブを開いて該当部分を表示するといったことが可能です。
デフォルトの設定では、エラーが発生するたびに編集中のファイルからエラー発生箇所に移動してしまいますが、別タブで開く設定をしておけば、タブを移動するだけで編集中の状態に戻れるので、ストレスが軽減します。デバッグ中にエラーが発生したときやブレイクポイントに達したときに別のタブを開く
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"を選択することで、変数の内容を表示するビューとコンソールが表示されます。
ちなみにタブ名とインスペクターを隠す設定は任意です。ビルドに失敗したときに問題箇所を表示するナビゲーターを開く
Behaviors > Build > Fails から以下の項目を設定します。
- "Show" navigator "Issue navigator"
この項目はデフォルトで設定されているべきでは?と思うのですが、なぜかデフォルトではないようです。
テストに失敗したときに問題箇所を表示するナビゲーターを開く
ビルドのときと同様です。
Behaviors > Testing > Failsから以下の項目を設定します。
- "Show" navigator "Test navigator"
参考
公式ドキュメント(あまり情報がない)
- Customizing Your Workflow
- Configure actions for eventsタブをさらに使いこなしたい方はこちらが詳しいです
XcodeのBehaviorsを設定してデバッグ時にウインドウを自動で切り替える他の設定項目についてはこちら
Xcodeのオススメ初期セットアップ
- 投稿日:2020-07-26T20:00:37+09:00
[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にアクセスするための定義を抽象化
実装
全体像
- ContentView:UI表示
- GitHubSearchModel:ロジックを管理するクラス
- GitHubRepository:APIへのアクセス方法を定義
API基本部分
API部分のクラス相関図は以下のようになります。
GitHubRepository.swiftについては、中身は以下のようになっています。
GitHubRepository.swiftimport 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.swiftとGitHubDecoder.swiftとAPIDataParser.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
こちらをご確認ください。実際に使ってみた
ソースコード
今回のソースコードはこちらに置いてあります。
https://github.com/usk-lab/APIKitCombine再利用性について
今回はGitHub APIを使用しました。
GitHubRepository
,GitHubRequestProtocol
,GitHubDecoder
を少し変えることで他のAPIサービスでも利用できます。参考
- https://github.com/ishkawa/APIKit/
- https://developer.github.com/v3/search/
- https://qiita.com/woxtu/items/ec689cf7709c3ce7d954
- https://qiita.com/renchild8/items/6cdaaad52c900f6efd38
- https://qiita.com/sgr-ksmt/items/e822a379d41462e05e0d
- https://dev.classmethod.jp/articles/rxswift-apikit-decodable-incremental-search/
- https://dev.classmethod.jp/articles/ios13-swiftui-combine-incremental-search/
- 投稿日:2020-07-26T13:21:46+09:00
超簡単にViewの背景をぼかす方法【iOS開発】
背景をぼかしたい!!
背景をぼかしたいなーと思った時にはUIVisualEffectViewを使うと思うんですけど、ぼかす処理を毎回書くのがめんどくさい!!
というわけで超簡単に背景ぼかしができるextensionを作りましょうextensionを書く!
新しいファイルを作ってこれを書きましょう!
addVisualEffect.swiftimport 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.swiftoverride func viewDidLoad() { super.viewDidLoad() self.view.addVisualEffect() }たったこれだけで背景をぼかすことができるんです!!便利!!
最後に
メモ程度の記事に最後までつきあってくれてありがとうございます
是非使ってみてください!!環境:Xcode11.6(11E708)
- 投稿日:2020-07-26T12:37:45+09:00
【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 に適合するために必要な実装をします。
必要最低限の実装
AnimatableValues
をVectorArithmetic
に適合するためには、ベクトル空間の公理系を満たす必要があり、以下のように実装します。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] } }
- 投稿日:2020-07-26T11:07:38+09:00
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!)