20220116のSwiftに関する記事は4件です。

SwiftUIでのプレビューをより便利にする

概要 よく使うSwiftUIのプレビュー機能は実装を共通化しておくと便利です。 本記事では一例として以下の実装例を紹介します。 BoolPreview|Bool値のプレビュー ColorSchemePreview|外観モードのプレビュー LocalizedPreview|ローカライズ言語のプレビュー BoolPreview 利用例 ViewのもつBool値の状態を切り替えてプレビューします。 こんなふうに入れ子にして利用するのも便利です。 実装 struct BoolPreview<Content>: View where Content: View { let content: (Bool) -> Content var body: some View { ForEach([true, false], id: \.self) { boolValue in content(boolValue) } } } プロジェクト全体でBoolPreviewを利用すると、必ずプレビューがtrue→falseなど順序を統一できるという効果もあります。 ColorSchemePreview 利用例 外観モード(ライト/ダーク)を切り替えてプレビューします。 実装 struct ColorSchemePreview<Content>: View where Content: View { let content: () -> Content private var colorSchemes: [ColorScheme] { [ColorScheme.light, ColorScheme.dark] } var body: some View { ForEach(colorSchemes, id: \.self) { colorScheme in content() .preferredColorScheme(colorScheme) } } } LocalizedPreview 利用例 プロジェクトに設定されたローカライズ言語を切り替えてプレビューします。 実装 struct LocalizePreview<Content>: View where Content: View { let content: () -> Content private var localizations: [Locale] { Bundle.main.localizations .map(Locale.init) .filter { $0.identifier != "base" } } var body: some View { ForEach(localizations, id: \.identifier) { locale in content() .environment(\.locale, locale) } } } 最後に 本記事ではSwiftUIのプレビューを便利にする実装例の一部をご紹介しました。 他にも以下のようにいくらでもPreview型をつくることができます。 それぞれのプロジェクトにあった必要なものを実装すると良いです。 OrientationPreview|デバイスの回転のプレビュー https://developer.apple.com/documentation/swiftui/view/previewinterfaceorientation(_:) DynamicTypePreview|DynamicType設定に応じたプレビュー https://developer.apple.com/documentation/swiftui/environmentvalues/dynamictypesize IsEnabledPreview|ユーザー操作を許可しているかに応じたプレビュー https://developer.apple.com/documentation/swiftui/environmentvalues/isenabled DevicePreview|iPhoneとiPadなど、デバイス種別に応じたプレビュー https://developer.apple.com/documentation/swiftui/view/previewdevice(_:) 参考 https://developer.apple.com/documentation/swiftui/previews-in-xcode https://developer.apple.com/documentation/swiftui/environmentvalues https://developer.apple.com/documentation/swiftui/view-appearance
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Combine】dropFirst(_:) オペレータを理解する

