- 投稿日:2020-06-27T21:58:57+09:00
UITabBarController において選択されたタブに応じた処理をしたい時
先日、 Google Analytics などのイベントトラッキング系の実装をしている時に、
「選択されたタブのログを取りたい、ただし現在開いているタブは取らなくていい。」という場面に出会いました。画面に依らない共通の処理だったので、それぞれの
ViewController
ではなくUITabBarController
に書くことにしました。
簡単な実装かと思いきや、結構奥深い学びがあったので共有します。
- バッドプラクティスとそれが悪い実装となる理由
- 最終的な実装例
を紹介します。
2020 年に Swift 始めたばかりの初心者なので、アドバイス・指摘待っています!!!
ざっくりとした結論
始めに、最終的に至った形を示しておきます。
後の理解を深めるためと、時間がない人のためです。
tabBar(_:didSelect:)
を使おう- 引数
item
とitems
配列でパターンマッチングしようitem.tag
を使った指定はバッドプラクティスになりがちなので、極力避けよう!これを読んで、「そんなの当たり前じゃん?」ってなった方はもうここから先を読む必要はないです。
逆に「なんでそれがバッドプラクティス?」ってなった方は読んでみて下さい!!バッドプラクティス:
item.tag
を使うググってみると、ちょこちょこ見かける方法ですが、これは 基本的にバッドプラクティスになりがち です。
具体的には以下のような実装ですね。 (僕も最初こうやってました)class TabBarController: UITabBarController { override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { switch item.tag { case 0: // tag = 0 に対する処理 case 1: // tag = 2 に対する処理 ... default: break } } }理由はまだしっかり理解できていませんが (誰が教えて…)、
- 個別に
tag
を設定する必要がある- そしてその
tag
を参照することで依存が生まれるからかなーと思っています。
個別に
tag
を設定する必要があるということは、別に左から 0, 1, 2, 3, ... と付けなくても良いわけで…
めちゃめちゃ屁理屈な人が 4, 8, 12, ... とか付けてたら泣きますよね笑このように特定の
UIView
インスタンスへの依存を生むような実装は避けるべきです。最終的な実装例
じゃあどうするかというと、メソッドの引数である
item
とtabBar.items
の配列を照らし合わせる形で実装しました。まず、実装の前提ですが、以下のような
Tab
というタブの型を定義しています。class TabBarController: UITabBarController { private enum Tab: Int { case home case history case setting } ... }使うメソッドは先と同じく、
tabBar(_:didSelect:)
です。
ただし異なる点として、tabBar.items
の配列とパターンマッチングしていきます。class TabBarController: UITabBarController { ... // 現在選択されているタブをプロパティとして持っておく private var selectedTab: Tab = .home override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let firstIndex = tabBar.items?.firstIndex(of: item), let tab = Tab(rawValue: firstIndex) else { return } // 表示されている画面と同じタブがタップされた場合は何もしない if tab == selectedTab { return } switch tab { case .home: // .home 画面に対する処理 case .history: // .history 画面に対する処理 case .setting: // .setting 画面に対する処理 } // 選択されているタブの更新 selectedTab = tab } }肝になるのはここですね。
guard let firstIndex = tabBar.items?.firstIndex(of: item), let tab = Tab(rawValue: firstIndex) else { return }何をしているかというと、
- 引数である
item
で選択されたUITabBarItem
を取得してtabbar
のitems
(UITabBarItem
が順番に入った入った箱) の中の何番目かをfirstIndex(of:)
で調べてTab
型に変換するということをしています。
最後の型の変換は今回の実装ならではですが、2
番目までで、選択されたタブの順番が取得できるので、あとはその順番を使って処理を作っていけばいいです。この実装では
UIKit
やUIView
への依存を生んでおらず、Tab
を使ってUITabBar
をセットアップしていけば後でバグが生まれるということが少ないです。
(仮にバグが生まれても、発見が早い)まとめ
UITabBarController
初めて触ったのですが、奥深すぎて仲良くなれる気がしません…
(UITabBarController
とUITabBar
の違いも分かってない)
追記
本記事公開後、 @lovee さんより、UITabBarController
とUITabBar
の違いについてコメント頂いたのでそのまま記載します!!ちなみに
UITabBar
とUITabBarController
の違いはそれぞれの名前とおり、UITabBar
はバー(UIView
継承)でUITabBarController
はコントローラー(UIViewController
継承)です
ですが、今回の実装を経験して、「後から保守性が落ちるコード」、「クラッシュ・バグを生みかねないコード」を書かないように意識して実装したいと思いました。
いずれは人のコード読んで「なんか怪しいな」と怪しいコードを 嗅ぎ分けられる ようになりたいですこの記事は自分の備忘録を建前として、 iOS エンジニアの皆様から「もっとこうしたら良いよ!」 「こんな方法もあるよ!」 「その実装だとこんな時危険だよ!!」 という意見を頂戴することを裏の目的としていますので、
アドバイス・指摘バンバンください!参考
- 投稿日:2020-06-27T19:01:03+09:00
[Swift] Date生成時にありえない日付のときはnilを返したい
TL;DR
DateFormatterを使い
isLenient = false
に設定する。func makeDate(year: Int, month: Int, day: Int) -> Date? { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.isLenient = false formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: String(format: "%d-%02d-%02d", year, month, day)) } let date = makeDate(year: 2021, month: 2, day: 29) // -> nilやりたいこと
例えば年と月と日を別々に入力するUIがあってそこからDate型を生成したいとき、ユーザーが2021年2月29日のような存在しない日付を入力したらnilを返したい。
❌ DateComponentsを使う方法
func makeDate(year: Int, month: Int, day: Int) -> Date? { var components = DateComponents() components.calendar = Calendar(identifier: .gregorian) components.timeZone = TimeZone(secondsFromGMT: 0) components.year = year components.month = month components.day = day return components.date } let date = makeDate(year: 2021, month: 2, day: 29) // -> 2021年3月1日自動で日が進んで別の日付を返してしまう。
❌ DateFormatterを使ってisLenientを指定しない方法
func makeDate(year: Int, month: Int, day: Int) -> Date? { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.dateFormat = "yyyy-MM-dd" return formatter.date(from: String(format: "%d-%02d-%02d", year, month, day)) } let date = makeDate(year: 2021, month: 2, day: 29) // -> 2021年3月1日自動で日が進んで別の日付を返してしまう。
ただし、32日を指定するとnilが返ってくる。
この挙動はDateComponentsとは異なる。let date = makeDate(year: 2021, month: 2, day: 32) // -> nil環境
Swift 5.1
謝辞
@kishikawakatsumi さんに教えていただきました。ありがとうございました。
- 投稿日:2020-06-27T17:12:03+09:00
NavigationLinkとList
Discordの swift-developers-japanサーバー で興味深い話題があったので、気になって調べてみたものを雑にまとめました (話題自体は このへん から)
特に明記していない限り、Xcode 11.5とシミュレーター(iOS 13.5、iPhone SE (1st generation))で確認しています。SwiftUIは
まだ発展途上なので進化が早いので、しばらく経つとここに書いたものは全く意味をなさないゴミになってるかもしれませんが、むしろ早くそうなることを望んでいますNavigationLinkで遷移する前の画面はつながっている
まずは
NavigationLink
を使ったこんな例を見てください。
A, B, Cのボタンが並んだ画面(一覧画面)が表示され、Aを押すと詳細画面へ遷移します。全体のコードはこうなっています。
import SwiftUI struct ContentView: View { var body: some View { NavigationView { FirstView() } .environmentObject(ItemContainer()) } } struct FirstView: View { @EnvironmentObject var container: ItemContainer var body: some View { VStack(spacing: 16) { ForEach(container.items) { item in NavigationLink(destination: SecondView(id: item.id)) { Text("\(item.id)") } } } .navigationBarTitle("Items") } } struct SecondView: View { @EnvironmentObject var container: ItemContainer let id: String var body: some View { VStack(spacing: 16) { Button("rotate") { self.container.rotateItem() } Button("remove first") { self.container.removeItem(id: self.container.items.first?.id ?? "") } Button("remove this") { self.container.removeItem(id: self.id) } } .navigationBarTitle(id) } } struct Item: Identifiable { let id: String } final class ItemContainer: ObservableObject { @Published var items: [Item] = [ Item(id: "A"), Item(id: "B"), Item(id: "C"), ] func rotateItem() { var items = self.items if let item = items.popLast() { items.insert(item, at: 0) } self.items = items } func removeItem(id: String) { items = items.filter { $0.id != id } } }さて、Aの詳細画面にて「rotate」をタップすると、
ItemContainer
のrotateItem()
が呼ばれて、最後のアイテムを先頭へ持ってくるように並び替えます。つまり A, B, C だったものが C, A, B になるわけです。前の画面(
FirstView
)はItemContainer
を参照しているので、このとき画面には見えていませんが、UIはちゃんと作り直されています。
「rotate」をタップしてちょろっと前の画面へpopしかけるジェスチャーをすればボタンの並びが C, A, B に変わっているのがわかります。もう一度「rotate」をタップすると B, C, A になっています。次に「remove first」をタップすると、先頭のアイテムが削除されます。 B, C, A だったものが C, A になります。
「remove this」をタップすると、自分自身のアイテムを削除します。 A が削除されて C だけが残るはずですが、そうするとどうなるでしょう?
答えは、自動的に一覧画面へ戻ります。おそらく、Aの詳細画面のViewや、そこへ遷移していた
NavigationLink
が存在しなくなったからでしょう。
つまり、前の画面のUIが作り変えられることが、自分の画面に影響するということです。
画面遷移単位ではなくて、全部がつながっているんだと考えると、一貫性のある妥当な動作だと言えます。※なお、Aの詳細画面からさらに
NavigationLink
を使って第3画面まで進めるようにして、そこで A を削除すると、なぜか画面は戻りません。そこで手動で前の画面へ戻るとそのままAの詳細画面にいる矛盾した状態が作れます(iOS 13.5)。しかし、 Xcode 12 beta + iOS 14 beta では、第3画面で A を削除した時点で一覧画面まで戻るので、2つ画面を進んだ時に矛盾が発生するこの動作はおそらくiOS 13のバグなんじゃないかと思います。一覧画面を
List
に変更するここで
FirstView
のbody
をVStack
+ForEach
ではなく、List
に変えてみます。var body: some View { List(container.items) { item in NavigationLink(destination: SecondView(id: item.id)) { Text("\(item.id)") } } .navigationBarTitle("Items") }こちらの方がより一般的でしょう。画面はこうなります。
ここから先ほどと同じように、Aの詳細画面へ遷移して、同じように「rotate」をタップしてみます。A, B, C だったものが C, A, B の並び変わるだけのはずですが、やってみると、なぜか一覧画面に戻ってしまいます。
これはちょっと困ってしまう挙動です。Aが削除されたわけではないのに勝手に画面が戻ってしまうのでは使い物になりません。
何が起こっているのだろうか(推測)
「rotate」で前の画面へ戻った際に、コンソールになにやらログが出ています。
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7ffab2044800; baseClass = UITableView; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x600000a54db0>; layer = <CALayer: 0x6000004becc0>; contentOffset: {0, -111}; contentSize: {320, 132}; adjustedContentInset: {111, 0, 0, 0}; dataSource: <_TtGC7SwiftUIP13$7fff2c9a5ad419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x7ffab1514330>>UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window).
どうやら
List
は内部的にはUITableView
を使って実現されているようですが、ビュー階層(ここでのビューはUIView
)に存在しないときにそれを更新しようとして失敗してるように見えます。
おそらく見えている部分のセルに対応するView
が仮想Viewツリー上に構築されるようになっているんじゃないかと思うのですが、そこで失敗していったんList
の下のViewツリーがなくなっちゃってるのかもしれませんね。
UITableViewAlertForLayoutOutsideViewHierarchy
にシンボリックブレークポイントを張るとデバッガでキャッチできるよ、とあるので張ってみましたが、-cellForRowAtIndexPath:
を呼んでいるなということしかわかりませんでした。スタックトレース(の一部)
#0 0x00007fff48e6ddd0 in UITableViewAlertForLayoutOutsideViewHierarchy () #1 0x00007fff48e6cacc in -[UITableView _updateVisibleCellsNow:] () #2 0x00007fff48e81c3b in -[UITableView _cellForRowAtIndexPath:usingPresentationValues:] () #3 0x00007fff48e81b10 in -[UITableView cellForRowAtIndexPath:] () #4 0x00007fff2c606dc6 in ListCoreCoordinator.updateListContents(_:) () #5 0x00007fff2c6062a7 in ListCoreCoordinator.updateUITableView(_:to:) () #6 0x00007fff2c604e94 in ListRepresentable.updateUIView(_:context:) ()まとめ
- 画面遷移しても、Viewとしては前画面とつながっているし、見えていなくても前画面は更新される。それが現在の画面に影響を与えることもある。(これ自体は矛盾が発生しない一貫性のある妥当な動作だと思います)
- が、iOS 13.5時点ではさらに遷移して3画面になるとなんか一貫性が崩れる → iOS 14 betaでは直っているのでこれはバグか?
List
からNavigationLink
で画面遷移したあと、前画面のList
で内容の順番に変更が入るだけで前画面に戻される。困る- 内部的に
UITableView
を使ってるところでうまくいってないのか?(推測)
- 投稿日:2020-06-27T12:04:20+09:00
iOSのExposureNotificationのAPIをサンプルアプリとドキュメントから見てみる
ExposureNotification概要
ExposureNotificationはCOVID-19への潜在的な曝露を人々に通知します。
ExposureNotificationの機能を実現するにはExposure Notification Serverの実装が必須です。おことわり
実機ビルドするには Exposure Notification Entitlement Requestで申請する必要があり、ハードルが高いので、
あくまで、公式ドキュメントとサンプルアプリのシミュレータでの確認になっています。
大いに間違っている可能性があるので公式ドキュメントを正としてください。公式ドキュメント
https://developer.apple.com/documentation/exposurenotification
サンプルアプリ
ENManager
activate
func activate(completionHandler: @escaping ENErrorHandler)ENManagerを使う前に最初に呼び出す。
これを行い、完了ハンドラが戻ってきたところでENManagerの機能が使えるようになる。setExposureNotificationEnabled
func setExposureNotificationEnabled(_ enabled: Bool,completionHandler: @escaping ENErrorHandler)曝露通知を有効/無効にするクラス。
自動的に通知許可をもらうダイアログも表示する。
許可が断られた場合、completionHandlerはENError.notAuthorized
を返す?
enabledをfalseにした場合、 Bluetoothのスキャンとアドバタイズを停止するが、取得していた診断データはそのまま残る。
残念ながらシミュレータで動作させてもDomain=NSOSStatusErrorDomain Code=-71148
のErrorになります。detectExposures
func detectExposures(configuration: ENExposureConfiguration, diagnosisKeyURLs: [URL], completionHandler: @escaping ENDetectExposuresHandler) -> Progress曝露を検出する。
diagnosisKeyURLsは、Exposure Notification ServerからダウンロードしたキーのローカルURLとする。
configurationに関しても、Exposure Notification Serverから取得するデータから生成することをサンプルアプリでは想定している。このAPIはBluetooth経由で得ているデータをワンショット返すようで、サンプルアプリではBackgroundTasksフレームワークを用いて定期的に検出を行っている。
completionHandlerで返ってくる
ENDetectExposuresHandler
にはENExposureDetectionSummary
が含まれていて、後述のgetExposureInfo
で使用する。getExposureInfo
func getExposureInfo(summary: ENExposureDetectionSummary, userExplanation: String, completionHandler: @escaping ENGetExposureInfoHandler) -> Progress前述の
detectExposures
から得たENExposureDetectionSummary
をパラメータにして曝露情報を取得する。
userExplanation
はUIの一部としてユーザーに表示するために使われる。
コールバックとしてENExposureInfo
が受け取れる。
サンプルアプリではこの情報を独自の構造にマッピングしてローカルに保存している。getDiagnosisKeys
func getDiagnosisKeys(completionHandler: @escaping ENGetDiagnosisKeysHandler)
ENTemporaryExposureKey
を取得する。
このメソッドを呼び出すたびに、ユーザーは承認を行う必要がある。
このキーはそのままExposure Notification Serverに送信する必要があり、サンプルアプリでは一旦JSONにデコードして送信する想定となっている。getTestDiagnosisKeys
func getTestDiagnosisKeys(completionHandler: @escaping ENGetDiagnosisKeysHandler)
getDiagnosisKeys
のテスト用API。
シミュレータで動作させてもDomain=NSOSStatusErrorDomain Code=-71148
のErrorになります。invalidate
func invalidate()ENManagerを無効にする。
ENStatus
曝露通知システムの有効状態を示すenum。
特筆したいのがbluetoothOff
という種別であり、ENStatusにしかBluetooth使用を示すものがないので、ExposureNotificationのフレームワーク内で完全にBluetoothの処理は閉じていてBluetoothに対するコントロールは出来なさそう。tvOS14(おまけ)
tvOS14にもExposureNotification.frameworkが有効なんですが、
- ENAuthorizationStatus
- ENStatus
- ENError
と、限定的なAPIしか使えず、メインのENManagerが使えない模様。
- 投稿日:2020-06-27T04:26:01+09:00
@IBDesignable を指定した View の中で Asset Catalog の内容を読み取るために必要なこと
概要
@IBDesignable
を指定したカスタム View クラスの中でUIColor
クラスのイニシャライザinit(named:)
を呼び出す処理を書いておき、Interface Builder 内で、ある View Controller クラスの View にそのカスタム View クラスを 追加したところ、以下のようなビルドエラーが発生しました。IB Designables: Failed to render and update auto layout status for [View Controller のクラス名] ([Interface Builder 内での View Controller の Object ID]): The agent crashedこの問題の原因について調査したところ、
@IBDesignable
を指定したカスタム View クラスのprepareForInterfaceBuilder()
から呼び出される処理の中で Asset Catalog の内容を正しく読み出されないためであることがわかりました。
prepareForInterfaceBuilder()
から呼び出される処理の中でも Asset Catalog の内容を正しく読み取れるように設定することにより、 Asset Catalog 内で定義した色情報を使用できるようになりました。エラーログの格納場所
本記事が対象とするエラーの詳細ログは
~/Library/Logs/DiagnosticReports
ディレクトリ内のIBDesignablesAgent-iOS_YYYY-MM-DD-HHMMDD_[マシン名].crash
に記録されています。
@IBDesignable
を指定したカスタム View の定義方法
@IBDesignable
を指定したカスタム View クラスを使用することにより、 Interface Builder 上でカスタム View のプレビューをするなどの利点があります。
@IBDesinable
を使用する利点や使用方法については、下記リンク先の記事が参考になります。カスタムコンポーネントを使用したUI実装について - ZOZO Technologies TECH BLOG
prepareForInterfaceBuilder()
から Asset Catalog の内容を読み取る
prepareForInterfaceBuilder()
から呼び出される処理の中でUIColor
クラスのイニシャライザinit(named:)
を呼び出すとビルドエラーとなるのは、init(named:)
では Asset Catalog の内容を正しく読み取ることができないためです。Asset Catalog の内容を正しく読み取るためには
init(named:)
の代わりにinit(named:in:compatibleWith:)
を使って、 対象の Bundle を明示的に指定する必要があります。具体的なコードの例を下に示します。ビルドエラーとなる例@IBDesignable final class RoundedRectButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setupAttributes() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupAttributes() } override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() setupAttributes() } private func setupAttributes() { clipsToBounds = true layer.cornerRadius = 8.0 layer.borderWidth = 2.0 layer.borderColor = UIColor.black.cgColor // この実装だとビルドエラー backgroundColor = UIColor(named: "MyColor")! } }ビルド成功となる例@IBDesignable final class RoundedRectButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setupAttributes() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupAttributes() } override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() setupAttributes() } private func setupAttributes() { clipsToBounds = true layer.cornerRadius = 8.0 layer.borderWidth = 2.0 layer.borderColor = UIColor.black.cgColor // この実装だとビルド成功 backgroundColor = UIColor(named: "MyColor", in: Bundle(for: type(of: self)), compatibleWith: nil)! } }
prepareForInterfaceBuilder()
から呼び出される処理の中では、 Asset Catalog 内の画像を使用する場合にも同様に、UIImage
クラスのイニシャライザにはinit(named:)
ではなくinit(named:in:with:)
を使う必要があります。その他の対応方法
今回着目したビルドエラーは上記対応方法にて解決できるのですが、もしその他の理由により
prepareForInterfaceBuilder()
内で実行する処理とそれ以外を区別したい場合には、TARGET_INTERFACE_BUILDER
というプリプロセッサマクロが役に立ちます。TARGET_INTERFACE_BUILDERの使用例#if TARGET_INTERFACE_BUILDER // prepareForInterfaceBuilder()内でのみ有効となる #else // prepareForInterfaceBuilder()内以外でのみ有効となる #endif参考情報
- 投稿日:2020-06-27T04:26:01+09:00
IBDesignable を指定した View の中で Asset Catalog の内容を読み取るために必要なこと
概要
@IBDesignable
を指定したカスタム View クラスの中でUIColor
クラスのイニシャライザinit?(named:)
を呼び出す処理を書いておき、Interface Builder 内で、ある View Controller クラスの View にそのカスタム View クラスを 追加したところ、以下のようなビルドエラーが発生しました。IB Designables: Failed to render and update auto layout status for [View Controller のクラス名] ([Interface Builder 内での View Controller の Object ID]): The agent crashedこの問題の原因について調査したところ、
@IBDesignable
を指定したカスタム View クラスのprepareForInterfaceBuilder()
から呼び出される処理の中で Asset Catalog の内容を正しく読み出されないためであることがわかりました。
prepareForInterfaceBuilder()
から呼び出される処理の中でも Asset Catalog の内容を正しく読み取れるように設定することにより、 Asset Catalog 内で定義した色情報を正常に使用できるようになりました。エラーログの格納場所
本記事が対象とするエラーの詳細ログは
~/Library/Logs/DiagnosticReports
ディレクトリ内のIBDesignablesAgent-iOS_YYYY-MM-DD-HHMMDD_[マシン名].crash
に記録されています。
@IBDesignable
を指定したカスタム View の定義方法
@IBDesignable
を指定したカスタム View クラスを使用することにより、 Interface Builder 上でカスタム View のプレビューをするなどの利点があります。
@IBDesinable
を使用する具体的な利点や使用方法については、下記リンク先の記事が参考になります。カスタムコンポーネントを使用したUI実装について - ZOZO Technologies TECH BLOG
prepareForInterfaceBuilder()
から Asset Catalog の内容を読み取る
prepareForInterfaceBuilder()
から呼び出される処理の中でUIColor
クラスのイニシャライザinit?(named:)
を呼び出すとビルドエラーとなるのは、init?(named:)
では Asset Catalog の内容を正しく読み取ることができないためです。Asset Catalog の内容を正しく読み取るためには
init(named:)
の代わりにinit(named:in:compatibleWith:)
を使って、 対象の Bundle を明示的に指定する必要があります。具体的なコードの例を下に示します。ビルドエラーとなる例@IBDesignable final class RoundedRectButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setupAttributes() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupAttributes() } override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() setupAttributes() } private func setupAttributes() { clipsToBounds = true layer.cornerRadius = 8.0 layer.borderWidth = 2.0 layer.borderColor = UIColor.black.cgColor // この実装だとビルドエラー backgroundColor = UIColor(named: "MyColor")! } }ビルド成功となる例@IBDesignable final class RoundedRectButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) setupAttributes() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupAttributes() } override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() setupAttributes() } private func setupAttributes() { clipsToBounds = true layer.cornerRadius = 8.0 layer.borderWidth = 2.0 layer.borderColor = UIColor.black.cgColor // この実装だとビルド成功 backgroundColor = UIColor.init(named: "MyColor", in: Bundle(for: type(of: self)), compatibleWith: nil) } }
prepareForInterfaceBuilder()
から呼び出される処理の中では、 Asset Catalog 内の画像を使用する場合にも同様に、UIImage
クラスのイニシャライザにはinit?(named:)
ではなくinit?(named:in:with:)
を使う必要があります。その他の対応方法
今回着目したビルドエラーは上記対応方法にて解決できるのですが、もしその他の理由により
prepareForInterfaceBuilder()
内で実行する処理とそれ以外を区別したい場合には、TARGET_INTERFACE_BUILDER
というプリプロセッサマクロが役に立ちます。TARGET_INTERFACE_BUILDERの使用例#if TARGET_INTERFACE_BUILDER // prepareForInterfaceBuilder()内でのみ有効となる #else // prepareForInterfaceBuilder()内以外でのみ有効となる #endif参考情報
- 投稿日:2020-06-27T00:07:29+09:00
iOS 開発初心者による Xcode におけるビルドの使い分け
iOS 開発者の皆さん、 ビルド 、してますか!? (当たり前)
ビルド待ちって筋トレしたくなるくらい暇な時間ですよね。ビルド待ちの間についつい Twitter や Slack 見てて、気づいたら夜だったなんてこともあったりなかったり。
僕は iOS 開発を初めて 2 ヶ月の初心者ですが、そんな僕から「ビルドの使い分け」をシェアしたいです。
最初は ⌘ + R しか知らなかった僕ですが、今ではその他に 2 つのビルド方法 を使い分けています。本題
今日紹介するビルド方法は以下の 3 つです。
- ⌘ + R (
Run
)- ⌘ + ⇧ + R (
Build for Running
)- ⌘ + ⌃ + R (
Run without Building
)この 3 つがあれば、以下のような使い分けができます。
Run
: ビルドして、実機 or シミュレータで動作を確認したいときBuild for Running
: ビルドだけして、動作確認はしないときRun without Building
: ビルドはせずに、実機 or シミュレータによる動作の確認だけしたいときこれで UseCase に応じてキーボードショートカットを覚えて帰っていただいても大いに結構なのですが、せっかくなのでそれぞれがどんなことをしているかを軽く説明しときます。
⌘ + ⇧ + R (
Build for Running
)順番が最初と違いますが、まずはこれから。
ここでは ビルドのみ 行います。ビルドとは、コンパイルしたりレジスタを初期化したりゴニョゴニョやる 下準備 みたいなものです。
これに関してもっと理解したい方は この記事 が参考になります。
ざっくり、コードを変更したら必要になるのがビルドです。そして、 "Build Succeeded" となって成功する or "Build Faild" となると失敗して終了です。
どんな時に使うかと言うと、
- コードを変更してとりあえずコンパイルだけしたいとき (エラーが出ないかだけ確認したい)
- 見た目に関する変更が無くて実機 or シミュレータによる動作確認が不要なとき
に使っています。
次、 ⌘ + ⌃ + R (Run without Building
) を説明します。⌘ + ⌃ + R (
Run without Building
)これは知らなかった人は是非知ってください。
なんと ビルド待ち時間が 0 になります。やばいですよね。このコマンドでは、
Run without Building
から分かるように、 ビルドをしません。
なのでビルド待ちが 0 になるわけです。じゃあ何をするかというと、 前回成功したビルド結果を元に実行 します。
ここでの実行とは、実機やシミュレータで動作確認することです。
前回成功したビルドなので、どれだけコードを変更しても反映されません。
ここだけは注意して使い分けて下さい。このコマンドは必ず成功して実行されますが、それはあなたが追加・変更したコードがうまく行っていることとは無関係ということです。
どんな時に使うかと言うと、
- 実行したがクラッシュしてもう一度実行したいとき
- とりあえず実機による動作確認がしたいとき
などです。「前回のビルド結果でいいから実行したいとき」ですね。
⌘ + R (
Run
)最後、一番知られてるやつです。
Xcode 画面左上の ▶ 押して実行するやつです。このコマンドでは、 ビルド + 実行 をしています。
今回紹介した中では最も時間がかかる処理です。
ビルド + 実行なので、先に紹介した 2 つのコマンドを直列実行しているということですね。このコマンドは、
- コードを変更し、その変更を動作確認したいとき
などに使います。
おまけ
ここでは、
Run
に関するコマンドでしたが、R
をU
に変えるとユニットテストに全く同じ感覚で使えます。
さらにR
をI
に変えると Profiling とかいう謎のことにも使えるそうです。僕は使ったことないです。まとめ
まとめると以下のようになります。
ショートカット コマンド名 動作 使用例 ⌘ + ⇧ + R Build for Running
ビルドのみ とりあえずコンパイルだけしたいとき ⌘ + ⌃ + R Run without Building
実行のみ とりあえず実機による動作確認がしたいとき ⌘ + R Run
ビルド + 実行 コードを変更し、その変更を動作確認したいとき 使い分けてビルド待ちを最小にしましょう!!
くれぐれもビルド待ち中に筋トレしすぎてムキムキにならないように!!