- 投稿日:2020-06-03T22:59:21+09:00
【プログラミング初心者】Swift基礎~if文・switch文~
はじめに
今回は条件判定のために必要な構文、if文とswitch文の書き方について紹介します。
条件文は多少書き方の違いはありますがSwift以外の多言語全てで使用されるプログラムの基本構文の1つです。
この投稿で書き方をしっかりと覚えておいてください。条件文とは
プログラムでは異なる条件毎に処理を分岐させアプリを制御します。
例えば現実的な例としてマンガアプリで考えてみます。
そのアプリで購入したマンガを表示する処理を実装するとしましょう 。
その場合マンガのページを表示するにはどのような条件が考えられるでしょうか?対象のマンガは購入済みかどうか、未購入の場合は購入するためのアプリ内マネーが足りているかどうか、通信エラーが発生したかどうか、etc.
考え出すとキリがありませんが、こういった考えられる状況全てに対して条件分岐させそれぞれ異なった処理を実装していく必要があります。こういった場合に条件文と呼ばれるif文やswitch文を使用して制御していきます。
if文
それでは具体的な書き方を紹介していきます。
構文としては以下の通りです。if 条件 { 条件が真の場合の処理 } else { 条件が偽の場合の処理 }条件には
Bool
型を入れる必要があります。
if
に与えた条件がtrue
の場合最初の処理ブロックに入ります。
false
となった場合はelse
の処理ブロックに入りそれぞれの処理を実行します。
またそれぞれの処理ブロックはどちらか片方しか実行されず、if
に入った場合else
は実行されません。条件は
Bool
であれば変数、メソッド、式と何でも構いません。// 変数 let boolValue = true if boolValue { print("boolValue is true") } else { print("boolValue is false") } // 式 let intValue = 1 if intValue == 1 { print("intValue is 1") } else { print("intValue is not 1") } // メソッド func boolFunction() -> Bool { return true } if boolFunction() { print("boolFunction() is true") } else { print("boolFunction() is false") }
else
は必須ではなくif
のみでも使用できます。if value > 0 { print("0より大きい") }また
if
文同士繋げることも可能です。if value > 10 { print("10より大きい") } else if value < 10 { print("10より小さい") } else { print("それ以外") }条件は
Bool
あればいいので、論理演算子を使って複数の条件を組み合わせることができます。if value >= 0 && value < 10 { print("0以上10未満") } else { print("それ以外") }switch文
Swiftの
switch
はかなり汎用性が高く、数多くの書き方があります。
今回は基本となる書き方のみを紹介します。基本構文は以下となります。
switch 値 or 式 { case 値1: 値1の場合の処理 case 値2: 値2の場合の処理 ... default: caseに当てはまらなかった場合の処理 }具体的な実装は以下のようになります。
let value = 0 switch value { case 0: print("0です。") case 1: print("1です") case 2: print("2です") default: print("それ以外です") }このように
switch
に与えた値value
に対して判定を行ないます。
if
との違いは条件ではなく値そのものに対する判定ということです。当てはまる
case
から次のcase
までの間の処理が実行されます。
それ以外の処理は実行されません。
(他言語を触った人からするとbreak
がないのに次が実行されないことに違和感があるかもしれません。Swiftではbreak
は不要です。)
switch
に与える型は値の判定ができれば構いません。
とはいえ値の判定ができるのは数値型、文字列型、列挙型くらいで、現実的に使うのはこのあたりの型になるかとは想います。1つの
case
に複数指定することも可能です。let club = "野球" switch club { case "野球", "サッカー", "テニス": print("運動部です") case "吹奏楽", "茶道": print("文化部です") default: print("未定義の部活です") }また数値の場合
(数値1…数値2)
とすることで範囲によって条件を分岐させることも可能です。let score = 80 switch score { case (80...100): print("優") case (70...79): print("良") case (60...69): print("可") case (0...59): print("不可") default: print("未定義") }以上のように値に対して判定を行ない、当てはまる
case
の処理を実行するのがswitch
文です。
いくつか例を紹介しましたがまだまだ使い方は色々あります。
条件判定する際は調べてみてください。最後に
今回はプログラミングする上では欠かせない条件分岐の方法として
if
とswitch
を紹介しました。
どちらも似たようなものですね。
実際switch
をif~else if~...
と条件分岐を行っても同じ結果が得られます。
プログラムを管理しやすい方、ミスが起きにくい方を状況に応じて選んでいきたいところです。今回の内容は以上です。
本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684
- 投稿日:2020-06-03T22:18:14+09:00
「this class is not key value coding-compliant for the key」のエラーが取れないとき
AppDelegateで止まってしまう、めちゃくちゃありがちなエラーです。
コンポーネントをソースファイルにドラッグして繋いだあとに、コード側だけ削除してコンポーネントの接続をバツで消さないと起こるやつですね。
AppDelegateが突然出てきたときはほぼこれなのでエラーメッセージすら読まずに「どこ繋ぎ忘れたかな」とさっと確認して即直せたのですが、今回は微妙にハマりました。「このaction、ちゃんと繋がってるのになぜ??」とクリーンしたり再起動したり色々してもダメでした。
原因ですが、最初にうっかりoutlet接続してしまって、それが残っていました。
なんとも間抜けですが、同じ名前なので気付きにくかったです。おそらく、どこかおかしなところに同じ名前でくっついてしまっているんだろうと、色んなところを探し回りましたが見つからず。。。
しかし、さくっと見つける方法が一つありました。
ストーリーボードのviewControllerの一番上の階層をクリックして、そこの接続を見るとなんとコードと接続されていない消すべきoutlet接続に黄色い警告マークが出ます。まとめると「一番上の階層から黄色警告を探してみてください。おそらくそいつが消すべき接続です」という感じです。
- 投稿日:2020-06-03T22:16:09+09:00
【Swift】KVOでUserDefaultsの値の変更を監視する
KVOとは
KVO(Key Value Observing)の略名になります。
その名の通り、オブジェクトの値を監視する為に用いる技術です。
値の更新や削除などの変更を検知することができとても便利です!UserDefaultsの値の変更を監視する
UserDefaults
の値をdynamic
で取得する// 今回はIntの値を扱っています extension UserDefaults { @objc dynamic var translateLimit: Int { return integer(forKey: keyName) } }監視する処理
var observer: NSKeyValueObservation? private func setKVO() { observer = UserDefaults.standard.observe(\.translateLimit, options: [.initial, .new], changeHandler: { [weak self] (defaults, change) in // 行いたい処理を書く }) }これで監視の処理は完了!
簡単だしとても便利。
- 投稿日:2020-06-03T22:03:49+09:00
UnityでSwiftの自作「Static Library」を使う
環境
- Unity 2018.4.23f1
- Xcode 11.5
- Swift 5
- iOS 13.4.1(iPhone 11)
手順の概要
「Unity」から直接「Swift」のコードを呼び出すことはできないようです。。。orz
その為、下記のような形で、「Objective-C++」を経由する事で、「Swift」の関数を呼び出します。設定手順
「Swift」の「Static Library」を作成
「Xcode」で、プロジェクトを作成
項目 値 Product Name 任意 Team 任意 Organization Name 任意 Organaization Identifier 任意 Language Objective-C
- 任意のディレクトリにプロジェクトを作成
呼び出される「Swift」ファイルの作成
- 「プロジェクト」→「コンテキストメニュー」→「New File」を押下
- 「iOS」→「Source」→「Swift File」を選択して、「Next」を押下
- 任意の名前を設定して、「Create」を押下
- 「Create Briding Header」を押下
- 作成した「Swift」ファイルを選択して、呼び出す関数を作成
// // SwiftTest.swift // SwiftStaticLibrary // // Created by Kumatta_ss on 2020/05/31. // Copyright © 2020 Kumatta_ss. All rights reserved. // import Foundation class SwiftTest: NSObject { /// 呼び出しのみ @objc static func swiftCallTest() { NSLog("swiftCallTest OK!") } /// 引数のみ /// - Parameters: /// - val1: 引数1 @objc static func swiftCallTestArgument(val1: String) { NSLog("swiftCallTestArgument OK! Argument:" + val1) } /// 引数、戻り値あり /// - Parameters: /// - val1: 引数1 /// - Returns:戻り値 @objc static func swiftCallTestArgumentReturn(val1: String) -> String { NSLog("swiftCallTestArgumentReturn OK!") return "Return swiftCallTestArgumentReturn Argument:" + val1; } }外部公開用の「Objective-C++」を作成する
- 「Command + B」で、Buildする
※Swiftのヘッダーファイルを生成させる- 初期で作成されている、「Objective-C」のファイルの拡張子を、「m」を、「mm」にする
※「Objective-C++」に変更- 変更したファイルに、Swiftを呼び出す関数を作成
// Swiftの関数宣言ヘッダー(名称は、「{プロジェクト名}-Swift.h」形式) #import <SwiftStaticLibrary-Swift.h> // 外部に公開用の関数宣言 extern "C" { void CallTest(); void CallTestArgument(const char *val1); const char* CallTestArgumentReturn(const char *val1); } // 呼び出しのみ void CallTest() { NSLog(@"CallTest"); [SwiftTest swiftCallTest]; } // 引数のみ void CallTestArgument(const char *val1) { NSLog(@"CallTestArgument"); [SwiftTest swiftCallTestArgumentWithVal1:@(val1)]; } // 引数、戻り値あり const char* CallTestArgumentReturn(const char *val1) { NSLog(@"CallTestArgumentReturn"); NSString *result = [SwiftTest swiftCallTestArgumentReturnWithVal1:@(val1)]; const char *resultEncodeVal = [result cStringUsingEncoding:NSUTF8StringEncoding]; char *returnVal = (char*)malloc(strlen(resultEncodeVal) + 1); strcpy(returnVal, resultEncodeVal); return returnVal; }ライブラリの生成
- ビルドすると「Products」ディレクトリ配下に、「a」ファイルが作成される
ファイルを選択した状態で、右のメニューの「Indentity andType」→「Full Path」の矢印を押下すると、格納ディレクトリを開きますUnityで「Sttaic Library」を使用する
ライブラリの配置
- 格納用ディレクトリを、下記の構成で作成
Assets ├─ Efitor ├─ Plugins | └─ iOS └─ Scripts
- 「Assets/Plugins/iOS」に生成された、「a」ファイルを格納
- 格納した「a」ファイル選択して、「Inspector」の下記の項目を設定して、「Apply」を押下
項目 値 Select platforms for plugin -> Include Platfprms 「iOS」のみにチェック Platform settings デフォルト 呼び出しクラスの作成
- 呼び出し確認用のクラスを、「Assets/Scripts」に作成
using System.Runtime.InteropServices; using UnityEngine; public class UnityCallTest : MonoBehaviour { [DllImport("__Internal")] private static extern void CallTest(); [DllImport("__Internal")] private static extern void CallTestArgument(string val1); [DllImport("__Internal")] private static extern string CallTestArgumentReturn(string val1); // Start is called before the first frame update void Start() { CallTest(); CallTestArgument("1.Unityからの呼び出しだ!"); string result = CallTestArgumentReturn("2.Unityからの呼び出しだ!"); Debug.Log("============================ Unity Result:" + result); } // Update is called once per frame void Update() { } }
- 「Hierarchy」に、任意のGameObjectを作成して、作成したスクリプトをアタッチ
ビルド設定
- 「Player setting」で、任意の設定をする
※今回は下記の赤い部分のみを変更しました
- 「Assets/Editor」配下に、拡張エディタを作成
そのままだと、ビルドしたXcodeプロジェクトに、毎回設定をしないといけないので、拡張エディタを作成using System.IO; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; public class TestPostProcessBuild { [PostProcessBuild] public static void BuildTest(BuildTarget buildTarget, string projectPath) { // iOSの場合のみ if (BuildTarget.iOS == buildTarget) { string projectFilePath = PBXProject.GetPBXProjectPath(projectPath); var proj = new PBXProject(); proj.ReadFromFile(projectFilePath); string target = proj.TargetGuidByName(PBXProject.GetUnityTargetName()); // BridgingHeaderを作成・設定 string projSwiftBridgingFile = "Classes/Unity-iPhone-Bridging-Header.h"; string swiftBridgingFile = Path.Combine(projectPath, projSwiftBridgingFile); var fs = File.Create(swiftBridgingFile); fs.Close(); string swiftBridgingFileGuid = proj.AddFile(swiftBridgingFile, projSwiftBridgingFile, PBXSourceTree.Source); proj.AddFileToBuild(target, swiftBridgingFileGuid); proj.SetBuildProperty(target, "SWIFT_OBJC_BRIDGING_HEADER", projSwiftBridgingFile); // Xcodeのビルド設定 proj.SetBuildProperty(target, "CLANG_ENABLE_MODULES", "YES"); proj.AddBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks"); proj.SetBuildProperty(target, "SWIFT_VERSION", "5.0"); proj.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES"); proj.SetBuildProperty(target, "SWIFT_INSTALL_OBJC_HEADER", "YES"); proj.SetBuildProperty(target, "SWIFT_PRECOMPILE_BRIDGING_HEADER", "YES"); proj.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "$(PRODUCT_NAME)-Swift.h"); proj.AddBuildProperty(target, "LIBRARY_SEARCH_PATHS", "/usr/lib/swift/"); proj.AddFileToBuild(target, proj.AddFile("usr/lib/swift/libswiftFoundation.tbd", "Frameworks/libswiftFoundation.tbd", PBXSourceTree.Sdk)); proj.WriteToFile(projectFilePath); } } }ビルドして実機で実行
- ログに実行結果が出力されると思います
あとがき
今回、Swiftで作成されたライブラリを、Unityで呼び出す必要があり、
「Objective-C」、「Swift」を触った事ない状態から、色々試した結果です。ビルドのプロパティについては、「Xcode」でネイティブアプリとして、
「Objective-C」と、「Objective-C + Swift」の二つを作成、解析して、必要な設定を割り出した物になります。問題や追加の説明が欲しいなどが、あったらコメントなどで、教えていただけたら嬉しいです。
- 投稿日:2020-06-03T19:09:33+09:00
Embedded Frameworkでビルドエラーが出る場合
- 投稿日:2020-06-03T18:21:00+09:00
肥大化したiOSアプリを課題解決のために半年弱でリアーキテクチャした話
はじめに
2019年に約半年弱の開発期間で公開中のiOSアプリを
リアーキテクチャーした話を備忘録としてまとめました。サービスインされているアプリを一気に作り直すケースは
ビジネス判断的にはかなり危い橋を渡っているのであまり無いとは思いますが
似たような状況で開発に臨んでいる方など、どなたかの参考になれば幸いです?開発の背景
- 2014年から毎年追加改修が走っているストア公開中のECアプリ案件(Objective-C)
- 画面数で見れば中規模から大規模にカテゴライズ
- iOSのバージョンについて古いものは毎年切り捨ててく方針
- 開発メンバーは毎年ほぼ総入替
- 自分は2018年頃から参加しました
抱えていた課題・技術的負債
受託案件あるあるですが以下の課題を抱えていました。
0️⃣ ピュアObjective-C
1️⃣ 可読性の低い巨大すぎるMVC
(MassiveViewController + 非同期処理通知はNSNotificationのみ, BlocksKit等の利用無し)
2️⃣ 同じ/似たような機能を再利用せずにコピペで複数箇所に実装
3️⃣ オンメモリでも全く問題ない不要なアプリ内DB(CoreData)
4️⃣ テストコード・内部設計書の無い長期運用
5️⃣ 上記の問題による運用・改修コストの増加特に 1️⃣の影響が大きく
非同期処理の通知を受け取る箇所が広範囲に点在されていた為に
毎年入れ替わるエンジニアへの引継ぎや改修時の影響範囲の調査に
余計なコストが掛かっていました。課題を解決するためのアプローチ
求められるアーキテクチャーの決定
当時の開発チーム全員で選定しCleanArchitectureで設計しました。
採用理由
課題解決のため責務の明確化と疎結合性の向上に加えて
長期的な運用と開発者がコロコロ替わるのに対応するため他の候補の見送り理由
- MVP => Datasourceを追加したとしてもPresenterが肥大化しそうだった為
- MVVM => RxSwiftの理解が必須なため、エンジニアの入れ替えが激しい要件にそぐわない
- VIPER => 後述のpythonを利用した旧プロジェクトから新プロジェクトへ変換が容易だった為見送り
その他
PEAKSから発行されていた iOSアプリ設計パターン入門 を輪読していたのも要因です
iOSアプリ設計パターン入門 Clean Architecture 10.4 p213 より
言い換えると、Clean Architectureはアプリが大規模で長命になるほど恩恵を受けるアーキテクチャと言えます。変化の激しい部分は容易に切り替えが可能ですし、GUI アーキテクチャではModelでまとめられていた役割をさらに分離する指針にもなります。複数人での開発や テストにおいても戸惑うことなく、整然と進められることでしょう。
開発メンバーと役割分担
??? : Photo, ItemList, CustomerSetting, XCTest ??? : Home, SideMenu, Cart, Payment, CodeConverter(Python) ??? : Photo, PhotoList, OCR ??♀️ : 途中で交代して引継ぎ Photo, CIFilter・XCUITest Me : DI, Launch, PushNotification, DeepLink, Routing, CICleanArchitectureのディレクトリ構成と各ファイルの責務
iOS開発でClean Architectureを採用した際のイイ感じのディレクトリ構成とは
を参考にData, Scenes といったディレクトリに分離させました。Data
担当したアプリは 1画面1APIのようなI/F設計にはなっていない ため画面に紐づく構成よりも
独立させて横断できる構成の方が把握が楽かなと感じました。
(※階層が若干深いので Xcodeのcommand + shift + j
は必須でした)DataディレクトリにはWebAPIに関連する各
Entity
Entityが持つObject
Request
Requestパラメータ
を配置しています。Scenes
Scenes以下に画面と紐づく名前でディレクトリを切って
各責務を担当するファイルを分けています。各責務の概要は以下です。
■Configurator -- ○○Assmbly.swift Swinject用の DIグラフ解決用のファイル Swinject利用方法のサンプルはこちら https://github.com/SatoshiN303/SwinjectStoryboardSample ■Domain --- ○○Protocols.swift 各ファイルのお互いを伝え合うProtocolをまとめて定義しておく 場所はDomain以下でなくてもよかったかも --- ○○Gateway.swift APIを叩いたり、Reamlからデータ取得したり 外側とやりとりする --- ○○Usecase.swift Gatewayから受け取ったオブジェクトをPresenter等に伝搬させる Translatorを呼び出してEntityをModelに変換したり --- ○○Model.swift & ○○Translator.swift View用にEntityを加工したオブジェクトと EnityからModelへ変換する機構 ※ModelとTranslatorを毎回作ってるとさすがに冗長だという意見もあり Entityの値を加工する必要がある場合のみ作成 ■Presentation -- ○○Presenter.swift Usecaseから受け取った値をViewへ伝搬させるなど Presneterが肥大化するような場面では ○○DataSource.swiftを持って分散したり -- ○○ViewController.swift 基本的にはfinalで。Scenes毎の画面遷移はSegueを使わずにVCにmakeInstanceな static関数を生やして依存性注入する。Objective-CのファイルをまとめてCleanArchitecture構造のSwiftファイルに変換するpythonスクリプトを利用
メンバーの提案で開発初期のスピードを早めるために
旧Xcodeプロジェクトをpythonでクロールして
CleanArchitectureの構造に定義されたSwiftファイルを
まとめて出力するスクリプトを利用しました。Swinjectを利用したDependencyGraphの解決、
どの画面がどのWebAPIを叩いてるかのプロトコルへの紐付けなど、
あらかじめ定義できる静的な部分を生成しています。生成例
私が実装したものではないので、主な処理内容だけ共有します。
前提 ディクショナリで 旧ファイル名 : 新ファイル名の変換表を保持 (1) storyBoardに関連付けられてるViewContorllerを取得 (2) 上記のViewControllerのソースコードを読み取り (3) WebAPIを叩いてるViewController見つけたらディクショナリ生成 (vc名: [WebAPIその1, WebAPIその2]) のような (4) 変換テーブルを用いてCleanArchitectureの形式でswiftファイルを一括生成 & 必要な定義をプロトコルに書き込んで紐付け開発方針・規約的なもの
- Segueは利用しない。画面遷移が発生するVCには
static func makeInstance()
的なものを生やして画面に必要な依存性を注入しつつDeepLink等にも対応可能にする- RxSwiftについてはエンジニア入れ替え時の負担を軽減するため部分的な利用に留める (Promise的に書きたい箇所やRxCocoa等など)
- swiftLint.yml はこちら (※記述が古いかもしれません)
- Swiftformat/CLI を利用
- 基本はCarthage, 必要に応じでCocoaPodを利用
- 本番、ステージング、開発環境の切り分けはxcconfigで
CIで行っていたこと
CIはBitriseを利用していました。
受託開発での iOS アプリプロジェクト新規作成プラクティス(下編:Bitrise 編)
を参考にPullRequestをトリガーにしてDangerやSwiftLintを実行する
最低限のコードレビュー自動化やAdhoc/Releaseビルドの配布を行っています。開発ツール・その他
開発で優先しなかったこと
- Xcode11, iOS13対応 => タイミング的に先送り可能な時期だったので
- 充実したテストコード => 動作が不安定だった為、全WebAPIのチェッキングのみ
作り直して解決したコト、 ポジティブなインパクト
- ? 「どの処理がどこで何をしているか」の責務が明確になり運用・改修コストが大体0.5〜1.5人日削減
- ?クラッシュの影響を受けていないユーザーが繁忙期計測値90%から99%へ上昇&キープ
- ?Swinject/SwinjectStoryboard によるDIでモック可能なテスタブルで構成に
- ?引き継ぎコストに1.5人日程度掛かっていた状態を0.5人日に短縮
- ?注文件数 前年比130% (リアーキテクチャー以外の要因も大きいですが一応…)
残っている課題
- embed frameworks化
- 端末依存のコード (isIPhoneX的なもの)
- 一部CleanArchitectureの思想から外れてしまったサイドメニューの実装等
番外編:リアーキテクチャー決定に至るまでの開発チームとしての根回し的なもの
形としては受託案件でしたので追加改修の工数を見積るタイミングで
以下を合わせて伝えておりました。(※クライアントとの関係性ありきです)
- 技術的負債の影響で追加改修に掛かる余計なタスク(==費用)を数値化し見積もりに含める
- 影響範囲の大きい要件は追加改修の影響で今後の開発コストが掛かる可能性も伝える
最初はメイン機能周辺をSwift化できれば開発チームとしては御の字だったのですが
タイミング的にビジネスを拡大して一気に攻めたいというクライアントの意向と重なり
(政治的な根回しもあり)結果的にほぼ全体をリアーキテクチャーするに至りました。最後に
昨年の話なのでSwiftUIやiOS13については特に触れておりませんが
どなたかのお役に立てれば幸いです。
つらつらとした長文でしたが、お読み頂きありがとうございます?参考文献・URL・スライド
非常に参考にさせて頂きました。ありがとうございます?♂️
- 投稿日:2020-06-03T17:24:05+09:00
XCTestの非同期テストで「呼び出されないこと」を検証する
非同期テスト
下記のようなサブスレッドを利用して結果を非同期に返すクラスがあるとします。
class AsyncTask { func execute(completion: @escaping (Bool) -> Void) { DispatchQueue.global().async { completion(false) } } }このクラスのテストをXCTestで書く際、以下のような実装をしてしまうと、該当のテストケースは非同期処理の完了を待たずにテストを終了させてしまいます。
class SampleTests: XCTestCase { var asyncTask: AsyncTask! override func setUpWithError() throws { asyncTask = AsyncTask() } func test() throws { asyncTask.execute { result in // このクロージャが呼び出される前にテストが終了してしまう XCTAssertTrue(result) } } }そのため、テストケースとしてはtrueを期待しており、AsyncTaskはfalseを返しているので、本来テストは失敗して欲しいところですが、このテストは意図とは異なり成功で終了してしまいます。
XCTestExpectation
この問題を解決するには、
XCTestExpectation
を利用する必要があります。
これはXCTestCase内でexpectation
メソッドを呼び出すことで生成でき、 XCTestCaseのwait
メソッドに受け渡すことで非同期処理が完了するまでテストの終了を待機させることができます。XCTestCaseに非同期処理が完了したことを伝えるには XCTestExpectationの
fulfill
を呼び出す必要があります。
また、waitメソッドのtimeoutまでにfullfillが呼び出されなかった場合はテストが失敗することになります。先ほどのサンプルコードをXCTestExpectationを利用した形に修正すると下記のようになります。
func test() throws { let expectation = self.expectation(description: "wait for async task") asyncTask.execute { result in XCTAssertTrue(result) // 非同期処理が完了したことを知らせる expectation.fulfill() } // fullfillメソッドが呼び出されるまでテストを終了させずに最大0.1秒間待機する self.wait(for: [expectation], timeout: 0.1) }これによりfullfillメソッドが呼び出されるまではテストが終了されないことを保証できるので、テストがちゃんと失敗してくれるようになります。
呼び出されないことの検証
非同期テストは基本的には上記の書き方で検証をすることができますが、たまに非同期で実行され得る処理が「呼び出されない」ことを検証しておきたいケースがあります。
このテストを実装するにはどうすればよいでしょうか。サンプルコードを少し修正して、引数によっては完了ハンドラを呼び出さずに関数を終了させるような実装に変更してみます。
class AsyncTask { func execute(shouldCallCompletion: Bool, completion: @escaping () -> Void) { if !shouldCallCompletion { return } DispatchQueue.global().async(execute: completion) } }テストコードは下記のようになります。
func testShouldNotCalled() throws { let expectation = self.expectation(description: "wait for async task") asyncTask.execute(shouldCallCompletion: false) { expectation.fulfill() } wait(for: [expectation], timeout: 0.1) }非同期関数の引数にfalseを受け渡しているので、この関数は完了ハンドラを呼び出さずに終了します。
そのためfulfillメソッドが呼び出されないことでwaitメソッドのタイムアウトを超過し、テストとしては失敗します。「テストが失敗している=呼び出されていない」という判断はできるので、期待している挙動になってはいます。
しかし、自動テストとしてこの状態をOKとすることはできません。こういった場合に利用できるのが、XCTestExpectationの
isInverted
プロパティです。
このプロパティにtrueを設定しておくと、テストケースは「fullfillが呼び出されない」という挙動を期待するようになります。func testShouldNotCalled() throws { let expectation = self.expectation(description: "wait for async task") + expectation.isInverted = true asyncTask.execute(shouldCallCompletion: false) { expectation.fulfill() } wait(for: [expectation], timeout: 0.1) }1行設定を追加して実行すると、テストが成功するようになります。
これによって期待している「ハンドラが呼び出されなければテストを成功させる」という挙動にすることができました注意点
この isInverted プロパティは「timeoutを超過するまでにfullfillが呼ばれなければ成功」という形でテストケースを扱うようになるため、timeoutに大きな値を設定していた場合、テストの実行時間が増加してしまう可能性があります。
そのため、必要最小限の値を設定しておくことをお勧めします。
- 投稿日:2020-06-03T15:57:16+09:00
Swift UIViewControllerにtableViewを表示する(標準で用意されているセル採用パターン)
ListViewController.swift
import UIKit class ListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { let dataList: Array = ["01","02"] @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self; tableView.dataSource = self; // Cell名の登録をおこなう. tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") // Do any additional setup after loading the view. } //追加③ セルの個数を指定するデリゲートメソッド(必須) func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataList.count } //追加④ セルに値を設定するデータソースメソッド(必須) func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // セルを取得する let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) // セルに表示する値を設定する cell.textLabel!.text = dataList[indexPath.row] return cell } }ListViewController.xib
tableViewを配置し、
ListViewController.swift
とOutlet接続を行ってください。
ListViewController.swift
の@IBOutlet weak var tableView: UITableView!
※
ListViewController.swift
でコードで生成行う場合は.xibファイル
自体不要です。
- 投稿日:2020-06-03T15:00:47+09:00
XcodeGenを用いて中規模プロジェクトをリリースした話
はじめに
久しぶりの投稿。
題名の通り、期間は半年、エンジニアは二人という開発体制のプロジェクトにXcodeGenを導入し、先日プロジェクトをリリースしましたので、その知見を共有します。XcodeGenとは?
iOSアプリをチーム開発のついて回る問題、
project.pbxproj
ファイルがコンフリクトしまくる問題があります。その問題解決できるツールがXcodeGenです。
最近取り入れてるプロジェクトは増えてきてるとはいえ、まだまだ発展途上のツールでガンガンアプデされており、機能がもりもりです。
XcodeGenは主に以下をやってくれます。(*1)XcodeGenがやってくれること
- コマンド一発で
.xcodeproj
をproject.yml
の設定を元に生成。
- ライブラリ依存、フレームワークも管理
- Build Configurationも管理
- Development Team、Provisioning Profileも管理
- Embedded Frameworkも管理
- etc...
- ファイルソートもしてくれる
要は
.xcodeproj
ファイルをproject.yml
に置き換えてるだけですね。
そうするとコンフリクト問題になっていたディレクトリ、ファイル構成は実際のディレクトリ構成から作成するので、コンフリクトはほぼなくなります。
後は、.xcodeproj
を丸ごと.gitignore
指定してコマンドを開発ルールに取り入れれば、OKです。https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md が一番参考になります。
完成設定ファイル
MintでXcodeGenを導入。
リリース後完成したproject.yml
は以下です。(プロジェクト名や長いライブラリ依存記述などは書き換えてますので、ご容赦を)
個別設定ファイルは.xcconfig
に切り分けてたりしました。project.ymlname: test-xcodegen # BuildConfiguration定義 configs: Debug: debug Stg: debug Release: release # 別途読み込みxcconfigファイル configFiles: Debug: configs/Debug.xcconfig Stg: configs/Stg.xcconfig Release: configs/Release.xcconfig # オプション options: developmentLanguage: ja # テンプレ設定 settingGroups: testSettings: SWIFT_OBJC_BRIDGING_HEADER: ${PRODUCT_NAME}/Applications/test-Bridging-Header.h CODE_SIGN_STYLE: Manual SWIFT_VERSION: 5.0 TARGETED_DEVICE_FAMILY: "1,2" INFOPLIST_FILE: test/Resources/Info.plist CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES OTHER_LINKER_FLAGS: $(inherited) -ObjC CODE_SIGN_ENTITLEMENTS: test/Resources/test_development.entitlements DEBUG_INFORMATION_FORMAT: dwarf-with-dsym testFrameworkSettings: CODE_SIGN_STYLE: Automatic LD_RUNPATH_SEARCH_PATHS: ${inherited} @executable_path/Frameworks @loader_path/Frameworks PRODUCT_BUNDLE_IDENTIFIER: test-framework.${PRODUCT_NAME} targets: # メインプロジェクト test: type: application platform: iOS scheme: {} deploymentTarget: "11.0" sources: - test - path: test/Resources/Generated/Assets-Constants.swift optional: true type: file - path: test/Resources/Generated/Colors-Constants.swift optional: true type: file - path: test/Resources/Generated/L10n-Constants.swift optional: true type: file # メイン設定 settings: groups: [testSettings] configs: Debug: ODE_SIGN_IDENTITY: Apple Development DEVELOPMENT_TEAM: hogehoge PROVISIONING_PROFILE_SPECIFIER: test.debug Stg: CODE_SIGN_IDENTITY: Apple Distribution DEVELOPMENT_TEAM: hogehoge PROVISIONING_PROFILE_SPECIFIER: test.stg Release: CODE_SIGN_IDENTITY: iPhone Distribution DEVELOPMENT_TEAM: hogehoge PROVISIONING_PROFILE_SPECIFIER: test.release CODE_SIGN_ENTITLEMENTS: test/Resources/test_production.entitlements # 依存ライブラリ、フレームワーク dependencies: - target: TestFramework - framework: SDK/Ad/test.framework embed: false - carthage: NavigationNotice - carthage: Nuke - carthage: Reusable - carthage: RxCocoa - carthage: RxRelay - carthage: RxSwift - carthage: RxSwiftExt - carthage: RxWebKit - carthage: SVProgressHUD - carthage: TagListView - carthage: TransitionableTab # 追加Build Phases preBuildScripts: - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml name: Generate resources with SwiftGen outputFiles: - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift - ${PRODUCT_NAME}/Resources/Generated/Colors-Constants.swift - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift - script: | mint run mono0926/LicensePlist license-plist --output-path ${PRODUCT_NAME}/Resources/Settings.bundle name: Run license-plist - script: | cp "${PROJECT_DIR}/${PROJECT_NAME}/Resources/Firebase/GoogleService-Info_${CONFIGURATION}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" name: Run Firebase postBuildScripts: - script: mint run SwiftLint swiftlint name: Run SwiftLint - script: "\"${PODS_ROOT}/FirebaseCrashlytics/run\"" name: Run Crashlytics inputFiles: - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME} - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH) testTests: # テスト設定は省略 testUITests: # テスト設定は省略 # Embedded Framework TestFramework: type: framework platform: iOS scheme: {} deploymentTarget: "11.0" sources: - Datasource - path: "TestFramework/Resources/Generated/L10n-Constants.swift" optional: true type: file settings: groups: [testFrameworkSettings] dependencies: - carthage: APIKit - carthage: CryptoSwift - carthage: Realm - carthage: RealmSwift - carthage: SwiftProtobuf preBuildScripts: - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/framework-swiftgen.yml name: Generate resources with SwiftGen outputFiles: - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift TestFrameworkTests: # テスト設定は省略env.xcconfig// Debug.xcconfig SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited) GCC_OPTIMIZATION_LEVEL = 0 ONLY_ACTIVE_ARCH = YES ENABLE_TESTABILITY = YES GCC_DYNAMIC_NO_PIC = NO MTL_ENABLE_DEBUG_INFO = YES SWIFT_OPTIMIZATION_LEVEL = -Onone OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -debug-time-function-bodies DISPLAY_NAME_PREFIX = debug- PRODUCT_BUNDLE_IDENTIFIER = test.debug ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-debug // Stg.xcconfig SWIFT_ACTIVE_COMPILATION_CONDITIONS = STG GCC_PREPROCESSOR_DEFINITIONS = STG=1 $(inherited) GCC_OPTIMIZATION_LEVEL = 0 ONLY_ACTIVE_ARCH = YES ENABLE_TESTABILITY = YES GCC_DYNAMIC_NO_PIC = NO MTL_ENABLE_DEBUG_INFO = YES SWIFT_OPTIMIZATION_LEVEL = -Onone OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -debug-time-function-bodies DISPLAY_NAME_PREFIX = stg- PRODUCT_BUNDLE_IDENTIFIER = test.stg ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-stg // Release.xcconfig ENABLE_NS_ASSERTIONS = NO VALIDATE_PRODUCT = YES MTL_ENABLE_DEBUG_INFO = NO SWIFT_OPTIMIZATION_LEVEL = -Owholemodule PRODUCT_BUNDLE_IDENTIFIER = test.release ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon知見
色々困ったことや工夫したことを書いていきます。とはいえ、まだまだ新しい記述なども増えていってますので、ProjectSpecを見ながら参考にすると良いです。
Build Configuration
project.yml# BuildConfiguration定義 configs: Debug: debug Stg: debug Release: release定義は簡単に書けます。
後は、定義名をキーにして、設定を分けて書いたりできます。個別のプロジェクト設定
project.ymltargets: # メインプロジェクト test: # メイン設定 settings: configs: Debug: ODE_SIGN_IDENTITY: Apple Development DEVELOPMENT_TEAM: hogehoge PROVISIONING_PROFILE_SPECIFIER: test.debug個別の設定は上記のように書くことができます。
project.yml# 別途読み込みxcconfigファイル configFiles: Debug: configs/Debug.xcconfig Stg: configs/Stg.xcconfig Release: configs/Release.xcconfigですが、個別の設定を
project.yml
に埋め込むと煩雑になってしまいました。
そこでconfigs/{env}.xcconfig
の記述を逃して、管理するようにしました。共通のプロジェクト設定
project.yml# テンプレ設定 settingGroups: testSettings: # 設定 testFrameworkSettings: # 設定 targets: # メインプロジェクト test: settings: groups: [testSettings] # EmbbededFramework TestFramework: settings: groups: [testSettings]
settingGroups
で定義すれば、ここのtarget
で用いれるようになるので便利です。
なので、基本的には共通の設定はsettingGroups
で、BuildConfigurationごとの個別設定は.xcconfig
に分けるようにしました。ソースコード
project.ymltargets: # メインプロジェクト test: sources: - test # 参照ディレクトリソースコードはディレクトリ指定さえしておけば、再起的にCompileSourceとして読み込んでくれます。
ビルド時生成ファイルの参照
project.ymltargets: # メインプロジェクト test: sources: # ビルド時生成ファイルの参照記述 - path: test/Resources/Generated/Assets-Constants.swift optional: true type: file # 省略 preBuildScripts: - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml name: Generate resources with SwiftGen outputFiles: - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift # 省略SwiftGenやR.swift などのビルド時生成ファイルが存在する場合は、参照先を上記のように加えなければなりません。
さらにBuild Phasesの追加スクリプト、preBuildScripts.outputFiles
でそれぞれ記述する必要があります。依存ライブラリ、フレームワーク
project.ymltargets: # メインプロジェクト test: # 依存ライブラリ、フレームワーク dependencies: - target: TestFramework # EmbbededFramework - framework: SDK/Ad/test.framework # 手動読み込みフレームワーク embed: false - carthage: NavigationNotice # Carthage - carthage: NukeEmbbededFramework、Carthageは簡単に記入できます。
違う方法もあるかもしれませんが、手動読み込みフレームワークはプロジェクトディレクトリ外+Path指定してあげる必要がありました。(*1)
Swift Packageもdependencies.package
の記述のみで行けるので、試してみたいですね。CococaPods
当初はCocoaPodsは導入せず、Carthageだけで行こうと思いましたが、広告系の導入はCocoaPodsが必要となり、導入することになりました。ですが、XcodeGenでは、CocoaPodsの依存解決が対応してませんでした。(*1)
そこで以下のようなMakeコマンドを追加してXcodeGenした後にpod install
をするルールに切り替えました。Makefilexcodegen: mint run XcodeGen xcodegen bundler exec pod installxcconfigファイル、Pod用のビルドスクリプトを記述すれば、解決できるかもですが、Podの方に依存した方が安全と判断したため、上記のやり方にしました。
Build Phases
preBuildScripts
project.ymltargets: # メインプロジェクト test: preBuildScripts: - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml name: Generate resources with SwiftGen outputFiles: - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift - ${PRODUCT_NAME}/Resources/Generated/Colors-Constants.swift - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift - script: | mint run mono0926/LicensePlist license-plist --output-path ${PRODUCT_NAME}/Resources/Settings.bundle name: Run license-plist - script: | cp "${PROJECT_DIR}/${PROJECT_NAME}/Resources/Firebase/GoogleService-Info_${CONFIGURATION}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" name: Run Firebaseビルド前に走らせるスクリプトです。
LicensePlist、SwiftGen、GoogleService-Info.plistの環境別読み込みなどしてます。postBuildScripts
project.ymltargets: # メインプロジェクト test: postBuildScripts: - script: mint run SwiftLint swiftlint name: Run SwiftLint - script: "\"${PODS_ROOT}/FirebaseCrashlytics/run\"" name: Run Crashlytics inputFiles: - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME} - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)ビルド後に走らせるスクリプトです。
SwiftLint、FirebaseCrashlyticsなどを走らせてます。CI
今回のプロジェクトはBitriseを採用しており、環境ごとにdeploygateアップロード、AppStoreConnectアップロードをしています。
画像の通り、XcodeGenを走らせるコマンドstepを導入するだけで簡単です。
先述でありました、Cocoapodsのインストール前に行うのを忘れずに。結論
導入コスト、学習コストに関しては基本的にProjectSpecを参考にしていれば、大体は大丈夫でした。
ですが、開発途中でどうしても細かいプロジェクト設定を適用する状況がありますのでその際に一旦Xcodeより設定してみてビルド。ビルド確認してOKそうならproject.yml
に落とし込む作業が多々発生し、なかなか骨が折れました。(もっと良い方法があるのかも)結局そこまでコンフリクトが問題にならないプロジェクトでした。
何よりドキュメントが少ない、導入実績も現時点(*1)でそこまで多くないので、先述の修正コストの方がでかかったです。今回のような規模のプロジェクトで初導入検討ならいらないかなと思いましたw脚注
(*1) 2020年6月現在では
- 投稿日:2020-06-03T13:18:22+09:00
追加要件:FCMにて画像付きPush通知を送る iOS10以上
前置き
- FCMコンソールからのPush通知が実装済みからの追加要件という内容です。
- 今回の記事では送信方法としてPostmanを使用しております。
- なるべく細かく書くつもりですが、後半になるにつれ適当になる可能性があります。
概要
iOS10から追加された Notification Service Extension を利用する。
これは「送信されたペイロード」 -> 「端末に表示」 までの間の部分をあれこれ出来る。
そのため表示される前に画像を読み込んで、それを表示させるといった流れを行える。目次
- Notification Service Extension追加
- Notification Service Extension用のProvisioningファイルを用意
- Notification Service Extension編集
- 送信
Notification Service Extension追加
- XcodeメニューバーのFile -> New -> Target
Product Nameを入力(ex: NotificationService) -> Finish
「MainProjectのBundleIdentifier.上記ProductName」という形式のBundleIdentifierとなる。
言わずともこれは後述するProvisioningファイルのBundle IDとなる。ダイアログが出たらActivateを選択
NotificationServiceExtension
ディレクトリ配下にNotificationService.swift
とInfo.plist
があるものが追加されていればOK
NotificationService.swift
には下記2つのメソッドが既存で作成されている。NotificationServiceoverride func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { ///省略 } override func serviceExtensionTimeWillExpire() { ///省略 }Notification Service Extension用のProvisioningファイルを用意
MainAppと異なる
BundleIdentifier
をもつTargetが追加されたため、
これに対してProvisioningファイルを用意する必要がある。
- AppleDeveloper SignIn
Certificates, Identifiers & Profiles
->Identifiers
-> 「+」押下
App IDs 選択 -> Continue
各項目入力 -> 作成
Description: NotificationServiceExtension
Bundle ID: 「MainProjectのBundleIdentifier.先程のProductName」
※コピーするとエラーが出ることがあるようで、その場合は手動入力をする必要がある。
Certificates, Identifiers & Profiles
->Profiles
-> 「+」押下
App Store
-> 先ほど作成したApp IDs選択 -> メインと同じCertificate
ファイルで作成作成すると既存Provisioningファイルが無効になるため Edit -> Save をし更新する。
xcodeに戻る。
XcodeメニューバーのXcode -> Preferences -> Download Manual Profiles より先ほど作成したProvisioningファイルをDL
Notification Service Extension編集
NotificationService.swiftoverride func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) self.realmWrite(request: request, bestAttemptContent: bestAttemptContent) if let imageUrl = request.content.userInfo["image_url"] as? String { let session = URLSession(configuration: URLSessionConfiguration.default) let task = session.dataTask(with: URL(string: imageUrl)!, completionHandler: {[weak self](data, response, error) in do { if let writePath = NSURL(fileURLWithPath:NSTemporaryDirectory()) .appendingPathComponent("tmp.jpg") { try data?.write(to: writePath) let identifier = "hogehoge" if let bestAttemptContent = self?.bestAttemptContent { let attachment = try UNNotificationAttachment(identifier: identifier, url: writePath, options: nil) bestAttemptContent.attachments = [attachment] contentHandler(bestAttemptContent) } } else { if let bestAttemptContent = self?.bestAttemptContent { contentHandler(bestAttemptContent) } } } catch let error as NSError { print(error.localizedDescription) if let bestAttemptContent = self?.bestAttemptContent { contentHandler(bestAttemptContent) } } }) task.resume() } else { if let bestAttemptContent = self.bestAttemptContent { contentHandler(bestAttemptContent) } } } override func serviceExtensionTimeWillExpire() { if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { // 実行時間をすぎた場合の救済 contentHandler(bestAttemptContent) } }送信
送り先
POST: https://fcm.googleapis.com/fcm/send
Header
HeaderContent-Type:application/json Authorization:key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Firebase
「xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx」をコピーしたトークンに変更
Body
Body{ "data" : { "image_url":"https://hogehoge.jp/fugafuga.png", "type" : "1" }, "notification" : { "title": "タイトル", "body": "ボディ" }, "mutable_content": true, "content_available": true, "to" : "token or topic" }Key:toに指定するもの
- 少しAppDelegateに追加
AppDelegate.swiftextension AppDelegate: MessagingDelegate { func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) { print("fcmToken:\(fcmToken)") Messaging.messaging().subscribe(toTopic: "/topics/global") { error in if let error = error { print("TopicErrorRegistError: \(error)") } } } }この状態でビルドし、コンソールに表示されたものを利用。
- デバイス単体に送りたい場合は
fcmToken:xxxxxxxxxxxxxxxxx
のxxxxxxxxxxxxxxxxx
部分- topic購読している全デバイスに送りたい場合は
/topics/global
toTopicはお好きなもので。
いざ!送信!!
届かなかった方はコメントください。追記
Debug
NotificationService
がきちんと走っている??
と気になりDebugで止めたい場合はMainAppをビルドし一旦メインをデバイスに流した後、NotificationService
を指定してビルドすることで可能脱線
今回の内容と少し違いますが
AppGroups
を利用することでMainとPushに共通データを持つことが可能。
例えば通知を見逃したユーザのために、通知履歴などをアプリに実装。~流れ~
1. 通知を送る
2.NotificationService.swift
にてRealm等に保存処理(SharingPath)
3.MainApp
にてSharingPathを元に通知App
にて保存したデータを共有Realmから読み取る
4. データを元に履歴List画面作成jsonの
data
に任意の値(この記事でいうtype)を追加するなどすれば、履歴リストからタップし、画面遷移なども行えますね!需要がありそうなら細かく書きますが、、
こんなこともできますよーという話でした。
- 投稿日:2020-06-03T08:01:28+09:00
SwiftUI使用時の `precondition failure: invalid input index: 2` のクラッシュ修正する
SwiftUIとCombineを使用している時に、ObservableObjectの値が素早く変更される時(自分の場合だと初期値が流れる→UserDefaultsを読み込んで値が流れる)に、UIコンポーネントの表示切り替えを if文で行っていると
precondition failure: invalid input index: 2
でクラッシュするという問題がありました。解決方法
現状
if
で分岐してクラッシュを回避するのは難しそうだったので、frameのheightを指定して非表示にするようにしました。(VStackの場合)もし、HStackを使用しているならwidthを指定すればいいと思います。
さらに、Imageなどのコンポーネント自体がサイズを持っている場合は親のサイズを変えても見えてしまう場合があるので、Opacityを0にします。問題箇所の抜粋
@State private var dataModel = DataModel() var body: some View { NavigationView { VStack { if dataModel.componentHidden { SomeComponent() } SomeComponent() } } .onReceive(adapter.store.computed.dataModel) { (dataModel) in self.dataModel = dataModel } }修正後
@State private var dataModel = SupportedCardListTabDataModel() var body: some View { NavigationView { VStack { SomeComponent() .frame(height: dataModel.isAdvertiseHidden ? .zero : nil) .opacity(dataModel.isAdvertiseHidden ? .zero : 1) SomeComponent() } } .onReceive(adapter.store.computed.dataModel) { (dataModel) in self.dataModel = dataModel } }参考資料
- 投稿日:2020-06-03T00:42:28+09:00
【プログラミング初心者】Swift基礎~演算子~
はじめに
プログラミングで何かを処理する場合は計算を行ないます。
今回は計算を行うための方法を紹介します。演算子
計算をするとき、数学と同様に式が用いられます。
この式の中で+
などの記号を使って計算しますが、この記号を演算子(Operator)と呼びます。
一方で演算子に計算される値を被演算子(Operand)と呼びます。例えば
let val = 1 + 2
の場合、=
と+
が演算子で1、2が被演算子です。
オペランドの型は全て同じ必要があり、let val = 1 + 0.9
などは許容されません。演算子には色々種類があるので代表的なものを紹介していきます。
代入演算子
=
変数を値を格納するときに使用します。
let val = 1数学で使う
=
は等価を意味しますがプログラム上では代入を意味します。算術演算子
値の数値計算を行うための演算子です。
計算された値の型はオペランドと同じ型になります。+
左右のオペランドを加算した結果を返します。
let val = 1 + 2 // 3
+
の場合は文字列など他の型にも対応できる場合があります。let val = "文字列を" + "結合" // "文字列を結合"-
左のオペランドから右のオペランドを減算した値を返します。
let val = 1 - 2 // -1*
左右のオペランドを乗算した値を返します。
let val = 2 * 3 // 6/
左のオペランドから右のオペランドを除算します。
let val = 4 / 2 // 2注意が必要なのは先述したように計算された値の型はオペランドと同じ型になります。
つまりオペランドがInt
型の場合は結果もInt
型となり整数となります。
割り切れない場合は少数部が切り捨てられます。let val = 5 / 2 // 2 let val = 5.0 / 2.0 // 2.5%
左のオペランドから右のオペランドを除算した余りを返します。
let val = 5 / 2 // 1+=
変数に右辺の値を加算します。
var val = 1 val += 9 // val: 10 // val = val + 9 と同義-=
変数から右辺の値を減算します。
var val = 10 val -= 8 // val: 2 // val = val - 8 と同義比較演算子
比較演算子は等号、不等号など、左右のオペランドを比較した結果を返します。
結果は真偽値を表すBool
型となります。
Bool
はtrue
とfalse
2つの値しか取らず、それぞれ真、偽を表します。==
左右のオペランドが等価か比較します。
let val1 = 1 == 1 // true let val2 = 1 == 2 // false let val3 = "文字列" == "文字列" // true!=
左右のオペランドが等価ではないかを比較します。
let val1 = 1 != 1 // false let val2 = 1 != 2 // true let val3 = "文字列" != "文字列" // false>
大なりを表し、左右のオペランドを比較した結果を返します。
let val1 = 1 > 0 // true let val2 = 1 > 2 // false let val3 = 1 > 1 // false<
小なりを表し、左右のオペランドを比較した結果を返します。
let val1 = 1 < 0 // false let val2 = 1 < 2 // true let val3 = 1 < 1 // false>=
大なりイコールを表し、左右のオペランドを比較した結果を返します。
let val1 = 1 >= 0 // true let val2 = 1 >= 2 // false let val3 = 1 >= 1 // true<=
小なりイコールを表し、左右のオペランドを比較した結果を返します。
let val1 = 1 <= 0 // false let val2 = 1 <= 2 // true let val3 = 1 <= 1 // true論理演算子
ANDやORなどの論理演算を行ないます。
オペランドはBool
で結果もBool
を返します。!
否定(NOT)を意味します。
let value1 = true let result1 = !value // false let value2 = false let result2 = !value // true
Aの値 演算結果 true false false true &&
論理積(AND)を意味します。つまり「AかつB」です。
let leftValue1 = true let rightValue1 = true let result1 = leftValue1 && rightValue1 // true let leftValue2 = true let rightValue2 = false let result2 = leftValue2 && rightValue2 // false
Aの値 Bの値 演算結果 true true true true false false false true false false false false ||
論理和(OR)を意味します。つまり「AまたはB」です。
let leftValue1 = true let rightValue1 = false let result1 = leftValue1 || rightValue1 // true let leftValue2 = false let rightValue2 = false let result2 = leftValue2 || rightValue2 // false
Aの値 Bの値 演算結果 true true true true false true false true true false false false 組み合わせる
演算子を1つの式の中に複数入れることも可能です。
算術演算子の場合は数学と同じで乗算、除算から計算されます。
また()
で括ると括った部分から計算されます。let val1 = 1 + 2 - 3 * 4 / 5 // 1 let val2 = (1 + 2 - 3) * 4 / 5 // 0論理演算子も同様に組み合わせることができます。
たとえば「1 < x < 10」を判定する場合以下のメソッドで判定することができます。func validate(x: Int) -> Bool { return x > 1 && x < 10 } let result1 = validate(x: 5) // true let result2 = validate(x: 15) // falseプログラムではこのように様々な演算子を組み合わせて演算していき、処理を処理を制御します。
最後に
今回は代表的な演算子について紹介しました。
紹介した演算子はプログラミング言語において基本的なもので、Swift以外の多言語でもほぼ同様の書き方ができます。
このあたりはかなりの頻度で使うので覚えていきましょう。今回の内容は以上です。
本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684