Combineを真面目に勉強しようと思い立った今年です。 というわけで、早速1つ目の記事を書こうと思います。 Combine自体への興味はもちろん大きいのですが、 これを学習することでリアクティブプログラミングへの理解を深めたいという目的もあります。 早速ですが、本題に入るにあたり少し前置きをさせてください。 学習する上で、「Combineをはじめよう」という宇佐美さんが書かれた本をよく読ませていただいています。 その本の中にこんな文章があります。 プログラマであれば、言葉であれこれ説明する前に、実際にコードを書いて動かしてみるのが理解が早いだろうと考えています。 私はその考えに則ったこの本の構成が非常に好みで、頭にすっと入ってきました。 なので、この記事でもそれを真似させていただきながら書いていこうと思います。 Combineについて書く初めての記事なので、多少前置きが多いですが、 次回からは省略します。 環境 【Xcode】13.1 【Swift】5.5 【macOS】Big Sur バージョン 11.4 公式ドキュメントから理解する 個人的な話で恐縮ですが今年の目標は、iOSの開発力をあげることです。 それにあたり、公式ドキュメントをきちんと読むことを忘れずにいこうと思っています。 また、念の為公式ドキュメントだけではなく、コード内に書かれているドキュメントコメントもちゃんと読もうと思います。 ドキュメントを読む習慣をつけることが目的です。 以上前置きでした。 dropFirst()オペレータに関してのドキュメントにあったコードはこちらです。 let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] cancellable = numbers.publisher .dropFirst(5) // ここで使用されている .sink { print("\($0)", terminator: " ") } これを実行すると、以下が出力されます。 6 7 8 9 10 このコードは一体何をしているのか、ドキュメントの記述から理解していこうと思います。 ドキュメントによりますと、 「連続する要素を再パブリッシュする前に、特定の数の要素を除外する」 Omits the specified number of elements before republishing subsequent elements. とのこと。 定義はこのようになっています。 func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self> サンプルコードには、dropFirst(1)と明示的に1を指定しているものも多いですが、 デフォルト値が既に1なので、dropFirst()でも良いですね。 公式ドキュメントのサンプルコードでは、 dropFirst(5)のオペレータにより、初めの5要素(1,2,3,4,5)がスキップされてprintされているのがわかります。 実際の使用例 Apple公式ドキュメントのサンプルコードは、挙動を理解する手助けにはなるものの 実際どのような場面で使用するのか、個人的にはよくわかりませんでした。。 そこで、別のサンプルコードを引っ張ってきました。 まずViewのコードです。 import SwiftUI struct ContentView: View { @StateObject private var viewModel = ContentViewModel() var body: some View { VStack(spacing: 20) { Text("Create a User ID") TextField("user id", text: $viewModel.userId) .padding() .border(statusColor) .padding() } .font(.title) } } private extension ContentView { // textfieldの入力値によって、textfieldのボーダーカラーを変更する private var statusColor: Color { switch viewModel.isUserIdValid { case .ok: return .green case .invalid: return .red case .notEvaluated: return .secondary } } } TextFieldが1つ存在している画面になります。 TextFieldのボーダーカラーは、入力値によって変化します。 次にViewModelクラスのコードです。 import Foundation enum Validation { case ok case invalid case notEvaluated } class ContentViewModel: ObservableObject { @Published var userId = "" @Published var isUserIdValid = Validation.notEvaluated init() { // ViewModelの初期化時に、userIdに空文字が割り当てられるため、このパイプラインは実行される $userId .dropFirst() // ここで使用されている .map { userId -> Validation in userId.count > 8 ? .ok : .invalid } .assign(to: &$isUserIdValid) } } TextFieldの入力値を変化を監視しています。ViewModelが初期化されるとき、userIdプロパティに空文字が割り当てられるため、$userId以下のパイプラインが実行されます。 ここでのコードより、 入力値が正当、つまり8文字以上の場合はボーダーカラーはグリーン(.ok) 不正な場合はレッド(.invalid) まだ評価されていない場合は、グレー(.notEvaluated) になることがわかります。 それでは仮にdropFirst()の行をコメントアウトしてアプリを実行した場合、 textFieldのボーダーカラーは何色になるでしょうか。 正解は・・・ 「レッド」です。 ユーザーが何も入力していないのに、レッドになってしまいます。 ですがまだtextFieldへの入力値がないのですから、未評価扱いつまりグレーにならなければいけないはずです。 こういった挙動を防ぐため、dropFirst()は使用されます。 前述したように、$userId以下のパイプラインが初めて実行されるのはいつかというと、 Viewが読み込まれ、ViewModelが初期化されたときです。 つまりアプリを初めて起動した段階で、パイプラインは実行されます。 そのため、dropFirst()が存在しない場合、 mapオペレータ内の8文字以下以上のロジック判断が行われ、 当然0文字なので不正値(.invalid)と判断されて、 最終的にisUserValidプロパティの値は.invalidとなるので、 TextFieldのボーダーカラーはレッドになってしまいます。 dropFirst()を使用すれば、 1回目のパブリッシュがスキップされるため、8文字以下以内の判断は行われません。 なのでisUserValidは初期化された時の.notEvaluatedのままになり、 初回起動時のTextFieldのボーダーカラーもめでたくグレーのままになります。 上記のコード全体は、以下に上がっていますので、ご参考ください。 RxSwiftでは こちらも少し前置きさせてください。 まず始めに、私はRxSwiftは触ったことはありません。。そして今回の記事をかくにあたっても、RxSwiftは触っていません。。 ですが以前読んだ比較記事や動画によると、実装の詳細は違えど、APIはかなり似ているという意見が多く見られました。 またCombineがiOS13から使用可能と言うこともあり、RxSwiftの需要もまだ大きいようです。 こちらのスクラップにちょこちょこメモ書いてます。↓ そこで今後もし自分がRxSwiftに携わる経験が出てきたら、 ああCombineで言うとこれねと言うのがわかるようにしておこうと思い、ここに書いておきます。 先輩エンジニアは私とは逆で、RxSwiftでいうとこれねになると思いますが、 私はCombineからリアクティブプログラミングに入門しているので、今後使用される可能性が高くなることを願ってCombineから勉強しております。 RxSwiftでいうとこれね、と言うのは、既に「RxSwift to Combine Cheatsheet」というのがあるので、そのリポジトリで調べた対応するオペレータを載せておき、 RxSwiftの公式ドキュメントを確認するに留めようと思います。 こちらも前置きが長くなりました。本題に戻ります。 CombineのdropFist()オペレータに対して、 RxSwiftのオペレータは、skip()になるようです。 ドキュメントにはこう書かれていました。 「監視対象のシーケンスで指定された要素数をスキップし、残りの要素を返す」 Bypasses a specified number of elements in an observable sequence and then returns the remaining elements. 定義はこう。 こちらは、Combineと違ってデフォルト値は設定されていないので、必ずcountにInt型の数値を指定する必要があります。 public func skip(_ count: Int) -> Observable<Element> Combine側のサンプルコードで、dropFirst(1)と明示的に指定しているコードが多いのは RxSwiftの名残りなのかなーと思いました。 おわりに 今回調べたのはdropFirstオペレータですが、dropがつくオペレータは他にも以下があるようです。 drop(untilOutputFrom:) drop(while:) tryDrop(while:) そしてcheat sheetをみると、それぞれに対応するRxSwiftのオペレータもあるようですね。 まだこの3つは使ったことがありませんが、今後使うことになったらもっと詳しく調査したいと思います。 前述の通りCombine初心者ですので、何か間違い等ありましたらコメントお待ちしています。  参考 dropFirst(_:) Apple Developer Documentation skip(_:) RxSwift公式ドキュメント MVVM with Combine Tutorial for iOS→Raywenderlichのチュートリアル Combine Mastery In SwiftUI→電子書籍。実際の使用例にあるコードはこちらのコードを少し修正したものです。 RxSwift to Combine Cheatsheet Combineをはじめよう→電子書籍
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode でテストにタイムアウトを設定する

