- 投稿日:2020-11-14T21:58:42+09:00
Moya+RxSwiftのAPIクライアントの一例(Swift5)
前提
SwiftではデフォルトではURLSessionクラスで通信処理を行うが、Moyaはそれのラッパとなるライブラリである。
便利に書ける事から、開発の現場ではスタンダードなやり方と思われる。特にRxSwiftと組み合わせるとさらに便利。
一応、URLSessionクラスもある程度知っておいた方がいい事から、初心者の方はまずURLSessionで慣れた後、Moyaに移行し便利に使える事を実感した方がいいだろう。
公式
Swift4 + Moya + RxSwift + Codableで作るAPIクライアント全体
APIClient.swift
APIClient.swiftfinal class APIClient { private init() {} static let shared = APIClient() // MARK: - Private private let provider = MoyaProvider<MultiTarget>() private let stubProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub) // MARK: - Public func request<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> { Single<G.Reponse>.create { observer in self.makeRequest(request) .subscribe(onSuccess: { response in observer(.success(response)) }, onError: { error in //プロジェクト全体で共通して行いたいエラーハンドリング等 observer(.error(error)) }) } } func makeRequest<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> { provider.rx .request(MultiTarget(request)) .flatMap({ response -> Single<Response> in // レスポンスヘッダーのチェック return Single.just(try response.lookingAllHeaderFields()) }) .flatMap { response -> Single<Response> in // エラーコードのチェック return Single.just(try response.successfulStatusCodesPolicy()) } .map(G.Reponse.self, failsOnEmptyData: false) } func requestStub<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> { Single<G.Reponse>.create { observer in self.makeRequestStub(request) .subscribe(onSuccess: { response in observer(.success(response)) }, onError: { error in if let error = error as? MoyaError { //プロジェクト全体で共通して行いたいエラーハンドリング等 observer(.error(error)) }) } } func makeRequestStub<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> { stubProvider.rx .request(MultiTarget(request)) .flatMap({ response -> Single<Response> in // レスポンスヘッダーのチェック return Single.just(try response.lookingAllHeaderFields()) }) .flatMap { response -> Single<Response> in // エラーコードのチェック return Single.just(try response.successfulStatusCodesPolicy()) } .map(G.Reponse.self, failsOnEmptyData: false) } }ApiTargetType.swift
ApiTargetType.swiftimport Foundation import Moya protocol ApiTargetType: TargetType { associatedtype Response: Codable } extension ApiTargetType { /// The target's base `URL`. var baseURL: URL { URL(string: "\(URLs.baseURL)")! } }解説
APIクライアントはシングルトンオブジェクト
- API通信を行うため、MoyaProviderクラスのオブジェクトが通信途中で解放されてしまうなどという事態は避けなくてはならない。APIクライアントを利用する側で、MoyaProviderクラスのオブジェクトを(メンバ変数として設定するなどして)十分長期間保持するということもできる。ここではそのような手間を避けるため、シングルトンオブジェクトとして
shared
を設定し、MoyaProviderがプログラムの実行の全過程を通じて存続するようにしている。APIClient.swiftfinal class APIClient { private init() {} static let shared = APIClient()MultiTargetの使用
- 異なる種類のTargetTypeをMoyaProviderに割り当てられるようにするため、
MultiTarget
を利用している。これにより、APIクライアント利用側ではジェネリクスを通じてMoyaProviderにTargetTypeを渡すという手間が省け、MoyaProviderの存在を利用側に対して隠蔽することができる。APIClient.swiftprivate let provider = MoyaProvider<MultiTarget>()Pluginの利用
- 必要であれば、MoyaProviderを初期化する際、Pluginというものを渡す事ができる。これはリクエストを送る前、またはレスポンスを受け取った後などに何か副作用を記載したい場合に利用できる。用途としては、公式のコメントに記載があるように、リクエストを送る・レスポンスを受け取るときにログを出すであるとか、インジケータ(ローディング中である事を示すグルグル回るもの)の出し入れを行うとか、リクエストを送る前にURLRequestの設定を変更したい時などに利用できる。
APIClient.swiftprivate let provider = MoyaProvider<MultiTarget>(plugins: [FooPlugin()])Plugin.swift/// A Moya Plugin receives callbacks to perform side effects wherever a request is sent or received. /// /// for example, a plugin may be used to /// - log network requests /// - hide and show a network activity indicator /// - inject additional information into a request public protocol PluginType { /// Called to modify a request before sending. func prepare(_ request: URLRequest, target: TargetType) -> URLRequest /// Called immediately before a request is sent over the network (or stubbed). func willSend(_ request: RequestType, target: TargetType) /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler. func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) /// Called to modify a result before completion. func process(_ result: Result<Moya.Response, MoyaError>, target: TargetType) -> Result<Moya.Response, MoyaError> }stubProviderの利用
- stubProviderを使うと、各TargetTypeに記載してあるサンプルデータをレスポンスとして返す事ができる。スタブの振る舞いとしては.immediatelyStubだと即座にスタブデータを返すが、.delayだと指定した秒数後にデータを返す事ができ、より本番に近い状況でテストしたい時には有効。
APIClient.swiftprivate let stubbingProvider = MoyaProvider<MultiTarget>(stubClosure: MoyaProvider.immediatelyStub) //.delayed(seconds: 1.0)等でも可レスポンスのチェック
- 返ってきたレスポンスにはステータスコード・ヘッダーフィールド・データが含まれているが、これらに対して行いたい処理がプロジェクト全体で共通している場合は、APIクライアントに記載してしまうと良い。エラーハンドリングや、ログを出す等。
APIClient.swift// MARK: - Public func request<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> { Single<G.Reponse>.create { observer in self.makeRequest(request) .subscribe(onSuccess: { response in observer(.success(response)) }, onError: { error in //プロジェクト全体で共通して行いたいエラーハンドリング等 observer(.error(error)) }) } } func makeRequest<G: ApiTargetType>(_ request: G) -> Single<G.Reponse> { provider.rx .request(MultiTarget(request)) .flatMap({ response -> Single<Response> in // レスポンスヘッダーのチェック return Single.just(try response.lookingAllHeaderFields()) }) .flatMap { response -> Single<Response> in // エラーコードのチェック return Single.just(try response.successfulStatusCodesPolicy()) } .map(G.Reponse.self, failsOnEmptyData: false) }ターゲットタイプ
プロジェクト全体で共通するもの(典型的には、ベースのURLなど)があれば、くくり出してTargetTypeのプロトコルを作ると良い。
protocol ApiTargetType: TargetType { associatedtype Response: Codable } extension ApiTargetType { /// The target's base `URL`. var baseURL: URL { URL(string: "\(URLs.baseURL)")! } }利用例
func fetchData() -> Single<HogeEntity> { return APIClient.shared.request(HogeTargetType()) }HogeTargetType.swiftimport Foundation import Moya struct HogeTargetType: ApiTargetType { typealias Response = HogeEntity var path: String { return "/hogehoge/" } var method: Moya.Method { return .get } var task: Task { let param: [String: Any] = [ "id": id ] return .requestParameters(parameters: param, encoding: URLEncoding.default) } var headers: [String: String]? { [ "Content-Type": "application/json", "x-auth-token": API.authToken ] } var sampleData: Data { let path = Bundle.main.path(forResource: "Hoge", ofType: "json")! guard let file = FileHandle(forReadingAtPath: path) else { return Data() } return file.readDataToEndOfFile() } // MARK: - Arguments /// ID var id: String { return DataManager.shared.id ?? "" } init() {} }HogeEntity.swiftstruct HogeEntity: Codable { let fuga: StringHoge.json{ "fuga": "test" }参考資料
Swift4 + Moya + RxSwift + Codableで作るAPIクライアント
Moya/Examples/Multi-Target/ViewController.swift
- 投稿日:2020-11-14T15:41:30+09:00
【備忘録】iOS 14 細かいTips 写真アクセス権限・バックボタン
iOS 14のTipsを書いていこうと思います。
写真のアクセス権限の変更
iOS 13までは写真のアクセス権限の選択肢が
許可しない
・OK
の2択でしたが、
iOS 14からは写真のアクセス権限の選択肢が写真を選択
・全ての写真へのアクセスを許可
・許可しない
の3択になりました。
iOS 14では写真個別にアクセス権限を与えれるようになりました。iOS 14ダイアログ
写真のアクセス権限の呼び出しは以下のコードでOSに合わせてダイアログを表示します。
PHPhotoLibrary.requestAuthorization { _ in // 省略 }iOS 14でも従来のダイアログを出すには?
アプリで写真を保存するだけの用途なら従来のダイアログで問題ないと思います。
まず、Info.plistに
Privacy - Photo Library Additions Usage Description (NSPhotoLibraryAddUsageDescription)
を設定して文言を追加します。なら以下のコードで大丈夫です。
if #available(iOS 14, *) { PHPhotoLibrary.requestAuthorization(for: .addOnly) { _ in // 省略 } } else { PHPhotoLibrary.requestAuthorization { _ in // 省略 } }
requestAuthorization
というメソッドで写真のみ追加
もしくは読み出し/書き込み
のどちらかで指定して呼び出しできるので.addOnly
を指定すると従来のダイアログが出すことができます。バックボタン
iOS 14からバックボタンを長押しすることで前の画面らをリストしたポップアップが表示され、一気に一番最初の画面に戻るなどできるようになりました。
他にもバックボタンの見た目も制御できるようになり、UINavigationItemクラスに以下のプロパティが追加されました。
var backButtonDisplayMode: UINavigationItem.BackButtonDisplayMode { get set }見た目は3種類設定できます
.default
前の画面タイトルがバックボタンに反映します
※タイトルが入ってなければBack
になります
.generic
.minimal
コード例
if #available(iOS 14.0, *) { self.navigationItem.backButtonDisplayMode = .minimal }注意
バックボタンの見た目は制御できますが、長押しした際のリストは画面タイトル(
self.title
)もしくはバックボタン(backButtonTitle
)の文言が表示されます。もし何も文言を入れてないと空欄にまたは同じ文言を入れると、どこの画面に戻るかぱっと見でわからなくなってしまいます。
iOS 14のバックボタンの対応として長押しした際にどこの画面に戻るか分かる文言を設定してあげるといいです。
- 投稿日:2020-11-14T13:17:18+09:00
RxSwift ボタン連打防止
throttle
Operatorを用いると良い。
throttle
及び似たような機能のあるdebounce
についてはこちらを参照。
RxSwiftのDebounceとThrottle
throttle
はボタン連打防止、debounce
はいわゆるインクリメンタルサーチなどAPI呼びすぎ防止などで使える。実際には以下のようなextensionを作ると便利である。
Reactive+Extenstions.swiftimport RxCocoa import RxSwift public extension Reactive where Base: UIButton { var throttledTap: ControlEvent<Void> { return ControlEvent<Void>(events: tap .throttle(.milliseconds(ContinuousTap.disableTapDuration), latest: false, scheduler: MainScheduler.instance)) } } public extension Reactive where Base: UIBarButtonItem { var throttledTap: ControlEvent<()> { return ControlEvent<()>(events: tap .throttle(.milliseconds(ContinuousTap.disableTapDuration), latest: false, scheduler: MainScheduler.instance)) } } public extension Reactive where Base: UITableView { var throttledItemSelected: ControlEvent<IndexPath> { return ControlEvent<IndexPath>(events: itemSelected .throttle(.milliseconds(ContinuousTap.disableTapDuration), latest: false, scheduler: MainScheduler.instance) ) } } public extension Reactive where Base: UICollectionView { var throttledItemSelected: ControlEvent<IndexPath> { return ControlEvent<IndexPath>(events: itemSelected .throttle(.milliseconds(ContinuousTap.disableTapDuration), latest: false, scheduler: MainScheduler.instance) ) } } enum ContinuousTap { /// Disable Tap Duration in Milliseconds static let disableTapDuration: Int = 500 }
- 投稿日:2020-11-14T12:28:12+09:00
scrollviewで可変サイズUIViewがタップできなくてハマった
ContainerVCを用いて、
ボタンを押すと大きさが可変となるUIViewを作成していました。
このUIViewにはtapgestureRecognizer あるいは touchesbeganで
タップを検出できるようにしていました。
(参考 https://qiita.com/Kyome/items/d86cefa9dbd7bd2d7cf0)ボタンが押されたときに
a.swift@IBOutlet weak var containerVCWidth: NSLayoutConstraint! self.myScrollView.contentSize.width += 100 self.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.width + 100, height: self.view.frame.height) }としていましたが、viewは大きくなるのにタップできない現象が発生しました。
解決策
containerViewのwidthに関する制約を
outlet接続して、それもインクリメントする必要があったようです。a.swift@IBOutlet weak var containerVCWidth: NSLayoutConstraint! @IBAction func tappedButton(_ sender: Any) { self.myScrollView.contentSize.width += 100 containerVCWidth.constant += 100 }
- 投稿日:2020-11-14T11:55:39+09:00
SwiftUI 他のフレームワークを組み合わせると使えるView
SwiftUIの他に各種フレームワークをimportすると使えるViewやModifierです。
MapKit
Map
が使えます。サンプルコード
import SwiftUI import MapKit struct SwiftUIView: View { @State var region: MKCoordinateRegion var body: some View { Map(coordinateRegion: $region) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { let initialCoordinate = CLLocationCoordinate2DMake(40,40) let span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5) let region = MKCoordinateRegion(center: initialCoordinate, span: span) return SwiftUIView(region: region) } }プレビュー
SpriteKit
SpriteView
が使えます。
GameScene
ファイルはXcodeプロジェクトをMultiplatformで作成した際のものを使っています。サンプルコード
import SwiftUI import SpriteKit struct SwiftUIView: View { let scene: SKScene var body: some View { SpriteView(scene: scene) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { guard let scene = SKScene(fileNamed: "GameScene") as? GameScene else { abort() } scene.scaleMode = .aspectFit return SwiftUIView(scene: scene) } }プレビュー
AuthenticationServices
SignInWithAppleButton
が使えます。サンプルコード
import SwiftUI import AuthenticationServices struct SwiftUIView: View { var body: some View { SignInWithAppleButton(.continue) { _ in } onCompletion: { _ in } .frame(width: 300.0, height: 44.0) .signInWithAppleButtonStyle(.black) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView() } }プレビュー
StoreKit
appStoreOverlay
が使えます。サンプルコード
import SwiftUI import StoreKit struct SwiftUIView: View { @State var showOverlay:Bool = false var body: some View { Button("App Store Overlay") { self.showOverlay.toggle() } .appStoreOverlay(isPresented: $showOverlay) { SKOverlay.AppConfiguration(appIdentifier: "687721425", position: .bottom) } } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { return SwiftUIView() } }プレビュー
AVKit
VideoPlayer
が使えます。
ドキュメントにはA view that displays the video content from a player object along with system-supplied playback controls.
とあるんですが、tvOSだとコントロールが出ない感じです。サンプルコード
import SwiftUI import AVKit struct SwiftUIView: View { let player: AVPlayer? var body: some View { VideoPlayer(player: player) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { return SwiftUIView(player: AVPlayer(url: URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!)) } }プレビュー
SceneKit
SceneView
が使えます。scnファイルは各自用意してください。サンプルコード
import SwiftUI import SceneKit struct SwiftUIView: View { let scene: SCNScene? var body: some View { SceneView(scene: scene) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { let scene = SCNScene(named: "Scene.scn") return SwiftUIView(scene: scene) } }プレビュー
HomeKit
CameraView
が使えます。
HMCameraSource
が必要になりますが、おそらくSwiftUIプレビューでは用意できないので、各自実機でご確認ください。サンプルコード
import SwiftUI import HomeKit struct SwiftUIView: View { let cameraSource:HMCameraSource var body: some View { CameraView(source: cameraSource) } }画像
WatchKit
NowPlayingView
が使えます。サンプルコード
import SwiftUI import WatchKit struct SwiftUIView: View { var body: some View { NowPlayingView() } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView() } }プレビュー
まとめ
つかおう!HomeKit!
- 投稿日:2020-11-14T11:55:39+09:00
SwiftUIを他のフレームワークと組み合わせる
SwiftUIの他に各種フレームワークをimportすると使えるViewやModifierです。
MapKit
Map
が使えます。サンプルコード
import SwiftUI import MapKit struct SwiftUIView: View { @State var region: MKCoordinateRegion var body: some View { Map(coordinateRegion: $region) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { let initialCoordinate = CLLocationCoordinate2DMake(40,40) let span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5) let region = MKCoordinateRegion(center: initialCoordinate, span: span) return SwiftUIView(region: region) } }プレビュー
SpriteKit
SpriteView
が使えます。
GameScene
ファイルはXcodeプロジェクトをMultiplatformで作成した際のものを使っています。サンプルコード
import SwiftUI import SpriteKit struct SwiftUIView: View { let scene: SKScene var body: some View { SpriteView(scene: scene) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { guard let scene = SKScene(fileNamed: "GameScene") as? GameScene else { abort() } scene.scaleMode = .aspectFit return SwiftUIView(scene: scene) } }プレビュー
AuthenticationServices
SignInWithAppleButton
が使えます。サンプルコード
import SwiftUI import AuthenticationServices struct SwiftUIView: View { var body: some View { SignInWithAppleButton(.continue) { _ in } onCompletion: { _ in } .frame(width: 300.0, height: 44.0) .signInWithAppleButtonStyle(.black) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView() } }プレビュー
StoreKit
appStoreOverlay
が使えます。サンプルコード
import SwiftUI import StoreKit struct SwiftUIView: View { @State var showOverlay:Bool = false var body: some View { Button("App Store Overlay") { self.showOverlay.toggle() } .appStoreOverlay(isPresented: $showOverlay) { SKOverlay.AppConfiguration(appIdentifier: "687721425", position: .bottom) } } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { return SwiftUIView() } }プレビュー
AVKit
VideoPlayer
が使えます。
ドキュメントにはA view that displays the video content from a player object along with system-supplied playback controls.
とあるんですが、tvOSだとコントロールが出ない感じです。サンプルコード
import SwiftUI import AVKit struct SwiftUIView: View { let player: AVPlayer? var body: some View { VideoPlayer(player: player) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { return SwiftUIView(player: AVPlayer(url: URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!)) } }プレビュー
SceneKit
SceneView
が使えます。scnファイルは各自用意してください。サンプルコード
import SwiftUI import SceneKit struct SwiftUIView: View { let scene: SCNScene? var body: some View { SceneView(scene: scene) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { let scene = SCNScene(named: "Scene.scn") return SwiftUIView(scene: scene) } }プレビュー
HomeKit
CameraView
が使えます。
HMCameraSource
が必要になりますが、おそらくSwiftUIプレビューでは用意できないので、各自実機でご確認ください。サンプルコード
import SwiftUI import HomeKit struct SwiftUIView: View { let cameraSource:HMCameraSource var body: some View { CameraView(source: cameraSource) } }画像
WatchKit
NowPlayingView
が使えます。サンプルコード
import SwiftUI import WatchKit struct SwiftUIView: View { var body: some View { NowPlayingView() } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView() } }プレビュー
まとめ
つかおう!HomeKit!
- 投稿日:2020-11-14T11:55:39+09:00
SwiftUIを他のフレームワークと組み合わせると使えるView
SwiftUIの他に各種フレームワークをimportすると使えるViewやModifierです。
MapKit
Map
が使えます。サンプルコード
import SwiftUI import MapKit struct SwiftUIView: View { @State var region: MKCoordinateRegion var body: some View { Map(coordinateRegion: $region) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { let initialCoordinate = CLLocationCoordinate2DMake(40,40) let span = MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5) let region = MKCoordinateRegion(center: initialCoordinate, span: span) return SwiftUIView(region: region) } }プレビュー
SpriteKit
SpriteView
が使えます。
GameScene
ファイルはXcodeプロジェクトをMultiplatformで作成した際のものを使っています。サンプルコード
import SwiftUI import SpriteKit struct SwiftUIView: View { let scene: SKScene var body: some View { SpriteView(scene: scene) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { guard let scene = SKScene(fileNamed: "GameScene") as? GameScene else { abort() } scene.scaleMode = .aspectFit return SwiftUIView(scene: scene) } }プレビュー
AuthenticationServices
SignInWithAppleButton
が使えます。サンプルコード
import SwiftUI import AuthenticationServices struct SwiftUIView: View { var body: some View { SignInWithAppleButton(.continue) { _ in } onCompletion: { _ in } .frame(width: 300.0, height: 44.0) .signInWithAppleButtonStyle(.black) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView() } }プレビュー
StoreKit
appStoreOverlay
が使えます。サンプルコード
import SwiftUI import StoreKit struct SwiftUIView: View { @State var showOverlay:Bool = false var body: some View { Button("App Store Overlay") { self.showOverlay.toggle() } .appStoreOverlay(isPresented: $showOverlay) { SKOverlay.AppConfiguration(appIdentifier: "687721425", position: .bottom) } } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { return SwiftUIView() } }プレビュー
AVKit
VideoPlayer
が使えます。
ドキュメントにはA view that displays the video content from a player object along with system-supplied playback controls.
とあるんですが、tvOSだとコントロールが出ない感じです。サンプルコード
import SwiftUI import AVKit struct SwiftUIView: View { let player: AVPlayer? var body: some View { VideoPlayer(player: player) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { return SwiftUIView(player: AVPlayer(url: URL(string: "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8")!)) } }プレビュー
SceneKit
SceneView
が使えます。scnファイルは各自用意してください。サンプルコード
import SwiftUI import SceneKit struct SwiftUIView: View { let scene: SCNScene? var body: some View { SceneView(scene: scene) } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { let scene = SCNScene(named: "Scene.scn") return SwiftUIView(scene: scene) } }プレビュー
HomeKit
CameraView
が使えます。
HMCameraSource
が必要になりますが、おそらくSwiftUIプレビューでは用意できないので、各自実機でご確認ください。サンプルコード
import SwiftUI import HomeKit struct SwiftUIView: View { let cameraSource:HMCameraSource var body: some View { CameraView(source: cameraSource) } }画像
WatchKit
NowPlayingView
が使えます。サンプルコード
import SwiftUI import WatchKit struct SwiftUIView: View { var body: some View { NowPlayingView() } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView() } }プレビュー
まとめ
つかおう!HomeKit!