- 投稿日: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-03T16:31:01+09:00
App Store Connect Q & (A)
App Store Connect に関する気になることを記載する
何か分かれば、Answerとして追記するアップロード関係
アップロードしたけどTestFlightに表示されない
Q.
アクティビティにはビルドとして表示されるが、TestFlightには表示されない
他のビルドバージョンは全て表示されているのにそのバージョンだけ抜けているアップロードしたアプリはダウンロードできるのか
A.
多分できないApp Analysis
アップデート数を確認したいのだが
A.
App Store Connectのウェブサイトでは項目としてなかったが、iOSアプリの方からは確認できたアップデート数の定義は?
Q.
調べても書いていないアップデート日じゃなくて公開日はどこに書いてある?
A.
多分、アクティビティ→App Storeバージョン→配信準備完了の日付
尚、公開予約した場合は「デベロッパによるリリース待ち」となっているはず機種ごとに各種データの集計をしたい
A.
機種を選ぶ項目がなかったため、機種ごとにはできない。
Apple TV, iPad, iPhone, iPodのいずれかの選択はできた。
また、iOSバーじょにゃあぷりReference
- 投稿日: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-03T08:44:48+09:00
マルチモジュールでIBでNamedColorを使用する方法
こちら を元に対応した内容ですが、あまり日本語で書いているところが少ないと思い書くことにしました。
XcodeのAssetでnamed colorを設定してIBで下記のように色を指定できて便利です。
※ ↑こちら の画像を参照しています
しかし、マルチモジュールの場合、IBで設定した後に以下のような警告が表示されて、Assetで定義をしていた色を変更しても色の変更が反映されないことがあります。
2018-04-26 17:30:30.009855+1200 MyApp[82987:11407859] WARNING: Unable to resolve the color named "PrimaryText" from any of the following bundles: com.myfirm.MyApp, com.myfirm.MyApp理由としては、IB内のxmlでは下記のようにnamedColorの名前と失敗した場合の時のfallbackColorは記載されていますが、どのモジュールにnamedColorの定義がされているかが記載されていません。
そのため、他のモジュールとかに色の定義がある場合、参照できずfallbackColor(恐らくIBで設定時の色)が使用されてしまうためです。※ ↑こちら の画像を参照しています
ならどのように防げば良いかと言うと、色のリソースはサイズがさほど大きいことはほぼないと思います。
なので、下記のように色のAssetのTargetを色を使用するモジュールそれぞれに含みfallbackColorではなくnamedColorの参照にxibやstoryboardでも成功させることで、Assetの色の定義を変更してもそれぞれのモジュールのIBで指定した色の箇所にも反映されるようになります。
(注意:色のリソースがモジュールごとに重複するのでわずかにアプリのサイズは大きくなると思います)
- 投稿日:2020-06-03T07:33:57+09:00
iOSのURLスキームの脆弱性について
概要
iOSのURLスキームが意外と脆弱だから気をつけた方が良いよと言う話をします。
※違うよ、とかあれば気軽にコメント頂ければ幸いです
iOSのURLスキームについて
下記のようにURLスキームを叩くことがあると思うのですが、
hoge://〜これってどの情報を元にどのアプリがこのURLスキームを受けるか決めているのでしょうか。
答えとしては、どのアプリが受け取ると言う情報が入っている訳ではありません。
なので
AアプリとBアプリでそれぞれ同じプロトコル名で受け取るようにしていた場合、どちらがそのURLスキームを受け取るかわかりません
(過去に確認した挙動だと、先にインストールされている方のアプリが受け取るようです)Aアプリ
hoge://〜 を受け取るように実装Bアプリ
hoge://〜 を受け取るように実装奪い取りたいURLスキームのプロトコルなんて他の人わからないじゃんって思うかもしれませんが、
こちら で使用しているツールのように結構に気軽に他のアプリが受け取るURLスキームは調べることはできそうです。Androidの場合は下記のように(アプリのパッケージ名)を指定することで起動するアプリを指定できます。
intent://〜#Intent;package=(アプリのパッケージ名);scheme=hoge;end;SFSafariViewControllerの例外
アプリからSFSafariViewControllerを開きURLスキーム(Deeplink)で戻ってくることがあると思いますが、
その場合はSFSafariViewControllerを開いたアプリが優先的にURLスキームを受け取るようで、
他のアプリに先に奪い取られてしまうことはないようです。まとめ
iOSでSFSafariViewController以外でURLスキームを送信・受信する際は、セキュリティに影響するような情報の受け渡しはやめよう。
iOSのURLスキームは他のアプリが受け取る可能性があることを考慮しよう。
- 投稿日: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