概要 Swift 5.5 で入った Concurrency を使ったテストで、await 時に想定外のことが起きて結果が返ってこずテストが永遠に終わらないということが起こったので、テストにタイムアウトを設定する方法を調べてまとめました。 動作検証はすべて Xcode 13.2.1 で行いました。 TestPlan を設定する まず、タイムアウトを設定するために TestPlan を有効にします。タイムアウトの設定のみであればビルド時に特定のコマンドラインオプションを渡すことでも可能なようですが、細かく設定しようと思うと TestPlan を作ってしまった方が便利なのでそちらの方法をとることにします。 Xcode の window 上部の Scheme をクリックして Edit Scheme... から Test の Info タブを開きます。右下の方に Convert to use Test Plans... というボタンがあるのでこれをクリックします。 3つのオプションが出てくるので、一番上の Create Test Plan from scheme を選びます。 作成された TestPlan の Configuration タブを開き TestExecution > Test Timeouts を On にすることでタイムアウトが有効になります。 タイムアウト値の設定 executionTimeAllowance という XCTestCase のプロパティを編集することでタイムアウト値を設定することができます。 https://developer.apple.com/documentation/xctest/xctestcase/3526064-executiontimeallowance ドキュメントにあるように、 executionTimeAllowance の値自体は秒数ですが実際のタイムアウト値は分単位での切り上げになることに注意しましょう。例えば 1 を設定すると1秒の切り上げの1分、 150 を設定すると2分30秒の切り上げの3分でテストがタイムアウトするようになります。この振る舞いは分かりづらいので executionTimeAllowance の値は 60 120 180 のようにちょうど分単位になるように設定するのがよい気がします。 例えば executionTimeAllowance を 60 にしてテスト中に 61 秒待つようにすると、期待通りにテストが落ちてくれます。エラーメッセージもなかなかわかりやすいと思います。 以上でテストにタイムアウトを設定することができましたが、すべてのテストでいちいち executionTimeAllowance を設定するのは大変です。TestPlan の Test Timeouts の下の Default Test Execution Time Allowance という項目で executionTimeAllowance のデフォルト値の設定ができます。以下のように設定すれば、すべてのテストが1分でタイムアウトするようになります。 ちなみに Default Test Execution Time Allowance 自体のデフォルト値は 600 なので、 Test Timeouts を On にした時点ですべてのテストが10分でタイムアウトするようになっていたことになります。 Default Test Execution Time Allowance で決まるタイムアウト値を各テストメソッドごとに上書きしたい場合は executionTimeAllowance に値を入れることでそちらが優先されます。 まとめ テストがなんらかの原因で hang しないようにテストメソッドにタイムアウトを設定するとよい TestPlan を有効にして Test Timeouts を On にすることでテストがタイムアウトするようになる タイムアウトまでの時間は、 TestPlan の設定の Default Test Execution Time Allowance と XCTestCase の executionTimeAllowance プロパティでそれぞれ設定できる 後者の設定が優先される どちらの設定も単位は秒だが、タイムアウトまでの時間は分単位での切り上げになることに注意 参考 https://developer.apple.com/documentation/xctest/xctestcase/3526064-executiontimeallowance https://benoitpasquier.com/advanced-testing-tips-xcode/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

String(format:)にはCVarArg...と[CVarArg]の2パターン存在する

ほとんどの場合 CVarArg... のほうしか使わないと思うけど。 // まったく相応しくない例 let price = "5000兆" let unit = "円" let formatted = String(format: "%@%@欲しい!!", price, unit) 可変引数部分もラップした関数を用意したとき、こう書いちゃだめ。 // ❌意図した動作にならない // argumentの中身は[CVarArg]であり、String(format:)には1つ目の可変引数と解釈されてしまう func localized(format: String, _ arguments: CVarArg...) -> String { return String( format: NSLocalizedString(format, comment: ""), arguments ) } arguments: キーを付けると[CVarArg]` を引数として取るので、こっちが正解。 // ✅ func localized(format: String, _ arguments: CVarArg...) -> String { return String( format: NSLocalizedString(format, comment: ""), arguments: arguments // argumentsキーをつけて渡す ) } ビルドエラーも出ないし、動作不定なので分かりづらいよね...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む