- 投稿日:2019-09-27T18:10:00+09:00
Xcode11で古いプロジェクトテンプレートを利用する
動機
先日iOS13がリリースされましたが、アプリを新規に始める場合に、iOS11〜1OS13あたりをターゲットに設定することは、まだまだあるかと思います。
開発では、新しいXcode11をメインに使用したいのですが、
Single View App
テンプレートでプロジェクトを新規作成した場合、そのままターゲット設定をiOS13未満にするとScenes関連のclassが使用できないためコンパイルエラーがでます。
Scenesとは、iOS13で登場したひとつのアプリプロセスに対してマルチUI(マルチウインドウ)をサポートするための仕組みのようで、現状では主にiPadでの用途があるようです。これによってSceneDelegate.swift
と関連設定が、デフォルトのプロジェクトに含まれているためです。ターゲットiOSバージョンによってはXcode11で、Scenes関連の設定が含まれないXcode10バージョンの
Single App Viewテンプレート
もまだまだ使いたい時があるなあ、と思ったのでXcode10からコピーしてXcode11からも使えるようにしておけば便利かもと思った次第です。というわけで、当記事ではXcodeのカスタムテンプレート追加手順を、表題の件に絞ってまとめておきます。
そもそもそういう頻度が少ない場合
そんなにプロジェクト新規作成の頻度が少ない場合、後述するテンプレートのコピーをしなくても、Xcode10&11併用環境であればiOS12〜以下をターゲットする場合、普通にXcode10.3でプロジェクトを新規作成して開始すれば大丈夫です。そうでない場合は、新規作成のためだけに古いXcodeを保持しておかなければならない、というデメリットがあります。(マイグレーションが必要な時などに役には立つのですが)
または手作業でXcode11で作成したプロジェクトの
SceneDelegate.swift
などを消すことも可能。しかし何度も行う場合には省略したいとも思います。
- Xcode 11 - Opt out of UISceneDelegate/SwiftUI on iOS 13
- How to remove SceneDelegate code from iOS 13 project?テンプレートのコピー手順
では、Xcode10から古いタイプの
Single View App
のプロジェクトテンプレートをコピーしてきて独自のテンプレートとして、Xcode11で使えるようにします。作業後の完成イメージのキャプチャですが、作業後にはプロジェクト作成ウィザードに画像のような形で選択出来るようになります。
カスタムテンプレート用のフォルダを作成
Xcode独自テンプレートは以下のパス配下に追加する決まりになってます。
# 独自Projectテンプレートのパス ~/Library/Developer/Xcode/Templates/Project\ Template # 独自Fileテンプレートのパス ~/Library/Developer/Xcode/Templates/File\ Templateテンプレートを置くフォルダを作成。テンプレートのカテゴリ表示はフォルダ名が反映されるので
My Templates
という名前のフォルダを作成することにします。$ mkdir -p ~/Library/Developer/Xcode/Templates/Project\ Templates/My\ TemplatesXcode10.3からのテンプレート複製
フォルダの準備ができましたので、次は中身です。プロジェクト新規作成ウィザードに表示されるテンプレートは
Xcode.app
パッケージを開いた中に入ってますので、そいつをコピーします。Xcode10.3がない場合はダウンロードしてくる必要があります。Xcode.appを右クリックからパッケージの内容を表示
で辿っていくか、以下のコマンドを実行して開きます。# パスの中の`Xcode_10.3.app`部分は適宜変更してください。 open /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/Finderで操作するか、以下のコマンドを実行し
Xcode_10.3.app
のパッケージの中からSingle View App.xctemplate
をカスタムテンプレート用フォルダへとコピーします。cp -r /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/Single\ View\ App.xctemplate ~/Library/Developer/Xcode/Templates/Project\ Templates/My\ Templates/Single\ View\ App.xctemplate追記(2019/09/29):
[重要] テンプレートのIdentifierがXcode11のオリジナルと重複していると、ウィザードにオリジナルの方のSingle View Appアイコンが表示されなくなってしまっていたので、
コピーした方のSingle View App.xctemplate
の中のTemplateInfo.plist
ファイルを開いて(Xcodeが起動するはず)Identifier
部分を何か好きな文字列に編集を行います。(↓画像の例ではオリジナルの文字列に_Xcode10.3
をを付加しました。)作業は以上で完了です。
確認
Xcode11を開いて、プロジェクト新規作成のウィザードを起動します。
File -> New -> Project...
を選択か、または、ショートカットキーでCmd + Shift + N
です。Xcode10からカスタムテンプレートとしてコピーした
Single View App
がMy Templates
項目に表示されており、選択するとScenes関連なしのプロジェクトを開始出来るようになっているはずです。まとめ
Xcodeでは、最新のAPIに合わせたテンプレートへ変わることは珍しくないように思いますので、過渡期期間では今後も役に立つかもしれません。
参考?
- 投稿日:2019-09-27T17:36:11+09:00
BrightFuturesのレスポンス結果を表示するのはonSuccessのスコープ内でやろう
BrightFuturesを使用した非同期処理の場合、
APIから取得した結果を表示するためにUILable等に設定するのは、onSuccess
のスコープ内でやること。
そうしないと非同期処理のため、値がまだ帰ってきていない内に描画処理が走り、想定通りに反映されない。普段JavaScriptで書いてるときは自然としてそうだが失念してた。
Async/Awaitほしい、、class SomeViewController: AutoLayoutViewController { private var someObj:SomeObject = SomeObject() private var someLabel: UILabel override func viewDidLoad() { [BrightFuturesを使った非同期処理] .onSuccess { success in someObj = success // ここが正解 self.someLabel.text = success.text } // ここでやると反映されない // self.someLabel.text = success.text } }
- 投稿日:2019-09-27T17:12:32+09:00
iOS13対応をまとめてみた
前書き
対応が必要そうなものを探してまとめてみた。
正直身に降りかからないと細かい仕様までは分からないので、現状をまとめておいて判明次第追記していく。今すぐ対応が必要
UISerchBarの
value(forKey:)
メソッドが廃止された必須か: 必須
内容:UIserchBarでvalue(forKey:)
を使用しているとアプリクラッシュします
参考:https://qiita.com/ktanaka117/items/1a30c4c6052cd8e1a0b7Bluetooth利用にパーミッションが必須になった
必須か:必須
いつまでに対応すべきか:今すぐ
内容:位置情報等のパーミッション同様、設定しないまま機能を使おうとすると落ちる
参考:http://harumi.sakura.ne.jp/wordpress/2019/08/27/ios13%E3%81%8B%E3%82%89%E3%81%AEbluetooth%E3%83%91%E3%83%BC%E3%83%9F%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3/UISearchDisplayControllerがdeprecatedから完全廃止になった
必須か:必須
内容:UISearchDisplayControllerを使用しているとアプリクラッシュする
参考:https://github.com/mapbox/mapbox-gl-native/issues/15559Sign in with Apple
必須か:アプリによっては必須
いつまでに対応すべきか:新しいアプリは必須。既存アプリは2020年4月。
内容:サードパーティ製またはソーシャル(Facebook、Google、Twitter、LinkedIn、Amazon、WeChat)のログインサービスを使用している場合は、AppleIDでのサインイン対応必須。自社システムのみのログインなら対応不要。
参考:https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple
https://qiita.com/_asa08_/items/4f6e383fc6c9d38046edVoIPプッシュの仕様が変わった
必須か:必須
いつまでに対応すべきか:新しいアプリは必須。既存アプリは2020年4月。
内容:VoIPプッシュの利用が着信のみになるらしい
参考:https://developer.apple.com/documentation/pushkit/responding_to_voip_notifications_from_pushkit?language=objc
https://qiita.com/sunskysoft/items/34665541d9bec438ee362020年4月以降は全てのアプリでiOS13対応、Xcode11でのビルドが必須
必須か:必須
いつまでに対応すべきか:新しいアプリは必須。既存アプリは2020年4月。
内容:題名の通り
参考:https://developer.apple.com/news/?id=09102019a&1568158483今後必須になりそうなもの
ダークモード対応
必須か:準必須(いまのところ必須ではないが以後必須になる可能性が高い)
いつまでに対応すべきか:不明
参考:https://developer.apple.com/documentation/xcode/supporting_dark_mode_in_your_interface/choosing_a_specific_interface_style_for_your_ios_app?language=objc
https://qiita.com/p_on_ro/items/91e6659fda662fb2aac0UIWebViewがiOS13以降deprecatedになった
必須か:準必須
いつまでに対応すべきか:不明
内容:ドキュメントがdeprecatedになっている。代替UIはWKWebView。
参考:https://developer.apple.com/documentation/uikit/views_and_controls?language=objcその他変更
遷移アニメーションの対応
必須か:オプション
いつまでに対応すべきか:必要に応じて
内容:遷移モードのデフォルト値が変更された。意図していない「戻る操作」が発生する可能性がある。
参考:https://qiita.com/yh_genephia/items/b82d4df36b4ef1fdcdd8システムフォントが変更された
必須か:オプション
いつまでに対応すべきか:必要に応じて
内容:boldがiOS12以前より太くなった。
参考:https://qiita.com/a_t/items/484f7ad52e8c9fd18903Realmが落ちることがある
必須か:オプション
いつまでに対応すべきか:必要に応じて
内容:iOS13のみエラーが発生することがあるとのこと。
参考:https://stackoverflow.com/questions/48810421/rlmexception-primary-key-property-serial-does-not-exist-on-object-book-migスクロールビューのインジケータの仕様が変わった
必須か:オプション
いつまでに対応すべきか:必要に応じて
内容:Could not cast value of type ‘_UIScrollViewScrollIndicator’
参考:https://qiita.com/MilanistaDev/items/8151dd2544b2eda9353b
- 投稿日:2019-09-27T17:05:36+09:00
OneSignalをXcode11でビルドするなら最新じゃないと動かない
- 投稿日:2019-09-27T16:45:38+09:00
Reactorのtransform内でPublishSubjectをマップした挙動をテストした際にハマったこと
transform(mutation:)
内の処理以下のように
event
をマップしたReactor内のtransformの挙動をテストする際につまづいたポイントを書き残しておきます。import ReactorKit import RxSwift final class HogeReactor: Reactor { ... func transform(mutation: Observable<Mutation>) -> Observable<Mutation> { return .merge( mutation, provider.accountService.event.flatMap { event -> Observable<Mutation> in switch event { case let .updateProfile(user): return .just(.setUser(user)) } } ) } ... }
event
の実装ちなみに
provider.accountService.event
は以下のようにPublishSubject
で実装されています。enum AccountEvent { case updateProfile(user: User) } protocol AccountServiceType { var event: PublishSubject<AccountEvent> { get } ... } final class AccountService: AccountServiceType { let event = PublishSubject<AccountEvent>() ... }テスト失敗
これをテストする際に単純にstreamを流しただけだとテストがうまくいきませんでした。
@testable import Hoge import Nimble import Quick class HogeReactorSpec: QuickSpec { ... describe("state.user") { context("when receives updateProfile event", closure: { it("is updated user", closure: { let updatedUser = TestData.testUser() stubServiceProvider.accountService.event.onNext(.updateProfile(user: updatedUser)) // Failed:stateにupdatedUserがセットされず expect(hogeReactor.currentState.user) == updatedUser }) }) } }一度も
transform(mutation:)
が呼ばれてないため、event
のマップがそもそもできていないのが原因ぽいです。テスト通過
transform(mutation:)
を呼ぶために適当にactionを呼んでやるとうまくいきました。@testable import Hoge import Nimble import Quick class HogeReactorSpec: QuickSpec { ... describe("state.user") { context("when receives updateProfile event", closure: { it("is updated user", closure: { // NOTE: Call any action to set up transformed mutation hogeReactor.action.onNext(.load) let updatedUser = TestData.testUser() stubServiceProvider.accountService.event.onNext(.updateProfile(user: updatedUser)) // Pass:stateにupdatedUserがセットされる expect(hogeReactor.currentState.user) == updatedUser }) }) } }本当にこれが正しいのかわかりませんが、とりあえず動いたので良しとしてます。
知見ある方いましたらコメントで教えていただけると嬉しいです。
- 投稿日:2019-09-27T12:14:25+09:00
Xcode11で作成したプロジェクトからSceneに関する機能をオミットする
Xcode11とUISceneとUIWindowにまつわるトラブルシュートに追われている。
個別のケースに分解してお届け。背景
Xcode10で作成したプロジェクトをXcode11で開いた場合と、Xcode11で作成したプロジェクトで
UIWindow
の振る舞いが異なった。この違いはプロジェクトがScene Based Lifecycleに対応しているかどうかに左右されることが分かった。対応
簡単に言うと以下を実施すればよい。
AppDelegate
にwindow
を生やす- Info.plistから
Scene
に関する項目を取り除くAppDelegate
からScene
に関する項目を取り除くこれにより、iOS13で動かしてもAppDelegateを基本としたアプリのように動作する。(View Hierarchyを見るとUISceneは健在なので、互換性を保っているだけっぽい)
AppDelegate
にwindow
を生やすSceneに関連する物事をオミットしたあと、AppDelegateにwindowプロパティが無いとブラックアウトしてしまうので、生やす。
// AppDelegate.swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? }// AppDelegate.h @interface AppDelegate: UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @endInfo.plistから
Scene
に関する項目を取り除くInfo.plist から [Application Scene Manifest] の項目を取り除く。
ここまでの対応だけだと、iOS13でアプリを実行した場合に表示すべきStoryboardの情報が見つけられずブラックアウトする。
AppDelegate
からScene
に関する項目を取り除くAppDelegateから、Scene Based Lifecycleに関するメソッドを取り除くことで、オミットが達成される。
具体的には
AppDelegate application:configurationForConnectionSceneSession:
を取り除けば良い。
- 投稿日:2019-09-27T11:33:41+09:00
iOS実機でborder-radiusが効かない場合の対応
- 投稿日:2019-09-27T10:42:08+09:00
ios開発、addTargetメソッドを調べてみた
addTargetの引数
target アクションメソッドが呼ばれるオブジェクトを指定。 action 呼ばれるメソッドを記述 controlEvents UIControlEventsの定数を記述 controlEventsに入るUIControlEventsには以下がある
touchDown コントロール内でのタッチダウン touchDownRepeat 複数回のタッチダウンイベント touchDragInside コントロール内で指がドラッグされた touchDragOutside コントロール外で指がドラッグされた touchDragEnter コントロール内へ指がドラッグされた touchDragExit コントロール内から外へドラッグされた touchUpInside コントロール内でタッチアップされた touchUpOutside コントロール外でタッチアップされた touchCancel 不明 valueChanged 値の変化 primaryActionTriggered タッチアップと同じ? editingDidBegin TextFieldでの編集開始 editingChanged TextField内の文字変化や入力 editingDidEnd 画面外タップでのTextFieldの編集終了 editingDidEndOnExit リターンキーによるTextFieldの編集終了 allTouchEvents すべてのタッチイベント allEditingEvents TextFieldに関するイベント applicationReserved 予備だと思う systemReserved 予備だと思う allEvents 全てのイベント 参考
- 投稿日:2019-09-27T09:31:07+09:00
CircleCI + fastlane で Firebase App Distribution に ipa をアップロードする
Firebase App Distribution とは
公開前のAndroid/iOSアプリを配布できるサービスです。
https://firebase.google.com/products/app-distribution?hl=jaconfig.yml
CircleCI 上で firebase CLI を使用するため、
firebase-tools
をインストールしましょう。fad: steps: ... (いろいろ設定) ... - run: name: Install Firebase Tools command: npm install firebase-tools - run: name: Deploy IPA file to Firebase App Distribution command: | bundle exec fastlane ios fadFastfile
Fastlane に
firebase_app_distribution
という action が用意されているのでそれを使います。Distribute iOS apps to testers using fastlane | Firebase
desc "Push a new develop build to Firebase App Distribution" lane :fad do ... (ここでipaを作る) ... firebase_app_distribution( app: "1:123456789:ios:abcd1234", testers: "tester1@company.com, tester2@company.com", release_notes: "Lots of amazing new features to test out!", firebase_cli_path: "./node_modules/.bin/firebase" ) endこれで継続的に Firebase App Distribution に ipa をアップロードできるようになりました!
リンク
- 投稿日:2019-09-27T07:41:04+09:00
Xcode11で作成したプロジェクトのDeployment Targetを下げるとブラックアウトする問題の対処法
Xcode11とUISceneとUIWindowにまつわるトラブルシュートに追われている。
個別のケースに分解してお届け。現象
Xcode11で作成したプロジェクトをiOS13未満の環境で実行するとブラックアウトしてしまう。
再現手順
- Xcode11にて、新規プロジェクトの作成を行う。
- test off
- SwiftUI off
- Project設定を開き、Deployment Targetを iOS12 にする。
- iOS13未満ならなんでもいい
- iOS シミュレーターのうち、iOS12のもので実行する。
- iOS13未満ならなんでもいい
原因
コンソールに「
AppDelegate
にUIWindow *window
が必要だ」と説明されている。2019-09-27 07:34:57.175062+0900 sample-xcode11[52812:454025] [Application] The app delegate must implement the window property if it wants to use a main storyboard file.iOS13を対象としたアプリケーションでは、root windowは SceneDelegateが所有しているので、AppDelegateには必要ないというAppleのおせっかいである。
対応
AppDelegate
にwindow
を生やす。// AppDelegate.swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? }// AppDelegate.h @interface AppDelegate: UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @end
- 投稿日:2019-09-27T01:31:30+09:00
【iOS】Sign In with Apple 実装してみた。
Sign in with Appleの必須化
2019/6/3に更新されたレビューガイドラインに以下の記述がありました。
Sign in with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year
※ 公式:https://developer.apple.com/news/?id=06032019jどうやらログイン連携にサードパーティを用いているアプリには、Sign in with Apple の実装が必須になるよう…。
これは早めに試してみようということで早速実装してみました!!環境
- Xcode11 以上
- 対応端末iOS13 以上
今回は自社の HiNative を使ってお試し実装してみました。
(ちゃっかり宣伝: WEB・Apple Store)実装
ステップはたったの3つ!結論からいうと超簡単でした。
- Xcodeの設定
- Providerを生成してリクエスト
- Delegateでその後の処理
1. Xcodeの設定
TARGET > Signing & Capabilities に Sign in with Apple を追加します。
これでXcodeの設定は完了です。
1.2. (ステップには記載なし) 適当にボタン作ってね
ボタンとActionを用意してください〜。
ボタンのレイアウトは公式から指定があるので、こちらの公式を参考に!2. Providerを生成してリクエスト
ViewController.swiftimport AuthenticationServices @objc func authorizationAppleID() { if #available(iOS 13.0, *) { let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.performRequests() } }
if #available(iOS 13.0, *)
なので、ボタンの表示非表示も気をつけないといけないですね。3. Delegateでその後の処理
ViewController.swiftextension ViewController: ASAuthorizationControllerDelegate { @available(iOS 13.0, *) func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { // 取得できる値 let userIdentifier = appleIDCredential.user let fullName = appleIDCredential.fullName let email = appleIDCredential.email } } @available(iOS 13.0, *) func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { // エラー処理 } }補足
ASAuthorizationAppleIDCredential
は 2 で生成したASAuthorizationAppleIDProvider
のリクエストが成功した際に呼ばれます。
今回はこの実装のみですが、他にも iCloud Keychainのクレデンシャル情報 を用いた実装もあるようです。
(詳しく知りたい方は下記の参考記事を見てみてください!)動かすとこんな画面が出てきます
FaceIDでログインできるので本当に楽…!!
参考にさせて頂いた記事
- 投稿日:2019-09-27T00:28:13+09:00
SwiftUIでWeb APIから取得した結果を表示する
SwiftUIのViewを更新するためにはデータに応じて、適切な状態の宣言をする必要があります。
ここでは、Web APIと通信し結果を表示するという、よくあるアプリをSwiftUIで実装することで、その方法を紹介します。
必要な実装は以下の2つです。
ObservableObject
に準拠するクラスを作成し変更の通知を発行- SwiftUIのViewで通知を監視
それでは、GitHubのフォローしているユーザー一覧を表示するアプリを例にし、実装していきます。
ObservableObjectに準拠するクラスを作成し変更の通知を発行
通信を行い取得したユーザーを保持するクラスを作成し、
ObservableObject
に準拠させます。(準拠させると言ってもクラスであれば特に何かを実装する必要は無いです)
そして、保持したユーザーの変更を通知するためには、@Published
属性をプロパティに付与します。import Foundation import Combine class FollowingUserStore: ObservableObject { @Published var users: [User] = [] init() { load() } func load() { let url = URL(string: "https://api.github.com/users/maoyama/following")! URLSession.shared.dataTask(with: url) { data, response, error in DispatchQueue.main.async { self.users = try! JSONDecoder().decode([User].self, from: data!) } }.resume() } } struct User: Decodable, Identifiable { var id: Int var login: String }たったこれだけの実装で、通信を行いユーザーを取得できた際の通知を発行することが可能になります。
SwiftUIのViewで通知を監視
今回のような、あるViewだけで使用するデータの通知を監視すれば良い場合はProperty Wrapperの
ObservedObject
を使う方法が適切です。
ObservableObject
をプロパティに保持し、@ObservedObject
を付与します。import SwiftUI struct ContentView: View { @ObservedObject var store = FollowingUserStore() var body: some View { List(store.users) { (user) in UserRow(user: user) } } } struct UserRow: View { var user: User var body: some View { Text(user.login) } }これで
FollowingUserStore.users
の変更を監視できます。まとめ
これだけの実装でWeb APIから取得した結果を表示することができました。
また、適切な宣言をするだけで、データの同期を手動ですることなく、SwiftUIとCombineが自動で行ってくれています。これにより、コードがとてもシンプルで読みやすくなることが期待できそうです。
参考資料
https://developer.apple.com/documentation/swiftui/state_and_data_flow
https://developer.apple.com/videos/play/wwdc2019/226/
https://developer.apple.com/documentation/combine/observableobject
- 投稿日:2019-09-27T00:03:17+09:00
【Swift】Alamofire, MoyaのURLEncodingで、Boolが0,1になってしまうことへの対応
事象: Moyaを使ってAPIをリクエストしたが、Boolが0/1で送られていた
API系のライブラリとしてよく用いられるのが、 Moya というライブラリだと思います。
私自身も99%のプロジェクトでMoyaを使っています。
Moyaでリクエストパラメータを送る際に以下の形式で送っています。
requestParameters(parameters: [String : Any], encoding: Moya.ParameterEncoding)しかしながら、この形式で送ると、Bool型が一度Any型に変換され、そして、最終的にNSNumberとしてリクエストされるようです。
詳しくは こちら
とはいえ、↑この人が言うように、BoolをAnyにすると、NSNumberになる的な事象は、playground上では確認できませんでした。。
対応: Bool.description で、Bool型をString型に変更して対応
Bool型→Any型: NSNumber (0/1)
Bool型→String型→Any型: Bool (true/false)↑のようになるようです。
元々のコード
var parameters: [String: Any]? { return ["key" : boolParam] }↓↓↓↓↓↓↓↓↓
修正後のコード
var parameters: [String: Any]? { return ["key" : boolParam.description] }その他
パケットキャプチャで確認しましたが、こちら↓とても参考になりました。
通信系のデバッグには Charles が便利