diff --git a/HomeKitCatalog/HMCatalog.entitlements b/HomeKitCatalog/HMCatalog.entitlements
new file mode 100644
index 00000000..fba8c1f3
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.developer.homekit
+
+ com.apple.external-accessory.wireless-configuration
+
+
+
diff --git a/HomeKitCatalog/HMCatalog.xcodeproj/project.pbxproj b/HomeKitCatalog/HMCatalog.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..06feb854
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog.xcodeproj/project.pbxproj
@@ -0,0 +1,714 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 6A3B541F1AF92D37007CA237 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */; };
+ 6A4D140F1ADDF2DC00364DE0 /* HMCatalogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */; };
+ 6A589F641B0FAF3200CDD54B /* SegmentedTimeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */; };
+ 6A58EE961AF147BE00ECAD21 /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */; };
+ 6A5D85281B0CF3C5008DF524 /* TriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */; };
+ 6A5D852A1B0CFE6C008DF524 /* TimerTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */; };
+ 6A5D852C1B0D05DD008DF524 /* LocationTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */; };
+ 6A5D852F1B0D2155008DF524 /* EventTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */; };
+ 6A5D85321B0D3032008DF524 /* TimeConditionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */; };
+ 6A5D85341B0D40B0008DF524 /* TimePickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */; };
+ 6A5D85391B0D5100008DF524 /* EventTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */; };
+ 6A5D853B1B0D63EB008DF524 /* NSPredicate+Condition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */; };
+ 6A6DA8881B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */; };
+ 6A8347531B0574040055198E /* CharacteristicSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */; };
+ 6A8347551B06668B0055198E /* CharacteristicTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */; };
+ 6A96D1211B051326004DD072 /* UIColor+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A96D1201B051326004DD072 /* UIColor+Custom.swift */; };
+ 6AA42B471AEEF28500A92A79 /* FavoritesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */; };
+ 6AA850DA1B1E645300A77A3E /* UITableViewController+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */; };
+ 6AAE046E1B0A93660084A575 /* LocationTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */; };
+ 6ABBF3021ADF1A1C00C1CF69 /* Array+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABBF3011ADF1A1C00C1CF69 /* Array+Sorting.swift */; };
+ 6ACBCF5D1B02DAA000851BD3 /* CharacteristicTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */; };
+ 6ACBCF5E1B02DAA000851BD3 /* MapOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */; };
+ 6ACBCF5F1B02DAA000851BD3 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF591B02DAA000851BD3 /* MapViewController.swift */; };
+ 6ACBCF601B02DAA000851BD3 /* TimerTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */; };
+ 6ACBCF621B02DB1700851BD3 /* TriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */; };
+ 6ACBCFA01B040AA600851BD3 /* HMEventTrigger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */; };
+ 6ACCD8C21B1266A70002FA61 /* ConditionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */; };
+ 6AD5641C1AFA792A00321F78 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD5641B1AFA792A00321F78 /* TabBarController.swift */; };
+ 6ADF251E1AE1940B00CD05D0 /* HomeKitObjectCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */; };
+ 6ADF25221AE5AA8200CD05D0 /* TextCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */; };
+ 6ADF25241AE5AAA100CD05D0 /* TextCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */; };
+ 6AE2A5641B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */; };
+ DC0BBB991A1FB60E002AB35C /* ServiceGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */; };
+ DC0BBB9D1A1FBC46002AB35C /* AddServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */; };
+ DC1310FC1A1438CB004E5DB5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310EE1A1438CB004E5DB5 /* AppDelegate.swift */; };
+ DC1310FF1A1438CB004E5DB5 /* HMCharacteristic+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310F31A1438CB004E5DB5 /* HMCharacteristic+Properties.swift */; };
+ DC1311001A1438CB004E5DB5 /* HMService+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310F41A1438CB004E5DB5 /* HMService+Properties.swift */; };
+ DC1311041A1438CB004E5DB5 /* UIAlertController+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */; };
+ DC1311051A1438CB004E5DB5 /* UIViewController+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */; };
+ DC5042D41A1D5EFD000E3973 /* ActionSetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */; };
+ DC5042D61A1D5FFE000E3973 /* ActionSetCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */; };
+ DC53509F1A1D71F2000A8F0E /* ActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC53509E1A1D71F2000A8F0E /* ActionCell.swift */; };
+ DC58FA801A1BD10400550AD3 /* ServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */; };
+ DC58FA831A1BE32000550AD3 /* CharacteristicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */; };
+ DC58FA881A1BE59500550AD3 /* CharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */; };
+ DC58FA891A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */; };
+ DC58FA8A1A1BE59500550AD3 /* SliderCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */; };
+ DC58FA8B1A1BE59500550AD3 /* SwitchCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */; };
+ DC58FA8D1A1BE5A300550AD3 /* CharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */; };
+ DC58FA8F1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */; };
+ DC58FA911A1BE5C400550AD3 /* SliderCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */; };
+ DC58FA931A1BE5D000550AD3 /* SwitchCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */; };
+ DC58FA951A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */; };
+ DC58FA971A1BF4DD00550AD3 /* AccessoryUpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */; };
+ DCD480611A16B31C001BFEE3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCD480601A16B31C001BFEE3 /* Main.storyboard */; };
+ DCD480631A16B371001BFEE3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCD480621A16B371001BFEE3 /* Images.xcassets */; };
+ DCD480651A16B3AC001BFEE3 /* HomeListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */; };
+ DCD480671A16B4C3001BFEE3 /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480661A16B4C3001BFEE3 /* HomeStore.swift */; };
+ DCD4806E1A16BD1D001BFEE3 /* HMHome+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */; };
+ DCD480701A16C682001BFEE3 /* ControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */; };
+ DCD480721A16C69C001BFEE3 /* ControlsTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */; };
+ DCD480741A16C948001BFEE3 /* ServiceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480731A16C948001BFEE3 /* ServiceCell.swift */; };
+ DCD480761A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */; };
+ DCF046BE1A1A5D92002DBFBF /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */; };
+ DCF046C21A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */; };
+ DCF046C41A1A939F002DBFBF /* RoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C31A1A939F002DBFBF /* RoomViewController.swift */; };
+ DCF046C61A1A9A73002DBFBF /* ZoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */; };
+ DCF046C81A1A9D25002DBFBF /* AddRoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */; };
+ DCF046D41A1AB19F002DBFBF /* AccessoryBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */; };
+ DCF046D61A1AB627002DBFBF /* ModifyAccessoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ CAA68A951AF0614300905CFE /* Embed App Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ );
+ name = "Embed App Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 3E7CACDF1B1E4AAB00891CE0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
+ 6A10AEE81AF84BEA000A2CD6 /* HMCatalog.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = HMCatalog.entitlements; sourceTree = ""; };
+ 6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; };
+ 6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HMCatalogViewController.swift; sourceTree = ""; };
+ 6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SegmentedTimeCell.swift; sourceTree = ""; tabWidth = 4; };
+ 6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FavoritesViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimerTriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = LocationTriggerCreator.swift; sourceTree = ""; tabWidth = 4; };
+ 6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = EventTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimeConditionViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = TimePickerCell.swift; sourceTree = ""; tabWidth = 4; };
+ 6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = EventTriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "NSPredicate+Condition.swift"; sourceTree = ""; tabWidth = 4; };
+ 6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HMActionSet+BuiltIn.swift"; sourceTree = ""; };
+ 6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicSelectionViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicTriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6A96D1201B051326004DD072 /* UIColor+Custom.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Custom.swift"; sourceTree = ""; tabWidth = 4; };
+ 6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FavoritesManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Convenience.swift"; sourceTree = ""; };
+ 6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LocationTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6ABBF3011ADF1A1C00C1CF69 /* Array+Sorting.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "Array+Sorting.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MapOverlayView.swift; sourceTree = ""; tabWidth = 4; };
+ 6ACBCF591B02DAA000851BD3 /* MapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; tabWidth = 4; };
+ 6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimerTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "HMEventTrigger+Convenience.swift"; sourceTree = ""; tabWidth = 4; };
+ 6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ConditionCell.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6AD5641B1AFA792A00321F78 /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; };
+ 6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeKitObjectCollection.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ 6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextCharacteristicCell.xib; sourceTree = ""; };
+ 6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCharacteristicCell.swift; sourceTree = ""; };
+ 6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CNMutablePostalAddress+Convenience.swift"; sourceTree = ""; };
+ DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ServiceGroupViewController.swift; sourceTree = ""; tabWidth = 4; };
+ DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AddServicesViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC1310BE1A14201E004E5DB5 /* HMCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HMCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DC1310EE1A1438CB004E5DB5 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC1310F31A1438CB004E5DB5 /* HMCharacteristic+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMCharacteristic+Properties.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC1310F41A1438CB004E5DB5 /* HMService+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMService+Properties.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC1310F81A1438CB004E5DB5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Convenience.swift"; sourceTree = ""; tabWidth = 4; };
+ DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "UIViewController+Convenience.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSetViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSetCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC53509E1A1D71F2000A8F0E /* ActionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionCell.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ServicesViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicsViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CharacteristicCell.xib; sourceTree = ""; };
+ DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SegmentedControlCharacteristicCell.xib; sourceTree = ""; };
+ DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderCharacteristicCell.xib; sourceTree = ""; };
+ DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchCharacteristicCell.xib; sourceTree = ""; };
+ DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicCell.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlCharacteristicCell.swift; sourceTree = ""; tabWidth = 4; };
+ DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = SliderCharacteristicCell.swift; sourceTree = ""; tabWidth = 3; };
+ DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SwitchCharacteristicCell.swift; sourceTree = ""; tabWidth = 4; };
+ DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicsTableViewDataSource.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccessoryUpdateController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCD480601A16B31C001BFEE3 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; };
+ DCD480621A16B371001BFEE3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
+ DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeListViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCD480661A16B4C3001BFEE3 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; tabWidth = 4; };
+ DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMHome+Properties.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ControlsViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ControlsTableViewDataSource.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCD480731A16C948001BFEE3 /* ServiceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ServiceCell.swift; sourceTree = ""; tabWidth = 4; };
+ DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeListConfigurationViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboardSegue+IntendedDestination.swift"; sourceTree = ""; tabWidth = 4; };
+ DCF046C31A1A939F002DBFBF /* RoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = RoomViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ZoneViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AddRoomViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccessoryBrowserViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+ DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ModifyAccessoryViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ DC1310BB1A14201E004E5DB5 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 6A58EE941AF1477700ECAD21 /* Favorites */ = {
+ isa = PBXGroup;
+ children = (
+ 6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */,
+ 6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */,
+ );
+ path = Favorites;
+ sourceTree = "";
+ };
+ 6A5D852D1B0D2134008DF524 /* Event */ = {
+ isa = PBXGroup;
+ children = (
+ 6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */,
+ 6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */,
+ 6A5D85301B0D3017008DF524 /* Conditions */,
+ 6A8347511B0573C60055198E /* Characteristic */,
+ 6A8347501B0573A00055198E /* Location */,
+ );
+ path = Event;
+ sourceTree = "";
+ };
+ 6A5D85301B0D3017008DF524 /* Conditions */ = {
+ isa = PBXGroup;
+ children = (
+ 6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */,
+ 6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */,
+ 6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */,
+ 6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */,
+ );
+ path = Conditions;
+ sourceTree = "";
+ };
+ 6A8347501B0573A00055198E /* Location */ = {
+ isa = PBXGroup;
+ children = (
+ 6ACBCF631B02DB2400851BD3 /* Mapping */,
+ 6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */,
+ 6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */,
+ );
+ path = Location;
+ sourceTree = "";
+ };
+ 6A8347511B0573C60055198E /* Characteristic */ = {
+ isa = PBXGroup;
+ children = (
+ 6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */,
+ 6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */,
+ 6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */,
+ );
+ path = Characteristic;
+ sourceTree = "";
+ };
+ 6ACBCF5B1B02DAA000851BD3 /* Timer */ = {
+ isa = PBXGroup;
+ children = (
+ 6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */,
+ 6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */,
+ );
+ path = Timer;
+ sourceTree = "";
+ };
+ 6ACBCF631B02DB2400851BD3 /* Mapping */ = {
+ isa = PBXGroup;
+ children = (
+ 6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */,
+ 6ACBCF591B02DAA000851BD3 /* MapViewController.swift */,
+ );
+ path = Mapping;
+ sourceTree = "";
+ };
+ DC1310B51A14201E004E5DB5 = {
+ isa = PBXGroup;
+ children = (
+ 3E7CACDF1B1E4AAB00891CE0 /* README.md */,
+ 6A10AEE81AF84BEA000A2CD6 /* HMCatalog.entitlements */,
+ DC1310ED1A1438CB004E5DB5 /* HMCatalog */,
+ DC1310BF1A14201E004E5DB5 /* Products */,
+ );
+ sourceTree = "";
+ };
+ DC1310BF1A14201E004E5DB5 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ DC1310BE1A14201E004E5DB5 /* HMCatalog.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ DC1310ED1A1438CB004E5DB5 /* HMCatalog */ = {
+ isa = PBXGroup;
+ children = (
+ DC1310EE1A1438CB004E5DB5 /* AppDelegate.swift */,
+ 6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */,
+ 6AD5641B1AFA792A00321F78 /* TabBarController.swift */,
+ DCD480601A16B31C001BFEE3 /* Main.storyboard */,
+ 6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */,
+ 6A58EE941AF1477700ECAD21 /* Favorites */,
+ DCF046D21A1AB107002DBFBF /* Homes */,
+ DCD480621A16B371001BFEE3 /* Images.xcassets */,
+ DC1310F71A1438CB004E5DB5 /* Supporting Files */,
+ );
+ path = HMCatalog;
+ sourceTree = "";
+ };
+ DC1310F71A1438CB004E5DB5 /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 6ABBF3011ADF1A1C00C1CF69 /* Array+Sorting.swift */,
+ 6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */,
+ 6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */,
+ DC1310F31A1438CB004E5DB5 /* HMCharacteristic+Properties.swift */,
+ 6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */,
+ DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */,
+ DC1310F41A1438CB004E5DB5 /* HMService+Properties.swift */,
+ 6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */,
+ DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */,
+ 6A96D1201B051326004DD072 /* UIColor+Custom.swift */,
+ DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */,
+ 6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */,
+ DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */,
+ DC1310F81A1438CB004E5DB5 /* Info.plist */,
+ );
+ path = "Supporting Files";
+ sourceTree = "";
+ };
+ DCF046CA1A1AB0B9002DBFBF /* Rooms */ = {
+ isa = PBXGroup;
+ children = (
+ DCF046C31A1A939F002DBFBF /* RoomViewController.swift */,
+ );
+ path = Rooms;
+ sourceTree = "";
+ };
+ DCF046CB1A1AB0C1002DBFBF /* Zones */ = {
+ isa = PBXGroup;
+ children = (
+ DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */,
+ DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */,
+ );
+ path = Zones;
+ sourceTree = "";
+ };
+ DCF046CC1A1AB0C5002DBFBF /* Accessories */ = {
+ isa = PBXGroup;
+ children = (
+ DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */,
+ DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */,
+ DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */,
+ DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */,
+ DCF046CD1A1AB0CB002DBFBF /* Services */,
+ DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */,
+ );
+ path = Accessories;
+ sourceTree = "";
+ };
+ DCF046CD1A1AB0CB002DBFBF /* Services */ = {
+ isa = PBXGroup;
+ children = (
+ DCD480731A16C948001BFEE3 /* ServiceCell.swift */,
+ DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */,
+ DCF046CE1A1AB0D2002DBFBF /* Characteristic Cells */,
+ DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */,
+ DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */,
+ );
+ path = Services;
+ sourceTree = "";
+ };
+ DCF046CE1A1AB0D2002DBFBF /* Characteristic Cells */ = {
+ isa = PBXGroup;
+ children = (
+ DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */,
+ DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */,
+ DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */,
+ DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */,
+ DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */,
+ DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */,
+ DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */,
+ DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */,
+ 6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */,
+ 6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */,
+ );
+ path = "Characteristic Cells";
+ sourceTree = "";
+ };
+ DCF046CF1A1AB0D9002DBFBF /* Service Groups */ = {
+ isa = PBXGroup;
+ children = (
+ DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */,
+ DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */,
+ );
+ path = "Service Groups";
+ sourceTree = "";
+ };
+ DCF046D01A1AB0E5002DBFBF /* Action Sets */ = {
+ isa = PBXGroup;
+ children = (
+ DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */,
+ DC53509E1A1D71F2000A8F0E /* ActionCell.swift */,
+ DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */,
+ );
+ path = "Action Sets";
+ sourceTree = "";
+ };
+ DCF046D11A1AB0EC002DBFBF /* Triggers */ = {
+ isa = PBXGroup;
+ children = (
+ 6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */,
+ 6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */,
+ 6A5D852D1B0D2134008DF524 /* Event */,
+ 6ACBCF5B1B02DAA000851BD3 /* Timer */,
+ );
+ path = Triggers;
+ sourceTree = "";
+ };
+ DCF046D21A1AB107002DBFBF /* Homes */ = {
+ isa = PBXGroup;
+ children = (
+ DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */,
+ DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */,
+ DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */,
+ 6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */,
+ DCD480661A16B4C3001BFEE3 /* HomeStore.swift */,
+ DCF046CC1A1AB0C5002DBFBF /* Accessories */,
+ DCF046CA1A1AB0B9002DBFBF /* Rooms */,
+ DCF046CB1A1AB0C1002DBFBF /* Zones */,
+ DCF046D01A1AB0E5002DBFBF /* Action Sets */,
+ DCF046D11A1AB0EC002DBFBF /* Triggers */,
+ DCF046CF1A1AB0D9002DBFBF /* Service Groups */,
+ );
+ path = Homes;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ DC1310BD1A14201E004E5DB5 /* HMCatalog */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DC1310DD1A14201E004E5DB5 /* Build configuration list for PBXNativeTarget "HMCatalog" */;
+ buildPhases = (
+ DC1310BA1A14201E004E5DB5 /* Sources */,
+ DC1310BB1A14201E004E5DB5 /* Frameworks */,
+ DC1310BC1A14201E004E5DB5 /* Resources */,
+ CAA68A951AF0614300905CFE /* Embed App Extensions */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = HMCatalog;
+ productName = sldkfjghslkjdgh;
+ productReference = DC1310BE1A14201E004E5DB5 /* HMCatalog.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ DC1310B61A14201E004E5DB5 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0700;
+ LastUpgradeCheck = 0800;
+ ORGANIZATIONNAME = "Apple, Inc";
+ TargetAttributes = {
+ DC1310BD1A14201E004E5DB5 = {
+ CreatedOnToolsVersion = 6.1;
+ LastSwiftMigration = 0800;
+ SystemCapabilities = {
+ com.apple.HomeKit = {
+ enabled = 1;
+ };
+ com.apple.WAC = {
+ enabled = 1;
+ };
+ };
+ };
+ };
+ };
+ buildConfigurationList = DC1310B91A14201E004E5DB5 /* Build configuration list for PBXProject "HMCatalog" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = DC1310B51A14201E004E5DB5;
+ productRefGroup = DC1310BF1A14201E004E5DB5 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ DC1310BD1A14201E004E5DB5 /* HMCatalog */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ DC1310BC1A14201E004E5DB5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DC58FA8B1A1BE59500550AD3 /* SwitchCharacteristicCell.xib in Resources */,
+ DCD480611A16B31C001BFEE3 /* Main.storyboard in Resources */,
+ 6A3B541F1AF92D37007CA237 /* Launch Screen.storyboard in Resources */,
+ 6ADF25221AE5AA8200CD05D0 /* TextCharacteristicCell.xib in Resources */,
+ DC58FA891A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib in Resources */,
+ DC58FA8A1A1BE59500550AD3 /* SliderCharacteristicCell.xib in Resources */,
+ DCD480631A16B371001BFEE3 /* Images.xcassets in Resources */,
+ DC58FA881A1BE59500550AD3 /* CharacteristicCell.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ DC1310BA1A14201E004E5DB5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DCD480671A16B4C3001BFEE3 /* HomeStore.swift in Sources */,
+ 6ACCD8C21B1266A70002FA61 /* ConditionCell.swift in Sources */,
+ 6AA850DA1B1E645300A77A3E /* UITableViewController+Convenience.swift in Sources */,
+ 6ACBCF621B02DB1700851BD3 /* TriggerViewController.swift in Sources */,
+ DC58FA951A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift in Sources */,
+ 6A58EE961AF147BE00ECAD21 /* FavoritesViewController.swift in Sources */,
+ DC1311041A1438CB004E5DB5 /* UIAlertController+Convenience.swift in Sources */,
+ 6ACBCF601B02DAA000851BD3 /* TimerTriggerViewController.swift in Sources */,
+ DC1310FF1A1438CB004E5DB5 /* HMCharacteristic+Properties.swift in Sources */,
+ DC1310FC1A1438CB004E5DB5 /* AppDelegate.swift in Sources */,
+ 6ADF25241AE5AAA100CD05D0 /* TextCharacteristicCell.swift in Sources */,
+ DCF046C61A1A9A73002DBFBF /* ZoneViewController.swift in Sources */,
+ DC1311051A1438CB004E5DB5 /* UIViewController+Convenience.swift in Sources */,
+ DCF046BE1A1A5D92002DBFBF /* HomeViewController.swift in Sources */,
+ 6A5D852C1B0D05DD008DF524 /* LocationTriggerCreator.swift in Sources */,
+ 6AA42B471AEEF28500A92A79 /* FavoritesManager.swift in Sources */,
+ DCD4806E1A16BD1D001BFEE3 /* HMHome+Properties.swift in Sources */,
+ 6A6DA8881B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift in Sources */,
+ DC53509F1A1D71F2000A8F0E /* ActionCell.swift in Sources */,
+ DCF046D61A1AB627002DBFBF /* ModifyAccessoryViewController.swift in Sources */,
+ DCD480721A16C69C001BFEE3 /* ControlsTableViewDataSource.swift in Sources */,
+ 6A5D85281B0CF3C5008DF524 /* TriggerCreator.swift in Sources */,
+ DC0BBB991A1FB60E002AB35C /* ServiceGroupViewController.swift in Sources */,
+ 6A96D1211B051326004DD072 /* UIColor+Custom.swift in Sources */,
+ DC58FA831A1BE32000550AD3 /* CharacteristicsViewController.swift in Sources */,
+ DC1311001A1438CB004E5DB5 /* HMService+Properties.swift in Sources */,
+ DC58FA8D1A1BE5A300550AD3 /* CharacteristicCell.swift in Sources */,
+ 6A5D85341B0D40B0008DF524 /* TimePickerCell.swift in Sources */,
+ DC58FA911A1BE5C400550AD3 /* SliderCharacteristicCell.swift in Sources */,
+ 6A5D85321B0D3032008DF524 /* TimeConditionViewController.swift in Sources */,
+ 6A5D853B1B0D63EB008DF524 /* NSPredicate+Condition.swift in Sources */,
+ 6AD5641C1AFA792A00321F78 /* TabBarController.swift in Sources */,
+ 6ADF251E1AE1940B00CD05D0 /* HomeKitObjectCollection.swift in Sources */,
+ DCF046C81A1A9D25002DBFBF /* AddRoomViewController.swift in Sources */,
+ DCD480741A16C948001BFEE3 /* ServiceCell.swift in Sources */,
+ DC58FA931A1BE5D000550AD3 /* SwitchCharacteristicCell.swift in Sources */,
+ DC58FA971A1BF4DD00550AD3 /* AccessoryUpdateController.swift in Sources */,
+ DCD480701A16C682001BFEE3 /* ControlsViewController.swift in Sources */,
+ 6AAE046E1B0A93660084A575 /* LocationTriggerViewController.swift in Sources */,
+ 6A5D85391B0D5100008DF524 /* EventTriggerCreator.swift in Sources */,
+ 6A589F641B0FAF3200CDD54B /* SegmentedTimeCell.swift in Sources */,
+ 6A5D852F1B0D2155008DF524 /* EventTriggerViewController.swift in Sources */,
+ DCF046C41A1A939F002DBFBF /* RoomViewController.swift in Sources */,
+ 6ACBCF5F1B02DAA000851BD3 /* MapViewController.swift in Sources */,
+ 6A5D852A1B0CFE6C008DF524 /* TimerTriggerCreator.swift in Sources */,
+ 6ACBCF5D1B02DAA000851BD3 /* CharacteristicTriggerViewController.swift in Sources */,
+ 6A8347531B0574040055198E /* CharacteristicSelectionViewController.swift in Sources */,
+ 6ACBCFA01B040AA600851BD3 /* HMEventTrigger+Convenience.swift in Sources */,
+ 6ABBF3021ADF1A1C00C1CF69 /* Array+Sorting.swift in Sources */,
+ 6A4D140F1ADDF2DC00364DE0 /* HMCatalogViewController.swift in Sources */,
+ DCD480761A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift in Sources */,
+ DCF046C21A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift in Sources */,
+ DC5042D41A1D5EFD000E3973 /* ActionSetViewController.swift in Sources */,
+ 6AE2A5641B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift in Sources */,
+ DC5042D61A1D5FFE000E3973 /* ActionSetCreator.swift in Sources */,
+ 6A8347551B06668B0055198E /* CharacteristicTriggerCreator.swift in Sources */,
+ DCF046D41A1AB19F002DBFBF /* AccessoryBrowserViewController.swift in Sources */,
+ 6ACBCF5E1B02DAA000851BD3 /* MapOverlayView.swift in Sources */,
+ DC58FA801A1BD10400550AD3 /* ServicesViewController.swift in Sources */,
+ DC58FA8F1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift in Sources */,
+ DC0BBB9D1A1FBC46002AB35C /* AddServicesViewController.swift in Sources */,
+ DCD480651A16B3AC001BFEE3 /* HomeListViewController.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ DC1310DB1A14201E004E5DB5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.2;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ DC1310DC1A14201E004E5DB5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.2;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ DC1310DE1A14201E004E5DB5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = HMCatalog.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ ENABLE_ON_DEMAND_RESOURCES = NO;
+ INFOPLIST_FILE = "$(SRCROOT)/HMCatalog/Supporting Files/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)";
+ PRODUCT_NAME = HMCatalog;
+ PROVISIONING_PROFILE = "";
+ SWIFT_VERSION = 2.3;
+ };
+ name = Debug;
+ };
+ DC1310DF1A14201E004E5DB5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = HMCatalog.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ ENABLE_ON_DEMAND_RESOURCES = NO;
+ INFOPLIST_FILE = "$(SRCROOT)/HMCatalog/Supporting Files/Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)";
+ PRODUCT_NAME = HMCatalog;
+ PROVISIONING_PROFILE = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ SWIFT_VERSION = 2.3;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ DC1310B91A14201E004E5DB5 /* Build configuration list for PBXProject "HMCatalog" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DC1310DB1A14201E004E5DB5 /* Debug */,
+ DC1310DC1A14201E004E5DB5 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DC1310DD1A14201E004E5DB5 /* Build configuration list for PBXNativeTarget "HMCatalog" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DC1310DE1A14201E004E5DB5 /* Debug */,
+ DC1310DF1A14201E004E5DB5 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = DC1310B61A14201E004E5DB5 /* Project object */;
+}
diff --git a/HomeKitCatalog/HMCatalog/AppDelegate.swift b/HomeKitCatalog/HMCatalog/AppDelegate.swift
new file mode 100644
index 00000000..d1551047
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/AppDelegate.swift
@@ -0,0 +1,17 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `AppDelegate` handles higher-level app events.
+*/
+
+import UIKit
+
+/// A standard app delegate.
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ // MARK: Properties
+
+ var window: UIWindow?
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Favorites/FavoritesManager.swift b/HomeKitCatalog/HMCatalog/Favorites/FavoritesManager.swift
new file mode 100644
index 00000000..6ec21981
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Favorites/FavoritesManager.swift
@@ -0,0 +1,300 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `FavoritesManager` stores and saves characteristics that have been pinned by the user.
+*/
+
+import HomeKit
+
+/// Handles interactions with `NSUserDefault`s to save the user's favorite accessories.
+class FavoritesManager {
+ // MARK: Types
+
+ static let accessoryToCharacteristicIdentifierMappingKey = "FavoritesManager.accessoryToCharacteristicIdentifierMappingKey"
+
+ static let accessoryIdentifiersKey = "FavoritesManager.accessoryIdentifiersKey"
+
+ // MARK: Properties
+
+ /// A shared, singleton manager.
+ static let sharedManager = FavoritesManager()
+
+ var home: HMHome? {
+ return HomeStore.sharedStore.home
+ }
+
+ /**
+ An internal mapping of accessory unique identifiers to an array of their
+ favorite characteristic's unique identifiers.
+ */
+ private var accessoryToCharacteristicIdentifiers = [NSUUID: [NSUUID]]()
+
+ /// An internal array of all favorite accessory unique identifiers.
+ private var accessoryIdentifiers = [NSUUID]()
+
+ /**
+ Loads the unique identifier map and array data from `NSUserDefaults`
+ into internal variables.
+ */
+ init() {
+ let userDefaults = NSUserDefaults.standardUserDefaults()
+
+ if let mapData = userDefaults.objectForKey(FavoritesManager.accessoryToCharacteristicIdentifierMappingKey) as? NSData,
+ arrayData = userDefaults.objectForKey(FavoritesManager.accessoryIdentifiersKey) as? NSData {
+
+ accessoryToCharacteristicIdentifiers = NSKeyedUnarchiver.unarchiveObjectWithData(mapData) as? [NSUUID: [NSUUID]] ?? [:]
+
+ accessoryIdentifiers = NSKeyedUnarchiver.unarchiveObjectWithData(arrayData) as? [NSUUID] ?? []
+ }
+ }
+
+ /**
+ - returns: An array of all favorite characteristics.
+ The array is sorted by localized type.
+ */
+ var favoriteCharacteristics: [HMCharacteristic] {
+ // Find all of the favorite characteristics.
+ let favoriteCharacteristics = HomeStore.sharedStore.homeManager.homes.map { home in
+ return home.allCharacteristics.filter { return $0.isFavorite }
+ }
+
+ // Need to flatten an [[HMCharacteristic]] to an [HMCharacteristic].
+ return favoriteCharacteristics.reduce([], combine: +)
+ .sort(characteristicOrderedBefore)
+ }
+
+ /**
+ - returns: An array of all favorite accessories.
+ The array is sorted by localized name.
+ */
+ var favoriteAccessories: [HMAccessory] {
+ // Find all of the favorite accessories.
+ let newAccessories = accessoryIdentifiers.map { accessoryIdentifier in
+ return HomeStore.sharedStore.homeManager.homes.map { home in
+ return home.accessories.filter { accessory in
+ return accessory.uniqueIdentifier == accessoryIdentifier
+ }
+ }
+ }
+
+ // Need to flatten [[[HMAccessory]]] to [HMAccessory].
+ return newAccessories.reduce([], combine: +)
+ .reduce([], combine: +)
+ .sortByLocalizedName()
+ }
+
+
+ /**
+ - returns: An array of tuples representing accessories and
+ all favorite characteristics they contain.
+ The array is sorted by localized type.
+ */
+ var favoriteGroups:[(accessory: HMAccessory, characteristics: [HMCharacteristic])] {
+ return favoriteAccessories.map { accessory in
+ let favoriteCharacteristics = favoriteCharacteristicsForAccessory(accessory)
+
+ return (accessory: accessory, characteristics: favoriteCharacteristics)
+ }
+ }
+
+
+ /**
+ Evaluates whether or not an `HMCharacteristic` is a favorite.
+
+ - parameter characteristic: The `HMCharacteristic` to evaluate.
+
+ - returns: A `Bool`, whether or not the characteristic is a favorite.
+ */
+ func characteristicIsFavorite(characteristic: HMCharacteristic) -> Bool {
+ guard let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier else {
+ return false
+ }
+
+ guard let characteristicIdentifiers = accessoryToCharacteristicIdentifiers[accessoryIdentifier] else {
+ return false
+ }
+
+ return characteristicIdentifiers.contains(characteristic.uniqueIdentifier)
+ }
+
+ /**
+ Favorites a characteristic.
+
+ - parameter characteristic: The `HMCharacteristic` to favorite.
+ */
+ func favoriteCharacteristic(characteristic: HMCharacteristic) {
+ if characteristicIsFavorite(characteristic) {
+ return
+ }
+
+ if let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier where accessoryToCharacteristicIdentifiers[accessoryIdentifier] != nil {
+ // Accessory is already favorite, add the characteristic.
+ accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.append(characteristic.uniqueIdentifier)
+ save()
+ }
+ else if let accessory = characteristic.service?.accessory {
+ // New accessory, make a new entry.
+ accessoryIdentifiers.append(accessory.uniqueIdentifier)
+ accessoryToCharacteristicIdentifiers[accessory.uniqueIdentifier] = [characteristic.uniqueIdentifier]
+ save()
+ }
+ }
+
+ /**
+ Provides an array of favorite `HMCharacteristic`s within a given accessory.
+
+ - parameter accessory: The `HMAccessory` to query.
+
+ - returns: An array of `HMCharacteristic`s which are favorites for the provided accessory.
+ */
+ func favoriteCharacteristicsForAccessory(accessory: HMAccessory) -> [HMCharacteristic] {
+ let characteristics = accessory.services.map { service in
+ return service.characteristics.filter { characteristic in
+ return characteristic.isFavorite
+ }
+ }
+ return characteristics.reduce([], combine: +)
+ .sort(characteristicOrderedBefore)
+ }
+
+
+ /**
+ Unfavorites a characteristic.
+
+ - parameter characteristic: The `HMCharacteristic` to unfavorite.
+ */
+ func unfavoriteCharacteristic(characteristic: HMCharacteristic) {
+ guard let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier else { return }
+
+ guard let characteristicIdentifiers = accessoryToCharacteristicIdentifiers[accessoryIdentifier] else { return }
+
+ guard let indexOfCharacteristic = characteristicIdentifiers.indexOf(characteristic.uniqueIdentifier) else { return }
+
+ // Remove the characteristic from the mapped collection.
+ accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.removeAtIndex(indexOfCharacteristic)
+ if let indexOfAccessory = accessoryIdentifiers.indexOf(accessoryIdentifier),
+ isEmpty = accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.isEmpty
+ where isEmpty {
+ /*
+ If that was the last characteristic for that accessory, remove
+ the accessory from the internal array.
+ */
+ accessoryIdentifiers.removeAtIndex(indexOfAccessory)
+ accessoryToCharacteristicIdentifiers.removeValueForKey(accessoryIdentifier)
+ }
+
+ save()
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ First, cleans out the internal identifier structures, then saves
+ the `accessoryToCharacteristicIdentifiers` map and `accessoryIdentifiers`
+ array into `NSUserDefaults`.
+
+ This method should be called whenever a change is made to the internal structures.
+ */
+ private func save() {
+ removeUnusedIdentifiers()
+
+ let userDefaults = NSUserDefaults.standardUserDefaults()
+
+ let mapData = NSKeyedArchiver.archivedDataWithRootObject(accessoryToCharacteristicIdentifiers)
+ let arrayData = NSKeyedArchiver.archivedDataWithRootObject(accessoryIdentifiers)
+
+ userDefaults.setObject(mapData, forKey: FavoritesManager.accessoryToCharacteristicIdentifierMappingKey)
+ userDefaults.setObject(arrayData, forKey: FavoritesManager.accessoryIdentifiersKey)
+ }
+
+ /**
+ Filters out any accessories or characteristic which are not longer
+ valid in HomeKit.
+ */
+ private func removeUnusedIdentifiers() {
+ accessoryIdentifiers = accessoryIdentifiers.filter { identifier in
+ return accessoryIdentifierExists(identifier)
+ }
+
+ let filteredPairs = accessoryToCharacteristicIdentifiers.filter { accessoryId, _ in
+ return accessoryIdentifierExists(accessoryId)
+ }
+
+ accessoryToCharacteristicIdentifiers.removeAll()
+
+ for (accessoryId, characteristicIds) in filteredPairs {
+ accessoryToCharacteristicIdentifiers[accessoryId] = characteristicIds
+ }
+
+ for accessoryIdentifier in accessoryToCharacteristicIdentifiers.keys {
+ let filteredCharacteristics = accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.filter { characteristicId in
+ return characteristicIdentifierExists(characteristicId)
+ }
+
+ accessoryToCharacteristicIdentifiers[accessoryIdentifier] = filteredCharacteristics
+ }
+ }
+
+ /**
+ - returns: `true` if there exists an accessory in HomeKit with the given
+ identifier; `false` otherwise.
+ */
+ private func accessoryIdentifierExists(identifier: NSUUID) -> Bool {
+ return HomeStore.sharedStore.homeManager.homes.contains { home in
+ return home.accessories.contains { accessory in
+ return accessory.uniqueIdentifier == identifier
+ }
+ }
+ }
+
+ /**
+ - returns: `true` if there exists a characteristic in HomeKit with the given
+ identifier; `false` otherwise.
+ */
+ private func characteristicIdentifierExists(identifier: NSUUID) -> Bool {
+ return HomeStore.sharedStore.homeManager.homes.contains { home in
+ return home.accessories.contains { accessory in
+ return accessory.services.contains { service in
+ return service.characteristics.contains { characteristic in
+ return characteristic.uniqueIdentifier == identifier
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ Evaluates two `HMCharacteristic` objects to determine if the first is ordered before the second.
+
+ - parameter characteristic1: The first `HMCharacteristic` to evaluate.
+ - parameter characteristic2: The second `HMCharacteristic` to evaluate.
+
+ - returns: `true` if the characteristics are localized ordered ascending, `false` otherwise.
+ */
+ private func characteristicOrderedBefore(characteristic1: HMCharacteristic, characteristic2: HMCharacteristic) -> Bool {
+ let type1 = characteristic1.localizedCharacteristicType
+ let type2 = characteristic2.localizedCharacteristicType
+
+ return type1.localizedCompare(type2) == .OrderedAscending
+ }
+}
+
+extension HMCharacteristic {
+ /// A convenience property to favorite, unfavorite, and query the status of a characteristic.
+ var isFavorite: Bool {
+ get {
+ return FavoritesManager.sharedManager.characteristicIsFavorite(self)
+ }
+
+ set {
+ if newValue {
+ FavoritesManager.sharedManager.favoriteCharacteristic(self)
+ }
+ else {
+ FavoritesManager.sharedManager.unfavoriteCharacteristic(self)
+ }
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Favorites/FavoritesViewController.swift b/HomeKitCatalog/HMCatalog/Favorites/FavoritesViewController.swift
new file mode 100644
index 00000000..6a81a2ad
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Favorites/FavoritesViewController.swift
@@ -0,0 +1,256 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `FavoritesViewController` allows users to control pinned accessories.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ Lists favorite characteristics (grouped by accessory) and allows users to
+ manipulate their values.
+*/
+class FavoritesViewController: UITableViewController, UITabBarControllerDelegate, HMAccessoryDelegate, HMHomeManagerDelegate {
+
+ // MARK: Types
+
+ struct Identifiers {
+ static let characteristicCell = "CharacteristicCell"
+ static let segmentedControlCharacteristicCell = "SegmentedControlCharacteristicCell"
+ static let switchCharacteristicCell = "SwitchCharacteristicCell"
+ static let sliderCharacteristicCell = "SliderCharacteristicCell"
+ static let textCharacteristicCell = "TextCharacteristicCell"
+ static let serviceTypeCell = "ServiceTypeCell"
+ }
+
+ // MARK: Properties
+
+ var favoriteAccessories = FavoritesManager.sharedManager.favoriteAccessories
+
+ var cellDelegate = AccessoryUpdateController()
+
+ @IBOutlet weak var editButton: UIBarButtonItem!
+
+ /// If `true`, the characteristic cells should show stars.
+ var showsFavorites = false {
+ didSet {
+ editButton.title = showsFavorites ? NSLocalizedString("Done", comment: "Done") : NSLocalizedString("Edit", comment: "Edit")
+
+ reloadData()
+ }
+ }
+
+ // MARK: View Methods
+
+ /// Configures the table view and tab bar.
+ override func awakeFromNib() {
+ tableView.estimatedRowHeight = 44.0
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.allowsSelectionDuringEditing = true
+
+ registerReuseIdentifiers()
+
+ tabBarController?.delegate = self
+ }
+
+ /// Prepares HomeKit objects and reloads view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+
+ registerAsDelegate()
+
+ setNotificationsEnabled(true)
+
+ reloadData()
+ }
+
+ /// Disables notifications and "unregisters" as the delegate for the home manager.
+ override func viewWillDisappear(animated: Bool) {
+ super.viewWillDisappear(animated)
+ setNotificationsEnabled(false)
+
+ // We don't want any more callbacks once the view has disappeared.
+ HomeStore.sharedStore.homeManager.delegate = nil
+ }
+
+ /// Registers for all types of characteristic cells.
+ private func registerReuseIdentifiers() {
+ let characteristicNib = UINib(nibName: Identifiers.characteristicCell, bundle: nil)
+ tableView.registerNib(characteristicNib, forCellReuseIdentifier: Identifiers.characteristicCell)
+
+ let sliderNib = UINib(nibName: Identifiers.sliderCharacteristicCell, bundle: nil)
+ tableView.registerNib(sliderNib, forCellReuseIdentifier: Identifiers.sliderCharacteristicCell)
+
+ let switchNib = UINib(nibName: Identifiers.switchCharacteristicCell, bundle: nil)
+ tableView.registerNib(switchNib, forCellReuseIdentifier: Identifiers.switchCharacteristicCell)
+
+ let segmentedNib = UINib(nibName: Identifiers.segmentedControlCharacteristicCell, bundle: nil)
+ tableView.registerNib(segmentedNib, forCellReuseIdentifier: Identifiers.segmentedControlCharacteristicCell)
+
+ let textNib = UINib(nibName: Identifiers.textCharacteristicCell, bundle: nil)
+ tableView.registerNib(textNib, forCellReuseIdentifier: Identifiers.textCharacteristicCell)
+
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.serviceTypeCell)
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ Provides the number of sections based on the favorite accessories count.
+ Also, add/removes the background message, if required.
+
+ - returns: The favorite accessories count.
+ */
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ let sectionCount = favoriteAccessories.count
+
+ if sectionCount == 0 {
+ let message = NSLocalizedString("No Favorite Characteristics", comment: "No Favorite Characteristics")
+
+ setBackgroundMessage(message)
+ }
+ else {
+ setBackgroundMessage(nil)
+ }
+
+ return sectionCount
+ }
+
+ /// - returns: The number of characteristics for accessory represented by the section index.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let accessory = favoriteAccessories[section]
+
+ let characteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(accessory)
+
+ return characteristics.count
+ }
+
+ /**
+ Dequeues the appropriate characteristic cell for the characteristic at the
+ given index path and configures the cell based on view configurations.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let characteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(favoriteAccessories[indexPath.section])
+
+ let characteristic = characteristics[indexPath.row]
+
+ var reuseIdentifier = Identifiers.characteristicCell
+
+ if characteristic.isReadOnly || characteristic.isWriteOnly {
+ reuseIdentifier = Identifiers.characteristicCell
+ }
+ else if characteristic.isBoolean {
+ reuseIdentifier = Identifiers.switchCharacteristicCell
+ }
+ else if characteristic.hasPredeterminedValueDescriptions {
+ reuseIdentifier = Identifiers.segmentedControlCharacteristicCell
+ }
+ else if characteristic.isNumeric {
+ reuseIdentifier = Identifiers.sliderCharacteristicCell
+ }
+ else if characteristic.isTextWritable {
+ reuseIdentifier = Identifiers.textCharacteristicCell
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CharacteristicCell
+
+ cell.showsFavorites = showsFavorites
+ cell.delegate = cellDelegate
+ cell.characteristic = characteristic
+
+ return cell
+ }
+
+ /// - returns: The name of the accessory at the specified index path.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return favoriteAccessories[section].name
+ }
+
+ // MARK: IBAction Methods
+
+ /// Toggles `showsFavorites`, which will also reload the view.
+ @IBAction func didTapEdit(sender: UIBarButtonItem) {
+ showsFavorites = !showsFavorites
+ }
+
+
+ // MARK: Helper Methods
+
+ /**
+ Resets the `favoriteAccessories` array from the `FavoritesManager`,
+ resets the state of the edit button, and reloads the data.
+ */
+ private func reloadData() {
+ favoriteAccessories = FavoritesManager.sharedManager.favoriteAccessories
+
+ editButton.enabled = !favoriteAccessories.isEmpty
+
+ tableView.reloadData()
+ }
+
+ /**
+ Enables or disables notifications for all favorite characteristics which
+ support event notifications.
+
+ - parameter notificationsEnabled: A `Bool` representing enabled or disabled.
+ */
+ private func setNotificationsEnabled(notificationsEnabled: Bool) {
+ for characteristic in FavoritesManager.sharedManager.favoriteCharacteristics {
+ if characteristic.supportsEventNotification {
+ characteristic.enableNotification(notificationsEnabled) { error in
+ if let error = error {
+ print("HomeKit: Error enabling notification on characteristic \(characteristic): \(error.localizedDescription).")
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ Registers as the delegate for the home manager and all
+ favorite accessories.
+ */
+ private func registerAsDelegate() {
+ HomeStore.sharedStore.homeManager.delegate = self
+
+ for accessory in favoriteAccessories {
+ accessory.delegate = self
+ }
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ /// Update the view to disable cells with unavailable accessories.
+ func accessoryDidUpdateReachability(accessory: HMAccessory) {
+ reloadData()
+ }
+
+ /// Search for the cell corresponding to that characteristic and update its value.
+ func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) {
+ guard let accessory = characteristic.service?.accessory else { return }
+
+ guard let indexOfAccessory = favoriteAccessories.indexOf(accessory) else { return }
+
+ let favoriteCharacteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(accessory)
+
+ guard let indexOfCharacteristic = favoriteCharacteristics.indexOf(characteristic) else { return }
+
+ let indexPath = NSIndexPath(forRow: indexOfCharacteristic, inSection: indexOfAccessory)
+
+ let cell = tableView.cellForRowAtIndexPath(indexPath) as! CharacteristicCell
+
+ cell.setValue(characteristic.value, notify: false)
+ }
+
+ // MARK: HMHomeManagerDelegate Methods
+
+ /// Reloads views and re-configures characteristics.
+ func homeManagerDidUpdateHomes(manager: HMHomeManager) {
+ registerAsDelegate()
+ setNotificationsEnabled(true)
+ reloadData()
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/HMCatalogViewController.swift b/HomeKitCatalog/HMCatalog/HMCatalogViewController.swift
new file mode 100644
index 00000000..14d060fd
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/HMCatalogViewController.swift
@@ -0,0 +1,73 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HMCatalogViewController` is a super class which mainly provides easy-access methods for shared HomeKit objects.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ The super class for most table view controllers in this app. It manages home
+ delegate registration and facilitates 'popping back' when it's discovered that
+ a home has been deleted.
+*/
+class HMCatalogViewController: UITableViewController, HMHomeDelegate {
+ // MARK: Properties
+
+ var homeStore: HomeStore {
+ return HomeStore.sharedStore
+ }
+
+ var home: HMHome! {
+ return homeStore.home
+ }
+
+ // MARK: View Methods
+
+ /**
+ Evaluates whether or not the view controller should pop to
+ the list of homes.
+
+ - returns: `true` if this instance is not the root view controller
+ and the `home` is nil; `false` otherwise.
+ */
+ private func shouldPopViewController() -> Bool {
+ if let rootViewController = navigationController?.viewControllers.first
+ where rootViewController == self {
+ return false
+ }
+
+ return home == nil
+ }
+
+ /// Pops the view controller, if required. Invokes the delegate registration method.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+
+ if shouldPopViewController() {
+ // Pop to root view controller if our home was destroyed while we were away.
+ navigationController?.popToRootViewControllerAnimated(true)
+ return
+ }
+
+ registerAsDelegate()
+ }
+
+ // MARK: Delegate Registration
+
+ /**
+ A hierarchical method, to be overriden by superclasses.
+ The base implementation registers as the delegate for the `HomeStore`'s home.
+ Thus, any subclasses may override this, register as the delegate for any
+ objects they please, and then call `super.registerAsDelegate()` to register
+ as the home delegate as well.
+
+ This method will be called when the view appears.
+ */
+ func registerAsDelegate() {
+ homeStore.home?.delegate = self
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryBrowserViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryBrowserViewController.swift
new file mode 100644
index 00000000..ecb11621
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryBrowserViewController.swift
@@ -0,0 +1,307 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `AccessoryBrowserViewController` displays new accessories and allows the user to pair with them.
+*/
+
+import UIKit
+import HomeKit
+import ExternalAccessory
+
+/// Represents an accessory type and encapsulated accessory.
+enum AccessoryType: Equatable, Nameable {
+ /// A HomeKit object
+ case HomeKit(accessory: HMAccessory)
+
+ /// An external, `EAWiFiUnconfiguredAccessory` object
+ case External(accessory: EAWiFiUnconfiguredAccessory)
+
+ /// The name of the accessory.
+ var name: String {
+ return accessory.name
+ }
+
+ /// The accessory within the `AccessoryType`.
+ var accessory: AnyObject {
+ switch self {
+ case .HomeKit(let accessory):
+ return accessory
+
+ case .External(let accessory):
+ return accessory
+ }
+ }
+}
+
+/// Comparison of `AccessoryType`s based on name.
+func ==(lhs: AccessoryType, rhs: AccessoryType) -> Bool {
+ return lhs.name == rhs.name
+}
+
+/**
+ A view controller that displays a list of nearby accessories and allows the
+ user to add them to the provided HMHome.
+*/
+class AccessoryBrowserViewController: HMCatalogViewController, ModifyAccessoryDelegate, EAWiFiUnconfiguredAccessoryBrowserDelegate, HMAccessoryBrowserDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let accessoryCell = "AccessoryCell"
+ static let addedAccessoryCell = "AddedAccessoryCell"
+ static let addAccessorySegue = "Add Accessory"
+ }
+
+ // MARK: Properties
+
+ var addedAccessories = [HMAccessory]()
+ var displayedAccessories = [AccessoryType]()
+ let accessoryBrowser = HMAccessoryBrowser()
+ var externalAccessoryBrowser: EAWiFiUnconfiguredAccessoryBrowser?
+
+ // MARK: View Methods
+
+ /// Configures the table view and initializes the accessory browsers.
+ override func viewDidLoad() {
+ tableView.estimatedRowHeight = 44.0
+ tableView.rowHeight = UITableViewAutomaticDimension
+ accessoryBrowser.delegate = self
+
+ #if arch(arm)
+ // We can't use the ExternalAccessory framework on the iPhone simulator.
+ externalAccessoryBrowser = EAWiFiUnconfiguredAccessoryBrowser(delegate: self, queue: dispatch_get_main_queue())
+ #endif
+
+ startBrowsing()
+ }
+
+ /// Reloads the view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ reloadTable()
+ }
+
+ // MARK: IBAction Methods
+
+ /// Stops browsing and dismisses the view controller.
+ @IBAction func dismiss(sender: AnyObject) {
+ stopBrowsing()
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ /// Sets the accessory, home, and delegate of a ModifyAccessoryViewController.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+
+ if let sender = sender as? HMAccessory where segue.identifier == Identifiers.addAccessorySegue {
+ let modifyViewController = segue.intendedDestinationViewController as! ModifyAccessoryViewController
+ modifyViewController.accessory = sender
+ modifyViewController.delegate = self
+ }
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ Generates the number of rows based on the number of displayed accessories.
+
+ This method will also display a table view background message, if required.
+
+ - returns: The number of rows based on the number of displayed accessories.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let rows = displayedAccessories.count
+
+ if rows == 0 {
+ let message = NSLocalizedString("No Discovered Accessories", comment: "No Discovered Accessories")
+ setBackgroundMessage(message)
+ }
+ else {
+ setBackgroundMessage(nil)
+ }
+
+ return rows
+ }
+
+ /**
+ - returns: Creates a cell that lists an accessory, and if it hasn't been added to the home,
+ shows a disclosure indicator instead of a checkmark.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let accessoryType = displayedAccessories[indexPath.row]
+
+ var reuseIdentifier = Identifiers.accessoryCell
+
+ if case let .HomeKit(hmAccessory) = accessoryType where addedAccessories.contains(hmAccessory) {
+ reuseIdentifier = Identifiers.addedAccessoryCell
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
+ cell.textLabel?.text = accessoryType.name
+
+ if let accessory = accessoryType.accessory as? HMAccessory {
+ cell.detailTextLabel?.text = accessory.category.localizedDescription
+ }
+ else {
+ cell.detailTextLabel?.text = NSLocalizedString("External Accessory", comment: "External Accessory")
+ }
+
+ return cell
+ }
+
+ /// Configures the accessory based on its type.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ switch displayedAccessories[indexPath.row] {
+ case .HomeKit(let accessory):
+ configureAccessory(accessory)
+
+ case .External(let accessory):
+ externalAccessoryBrowser?.configureAccessory(accessory, withConfigurationUIOnViewController: self)
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /// Starts browsing on both HomeKit and External accessory browsers.
+ private func startBrowsing(){
+ accessoryBrowser.startSearchingForNewAccessories()
+ externalAccessoryBrowser?.startSearchingForUnconfiguredAccessoriesMatchingPredicate(nil)
+ }
+
+ /// Stops browsing on both HomeKit and External accessory browsers.
+ private func stopBrowsing(){
+ accessoryBrowser.stopSearchingForNewAccessories()
+ externalAccessoryBrowser?.stopSearchingForUnconfiguredAccessories()
+ }
+
+ /**
+ Concatenates and sorts the discovered and added accessories.
+
+ - returns: A sorted list of all accessories involved with this
+ browser session.
+ */
+ func allAccessories() -> [AccessoryType] {
+ var accessories = [AccessoryType]()
+ accessories += accessoryBrowser.discoveredAccessories.map { .HomeKit(accessory: $0) }
+
+ accessories += addedAccessories.flatMap { addedAccessory in
+ let accessoryType = AccessoryType.HomeKit(accessory: addedAccessory)
+
+ return accessories.contains(accessoryType) ? nil : accessoryType
+ }
+
+ if let external = externalAccessoryBrowser?.unconfiguredAccessories {
+ let unconfiguredAccessoriesArray = Array(external)
+
+ accessories += unconfiguredAccessoriesArray.flatMap { addedAccessory in
+ let accessoryType = AccessoryType.External(accessory: addedAccessory)
+
+ return accessories.contains(accessoryType) ? nil : accessoryType
+ }
+ }
+
+ return accessories.sortByLocalizedName()
+ }
+
+ /// Updates the displayed accesories array and reloads the table view.
+ private func reloadTable() {
+ displayedAccessories = allAccessories()
+ tableView.reloadData()
+ }
+
+ /// Sends the accessory to the next view.
+ func configureAccessory(accessory: HMAccessory) {
+ if displayedAccessories.contains(.HomeKit(accessory: accessory)) {
+ performSegueWithIdentifier(Identifiers.addAccessorySegue, sender: accessory)
+ }
+ }
+
+ /**
+ Finds an unconfigured accessory with a specified name.
+
+ - parameter name: The name string of the accessory.
+
+ - returns: An `HMAccessory?` from the search; `nil` if
+ the accessory could not be found.
+ */
+ func unconfiguredHomeKitAccessoryWithName(name: String) -> HMAccessory? {
+ for type in displayedAccessories {
+ if case let .HomeKit(accessory) = type where accessory.name == name {
+ return accessory
+ }
+ }
+ return nil
+ }
+
+ // MARK: ModifyAccessoryDelegate Methods
+
+ /// Adds the accessory to the internal array and reloads the views.
+ func accessoryViewController(accessoryViewController: ModifyAccessoryViewController, didSaveAccessory accessory: HMAccessory) {
+ addedAccessories.append(accessory)
+ reloadTable()
+ }
+
+ // MARK: EAWiFiUnconfiguredAccessoryBrowserDelegate Methods
+
+ // Any updates to the external accessory browser causes a reload in the table view.
+
+ func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didFindUnconfiguredAccessories accessories: Set) {
+ reloadTable()
+ }
+
+ func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didRemoveUnconfiguredAccessories accessories: Set) {
+ reloadTable()
+ }
+
+ func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didUpdateState state: EAWiFiUnconfiguredAccessoryBrowserState) {
+ reloadTable()
+ }
+
+ /// If the configuration was successful, presents the 'Add Accessory' view.
+ func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didFinishConfiguringAccessory accessory: EAWiFiUnconfiguredAccessory, withStatus status: EAWiFiUnconfiguredAccessoryConfigurationStatus) {
+ if status != .Success {
+ return
+ }
+
+ if let foundAccessory = unconfiguredHomeKitAccessoryWithName(accessory.name) {
+ configureAccessory(foundAccessory)
+ }
+ }
+
+ // MARK: HMAccessoryBrowserDelegate Methods
+
+ /**
+ Inserts the accessory into the internal array and inserts the
+ row into the table view.
+ */
+ func accessoryBrowser(browser: HMAccessoryBrowser, didFindNewAccessory accessory: HMAccessory) {
+ let newAccessory = AccessoryType.HomeKit(accessory: accessory)
+ if displayedAccessories.contains(newAccessory) {
+ return
+ }
+ displayedAccessories.append(newAccessory)
+ displayedAccessories = displayedAccessories.sortByLocalizedName()
+
+ if let newIndex = displayedAccessories.indexOf(newAccessory) {
+ let newIndexPath = NSIndexPath(forRow: newIndex, inSection: 0)
+ tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /**
+ Removes the accessory from the internal array and deletes the
+ row from the table view.
+ */
+ func accessoryBrowser(browser: HMAccessoryBrowser, didRemoveNewAccessory accessory: HMAccessory) {
+ let removedAccessory = AccessoryType.HomeKit(accessory: accessory)
+ if !displayedAccessories.contains(removedAccessory) {
+ return
+ }
+ if let removedIndex = displayedAccessories.indexOf(removedAccessory) {
+ let removedIndexPath = NSIndexPath(forRow: removedIndex, inSection: 0)
+ displayedAccessories.removeAtIndex(removedIndex)
+ tableView.deleteRowsAtIndexPaths([removedIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryUpdateController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryUpdateController.swift
new file mode 100644
index 00000000..6009586b
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryUpdateController.swift
@@ -0,0 +1,110 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `AccessoryUpdateController` manages `CharacteristicCell` updates and buffers them up before sending them to HomeKit.
+*/
+
+import HomeKit
+
+/// An object that responds to `CharacteristicCell` updates and notifies HomeKit of changes.
+class AccessoryUpdateController: NSObject, CharacteristicCellDelegate {
+ // MARK: Properties
+
+ let updateQueue = dispatch_queue_create("com.sample.HMCatalog.CharacteristicUpdateQueue", DISPATCH_QUEUE_SERIAL)
+
+ lazy var pendingWrites = [HMCharacteristic:AnyObject]()
+ lazy var sentWrites = [HMCharacteristic:AnyObject]()
+
+ // Implicitly unwrapped optional because we need `self` to initialize.
+ var updateValueTimer: NSTimer!
+
+ /// Starts the update timer on creation.
+ override init() {
+ super.init()
+ startListeningForCellUpdates()
+ }
+
+ /// Responds to a cell change, and if the update was marked immediate, updates the characteristics.
+ func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) {
+ pendingWrites[characteristic] = value
+ if immediate {
+ updateCharacteristics()
+ }
+ }
+
+ /**
+ Reads the characteristic's value and calls the completion with the characteristic's value.
+
+ If there is a pending write request on the same characteristic, the read is ignored to prevent
+ "UI glitching".
+ */
+ func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) {
+ characteristic.readValueWithCompletionHandler { error in
+ dispatch_sync(self.updateQueue) {
+ if let sentValue = self.sentWrites[characteristic] {
+ completion(sentValue, nil)
+ return
+ }
+
+ dispatch_async(dispatch_get_main_queue()) {
+ completion(characteristic.value, error)
+ }
+ }
+ }
+ }
+
+ /// Creates and starts the update value timer.
+ func startListeningForCellUpdates() {
+ updateValueTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(AccessoryUpdateController.updateCharacteristics), userInfo: nil, repeats: true)
+ }
+
+ /// Invalidates the update timer.
+ func stopListeningForCellUpdates() {
+ updateValueTimer.invalidate()
+ }
+
+ /// Sends all pending requests in the array.
+ func updateCharacteristics() {
+ dispatch_sync(updateQueue) {
+ for (characteristic, value) in self.pendingWrites {
+ self.sentWrites[characteristic] = value
+
+ characteristic.writeValue(value) { error in
+ if let error = error {
+ print("HomeKit: Could not change value: \(error.localizedDescription).")
+ }
+
+ self.didCompleteWrite(characteristic, value: value)
+ }
+ }
+
+ self.pendingWrites.removeAll()
+ }
+ }
+
+ /**
+ Synchronously adds the characteristic-value pair into the `sentWrites` map.
+
+ - parameter characteristic: The `HMCharacteristic` to add.
+ - parameter value: The value of the `characteristic`.
+ */
+ func didSendWrite(characteristic: HMCharacteristic, value: AnyObject) {
+ dispatch_sync(updateQueue) {
+ self.sentWrites[characteristic] = value
+ }
+ }
+
+ /**
+ Synchronously removes the characteristic-value pair from the `sentWrites` map.
+
+ - parameter characteristic: The `HMCharacteristic` to remove.
+ - parameter value: The value of the `characteristic` (unused, but included for clarity).
+ */
+ func didCompleteWrite(characteristic: HMCharacteristic, value: AnyObject) {
+ dispatch_sync(updateQueue) {
+ self.sentWrites.removeValueForKey(characteristic)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsTableViewDataSource.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsTableViewDataSource.swift
new file mode 100644
index 00000000..b41e83ac
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsTableViewDataSource.swift
@@ -0,0 +1,109 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ControlsTableViewDataSource` provides data for the `ControlsViewController`.
+*/
+
+import UIKit
+import HomeKit
+
+/// A `UITableViewDataSource` that populates the table in `ControlsViewController`.
+class ControlsTableViewDataSource: NSObject, UITableViewDataSource {
+ // MARK: Types
+
+ struct Identifiers {
+ static let serviceCell = "ServiceCell"
+ static let unreachableServiceCell = "UnreachableServiceCell"
+ }
+
+ // MARK: Properties
+
+ var serviceTable: [String: [HMService]]?
+ var sortedKeys: [String]?
+
+ let tableView: UITableView
+ var home: HMHome? {
+ return HomeStore.sharedStore.home
+ }
+
+ /// Initializes the table view and data source.
+ required init(tableView: UITableView) {
+ self.tableView = tableView
+ super.init()
+ self.tableView.dataSource = self
+ }
+
+ /**
+ Reloads the table, sets the table's dataSource to self,
+ regenerated the service table, creates a sorted list of keys,
+ sets the home's delegate, and reloads the table.
+ */
+ func reloadTable() {
+ if let home = home {
+ serviceTable = home.serviceTable
+ sortedKeys = serviceTable!.keys.sort()
+ tableView.reloadData()
+ }
+ else {
+ serviceTable = nil
+ sortedKeys = nil
+ }
+
+ tableView.reloadData()
+ }
+
+ /// - returns: The localized description of the service type for that section.
+ func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return sortedKeys?[section]
+ }
+
+ func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return sortedKeys?.count ?? 0
+ }
+
+ /**
+ - returns: A message that corresponds to the current most important reason
+ that there are no services in the table. Either "No Accessories"
+ or "No Services".
+ */
+ func emptyMessage() -> String {
+ if home?.accessories.count == 0 {
+ return NSLocalizedString("No Accessories", comment: "No Accessories")
+ }
+ else {
+ return NSLocalizedString("No Services", comment: "No Services")
+ }
+ }
+
+ /// - returns: The number of services matching the service type in that section.
+ func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return serviceTable![sortedKeys![section]]!.count
+ }
+
+ /// - returns: A `ServiceCell` set for the service at the provided index path.
+ func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let service = serviceForIndexPath(indexPath)!
+
+ let reuseIdentifier = service.accessory!.reachable ? Identifiers.serviceCell : Identifiers.unreachableServiceCell
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! ServiceCell
+
+ cell.service = service
+
+ return cell
+ }
+
+ /// - returns: The service represented at the index path in the table.
+ func serviceForIndexPath(indexPath: NSIndexPath) -> HMService? {
+ if let sortedKeys = sortedKeys,
+ serviceTable = serviceTable,
+ services = serviceTable[sortedKeys[indexPath.section]] {
+ return services[indexPath.row]
+ }
+
+ return nil
+ }
+
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsViewController.swift
new file mode 100644
index 00000000..c00f26f6
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsViewController.swift
@@ -0,0 +1,119 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ControlsViewController` lists services in the selected home.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller which displays a list of `HMServices`, separated by Service Type.
+class ControlsViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let showServiceSegue = "Show Service"
+ }
+
+ // MARK: Properties
+
+ var tableViewDataSource: ControlsTableViewDataSource!
+ var cellController = AccessoryUpdateController()
+
+ @IBOutlet weak var addButton: UIBarButtonItem!
+
+ // MARK: View Methods
+
+ /// Sends the selected service into the destination view controller.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ if segue.identifier == Identifiers.showServiceSegue {
+ if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) {
+ let characteristicsViewController = segue.intendedDestinationViewController as! CharacteristicsViewController
+
+ if let selectedService = tableViewDataSource.serviceForIndexPath(indexPath) {
+ characteristicsViewController.service = selectedService
+ }
+
+ characteristicsViewController.cellDelegate = cellController
+ }
+ }
+ }
+
+ /// Initializes the table view data source.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableViewDataSource = ControlsTableViewDataSource(tableView: tableView)
+ }
+
+ /// Reloads the view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ navigationItem.title = home.name
+ reloadData()
+ }
+
+ // MARK: Helper Methods
+
+ private func reloadData() {
+ tableViewDataSource.reloadTable()
+ let sections = tableViewDataSource.numberOfSectionsInTableView(tableView)
+
+ if sections == 0 {
+ setBackgroundMessage(tableViewDataSource.emptyMessage())
+ }
+ else {
+ setBackgroundMessage(nil)
+ }
+ }
+
+ // MARK: Delegate Registration
+
+ /// Registers as the delegate for the current home and all accessories in the home.
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+ for accessory in home.accessories {
+ accessory.delegate = self
+ }
+ }
+
+ /*
+ Any delegate methods which could change data will reload the
+ table view data source.
+ */
+
+ // MARK: HMHomeDelegate Methods
+
+ func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
+ accessory.delegate = self
+ reloadData()
+ }
+
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ reloadData()
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ func accessoryDidUpdateReachability(accessory: HMAccessory) {
+ reloadData()
+ }
+
+ func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
+ reloadData()
+ }
+
+ func accessory(accessory: HMAccessory, didUpdateAssociatedServiceTypeForService service: HMService) {
+ reloadData()
+ }
+
+ func accessoryDidUpdateServices(accessory: HMAccessory) {
+ reloadData()
+ }
+
+ func accessoryDidUpdateName(accessory: HMAccessory) {
+ reloadData()
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/ModifyAccessoryViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/ModifyAccessoryViewController.swift
new file mode 100644
index 00000000..a43b65ec
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/ModifyAccessoryViewController.swift
@@ -0,0 +1,388 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ModifyAccessoryViewController` allows the user to modify a HomeKit accessory.
+*/
+
+import UIKit
+import HomeKit
+
+/// Represents the sections in the `ModifyAccessoryViewController`.
+enum AddAccessoryTableViewSection: Int {
+ case Name, Rooms, Identify
+
+ static let count = 3
+}
+
+/// Contains a method for notifying the delegate that the accessory was saved.
+protocol ModifyAccessoryDelegate {
+ func accessoryViewController(accessoryViewController: ModifyAccessoryViewController, didSaveAccessory accessory: HMAccessory)
+}
+
+/// A view controller that allows for renaming, reassigning, and identifying accessories before and after they've been added to a home.
+class ModifyAccessoryViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let roomCell = "RoomCell"
+ }
+
+ // MARK: Properties
+
+ // Update this if the acessory failed in any way.
+ private var didEncounterError = false
+
+ private var selectedIndexPath: NSIndexPath?
+ private var selectedRoom: HMRoom!
+
+ @IBOutlet weak var nameField: UITextField!
+ private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
+
+ private let saveAccessoryGroup = dispatch_group_create()
+
+ private var editingExistingAccessory = false
+
+ // Strong reference, because we will replace the button with an activity indicator.
+ @IBOutlet /* strong */ var addButton: UIBarButtonItem!
+ var delegate: ModifyAccessoryDelegate?
+ var rooms = [HMRoom]()
+
+ var accessory: HMAccessory!
+
+ // MARK: View Methods
+
+ /// Configures the table view and initializes view elements.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableView.estimatedRowHeight = 44.0
+ tableView.rowHeight = UITableViewAutomaticDimension
+
+ selectedRoom = accessory.room ?? home.roomForEntireHome()
+
+ // If the accessory belongs to the home already, we are in 'edit' mode.
+ editingExistingAccessory = accessoryHasBeenAddedToHome()
+ if editingExistingAccessory {
+ // Show 'save' instead of 'add.'
+ addButton.title = NSLocalizedString("Save", comment: "Save")
+ }
+ else {
+ /*
+ If we're not editing an existing accessory, then let the back
+ button show in the left.
+ */
+ navigationItem.leftBarButtonItem = nil
+ }
+
+ // Put the accessory's name in the 'name' field.
+ resetNameField()
+
+ // Register a cell for the rooms.
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.roomCell)
+ }
+
+ /**
+ Registers as the delegate for the current home
+ and the accessory.
+ */
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+ accessory.delegate = self
+ }
+
+ /// Replaces the activity indicator with the 'Add' or 'Save' button.
+ func hideActivityIndicator() {
+ activityIndicator.stopAnimating()
+ navigationItem.rightBarButtonItem = addButton
+ }
+
+ /// Temporarily replaces the 'Add' or 'Save' button with an activity indicator.
+ func showActivityIndicator() {
+ navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityIndicator)
+ activityIndicator.startAnimating()
+ }
+
+ /**
+ Called whenever the user taps the 'add' button.
+
+ This method:
+ 1. Adds the accessory to the home, if not already added.
+ 2. Updates the accessory's name, if necessary.
+ 3. Assigns the accessory to the selected room, if necessary.
+ */
+ @IBAction func didTapAddButton() {
+ let name = trimmedName
+ showActivityIndicator()
+
+ if editingExistingAccessory {
+ home(home, assignAccessory: accessory, toRoom: selectedRoom)
+ updateName(name, forAccessory: accessory)
+ }
+ else {
+ dispatch_group_enter(saveAccessoryGroup)
+ home.addAccessory(accessory) { error in
+ if let error = error {
+ self.hideActivityIndicator()
+ self.displayError(error)
+ self.didEncounterError = true
+ }
+ else {
+ // Once it's successfully added to the home, add it to the room that's selected.
+ self.home(self.home, assignAccessory:self.accessory, toRoom: self.selectedRoom)
+ self.updateName(name, forAccessory: self.accessory)
+ }
+ dispatch_group_leave(self.saveAccessoryGroup)
+ }
+ }
+
+ dispatch_group_notify(saveAccessoryGroup, dispatch_get_main_queue()) {
+ self.hideActivityIndicator()
+ if !self.didEncounterError {
+ self.dismiss(nil)
+ }
+ }
+ }
+
+ /**
+ Informs the delegate that the accessory has been saved, and
+ dismisses the view controller.
+ */
+ @IBAction func dismiss(sender: AnyObject?) {
+ delegate?.accessoryViewController(self, didSaveAccessory: accessory)
+ if editingExistingAccessory {
+ presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
+ }
+ else {
+ navigationController?.popViewControllerAnimated(true)
+ }
+ }
+
+ /**
+ - returns: `true` if the accessory has already been added to
+ the home; `false` otherwise.
+ */
+ func accessoryHasBeenAddedToHome() -> Bool {
+ return home.accessories.contains(accessory)
+ }
+
+ /**
+ Updates the accessories name. This function will enter and leave the saved dispatch group.
+ If the accessory's name is already equal to the passed-in name, this method does nothing.
+
+ - parameter name: The new name for the accessory.
+ - parameter accessory: The accessory to rename.
+ */
+ func updateName(name: String, forAccessory accessory: HMAccessory) {
+ if accessory.name == name {
+ return
+ }
+ dispatch_group_enter(saveAccessoryGroup)
+ accessory.updateName(name) { error in
+ if let error = error {
+ self.displayError(error)
+ self.didEncounterError = true
+ }
+ dispatch_group_leave(self.saveAccessoryGroup)
+ }
+ }
+
+ /**
+ Assigns the given accessory to the provided room. This method will enter and leave the saved dispatch group.
+
+ - parameter home: The home to assign.
+ - parameter accessory: The accessory to be assigned.
+ - parameter room: The room to which to assign the accessory.
+ */
+ func home(home: HMHome, assignAccessory accessory: HMAccessory, toRoom room: HMRoom) {
+ if accessory.room == room {
+ return
+ }
+ dispatch_group_enter(saveAccessoryGroup)
+ home.assignAccessory(accessory, toRoom: room) { error in
+ if let error = error {
+ self.displayError(error)
+ self.didEncounterError = true
+ }
+ dispatch_group_leave(self.saveAccessoryGroup)
+ }
+ }
+
+ /// Tells the current accessory to identify itself.
+ func identifyAccessory() {
+ accessory.identifyWithCompletionHandler { error in
+ if let error = error {
+ self.displayError(error)
+ }
+ }
+ }
+
+ /// Enables the name field if the accessory's name changes.
+ func resetNameField() {
+ var action: String
+ if editingExistingAccessory {
+ action = NSLocalizedString("Edit %@", comment: "Edit Accessory")
+ }
+ else {
+ action = NSLocalizedString("Add %@", comment: "Add Accessory")
+ }
+ navigationItem.title = NSString(format: action, accessory.name) as String
+ nameField.text = accessory.name
+ nameField.enabled = home.isAdmin
+ enableAddButtonIfApplicable()
+ }
+
+ /// Enables the save button if the name field is not empty.
+ func enableAddButtonIfApplicable() {
+ addButton.enabled = home.isAdmin && trimmedName.characters.count > 0
+ }
+
+ /// - returns: The `nameField`'s text, trimmed of newline and whitespace characters.
+ var trimmedName: String {
+ return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
+ }
+
+ /// Enables or disables the add button.
+ @IBAction func nameFieldDidChange(sender: AnyObject) {
+ enableAddButtonIfApplicable()
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of `AddAccessoryTableViewSection`s.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return AddAccessoryTableViewSection.count
+ }
+
+ /// - returns: The number rows for the rooms section. All other sections are static.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch AddAccessoryTableViewSection(rawValue: section) {
+ case .Rooms?:
+ return home.allRooms.count
+
+ case nil:
+ fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, numberOfRowsInSection: section)
+ }
+ }
+
+ /// - returns: `UITableViewAutomaticDimension` for dynamic cell, super otherwise.
+ override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
+ switch AddAccessoryTableViewSection(rawValue: indexPath.section) {
+ case .Rooms?:
+ return UITableViewAutomaticDimension
+
+ case nil:
+ fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /// - returns: A 'room cell' for the rooms section, super otherwise.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch AddAccessoryTableViewSection(rawValue: indexPath.section) {
+ case .Rooms?:
+ return self.tableView(tableView, roomCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Creates a cell with the name of each room within the home, displaying a checkmark if the room
+ is the currently selected room.
+ */
+ func tableView(tableView: UITableView, roomCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath)
+ let room = home.allRooms[indexPath.row] as HMRoom
+
+ cell.textLabel?.text = home.nameForRoom(room)
+
+ // Put a checkmark on the selected room.
+ cell.accessoryType = room == selectedRoom ? .Checkmark : .None
+ if !home.isAdmin {
+ cell.selectionStyle = .None
+ }
+ return cell
+ }
+
+
+ /// Handles row selection based on the section.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ switch AddAccessoryTableViewSection(rawValue: indexPath.section) {
+ case .Rooms?:
+ guard home.isAdmin else { return }
+
+ selectedRoom = home.allRooms[indexPath.row]
+
+ let sections = NSIndexSet(index: AddAccessoryTableViewSection.Rooms.rawValue)
+
+ tableView.reloadSections(sections, withRowAnimation: .Automatic)
+
+ case .Identify?:
+ identifyAccessory()
+
+ case nil:
+ fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
+
+ default: break
+ }
+ }
+
+ /// Required override.
+ override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
+ return super.tableView(tableView, indentationLevelForRowAtIndexPath: NSIndexPath(forRow: 0, inSection: indexPath.section))
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ // All home changes reload the view.
+
+ func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
+ tableView.reloadData()
+ }
+
+ func home(home: HMHome, didAddRoom room: HMRoom) {
+ tableView.reloadData()
+ }
+
+ func home(home: HMHome, didRemoveRoom room: HMRoom) {
+ if selectedRoom == room {
+ // Reset the selected room if ours was deleted.
+ selectedRoom = homeStore.home!.roomForEntireHome()
+ }
+ tableView.reloadData()
+ }
+
+ func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
+ /*
+ Bridged accessories don't call the original completion handler if their
+ bridges are added to the home. We must respond to `HMHomeDelegate`'s
+ `home(_:didAddAccessory:)` and assign bridged accessories properly.
+ */
+ if selectedRoom != nil {
+ self.home(home, assignAccessory: accessory, toRoom: selectedRoom)
+ }
+ }
+
+ func home(home: HMHome, didUnblockAccessory accessory: HMAccessory) {
+ tableView.reloadData()
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ /// If the accessory's name changes, we update the name field.
+ func accessoryDidUpdateName(accessory: HMAccessory) {
+ resetNameField()
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.swift
new file mode 100644
index 00000000..eb9e5641
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.swift
@@ -0,0 +1,182 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ `CharacteristicCell` is a superclass which represents the state of a HomeKit characteristic.
+*/
+
+import UIKit
+import HomeKit
+
+/// Methods for handling cell reads and updates.
+protocol CharacteristicCellDelegate {
+
+ /**
+ Called whenever the control within the cell updates its value.
+
+ - parameter cell: The cell which has updated its value.
+ - parameter newValue: The new value represented by the cell's control.
+ - parameter characteristic: The characteristic the cell represents.
+ - parameter immediate: Whether or not to update external values immediately.
+
+ For example, Slider cells should not update immediately upon value change,
+ so their values are cached and updates are coalesced. Subclasses can decide
+ whether or not their values are meant to be updated immediately.
+ */
+ func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool)
+
+ /**
+ Called when the characteristic cell needs to reload its value from an external source.
+ Consider using this call to look up values in memory or query them from an accessory.
+
+ - parameter cell: The cell requesting a value update.
+ - parameter characteristic: The characteristic for whose value the cell is asking.
+ - parameter completion: The closure that the cell provides to be called when values have been read successfully.
+ */
+ func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void)
+}
+
+/**
+ A `UITableViewCell` subclass that displays the current value of an `HMCharacteristic` and
+ notifies its delegate of changes. Subclasses of this class will provide additional controls
+ to display different kinds of data.
+*/
+class CharacteristicCell: UITableViewCell {
+ /// An alpha percentage used when disabling cells.
+ static let DisabledAlpha: CGFloat = 0.4
+
+ /// Required init.
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+
+ /// Subclasses can return false if they have many frequent updates that should be deferred.
+ class var updatesImmediately: Bool {
+ return true
+ }
+
+ // MARK: Properties
+
+ @IBOutlet weak var typeLabel: UILabel!
+ @IBOutlet weak var valueLabel: UILabel!
+ @IBOutlet weak var favoriteButton: UIButton!
+
+ @IBOutlet weak var favoriteButtonWidthConstraint: NSLayoutConstraint!
+ @IBOutlet weak var favoriteButtonHeightContraint: NSLayoutConstraint!
+
+ /**
+ Show / hide the favoriteButton and adjust the constraints
+ to ensure proper layout.
+ */
+ var showsFavorites = false {
+ didSet {
+ if showsFavorites {
+ favoriteButton.hidden = false
+ favoriteButtonWidthConstraint.constant = favoriteButtonHeightContraint.constant
+ }
+ else {
+ favoriteButton.hidden = true
+ favoriteButtonWidthConstraint.constant = 15.0
+ }
+ }
+ }
+
+ /**
+ - returns: `true` if the represented characteristic is reachable;
+ `false` otherwise.
+ */
+ var enabled: Bool {
+ return (characteristic.service?.accessory?.reachable ?? false)
+ }
+
+ /**
+ The value currently represented by the cell.
+
+ This is not necessarily the value of this cell's characteristic,
+ because the cell's value changes independently of the characteristic.
+ */
+ var value: AnyObject?
+
+ /// The delegate that will respond to cell value changes.
+ var delegate: CharacteristicCellDelegate?
+
+ /**
+ The characteristic represented by this cell.
+
+ When this is set, the cell populates based on
+ the characteristic's value and requests an initial value
+ from its delegate.
+ */
+ var characteristic: HMCharacteristic! {
+ didSet {
+ typeLabel.text = characteristic.localizedCharacteristicType
+
+ selectionStyle = characteristic.isIdentify ? .Default : .None
+
+ setValue(characteristic.value, notify: false)
+
+ if characteristic.isWriteOnly {
+ // Don't read the value for write-only characteristics.
+ return
+ }
+
+ // Set initial state of the favorite button
+ favoriteButton.selected = characteristic.isFavorite
+
+ // "Enable" the cell if the accessory is reachable or we are displaying the favorites.
+
+ // Configure the views.
+ typeLabel.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
+ valueLabel?.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
+
+ if enabled {
+ delegate?.characteristicCell(self, readInitialValueForCharacteristic: characteristic) { value, error in
+ if let error = error {
+ print("HomeKit: Error reading value for characteristic \(self.characteristic): \(error.localizedDescription).")
+ }
+ else {
+ self.setValue(value, notify: false)
+ }
+ }
+ }
+
+ }
+ }
+
+ /// Resets the value label to the localized description from HMCharacteristic+Readability.
+ func resetValueLabel() {
+ if let value = value {
+ valueLabel?.text = characteristic.localizedDescriptionForValue(value)
+ }
+ }
+
+ /**
+ Toggles the star button and saves the favorite status
+ of the characteristic in the FavoriteManager.
+ */
+ @IBAction func didTapFavoriteButton(sender: UIButton) {
+ sender.selected = !sender.selected
+ characteristic.isFavorite = sender.selected
+ }
+
+ /**
+ Sets the cell's value and resets the label.
+
+ - parameter newValue: The new value.
+ - parameter notify: If true, the cell notifies its delegate of the change.
+ */
+ func setValue(newValue: AnyObject?, notify: Bool) {
+ value = newValue
+ if let newValue = newValue {
+ resetValueLabel()
+ /*
+ We do not allow the setting of nil values from the app,
+ but we do have to deal with incoming nil values.
+ */
+ if notify {
+ delegate?.characteristicCell(self, didUpdateValue: newValue, forCharacteristic: characteristic, immediate: self.dynamicType.updatesImmediately)
+ }
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.xib
new file mode 100644
index 00000000..de349de2
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.xib
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SegmentedControlCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SegmentedControlCharacteristicCell.swift
new file mode 100644
index 00000000..cac8b6e9
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SegmentedControlCharacteristicCell.swift
@@ -0,0 +1,86 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `SegmentedControlCharacteristicCell` displays characteristics with associated values.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ A `CharacteristicCell` subclass that contains a `UISegmentedControl`.
+
+ Used for `HMCharacteristic`s which have associated, non-numeric values, like Lock Management State.
+*/
+class SegmentedControlCharacteristicCell: CharacteristicCell {
+ // MARK: Properties
+
+ @IBOutlet weak var segmentedControl: UISegmentedControl!
+
+ /**
+ Calls the super class's didSet, and also computes a list
+ of possible values.
+ */
+ override var characteristic: HMCharacteristic! {
+ didSet {
+ segmentedControl.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
+ segmentedControl.userInteractionEnabled = enabled
+
+ if let values = characteristic.allPossibleValues as? [Int] {
+ possibleValues = values
+ }
+ }
+ }
+
+ /**
+ The possible values for this characteristic.
+ When this is set, adds localized descriptions to the segmented control.
+ */
+ var possibleValues = [Int]() {
+ didSet {
+ segmentedControl.removeAllSegments()
+ for index in 0..
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.swift
new file mode 100644
index 00000000..bfd68d65
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.swift
@@ -0,0 +1,77 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `SliderCharacteristicCell` displays characteristics with a continuous range of options.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ A `CharacteristicCell` subclass that contains a slider.
+ Used for numeric characteristics that have a continuous range of options.
+*/
+class SliderCharacteristicCell: CharacteristicCell {
+ // MARK: Properties
+
+ @IBOutlet weak var valueSlider: UISlider!
+
+ override var characteristic: HMCharacteristic! {
+ didSet {
+ valueSlider.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
+ valueSlider.userInteractionEnabled = enabled
+ }
+
+ willSet(newCharacteristic) {
+ // These are sane defaults in case the max and min are not set.
+ valueSlider.minimumValue = newCharacteristic.metadata?.minimumValue as? Float ?? 0.0
+ valueSlider.maximumValue = newCharacteristic.metadata?.maximumValue as? Float ?? 100.0
+ }
+ }
+
+ /// If notify is false, sets the valueSlider's represented value.
+ override func setValue(newValue: AnyObject?, notify: Bool) {
+ super.setValue(newValue, notify: notify)
+ if let newValue = newValue as? NSNumber where !notify {
+ valueSlider.value = newValue.floatValue
+ }
+ }
+
+ /**
+ Restricts a value to the step value provided in the cell's
+ characteristic's metadata.
+
+ - parameter sliderValue: The provided value.
+
+ - returns: The value adjusted to align with a step value.
+ */
+ func roundedValueForSliderValue(value: Float) -> Float {
+ if let metadata = characteristic.metadata,
+ stepValue = metadata.stepValue as? Float
+ where stepValue > 0 {
+ let newStep = roundf(value / stepValue)
+ let stepped = newStep * stepValue
+ return stepped
+ }
+
+ return value
+ }
+
+ // Sliders don't update immediately, because sliders generate many updates.
+ override class var updatesImmediately: Bool {
+ return false
+ }
+
+ /**
+ Responds to a slider change and sets the cell's value.
+
+ - parameter slider: The slider that changed.
+ */
+ func didChangeSliderValue(slider: UISlider) {
+ let value = roundedValueForSliderValue(slider.value)
+ setValue(value, notify: true)
+ }
+
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.xib
new file mode 100644
index 00000000..9caf5733
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.xib
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.swift
new file mode 100644
index 00000000..5bde39b6
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.swift
@@ -0,0 +1,49 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `SwitchCharacteristicCell` displays Boolean characteristics.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ A `CharacteristicCell` subclass that contains a single switch.
+ Used for Boolean characteristics.
+*/
+class SwitchCharacteristicCell: CharacteristicCell {
+ // MARK: Properties
+
+ @IBOutlet weak var valueSwitch: UISwitch!
+
+ override var characteristic: HMCharacteristic! {
+ didSet {
+ valueSwitch.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
+ valueSwitch.userInteractionEnabled = enabled
+ }
+ }
+
+ /// If notify is false, sets the switch to the value.
+ override func setValue(newValue: AnyObject?, notify: Bool) {
+ super.setValue(newValue, notify: notify)
+
+ if !notify {
+ if let boolValue = newValue as? Bool {
+ valueSwitch.setOn(boolValue, animated: true)
+ }
+ }
+ }
+
+ /**
+ Responds to the switch updating and sets the
+ value to the switch's value.
+
+ - parameter valueSwitch: The switch that updated.
+ */
+ func didChangeSwitchValue(valueSwitch: UISwitch) {
+ setValue(valueSwitch.on, notify: true)
+ }
+
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.xib
new file mode 100644
index 00000000..04b433d0
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.xib
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.swift
new file mode 100644
index 00000000..ab84aa86
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.swift
@@ -0,0 +1,48 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TextCharacteristicCell` represents text-input characteristics.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ A `CharacteristicCell` subclass that contains a text field.
+ Used for text-input characteristics.
+*/
+class TextCharacteristicCell: CharacteristicCell, UITextFieldDelegate {
+ // MARK: Properties
+
+ @IBOutlet weak var textField: UITextField!
+
+ override var characteristic: HMCharacteristic! {
+ didSet {
+ textField.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
+ textField.userInteractionEnabled = enabled
+ }
+ }
+
+ /// If notify is false, sets the text field's text from the value.
+ override func setValue(newValue: AnyObject?, notify: Bool) {
+ super.setValue(newValue, notify: notify)
+ if !notify {
+ if let newStringValue = newValue as? String {
+ textField.text = newStringValue
+ }
+ }
+ }
+
+ /// Dismiss the keyboard when "Go" is clicked
+ func textFieldShouldReturn(textField: UITextField) -> Bool {
+ textField.resignFirstResponder()
+ return true
+ }
+
+ /// Sets the value of the characteristic when editing is complete.
+ func textFieldDidEndEditing(textField: UITextField) {
+ setValue(textField.text, notify: true)
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.xib
new file mode 100644
index 00000000..743a3f89
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.xib
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsTableViewDataSource.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsTableViewDataSource.swift
new file mode 100644
index 00000000..a3a3ecc9
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsTableViewDataSource.swift
@@ -0,0 +1,207 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `CharacteristicsTableViewDataSource` provides the data for the `CharacteristicsViewController`.
+*/
+
+import UIKit
+import HomeKit
+
+/// Represents the sections in the `CharacteristicsViewController`.
+enum CharacteristicTableViewSection: Int {
+ case Characteristics, AssociatedServiceType
+}
+
+/// A `UITableViewDataSource` that populates a `CharacteristicsViewController`.
+class CharacteristicsTableViewDataSource: NSObject, UITableViewDelegate, UITableViewDataSource {
+ // MARK: Types
+
+ struct Identifiers {
+ static let characteristicCell = "CharacteristicCell"
+ static let sliderCharacteristicCell = "SliderCharacteristicCell"
+ static let switchCharacteristicCell = "SwitchCharacteristicCell"
+ static let segmentedControlCharacteristicCell = "SegmentedControlCharacteristicCell"
+ static let textCharacteristicCell = "TextCharacteristicCell"
+ static let serviceTypeCell = "ServiceTypeCell"
+ }
+
+ // MARK: Properties
+
+ var service: HMService
+ var tableView: UITableView
+ var delegate: CharacteristicCellDelegate
+ var showsFavorites: Bool
+ var allowsAllWrites: Bool
+
+ /// Sets up properties from specified values, configures the table view, and cell reuse identifiers.
+ required init(service: HMService, tableView: UITableView, delegate: CharacteristicCellDelegate, showsFavorites: Bool = false, allowsAllWrites: Bool = false) {
+ self.service = service
+ self.tableView = tableView
+ self.delegate = delegate
+ self.showsFavorites = showsFavorites
+ self.allowsAllWrites = allowsAllWrites
+ super.init()
+ tableView.dataSource = self
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 50.0
+ registerReuseIdentifiers()
+ }
+
+ /// Registers all of the characteristic cell reuse identifiers with this table.
+ func registerReuseIdentifiers() {
+ let characteristicNib = UINib(nibName: Identifiers.characteristicCell, bundle: nil)
+ tableView.registerNib(characteristicNib, forCellReuseIdentifier: Identifiers.characteristicCell)
+
+ let sliderNib = UINib(nibName: Identifiers.sliderCharacteristicCell, bundle: nil)
+ tableView.registerNib(sliderNib, forCellReuseIdentifier: Identifiers.sliderCharacteristicCell)
+
+ let switchNib = UINib(nibName: Identifiers.switchCharacteristicCell, bundle: nil)
+ tableView.registerNib(switchNib, forCellReuseIdentifier: Identifiers.switchCharacteristicCell)
+
+ let segmentedNib = UINib(nibName: Identifiers.segmentedControlCharacteristicCell, bundle: nil)
+ tableView.registerNib(segmentedNib, forCellReuseIdentifier: Identifiers.segmentedControlCharacteristicCell)
+
+ let textNib = UINib(nibName: Identifiers.textCharacteristicCell, bundle: nil)
+ tableView.registerNib(textNib, forCellReuseIdentifier: Identifiers.textCharacteristicCell)
+
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.serviceTypeCell)
+ }
+
+ /**
+ - returns: The number of sections, computed from whether or not
+ the services supports an associated service type.
+ */
+ func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return service.supportsAssociatedServiceType ? 2 : 1
+ }
+
+ /**
+ The characteristics section uses the services count to generate the number of rows.
+ The associated service type uses the valid associated service types.
+ */
+ func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch CharacteristicTableViewSection(rawValue: section) {
+ case .Characteristics?:
+ return service.characteristics.count
+
+ case .AssociatedServiceType?:
+ // For 'None'.
+ return HMService.validAssociatedServiceTypes.count + 1
+
+ case nil:
+ fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
+ }
+ }
+
+ /**
+ Looks up the appropriate service type for the row in the list and returns a localized version,
+ or 'None' if the row doesn't correspond to any valid service type.
+
+ - parameter row: The row to look up.
+
+ - returns: The localized service type in that row, or 'None'.
+ */
+ func displayedServiceTypeForRow(row: Int) -> String {
+ let serviceTypes = HMService.validAssociatedServiceTypes
+ if row < serviceTypes.count {
+ return HMService.localizedDescriptionForServiceType(serviceTypes[row])
+ }
+
+ return NSLocalizedString("None", comment: "None")
+ }
+
+ /**
+ Evaluates whether or not a service type is selected for a given row.
+
+ - parameter row: The selected row.
+
+ - returns: `true` if the current row is a valid service type, `false` otherwise
+ */
+ func serviceTypeIsSelectedForRow(row: Int) -> Bool {
+ let serviceTypes = HMService.validAssociatedServiceTypes
+ if row >= serviceTypes.count {
+ return service.associatedServiceType == nil
+ }
+
+ if let associatedServiceType = service.associatedServiceType {
+ return serviceTypes[row] == associatedServiceType
+ }
+
+ return false
+ }
+
+ /// Generates a cell for an associated service.
+ private func tableView(tableView: UITableView, associatedServiceTypeCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceTypeCell, forIndexPath: indexPath)
+
+ cell.textLabel?.text = displayedServiceTypeForRow(indexPath.row)
+ cell.accessoryType = serviceTypeIsSelectedForRow(indexPath.row) ? .Checkmark : .None
+
+ return cell
+ }
+
+ /**
+ Generates a characteristic cell based on the type of characteristic
+ located at the specified index path.
+ */
+ private func tableView(tableView: UITableView, characteristicCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let characteristic = service.characteristics[indexPath.row]
+
+ var reuseIdentifier = Identifiers.characteristicCell
+
+ if (characteristic.isReadOnly || characteristic.isWriteOnly) && !allowsAllWrites {
+ reuseIdentifier = Identifiers.characteristicCell
+ }
+ else if characteristic.isBoolean {
+ reuseIdentifier = Identifiers.switchCharacteristicCell
+ }
+ else if characteristic.hasPredeterminedValueDescriptions {
+ reuseIdentifier = Identifiers.segmentedControlCharacteristicCell
+ }
+ else if characteristic.isNumeric {
+ reuseIdentifier = Identifiers.sliderCharacteristicCell
+ }
+ else if characteristic.isTextWritable {
+ reuseIdentifier = Identifiers.textCharacteristicCell
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CharacteristicCell
+
+ cell.showsFavorites = showsFavorites
+ cell.delegate = delegate
+ cell.characteristic = characteristic
+
+ return cell
+ }
+
+ /// Uses convenience methods to generate a cell based on the index path's section.
+ func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch CharacteristicTableViewSection(rawValue: indexPath.section) {
+ case .Characteristics?:
+ return self.tableView(tableView, characteristicCellForRowAtIndexPath: indexPath)
+
+ case .AssociatedServiceType?:
+ return self.tableView(tableView, associatedServiceTypeCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
+ }
+ }
+
+ /// - returns: A localized string for the section.
+ func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch CharacteristicTableViewSection(rawValue: section) {
+ case .Characteristics?:
+ return NSLocalizedString("Characteristics", comment: "Characteristics")
+
+ case .AssociatedServiceType?:
+ return NSLocalizedString("Associated Service Type", comment: "Associated Service Type")
+
+ case nil:
+ fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
+ }
+ }
+
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsViewController.swift
new file mode 100644
index 00000000..14045ae6
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsViewController.swift
@@ -0,0 +1,167 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `CharacteristicsViewController` displays characteristics within a service.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller that displays a list of characteristics within an `HMService`.
+class CharacteristicsViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Properties
+
+ var service: HMService!
+ var cellDelegate: CharacteristicCellDelegate!
+ private var tableViewDataSource: CharacteristicsTableViewDataSource!
+ var showsFavorites = false
+ var allowsAllWrites = false
+
+ // MARK: View Methods
+
+ /// Initializes the data source.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableViewDataSource = CharacteristicsTableViewDataSource(service: service, tableView: tableView, delegate: cellDelegate, showsFavorites: showsFavorites, allowsAllWrites: allowsAllWrites)
+ }
+
+ /// Reloads the view and enabled notifications for all relevant characteristics.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ title = service.name
+ setNotificationsEnabled(true)
+ reloadTableView()
+ }
+
+ /// Disables notifications for characteristics.
+ override func viewWillDisappear(animated: Bool) {
+ super.viewWillDisappear(animated)
+ setNotificationsEnabled(false)
+ }
+
+ /**
+ Registers as the delegate for the current home and
+ the service's accessory.
+ */
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+ service.accessory?.delegate = self
+ }
+
+ /**
+ Enables or disables notifications on all characteristics within this service.
+
+ - parameter notificationsEnabled: A `Bool`; whether to enable or disable.
+ */
+ func setNotificationsEnabled(notificationsEnabled: Bool) {
+ for characteristic in service.characteristics {
+ if characteristic.supportsEventNotification {
+ characteristic.enableNotification(notificationsEnabled) { error in
+ if let error = error {
+ print("HomeKit: Error enabling notification on charcteristic '\(characteristic)': \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+ }
+
+ /// Reloads the table view and stops the refresh control.
+ func reloadTableView() {
+ setNotificationsEnabled(true)
+ tableViewDataSource.service = service
+ refreshControl?.endRefreshing()
+ tableView.reloadData()
+ }
+
+ // MARK: Table View Methods
+
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ switch CharacteristicTableViewSection(rawValue: indexPath.section) {
+ case .Characteristics?:
+ let characteristic = service.characteristics[indexPath.row]
+ didSelectCharacteristic(characteristic, atIndexPath: indexPath)
+
+ case .AssociatedServiceType?:
+ didSelectAssociatedServiceTypeAtIndexPath(indexPath)
+
+ case nil:
+ fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
+ }
+ }
+
+ /**
+ If a characteristic is selected, and it is the 'Identify' characteristic,
+ perform an identify on that accessory.
+ */
+ private func didSelectCharacteristic(characteristic: HMCharacteristic, atIndexPath indexPath: NSIndexPath) {
+ if characteristic.isIdentify {
+ service.accessory?.identifyWithCompletionHandler { error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+ }
+ }
+ }
+
+ /**
+ Handles selection of one of the associated service types in the list.
+
+ - parameter indexPath: The selected index path.
+ */
+ private func didSelectAssociatedServiceTypeAtIndexPath(indexPath: NSIndexPath) {
+ let serviceTypes = HMService.validAssociatedServiceTypes
+ var newServiceType: String?
+ if indexPath.row < serviceTypes.count {
+ newServiceType = serviceTypes[indexPath.row]
+ }
+ service.updateAssociatedServiceType(newServiceType) { error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+
+ self.didUpdateAssociatedServiceType()
+ }
+ }
+
+ /// Reloads the associated service section in the table view.
+ private func didUpdateAssociatedServiceType() {
+ let associatedServiceTypeIndexSet = NSIndexSet(index: CharacteristicTableViewSection.AssociatedServiceType.rawValue)
+
+ tableView.reloadSections(associatedServiceTypeIndexSet, withRowAnimation: .Automatic)
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// If our accessory was removed, pop to root view controller.
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ if accessory == service.accessory {
+ navigationController?.popToRootViewControllerAnimated(true)
+ }
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ /// If our accessory becomes unreachable, pop to root view controller.
+ func accessoryDidUpdateReachability(accessory: HMAccessory) {
+ if accessory == service.accessory && !accessory.reachable {
+ navigationController?.popToRootViewControllerAnimated(true)
+ }
+ }
+
+ /**
+ Search for the cell corresponding to that characteristic and
+ update its value.
+ */
+ func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) {
+ if let index = service.characteristics.indexOf(characteristic) {
+ let indexPath = NSIndexPath(forRow: index, inSection: 0)
+ let cell = tableView.cellForRowAtIndexPath(indexPath) as! CharacteristicCell
+ cell.setValue(characteristic.value, notify: false)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServiceCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServiceCell.swift
new file mode 100644
index 00000000..edb5c6ec
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServiceCell.swift
@@ -0,0 +1,47 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ServiceCell` displays a service and information about it.
+*/
+
+import UIKit
+import HomeKit
+
+/// A `UITableViewCell` subclass for displaying a service and the room and accessory where it resides.
+class ServiceCell: UITableViewCell {
+
+ // MARK: Properties
+ var includeAccessoryText = true
+
+ /// Required init.
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+
+ /**
+ The cell's service.
+
+ When the service is set, the cell's `textLabel` will contain the service's name
+ or the accessory's name if the service has no name.
+ The detail text will contain information about where this service lives.
+ */
+ var service: HMService? {
+ didSet {
+ if let service = service,
+ accessory = service.accessory {
+ textLabel?.text = service.name ?? accessory.name
+ let accessoryName = accessory.name
+ let roomName = accessory.room!.name
+ if includeAccessoryText {
+ let inIdentifier = NSLocalizedString("%@ in %@", comment: "Accessory in Room")
+ detailTextLabel?.text = String(format: inIdentifier, accessoryName, roomName)
+ }
+ else {
+ detailTextLabel?.text = ""
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServicesViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServicesViewController.swift
new file mode 100644
index 00000000..d12a706f
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServicesViewController.swift
@@ -0,0 +1,259 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ServicesViewController` displays an accessory's services.
+*/
+
+import UIKit
+import HomeKit
+
+/// Represents the sections in the `ServicesViewController`.
+enum AccessoryTableViewSection: Int {
+ case Services, BridgedAccessories
+}
+
+/**
+ A view controller which displays all the services of a provided accessory, and
+ passes its cell delegate onto a `CharacteristicsViewController`.
+*/
+class ServicesViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let accessoryCell = "AccessoryCell"
+ static let serviceCell = "ServiceCell"
+ static let showServiceSegue = "Show Service"
+ }
+
+ // MARK: Properties
+
+ var accessory: HMAccessory!
+ lazy var cellDelegate: CharacteristicCellDelegate = AccessoryUpdateController()
+ var showsFavorites = false
+ var allowsAllWrites = false
+ var onlyShowsControlServices = false
+ var displayedServices = [HMService]()
+ var bridgedAccessories = [HMAccessory]()
+
+ // MARK: View Methods
+
+ /// Configures table view.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.estimatedRowHeight = 44.0
+ tableView.rowHeight = UITableViewAutomaticDimension
+ }
+
+ /// Reloads the view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ updateTitle()
+ reloadData()
+ }
+
+ /// Pops the view controller, if required.
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+ if shouldPopViewController() {
+ navigationController?.popToRootViewControllerAnimated(true)
+ }
+ }
+
+ /**
+ Passes the `CharacteristicsViewController` the service from the cell and
+ configures the view controller.
+ */
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+
+ guard segue.identifier == Identifiers.showServiceSegue else { return }
+
+ if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) {
+ let selectedService = displayedServices[indexPath.row]
+ let characteristicsViewController = segue.intendedDestinationViewController as! CharacteristicsViewController
+ characteristicsViewController.showsFavorites = showsFavorites
+ characteristicsViewController.allowsAllWrites = allowsAllWrites
+ characteristicsViewController.service = selectedService
+ characteristicsViewController.cellDelegate = cellDelegate
+ }
+ }
+
+ /**
+ - returns: `true` if our accessory is no longer in the
+ current home's list of accessories.
+ */
+ private func shouldPopViewController() -> Bool {
+ for accessory in homeStore.home!.accessories {
+ if accessory == accessory {
+ return false
+ }
+ }
+ return true
+ }
+
+ // MARK: Delegate Registration
+
+ /**
+ Registers as the delegate for the current home
+ and for the current accessory.
+ */
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+ accessory.delegate = self
+ }
+
+ // MARK: Table View Methods
+
+ /// Two sections if we're showing bridged accessories.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ if accessory.uniqueIdentifiersForBridgedAccessories != nil {
+ return 2
+ }
+ return 1
+ }
+
+ /**
+ Section 1 contains the services within the accessory.
+ Section 2 contains the bridged accessories.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch AccessoryTableViewSection(rawValue: section) {
+ case .Services?:
+ return displayedServices.count
+
+ case .BridgedAccessories?:
+ return bridgedAccessories.count
+
+ case nil:
+ fatalError("Unexpected `AccessoryTableViewSection` raw value.")
+ }
+ }
+
+ /**
+ - returns: A Service or Bridged Accessory Cell based
+ on the section.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch AccessoryTableViewSection(rawValue: indexPath.section) {
+ case .Services?:
+ return self.tableView(tableView, serviceCellForRowAtIndexPath: indexPath)
+
+ case .BridgedAccessories?:
+ return self.tableView(tableView, bridgedAccessoryCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `AccessoryTableViewSection` raw value.")
+ }
+ }
+
+ /**
+ - returns: A cell containing the name of a bridged
+ accessory at a given index path.
+ */
+ func tableView(tableView: UITableView, bridgedAccessoryCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.accessoryCell, forIndexPath: indexPath)
+ let accessory = bridgedAccessories[indexPath.row]
+ cell.textLabel?.text = accessory.name
+ return cell
+ }
+
+ /**
+ - returns: A cell containing the name of a service at
+ a given index path, as well as a localized
+ description of its service type.
+ */
+ func tableView(tableView: UITableView, serviceCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath)
+ let service = displayedServices[indexPath.row]
+
+ // Inherit the name from the accessory if the Service doesn't have one.
+ cell.textLabel?.text = service.name ?? service.accessory?.name
+ cell.detailTextLabel?.text = service.localizedDescription
+ return cell
+ }
+
+ /// - returns: A title string for the section.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch AccessoryTableViewSection(rawValue: section) {
+ case .Services?:
+ return NSLocalizedString("Services", comment: "Services")
+
+ case .BridgedAccessories?:
+ return NSLocalizedString("Bridged Accessories", comment: "Bridged Accessories")
+
+ case nil:
+ fatalError("Unexpected `AccessoryTableViewSection` raw value.")
+ }
+ }
+
+ /// - returns: A localized description of the accessories bridged status.
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ if accessory.bridged && AccessoryTableViewSection(rawValue: section)! == .Services {
+ let formatString = NSLocalizedString("This accessory is being bridged into HomeKit by %@.", comment: "Bridge Description")
+ if let bridge = home.bridgeForAccessory(accessory) {
+ return String(format: formatString, bridge.name)
+ }
+ else {
+ return NSLocalizedString("This accessory is being bridged into HomeKit.", comment: "Bridge Description Without Bridge")
+ }
+ }
+ return nil
+ }
+
+ // MARK: Helper Methods
+
+ /// Updates the navigation bar's title.
+ func updateTitle() {
+ navigationItem.title = accessory.name
+ }
+
+ /**
+ Updates the title, resets the displayed services based on
+ view controller configurations, reloads the bridge accessory
+ array and reloads the table view.
+ */
+ private func reloadData() {
+ displayedServices = accessory.services.sortByLocalizedName()
+ if onlyShowsControlServices {
+ // We are configured to only show control services, filter the array.
+ displayedServices = displayedServices.filter { service -> Bool in
+ return service.isControlType
+ }
+ }
+
+ if let identifiers = accessory.uniqueIdentifiersForBridgedAccessories {
+ bridgedAccessories = home.accessoriesWithIdentifiers(identifiers).sortByLocalizedName()
+ }
+ tableView.reloadData()
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ /// Reloads the title based on the accessories new name.
+ func accessoryDidUpdateName(accessory: HMAccessory) {
+ updateTitle()
+ }
+
+ /// Reloads the cell for the specified service.
+ func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
+ if let index = displayedServices.indexOf(service) {
+ let path = NSIndexPath(forRow: index, inSection: AccessoryTableViewSection.Services.rawValue)
+ tableView.reloadRowsAtIndexPaths([path], withRowAnimation: .Automatic)
+ }
+ }
+
+ /// Reloads the view.
+ func accessoryDidUpdateServices(accessory: HMAccessory) {
+ reloadData()
+ }
+
+ /// If our accessory has become unreachable, go back the previous view.
+ func accessoryDidUpdateReachability(accessory: HMAccessory) {
+ if self.accessory == accessory {
+ navigationController?.popViewControllerAnimated(true)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionCell.swift b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionCell.swift
new file mode 100644
index 00000000..c8016f2b
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionCell.swift
@@ -0,0 +1,47 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ActionCell` displays a characteristic and a target value.
+*/
+
+import UIKit
+import HomeKit
+
+/// A `UITableViewCell` subclass that displays a characteristic's 'target value'.
+class ActionCell: UITableViewCell {
+ /// Ignores the passed-in style and overrides it with `.Subtitle`.
+ override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
+ super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
+ selectionStyle = .None
+ detailTextLabel?.textColor = UIColor.lightGrayColor()
+ accessoryType = .None
+ }
+
+ /// Required init.
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+
+ /**
+ Sets the cell's text to represent a characteristic and target value.
+ For example, "Brightness → 60%"
+ Sets the subtitle to the service and accessory that this characteristic represents.
+
+ - parameter characteristic: The characteristic this cell represents.
+ - parameter targetValue: The target value from this action.
+ */
+ func setCharacteristic(characteristic: HMCharacteristic, targetValue: AnyObject) {
+ let targetDescription = "\(characteristic.localizedDescription) → \(characteristic.localizedDescriptionForValue(targetValue))"
+ textLabel?.text = targetDescription
+
+ let contextDescription = NSLocalizedString("%@ in %@", comment: "Service in Accessory")
+ if let service = characteristic.service, accessory = service.accessory {
+ detailTextLabel?.text = String(format: contextDescription, service.name, accessory.name)
+ }
+ else {
+ detailTextLabel?.text = NSLocalizedString("Unknown Characteristic", comment: "Unknown Characteristic")
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetCreator.swift
new file mode 100644
index 00000000..3da23850
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetCreator.swift
@@ -0,0 +1,294 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ActionSetCreator` builds `HMActionSet`s.
+*/
+
+import HomeKit
+
+/// A `CharacteristicCellDelegate` that builds an `HMActionSet` when it receives delegate callbacks.
+class ActionSetCreator: CharacteristicCellDelegate {
+ // MARK: Properties
+
+ var actionSet: HMActionSet?
+ var home: HMHome
+
+ var saveError: NSError?
+
+ /// The structure we're going to use to hold the target values.
+ let targetValueMap = NSMapTable.strongToStrongObjectsMapTable()
+
+ /// A dispatch group to wait for all of the individual components of the saving process.
+ let saveActionSetGroup = dispatch_group_create()
+
+ required init(actionSet: HMActionSet?, home: HMHome) {
+ self.actionSet = actionSet
+ self.home = home
+ }
+
+ /**
+ If there is an action set, saves the action set and then updates its name.
+ Otherwise creates a new action set and adds all actions to it.
+
+ - parameter name: The new name for the action set.
+ - parameter completionHandler: A closure to call once the action set has been completely saved.
+ */
+ func saveActionSetWithName(name: NSString, completionHandler: (error: NSError?) -> Void) {
+ if let actionSet = actionSet {
+ saveActionSet(actionSet)
+ updateNameIfNecessary(name)
+ }
+ else {
+ createActionSetWithName(name)
+ }
+ dispatch_group_notify(saveActionSetGroup, dispatch_get_main_queue()) {
+ completionHandler(error: self.saveError)
+ self.saveError = nil
+ }
+ }
+
+ /**
+ Adds all of the actions that have been requested to the Action Set, then runs a completion block.
+
+ - parameter completion: A closure to be called when all of the actions have been added.
+ */
+ func saveActionSet(actionSet: HMActionSet) {
+ let actions = actionsFromMapTable(targetValueMap)
+ for action in actions {
+ dispatch_group_enter(saveActionSetGroup)
+ addAction(action, toActionSet: actionSet) { error in
+ if let error = error {
+ print("HomeKit: Error adding action: \(error.localizedDescription)")
+ self.saveError = error
+ }
+ dispatch_group_leave(self.saveActionSetGroup)
+ }
+ }
+ }
+
+ /**
+ Sets the name of an existing action set.
+
+ - parameter name: The new name for the action set.
+ */
+ func updateNameIfNecessary(name: NSString) {
+ if actionSet?.name == name {
+ return
+ }
+ dispatch_group_enter(saveActionSetGroup)
+ actionSet?.updateName(name as String) { error in
+ if let error = error {
+ print("HomeKit: Error updating name: \(error.localizedDescription)")
+ self.saveError = error
+ }
+ dispatch_group_leave(self.saveActionSetGroup)
+ }
+ }
+
+ /**
+ Creates and saves an action set with the provided name.
+
+ - parameter name: The name for the new action set.
+ */
+ func createActionSetWithName(name: NSString) {
+ dispatch_group_enter(saveActionSetGroup)
+ home.addActionSetWithName(name as String) { actionSet, error in
+ if let error = error {
+ print("HomeKit: Error creating action set: \(error.localizedDescription)")
+ self.saveError = error
+ }
+ else {
+ // There is no error, so the action set has a value.
+ self.saveActionSet(actionSet!)
+ }
+ dispatch_group_leave(self.saveActionSetGroup)
+ }
+ }
+
+ /**
+ Checks to see if an action already exists to modify the same characteristic
+ as the action passed in. If such an action exists, the method tells the
+ existing action to update its target value. Otherwise, the new action is
+ simply added to the action set.
+
+ - parameter action: The action to add or update.
+ - parameter actionSet: The action set to which to add the action.
+ - parameter completion: A closure to call when the addition has finished.
+ */
+ func addAction(action: HMCharacteristicWriteAction, toActionSet actionSet: HMActionSet, completion: (NSError?) -> Void) {
+ if let existingAction = existingActionInActionSetMatchingAction(action) {
+ existingAction.updateTargetValue(action.targetValue, completionHandler: completion)
+ }
+ else {
+ actionSet.addAction(action, completionHandler: completion)
+ }
+ }
+
+ /**
+ Checks to see if there is already an HMCharacteristicWriteAction in
+ the action set that matches the provided action.
+
+ - parameter action: The action in question.
+
+ - returns: The existing action that matches the characteristic or nil if
+ there is no existing action.
+ */
+ func existingActionInActionSetMatchingAction(action: HMCharacteristicWriteAction) -> HMCharacteristicWriteAction? {
+ if let actionSet = actionSet {
+ for existingAction in Array(actionSet.actions) as! [HMCharacteristicWriteAction] {
+ if action.characteristic == existingAction.characteristic {
+ return existingAction
+ }
+ }
+ }
+ return nil
+ }
+
+ /**
+ Iterates over a map table of HMCharacteristic -> id objects and creates
+ an array of HMCharacteristicWriteActions based on those targets.
+
+ - parameter table: An NSMapTable mapping HMCharacteristics to id's.
+
+ - returns: An array of HMCharacteristicWriteActions.
+ */
+ func actionsFromMapTable(table: NSMapTable) -> [HMCharacteristicWriteAction] {
+ return targetValueMap.keyEnumerator().allObjects.map { characteristic in
+ let targetValue = targetValueMap.objectForKey(characteristic) as! NSCopying
+ return HMCharacteristicWriteAction(characteristic: characteristic as! HMCharacteristic, targetValue: targetValue)
+ }
+ }
+
+ /**
+ - returns: `true` if the characteristic count is greater than zero;
+ `false` otherwise.
+ */
+ var containsActions: Bool {
+ return !allCharacteristics.isEmpty
+ }
+
+ /**
+ All existing characteristics within `HMCharacteristiWriteActions`
+ and target values in the target value map.
+ */
+ var allCharacteristics: [HMCharacteristic] {
+ var characteristics = Set()
+
+ if let actionSet = actionSet, actions = Array(actionSet.actions) as? [HMCharacteristicWriteAction] {
+ let actionSetCharacteristics = actions.map { action -> HMCharacteristic in
+ return action.characteristic
+ }
+ characteristics.unionInPlace(actionSetCharacteristics)
+ }
+
+ characteristics.unionInPlace(targetValueMap.keyEnumerator().allObjects as! [HMCharacteristic])
+
+ return Array(characteristics)
+ }
+
+ /**
+ Searches through the target value map and existing `HMCharacteristicWriteActions`
+ to find the target value for the characteristic in question.
+
+ - parameter characteristic: The characteristic in question.
+
+ - returns: The target value for this characteristic, or nil if there is no target.
+ */
+ func targetValueForCharacteristic(characteristic: HMCharacteristic) -> AnyObject? {
+ if let value = targetValueMap.objectForKey(characteristic) {
+ return value
+ }
+ else if let actions = actionSet?.actions {
+ for action in actions {
+ if let writeAction = action as? HMCharacteristicWriteAction
+ where writeAction.characteristic == characteristic {
+ return writeAction.targetValue
+ }
+ }
+ }
+
+ return nil
+ }
+
+ /**
+ First removes the characteristic from the `targetValueMap`.
+ Then removes any `HMCharacteristicWriteAction`s from the action set
+ which set the specified characteristic.
+
+ - parameter characteristic: The `HMCharacteristic` to remove.
+ - parameter completion: The closure to invoke when the characteristic has been removed.
+ */
+ func removeTargetValueForCharacteristic(characteristic: HMCharacteristic, completion: () -> Void) {
+ /*
+ We need to create a dispatch group here, because in many cases
+ there will be one characteristic saved in the Action Set, and one
+ in the target value map. We want to run the completion closure only one time,
+ to ensure we've removed both.
+ */
+ let group = dispatch_group_create()
+ if targetValueMap.objectForKey(characteristic) != nil {
+ // Remove the characteristic from the target value map.
+ dispatch_group_async(group, dispatch_get_main_queue()) {
+ self.targetValueMap.removeObjectForKey(characteristic)
+ }
+ }
+ if let actions = actionSet?.actions as? Set {
+ for action in Array(actions) {
+ if action.characteristic == characteristic {
+ /*
+ Also remove the action, and only relinquish the dispatch group
+ once the action set has finished.
+ */
+ dispatch_group_enter(group)
+ actionSet?.removeAction(action) { error in
+ if let error = error {
+ print(error.localizedDescription)
+ }
+ dispatch_group_leave(group)
+ }
+ }
+ }
+ }
+ // Once we're positive both have finished, run the completion closure on the main queue.
+ dispatch_group_notify(group, dispatch_get_main_queue(), completion)
+ }
+
+ // MARK: Characteristic Cell Delegate
+
+ /**
+ Receives a callback from a `CharacteristicCell` with a value change.
+ Adds this value change into the targetValueMap, overwriting other value changes.
+ */
+ func characteristicCell(cell: CharacteristicCell, didUpdateValue newValue: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) {
+ targetValueMap.setObject(newValue, forKey: characteristic)
+ }
+
+ /**
+ Receives a callback from a `CharacteristicCell`, requesting an initial value for
+ a given characteristic.
+
+ It checks to see if we have an action in this Action Set that matches the characteristic.
+ If so, calls the completion closure with the target value.
+ */
+ func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) {
+ if let value = targetValueForCharacteristic(characteristic) {
+ completion(value, nil)
+ return
+ }
+
+ characteristic.readValueWithCompletionHandler { error in
+ /*
+ The user may have updated the cell value while the
+ read was happening. We check the map one more time.
+ */
+ if let value = self.targetValueForCharacteristic(characteristic) {
+ completion(value, nil)
+ }
+ else {
+ completion(characteristic.value, error)
+ }
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetViewController.swift
new file mode 100644
index 00000000..51e41f3e
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetViewController.swift
@@ -0,0 +1,334 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ActionSetViewController` allows users to create and modify action sets.
+*/
+
+
+import UIKit
+import HomeKit
+
+/// Represents table view sections of the `ActionSetViewController`.
+enum ActionSetTableViewSection: Int {
+ case Name, Actions, Accessories
+}
+
+/**
+ A view controller that facilitates creation of Action Sets.
+
+ It contains a cell for a name, and lists accessories within a home.
+ If there are actions within the action set, it also displays a list of ActionCells displaying those actions.
+ It owns an `ActionSetCreator` and routes events to the creator as appropriate.
+*/
+class ActionSetViewController: HMCatalogViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let accessoryCell = "AccessoryCell"
+ static let unreachableAccessoryCell = "UnreachableAccessoryCell"
+ static let actionCell = "ActionCell"
+ static let showServiceSegue = "Show Services"
+ }
+
+ // MARK: Properties
+
+ @IBOutlet weak var nameField: UITextField!
+ @IBOutlet weak var saveButton: UIBarButtonItem!
+
+ var actionSet: HMActionSet?
+ var actionSetCreator: ActionSetCreator!
+ var displayedAccessories = [HMAccessory]()
+
+ // MARK: View Methods
+
+ /**
+ Creates the action set creator, registers the appropriate reuse identifiers in the table,
+ and sets the `nameField` if appropriate.
+ */
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ actionSetCreator = ActionSetCreator(actionSet: actionSet, home: home)
+ displayedAccessories = home.sortedControlAccessories
+
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.accessoryCell)
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.unreachableAccessoryCell)
+ tableView.registerClass(ActionCell.self, forCellReuseIdentifier: Identifiers.actionCell)
+
+ tableView.rowHeight = UITableViewAutomaticDimension
+
+ tableView.estimatedRowHeight = 44.0
+
+ if let actionSet = actionSet {
+ nameField.text = actionSet.name
+ nameFieldDidChange(nameField)
+ }
+
+ if !home.isAdmin {
+ nameField.enabled = false
+ }
+ }
+
+ /// Reloads the data and view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ tableView.reloadData()
+ enableSaveButtonIfNecessary()
+ }
+
+ /// Dismisses the view controller if our data is invalid.
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+ if shouldPopViewController() {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+
+ /// Dismisses the keyboard when we dismiss.
+ override func viewWillDisappear(animated: Bool) {
+ super.viewWillDisappear(animated)
+ resignFirstResponder()
+ }
+
+ /// Passes our accessory into the `ServicesViewController`.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ if segue.identifier == Identifiers.showServiceSegue {
+ let servicesViewController = segue.intendedDestinationViewController as! ServicesViewController
+ servicesViewController.onlyShowsControlServices = true
+ servicesViewController.cellDelegate = actionSetCreator
+
+ let index = tableView.indexPathForCell(sender as! UITableViewCell)!.row
+
+ servicesViewController.accessory = displayedAccessories[index]
+ servicesViewController.cellDelegate = actionSetCreator
+ }
+ }
+
+ // MARK: IBAction Methods
+
+ /// Dismisses the view controller.
+ @IBAction func dismiss() {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ /// Saves the action set, adds it to the home, and dismisses the view.
+ @IBAction func saveAndDismiss() {
+ saveButton.enabled = false
+
+ actionSetCreator.saveActionSetWithName(trimmedName) { error in
+ self.saveButton.enabled = true
+
+ if let error = error {
+ self.displayError(error)
+ }
+ else {
+ self.dismiss()
+ }
+ }
+ }
+
+ /// Prompts an update to the save button enabled state.
+ @IBAction func nameFieldDidChange(field: UITextField) {
+ enableSaveButtonIfNecessary()
+ }
+
+ // MARK: Table View Methods
+
+ /// We do not allow the creation of action sets in a shared home.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return home.isAdmin ? 3 : 2
+ }
+
+ /**
+ - returns: In the Actions section: the number of actions this set will contain upon saving.
+ In the Accessories section: The number of accessories in the home.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch ActionSetTableViewSection(rawValue: section) {
+ case .Name?:
+ return super.tableView(tableView, numberOfRowsInSection: section)
+
+ case .Actions?:
+ return max(actionSetCreator.allCharacteristics.count, 1)
+
+ case .Accessories?:
+ return displayedAccessories.count
+
+ case nil:
+ fatalError("Unexpected `ActionSetTableViewSection` raw value.")
+ }
+ }
+
+ /**
+ Required override to allow for a tableView with both static and dynamic content.
+ Basically, since the superclass's indentationLevelForRowAtIndexPath is only
+ expecting 1 row per section, just call the super class's implementation
+ for the first row.
+ */
+ override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
+ return super.tableView(tableView, indentationLevelForRowAtIndexPath: NSIndexPath(forRow: 0, inSection: indexPath.section))
+ }
+
+ /// Removes the action associated with the index path.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ let characteristic = actionSetCreator.allCharacteristics[indexPath.row]
+ actionSetCreator.removeTargetValueForCharacteristic(characteristic) {
+ if self.actionSetCreator.containsActions {
+ tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+ else {
+ tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+ }
+ }
+ }
+
+ /// - returns: `true` for the Actions section; `false` otherwise.
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ return ActionSetTableViewSection(rawValue: indexPath.section) == .Actions && home.isAdmin
+ }
+
+ /// - returns: `UITableViewAutomaticDimension` for dynamic sections, otherwise the superclass's implementation.
+ override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
+ switch ActionSetTableViewSection(rawValue: indexPath.section) {
+ case .Name?:
+ return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
+
+ case .Actions?, .Accessories?:
+ return UITableViewAutomaticDimension
+
+ case nil:
+ fatalError("Unexpected `ActionSetTableViewSection` raw value.")
+ }
+ }
+
+ /// - returns: An action cell for the actions section, an accessory cell for the accessory section, or the superclass's implementation.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch ActionSetTableViewSection(rawValue: indexPath.section) {
+ case .Name?:
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+
+ case .Actions?:
+ if actionSetCreator.containsActions {
+ return self.tableView(tableView, actionCellForRowAtIndexPath: indexPath)
+ }
+ else {
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+
+ case .Accessories?:
+ return self.tableView(tableView, accessoryCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `ActionSetTableViewSection` raw value.")
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /// Enables the save button if there is a valid name and at least one action.
+ private func enableSaveButtonIfNecessary() {
+ saveButton.enabled = home.isAdmin && trimmedName.characters.count > 0 && actionSetCreator.containsActions
+ }
+
+ /// - returns: The contents of the nameField, with whitespace trimmed from the beginning and end.
+ private var trimmedName: String {
+ return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
+ }
+
+ /**
+ - returns: `true` if there are no accessories in the home, we have no set action set,
+ or if our home no longer exists; `false` otherwise
+ */
+ private func shouldPopViewController() -> Bool {
+ if homeStore.home?.accessories.count == 0 && actionSet == nil {
+ return true
+ }
+
+ return !homeStore.homeManager.homes.contains { $0 == homeStore.home }
+ }
+
+ /// - returns: An `ActionCell` instance with the target value for the characteristic at the specified index path.
+ private func tableView(tableView: UITableView, actionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.actionCell, forIndexPath: indexPath) as! ActionCell
+ let characteristic = actionSetCreator.allCharacteristics[indexPath.row] as HMCharacteristic
+
+ if let target = actionSetCreator.targetValueForCharacteristic(characteristic) {
+ cell.setCharacteristic(characteristic, targetValue: target)
+ }
+
+ return cell
+ }
+
+ /// - returns: An Accessory cell that contains an accessory's name.
+ private func tableView(tableView: UITableView, accessoryCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ /*
+ These cells are static, the identifiers are defined in the Storyboard,
+ but they're not recognized here. In viewDidLoad:, we're registering
+ `UITableViewCell` as the class for "AccessoryCell" and "UnreachableAccessoryCell".
+ We must configure these cells manually, the cells in the Storyboard
+ are just for reference.
+ */
+
+ let accessory = displayedAccessories[indexPath.row]
+ let cellIdentifier = accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
+ cell.textLabel?.text = accessory.name
+
+ if accessory.reachable {
+ cell.textLabel?.textColor = UIColor.darkTextColor()
+ cell.accessoryType = .DisclosureIndicator
+ cell.selectionStyle = .Default
+ }
+ else {
+ cell.textLabel?.textColor = UIColor.lightGrayColor()
+ cell.accessoryType = .None
+ cell.selectionStyle = .None
+ }
+
+ return cell
+ }
+
+ /// Shows the services in the selected accessory.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ let cell = tableView.cellForRowAtIndexPath(indexPath)!
+ if cell.selectionStyle == .None {
+ return
+ }
+
+ if ActionSetTableViewSection(rawValue: indexPath.section) == .Accessories {
+ performSegueWithIdentifier(Identifiers.showServiceSegue, sender: cell)
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /**
+ Pops the view controller if our configuration is invalid;
+ reloads the view otherwise.
+ */
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ if shouldPopViewController() {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ else {
+ tableView.reloadData()
+ }
+ }
+
+ /// Reloads the table view data.
+ func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
+ tableView.reloadData()
+ }
+
+ /// If our action set was removed, dismiss the view.
+ func home(home: HMHome, didRemoveActionSet actionSet: HMActionSet) {
+ if actionSet == self.actionSet {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeKitObjectCollection.swift b/HomeKitCatalog/HMCatalog/Homes/HomeKitObjectCollection.swift
new file mode 100644
index 00000000..1dfc22eb
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/HomeKitObjectCollection.swift
@@ -0,0 +1,236 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HomeKitObjectCollection` is a model object for the `HomeViewController`.
+ It manages arrays of HomeKit objects.
+*/
+
+import HomeKit
+
+/// Represents the all different types of HomeKit objects.
+enum HomeKitObjectSection: Int {
+ case Accessory, Room, Zone, User, ActionSet, Trigger, ServiceGroup
+
+ static let count = 7
+}
+
+/**
+ Manages internal lists of HomeKit objects to allow for
+ save insertion into a table view.
+*/
+class HomeKitObjectCollection {
+ // MARK: Properties
+
+ var accessories = [HMAccessory]()
+ var rooms = [HMRoom]()
+ var zones = [HMZone]()
+ var actionSets = [HMActionSet]()
+ var triggers = [HMTrigger]()
+ var serviceGroups = [HMServiceGroup]()
+
+ /**
+ Adds an object to the collection by finding its corresponding
+ array and appending the object to it.
+
+ - parameter object: The HomeKit object to append.
+ */
+ func append(object: AnyObject) {
+ switch object {
+ case let actionSet as HMActionSet:
+ actionSets.append(actionSet)
+ actionSets = actionSets.sortByTypeAndLocalizedName()
+
+ case let accessory as HMAccessory:
+ accessories.append(accessory)
+ accessories = accessories.sortByLocalizedName()
+
+ case let room as HMRoom:
+ rooms.append(room)
+ rooms = rooms.sortByLocalizedName()
+
+ case let zone as HMZone:
+ zones.append(zone)
+ zones = zones.sortByLocalizedName()
+
+ case let trigger as HMTrigger:
+ triggers.append(trigger)
+ triggers = triggers.sortByLocalizedName()
+
+ case let serviceGroup as HMServiceGroup:
+ serviceGroups.append(serviceGroup)
+ serviceGroups = serviceGroups.sortByLocalizedName()
+
+ default:
+ break
+ }
+ }
+
+ /**
+ Creates an `NSIndexPath` representing the location of the
+ HomeKit object in the table view.
+
+ - parameter object: The HomeKit object to find.
+
+ - returns: The `NSIndexPath` representing the location of
+ the HomeKit object in the table view.
+ */
+ func indexPathOfObject(object: AnyObject) -> NSIndexPath? {
+ switch object {
+ case let actionSet as HMActionSet:
+ if let index = actionSets.indexOf(actionSet) {
+ return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.ActionSet.rawValue)
+ }
+
+ case let accessory as HMAccessory:
+ if let index = accessories.indexOf(accessory) {
+ return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Accessory.rawValue)
+ }
+
+ case let room as HMRoom:
+ if let index = rooms.indexOf(room) {
+ return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Room.rawValue)
+ }
+
+ case let zone as HMZone:
+ if let index = zones.indexOf(zone) {
+ return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Zone.rawValue)
+ }
+
+ case let trigger as HMTrigger:
+ if let index = triggers.indexOf(trigger) {
+ return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Trigger.rawValue)
+ }
+
+ case let serviceGroup as HMServiceGroup:
+ if let index = serviceGroups.indexOf(serviceGroup) {
+ return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.ServiceGroup.rawValue)
+ }
+
+ default: break
+ }
+
+ return nil
+ }
+
+ /**
+ Removes a HomeKit object from the collection.
+
+ - parameter object: The HomeKit object to remove.
+ */
+ func remove(object: AnyObject) {
+ switch object {
+ case let actionSet as HMActionSet:
+ if let index = actionSets.indexOf(actionSet) {
+ actionSets.removeAtIndex(index)
+ }
+
+ case let accessory as HMAccessory:
+ if let index = accessories.indexOf(accessory) {
+ accessories.removeAtIndex(index)
+ }
+
+ case let room as HMRoom:
+ if let index = rooms.indexOf(room) {
+ rooms.removeAtIndex(index)
+ }
+
+ case let zone as HMZone:
+ if let index = zones.indexOf(zone) {
+ zones.removeAtIndex(index)
+ }
+
+ case let trigger as HMTrigger:
+ if let index = triggers.indexOf(trigger) {
+ triggers.removeAtIndex(index)
+ }
+
+ case let serviceGroup as HMServiceGroup:
+ if let index = serviceGroups.indexOf(serviceGroup) {
+ serviceGroups.removeAtIndex(index)
+ }
+
+ default:
+ break
+ }
+ }
+
+ /**
+ Provides the array of `NSObject`s corresponding to the provided section.
+
+ - parameter section: A `HomeKitObjectSection`.
+
+ - returns: An array of `NSObject`s corresponding to the provided section.
+ */
+ func objectsForSection(section: HomeKitObjectSection) -> [NSObject] {
+ switch section {
+ case .ActionSet:
+ return actionSets
+
+ case .Accessory:
+ return accessories
+
+ case .Room:
+ return rooms
+
+ case .Zone:
+ return zones
+
+ case .Trigger:
+ return triggers
+
+ case .ServiceGroup:
+ return serviceGroups
+
+ default:
+ return []
+ }
+ }
+
+ /**
+ Provides an `HomeKitObjectSection` for a given object.
+
+ - parameter object: A HomeKit object.
+
+ - returns: The corrosponding `HomeKitObjectSection`
+ */
+ class func sectionForObject(object: AnyObject?) -> HomeKitObjectSection? {
+ switch object {
+ case is HMActionSet:
+ return .ActionSet
+
+ case is HMAccessory:
+ return .Accessory
+
+ case is HMZone:
+ return .Zone
+
+ case is HMRoom:
+ return .Room
+
+ case is HMTrigger:
+ return .Trigger
+
+ case is HMServiceGroup:
+ return .ServiceGroup
+
+ default:
+ return nil
+ }
+ }
+
+ /**
+ Reloads all internal structures based on the provided home.
+
+ - parameter home: The `HMHome` with which to reset the collection.
+ */
+ func resetWithHome(home: HMHome) {
+ accessories = home.accessories.sortByLocalizedName()
+ rooms = home.allRooms
+ zones = home.zones.sortByLocalizedName()
+ actionSets = home.actionSets.sortByTypeAndLocalizedName()
+ triggers = home.triggers.sortByLocalizedName()
+ serviceGroups = home.serviceGroups.sortByLocalizedName()
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeListConfigurationViewController.swift b/HomeKitCatalog/HMCatalog/Homes/HomeListConfigurationViewController.swift
new file mode 100644
index 00000000..5a7e9ea1
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/HomeListConfigurationViewController.swift
@@ -0,0 +1,306 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HomeListConfigurationViewController` allows for the creation and deletion of homes.
+*/
+
+import UIKit
+import HomeKit
+
+// Represents the sections in the `HomeListConfigurationViewController`.
+enum HomeListSection: Int {
+ case Homes, PrimaryHome
+
+ static let count = 2
+}
+
+/**
+ A `HomeListViewController` subclass which allows the user to add and remove
+ homes and set the primary home.
+*/
+class HomeListConfigurationViewController: HomeListViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let addHomeCell = "AddHomeCell"
+ static let noHomesCell = "NoHomesCell"
+ static let primaryHomeCell = "PrimaryHomeCell"
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of sections in the `HomeListSection` enum.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return HomeListSection.count
+ }
+
+ /// Provides the number of rows in the section using the internal home's list.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch HomeListSection(rawValue: section) {
+ // Add row.
+ case .Homes?:
+ return homes.count + 1
+
+ // 'No homes' row.
+ case .PrimaryHome?:
+ return max(homes.count, 1)
+
+ case nil: fatalError("Unexpected `HomeListSection` raw value.")
+ }
+ }
+
+ /**
+ Generates and configures either a content cell or an add cell using the
+ provided index path.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ if indexPathIsAdd(indexPath) {
+ return tableView.dequeueReusableCellWithIdentifier(Identifiers.addHomeCell, forIndexPath: indexPath)
+ }
+ else if homes.isEmpty {
+ return tableView.dequeueReusableCellWithIdentifier(Identifiers.noHomesCell, forIndexPath: indexPath)
+ }
+
+ let reuseIdentifier: String
+
+ switch HomeListSection(rawValue: indexPath.section) {
+ case .Homes?:
+ reuseIdentifier = Identifiers.homeCell
+
+ case .PrimaryHome?:
+ reuseIdentifier = Identifiers.primaryHomeCell
+
+ case nil: fatalError("Unexpected `HomeListSection` raw value.")
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
+ let home = homes[indexPath.row]
+
+ cell.textLabel!.text = home.name
+ cell.detailTextLabel?.text = sharedTextForHome(home)
+
+ // Mark the primary home with checkmark.
+ if HomeListSection(rawValue: indexPath.section) == .PrimaryHome {
+ if home == homeManager.primaryHome {
+ cell.accessoryType = .Checkmark
+ }
+ else {
+ cell.accessoryType = .None
+ }
+ }
+
+ return cell
+ }
+
+ /// Homes in the list section can be deleted. The add row cannot be deleted.
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ return HomeListSection(rawValue: indexPath.section) == .Homes && !indexPathIsAdd(indexPath)
+ }
+
+ /// Only the 'primary home' section has a title.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ if HomeListSection(rawValue: section) == .PrimaryHome {
+ return NSLocalizedString("Primary Home", comment: "Primary Home")
+ }
+
+ return nil
+ }
+
+ /// Provides subtext about the use of designating a "primary home".
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ if section == HomeListSection.PrimaryHome.rawValue {
+ return NSLocalizedString("The primary home is used by Siri to route commands if the home is not specified.", comment: "Primary Home Description")
+ }
+ return nil
+ }
+
+ /**
+ If selecting a regular home, a segue will be performed.
+ If this method is called, the user either selected the 'add' row,
+ a primary home cell, or the `No Homes` cell.
+ */
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ if indexPathIsAdd(indexPath) {
+ addNewHome()
+ }
+ else if indexPathIsNone(indexPath) {
+ return
+ }
+ else if HomeListSection(rawValue: indexPath.section) == .PrimaryHome {
+ let newPrimaryHome = homes[indexPath.row]
+ updatePrimaryHome(newPrimaryHome)
+ }
+ }
+
+ /// Removes the home from HomeKit if the row is deleted.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ removeHomeAtIndexPath(indexPath)
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Updates the primary home in HomeKit and reloads the view.
+ If the home is already selected, no action is taken.
+
+ - parameter newPrimaryHome: The new `HMHome` to set as the primary home.
+ */
+ private func updatePrimaryHome(newPrimaryHome: HMHome) {
+ guard newPrimaryHome != homeManager.primaryHome else { return }
+
+ homeManager.updatePrimaryHome(newPrimaryHome) { error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+
+ self.didUpdatePrimaryHome()
+ }
+ }
+
+ /// Reloads the 'primary home' section.
+ private func didUpdatePrimaryHome() {
+ let primaryIndexSet = NSIndexSet(index: HomeListSection.PrimaryHome.rawValue)
+
+ tableView.reloadSections(primaryIndexSet, withRowAnimation: .Automatic)
+ }
+
+ /**
+ Removed the home at the specified index path from HomeKit and updates the view.
+
+ - parameter indexPath: The `NSIndexPath` of the home to remove.
+ */
+ private func removeHomeAtIndexPath(indexPath: NSIndexPath) {
+ let home = homes[indexPath.row]
+
+ // Remove the home from the data structure. If it fails, put it back.
+ didRemoveHome(home)
+ homeManager.removeHome(home) { error in
+ if let error = error {
+ self.displayError(error)
+ self.didAddHome(home)
+ return
+ }
+ }
+ }
+
+ /**
+ Presents an alert controller so the user can provide a name. If committed,
+ the home is created.
+ */
+ private func addNewHome() {
+ let attributedType = NSLocalizedString("Home", comment: "Home")
+ let placeholder = NSLocalizedString("Apartment", comment: "Apartment")
+
+ presentAddAlertWithAttributeType(attributedType, placeholder: placeholder) { name in
+ self.addHomeWithName(name)
+ }
+ }
+
+ /**
+ Removes a home from the internal structure and updates the view.
+
+ - parameter home: The `HMHome` to remove.
+ */
+ override func didRemoveHome(home: HMHome) {
+ guard let index = homes.indexOf(home) else { return }
+
+ let indexPath = NSIndexPath(forRow: index, inSection: HomeListSection.Homes.rawValue)
+ homes.removeAtIndex(index)
+ let primaryIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.PrimaryHome.rawValue)
+
+ /*
+ If there aren't any homes, we still want one cell to display 'No Homes'.
+ Just reload.
+ */
+ tableView.beginUpdates()
+ if homes.isEmpty {
+ tableView.reloadRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Fade)
+ }
+ else {
+ tableView.deleteRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Automatic)
+ }
+ tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ tableView.endUpdates()
+
+ }
+
+ /// Adds the home to the internal structure and updates the view.
+ override func didAddHome(home: HMHome) {
+ homes.append(home)
+ sortHomes()
+ guard let newHomeIndex = homes.indexOf(home) else { return }
+
+ let indexPath = NSIndexPath(forRow: newHomeIndex, inSection: HomeListSection.Homes.rawValue)
+
+ let primaryIndexPath = NSIndexPath(forRow: newHomeIndex, inSection: HomeListSection.PrimaryHome.rawValue)
+
+ tableView.beginUpdates()
+
+ if homes.count == 1 {
+ tableView.reloadRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Fade)
+ }
+ else {
+ tableView.insertRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Automatic)
+ }
+
+ tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ tableView.endUpdates()
+ }
+
+ /**
+ Creates a new home with the provided name, adds the home to HomeKit
+ and reloads the view.
+ */
+ private func addHomeWithName(name: String) {
+ homeManager.addHomeWithName(name) { newHome, error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+
+ self.didAddHome(newHome!)
+ }
+ }
+
+
+ /// - returns: `true` if the index path is the 'add row'; `false` otherwise.
+ private func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
+ return HomeListSection(rawValue: indexPath.section) == .Homes &&
+ indexPath.row == homes.count
+ }
+
+ /// - returns: `true` if the index path is the 'No Homes' cell; `false` otherwise.
+ private func indexPathIsNone(indexPath: NSIndexPath) -> Bool {
+ return HomeListSection(rawValue: indexPath.section) == .PrimaryHome && homes.isEmpty
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// Finds the home in the internal structure and reloads the corresponding row.
+ override func homeDidUpdateName(home: HMHome) {
+ if let index = homes.indexOf(home) {
+ let listIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.Homes.rawValue)
+
+ let primaryIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.PrimaryHome.rawValue)
+
+ tableView.reloadRowsAtIndexPaths([listIndexPath, primaryIndexPath], withRowAnimation: .Automatic)
+ }
+ else {
+ // Just reload the data since we don't know the index path.
+ tableView.reloadData()
+ }
+ }
+
+ // MARK: HMHomeManagerDelegate Methods
+
+ /// Reloads the 'primary home' section.
+ func homeManagerDidUpdatePrimaryHome(manager: HMHomeManager) {
+ didUpdatePrimaryHome()
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeListViewController.swift b/HomeKitCatalog/HMCatalog/Homes/HomeListViewController.swift
new file mode 100644
index 00000000..8500b063
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/HomeListViewController.swift
@@ -0,0 +1,257 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HomeListViewController` is a superclass that lists the user's homes.
+*/
+
+import UIKit
+import HomeKit
+
+
+/// A generic view controller for displaying a list of homes in a home manager.
+class HomeListViewController: HMCatalogViewController, HMHomeManagerDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let homeCell = "HomeCell"
+ static let showHomeSegue = "Show Home"
+ }
+
+ // MARK: Properties
+
+ var homes = [HMHome]()
+
+ var homeManager: HMHomeManager {
+ return homeStore.homeManager
+ }
+
+ // MARK: View Methods
+
+ /// Configures the table view.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableView.estimatedRowHeight = 44.0
+
+ tableView.rowHeight = UITableViewAutomaticDimension
+ }
+
+ /// Resets the list of homes (which will update the view).
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ resetHomesList()
+ }
+
+ // MARK: Delegate Registration
+
+ /**
+ Registers as the delegate for the home manager and all homes in the internal
+ homes list.
+ */
+ override func registerAsDelegate() {
+ homeManager.delegate = self
+
+ for home in homes {
+ home.delegate = self
+ }
+ }
+
+ /// Sets the home store's current home based on which cell was selected.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+
+ if segue.identifier == Identifiers.showHomeSegue {
+ if sender === self {
+ // Don't update the selected home if we sent ourselves here.
+ return
+ }
+
+ if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) {
+ homeStore.home = homes[indexPath.row]
+ }
+ }
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ Provides the number of sections based on the home array count.
+ Updates the background message for the table view.
+
+ - returns: The number of homes in the internal array.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let rows = homes.count
+
+ if rows == 0 {
+ let message = NSLocalizedString("No Homes", comment: "No Homes")
+ setBackgroundMessage(message)
+ }
+ else {
+ setBackgroundMessage(nil)
+ }
+
+ return rows
+ }
+
+ /**
+ Generates a basic cell for a home.
+ Subtext is provided to tell the user if the home is shared or owned by the user.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.homeCell, forIndexPath: indexPath)
+ let home = homes[indexPath.row]
+
+ cell.textLabel?.text = home.name
+ cell.detailTextLabel?.text = sharedTextForHome(home)
+
+ return cell
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Provides an ordering for homes.
+
+ Homes are first ordered by their 'shared' status, then by name.
+
+ - parameter home1: The first `HMHome`.
+ - parameter home2: The second `HMHome`.
+
+ - returns: `true` if `home1` is ordered before `home2`; `false` otherwise.
+ */
+ private func orderHomes(home1: HMHome, home2: HMHome) -> Bool {
+ if home1.isAdmin == home2.isAdmin {
+ /*
+ We are comparing two shared homes or two of our homes, just compare
+ names.
+ */
+ return home1.name.localizedCompare(home2.name) == .OrderedAscending
+ }
+ else {
+ /*
+ We are comparing a shared home and one of our homes, if home1 is
+ ours, put it first.
+ */
+ return home1.isAdmin
+ }
+ }
+
+ /**
+ Regenerates the list of homes using list provided by the home manager.
+ The list is then sorted and the view is reloaded.
+ */
+ private func resetHomesList() {
+ homes = homeManager.homes.sort(orderHomes)
+ tableView.reloadData()
+ }
+
+ /// Sorts the list of homes (without reloading from the home manager).
+ func sortHomes() {
+ homes.sortInPlace(orderHomes)
+ }
+
+ /**
+ Adds a new home into the internal homes array and inserts the new
+ row into the table view.
+
+ - parameter home: The new `HMHome` that's been added.
+ */
+ func didAddHome(home: HMHome) {
+ homes.append(home)
+
+ sortHomes()
+
+ if let newHomeIndex = homes.indexOf(home) {
+ let indexPathOfNewHome = NSIndexPath(forRow: newHomeIndex, inSection: 0)
+
+ tableView.insertRowsAtIndexPaths([indexPathOfNewHome], withRowAnimation: .Automatic)
+ }
+ }
+
+ /**
+ Removes a home from the internal homes array (if it exists) and
+ deletes corresponding row from the table view.
+
+ - parameter home: The `HMHome` to remove.
+ */
+ func didRemoveHome(home: HMHome) {
+ guard let removedHomeIndex = homes.indexOf(home) else { return }
+
+ homes.removeAtIndex(removedHomeIndex)
+ let indexPath = NSIndexPath(forRow: removedHomeIndex, inSection: 0)
+ tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+
+ /**
+ - returns: A localized description of who owns the provided home.
+
+ - parameter home: The `HMHome` to describe.
+ */
+ func sharedTextForHome(home: HMHome) -> String {
+ if !home.isAdmin {
+ return NSLocalizedString("Shared with Me", comment: "Shared with Me")
+ }
+ else {
+ return NSLocalizedString("My Home", comment: "My Home")
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// Finds the cell with corresponds to the provided home and reloads it.
+ func homeDidUpdateName(home: HMHome) {
+ if let homeIndex = homes.indexOf(home) {
+ let indexPath = NSIndexPath(forRow: homeIndex, inSection: 0)
+
+ tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ // MARK: HMHomeManagerDelegate Methods
+
+ /**
+ Reloads data and view.
+
+ This view controller, in most cases, will remain the home manager delegate.
+ For this reason, this method will close all modal views and pop all detail views
+ if the home store's current home is no longer in the home manager's list of homes.
+ */
+ func homeManagerDidUpdateHomes(manager: HMHomeManager) {
+ registerAsDelegate()
+ resetHomesList()
+
+ if let home = homeStore.home where !manager.homes.contains(home) {
+ // Close all modal and detail views.
+ dismissViewControllerAnimated(true, completion: nil)
+ navigationController?.popToRootViewControllerAnimated(true)
+ }
+ }
+
+ /// Registers for the delegate of the new home and updates the view.
+ func homeManager(manager: HMHomeManager, didAddHome home: HMHome) {
+ home.delegate = self
+
+ didAddHome(home)
+ }
+
+ /**
+ Removes the home from the current list of homes and updates the view.
+
+ If the removed home was the current home, this view controller will dismiss
+ all modals views and pop all detail views.
+ */
+ func homeManager(manager: HMHomeManager, didRemoveHome home: HMHome) {
+ didRemoveHome(home)
+
+ guard let selectedHome = homeStore.home where home == selectedHome else { return }
+
+ homeStore.home = nil
+
+ // Close all modal and detail views.
+ dismissViewControllerAnimated(true, completion: nil)
+ navigationController?.popToRootViewControllerAnimated(true)
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeStore.swift b/HomeKitCatalog/HMCatalog/Homes/HomeStore.swift
new file mode 100644
index 00000000..97d6a8f8
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/HomeStore.swift
@@ -0,0 +1,22 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HomeStore` class is a simple singleton class which holds a home manager and the current selected home.
+*/
+
+import HomeKit
+
+/// A static, singleton class which holds a home manager and the current home.
+class HomeStore: NSObject, HMHomeManagerDelegate {
+ static let sharedStore = HomeStore()
+
+ // MARK: Properties
+
+ /// The current 'selected' home.
+ var home: HMHome?
+
+ /// The singleton home manager.
+ var homeManager = HMHomeManager()
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeViewController.swift b/HomeKitCatalog/HMCatalog/Homes/HomeViewController.swift
new file mode 100644
index 00000000..9604f814
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/HomeViewController.swift
@@ -0,0 +1,929 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HomeViewController` displays all of the HomeKit objects in a selected home.
+*/
+
+import Foundation
+import UIKit
+import HomeKit
+
+/// Distinguishes between the three types of cells in the `HomeViewController`.
+enum HomeCellType {
+ /// Represents an actual object in HomeKit.
+ case Object
+
+ /// Represents an "Add" row for users to select to create an object in HomeKit.
+ case Add
+
+ /// The cell is displaying text to show the user that no objects exist in this section.
+ case None
+}
+
+/**
+ A view controller that displays all elements within a home.
+ It contains separate sections for Accessories, Rooms, Zones, Action Sets,
+ Triggers, and Service Groups.
+*/
+class HomeViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let addCell = "AddCell"
+ static let disabledAddCell = "DisabledAddCell"
+ static let accessoryCell = "AccessoryCell"
+ static let unreachableAccessoryCell = "UnreachableAccessoryCell"
+ static let roomCell = "RoomCell"
+ static let zoneCell = "ZoneCell"
+ static let userCell = "UserCell"
+ static let actionSetCell = "ActionSetCell"
+ static let triggerCell = "TriggerCell"
+ static let serviceGroupCell = "ServiceGroupCell"
+ static let addTimerTriggerSegue = "Add Timer Trigger"
+ static let addCharacteristicTriggerSegue = "Add Characteristic Trigger"
+ static let addLocationTriggerSegue = "Add Location Trigger"
+ static let addActionSetSegue = "Add Action Set"
+ static let addAccessoriesSegue = "Add Accessories"
+ static let showRoomSegue = "Show Room"
+ static let showZoneSegue = "Show Zone"
+ static let showActionSetSegue = "Show Action Set"
+ static let showServiceGroupSegue = "Show Service Group"
+ static let showAccessorySegue = "Show Accessory"
+ static let modifyAccessorySegue = "Modify Accessory"
+ static let showTimerTriggerSegue = "Show Timer Trigger"
+ static let showLocationTriggerSegue = "Show Location Trigger"
+ static let showCharacteristicTriggerSegue = "Show Characteristic Trigger"
+ }
+
+ // MARK: Properties
+
+ /// A structure to maintain internal arrays of HomeKit objects.
+ private var objectCollection = HomeKitObjectCollection()
+
+ // MARK: View Methods
+
+ /**
+ Determines the destination of the segue and passes the correct
+ HomeKit object onto the next view controller.
+ */
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ guard let sender = sender as? UITableViewCell else { return }
+ guard let indexPath = tableView.indexPathForCell(sender) else { return }
+
+ let homeKitObject = homeKitObjectAtIndexPath(indexPath)
+ let destinationViewController = segue.intendedDestinationViewController
+
+ switch segue.identifier! {
+ case Identifiers.showRoomSegue:
+ let roomVC = destinationViewController as! RoomViewController
+ roomVC.room = homeKitObject as? HMRoom
+
+ case Identifiers.showZoneSegue:
+ let zoneViewController = destinationViewController as! ZoneViewController
+ zoneViewController.homeZone = homeKitObject as? HMZone
+
+ case Identifiers.showActionSetSegue:
+ let actionSetVC = destinationViewController as! ActionSetViewController
+ actionSetVC.actionSet = homeKitObject as? HMActionSet
+
+ case Identifiers.showServiceGroupSegue:
+ let serviceGroupVC = destinationViewController as! ServiceGroupViewController
+ serviceGroupVC.serviceGroup = homeKitObject as? HMServiceGroup
+
+ case Identifiers.showAccessorySegue:
+ let detailVC = destinationViewController as! ServicesViewController
+ /*
+ The services view controller is generic, we need to provide
+ `showsFavorites` to display the stars next to characteristics.
+ */
+ detailVC.accessory = homeKitObject as? HMAccessory
+ detailVC.showsFavorites = true
+ detailVC.cellDelegate = AccessoryUpdateController()
+
+ case Identifiers.modifyAccessorySegue:
+ let addAccessoryVC = destinationViewController as! ModifyAccessoryViewController
+ addAccessoryVC.accessory = homeKitObject as? HMAccessory
+
+ case Identifiers.showTimerTriggerSegue:
+ let triggerVC = destinationViewController as! TimerTriggerViewController
+ triggerVC.trigger = homeKitObject as? HMTimerTrigger
+
+ case Identifiers.showLocationTriggerSegue:
+ let triggerVC = destinationViewController as! LocationTriggerViewController
+ triggerVC.trigger = homeKitObject as? HMEventTrigger
+
+ case Identifiers.showCharacteristicTriggerSegue:
+ let triggerVC = destinationViewController as! CharacteristicTriggerViewController
+ triggerVC.trigger = homeKitObject as? HMEventTrigger
+
+ default:
+ print("Received unknown segue identifier: \(segue.identifier).")
+ }
+ }
+
+ /// Configures the table view.
+ override func awakeFromNib() {
+ super.awakeFromNib()
+ tableView.estimatedRowHeight = 44.0
+ tableView.rowHeight = UITableViewAutomaticDimension
+ }
+
+ /// Sets the navigation title and reloads view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ navigationItem.title = home.name
+ reloadTable()
+ }
+
+ // MARK: Delegate Registration
+
+ /**
+ Registers as the delegate for the home store's current home
+ and all accessories in the home.
+ */
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+
+ for accessory in home.accessories {
+ accessory.delegate = self
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /// Resets the object collection and reloads the view.
+ private func reloadTable() {
+ objectCollection.resetWithHome(home)
+ tableView.reloadData()
+ }
+
+ /**
+ Determines the type of the cell based on the index path.
+
+ - parameter indexPath: The `NSIndexPath` of the cell.
+
+ - returns: The `HomeCellType` for cell.
+ */
+ private func cellTypeForIndexPath(indexPath: NSIndexPath) -> HomeCellType {
+ guard let section = HomeKitObjectSection(rawValue: indexPath.section) else { return .None }
+
+ let objectCount = objectCollection.objectsForSection(section).count
+
+ if objectCount == 0 {
+ // No objects -- this is either an 'Add Row' or a 'None Row'.
+ return home.isAdmin ? .Add : .None
+ }
+ else if indexPath.row == objectCount {
+ return .Add
+ }
+ else {
+ return .Object
+ }
+ }
+
+ /// Reloads the trigger section.
+ private func updateTriggerAddRow() {
+ let triggerSection = NSIndexSet(index: HomeKitObjectSection.Trigger.rawValue)
+
+ tableView.reloadSections(triggerSection, withRowAnimation: .Automatic)
+ }
+
+ /// Reloads the action set section.
+ private func updateActionSetSection() {
+ let actionSetSection = NSIndexSet(index: HomeKitObjectSection.ActionSet.rawValue)
+
+ tableView.reloadSections(actionSetSection, withRowAnimation: .Automatic)
+
+ updateTriggerAddRow()
+ }
+
+ /// - returns: `true` if there are accessories within the home; `false` otherwise.
+ private var canAddActionSet: Bool {
+ return !objectCollection.accessories.isEmpty
+ }
+
+ /// - returns: `true` if there are action sets (with actions) within the home; `false` otherwise.
+ private var canAddTrigger: Bool {
+ return objectCollection.actionSets.contains { actionSet in
+ return !actionSet.actions.isEmpty
+ }
+ }
+
+ /**
+ Provides the 'HomeKit object' (`AnyObject?`) at the specified index path.
+
+ - parameter indexPath: The `NSIndexPath` of the object.
+
+ - returns: The HomeKit object.
+ */
+ private func homeKitObjectAtIndexPath(indexPath: NSIndexPath) -> AnyObject? {
+ if cellTypeForIndexPath(indexPath) != .Object {
+ return nil
+ }
+
+ if let section = HomeKitObjectSection(rawValue: indexPath.section) {
+ return objectCollection.objectsForSection(section)[indexPath.row]
+ }
+
+ return nil
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of `HomeKitObjectSection`s.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return HomeKitObjectSection.count
+ }
+
+ /// - returns: Localized titles for each of the HomeKit sections.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch HomeKitObjectSection(rawValue: section) {
+ case .Accessory?:
+ return NSLocalizedString("Accessories", comment: "Accessories")
+
+ case .Room?:
+ return NSLocalizedString("Rooms", comment: "Rooms")
+
+ case .Zone?:
+ return NSLocalizedString("Zones", comment: "Zones")
+
+ case .User?:
+ return NSLocalizedString("Users", comment: "Users")
+
+ case .ActionSet?:
+ return NSLocalizedString("Scenes", comment: "Scenes")
+
+ case .Trigger?:
+ return NSLocalizedString("Triggers", comment: "Triggers")
+
+ case .ServiceGroup?:
+ return NSLocalizedString("Service Groups", comment: "Service Groups")
+
+ case nil:
+ fatalError("Unexpected `HomeKitObjectSection` raw value.")
+ }
+
+ }
+
+ /// - returns: Localized text for the 'add row'.
+ private func titleForAddRowInSection(section: HomeKitObjectSection) -> String {
+ switch section {
+ case .Accessory:
+ return NSLocalizedString("Add Accessory…", comment: "Add Accessory")
+
+ case .Room:
+ return NSLocalizedString("Add Room…", comment: "Add Room")
+
+ case .Zone:
+ return NSLocalizedString("Add Zone…", comment: "Add Zone")
+
+ case .User:
+ return NSLocalizedString("Manage Users…", comment: "Manage Users")
+
+ case .ActionSet:
+ return NSLocalizedString("Add Scene…", comment: "Add Scene")
+
+ case .Trigger:
+ return NSLocalizedString("Add Trigger…", comment: "Add Trigger")
+
+ case .ServiceGroup:
+ return NSLocalizedString("Add Service Group…", comment: "Add Service Group")
+ }
+ }
+
+ /// - returns: Localized text for the 'none row'.
+ private func titleForNoneRowInSection(section: HomeKitObjectSection) -> String {
+ switch section {
+ case .Accessory:
+ return NSLocalizedString("No Accessories…", comment: "No Accessories")
+
+ case .Room:
+ return NSLocalizedString("No Rooms…", comment: "No Rooms")
+
+ case .Zone:
+ return NSLocalizedString("No Zones…", comment: "No Zones")
+
+ case .User:
+ // We only ever list 'Manage Users'.
+ return NSLocalizedString("Manage Users…", comment: "Manage Users")
+
+ case .ActionSet:
+ return NSLocalizedString("No Scenes…", comment: "No Scenes")
+
+ case .Trigger:
+ return NSLocalizedString("No Triggers…", comment: "No Triggers")
+
+ case .ServiceGroup:
+ return NSLocalizedString("No Service Groups…", comment: "No Service Groups")
+ }
+ }
+
+ /// - returns: Localized descriptions for HomeKit object types.
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ switch HomeKitObjectSection(rawValue: section) {
+ case .Zone?:
+ return NSLocalizedString("Zones are optional collections of rooms.", comment: "Zones Description")
+
+ case .User?:
+ return NSLocalizedString("Users can control the accessories in your home. You can share your home with anybody with an iCloud account.", comment: "Users Description")
+
+ case .ActionSet?:
+ return NSLocalizedString("Scenes (action sets) represent a state of your home. You must have at least one paired accessory to create a scene.", comment: "Scenes Description")
+
+ case .Trigger?:
+ return NSLocalizedString("Triggers set scenes at specific times, when you get to locations, or when a characteristic is in a specific state. You must have created at least one scene with an action to create a trigger.", comment: "Trigger Description")
+
+ case .ServiceGroup?:
+ return NSLocalizedString("Service groups organize services in a custom way. For example, add a subset of lights in your living room to control them without controlling all the lights in the living room.", comment: "Service Group Description")
+
+ case nil:
+ fatalError("Unexpected `HomeKitObjectSection` raw value.")
+
+ default:
+ return nil
+ }
+ }
+
+ /**
+ Provides the number of rows in each HomeKit object section.
+ Most sections just return the object count, but we also handle special cases.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let sectionEnum = HomeKitObjectSection(rawValue: section)!
+
+ // Only "Manage Users" button is in the Users section
+ if sectionEnum == .User {
+ return 1
+ }
+
+ let objectCount = objectCollection.objectsForSection(sectionEnum).count
+ if home.isAdmin {
+ // For add row.
+ return objectCount + 1
+ }
+ else {
+ // Always show at least one row in the section.
+ return max(objectCount, 1)
+ }
+ }
+
+ /// Generates a cell based on it's computed type.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch cellTypeForIndexPath(indexPath) {
+ case .Add:
+ return self.tableView(tableView, addCellForRowAtIndexPath: indexPath)
+
+ case .Object:
+ return self.tableView(tableView, homeKitObjectCellForRowAtIndexPath: indexPath)
+
+ case .None:
+ return self.tableView(tableView, noneCellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /// Generates a 'none cell' with a localized title.
+ private func tableView(tableView: UITableView, noneCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.disabledAddCell, forIndexPath: indexPath)
+
+ let section = HomeKitObjectSection(rawValue: indexPath.section)!
+
+ cell.textLabel!.text = titleForNoneRowInSection(section)
+
+ return cell
+ }
+
+ /**
+ Generates an 'add cell' with a localized title.
+
+ In some cases, the 'add cell' will be 'disabled' because the user is not
+ allowed to perform the action.
+ */
+ private func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ var reuseIdentifier = Identifiers.addCell
+
+ let section = HomeKitObjectSection(rawValue: indexPath.section)
+
+ if (!canAddActionSet && section == .ActionSet) ||
+ (!canAddTrigger && section == .Trigger) || !home.isAdmin {
+ reuseIdentifier = Identifiers.disabledAddCell
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
+
+ cell.textLabel!.text = titleForAddRowInSection(section!)
+
+ return cell
+ }
+
+ /**
+ Produces the cell reuse identifier based on the section.
+
+ - parameter indexPath: The `NSIndexPath` of the cell.
+
+ - returns: The cell reuse identifier.
+ */
+ private func reuseIdentifierForIndexPath(indexPath: NSIndexPath) -> String {
+ switch HomeKitObjectSection(rawValue: indexPath.section) {
+ case .Accessory?:
+ let accessory = homeKitObjectAtIndexPath(indexPath) as! HMAccessory
+ return accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell
+
+ case .Room?:
+ return Identifiers.roomCell
+
+ case .Zone?:
+ return Identifiers.zoneCell
+
+ case .User?:
+ return Identifiers.userCell
+
+ case .ActionSet?:
+ return Identifiers.actionSetCell
+
+ case .Trigger?:
+ return Identifiers.triggerCell
+
+ case .ServiceGroup?:
+ return Identifiers.serviceGroupCell
+
+ case nil:
+ fatalError("Unexpected `HomeKitObjectSection` raw value.")
+ }
+ }
+
+ /// Generates a cell for the HomeKit object at the specified index path.
+ private func tableView(tableView: UITableView, homeKitObjectCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ // Grab the object associated with this indexPath.
+ let homeKitObject = homeKitObjectAtIndexPath(indexPath)
+
+ // Get the name of the object.
+ let name: String
+ switch HomeKitObjectSection(rawValue: indexPath.section) {
+ case .Accessory?:
+ let accessory = homeKitObject as! HMAccessory
+ name = accessory.name
+
+ case .Room?:
+ let room = homeKitObject as! HMRoom
+ name = self.home.nameForRoom(room)
+
+ case .Zone?:
+ let zone = homeKitObject as! HMZone
+ name = zone.name
+ case .User?:
+ name = ""
+
+ case .ActionSet?:
+ let actionSet = homeKitObject as! HMActionSet
+ name = actionSet.name
+
+ case .Trigger?:
+ let trigger = homeKitObject as! HMTrigger
+ name = trigger.name
+
+ case .ServiceGroup?:
+ let serviceGroup = homeKitObject as! HMServiceGroup
+ name = serviceGroup.name
+
+ case nil:
+ fatalError("Unexpected `HomeKitObjectSection` raw value.")
+ }
+
+
+ // Grab the appropriate reuse identifier for this index path.
+ let reuseIdentifier = reuseIdentifierForIndexPath(indexPath)
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
+ cell.textLabel?.text = name
+
+ return cell
+ }
+
+ /// Allows users to remove HomeKit object rows if they are the admin of the home.
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ let homeKitObject = homeKitObjectAtIndexPath(indexPath)
+
+ if !home.isAdmin {
+ return false
+ }
+
+ if let actionSet = homeKitObject as? HMActionSet where actionSet.isBuiltIn {
+ // We cannot remove built-in action sets.
+ return false
+ }
+
+ // Any row that is not an 'add' row, and is not the roomForEntireHome, can be removed.
+ return !(homeKitObject as? NSObject == home.roomForEntireHome() || cellTypeForIndexPath(indexPath) == .Add)
+ }
+
+ /// Removes the HomeKit object at the specified index path.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ let homeKitObject = homeKitObjectAtIndexPath(indexPath)!
+
+ // Remove the object from the data structure. If it fails put it back.
+ didRemoveHomeKitObject(homeKitObject)
+ removeHomeKitObject(homeKitObject) { error in
+ guard let error = error else { return }
+
+ self.displayError(error)
+ self.didAddHomeKitObject(homeKitObject)
+ }
+ }
+ }
+
+ /// Handles cell selection based on the cell type.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ let cell = tableView.cellForRowAtIndexPath(indexPath)!
+
+ guard cell.selectionStyle != .None else { return }
+
+ guard let section = HomeKitObjectSection(rawValue: indexPath.section) else {
+ fatalError("Unexpected `HomeKitObjectSection` raw value.")
+ }
+
+ if cellTypeForIndexPath(indexPath) == .Add{
+ switch section {
+ case .Accessory:
+ browseForAccessories()
+
+ case .Room:
+ addNewRoom()
+
+ case .Zone:
+ addNewZone()
+
+ case .User:
+ manageUsers()
+
+ case .ActionSet:
+ addNewActionSet()
+
+ case .Trigger:
+ addNewTrigger()
+
+ case .ServiceGroup:
+ addNewServiceGroup()
+ }
+ }
+ else if section == .ActionSet {
+ let selectedActionSet = homeKitObjectAtIndexPath(indexPath) as! HMActionSet
+ executeActionSet(selectedActionSet)
+ }
+ }
+
+ /// Handles an accessory button tap based on the section.
+ override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
+ let cell = tableView.cellForRowAtIndexPath(indexPath)
+
+ if HomeKitObjectSection(rawValue: indexPath.section) == .Trigger {
+ let trigger = homeKitObjectAtIndexPath(indexPath)
+
+ switch trigger {
+ case is HMTimerTrigger:
+ performSegueWithIdentifier(Identifiers.showTimerTriggerSegue, sender: cell)
+
+ case let eventTrigger as HMEventTrigger:
+ if eventTrigger.isLocationEvent {
+ performSegueWithIdentifier(Identifiers.showLocationTriggerSegue, sender: cell)
+ }
+ else {
+ performSegueWithIdentifier(Identifiers.showCharacteristicTriggerSegue, sender: cell)
+ }
+
+ default: break
+ }
+ }
+ }
+
+ // MARK: Action Methods
+
+ /// Presents an alert controller to allow the user to choose a trigger type.
+ private func addNewTrigger() {
+ let title = NSLocalizedString("Add Trigger", comment: "Add Trigger")
+ let alertController = UIAlertController(title: title, message: nil, preferredStyle: .ActionSheet)
+
+ // Timer trigger
+ let timeAction = UIAlertAction(title: NSLocalizedString("Time", comment: "Time"), style: .Default) { _ in
+ self.performSegueWithIdentifier(Identifiers.addTimerTriggerSegue, sender: self)
+ }
+ alertController.addAction(timeAction)
+
+ // Characteristic trigger
+ let eventAction = UIAlertAction(title: NSLocalizedString("Characteristic", comment: "Characteristic"), style: .Default) { _ in
+ self.performSegueWithIdentifier(Identifiers.addCharacteristicTriggerSegue, sender: self)
+ }
+ alertController.addAction(eventAction)
+
+ // Location trigger
+ let locationAction = UIAlertAction(title: NSLocalizedString("Location", comment: "Location"), style: .Default) { _ in
+ self.performSegueWithIdentifier(Identifiers.addLocationTriggerSegue, sender: self)
+ }
+ alertController.addAction(locationAction)
+
+ // Cancel
+ let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel, handler: nil)
+ alertController.addAction(cancelAction)
+
+ // Present alert
+ presentViewController(alertController, animated: true, completion: nil)
+ }
+
+ /// Navigates into the action set view controller.
+ private func addNewActionSet() {
+ performSegueWithIdentifier(Identifiers.addActionSetSegue, sender: self)
+ }
+
+ /// Navigates into the browse accessory view controller.
+ private func browseForAccessories() {
+ performSegueWithIdentifier(Identifiers.addAccessoriesSegue, sender: self)
+ }
+
+ // MARK: Dialog Creation Methods
+
+ /// Presents a dialog to name a new room and generates the HomeKit object if committed.
+ private func addNewRoom() {
+ presentAddAlertWithAttributeType(NSLocalizedString("Room", comment: "Room"),
+ placeholder: NSLocalizedString("Living Room", comment: "Living Room")) { roomName in
+ self.addRoomWithName(roomName)
+ }
+ }
+
+ /// Presents a dialog to name a new service group and generates the HomeKit object if committed.
+ private func addNewServiceGroup() {
+ presentAddAlertWithAttributeType(NSLocalizedString("Service Group", comment: "Service Group"),
+ placeholder: NSLocalizedString("Group", comment: "Group")) { groupName in
+ self.addServiceGroupWithName(groupName)
+ }
+ }
+
+ /// Presents a dialog to name a new zone and generates the HomeKit object if committed.
+ private func addNewZone() {
+ presentAddAlertWithAttributeType(NSLocalizedString("Zone", comment: "Zone"),
+ placeholder: NSLocalizedString("Upstairs", comment: "Upstairs")) { zoneName in
+ self.addZoneWithName(zoneName)
+ }
+ }
+
+ // MARK: HomeKit Object Creation and Deletion
+
+ /**
+ Switches based on the type of object attempts to remove the HomeKit object
+ from the curret home.
+
+ - parameter object: The HomeKit object to remove.
+ - parameter completionHandler: The closure to invote when the removal has been completed.
+ */
+ private func removeHomeKitObject(object: AnyObject, completionHandler: NSError? -> Void) {
+ switch object {
+ case let actionSet as HMActionSet:
+ home.removeActionSet(actionSet) { error in
+ completionHandler(error)
+ self.updateActionSetSection()
+ }
+
+ case let accessory as HMAccessory:
+ home.removeAccessory(accessory, completionHandler: completionHandler)
+
+ case let room as HMRoom:
+ home.removeRoom(room, completionHandler: completionHandler)
+
+ case let zone as HMZone:
+ home.removeZone(zone, completionHandler: completionHandler)
+
+ case let trigger as HMTrigger:
+ home.removeTrigger(trigger, completionHandler: completionHandler)
+
+ case let serviceGroup as HMServiceGroup:
+ home.removeServiceGroup(serviceGroup, completionHandler: completionHandler)
+
+ default:
+ fatalError("Attempted to remove unknown HomeKit object.")
+ }
+ }
+
+ /**
+ Adds a room to the current home.
+
+ - parameter name: The name of the new room.
+ */
+ private func addRoomWithName(name: String) {
+ home.addRoomWithName(name) { newRoom, error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+
+ self.didAddHomeKitObject(newRoom)
+ }
+ }
+
+ /**
+ Adds a service group to the current home.
+
+ - parameter name: The name of the new service group.
+ */
+ private func addServiceGroupWithName(name: String) {
+ home.addServiceGroupWithName(name) { newGroup, error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+
+ self.didAddHomeKitObject(newGroup)
+ }
+ }
+
+ /**
+ Adds a zone to the current home.
+
+ - parameter name: The name of the new zone.
+ */
+ private func addZoneWithName(name: String) {
+ home.addZoneWithName(name) { newZone, error in
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+
+ self.didAddHomeKitObject(newZone)
+ }
+ }
+
+ /// Presents modal view for managing users.
+ private func manageUsers() {
+ home.manageUsersWithCompletionHandler { error in
+ if let error = error {
+ self.displayError(error)
+ }
+ }
+ }
+
+ /**
+ Checks to see if an action set has any actions.
+ If actions exists, the action set will be executed.
+ Otherwise, the user will be alerted.
+
+ - parameter actionSet: The `HMActionSet` to evaluate and execute.
+ */
+ private func executeActionSet(actionSet: HMActionSet) {
+ if actionSet.actions.isEmpty {
+ let alertTitle = NSLocalizedString("Empty Scene", comment: "Empty Scene")
+
+ let alertMessage = NSLocalizedString("This scene is empty. To set this scene, first add some actions to it.", comment: "Empty Scene Description")
+
+ displayMessage(alertTitle, message: alertMessage)
+
+ return
+ }
+
+ home.executeActionSet(actionSet) { error in
+ guard let error = error else { return }
+
+ self.displayError(error)
+ }
+ }
+
+ /**
+ Adds the HomeKit object into the object collection and inserts the new row into the section.
+
+ - parameter object: The HomeKit object to add.
+ */
+ private func didAddHomeKitObject(object: AnyObject?) {
+ if let object = object {
+ objectCollection.append(object)
+ if let newObjectIndexPath = objectCollection.indexPathOfObject(object) {
+ tableView.insertRowsAtIndexPaths([newObjectIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+ }
+
+ /**
+ Finds the `NSIndexPath` of the specified object and reloads it in the table view.
+
+ - parameter object: The HomeKit object that was modified.
+ */
+ private func didModifyHomeKitObject(object: AnyObject?) {
+ if let object = object,
+ objectIndexPath = objectCollection.indexPathOfObject(object) {
+ tableView.reloadRowsAtIndexPaths([objectIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /**
+ Removes the HomeKit object from the object collection and then deletes the row from the section.
+
+ - parameter object: The HomeKit object to remove.
+ */
+ private func didRemoveHomeKitObject(object: AnyObject?) {
+ if let object = object,
+ objectIndexPath = objectCollection.indexPathOfObject(object) {
+ objectCollection.remove(object)
+ tableView.deleteRowsAtIndexPaths([objectIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /*
+ The following methods call the above helper methds to handle
+ the addition, removal, and modification of HomeKit objects.
+ */
+
+ // MARK: HMHomeDelegate Methods
+
+ func homeDidUpdateName(home: HMHome) {
+ navigationItem.title = home.name
+ reloadTable()
+ }
+
+ func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
+ didAddHomeKitObject(accessory)
+ accessory.delegate = self
+ }
+
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ didRemoveHomeKitObject(accessory)
+ }
+
+ // MARK: Triggers
+
+ func home(home: HMHome, didAddTrigger trigger: HMTrigger) {
+ didAddHomeKitObject(trigger)
+ }
+
+ func home(home: HMHome, didRemoveTrigger trigger: HMTrigger) {
+ didRemoveHomeKitObject(trigger)
+ }
+
+ func home(home: HMHome, didUpdateNameForTrigger trigger: HMTrigger) {
+ didModifyHomeKitObject(trigger)
+ }
+
+ // MARK: Service Groups
+
+ func home(home: HMHome, didAddServiceGroup group: HMServiceGroup) {
+ didAddHomeKitObject(group)
+ }
+
+ func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) {
+ didRemoveHomeKitObject(group)
+ }
+
+ func home(home: HMHome, didUpdateNameForServiceGroup group: HMServiceGroup) {
+ didModifyHomeKitObject(group)
+ }
+
+ // MARK: Action Sets
+
+ func home(home: HMHome, didAddActionSet actionSet: HMActionSet) {
+ didAddHomeKitObject(actionSet)
+ }
+
+ func home(home: HMHome, didRemoveActionSet actionSet: HMActionSet) {
+ didRemoveHomeKitObject(actionSet)
+ }
+
+ func home(home: HMHome, didUpdateNameForActionSet actionSet: HMActionSet) {
+ didModifyHomeKitObject(actionSet)
+ }
+
+ // MARK: Zones
+
+ func home(home: HMHome, didAddZone zone: HMZone) {
+ didAddHomeKitObject(zone)
+ }
+
+ func home(home: HMHome, didRemoveZone zone: HMZone) {
+ didRemoveHomeKitObject(zone)
+ }
+
+ func home(home: HMHome, didUpdateNameForZone zone: HMZone) {
+ didModifyHomeKitObject(zone)
+ }
+
+ // MARK: Rooms
+
+ func home(home: HMHome, didAddRoom room: HMRoom) {
+ didAddHomeKitObject(room)
+ }
+
+ func home(home: HMHome, didRemoveRoom room: HMRoom) {
+ didRemoveHomeKitObject(room)
+ }
+
+ func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
+ didModifyHomeKitObject(room)
+ }
+
+ // MARK: Accessories
+
+ func accessoryDidUpdateReachability(accessory: HMAccessory) {
+ didModifyHomeKitObject(accessory)
+ }
+
+ func accessoryDidUpdateName(accessory: HMAccessory) {
+ didModifyHomeKitObject(accessory)
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Rooms/RoomViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Rooms/RoomViewController.swift
new file mode 100644
index 00000000..f0ccec8b
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Rooms/RoomViewController.swift
@@ -0,0 +1,266 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `RoomViewController` lists the accessory within a room.
+*/
+
+
+import UIKit
+import HomeKit
+
+/// A view controller that lists the accessories within a room.
+class RoomViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let accessoryCell = "AccessoryCell"
+ static let unreachableAccessoryCell = "UnreachableAccessoryCell"
+ static let modifyAccessorySegue = "Modify Accessory"
+ }
+
+ // MARK: Properties
+
+ var room: HMRoom! {
+ didSet {
+ navigationItem.title = room.name
+ }
+ }
+
+ var accessories = [HMAccessory]()
+
+ // MARK: View Methods
+
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ reloadData()
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of accessories within this room.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let rows = accessories.count
+ if rows == 0 {
+ let message = NSLocalizedString("No Accessories", comment: "No Accessories")
+ setBackgroundMessage(message)
+ }
+ else {
+ setBackgroundMessage(nil)
+ }
+
+ return rows
+ }
+
+ /// - returns: `true` if the current room is not the home's roomForEntireHome; `false` otherwise.
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ return room != home.roomForEntireHome()
+ }
+
+ /// - returns: Localized "Unassign".
+ override func tableView(tableView: UITableView, titleForDeleteConfirmationButtonForRowAtIndexPath indexPath: NSIndexPath) -> String? {
+ return NSLocalizedString("Unassign", comment: "Unassign")
+ }
+
+ /// Assigns the 'deleted' room to the home's roomForEntireHome.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ unassignAccessory(accessories[indexPath.row])
+ }
+ }
+
+ /// - returns: A cell representing an accessory.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let accessory = accessories[indexPath.row]
+
+ var reuseIdentifier = Identifiers.accessoryCell
+
+ if !accessory.reachable {
+ reuseIdentifier = Identifiers.unreachableAccessoryCell
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
+
+ cell.textLabel?.text = accessory.name
+
+ return cell
+ }
+
+ /// - returns: A localized description, "Accessories" if there are accessories to list.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ if accessories.isEmpty {
+ return nil
+ }
+
+ return NSLocalizedString("Accessories", comment: "Accessories")
+ }
+
+ // MARK: Helper Methods
+
+ /// Updates the internal array of accessories and reloads the table view.
+ private func reloadData() {
+ accessories = room.accessories.sortByLocalizedName()
+ tableView.reloadData()
+ }
+
+ /// Sorts the internal list of accessories by localized name.
+ private func sortAccessories() {
+ accessories = accessories.sortByLocalizedName()
+ }
+
+ /**
+ Registers as the delegate for the current home and
+ all accessories in our room.
+ */
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+ for accessory in room.accessories {
+ accessory.delegate = self
+ }
+ }
+
+ /// Sets the accessory and home of the modifyAccessoryViewController that will be presented.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
+ if segue.identifier == Identifiers.modifyAccessorySegue {
+ let modifyViewController = segue.intendedDestinationViewController as! ModifyAccessoryViewController
+ modifyViewController.accessory = room.accessories[indexPath.row]
+ }
+ }
+
+ /**
+ Adds an accessory into the internal list of accessories
+ and inserts the row into the table view.
+
+ - parameter accessory: The `HMAccessory` to add.
+ */
+ private func didAssignAccessory(accessory: HMAccessory) {
+ accessories.append(accessory)
+ sortAccessories()
+ if let newAccessoryIndex = accessories.indexOf(accessory) {
+ let newAccessoryIndexPath = NSIndexPath(forRow: newAccessoryIndex, inSection: 0)
+ tableView.insertRowsAtIndexPaths([newAccessoryIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /**
+ Removes an accessory from the internal list of accessory (if it
+ exists) and deletes the row from the table view.
+
+ - parameter accessory: The `HMAccessory` to remove.
+ */
+ private func didUnassignAccessory(accessory: HMAccessory) {
+ if let accessoryIndex = accessories.indexOf(accessory) {
+ accessories.removeAtIndex(accessoryIndex)
+ let accessoryIndexPath = NSIndexPath(forRow: accessoryIndex, inSection: 0)
+ tableView.deleteRowsAtIndexPaths([accessoryIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /**
+ Assigns an accessory to the current room.
+
+ - parameter accessory: The `HMAccessory` to assign to the room.
+ */
+ private func assignAccessory(accessory: HMAccessory) {
+ didAssignAccessory(accessory)
+ home.assignAccessory(accessory, toRoom: room) { error in
+ if let error = error {
+ self.displayError(error)
+ self.didUnassignAccessory(accessory)
+ }
+ }
+ }
+
+ /**
+ Assigns the current room back into `roomForEntireHome`.
+
+ - parameter accessory: The `HMAccessory` to reassign.
+ */
+ private func unassignAccessory(accessory: HMAccessory) {
+ didUnassignAccessory(accessory)
+ home.assignAccessory(accessory, toRoom: home.roomForEntireHome()) { error in
+ if let error = error {
+ self.displayError(error)
+ self.didAssignAccessory(accessory)
+ }
+ }
+ }
+
+ /**
+ Finds an accessory in the internal array of accessories
+ and updates its row in the table view.
+
+ - parameter accessory: The `HMAccessory` to reload.
+ */
+ func didModifyAccessory(accessory: HMAccessory){
+ if let index = accessories.indexOf(accessory) {
+ let indexPaths = [
+ NSIndexPath(forRow: index, inSection: 0)
+ ]
+
+ tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// If the accessory was added to this room, insert it.
+ func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
+ if accessory.room == room {
+ accessory.delegate = self
+ didAssignAccessory(accessory)
+ }
+ }
+
+ /// Remove the accessory from our room, if required.
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ didUnassignAccessory(accessory)
+ }
+
+ /**
+ Handles the update.
+
+ We act based on one of three options:
+
+ 1. A new accessory is being added to this room.
+ 2. An accessory is being assigned from this room to another room.
+ 3. We can ignore this message.
+ */
+ func home(home: HMHome, didUpdateRoom room: HMRoom, forAccessory accessory: HMAccessory) {
+ if room == self.room {
+ didAssignAccessory(accessory)
+ }
+ else if accessories.contains(accessory) {
+ didUnassignAccessory(accessory)
+ }
+ }
+
+ /// If our room was removed, pop back.
+ func home(home: HMHome, didRemoveRoom room: HMRoom) {
+ if room == self.room {
+ navigationController!.popViewControllerAnimated(true)
+ }
+ }
+
+ /// If our room was renamed, reload our title.
+ func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
+ if room == self.room {
+ navigationItem.title = room.name
+ }
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ // Accessory updates will reload the cell for the accessory.
+
+ func accessoryDidUpdateReachability(accessory: HMAccessory) {
+ didModifyAccessory(accessory)
+ }
+
+ func accessoryDidUpdateName(accessory: HMAccessory) {
+ didModifyAccessory(accessory)
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Service Groups/AddServicesViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Service Groups/AddServicesViewController.swift
new file mode 100644
index 00000000..fefe63cc
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Service Groups/AddServicesViewController.swift
@@ -0,0 +1,209 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `AddServicesViewController` allows users to add services to a service group.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ A view controller that provides a list of services and lets the user select services to be added to the provided Service Group.
+
+ The services are not added to the service group until the 'Done' button is pressed.
+*/
+class AddServicesViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let serviceCell = "ServiceCell"
+ }
+
+ // MARK: Properties
+
+ lazy private var displayedAccessories = [HMAccessory]()
+ lazy private var displayedServicesForAccessory = [HMAccessory: [HMService]]()
+ lazy private var selectedServices = [HMService]()
+
+ var serviceGroup: HMServiceGroup!
+
+ // MARK: View Methods
+
+ /// Reloads internal data and view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ selectedServices = []
+ reloadTable()
+ }
+
+ /// Registers as the delegate for the home and all accessories.
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+ for accessory in homeStore.home!.accessories {
+ accessory.delegate = self
+ }
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of displayed accessories.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return displayedAccessories.count
+ }
+
+ /// - returns: The number of displayed services for the provided accessory.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let accessory = displayedAccessories[section]
+ return displayedServicesForAccessory[accessory]!.count
+ }
+
+ /// - returns: A configured `ServiceCell`.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) as! ServiceCell
+
+ let service = serviceAtIndexPath(indexPath)
+
+ cell.includeAccessoryText = false
+ cell.service = service
+ cell.accessoryType = selectedServices.contains(service) ? .Checkmark : .None
+
+ return cell
+ }
+
+ /**
+ When an indexPath is selected, this function either adds or removes the selected service from the
+ service group.
+ */
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ // Get the service associated with this index.
+ let service = serviceAtIndexPath(indexPath)
+
+ // Call the appropriate add/remove operation with the closure from above.
+ if let index = selectedServices.indexOf(service) {
+ selectedServices.removeAtIndex(index)
+ }
+ else {
+ selectedServices.append(service)
+ }
+
+ tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+
+ /// - returns: The name of the displayed accessory at the given section.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return displayedAccessories[section].name
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Adds the selected services to the service group.
+
+ Calls the provided completion handler once all services have been added.
+ */
+ func addSelectedServicesWithCompletionHandler(completion: () -> Void) {
+ // Create a dispatch group for each of the service additions.
+ let addServicesGroup = dispatch_group_create()
+ for service in selectedServices {
+ dispatch_group_enter(addServicesGroup)
+ serviceGroup.addService(service) { error in
+ if let error = error {
+ self.displayError(error)
+ }
+ dispatch_group_leave(addServicesGroup)
+ }
+ }
+ dispatch_group_notify(addServicesGroup, dispatch_get_main_queue(), completion)
+ }
+
+ /**
+ Finds the service at a specific index path.
+
+ - parameter indexPath: An `NSIndexPath`
+
+ - returns: The `HMService` at the given index path.
+ */
+ private func serviceAtIndexPath(indexPath: NSIndexPath) -> HMService {
+ let accessory = displayedAccessories[indexPath.section]
+ let services = displayedServicesForAccessory[accessory]!
+ return services[indexPath.row]
+ }
+
+ /**
+ Commits the changes to the service group
+ and dismisses the view.
+ */
+ @IBAction func dismiss() {
+ addSelectedServicesWithCompletionHandler {
+ self.dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+
+ /// Resets internal data and view.
+ func reloadTable() {
+ resetDisplayedServices()
+ tableView.reloadData()
+ }
+
+ /**
+ Updates internal array of accessories and the mapping
+ of accessories to selected services.
+ */
+ func resetDisplayedServices() {
+ displayedAccessories = []
+ let allAccessories = home.accessories.sortByLocalizedName()
+ displayedServicesForAccessory = [:]
+ for accessory in allAccessories {
+ var displayedServices = [HMService]()
+ for service in accessory.services {
+ if !serviceGroup.services.contains(service) && service.serviceType != HMServiceTypeAccessoryInformation {
+ displayedServices.append(service)
+ }
+ }
+
+ // Only add the accessory if it has displayed services.
+ if !displayedServices.isEmpty {
+ displayedServicesForAccessory[accessory] = displayedServices.sortByLocalizedName()
+ displayedAccessories.append(accessory)
+ }
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// Dismisses the view controller if our service group was removed.
+ func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) {
+ if serviceGroup == group {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+
+ /// Reloads the view if an accessory was added to HomeKit.
+ func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
+ reloadTable()
+ accessory.delegate = self
+ }
+
+ /// Dismisses the view controller if we no longer have accesories.
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ if home.accessories.isEmpty {
+ navigationController?.dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ reloadTable()
+ }
+
+ // MARK: HMAccessoryDelegate Methods
+
+ // Accessory changes reload the data and view.
+
+ func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
+ reloadTable()
+ }
+
+ func accessoryDidUpdateServices(accessory: HMAccessory) {
+ reloadTable()
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Service Groups/ServiceGroupViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Service Groups/ServiceGroupViewController.swift
new file mode 100644
index 00000000..6ac6337f
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Service Groups/ServiceGroupViewController.swift
@@ -0,0 +1,246 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ServiceGroupViewController` allows users to modify service groups.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller that allows the user to add services to a service group.
+class ServiceGroupViewController: HMCatalogViewController, HMAccessoryDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let serviceCell = "ServiceCell"
+ static let addServicesSegue = "Add Services Plus"
+ }
+
+ // MARK: Properties
+
+ @IBOutlet weak var plusButton: UIBarButtonItem!
+
+ var serviceGroup: HMServiceGroup!
+ lazy private var accessories = [HMAccessory]()
+ lazy private var servicesForAccessory = [HMAccessory: [HMService]]()
+
+ // MARK: View Methods
+
+ /// Reloads the view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ title = serviceGroup.name
+ reloadData()
+ }
+
+ /// Pops the view controller if our data is invalid.
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+ if shouldPopViewController() {
+ navigationController?.popViewControllerAnimated(true)
+ }
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ Generates the number of sections and adds a table view
+ back ground message, if required.
+
+ - returns: The number of accessories in the service group.
+ */
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ let sections = accessories.count
+ if sections == 0 {
+ setBackgroundMessage(NSLocalizedString("No Services", comment: "No Services"))
+ }
+ else {
+ setBackgroundMessage(nil)
+ }
+
+ return sections
+ }
+
+ /// - returns: The number of services for the accessory at the specified section.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let accessory = accessories[section]
+ let services = servicesForAccessory[accessory]
+
+ return services?.count ?? 0
+ }
+
+ /// - returns: The name of the accessory at the specified section.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return accessories[section].name
+ }
+
+ /// All cells in the table view represent services and can be deleted.
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ return true
+ }
+
+ /// - returns: A `ServiceCell` with the service at the given index path.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) as! ServiceCell
+ let service = serviceAtIndexPath(indexPath)
+ cell.includeAccessoryText = false
+ cell.service = service
+ return cell
+ }
+
+ /**
+ - returns: `true` if there are any services not already in the service group;
+ `false` otherwise.
+ */
+ private func shouldEnableAdd() -> Bool {
+ let unAddedServices = home.servicesNotAlreadyInServiceGroup(serviceGroup)
+ return unAddedServices.count != 0
+ }
+
+ /// Deleting a cell removes the corresponding service from the service group.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ removeServiceAtIndexPath(indexPath)
+ }
+ }
+
+ /**
+ Removes the service associated with the cell at a given index path.
+
+ - parameters indexPath: The `NSIndexPath` to remove.
+ */
+ private func removeServiceAtIndexPath(indexPath: NSIndexPath) {
+ let service = serviceAtIndexPath(indexPath)
+ serviceGroup.removeService(service) { error in
+ if let error = error {
+ self.displayError(error)
+ }
+
+ self.reloadData()
+ }
+ }
+
+ /**
+ Finds the service at a given index path.
+
+ - parameter indexPath: An `NSIndexPath`.
+
+ - returns: The service at the given index path
+ */
+ private func serviceAtIndexPath(indexPath: NSIndexPath) -> HMService {
+ let accessory = accessories[indexPath.section]
+ let services = servicesForAccessory[accessory]!
+ return services[indexPath.row]
+ }
+
+ /// Passes the service group into the `AddServicesViewController`
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ if segue.identifier == Identifiers.addServicesSegue {
+ let addServicesVC = segue.intendedDestinationViewController as! AddServicesViewController
+ addServicesVC.serviceGroup = serviceGroup
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Resets accessory and service lists, resets the plus
+ button's enabled status and reloads the table view.
+ */
+ private func reloadData() {
+ resetLists()
+ plusButton.enabled = shouldEnableAdd()
+ tableView.reloadData()
+ }
+
+ /**
+ Resets the accessories array and the service-accessory mapping
+ using the original HomeKit objects.
+ */
+ private func resetLists() {
+ accessories = []
+ servicesForAccessory = [:]
+
+ for service in serviceGroup.services {
+ if let accessory = service.accessory {
+ if servicesForAccessory[accessory] == nil {
+ accessories.append(accessory)
+ servicesForAccessory[accessory] = [service]
+ }
+ else {
+ servicesForAccessory[accessory]?.append(service)
+ }
+ }
+ }
+
+ // Sort all service lists.
+ for accessory in accessories {
+ servicesForAccessory[accessory] = servicesForAccessory[accessory]?.sortByLocalizedName()
+ }
+
+ // Sort accessory list.
+ accessories = accessories.sortByLocalizedName()
+ }
+
+ /**
+ - returns: `true` if our service group is not
+ in the home any more; `false` otherwise.
+ */
+ private func shouldPopViewController() -> Bool {
+ guard let home = homeStore.home else { return true }
+
+ return !home.serviceGroups.contains { group in
+ return group == serviceGroup
+ }
+ }
+
+ /**
+ Registers as the delegate for the home and
+ all accessories which are related to our service group.
+ */
+ override func registerAsDelegate() {
+ super.registerAsDelegate()
+
+ for service in serviceGroup.services {
+ service.accessory?.delegate = self
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// Pops the view controller if our service group has been deleted.
+ func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) {
+ if group == serviceGroup {
+ navigationController?.popViewControllerAnimated(true)
+ }
+ }
+
+ // Home and accessory changes result in a full data reload.
+
+ func home(home: HMHome, didAddService service: HMService, toServiceGroup group: HMServiceGroup) {
+ if serviceGroup == group {
+ reloadData()
+ }
+ }
+
+ func home(home: HMHome, didRemoveService service: HMService, fromServiceGroup group: HMServiceGroup) {
+ if serviceGroup == group {
+ reloadData()
+ }
+ }
+
+ func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
+ reloadData()
+ }
+
+ func accessoryDidUpdateServices(accessory: HMAccessory) {
+ reloadData()
+ }
+
+ func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
+ reloadData()
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicSelectionViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicSelectionViewController.swift
new file mode 100644
index 00000000..2f5028e6
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicSelectionViewController.swift
@@ -0,0 +1,110 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `CharacteristicSelectionViewController` allows for the selection of characteristics.
+ This is mainly used for creating characteristic events and conditions
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ Allows for the selection of characteristics.
+ This is mainly used for creating characteristic events and conditions
+*/
+class CharacteristicSelectionViewController: HMCatalogViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let accessoryCell = "AccessoryCell"
+ static let unreachableAccessoryCell = "UnreachableAccessoryCell"
+ static let showServicesSegue = "Show Services"
+ }
+
+ // MARK: Properties
+
+ var eventTrigger: HMEventTrigger?
+ var triggerCreator: EventTriggerCreator!
+
+ /// An internal copy of all controllable accessories in the home.
+ private var accessories = [HMAccessory]()
+
+ @IBOutlet weak var saveButton: UIBarButtonItem!
+
+ // MARK: View Methods
+
+ /// Resets the internal array of accessories from the home.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ // Only take accessories which have one control service.
+ accessories = home.sortedControlAccessories
+ }
+
+ /// Configures the `ServicesViewController` and passes it the correct accessory.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ if segue.identifier == Identifiers.showServicesSegue {
+ let senderCell = sender as! UITableViewCell
+ let servicesVC = segue.intendedDestinationViewController as! ServicesViewController
+ let cellIndex = tableView.indexPathForCell(senderCell)!.row
+ servicesVC.allowsAllWrites = true
+ servicesVC.onlyShowsControlServices = true
+ servicesVC.accessory = accessories[cellIndex]
+ servicesVC.cellDelegate = triggerCreator
+ }
+ }
+
+ // MARK: IBAction Methods
+
+ /**
+ Updates the predicates in the trigger creator and then
+ dismisses the view controller.
+ */
+ @IBAction func didTapSave(sender: UIBarButtonItem) {
+ /*
+ We should not save the trigger completely, the user still has a chance to bail out.
+ Instead, we generate all of the predicates that were in the map.
+ */
+ triggerCreator.updatePredicates()
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ // MARK: Table View Methods
+
+ /// Single section view controller.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return 1
+ }
+
+ /// - returns: The number of accessories. If there are none, will return 1 (for the 'none row').
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return max(accessories.count, 1)
+ }
+
+ /// - returns: An Accessory cell that contains an accessory's name.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let accessory = accessories.sortByLocalizedName()[indexPath.row]
+ let cellIdentifier = accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
+ cell.textLabel?.text = accessory.name
+
+ return cell
+ }
+
+ /// Shows the services in the selected accessory.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ let cell = tableView.cellForRowAtIndexPath(indexPath)!
+ if cell.selectionStyle == .None {
+ return
+ }
+ performSegueWithIdentifier(Identifiers.showServicesSegue, sender: cell)
+ }
+
+ /// - returns: Localized "Accessories" string.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return NSLocalizedString("Accessories", comment: "Accessories")
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerCreator.swift
new file mode 100644
index 00000000..e4eb4e1c
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerCreator.swift
@@ -0,0 +1,254 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `CharacteristicTriggerCreator` creates characteristic triggers.
+*/
+
+import UIKit
+import HomeKit
+
+/// Represents modes for a `CharacteristicTriggerCreator`.
+enum CharacteristicTriggerCreatorMode: Int {
+ case Event, Condition
+}
+
+/**
+ An `EventTriggerCreator` subclass which allows for the creation
+ of characteristic triggers.
+*/
+class CharacteristicTriggerCreator: EventTriggerCreator {
+ // MARK: Properties
+
+ var eventTrigger: HMEventTrigger? {
+ return self.trigger as? HMEventTrigger
+ }
+
+ /**
+ This object will be a characteristic cell delegate and will therefore
+ be receiving updates when UI elements change value. However, this object
+ can construct both characteristic events and characteristic triggers.
+ Setting the `mode` determines how this trigger creator will handle
+ cell delegate callbacks.
+ */
+ var mode: CharacteristicTriggerCreatorMode = .Event
+
+ /**
+ Contains the new pending mapping of `HMCharacteristic`s to their trigger (`NSCopying`) values.
+ When `saveTriggerWithName(name:completion:)` is called, all of these mappings will be converted
+ into `HMCharacteristicEvent`s and added to the `HMEventTrigger`.
+ */
+ private let targetValueMap = NSMapTable.strongToStrongObjectsMapTable()
+
+ /// `HMCharacteristicEvent`s that should be removed if `saveTriggerWithName(name:completion:)` is called.
+ private var removalCharacteristicEvents = [HMCharacteristicEvent]()
+
+ // MARK: Trigger Creator Methods
+
+ /// Syncs the stored event trigger using internal values.
+ override func updateTrigger() {
+ guard let eventTrigger = eventTrigger else { return }
+ matchEventsFromTriggerIfNecessary()
+ removePendingEventsFromTrigger()
+ for (characteristic, triggerValue) in pairsFromMapTable(targetValueMap) {
+ let newEvent = HMCharacteristicEvent(characteristic: characteristic, triggerValue: triggerValue)
+ dispatch_group_enter(self.saveTriggerGroup)
+ eventTrigger.addEvent(newEvent) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+ savePredicate()
+ }
+
+ /**
+ - returns: A new `HMEventTrigger` with the pending
+ characteristic events and constructed predicate.
+ */
+ override func newTrigger() -> HMTrigger? {
+ return HMEventTrigger(name: name, events: pendingCharacteristicEvents, predicate: newPredicate())
+ }
+
+ /**
+ Remove all objects from the map so they don't show up
+ in the `events` computed array.
+ */
+ override func cleanUp() {
+ targetValueMap.removeAllObjects()
+ }
+
+ /**
+ Removes an event from the map table if it's a new event and
+ queues it for removal if it already existed in the event trigger.
+
+ - parameter event: `HMCharacteristicEvent` to be removed.
+ */
+ func removeEvent(event: HMCharacteristicEvent) {
+ if targetValueMap.objectForKey(event.characteristic) != nil {
+ // Remove the characteristic from the target value map.
+ targetValueMap.removeObjectForKey(event.characteristic)
+ }
+
+ if let characteristicEvents = eventTrigger?.characteristicEvents where characteristicEvents.contains(event) {
+ // If the given event is in the event array, queue it for removal.
+ removalCharacteristicEvents.append(event)
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Any characteristic events in the map table that have not yet been
+ added to the trigger.
+ */
+ var pendingCharacteristicEvents: [HMCharacteristicEvent] {
+ return pairsFromMapTable(targetValueMap).map { (characteristic, triggerValue) -> HMCharacteristicEvent in
+ return HMCharacteristicEvent(characteristic: characteristic, triggerValue: triggerValue)
+ }
+ }
+
+ /**
+ Loops through the characteristic events in the trigger.
+ If any characteristics in our map table are also in the event,
+ replace the value with the one we have stored and remove that entry from
+ our map table.
+ */
+ private func matchEventsFromTriggerIfNecessary() {
+ guard let eventTrigger = eventTrigger else { return }
+ for event in eventTrigger.characteristicEvents {
+ // Find events who's characteristic is in our map table.
+ if let triggerValue = targetValueMap.objectForKey(event.characteristic) as? NSCopying {
+ dispatch_group_enter(self.saveTriggerGroup)
+ event.updateTriggerValue(triggerValue) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+ }
+ }
+
+ /**
+ Removes all `HMCharacteristicEvent`s from the `removalCharacteristicEvents`
+ array and stores any errors that accumulate.
+ */
+ private func removePendingEventsFromTrigger() {
+ guard let eventTrigger = eventTrigger else { return }
+ for event in removalCharacteristicEvents {
+ dispatch_group_enter(saveTriggerGroup)
+ eventTrigger.removeEvent(event) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+ removalCharacteristicEvents.removeAll()
+ }
+
+
+
+ /// All `HMCharacteristic`s in the `targetValueMap`.
+ private var allCharacteristics: [HMCharacteristic] {
+ var characteristics = Set()
+ for characteristic in targetValueMap.keyEnumerator().allObjects as! [HMCharacteristic] {
+ characteristics.insert(characteristic)
+ }
+ return Array(characteristics)
+ }
+
+ /**
+ Saves a characteristic and value into the pending map
+ of characteristic events.
+
+ - parameter value: The value of the characteristic.
+ - parameter characteristic: The `HMCharacteristic` that has been updated.
+ */
+ private func updateEventValue(value: AnyObject, forCharacteristic characteristic: HMCharacteristic) {
+ for (index, event) in removalCharacteristicEvents.enumerate() {
+ if event.characteristic == characteristic {
+ /*
+ We have this event pending for deletion,
+ but we are going to want to update it.
+ remove it from the removal array.
+ */
+ removalCharacteristicEvents.removeAtIndex(index)
+ break
+ }
+ }
+ targetValueMap.setObject(value, forKey: characteristic)
+ }
+
+ /**
+ The current, sorted collection of `HMCharacteristicEvent`s accumulated by
+ filtering out the events pending removal from the original trigger events and
+ then adding new pending events.
+ */
+ var events: [HMCharacteristicEvent] {
+ let characteristicEvents = eventTrigger?.characteristicEvents ?? []
+
+ let originalEvents = characteristicEvents.filter {
+ return !removalCharacteristicEvents.contains($0)
+ }
+
+ let allEvents = originalEvents + pendingCharacteristicEvents
+
+ return allEvents.sort { (event1: HMCharacteristicEvent, event2: HMCharacteristicEvent) in
+ let type1 = event1.characteristic.localizedCharacteristicType
+ let type2 = event2.characteristic.localizedCharacteristicType
+ return type1.localizedCompare(type2) == .OrderedAscending
+ }
+ }
+
+ // MARK: CharacteristicCellDelegate Methods
+
+ /**
+ If the mode is event, update the event value.
+ Otherwise, default to super implementation
+ */
+ override func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) {
+ switch mode {
+ case .Event:
+ updateEventValue(value, forCharacteristic: characteristic)
+
+ default:
+ super.characteristicCell(cell, didUpdateValue: value, forCharacteristic: characteristic, immediate: immediate)
+ }
+ }
+
+ /**
+ Tries to find the characteristic in either the event map or the
+ condition map (based on the current mode). Then calls read value.
+ When the value comes back, we check the selected map for the value
+ */
+ override func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) {
+ if mode == .Condition {
+ // This is a condition, fall back to the `EventTriggerCreator` read.
+ super.characteristicCell(cell, readInitialValueForCharacteristic: characteristic, completion: completion)
+ return
+ }
+
+ if let value = targetValueMap.objectForKey(characteristic) {
+ completion(value, nil)
+ return
+ }
+
+ characteristic.readValueWithCompletionHandler { error in
+ /*
+ The user may have updated the cell value while the
+ read was happening. We check the map one more time.
+ */
+ if let value = self.targetValueMap.objectForKey(characteristic) {
+ completion(value, nil)
+ }
+ else {
+ completion(characteristic.value, error)
+ }
+ }
+ }
+
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerViewController.swift
new file mode 100644
index 00000000..15927e7e
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerViewController.swift
@@ -0,0 +1,265 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `CharacteristicTriggerViewController` allows the user to create a characteristic trigger.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller which facilitates the creation of characteristic triggers.
+class CharacteristicTriggerViewController: EventTriggerViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let selectCharacteristicSegue = "Select Characteristic"
+ }
+
+ // MARK: Properties
+
+ private var characteristicTriggerCreator: CharacteristicTriggerCreator {
+ return triggerCreator as! CharacteristicTriggerCreator
+ }
+
+ var eventTrigger: HMEventTrigger? {
+ return trigger as? HMEventTrigger
+ }
+
+ /// An internal array of `HMCharacteristicEvent`s to save into the trigger.
+ private var events = [HMCharacteristicEvent]()
+
+ // MARK: View Methods
+
+ /// Creates the trigger creator.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ triggerCreator = CharacteristicTriggerCreator(trigger: eventTrigger, home: home)
+ }
+
+ /// Reloads the internal data.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ reloadData()
+ }
+
+ /// Passes our event trigger and trigger creator to the `CharacteristicSelectionViewController`
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ if segue.identifier == Identifiers.selectCharacteristicSegue {
+ if let destinationVC = segue.intendedDestinationViewController as? CharacteristicSelectionViewController {
+ destinationVC.eventTrigger = eventTrigger
+ destinationVC.triggerCreator = characteristicTriggerCreator
+ }
+ }
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ - returns: The characteristic events for the Characteristics section.
+ Defaults to super implementation.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sectionForIndex(section) {
+ case .Characteristics?:
+ // Plus one for the add row.
+ return events.count + 1
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, numberOfRowsInSection: section)
+ }
+ }
+
+ /**
+ Switches based on cell type to generate the correct cell for the index path.
+ Defaults to super implementation.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ if indexPathIsAdd(indexPath) {
+ return self.tableView(tableView, addCellForRowAtIndexPath: indexPath)
+ }
+
+ switch sectionForIndex(indexPath.section) {
+ case .Characteristics?:
+ return self.tableView(tableView, conditionCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /// - returns: A 'condition cell' with the event at the specified index path.
+ private func tableView(tableView: UITableView, conditionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.conditionCell, forIndexPath: indexPath) as! ConditionCell
+ let event = events[indexPath.row]
+ cell.setCharacteristic(event.characteristic, targetValue: event.triggerValue!)
+ return cell
+ }
+
+ /**
+ - returns: An 'add cell' with localized text.
+ Defaults to super implementation.
+ */
+ override func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch sectionForIndex(indexPath.section) {
+ case .Characteristics?:
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.addCell, forIndexPath: indexPath)
+ cell.textLabel?.text = NSLocalizedString("Add Characteristic…", comment: "Add Characteristic")
+ cell.textLabel?.textColor = UIColor.editableBlueColor()
+ return cell
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, addCellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Handles the selection of characteristic events.
+ Defaults to super implementation for other sections.
+ */
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ switch sectionForIndex(indexPath.section) {
+ case .Characteristics?:
+ if indexPathIsAdd(indexPath) {
+ addEvent()
+ return
+ }
+ let cell = tableView.cellForRowAtIndexPath(indexPath)
+ performSegueWithIdentifier(Identifiers.selectCharacteristicSegue, sender: cell)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ - returns: `true` for characteristic cells,
+ otherwise defaults to super implementation.
+ */
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ if indexPathIsAdd(indexPath) {
+ return false
+ }
+ switch sectionForIndex(indexPath.section) {
+ case .Characteristics?:
+ return true
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, canEditRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Removes events from the trigger creator.
+ Defaults to super implementation for other sections.
+ */
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ switch sectionForIndex(indexPath.section) {
+ case .Characteristics?:
+ characteristicTriggerCreator.removeEvent(events[indexPath.row])
+ events = characteristicTriggerCreator.events
+ tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ super.tableView(tableView, commitEditingStyle: editingStyle, forRowAtIndexPath: indexPath)
+ }
+ }
+ }
+
+ /**
+ - returns: A localized description of characteristic events
+ Defaults to super implementation for other sections.
+ */
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ switch sectionForIndex(section) {
+ case .Characteristics?:
+ return NSLocalizedString("This trigger will activate when any of these characteristics change to their value. For example, 'run when the garage door is opened'.", comment: "Characteristic Trigger Description")
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, titleForFooterInSection: section)
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /// Resets the internal events array from the trigger creator.
+ private func reloadData() {
+ events = characteristicTriggerCreator.events
+ tableView.reloadData()
+ }
+
+ /// Performs a segue to the `CharacteristicSelectionViewController`.
+ private func addEvent() {
+ characteristicTriggerCreator.mode = .Event
+ self.performSegueWithIdentifier(Identifiers.selectCharacteristicSegue, sender: nil)
+ }
+
+ /// - returns: `true` if the section is the Characteristic 'add row'; otherwise defaults to super implementation.
+ override func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
+ switch sectionForIndex(indexPath.section) {
+ case .Characteristics?:
+ return indexPath.row == events.count
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.indexPathIsAdd(indexPath)
+ }
+ }
+
+ // MARK: Trigger Controller Methods
+
+ /**
+ - parameter index: The section index.
+
+ - returns: The `TriggerTableViewSection` for the given index.
+ */
+ override func sectionForIndex(index: Int) -> TriggerTableViewSection? {
+ switch index {
+ case 0:
+ return .Name
+
+ case 1:
+ return .Enabled
+
+ case 2:
+ return .Characteristics
+
+ case 3:
+ return .Conditions
+
+ case 4:
+ return .ActionSets
+
+ default:
+ return nil
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/ConditionCell.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/ConditionCell.swift
new file mode 100644
index 00000000..a84cc631
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/ConditionCell.swift
@@ -0,0 +1,116 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ConditionCell` displays characteristic and location conditions.
+*/
+
+import UIKit
+import HomeKit
+
+/// A `UITableViewCell` subclass that displays a trigger condition.
+class ConditionCell: UITableViewCell {
+ /// A static, short date formatter.
+ static let dateFormatter: NSDateFormatter = {
+ let dateFormatter = NSDateFormatter()
+ dateFormatter.dateStyle = .NoStyle
+ dateFormatter.timeStyle = .ShortStyle
+ dateFormatter.locale = NSLocale.currentLocale()
+ return dateFormatter
+ }()
+
+ /// Ignores the passed-in style and overrides it with .Subtitle.
+ override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
+ super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
+ selectionStyle = .None
+ detailTextLabel?.textColor = UIColor.lightGrayColor()
+ accessoryType = .None
+ }
+
+ /// Required because we overwrote a designated initializer.
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+
+ /**
+ Sets the cell's text to represent a characteristic and target value.
+ For example, "Brightness → 60%"
+ Sets the subtitle to the service and accessory that this characteristic represents.
+
+ - parameter characteristic: The characteristic this cell represents.
+ - parameter targetValue: The target value from this action.
+ */
+ func setCharacteristic(characteristic: HMCharacteristic, targetValue: AnyObject) {
+ let targetDescription = "\(characteristic.localizedDescription) → \(characteristic.localizedDescriptionForValue(targetValue))"
+ textLabel?.text = targetDescription
+
+ let contextDescription = NSLocalizedString("%@ in %@", comment: "Service in Accessory")
+ if let service = characteristic.service, accessory = service.accessory {
+ detailTextLabel?.text = String(format: contextDescription, service.name, accessory.name)
+ }
+ else {
+ detailTextLabel?.text = NSLocalizedString("Unknown Characteristic", comment: "Unknown Characteristic")
+ }
+ }
+
+ /**
+ Sets the cell's text to represent an ordered time with a set context string.
+
+ - parameter order: A `TimeConditionOrder` which will map to a localized string.
+ - parameter timeString: The localized time string.
+ - parameter contextString: A localized string describing the time type.
+ */
+ private func setOrder(order: TimeConditionOrder, timeString: String, contextString: String) {
+ let formatString: String
+ switch order {
+ case .Before:
+ formatString = NSLocalizedString("Before %@", comment: "Before Time")
+
+ case .After:
+ formatString = NSLocalizedString("After %@", comment: "After Time")
+
+ case .At:
+ formatString = NSLocalizedString("At %@", comment: "At Time")
+ }
+ textLabel?.text = String(format: formatString, timeString)
+ detailTextLabel?.text = contextString
+ }
+
+ /**
+ Sets the cell's text to represent an exact time condition.
+
+ - parameter order: A `TimeConditionOrder` which will map to a localized string.
+ - parameter dateComponents: The date components of the exact time.
+ */
+ func setOrder(order: TimeConditionOrder, dateComponents: NSDateComponents) {
+ let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents)
+ let timeString = ConditionCell.dateFormatter.stringFromDate(date!)
+ setOrder(order, timeString: timeString, contextString: NSLocalizedString("Relative to Time", comment: "Relative to Time"))
+ }
+
+ /**
+ Sets the cell's text to represent a solar event time condition.
+
+ - parameter order: A `TimeConditionOrder` which will map to a localized string.
+ - parameter sunState: A `TimeConditionSunState` which will map to localized string.
+ */
+ func setOrder(order: TimeConditionOrder, sunState: TimeConditionSunState) {
+ let timeString: String
+ switch sunState {
+ case .Sunrise:
+ timeString = NSLocalizedString("Sunrise", comment: "Sunrise")
+
+ case .Sunset:
+ timeString = NSLocalizedString("Sunset", comment: "Sunset")
+ }
+ setOrder(order, timeString: timeString , contextString: NSLocalizedString("Relative to sun", comment: "Relative to Sun"))
+ }
+
+ /// Sets the cell's text to indicate the given condition is not handled by the app.
+ func setUnknown() {
+ let unknownString = NSLocalizedString("Unknown Condition", comment: "Unknown Condition")
+ detailTextLabel?.text = unknownString
+ textLabel?.text = unknownString
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/SegmentedTimeCell.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/SegmentedTimeCell.swift
new file mode 100644
index 00000000..b0540a12
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/SegmentedTimeCell.swift
@@ -0,0 +1,15 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `SegmentedTimeCell` has a segmented control, used for selecting the time type.
+*/
+
+import UIKit
+/// A `UITableViewCell` subclass with a `UISegmentedControl`, used for selecting the time type.
+class SegmentedTimeCell: UITableViewCell {
+ // MARK: Properties
+
+ @IBOutlet weak var segmentedControl: UISegmentedControl!
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimeConditionViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimeConditionViewController.swift
new file mode 100644
index 00000000..d859a9cc
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimeConditionViewController.swift
@@ -0,0 +1,350 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TimeConditionViewController` allows the user to create a new time condition.
+*/
+
+import UIKit
+import HomeKit
+
+/// Represents a section in the `TimeConditionViewController`.
+enum TimeConditionTableViewSection: Int {
+ /**
+ This section contains the segmented control to
+ choose a time condition type.
+ */
+ case TimeOrSun
+
+ /**
+ This section contains cells to allow the selection
+ of 'before', 'after', or 'at'. 'At' is only available
+ when the exact time is specified.
+ */
+ case BeforeOrAfter
+
+ /**
+ If the condition type is exact time, this section will
+ only have one cell, the date picker cell.
+
+ If the condition type is relative to a solar event,
+ this section will have two cells, one for 'sunrise' and
+ one for 'sunset.
+ */
+ case Value
+
+ static let count = 3
+}
+
+/**
+ Represents the type of time condition.
+
+ The condition can be an exact time, or relative to a solar event.
+*/
+enum TimeConditionType: Int {
+ case Time, Sun
+}
+
+/**
+ Represents the type of solar event.
+
+ This can be sunrise or sunset.
+*/
+enum TimeConditionSunState: Int {
+ case Sunrise, Sunset
+}
+
+/**
+ Represents the condition order.
+
+ Conditions can be before, after, or exactly at a given time.
+*/
+enum TimeConditionOrder: Int {
+ case Before, After, At
+}
+
+/// A view controller that facilitates the creation of time conditions for triggers.
+class TimeConditionViewController: HMCatalogViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let selectionCell = "SelectionCell"
+ static let timePickerCell = "TimePickerCell"
+ static let segmentedTimeCell = "SegmentedTimeCell"
+ }
+
+ static let timeOrSunTitles = [
+ NSLocalizedString("Relative to time", comment: "Relative to time"),
+ NSLocalizedString("Relative to sun", comment: "Relative to sun")
+ ]
+
+ static let beforeOrAfterTitles = [
+ NSLocalizedString("Before", comment: "Before"),
+ NSLocalizedString("After", comment: "After"),
+ NSLocalizedString("At", comment: "At")
+ ]
+
+ static let sunriseSunsetTitles = [
+ NSLocalizedString("Sunrise", comment: "Sunrise"),
+ NSLocalizedString("Sunset", comment: "Sunset")
+ ]
+
+ // MARK: Properties
+
+ private var timeType: TimeConditionType = .Time
+ private var order: TimeConditionOrder = .Before
+ private var sunState: TimeConditionSunState = .Sunrise
+
+ private var datePicker: UIDatePicker?
+
+ var triggerCreator: EventTriggerCreator?
+
+ // MARK: View Methods
+
+ /// Configures the table view.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 44.0
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of `TimeConditionTableViewSection`s.
+ override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
+ return TimeConditionTableViewSection.count
+ }
+
+ /**
+ - returns: The number rows based on the `TimeConditionTableViewSection`
+ and the `timeType`.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch TimeConditionTableViewSection(rawValue: section) {
+ case .TimeOrSun?:
+ return 1
+
+ case .BeforeOrAfter?:
+ // If we're choosing an exact time, we add the 'At' row.
+ return (timeType == .Time) ? 3 : 2
+
+ case .Value?:
+ // Date picker cell or sunrise/sunset selection cells
+ return (timeType == .Time) ? 1 : 2
+
+ case nil:
+ fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
+ }
+ }
+
+ /// Switches based on the section to generate a cell.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch TimeConditionTableViewSection(rawValue: indexPath.section) {
+ case .TimeOrSun?:
+ return self.tableView(tableView, segmentedCellForRowAtIndexPath: indexPath)
+
+ case .BeforeOrAfter?:
+ return self.tableView(tableView, selectionCellForRowAtIndexPath: indexPath)
+
+ case .Value?:
+ switch timeType {
+ case .Time:
+ return self.tableView(tableView, datePickerCellForRowAtIndexPath: indexPath)
+ case .Sun:
+ return self.tableView(tableView, selectionCellForRowAtIndexPath: indexPath)
+ }
+
+ case nil:
+ fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
+ }
+ }
+
+ /// - returns: A localized string describing the section.
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch TimeConditionTableViewSection(rawValue: section) {
+ case .TimeOrSun?:
+ return NSLocalizedString("Condition Type", comment: "Condition Type")
+
+ case .BeforeOrAfter?:
+ return nil
+
+ case .Value?:
+ if timeType == .Time {
+ return NSLocalizedString("Time", comment: "Time")
+ }
+ else {
+ return NSLocalizedString("Event", comment: "Event")
+ }
+
+ case nil:
+ fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
+ }
+ }
+
+ /// - returns: A localized description for condition type section; `nil` otherwise.
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ switch TimeConditionTableViewSection(rawValue: section) {
+ case .TimeOrSun?:
+ return NSLocalizedString("Time conditions can relate to specific times or special events, like sunrise and sunset.", comment: "Condition Type Description")
+
+ case .BeforeOrAfter?:
+ return nil
+
+ case .Value?:
+ return nil
+
+ case nil:
+ fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
+ }
+ }
+
+ /// Updates internal values based on row selection.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ let cell = tableView.cellForRowAtIndexPath(indexPath)!
+ if cell.selectionStyle == .None {
+ return
+ }
+
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+
+ switch TimeConditionTableViewSection(rawValue: indexPath.section) {
+ case .TimeOrSun?:
+ timeType = TimeConditionType(rawValue: indexPath.row)!
+ reloadDynamicSections()
+ return
+
+ case .BeforeOrAfter?:
+ order = TimeConditionOrder(rawValue: indexPath.row)!
+ tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
+
+ case .Value?:
+ if timeType == .Sun {
+ sunState = TimeConditionSunState(rawValue: indexPath.row)!
+ }
+ tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
+
+ case nil:
+ fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Generates a selection cell based on the section.
+ Ordering and sun-state sections have selections.
+ */
+ private func tableView(tableView: UITableView, selectionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.selectionCell, forIndexPath: indexPath)
+ switch TimeConditionTableViewSection(rawValue: indexPath.section) {
+ case .BeforeOrAfter?:
+ cell.textLabel?.text = TimeConditionViewController.beforeOrAfterTitles[indexPath.row]
+ cell.accessoryType = (order.rawValue == indexPath.row) ? .Checkmark : .None
+
+ case .Value?:
+ if timeType == .Sun {
+ cell.textLabel?.text = TimeConditionViewController.sunriseSunsetTitles[indexPath.row]
+ cell.accessoryType = (sunState.rawValue == indexPath.row) ? .Checkmark : .None
+ }
+
+ case nil:
+ fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
+
+ default:
+ break
+ }
+ return cell
+ }
+
+ /// Generates a date picker cell and sets the internal date picker when created.
+ private func tableView(tableView: UITableView, datePickerCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.timePickerCell, forIndexPath: indexPath) as! TimePickerCell
+ // Save the date picker so we can get the result later.
+ datePicker = cell.datePicker
+ return cell
+ }
+
+ /// Generates a segmented cell and sets its target when created.
+ private func tableView(tableView: UITableView, segmentedCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.segmentedTimeCell, forIndexPath: indexPath) as! SegmentedTimeCell
+ cell.segmentedControl.selectedSegmentIndex = timeType.rawValue
+ cell.segmentedControl.removeTarget(nil, action: nil, forControlEvents: .AllEvents)
+ cell.segmentedControl.addTarget(self, action: #selector(TimeConditionViewController.segmentedControlDidChange(_:)), forControlEvents: .ValueChanged)
+ return cell
+ }
+
+ /// Creates date components from the date picker's date.
+ var dateComponents: NSDateComponents? {
+ guard let datePicker = datePicker else { return nil }
+ let flags: NSCalendarUnit = [.Hour, .Minute]
+ return NSCalendar.currentCalendar().components(flags, fromDate: datePicker.date)
+ }
+
+ /**
+ Updates the time type and reloads dynamic sections.
+
+ - parameter segmentedControl: The segmented control that changed.
+ */
+ func segmentedControlDidChange(segmentedControl: UISegmentedControl) {
+ if let segmentedControlType = TimeConditionType(rawValue: segmentedControl.selectedSegmentIndex) {
+ timeType = segmentedControlType
+ }
+ reloadDynamicSections()
+ }
+
+ /// Reloads the BeforeOrAfter and Value section.
+ private func reloadDynamicSections() {
+ if timeType == .Sun && order == .At {
+ order = .Before
+ }
+ let reloadIndexSet = NSIndexSet(indexesInRange: NSMakeRange(TimeConditionTableViewSection.BeforeOrAfter.rawValue, 2))
+ tableView.reloadSections(reloadIndexSet, withRowAnimation: .Automatic)
+ }
+
+ // MARK: IBAction Methods
+
+ /**
+ Generates a predicate based on the stored values, adds
+ the condition to the trigger, then dismisses the view.
+ */
+ @IBAction func saveAndDismiss(sender: UIBarButtonItem) {
+ var predicate: NSPredicate?
+ switch timeType {
+ case .Time:
+ switch order {
+ case .Before:
+ predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringBeforeDateWithComponents(dateComponents!)
+
+ case .After:
+ predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringAfterDateWithComponents(dateComponents!)
+
+ case .At:
+ predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringOnDateWithComponents(dateComponents!)
+ }
+
+ case .Sun:
+ let significantEventString = (sunState == .Sunrise) ? HMSignificantEventSunrise : HMSignificantEventSunset
+ switch order {
+ case .Before:
+ predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringBeforeSignificantEvent(significantEventString, applyingOffset: nil)
+
+ case .After:
+ predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringAfterSignificantEvent(significantEventString, applyingOffset: nil)
+
+ case .At:
+ // Significant events must be specified 'before' or 'after'.
+ break
+ }
+ }
+ if let predicate = predicate {
+ triggerCreator?.addCondition(predicate)
+ }
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ /// Cancels the creation of the conditions and exits.
+ @IBAction func dismiss(sender: UIBarButtonItem) {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimePickerCell.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimePickerCell.swift
new file mode 100644
index 00000000..b1219dc1
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimePickerCell.swift
@@ -0,0 +1,16 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TimePickerCell` has a date picker, used for selecting a specific time of day.
+*/
+
+import UIKit
+
+/// A `UITableViewCell` subclass with a `UIDatePicker`, used for selecting a specific time of day.
+class TimePickerCell: UITableViewCell {
+ // MARK: Properties
+
+ @IBOutlet weak var datePicker: UIDatePicker!
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerCreator.swift
new file mode 100644
index 00000000..e78e1171
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerCreator.swift
@@ -0,0 +1,140 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `EventTriggerCreator` is a superclass that creates Characteristic and Location triggers.
+*/
+
+import HomeKit
+
+/**
+ A superclass for event trigger creators.
+
+ These classes manage the state for characteristic trigger conditions.
+*/
+class EventTriggerCreator: TriggerCreator, CharacteristicCellDelegate {
+ // MARK: Properties
+
+ /// A mapping of `HMCharacteristic`s to their values.
+ private let conditionValueMap = NSMapTable.strongToStrongObjectsMapTable()
+
+ private var eventTrigger: HMEventTrigger? {
+ return trigger as? HMEventTrigger
+ }
+
+ /**
+ An array of top-level `NSPredicate` objects.
+
+ Currently, HMCatalog only supports top-level `NSPredicate`s
+ which have type `AndPredicateType`.
+ */
+ var originalConditions: [NSPredicate] {
+ if let compoundPredicate = eventTrigger?.predicate as? NSCompoundPredicate,
+ subpredicates = compoundPredicate.subpredicates as? [NSPredicate] {
+ return subpredicates
+ }
+
+ return []
+ }
+
+ /// An array of new conditions which will be written when the trigger is saved.
+ lazy var conditions: [NSPredicate] = self.originalConditions
+
+ /**
+ Adds a predicate to the pending conditions.
+
+ - parameter predicate: The new `NSPredicate` to add.
+ */
+ func addCondition(predicate: NSPredicate) {
+ conditions.append(predicate)
+ }
+
+ /**
+ Removes a predicate from the pending conditions.
+
+ - parameter predicate: The `NSPredicate` to remove.
+ */
+ func removeCondition(predicate: NSPredicate) {
+ if let index = conditions.indexOf(predicate) {
+ conditions.removeAtIndex(index)
+ }
+ }
+
+ /**
+ - returns: The new `NSCompoundPredicate`, generated from
+ the pending conditions.
+ */
+ func newPredicate() -> NSPredicate {
+ return NSCompoundPredicate(type: .AndPredicateType, subpredicates: conditions)
+ }
+
+ /// Handles the value update and stores the value in the condition map.
+ func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) {
+ conditionValueMap.setObject(value, forKey: characteristic)
+ }
+
+ /**
+ Tries to use the value from the condition-value map, but falls back
+ to reading the characteristic's value from HomeKit.
+ */
+ func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) {
+ if let value = conditionValueMap.objectForKey(characteristic) {
+ completion(value, nil)
+ return
+ }
+
+ characteristic.readValueWithCompletionHandler { error in
+ /*
+ The user may have updated the cell value while the
+ read was happening. We check the map one more time.
+ */
+ if let value = self.conditionValueMap.objectForKey(characteristic) {
+ completion(value, nil)
+ }
+ else {
+ completion(characteristic.value, error)
+ }
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Updates the predicates and saves the new, generated
+ predicate to the event trigger.
+ */
+ func savePredicate() {
+ updatePredicates()
+ dispatch_group_enter(saveTriggerGroup)
+ eventTrigger?.updatePredicate(newPredicate()) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+
+ /// Generates predicates from the characteristic-value map and adds them to the pending conditions.
+ func updatePredicates() {
+ for (characteristic, value) in pairsFromMapTable(conditionValueMap) {
+ let predicate = HMEventTrigger.predicateForEvaluatingTriggerWithCharacteristic(characteristic, relatedBy: .EqualToPredicateOperatorType, toValue: value)
+ addCondition(predicate)
+ }
+
+ conditionValueMap.removeAllObjects()
+ }
+
+ /**
+ - parameter table: The `NSMapTable` from which to generate the pairs.
+
+ - returns: Tuples representing `HMCharacteristic`s and their associated return trigger values.
+ */
+ func pairsFromMapTable(table: NSMapTable) -> [(HMCharacteristic, NSCopying)] {
+ return table.keyEnumerator().allObjects.map { object in
+ let characteristic = object as! HMCharacteristic
+ let triggerValue = table.objectForKey(object) as! NSCopying
+ return (characteristic, triggerValue)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerViewController.swift
new file mode 100644
index 00000000..66fc81bf
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerViewController.swift
@@ -0,0 +1,250 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `EventTriggerViewController` is a superclass that helps users create Characteristic and Location triggers.
+*/
+
+import UIKit
+import HomeKit
+
+/**
+ A superclass for all event-based view controllers.
+
+ It handles the process of creating and managing trigger conditions.
+*/
+class EventTriggerViewController: TriggerViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let addCell = "AddCell"
+ static let conditionCell = "ConditionCell"
+ static let showTimeConditionSegue = "Show Time Condition"
+ }
+
+ // MARK: Properties
+
+ private var eventTriggerCreator: EventTriggerCreator {
+ return triggerCreator as! EventTriggerCreator
+ }
+
+ // MARK: View Methods
+
+ /// Registers table view for cells.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier:Identifiers.addCell)
+ tableView.registerClass(ConditionCell.self, forCellReuseIdentifier:Identifiers.conditionCell)
+ }
+
+ /// Hands off the trigger creator to the condition view controllers.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ switch segue.intendedDestinationViewController {
+ case let timeVC as TimeConditionViewController:
+ timeVC.triggerCreator = eventTriggerCreator
+
+ case let characteristicEventVC as CharacteristicSelectionViewController:
+ let characteristicTriggerCreator = triggerCreator as! EventTriggerCreator
+ characteristicEventVC.triggerCreator = characteristicTriggerCreator
+
+ default:
+ break
+ }
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ - returns: In the conditions section: the number of conditions, plus one
+ for the add row. Defaults to the super implementation.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sectionForIndex(section) {
+ case .Conditions?:
+ // Add row.
+ return eventTriggerCreator.conditions.count + 1
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, numberOfRowsInSection: section)
+ }
+ }
+
+ /**
+ Launchs "Add Condition" if the 'add index path' is selected.
+ Defaults to the super implementation.
+ */
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ switch sectionForIndex(indexPath.section) {
+ case .Conditions?:
+ if indexPathIsAdd(indexPath) {
+ addCondition()
+ }
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Switches to select the correct type of cell for the section.
+ Defaults to the super implementation.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ if indexPathIsAdd(indexPath) {
+ return self.tableView(tableView, addCellForRowAtIndexPath: indexPath)
+ }
+
+ switch sectionForIndex(indexPath.section) {
+ case .Conditions?:
+ return self.tableView(tableView, conditionCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ The conditions can be removed, the 'add index path' cannot.
+ For all others, default to super implementation.
+ */
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ if indexPathIsAdd(indexPath) {
+ return false
+ }
+
+ switch sectionForIndex(indexPath.section) {
+ case .Conditions?:
+ return true
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return false
+ }
+ }
+
+ /// Remove the selected condition from the trigger creator.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ let predicate = eventTriggerCreator.conditions[indexPath.row]
+ eventTriggerCreator.removeCondition(predicate)
+ tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /// - returns: An 'add cell' with 'Add Condition' text.
+ func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.addCell, forIndexPath: indexPath)
+ let cellText: String
+ switch sectionForIndex(indexPath.section) {
+ case .Conditions?:
+ cellText = NSLocalizedString("Add Condition…", comment: "Add Condition")
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ cellText = NSLocalizedString("Add…", comment: "Add")
+ }
+
+ cell.textLabel?.text = cellText
+ cell.textLabel?.textColor = UIColor.editableBlueColor()
+
+ return cell
+ }
+
+ /// - returns: A localized description of a trigger. Falls back to super implementation.
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ switch sectionForIndex(section) {
+ case .Conditions?:
+ return NSLocalizedString("When a trigger is activated by an event, it checks these conditions. If all of them are true, it will set its scenes.", comment: "Trigger Conditions Description")
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, titleForFooterInSection: section)
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /// - returns: A 'condition cell', which displays information about the condition.
+ private func tableView(tableView: UITableView, conditionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.conditionCell) as! ConditionCell
+ let condition = eventTriggerCreator.conditions[indexPath.row]
+
+ switch condition.homeKitConditionType {
+ case .Characteristic(let characteristic, let value):
+ cell.setCharacteristic(characteristic, targetValue: value)
+
+ case .ExactTime(let order, let dateComponents):
+ cell.setOrder(order, dateComponents: dateComponents)
+
+ case .SunTime(let order, let sunState):
+ cell.setOrder(order, sunState: sunState)
+
+ case .Unknown:
+ cell.setUnknown()
+ }
+
+ return cell
+ }
+
+ /// Presents an alert controller to choose the type of trigger.
+ private func addCondition() {
+ let title = NSLocalizedString("Add Condition", comment: "Add Condition")
+ let alertController = UIAlertController(title: title, message: nil, preferredStyle: .ActionSheet)
+
+ // Time Condition.
+ let timeAction = UIAlertAction(title: NSLocalizedString("Time", comment: "Time"), style: .Default) { _ in
+ self.performSegueWithIdentifier(Identifiers.showTimeConditionSegue, sender: self)
+ }
+ alertController.addAction(timeAction)
+
+ // Characteristic trigger.
+ let eventActionTitle = NSLocalizedString("Characteristic", comment: "Characteristic")
+
+ let eventAction = UIAlertAction(title: eventActionTitle, style: .Default, handler: { _ in
+ if let triggerCreator = self.triggerCreator as? CharacteristicTriggerCreator {
+ triggerCreator.mode = .Condition
+ }
+ self.performSegueWithIdentifier("Select Characteristic", sender: self)
+ })
+
+ alertController.addAction(eventAction)
+
+ // Cancel.
+ let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel, handler: nil)
+ alertController.addAction(cancelAction)
+
+ // Present alert.
+ presentViewController(alertController, animated: true, completion: nil)
+ }
+
+ /// - returns: `true` if the index path is the 'add row'; `false` otherwise.
+ func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
+ switch sectionForIndex(indexPath.section) {
+ case .Conditions?:
+ return indexPath.row == eventTriggerCreator.conditions.count
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return false
+ }
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerCreator.swift
new file mode 100644
index 00000000..72c46a76
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerCreator.swift
@@ -0,0 +1,94 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `LocationTriggerCreator` creates Location triggers.
+*/
+
+import HomeKit
+import MapKit
+
+/**
+ An `EventTriggerCreator` subclass which allows for the creation
+ of location triggers.
+*/
+class LocationTriggerCreator: EventTriggerCreator, MapViewControllerDelegate {
+ // MARK: Properties
+
+ var eventTrigger: HMEventTrigger? {
+ return trigger as? HMEventTrigger
+ }
+ var locationEvent: HMLocationEvent?
+ var targetRegion: CLCircularRegion?
+ var targetRegionStateIndex = 0
+
+ // MARK: Trigger Creator Methods
+
+ /// Initializes location event, target region, and region state.
+ required init(trigger: HMTrigger?, home: HMHome) {
+ super.init(trigger: trigger, home: home)
+ if let eventTrigger = eventTrigger {
+ self.locationEvent = eventTrigger.locationEvent
+ if let region = locationEvent?.region as? CLCircularRegion {
+ self.targetRegion = region
+ }
+ self.targetRegionStateIndex = (self.targetRegion?.notifyOnEntry ?? true) ? 0 : 1
+
+ }
+ }
+
+ /// Generates a new region and updates the location event.
+ override func updateTrigger() {
+ if let region = targetRegion {
+ prepareRegion()
+ if let locationEvent = locationEvent {
+ dispatch_group_enter(saveTriggerGroup)
+ locationEvent.updateRegion(region) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+ }
+
+ self.savePredicate()
+ }
+
+ /**
+ - returns: A new `HMEventTrigger` with a new generated
+ location event and predicate.
+ */
+ override func newTrigger() -> HMTrigger? {
+ var events = [HMLocationEvent]()
+ if let region = targetRegion {
+ prepareRegion()
+ events.append(HMLocationEvent(region: region))
+ }
+ return HMEventTrigger(name: name, events: events, predicate: newPredicate())
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Sets the `notifyOnEntry` and `notifyOnExit` region
+ properties based on the selected state.
+ */
+ private func prepareRegion() {
+ if let region = targetRegion {
+ region.notifyOnEntry = (targetRegionStateIndex == 0)
+ region.notifyOnExit = !region.notifyOnEntry
+ }
+ }
+
+ /**
+ Updates the target region from the one provided
+ by the delegate.
+
+ - parameter region: A new `CLCircularRegion`, provided by the delegate.
+ */
+ func mapViewDidUpdateRegion(region: CLCircularRegion) {
+ targetRegion = region
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerViewController.swift
new file mode 100644
index 00000000..04de5847
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerViewController.swift
@@ -0,0 +1,248 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `LocationTriggerViewController` allows the user to modify and create Location triggers.
+*/
+
+import UIKit
+import MapKit
+import HomeKit
+import AddressBookUI
+import Contacts
+
+/// A view controller which facilitates the creation of a location trigger.
+class LocationTriggerViewController: EventTriggerViewController {
+
+ struct Identifiers {
+ static let locationCell = "LocationCell"
+ static let regionStatusCell = "RegionStatusCell"
+ static let selectLocationSegue = "Select Location"
+ }
+
+ static let geocoder = CLGeocoder()
+
+ static let regionStatusTitles = [
+ NSLocalizedString("When I Enter The Area", comment: "When I Enter The Area"),
+ NSLocalizedString("When I Leave The Area", comment: "When I Leave The Area")
+ ]
+
+ var locationTriggerCreator: LocationTriggerCreator {
+ return triggerCreator as! LocationTriggerCreator
+ }
+
+ var localizedAddress: String?
+
+ var viewIsDisplayed = false
+
+ // MARK: View Methods
+
+ /// Initializes a trigger creator and registers for table view cells.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ triggerCreator = LocationTriggerCreator(trigger: trigger, home: home)
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.locationCell)
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.regionStatusCell)
+ }
+
+ /**
+ Generates an address string for the current region location and
+ reloads the table view.
+ */
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+ viewIsDisplayed = true
+ if let region = locationTriggerCreator.targetRegion {
+ let centerLocation = CLLocation(latitude: region.center.latitude, longitude: region.center.longitude)
+ LocationTriggerViewController.geocoder.reverseGeocodeLocation(centerLocation) { placemarks, error in
+ if !self.viewIsDisplayed {
+ // The geocoder took too long, we're not on this view any more.
+ return
+ }
+ if let error = error {
+ self.displayError(error)
+ return
+ }
+ if let mostLikelyPlacemark = placemarks?.first {
+ let address = CNMutablePostalAddress(placemark: mostLikelyPlacemark)
+ let addressFormatter = CNPostalAddressFormatter()
+ let addressString = addressFormatter.stringFromPostalAddress(address)
+ self.localizedAddress = addressString.stringByReplacingOccurrencesOfString("\n", withString: ", ")
+ let section = NSIndexSet(index: 2)
+ self.tableView.reloadSections(section, withRowAnimation: .Automatic)
+ }
+ }
+ }
+ tableView.reloadData()
+ }
+
+ /// Passes the trigger creator and region into the `MapViewController`.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ if segue.identifier == Identifiers.selectLocationSegue {
+ guard let destinationVC = segue.intendedDestinationViewController as? MapViewController else { return }
+ // Give the map the previous target region (if exists).
+ destinationVC.targetRegion = locationTriggerCreator.targetRegion
+ destinationVC.delegate = locationTriggerCreator
+ }
+ }
+
+ override func viewWillDisappear(animated: Bool) {
+ super.viewWillDisappear(animated)
+ viewIsDisplayed = false
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ - returns: The number of rows in the Region section;
+ defaults to the super implementation for other sections.
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sectionForIndex(section) {
+ case .Region?:
+ return 2
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, numberOfRowsInSection: section)
+ }
+ }
+
+ /**
+ Generates a cell based on the section.
+ Handles Region and Location sections, defaults to
+ super implementations for other sections.
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch sectionForIndex(indexPath.section) {
+ case .Region?:
+ return self.tableView(tableView, regionStatusCellForRowAtIndexPath: indexPath)
+
+ case .Location?:
+ return self.tableView(tableView, locationCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /// Generates the single location cell.
+ private func tableView(tableView: UITableView, locationCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.locationCell, forIndexPath: indexPath)
+ cell.accessoryType = .DisclosureIndicator
+
+ if locationTriggerCreator.targetRegion != nil {
+ cell.textLabel?.text = localizedAddress ?? NSLocalizedString("Update Location", comment: "Update Location")
+ }
+ else {
+ cell.textLabel?.text = NSLocalizedString("Set Location", comment: "Set Location")
+ }
+ return cell
+ }
+
+ /// Generates the cell which allow the user to select either 'on enter' or 'on exit'.
+ private func tableView(tableView: UITableView, regionStatusCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.regionStatusCell, forIndexPath: indexPath)
+ cell.textLabel?.text = LocationTriggerViewController.regionStatusTitles[indexPath.row]
+ cell.accessoryType = (locationTriggerCreator.targetRegionStateIndex == indexPath.row) ? .Checkmark : .None
+ return cell
+ }
+
+ /**
+ Allows the user to select a location or change the region status.
+ Defaults to the super implmentation for other sections.
+ */
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ switch sectionForIndex(indexPath.section) {
+ case .Location?:
+ performSegueWithIdentifier(Identifiers.selectLocationSegue, sender: self)
+
+ case .Region?:
+ locationTriggerCreator.targetRegionStateIndex = indexPath.row
+ let reloadIndexSet = NSIndexSet(index: indexPath.section)
+ tableView.reloadSections(reloadIndexSet, withRowAnimation: .Automatic)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ - returns: A localized title for the Location and Region sections.
+ Defaults to the super implmentation for other sections.
+ */
+ override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch sectionForIndex(section) {
+ case .Location?:
+ return NSLocalizedString("Location", comment: "Location")
+
+ case .Region?:
+ return NSLocalizedString("Region Status", comment: "Region Status")
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, titleForHeaderInSection: section)
+ }
+ }
+
+ /**
+ - returns: A localized description of the region status.
+ Defaults to the super implmentation for other sections.
+ */
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ switch sectionForIndex(section) {
+ case .Region?:
+ return NSLocalizedString("This trigger can activate when you enter or leave a region. For example, when you arrive at home or when you leave work.", comment: "Location Region Description")
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, titleForFooterInSection: section)
+ }
+ }
+
+ // MARK: Trigger Controller Methods
+
+ /**
+ - parameter index: The section index.
+
+ - returns: The `TriggerTableViewSection` for the given index.
+ */
+ override func sectionForIndex(index: Int) -> TriggerTableViewSection? {
+ switch index {
+ case 0:
+ return .Name
+
+ case 1:
+ return .Enabled
+
+ case 2:
+ return .Location
+
+ case 3:
+ return .Region
+
+ case 4:
+ return .Conditions
+
+ case 5:
+ return .ActionSets
+
+ default:
+ return nil
+ }
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapOverlayView.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapOverlayView.swift
new file mode 100644
index 00000000..dbae61ed
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapOverlayView.swift
@@ -0,0 +1,44 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `MapOverlayView` draws the circle over the `MapViewController`.
+*/
+
+import MapKit
+
+/**
+ A simple `UIView` subclass to draw a selection circle over
+ a MKMapView of the same size.
+*/
+class MapOverlayView: UIView {
+
+ /**
+ Draws a dashed circle in the center of the `rect` with
+ a radius 1/4th of the `rect`'s smallest side.
+ */
+ override func drawRect(rect: CGRect) {
+ super.drawRect(rect)
+ let context = UIGraphicsGetCurrentContext()
+
+ let strokeColor = UIColor.blueColor()
+
+ let circleDiameter: CGFloat = min(rect.width, rect.height) / 2.0
+ let circleRadius = circleDiameter / 2.0
+ let cirlceRect = CGRect(x: rect.midX - circleRadius, y: rect.midY - circleRadius, width: circleDiameter, height: circleDiameter)
+ let circlePath = UIBezierPath(ovalInRect: cirlceRect)
+
+ strokeColor.setStroke()
+ circlePath.lineWidth = 3
+ CGContextSaveGState(context!)
+ CGContextSetLineDash(context!, 0, [6, 6], 2)
+ circlePath.stroke()
+ CGContextRestoreGState(context!)
+ }
+
+ /// - returns: `false` to accept no touches.
+ override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
+ return false
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapViewController.swift
new file mode 100644
index 00000000..caaea012
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapViewController.swift
@@ -0,0 +1,227 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `MapViewController` allow the user to select a location using the map.
+ This location will be passed back to the sender when the user saves the view.
+*/
+
+import UIKit
+import MapKit
+
+/**
+ Allows the sender to get notified when there
+ have been changes to the region.
+*/
+protocol MapViewControllerDelegate {
+ /**
+ Notifies the delegate that the `MapViewController`'s
+ region has been updated.
+ */
+ func mapViewDidUpdateRegion(region: CLCircularRegion)
+}
+
+/**
+ A view controller which allows the selection of a
+ circular region on a map.
+*/
+class MapViewController: UIViewController, UISearchBarDelegate, CLLocationManagerDelegate, MKMapViewDelegate {
+ // MARK: Types
+
+ struct Identifiers {
+ static let circularRegion = "MapViewController.Region"
+ }
+
+ /// When the view loads, we'll zoom to this longitude/latitude span delta.
+ static let InitialZoomDelta: Double = 0.0015
+
+ /// When the view loads, we'll zoom into this span.
+ static let InitialZoomSpan = MKCoordinateSpan(latitudeDelta: MapViewController.InitialZoomDelta, longitudeDelta: MapViewController.InitialZoomDelta)
+
+ // The inverse of the percentage of the map view that should be captured in the region.
+ static let MapRegionFraction: Double = 4.0
+
+ // The size of the query region with respect to the map's zoom.
+ static let RegionQueryDegreeMultiplier: Double = 5.0
+
+ // MARK: Properties
+
+ @IBOutlet weak var overlayView: MapOverlayView!
+ @IBOutlet weak var searchBar: UISearchBar!
+ @IBOutlet weak var mapView: MKMapView!
+
+ var delegate: MapViewControllerDelegate?
+
+ var targetRegion: CLCircularRegion?
+
+ var circleOverlay: MKCircle? {
+ didSet {
+ // Remove the old overlay (if exists)
+ if let oldOverlay = oldValue {
+ mapView.removeOverlay(oldOverlay)
+ }
+
+ // Add the new overlay (if exists)
+ if let overlay = circleOverlay {
+ mapView.addOverlay(overlay)
+ }
+ }
+ }
+
+ var locationManager = CLLocationManager()
+
+ // MARK: View Methods
+
+ /// Configures the map view, search bar and location manager.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ searchBar.delegate = self
+ mapView.delegate = self
+ mapView.showsUserLocation = true
+ mapView.pitchEnabled = false
+ locationManager.delegate = self
+ }
+
+ /// Loads the user's location and zooms the target region.
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+ locationManager.requestWhenInUseAuthorization()
+ locationManager.requestLocation()
+
+ if let region = targetRegion {
+ annotateAndZoomToRegion(region)
+ }
+ }
+
+ /// Updates the overlay when the orientation changes.
+ override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
+ overlayView.setNeedsDisplay()
+ }
+
+ // MARK: Button Actions
+
+ /**
+ Generates a map region based on the map's position
+ and zoom, then notifies the delegate that the region has changed.
+ This will dismiss the view.
+ */
+ @IBAction func didTapSaveButton(sender: UIBarButtonItem) {
+ let circleDegreeDelta: CLLocationDegrees
+ let pointOnCircle: CLLocation
+
+ if mapView.region.span.latitudeDelta > mapView.region.span.longitudeDelta {
+ circleDegreeDelta = mapView.region.span.longitudeDelta / MapViewController.MapRegionFraction
+ pointOnCircle = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude - circleDegreeDelta)
+ }
+ else {
+ circleDegreeDelta = mapView.region.span.latitudeDelta / MapViewController.MapRegionFraction
+ pointOnCircle = CLLocation(latitude: mapView.region.center.latitude - circleDegreeDelta, longitude: mapView.region.center.longitude)
+ }
+
+
+ let mapCenterLocation = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude)
+ let distance = pointOnCircle.distanceFromLocation(mapCenterLocation)
+ let genericRegion = CLCircularRegion(center: mapView.region.center, radius: distance, identifier: Identifiers.circularRegion)
+
+ circleOverlay = MKCircle(centerCoordinate: genericRegion.center, radius: genericRegion.radius)
+ delegate?.mapViewDidUpdateRegion(genericRegion)
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ /// Dismisses the view without notifying the delegate.
+ @IBAction func didTapCancelButton(sender: UIBarButtonItem) {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ // MARK: Search Bar Methods
+
+ /**
+ Dismisses the keyboard and runs a new search from the
+ search bar.
+ */
+ func searchBarSearchButtonClicked(searchBar: UISearchBar) {
+ searchBar.resignFirstResponder()
+ mapView.removeAnnotations(mapView.annotations)
+ performSearch()
+ }
+
+ // MARK: Location Manager Methods
+
+ /// Zooms to the user's location if the region is not set.
+ func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ guard let lastLocation = locations.last else { return }
+ if targetRegion != nil {
+ // Do not zoom to the user's location if there is already a target region.
+ return
+ }
+ let newRegion = MKCoordinateRegion(center: lastLocation.coordinate, span: MapViewController.InitialZoomSpan)
+ mapView.setRegion(newRegion, animated: true)
+ }
+
+ /**
+ The method is required.
+ Simply logs the error.
+ */
+ func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
+ print("System: Location Manager Error: \(error)")
+ }
+
+ /**
+ When the user updates the authorization status, we want to
+ zoom to their current location by asking for it.
+ */
+ func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
+ locationManager.requestLocation()
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Updates the region overlay and zooms the map region
+
+ - parameter region: The new `CLCircularRegion`.
+ */
+ private func annotateAndZoomToRegion(region: CLCircularRegion) {
+ circleOverlay = MKCircle(centerCoordinate: region.center, radius: region.radius)
+ let multiplier = MapViewController.MapRegionFraction
+ let mapRegion = MKCoordinateRegionMakeWithDistance(region.center, region.radius*multiplier, region.radius*multiplier)
+ mapView.setRegion(mapRegion, animated: false)
+ }
+
+ /**
+ Performs a natural language search for locations
+ in the map's region that match the `searchBar`'s text.
+ */
+ private func performSearch() {
+ let request = MKLocalSearchRequest()
+ request.naturalLanguageQuery = searchBar.text
+ let multiplier = MapViewController.RegionQueryDegreeMultiplier
+ let querySpan = MKCoordinateSpan(latitudeDelta: mapView.region.span.latitudeDelta*multiplier, longitudeDelta: mapView.region.span.longitudeDelta*multiplier)
+ request.region = MKCoordinateRegion(center: mapView.region.center, span: querySpan)
+
+ let search = MKLocalSearch(request: request)
+
+ var matchingItems = [MKMapItem]()
+
+ search.startWithCompletionHandler { response, error in
+ let mapItems: [MKMapItem] = response?.mapItems ?? []
+ for item in mapItems {
+ matchingItems.append(item)
+ let annotation = MKPointAnnotation()
+ annotation.coordinate = item.placemark.coordinate
+ annotation.title = item.name
+ self.mapView.addAnnotation(annotation)
+ }
+ }
+ }
+
+ /// - returns: An `MKOverlayRenderer` with our custom stroke and fill.
+ func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
+ let circleRenderer = MKCircleRenderer(overlay: overlay)
+ circleRenderer.fillColor = UIColor.blueColor().colorWithAlphaComponent(0.2)
+ circleRenderer.strokeColor = UIColor.blackColor()
+ circleRenderer.lineWidth = 2.0
+ return circleRenderer
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerCreator.swift
new file mode 100644
index 00000000..1361496c
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerCreator.swift
@@ -0,0 +1,153 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TimerTriggerCreator` creates Timer triggers.
+*/
+
+import HomeKit
+
+/**
+ A `TriggerCreator` subclass which allows for the creation
+ of timer triggers.
+*/
+class TimerTriggerCreator: TriggerCreator {
+ static let RecurrenceComponents: [NSCalendarUnit] = [
+ .Hour,
+ .Day,
+ .WeekOfYear
+ ]
+
+ // MARK: Properties
+
+ var timerTrigger: HMTimerTrigger? {
+ return trigger as? HMTimerTrigger
+ }
+
+ var selectedRecurrenceIndex = NSNotFound
+
+ var rawFireDate = NSDate()
+ var fireDate: NSDate {
+ let flags: NSCalendarUnit = [.Year, .Weekday, .Month, .Day, .Hour, .Minute]
+ let dateComponents = NSCalendar.currentCalendar().components(flags, fromDate: self.rawFireDate)
+ let probableDate = NSCalendar.currentCalendar().dateFromComponents(dateComponents)
+ return probableDate ?? rawFireDate
+ }
+
+ // MARK: Trigger Creator Methods
+
+ /// Configures raw fire date and selected recurrence index.
+ required init(trigger: HMTrigger?, home: HMHome) {
+ super.init(trigger: trigger, home: home)
+ if let timerTrigger = timerTrigger {
+ rawFireDate = timerTrigger.fireDate
+ selectedRecurrenceIndex = recurrenceIndexFromDateComponents(timerTrigger.recurrence)
+ }
+ }
+
+ /// - returns: A new `HMTimerTrigger` with the stored configurations.
+ override func newTrigger() -> HMTrigger? {
+ return HMTimerTrigger(name: name, fireDate: fireDate, timeZone: NSCalendar.currentCalendar().timeZone, recurrence: recurrenceComponents, recurrenceCalendar: nil)
+ }
+
+ /// Updates the fire date and recurrence of the trigger.
+ override func updateTrigger() {
+ updateFireDateIfNecessary()
+ updateRecurrenceIfNecessary()
+ }
+
+ // MARK: Helper Methods
+
+ /**
+ Creates an NSDateComponent for the selected recurrence type.
+
+ - returns: An NSDateComponent where either `weekOfYear`,
+ `hour`, or `day` is set to 1.
+ */
+ var recurrenceComponents:NSDateComponents? {
+ if selectedRecurrenceIndex == NSNotFound {
+ return nil
+ }
+ let recurrenceComponents = NSDateComponents()
+ let unit = TimerTriggerCreator.RecurrenceComponents[selectedRecurrenceIndex]
+ switch unit {
+ case NSCalendarUnit.WeekOfYear:
+ recurrenceComponents.weekOfYear = 1
+
+ case NSCalendarUnit.Hour:
+ recurrenceComponents.hour = 1
+
+ case NSCalendarUnit.Day:
+ recurrenceComponents.day = 1
+
+ default:
+ break
+ }
+ return recurrenceComponents
+ }
+
+ /**
+ Maps the possible calendar units associated with recurrence titles, so we can properly
+ set our recurrenceUnit when an index is selected.
+
+ - parameter components: An optional `NSDateComponents` to query.
+
+ - returns: An index for the date components.
+ */
+ func recurrenceIndexFromDateComponents(components: NSDateComponents?) -> Int {
+ guard let components = components else { return NSNotFound }
+ var unit: NSCalendarUnit?
+ if components.day == 1 {
+ unit = NSCalendarUnit.Day
+ }
+ else if components.weekOfYear == 1 {
+ unit = NSCalendarUnit.WeekOfYear
+ }
+ else if components.hour == 1 {
+ unit = NSCalendarUnit.Hour
+ }
+ if let unit = unit {
+ return TimerTriggerCreator.RecurrenceComponents.indexOf(unit) ?? NSNotFound
+ }
+ return NSNotFound
+ }
+
+ /**
+ Updates the trigger's fire date, entering and leaving the dispatch group if necessary.
+ If the trigger's fire date is already equal to the passed-in fire date, this method does nothing.
+
+ - parameter fireDate: The trigger's new fire date.
+ */
+ private func updateFireDateIfNecessary() {
+ if timerTrigger?.fireDate == fireDate {
+ return
+ }
+ dispatch_group_enter(saveTriggerGroup)
+ timerTrigger?.updateFireDate(fireDate) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+
+ /**
+ Updates the trigger's recurrence components, entering and leaving the dispatch group if necessary.
+ If the trigger's components are already equal to the passed-in components, this method does nothing.
+
+ - parameter recurrenceComponents: The trigger's new recurrence components.
+ */
+ private func updateRecurrenceIfNecessary() {
+ if recurrenceComponents == timerTrigger?.recurrence {
+ return
+ }
+ dispatch_group_enter(saveTriggerGroup)
+ timerTrigger?.updateRecurrence(recurrenceComponents) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerViewController.swift
new file mode 100644
index 00000000..0c42b9c3
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerViewController.swift
@@ -0,0 +1,195 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TimerTriggerViewController` allows the user to create Timer triggers.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller which facilitates the creation of timer triggers.
+class TimerTriggerViewController: TriggerViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let recurrenceCell = "RecurrenceCell"
+ }
+
+ static let RecurrenceTitles = [
+ NSLocalizedString("Every Hour", comment: "Every Hour"),
+ NSLocalizedString("Every Day", comment: "Every Day"),
+ NSLocalizedString("Every Week", comment: "Every Week")
+ ]
+
+ // MARK: Properties
+
+ @IBOutlet weak var datePicker: UIDatePicker!
+
+ /**
+ Sets the stored fireDate to the new value.
+ HomeKit only accepts dates aligned with minute boundaries,
+ so we use NSDateComponents to only get the appropriate pieces of information from that date.
+ Eventually we will end up with a date following this format: "MM/dd/yyyy hh:mm"
+ */
+
+ var timerTrigger: HMTimerTrigger? {
+ return trigger as? HMTimerTrigger
+ }
+
+ var timerTriggerCreator: TimerTriggerCreator {
+ return triggerCreator as! TimerTriggerCreator
+ }
+
+ // MARK: View Methods
+
+ /// Configures the views and registers for table view cells.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 44.0
+ triggerCreator = TimerTriggerCreator(trigger: trigger, home: home)
+ datePicker.date = timerTriggerCreator.fireDate
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.recurrenceCell)
+ }
+
+ // MARK: IBAction Methods
+
+ /// Reset our saved fire date to the date in the picker.
+ @IBAction func didChangeDate(picker: UIDatePicker) {
+ timerTriggerCreator.rawFireDate = picker.date
+ }
+
+ // MARK: Table View Methods
+
+ /**
+ - returns: The number of rows in the Recurrence section;
+ defaults to the super implementation for other sections
+ */
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sectionForIndex(section) {
+ case .Recurrence?:
+ return TimerTriggerViewController.RecurrenceTitles.count
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, numberOfRowsInSection: section)
+ }
+ }
+
+ /**
+ Generates a recurrence cell.
+ Defaults to the super implementation for other sections
+ */
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ switch sectionForIndex(indexPath.section) {
+ case .Recurrence?:
+ return self.tableView(tableView, recurrenceCellForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /// Creates a cell that represents a recurrence type.
+ func tableView(tableView: UITableView, recurrenceCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.recurrenceCell, forIndexPath: indexPath)
+ let title = TimerTriggerViewController.RecurrenceTitles[indexPath.row]
+ cell.textLabel?.text = title
+
+ // The current preferred recurrence style should have a check mark.
+ if indexPath.row == timerTriggerCreator.selectedRecurrenceIndex {
+ cell.accessoryType = .Checkmark
+ }
+ else {
+ cell.accessoryType = .None
+ }
+ return cell
+ }
+
+ /**
+ Tell the tableView to automatically size the custom rows, while using the superclass's
+ static sizing for the static cells.
+ */
+ override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
+ switch sectionForIndex(indexPath.section) {
+ case .Recurrence?:
+ return UITableViewAutomaticDimension
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Handles recurrence cell selection.
+ Defaults to the super implementation for other sections
+ */
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ switch sectionForIndex(indexPath.section) {
+ case .Recurrence?:
+ self.tableView(tableView, didSelectRecurrenceComponentAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Handles selection of a recurrence cell.
+
+ If the newly selected recurrence component is the previously selected
+ recurrence component, reset the current selected component to `NSNotFound`
+ and deselect that row.
+ */
+ func tableView(tableView: UITableView, didSelectRecurrenceComponentAtIndexPath indexPath: NSIndexPath) {
+ if indexPath.row == timerTriggerCreator.selectedRecurrenceIndex {
+ timerTriggerCreator.selectedRecurrenceIndex = NSNotFound
+ }
+ else {
+ timerTriggerCreator.selectedRecurrenceIndex = indexPath.row
+ }
+ tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
+ }
+
+ /**
+ - parameter index: The section index.
+
+ - returns: The `TriggerTableViewSection` for the given index.
+ */
+ override func sectionForIndex(index: Int) -> TriggerTableViewSection? {
+ switch index {
+ case 0:
+ return .Name
+
+ case 1:
+ return .Enabled
+
+ case 2:
+ return .DateAndTime
+
+ case 3:
+ return .Recurrence
+
+ case 4:
+ return .ActionSets
+
+ default:
+ return nil
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerCreator.swift
new file mode 100644
index 00000000..2a5d5d25
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerCreator.swift
@@ -0,0 +1,161 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TriggerCreator` is a superclass that builds triggers.
+*/
+
+import HomeKit
+
+/**
+ A superclass for all trigger creators.
+
+ These classes manage the temporary state of the trigger
+ and unify some of the saving processes.
+*/
+class TriggerCreator {
+ // MARK: Properties
+
+ internal var home: HMHome
+ internal var trigger: HMTrigger?
+ internal var name = ""
+ internal let saveTriggerGroup = dispatch_group_create()
+ internal var errors = [NSError]()
+
+ /**
+ Initializes a trigger creator from an existing trigger (if it exists),
+ and the current home.
+
+ - parameter trigger: An `HMTrigger` or `nil`, if creation is desired.
+ - parameter home: The `HMHome` into which this trigger will go.
+ */
+ required init(trigger: HMTrigger?, home: HMHome) {
+ self.home = home
+ self.trigger = trigger
+ }
+
+ /**
+ Completes one of two actions based on the current status of the `trigger` object:
+
+ 1. Updates the existing trigger.
+ 2. Creates a new trigger.
+
+ - parameter name: The name to set for the new or updated trigger.
+ - parameter actionSets: The new list of action sets to set for the trigger
+ - parameter completion: The closure to call when all configurations have been completed.
+ */
+ func saveTriggerWithName(name: String, actionSets: [HMActionSet], completion: (HMTrigger?, [NSError]) -> Void) {
+ self.name = name
+ if trigger != nil {
+ // Let the subclass update the trigger.
+ updateTrigger()
+ updateNameIfNecessary()
+ configureWithActionSets(actionSets)
+ }
+ else {
+ self.trigger = newTrigger()
+ dispatch_group_enter(saveTriggerGroup)
+ home.addTrigger(trigger!) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ else {
+ self.configureWithActionSets(actionSets)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+
+ /*
+ Call the completion block with our event trigger and any accumulated errors
+ from the saving process.
+ */
+ dispatch_group_notify(saveTriggerGroup, dispatch_get_main_queue()) {
+ self.cleanUp()
+ completion(self.trigger, self.errors)
+ }
+ }
+
+ /**
+ Updates the trigger's internals.
+ Action sets and the trigger name need not be configured.
+
+ Implemented by subclasses.
+ */
+ internal func updateTrigger() { }
+
+ /**
+ Creates a new trigger to be added to the home.
+ Action sets and the trigger name need not be configured.
+
+ Implemented by subclasses.
+
+ - returns: A new, generated `HMTrigger`.
+ */
+ internal func newTrigger() -> HMTrigger? {
+ return nil
+ }
+
+ /**
+ Cleans up an internal structures after the trigger has been saved.
+
+ Implemented by subclasses.
+ */
+ internal func cleanUp() {}
+
+
+ // MARK: Helper Methods
+
+ /**
+ Syncs the trigger's action sets with the specified array of action sets.
+
+ - parameter actionSets: Array of `HMActionSet`s to match.
+ */
+ private func configureWithActionSets(actionSets: [HMActionSet]) {
+ guard let trigger = trigger else { return }
+ /*
+ Save a standard completion handler to use when we either add or remove
+ an action set.
+ */
+ let defaultCompletion: NSError? -> Void = { error in
+ // Leave the dispatch group, to notify that we've finished this task.
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+
+ // First pass, remove the action sets that have been deselected.
+ for actionSet in trigger.actionSets {
+ if actionSets.contains(actionSet) {
+ continue
+ }
+ dispatch_group_enter(saveTriggerGroup)
+ trigger.removeActionSet(actionSet, completionHandler: defaultCompletion)
+ }
+
+ // Second pass, add the new action sets that were just selected.
+ for actionSet in actionSets {
+ if trigger.actionSets.contains(actionSet) {
+ continue
+ }
+ dispatch_group_enter(saveTriggerGroup)
+ trigger.addActionSet(actionSet, completionHandler: defaultCompletion)
+ }
+ }
+
+ /// Updates the trigger's name from the stored name, entering and leaving the dispatch group if necessary.
+ func updateNameIfNecessary() {
+ if trigger?.name == self.name {
+ return
+ }
+ dispatch_group_enter(saveTriggerGroup)
+ trigger?.updateName(name) { error in
+ if let error = error {
+ self.errors.append(error)
+ }
+ dispatch_group_leave(self.saveTriggerGroup)
+ }
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerViewController.swift
new file mode 100644
index 00000000..185cd232
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerViewController.swift
@@ -0,0 +1,304 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TriggerViewController` is a superclass which allows users to create triggers.
+*/
+
+import UIKit
+import HomeKit
+
+/// Represents all possible sections in a `TriggerViewController` subclass.
+enum TriggerTableViewSection: Int {
+ // All triggers have these sections.
+ case Name, Enabled, ActionSets
+
+ // Timer triggers only.
+ case DateAndTime, Recurrence
+
+ // Location and Characteristic triggers only.
+ case Conditions
+
+ // Location triggers only.
+ case Location, Region
+
+ // Characteristic triggers only.
+ case Characteristics
+}
+
+/**
+ A superclass for all trigger view controllers.
+
+ It manages the name, enabled state, and action set components of the view,
+ as these are shared components.
+*/
+class TriggerViewController: HMCatalogViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let actionSetCell = "ActionSetCell"
+ }
+
+ // MARK: Properties
+
+ @IBOutlet weak var saveButton: UIBarButtonItem!
+ @IBOutlet weak var nameField: UITextField!
+ @IBOutlet weak var enabledSwitch: UISwitch!
+
+ var trigger: HMTrigger?
+ var triggerCreator: TriggerCreator?
+
+ /// An internal array of all action sets in the home.
+ var actionSets: [HMActionSet]!
+
+ /**
+ An array of all action sets that the user has selected.
+ This will be used to save the trigger when it is finalized.
+ */
+ lazy var selectedActionSets = [HMActionSet]()
+
+ // MARK: View Methods
+
+ /// Resets internal data, sets initial UI, and configures the table view.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ let filteredActionSets = home.actionSets.filter { actionSet in
+ return !actionSet.actions.isEmpty
+ }
+
+ actionSets = filteredActionSets.sortByTypeAndLocalizedName()
+ tableView.rowHeight = UITableViewAutomaticDimension
+ tableView.estimatedRowHeight = 44.0
+
+ /*
+ If we have a trigger, set the saved properties to the current properties
+ of the passed-in trigger.
+ */
+ if let trigger = trigger {
+ selectedActionSets = trigger.actionSets
+ nameField.text = trigger.name
+ enabledSwitch.on = trigger.enabled
+ }
+
+ enableSaveButtonIfApplicable()
+
+ tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.actionSetCell)
+ }
+
+ // MARK: IBAction Methods
+
+ /**
+ Any time the name field changed, reevaluate whether or not
+ to enable the save button.
+ */
+ @IBAction func nameFieldDidChange(sender: UITextField) {
+ enableSaveButtonIfApplicable()
+ }
+
+ /// Saves the trigger and dismisses this view controller.
+ @IBAction func saveAndDismiss() {
+ saveButton.enabled = false
+ triggerCreator?.saveTriggerWithName(trimmedName, actionSets: selectedActionSets) { trigger, errors in
+ self.trigger = trigger
+ self.saveButton.enabled = true
+
+ if !errors.isEmpty {
+ self.displayErrors(errors)
+ return
+ }
+
+ self.enableTrigger(self.trigger!) {
+ self.dismiss()
+ }
+ }
+ }
+
+ @IBAction func dismiss() {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+
+ // MARK: Subclass Methods
+
+ /**
+ Generates the section for the index.
+
+ This allows for the subclasses to lay out their content in different sections
+ while still maintaining common code in the `TriggerViewController`.
+
+ - parameter index: The index of the section
+
+ - returns: The `TriggerTableViewSection` for the provided index.
+ */
+ func sectionForIndex(index: Int) -> TriggerTableViewSection? {
+ return nil
+ }
+
+ // MARK: Helper Methods
+
+ /// Enable the trigger if necessary.
+ func enableTrigger(trigger: HMTrigger, completion: Void -> Void) {
+ if trigger.enabled == enabledSwitch.on {
+ completion()
+ return
+ }
+
+ trigger.enable(enabledSwitch.on) { error in
+ if let error = error {
+ self.displayError(error)
+ }
+ else {
+ completion()
+ }
+ }
+ }
+
+ /**
+ Enables the save button if:
+
+ 1. The name field is not empty, and
+ 2. There will be at least one action set in the trigger after saving.
+ */
+ private func enableSaveButtonIfApplicable() {
+ saveButton.enabled = !trimmedName.characters.isEmpty &&
+ (!selectedActionSets.isEmpty || trigger?.actionSets.count > 0)
+ }
+
+ /// - returns: The name from the `nameField`, stripping newline and whitespace characters.
+ var trimmedName: String {
+ return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
+ }
+
+ // MARK: Table View Methods
+
+ /// Creates a cell that represents either a selected or unselected action set cell.
+ private func tableView(tableView: UITableView, actionSetCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.actionSetCell, forIndexPath: indexPath)
+ let actionSet = actionSets[indexPath.row]
+
+ if selectedActionSets.contains(actionSet) {
+ cell.accessoryType = .Checkmark
+ }
+ else {
+ cell.accessoryType = .None
+ }
+
+ cell.textLabel?.text = actionSet.name
+
+ return cell
+ }
+
+
+ /// Only handles the ActionSets case, defaults to super.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ if sectionForIndex(section) == .ActionSets {
+ return actionSets.count ?? 0
+ }
+
+ return super.tableView(tableView, numberOfRowsInSection: section)
+ }
+
+ /// Only handles the ActionSets case, defaults to super.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ if sectionForIndex(indexPath.section) == .ActionSets {
+ return self.tableView(tableView, actionSetCellForRowAtIndexPath: indexPath)
+ }
+
+ return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
+ }
+
+ /**
+ This is necessary for mixing static and dynamic table view cells.
+ We return a fake index path because otherwise the superclass's implementation (which does not
+ know about the extra cells we're adding) will cause an error.
+
+ - returns: The superclass's indentationLevel for the first row in the provided section,
+ instead of the provided row.
+ */
+ override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
+ let newIndexPath = NSIndexPath(forRow: 0, inSection: indexPath.section)
+
+ return super.tableView(tableView, indentationLevelForRowAtIndexPath: newIndexPath)
+ }
+
+ /**
+ Tell the tableView to automatically size the custom rows, while using the superclass's
+ static sizing for the static cells.
+ */
+ override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
+ switch sectionForIndex(indexPath.section) {
+ case .Name?, .Enabled?:
+ return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return UITableViewAutomaticDimension
+ }
+ }
+
+ /// Handles row selction for action sets, defaults to super implementation.
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ tableView.deselectRowAtIndexPath(indexPath, animated: true)
+ if sectionForIndex(indexPath.section) == .ActionSets {
+ self.tableView(tableView, didSelectActionSetAtIndexPath: indexPath)
+ }
+ }
+
+ /**
+ Manages footer titles for higher-level sections. Superclasses should fall back
+ on this implementation after attempting to handle any special trigger sections.
+ */
+ override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
+ switch sectionForIndex(section) {
+ case .ActionSets?:
+ return NSLocalizedString("When this trigger is activated, it will set these scenes. You can only select scenes which have at least one action.", comment: "Scene Trigger Description")
+
+ case .Enabled?:
+ return NSLocalizedString("This trigger will only activate if it is enabled. You can disable triggers to temporarily stop them from running.", comment: "Trigger Enabled Description")
+
+ case nil:
+ fatalError("Unexpected `TriggerTableViewSection` raw value.")
+
+ default:
+ return super.tableView(tableView, titleForFooterInSection: section)
+ }
+ }
+
+ /**
+ Handle selection of an action set cell. If the action set is already part of the selected action sets,
+ then remove it from the selected list. Otherwise, add it to the selected list.
+ */
+ func tableView(tableView: UITableView, didSelectActionSetAtIndexPath indexPath: NSIndexPath) {
+ let actionSet = actionSets[indexPath.row]
+ if let index = selectedActionSets.indexOf(actionSet) {
+ selectedActionSets.removeAtIndex(index)
+ }
+ else {
+ selectedActionSets.append(actionSet)
+ }
+
+ enableSaveButtonIfApplicable()
+ tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /**
+ If our trigger has been removed from the home,
+ dismiss the view controller.
+ */
+ func home(home: HMHome, didRemoveTrigger trigger: HMTrigger) {
+ if self.trigger == trigger{
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+
+ /// If our trigger has been updated, reload our data.
+ func home(home: HMHome, didUpdateTrigger trigger: HMTrigger) {
+ if self.trigger == trigger{
+ tableView.reloadData()
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Zones/AddRoomViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Zones/AddRoomViewController.swift
new file mode 100644
index 00000000..33aff7b3
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Zones/AddRoomViewController.swift
@@ -0,0 +1,143 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `AddRoomViewController` allows the user to add rooms to a zone.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller that lists rooms within a home and allows the user to add the rooms to a provided zone.
+class AddRoomViewController: HMCatalogViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let roomCell = "RoomCell"
+ }
+
+ // MARK: Properties
+
+ var homeZone: HMZone!
+
+ lazy var displayedRooms = [HMRoom]()
+ lazy var selectedRooms = [HMRoom]()
+
+ // MARK: View Methods
+
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ title = homeZone.name
+ resetDisplayedRooms()
+ }
+
+ /// Adds the selected rooms to the zone and dismisses the view.
+ @IBAction func dismiss(sender: AnyObject) {
+ addSelectedRoomsToZoneWithCompletionHandler {
+ self.dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+
+ /**
+ Creates a dispatch group, adds all of the rooms to the zone,
+ and runs the provided completion once all rooms have been added.
+
+ - parameter completion: A closure to call once all rooms have been added.
+ */
+ func addSelectedRoomsToZoneWithCompletionHandler(completion: () -> Void) {
+ let group = dispatch_group_create()
+ for room in selectedRooms {
+ dispatch_group_enter(group)
+ homeZone.addRoom(room) { error in
+ if let error = error {
+ self.displayError(error)
+ }
+ dispatch_group_leave(group)
+ }
+ }
+ dispatch_group_notify(group, dispatch_get_main_queue(), completion)
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of displayed rooms.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return displayedRooms.count
+ }
+
+ /// - returns: A cell that includes the name of a room and a checkmark if it's intended to be added to the zone.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath)
+
+ let room = displayedRooms[indexPath.row]
+
+ cell.textLabel?.text = room.name
+ cell.accessoryType = selectedRooms.contains(room) ? .Checkmark : .None
+
+ return cell
+ }
+
+ /// Adds the selected room to the selected rooms array and reloads that cell
+ override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
+ let room = displayedRooms[indexPath.row]
+
+ if let index = selectedRooms.indexOf(room) {
+ selectedRooms.removeAtIndex(index)
+ }
+ else {
+ selectedRooms.append(room)
+ }
+
+ tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+ }
+
+ /// Resets the list of displayed rooms and reloads the table.
+ func resetDisplayedRooms() {
+ displayedRooms = home.roomsNotAlreadyInZone(homeZone, includingRooms: selectedRooms)
+ if displayedRooms.isEmpty {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ else {
+ tableView.reloadData()
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// If our zone was removed, dismiss this view.
+ func home(home: HMHome, didRemoveZone zone: HMZone) {
+ if zone == homeZone {
+ dismissViewControllerAnimated(true, completion: nil)
+ }
+ }
+
+ /// If our zone was renamed, reset our title.
+ func home(home: HMHome, didUpdateNameForZone zone: HMZone) {
+ if zone == homeZone {
+ title = zone.name
+ }
+ }
+
+ // All home updates reset the displayed homes and reload the view.
+
+ func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
+ resetDisplayedRooms()
+ }
+
+ func home(home: HMHome, didAddRoom room: HMRoom) {
+ resetDisplayedRooms()
+ }
+
+ func home(home: HMHome, didRemoveRoom room: HMRoom) {
+ resetDisplayedRooms()
+ }
+
+ func home(home: HMHome, didAddRoom room: HMRoom, toZone zone: HMZone) {
+ resetDisplayedRooms()
+ }
+
+ func home(home: HMHome, didRemoveRoom room: HMRoom, fromZone zone: HMZone) {
+ resetDisplayedRooms()
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Homes/Zones/ZoneViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Zones/ZoneViewController.swift
new file mode 100644
index 00000000..485fec36
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Homes/Zones/ZoneViewController.swift
@@ -0,0 +1,264 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `ZoneViewController` lists the rooms in a zone.
+*/
+
+import UIKit
+import HomeKit
+
+/// A view controller that lists the rooms within a provided zone.
+class ZoneViewController: HMCatalogViewController {
+ // MARK: Types
+
+ struct Identifiers {
+ static let roomCell = "RoomCell"
+ static let addCell = "AddCell"
+ static let disabledAddCell = "DisabledAddCell"
+ static let addRoomsSegue = "Add Rooms"
+ }
+
+ // MARK: Properties
+
+ var homeZone: HMZone!
+ var rooms = [HMRoom]()
+
+ // MARK: View Methods
+
+ /// Reload the data and configure the view.
+ override func viewWillAppear(animated: Bool) {
+ super.viewWillAppear(animated)
+ title = homeZone.name
+ reloadData()
+ }
+
+ /// If our data is invalid, pop the view controller.
+ override func viewDidAppear(animated: Bool) {
+ super.viewDidAppear(animated)
+ if shouldPopViewController() {
+ navigationController?.popViewControllerAnimated(true)
+ }
+ }
+
+ /// Provide the zone to `AddRoomViewController`.
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ super.prepareForSegue(segue, sender: sender)
+ if segue.identifier == Identifiers.addRoomsSegue {
+ let addViewController = segue.intendedDestinationViewController as! AddRoomViewController
+ addViewController.homeZone = homeZone
+ }
+ }
+
+ // MARK: Helper Methods
+
+ /// Resets the internal list of rooms and reloads the table view.
+ private func reloadData() {
+ rooms = homeZone.rooms.sortByLocalizedName()
+ tableView.reloadData()
+ }
+
+ /// Sorts the internal list of rooms by localized name.
+ private func sortRooms() {
+ rooms = rooms.sortByLocalizedName()
+ }
+
+ /// - returns: The `NSIndexPath` where the 'Add Cell' should be located.
+ private var addIndexPath: NSIndexPath {
+ return NSIndexPath(forRow: rooms.count, inSection: 0)
+ }
+
+ /**
+ - parameter indexPath: The index path in question.
+
+ - returns: `true` if the indexPath should contain
+ an 'add' cell, `false` otherwise
+ */
+ private func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
+ return indexPath.row == addIndexPath.row
+ }
+
+ /**
+ Reloads the `addIndexPath`.
+
+ This is typically used when something has changed to allow
+ the user to add a room.
+ */
+ private func reloadAddIndexPath() {
+ tableView.reloadRowsAtIndexPaths([addIndexPath], withRowAnimation: .Automatic)
+ }
+
+ /**
+ Adds a room to the internal array of rooms and inserts new row
+ into the table view.
+
+ - parameter room: The new `HMRoom` to add.
+ */
+ private func didAddRoom(room: HMRoom) {
+ rooms.append(room)
+
+ sortRooms()
+
+ if let newRoomIndex = rooms.indexOf(room) {
+ let newRoomIndexPath = NSIndexPath(forRow: newRoomIndex, inSection: 0)
+ tableView.insertRowsAtIndexPaths([newRoomIndexPath], withRowAnimation: .Automatic)
+ }
+
+ reloadAddIndexPath()
+ }
+
+ /**
+ Removes a room from the internal array of rooms and deletes
+ the row from the table view.
+
+ - parameter room: The `HMRoom` to remove.
+ */
+ private func didRemoveRoom(room: HMRoom) {
+ if let roomIndex = rooms.indexOf(room) {
+ rooms.removeAtIndex(roomIndex)
+ let roomIndexPath = NSIndexPath(forRow: roomIndex, inSection: 0)
+ tableView.deleteRowsAtIndexPaths([roomIndexPath], withRowAnimation: .Automatic)
+ }
+
+ reloadAddIndexPath()
+ }
+
+ /**
+ Reloads the cell corresponding a given room.
+
+ - parameter room: The `HMRoom` to reload.
+ */
+ private func didUpdateRoom(room: HMRoom) {
+ if let roomIndex = rooms.indexOf(room) {
+ let roomIndexPath = NSIndexPath(forRow: roomIndex, inSection: 0)
+ tableView.reloadRowsAtIndexPaths([roomIndexPath], withRowAnimation: .Automatic)
+ }
+ }
+
+ /**
+ Removes a room from HomeKit and updates the view.
+
+ - parameter room: The `HMRoom` to remove.
+ */
+ private func removeRoom(room: HMRoom) {
+ didRemoveRoom(room)
+ homeZone.removeRoom(room) { error in
+ if let error = error {
+ self.displayError(error)
+ self.didAddRoom(room)
+ }
+ }
+ }
+
+ /**
+ - returns: `true` if our current home no longer
+ exists, `false` otherwise.
+ */
+ private func shouldPopViewController() -> Bool {
+ for zone in home.zones {
+ if zone == homeZone {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ - returns: `true` if more rooms can be added to this zone;
+ `false` otherwise.
+ */
+ private var canAddRoom: Bool {
+ return rooms.count < home.rooms.count
+ }
+
+ // MARK: Table View Methods
+
+ /// - returns: The number of rooms in the zone, plus 1 for the 'add' row.
+ override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return rooms.count + 1
+ }
+
+ /// - returns: A cell containing the name of an HMRoom.
+ override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
+ if indexPathIsAdd(indexPath) {
+ let reuseIdentifier = home.isAdmin && canAddRoom ? Identifiers.addCell : Identifiers.disabledAddCell
+
+ return tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
+ }
+
+ let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath)
+
+ cell.textLabel?.text = rooms[indexPath.row].name
+
+ return cell
+ }
+
+ /**
+ - returns: `true` if the cell is anything but an 'add' cell;
+ `false` otherwise.
+ */
+ override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
+ return home.isAdmin && !indexPathIsAdd(indexPath)
+ }
+
+ /// Deletes the room at the provided index path.
+ override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
+ if editingStyle == .Delete {
+ let room = rooms[indexPath.row]
+
+ removeRoom(room)
+ }
+ }
+
+ // MARK: HMHomeDelegate Methods
+
+ /// If our zone was removed, pop the view controller.
+ func home(home: HMHome, didRemoveZone zone: HMZone) {
+ if zone == homeZone{
+ navigationController?.popViewControllerAnimated(true)
+ }
+ }
+
+ /// If our zone was renamed, update the title.
+ func home(home: HMHome, didUpdateNameForZone zone: HMZone) {
+ if zone == homeZone {
+ title = zone.name
+ }
+ }
+
+ /// Update the row for the room.
+ func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
+ didUpdateRoom(room)
+ }
+
+ /**
+ A room has been added, we may be able to add it to the zone.
+ Reload the 'addIndexPath'
+ */
+ func home(home: HMHome, didAddRoom room: HMRoom) {
+ reloadAddIndexPath()
+ }
+
+ /**
+ A room has been removed, attempt to remove it from the room.
+ This will always reload the 'addIndexPath'.
+ */
+ func home(home: HMHome, didRemoveRoom room: HMRoom) {
+ didRemoveRoom(room)
+ }
+
+ /// If the room was added to our zone, add it to the view.
+ func home(home: HMHome, didAddRoom room: HMRoom, toZone zone: HMZone) {
+ if zone == homeZone {
+ didAddRoom(room)
+ }
+ }
+
+ /// If the room was removed from our zone, remove it from the view.
+ func home(home: HMHome, didRemoveRoom room: HMRoom, fromZone zone: HMZone) {
+ if zone == homeZone {
+ didRemoveRoom(room)
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..6aff7f55
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,139 @@
+{
+ "images" : [
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-Small-40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "size" : "57x57",
+ "idiom" : "iphone",
+ "filename" : "Icon.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "57x57",
+ "idiom" : "iphone",
+ "filename" : "Icon@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-40.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-40@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "50x50",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-50.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "50x50",
+ "idiom" : "ipad",
+ "filename" : "Icon-Small-50@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "72x72",
+ "idiom" : "ipad",
+ "filename" : "Icon-72.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "72x72",
+ "idiom" : "ipad",
+ "filename" : "Icon-72@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-76.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-83_5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "iTunesArtwork.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "iTunesArtwork@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png
new file mode 100644
index 00000000..362a93a6
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png
new file mode 100644
index 00000000..b775c601
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72.png
new file mode 100644
index 00000000..05293876
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png
new file mode 100644
index 00000000..e41d2860
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76.png
new file mode 100644
index 00000000..4a9ce07b
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png
new file mode 100644
index 00000000..326c688e
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-83_5@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-83_5@2x.png
new file mode 100644
index 00000000..8e158097
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-83_5@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png
new file mode 100644
index 00000000..b07196be
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png
new file mode 100644
index 00000000..74dc7c6d
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png
new file mode 100644
index 00000000..b114c7a3
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png
new file mode 100644
index 00000000..12a43145
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png
new file mode 100644
index 00000000..da97d757
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
new file mode 100644
index 00000000..13c5af8f
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small.png
new file mode 100644
index 00000000..b07196be
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png
new file mode 100644
index 00000000..6113e73f
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png
new file mode 100644
index 00000000..6113e73f
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png
new file mode 100644
index 00000000..2e8d2304
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon.png
new file mode 100644
index 00000000..b1aa344a
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon@2x.png
new file mode 100644
index 00000000..5ea17591
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork.png
new file mode 100644
index 00000000..84891266
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png
new file mode 100644
index 00000000..84c50b5b
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf
new file mode 100644
index 00000000..8a53bc5a
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json
new file mode 100644
index 00000000..44e2bdb0
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "Configure.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/Contents.json
new file mode 100644
index 00000000..da4a164c
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json
new file mode 100644
index 00000000..9d6c89ff
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "Control.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf
new file mode 100644
index 00000000..09af6d0a
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json
new file mode 100644
index 00000000..db480e28
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "FavoriteTabIcon.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf
new file mode 100644
index 00000000..23eeb586
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json
new file mode 100644
index 00000000..2058f2a2
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "SelectedConfigureTabIcon.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/SelectedConfigureTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/SelectedConfigureTabIcon.pdf
new file mode 100644
index 00000000..245876a4
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/SelectedConfigureTabIcon.pdf differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json
new file mode 100644
index 00000000..ef1356bd
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "SelectedControlTabIcon.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf
new file mode 100644
index 00000000..5a5e5a1e
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json
new file mode 100644
index 00000000..043acf3d
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "SelectedFavoriteTabIcon.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/SelectedFavoriteTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/SelectedFavoriteTabIcon.pdf
new file mode 100644
index 00000000..ba2dc48b
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/SelectedFavoriteTabIcon.pdf differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json
new file mode 100644
index 00000000..775fd030
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "stariosfilled.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "stariosfilled@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "stariosfilled@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png
new file mode 100644
index 00000000..29d49ced
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png
new file mode 100644
index 00000000..0538ad84
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png
new file mode 100644
index 00000000..57a5535b
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json
new file mode 100644
index 00000000..531a670e
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "starios.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "starios@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "starios@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png
new file mode 100644
index 00000000..116373f5
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png
new file mode 100644
index 00000000..3d6e3d15
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png
new file mode 100644
index 00000000..814f6c77
Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png differ
diff --git a/HomeKitCatalog/HMCatalog/Launch Screen.storyboard b/HomeKitCatalog/HMCatalog/Launch Screen.storyboard
new file mode 100644
index 00000000..b5f6eac5
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Launch Screen.storyboard
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Main.storyboard b/HomeKitCatalog/HMCatalog/Main.storyboard
new file mode 100644
index 00000000..747c8113
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Main.storyboard
@@ -0,0 +1,2288 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/Array+Sorting.swift b/HomeKitCatalog/HMCatalog/Supporting Files/Array+Sorting.swift
new file mode 100644
index 00000000..4234debb
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/Array+Sorting.swift
@@ -0,0 +1,68 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `Array+Sorting` extension allows for easy sorting of HomeKit objects.
+*/
+
+import HomeKit
+
+/// A protocol for objects which have a property called `name`.
+protocol Nameable {
+ var name: String { get }
+}
+
+/*
+ All of these HomeKit objects have names and can conform
+ to this protocol without modification.
+*/
+
+extension HMHome: Nameable {}
+extension HMAccessory: Nameable {}
+extension HMRoom: Nameable {}
+extension HMZone: Nameable {}
+extension HMActionSet: Nameable {}
+extension HMService: Nameable {}
+extension HMServiceGroup: Nameable {}
+extension HMTrigger: Nameable {}
+
+extension CollectionType where Generator.Element: Nameable {
+ /**
+ Generates a new array from the original collection,
+ sorted by localized name.
+
+ - returns: New array sorted by localized name.
+ */
+ func sortByLocalizedName() -> [Generator.Element] {
+ return sort { return $0.name.localizedCompare($1.name) == .OrderedAscending }
+ }
+}
+
+extension CollectionType where Generator.Element: HMActionSet {
+ /**
+ Generates a new array from the original collection,
+ sorted by built-in first, then user-defined sorted
+ by localized name.
+
+ - returns: New array sorted by localized name.
+ */
+ func sortByTypeAndLocalizedName() -> [HMActionSet] {
+ return sort { (actionSet1, actionSet2) -> Bool in
+ if actionSet1.isBuiltIn != actionSet2.isBuiltIn {
+ // If comparing a built-in and a user-defined, the built-in is ranked first.
+ return actionSet1.isBuiltIn
+ }
+ else if actionSet1.isBuiltIn && actionSet2.isBuiltIn {
+ // If comparing two built-ins, we follow a standard ranking
+ let firstIndex = HMActionSet.Constants.builtInActionSetTypes.indexOf(actionSet1.actionSetType) ?? NSNotFound
+ let secondIndex = HMActionSet.Constants.builtInActionSetTypes.indexOf(actionSet2.actionSetType) ?? NSNotFound
+ return firstIndex < secondIndex
+ }
+ else {
+ // If comparing two user-defines, sort by localized name.
+ return actionSet1.name.localizedCompare(actionSet2.name) == .OrderedAscending
+ }
+ }
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/CNMutablePostalAddress+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/CNMutablePostalAddress+Convenience.swift
new file mode 100644
index 00000000..df46b09e
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/CNMutablePostalAddress+Convenience.swift
@@ -0,0 +1,22 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `CNMutablePostalAddress+Convenience` method creates `CNMutablePostalAddress` from a `CLPlacemark`.
+*/
+
+import MapKit
+import Contacts
+
+extension CNMutablePostalAddress {
+ /// Constructs a `CNMutablePostalAddress` from a `CLPlacemark`
+ convenience init(placemark: CLPlacemark) {
+ self.init()
+ self.street = (placemark.subThoroughfare ?? "") + " " + (placemark.thoroughfare ?? "")
+ self.city = placemark.locality ?? ""
+ self.state = placemark.administrativeArea ?? ""
+ self.postalCode = placemark.postalCode ?? ""
+ self.country = placemark.country ?? ""
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/HMActionSet+BuiltIn.swift b/HomeKitCatalog/HMCatalog/Supporting Files/HMActionSet+BuiltIn.swift
new file mode 100644
index 00000000..77421c1c
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/HMActionSet+BuiltIn.swift
@@ -0,0 +1,20 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HMActionSet+BuiltIn` extension provides a method for determining whether or not an action set is built-in.
+*/
+
+import HomeKit
+
+extension HMActionSet {
+ struct Constants {
+ static let builtInActionSetTypes = [HMActionSetTypeWakeUp, HMActionSetTypeHomeDeparture, HMActionSetTypeHomeArrival, HMActionSetTypeSleep]
+ }
+
+ /// - returns: `true` if the action set is built-in; `false` otherwise.
+ var isBuiltIn: Bool {
+ return Constants.builtInActionSetTypes.contains(self.actionSetType)
+ }
+}
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/HMCharacteristic+Properties.swift b/HomeKitCatalog/HMCatalog/Supporting Files/HMCharacteristic+Properties.swift
new file mode 100644
index 00000000..7954b26f
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/HMCharacteristic+Properties.swift
@@ -0,0 +1,428 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HMCharacteristic+Properties` methods are used to generate localized strings related to a characteristic or to evaluate a characteristic's type.
+*/
+
+import HomeKit
+
+extension HMCharacteristic {
+
+ private struct Constants {
+ static let valueFormatter = NSNumberFormatter()
+ static let numericFormats = [
+ HMCharacteristicMetadataFormatInt,
+ HMCharacteristicMetadataFormatFloat,
+ HMCharacteristicMetadataFormatUInt8,
+ HMCharacteristicMetadataFormatUInt16,
+ HMCharacteristicMetadataFormatUInt32,
+ HMCharacteristicMetadataFormatUInt64
+ ]
+ }
+
+ /**
+ Returns the localized description for a provided value, taking the characteristic's metadata and possible
+ values into account.
+
+ - parameter value: The value to look up.
+
+ - returns: A string representing the value in a localized way, e.g. `"24%"` or `"354º"`
+ */
+ func localizedDescriptionForValue(value: AnyObject) -> String {
+ if self.isWriteOnly {
+ return NSLocalizedString("Write-Only Characteristic", comment: "Write-Only Characteristic")
+ }
+ else if self.isBoolean {
+ if let boolValue = value.boolValue {
+ return boolValue ? NSLocalizedString("On", comment: "On") : NSLocalizedString("Off", comment: "Off")
+ }
+ }
+ if let number = value as? Int {
+ if let predeterminedValueString = self.predeterminedValueDescriptionForNumber(number) {
+ return predeterminedValueString
+ }
+
+ if let stepValue = self.metadata?.stepValue {
+ Constants.valueFormatter.minimumFractionDigits = Int(log10(1.0 / stepValue.doubleValue))
+ if let string = Constants.valueFormatter.stringFromNumber(number) {
+ return string + self.localizedUnitDecoration
+ }
+ }
+ }
+ return "\(value)"
+ }
+
+ /**
+ - parameter number: The value of this characteristic.
+
+ - returns: An optional, localized string for the value.
+ */
+ func predeterminedValueDescriptionForNumber(number: Int) -> String? {
+ switch self.characteristicType {
+ case HMCharacteristicTypePowerState, HMCharacteristicTypeInputEvent, HMCharacteristicTypeOutputState:
+ if Bool(number) {
+ return NSLocalizedString("On", comment: "On")
+ }
+ else {
+ return NSLocalizedString("Off", comment: "Off")
+ }
+
+ case HMCharacteristicTypeOutletInUse, HMCharacteristicTypeMotionDetected, HMCharacteristicTypeAdminOnlyAccess, HMCharacteristicTypeAudioFeedback, HMCharacteristicTypeObstructionDetected:
+ if Bool(number) {
+ return NSLocalizedString("Yes", comment: "Yes")
+ }
+ else {
+ return NSLocalizedString("No", comment: "No")
+ }
+
+ case HMCharacteristicTypeTargetDoorState, HMCharacteristicTypeCurrentDoorState:
+ if let doorState = HMCharacteristicValueDoorState(rawValue: number) {
+ switch doorState {
+ case .Open:
+ return NSLocalizedString("Open", comment: "Open")
+
+ case .Opening:
+ return NSLocalizedString("Opening", comment: "Opening")
+
+ case .Closed:
+ return NSLocalizedString("Closed", comment: "Closed")
+
+ case .Closing:
+ return NSLocalizedString("Closing", comment: "Closing")
+
+ case .Stopped:
+ return NSLocalizedString("Stopped", comment: "Stopped")
+ }
+ }
+
+ case HMCharacteristicTypeTargetHeatingCooling:
+ if let mode = HMCharacteristicValueHeatingCooling(rawValue: number) {
+ switch mode {
+ case .Off:
+ return NSLocalizedString("Off", comment: "Off")
+
+ case .Heat:
+ return NSLocalizedString("Heat", comment: "Heat")
+
+ case .Cool:
+ return NSLocalizedString("Cool", comment: "Cool")
+
+ case .Auto:
+ return NSLocalizedString("Auto", comment: "Auto")
+ }
+ }
+
+ case HMCharacteristicTypeCurrentHeatingCooling:
+ if let mode = HMCharacteristicValueHeatingCooling(rawValue: number) {
+ switch mode {
+ case .Off:
+ return NSLocalizedString("Off", comment: "Off")
+
+ case .Heat:
+ return NSLocalizedString("Heating", comment: "Heating")
+
+ case .Cool:
+ return NSLocalizedString("Cooling", comment: "Cooling")
+
+ case .Auto:
+ return NSLocalizedString("Auto", comment: "Auto")
+ }
+ }
+
+ case HMCharacteristicTypeTargetLockMechanismState, HMCharacteristicTypeCurrentLockMechanismState:
+ if let lockState = HMCharacteristicValueLockMechanismState(rawValue: number) {
+ switch lockState {
+ case .Unsecured:
+ return NSLocalizedString("Unsecured", comment: "Unsecured")
+
+ case .Secured:
+ return NSLocalizedString("Secured", comment: "Secured")
+
+ case .Unknown:
+ return NSLocalizedString("Unknown", comment: "Unknown")
+
+ case .Jammed:
+ return NSLocalizedString("Jammed", comment: "Jammed")
+ }
+ }
+
+ case HMCharacteristicTypeTemperatureUnits:
+ if let unit = HMCharacteristicValueTemperatureUnit(rawValue: number) {
+ switch unit {
+ case .Celsius:
+ return NSLocalizedString("Celsius", comment: "Celsius")
+
+ case .Fahrenheit:
+ return NSLocalizedString("Fahrenheit", comment: "Fahrenheit")
+ }
+ }
+
+ case HMCharacteristicTypeLockMechanismLastKnownAction:
+ if let lastKnownAction = HMCharacteristicValueLockMechanismLastKnownAction(rawValue: number) {
+ switch lastKnownAction {
+ case .SecuredUsingPhysicalMovementInterior:
+ return NSLocalizedString("Interior Secured", comment: "Interior Secured")
+
+ case .UnsecuredUsingPhysicalMovementInterior:
+ return NSLocalizedString("Exterior Unsecured", comment: "Exterior Unsecured")
+
+ case .SecuredUsingPhysicalMovementExterior:
+ return NSLocalizedString("Exterior Secured", comment: "Exterior Secured")
+
+ case .UnsecuredUsingPhysicalMovementExterior:
+ return NSLocalizedString("Exterior Unsecured", comment: "Exterior Unsecured")
+
+ case .SecuredWithKeypad:
+ return NSLocalizedString("Keypad Secured", comment: "Keypad Secured")
+
+ case .UnsecuredWithKeypad:
+ return NSLocalizedString("Keypad Unsecured", comment: "Keypad Unsecured")
+
+ case .SecuredRemotely:
+ return NSLocalizedString("Secured Remotely", comment: "Secured Remotely")
+
+ case .UnsecuredRemotely:
+ return NSLocalizedString("Unsecured Remotely", comment: "Unsecured Remotely")
+
+ case .SecuredWithAutomaticSecureTimeout:
+ return NSLocalizedString("Secured Automatically", comment: "Secured Automatically")
+
+ case .SecuredUsingPhysicalMovement:
+ return NSLocalizedString("Secured Using Physical Movement", comment: "Secured Using Physical Movement")
+
+ case .UnsecuredUsingPhysicalMovement:
+ return NSLocalizedString("Unsecured Using Physical Movement", comment: "Unsecured Using Physical Movement")
+ }
+ }
+
+ case HMCharacteristicTypeRotationDirection:
+ if let rotationDirection = HMCharacteristicValueRotationDirection(rawValue: number) {
+ switch rotationDirection {
+ case .Clockwise:
+ return NSLocalizedString("Clockwise", comment: "Clockwise")
+
+ case .CounterClockwise:
+ return NSLocalizedString("Counter Clockwise", comment: "Counter Clockwise")
+ }
+ }
+
+ case HMCharacteristicTypeAirParticulateSize:
+ if let size = HMCharacteristicValueAirParticulateSize(rawValue: number) {
+ switch size {
+ case .Size10:
+ return NSLocalizedString("Size 10", comment: "Size 10")
+
+ case .Size2_5:
+ return NSLocalizedString("Size 2.5", comment: "Size 2.5")
+ }
+ }
+
+ case HMCharacteristicTypePositionState:
+ if let state = HMCharacteristicValuePositionState(rawValue: number) {
+ switch state {
+ case .Opening:
+ return NSLocalizedString("Opening", comment: "Opening")
+
+ case .Closing:
+ return NSLocalizedString("Closing", comment: "Closing")
+
+ case .Stopped:
+ return NSLocalizedString("Stopped", comment: "Stopped")
+ }
+ }
+
+ case HMCharacteristicTypeCurrentSecuritySystemState:
+ if let state = HMCharacteristicValueCurrentSecuritySystemState(rawValue: number) {
+ switch state {
+ case .AwayArm:
+ return NSLocalizedString("Away", comment: "Away")
+
+ case .StayArm:
+ return NSLocalizedString("Home", comment: "Home")
+
+ case .NightArm:
+ return NSLocalizedString("Night", comment: "Night")
+
+ case .Disarmed:
+ return NSLocalizedString("Disarm", comment: "Disarm")
+
+ case .Triggered:
+ return NSLocalizedString("Triggered", comment: "Triggered")
+ }
+ }
+
+ case HMCharacteristicTypeTargetSecuritySystemState:
+ if let state = HMCharacteristicValueTargetSecuritySystemState(rawValue: number) {
+ switch state {
+ case .AwayArm:
+ return NSLocalizedString("Away", comment: "Away")
+
+ case .StayArm:
+ return NSLocalizedString("Home", comment: "Home")
+
+ case .NightArm:
+ return NSLocalizedString("Night", comment: "Night")
+
+ case .Disarm:
+ return NSLocalizedString("Disarm", comment: "Disarm")
+ }
+ }
+
+ default:
+ break
+ }
+ return nil
+ }
+
+ var supportsEventNotification: Bool {
+ return self.properties.contains(HMCharacteristicPropertySupportsEventNotification)
+ }
+
+ /// - returns: A string representing the value in a localized way, e.g. `"24%"` or `"354º"`
+ var localizedValueDescription: String {
+ if let value = value {
+ return self.localizedDescriptionForValue(value)
+ }
+ return ""
+ }
+
+ /// - returns: The decoration for the characteristic's units, localized, e.g. `"%"` or `"º"`
+ var localizedUnitDecoration: String {
+ if let units = self.metadata?.units {
+ switch units {
+ case HMCharacteristicMetadataUnitsCelsius:
+ return NSLocalizedString("℃", comment: "Degrees Celsius")
+
+ case HMCharacteristicMetadataUnitsArcDegree:
+ return NSLocalizedString("º", comment: "Arc Degrees")
+
+ case HMCharacteristicMetadataUnitsFahrenheit:
+ return NSLocalizedString("℉", comment: "Degrees Fahrenheit")
+
+ case HMCharacteristicMetadataUnitsPercentage:
+ return NSLocalizedString("%", comment: "Percentage")
+
+ default: break
+ }
+ }
+ return ""
+ }
+
+ /// - returns: The type of the characteristic, e.g. `"Current Lock Mechanism State"`
+ var localizedCharacteristicType: String {
+ var type = self.localizedDescription
+
+ var localizedDescription: NSString? = nil
+ if isReadOnly {
+ localizedDescription = NSLocalizedString("Read Only", comment: "Read Only")
+ }
+ else if isWriteOnly {
+ localizedDescription = NSLocalizedString("Write Only", comment: "Write Only")
+ }
+
+ if let localizedDescription = localizedDescription {
+ type = type + " (\(localizedDescription))"
+ }
+
+ return type
+ }
+
+ /// - returns: `true` if this characteristic has numeric values that are all integers; `false` otherwise.
+ var isInteger: Bool {
+ return self.isNumeric && !self.isFloatingPoint
+ }
+
+ /**
+ - returns: `true` if this characteristic has numeric values;
+ `false` otherwise.
+ */
+ var isNumeric: Bool {
+ guard let metadata = metadata else { return false }
+ guard let format = metadata.format else { return false }
+ return Constants.numericFormats.contains(format)
+ }
+
+ /// - returns: `true` if this characteristic is boolean; `false` otherwise.
+ var isBoolean: Bool {
+ guard let metadata = metadata else { return false }
+ return metadata.format == HMCharacteristicMetadataFormatBool
+ }
+
+ /**
+ - returns: `true` if this characteristic is text-writable;
+ `false` otherwise.
+ */
+ var isTextWritable: Bool {
+ guard let metadata = metadata else { return false }
+ return metadata.format == HMCharacteristicMetadataFormatString && properties.contains(HMCharacteristicPropertyWritable)
+ }
+
+ /**
+ - returns: `true` if this characteristic has numeric values
+ that are all floating point; `false` otherwise.
+ */
+ var isFloatingPoint: Bool {
+ guard let metadata = metadata else { return false }
+ return metadata.format == HMCharacteristicMetadataFormatFloat
+ }
+
+ /// - returns: `true` if characteristic is read only; `false` otherwise.
+ var isReadOnly: Bool {
+ return !properties.contains(HMCharacteristicPropertyWritable) &&
+ properties.contains(HMCharacteristicPropertyReadable)
+ }
+
+ /**
+ - returns: `true` if this characteristic is write only;
+ `false` otherwise.
+ */
+ var isWriteOnly: Bool {
+ return !properties.contains(HMCharacteristicPropertyReadable) &&
+ properties.contains(HMCharacteristicPropertyWritable)
+ }
+
+ /**
+ - returns: `true` if this characteristic is the 'Identify'
+ characteristic; `false` otherwise.
+ */
+ var isIdentify: Bool {
+ return self.characteristicType == HMCharacteristicTypeIdentify
+ }
+
+ /**
+ - returns: The number of possible values that this characteristic can contain.
+ The standard formula for the number of values between two numbers is
+ `((greater - lesser) + 1)`, and this takes step value into account.
+ */
+ var numberOfChoices: Int {
+ guard let metadata = metadata, minimumValue = metadata.minimumValue as? Int else { return 0 }
+ guard let maximumValue = metadata.maximumValue as? Int else { return 0 }
+ var range = maximumValue - minimumValue
+ if let stepValue = metadata.stepValue as? Double {
+ range = Int(Double(range) / stepValue)
+ }
+ return range + 1
+ }
+
+ /// - returns: All of the possible values that this characteristic can contain.
+ var allPossibleValues: [AnyObject]? {
+ guard self.isInteger else { return nil }
+ guard let metadata = metadata, stepValue = metadata.stepValue as? Double else { return nil }
+ let choices = Array(0.. [HMService] in
+ return accumulator + accessory.services.filter { return !accumulator.contains($0) }
+ })
+ }
+
+ /// All the characteristics within all of the services within the home.
+ var allCharacteristics: [HMCharacteristic] {
+ return allServices.reduce([], combine: { (accumulator, service) -> [HMCharacteristic] in
+ return accumulator + service.characteristics.filter { return !accumulator.contains($0) }
+ })
+ }
+
+ /**
+ - returns: A dictionary mapping localized service types to an array
+ of all services of that type.
+ */
+ var serviceTable: [String: [HMService]] {
+ var serviceDictionary = [String: [HMService]]()
+ for service in self.allServices {
+ if !service.isControlType {
+ continue
+ }
+
+ let serviceType = service.localizedDescription
+ var existingServices: [HMService] = serviceDictionary[serviceType] ?? [HMService]()
+ existingServices.append(service)
+ serviceDictionary[service.localizedDescription] = existingServices
+ }
+
+ for (serviceType, services) in serviceDictionary {
+ serviceDictionary[serviceType] = services.sort {
+ return $0.accessory!.name.localizedCompare($1.accessory!.name) == .OrderedAscending
+ }
+ }
+ return serviceDictionary
+ }
+
+ /// - returns: All rooms in the home, including `roomForEntireHome`.
+ var allRooms: [HMRoom] {
+ let allRooms = [self.roomForEntireHome()] + self.rooms
+ return allRooms.sortByLocalizedName()
+ }
+
+ /// - returns: `true` if the current user is the admin of this home; `false` otherwise.
+ var isAdmin: Bool {
+ return self.homeAccessControlForUser(currentUser).administrator
+ }
+
+ /// - returns: All accessories which are 'control accessories'.
+ var sortedControlAccessories: [HMAccessory] {
+ let filteredAccessories = self.accessories.filter { accessory -> Bool in
+ for service in accessory.services {
+ if service.isControlType {
+ return true
+ }
+ }
+ return false
+ }
+ return filteredAccessories.sortByLocalizedName()
+ }
+
+ /**
+ - parameter identifier: The UUID to look up.
+
+ - returns: The accessory within the receiver that matches the given UUID,
+ or nil if there is no accessory with that UUID.
+ */
+ func accessoryWithIdentifier(identifier: NSUUID) -> HMAccessory? {
+ for accessory in self.accessories {
+ if accessory.uniqueIdentifier == identifier {
+ return accessory
+ }
+ }
+ return nil
+ }
+
+ /**
+ - parameter identifiers: An array of `NSUUID`s that match accessories in the receiver.
+
+ - returns: An array of `HMAccessory` instances corresponding to
+ the UUIDs passed in.
+ */
+ func accessoriesWithIdentifiers(identifiers: [NSUUID]) -> [HMAccessory] {
+ return self.accessories.filter { accessory in
+ identifiers.contains(accessory.uniqueIdentifier)
+ }
+ }
+
+ /**
+ Searches through the home's accessories to find the accessory
+ that is bridging the provided accessory.
+
+ - parameter accessory: The bridged accessory.
+
+ - returns: The accessory bridging the bridged accessory.
+ */
+ func bridgeForAccessory(accessory: HMAccessory) -> HMAccessory? {
+ if !accessory.bridged {
+ return nil
+ }
+ for bridge in self.accessories {
+ if let identifiers = bridge.uniqueIdentifiersForBridgedAccessories where identifiers.contains(accessory.uniqueIdentifier) {
+ return bridge
+ }
+ }
+ return nil
+ }
+
+ /**
+ - parameter room: The room.
+
+ - returns: The name of the room, appending "Default Room" if the room
+ is the home's `roomForEntireHome`
+ */
+ func nameForRoom(room: HMRoom) -> String {
+ if room == self.roomForEntireHome() {
+ let defaultRoom = NSLocalizedString("Default Room", comment: "Default Room")
+ return room.name + " (\(defaultRoom))"
+ }
+ return room.name
+ }
+
+ /**
+ - parameter zone: The zone.
+ - parameter rooms: A list of rooms to add to the final list.
+
+ - returns: A list of rooms that exist in the home and have not
+ yet been added to this zone.
+ */
+ func roomsNotAlreadyInZone(zone: HMZone, includingRooms rooms: [HMRoom]? = nil) -> [HMRoom] {
+ var filteredRooms = self.rooms.filter { room in
+ return !zone.rooms.contains(room)
+ }
+ if let rooms = rooms {
+ filteredRooms += rooms
+ }
+ return filteredRooms
+ }
+
+ /**
+ - parameter home: The home.
+ - parameter serviceGroup: The service group.
+ - parameter services: A list of services to add to the final list.
+
+ - returns: A list of services that exist in the home and have not yet been added to this service group.
+ */
+ func servicesNotAlreadyInServiceGroup(serviceGroup: HMServiceGroup, includingServices services: [HMService]? = nil) -> [HMService] {
+ var filteredServices = self.allServices.filter { service in
+ /*
+ Exclude services that are already in the service group
+ and the accessory information service.
+ */
+ return !serviceGroup.services.contains(service) && service.serviceType != HMServiceTypeAccessoryInformation
+ }
+ if let services = services {
+ filteredServices += services
+ }
+ return filteredServices
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/HMService+Properties.swift b/HomeKitCatalog/HMCatalog/Supporting Files/HMService+Properties.swift
new file mode 100644
index 00000000..e97597b6
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/HMService+Properties.swift
@@ -0,0 +1,47 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `HMService+Properties` methods provide convenience methods for deconstructing `HMService` objects.
+*/
+
+import HomeKit
+
+extension HMService {
+ struct Constants {
+ static let serviceMap = [
+ HMServiceTypeLightbulb: NSLocalizedString("Lightbulb", comment: "Lightbulb"),
+ HMServiceTypeFan: NSLocalizedString("Fan", comment: "Fan")
+ ]
+ }
+
+ /**
+ - parameter serviceType: The service type.
+
+ - returns: A localized description of that service type or
+ the original `type` string if one cannot be found.
+ */
+ class func localizedDescriptionForServiceType(type: String) -> String {
+ return Constants.serviceMap[type] ?? type
+ }
+
+ /// - returns: `true` if this service supports the `associatedServiceType` property; `false` otherwise.
+ var supportsAssociatedServiceType: Bool {
+ return self.serviceType == HMServiceTypeOutlet || self.serviceType == HMServiceTypeSwitch
+ }
+
+ /// - returns: `true` if this service is a 'control type'; `false` otherwise.
+ var isControlType: Bool {
+ let noncontrolTypes = [HMServiceTypeAccessoryInformation, HMServiceTypeLockManagement]
+ return !noncontrolTypes.contains(self.serviceType)
+ }
+
+ /**
+ - returns: The valid associated service types for this service,
+ e.g. `HMServiceTypeFan` or `HMServiceTypeLightbulb`
+ */
+ class var validAssociatedServiceTypes: [String] {
+ return [HMServiceTypeFan, HMServiceTypeLightbulb]
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/Info.plist b/HomeKitCatalog/HMCatalog/Supporting Files/Info.plist
new file mode 100644
index 00000000..0357a312
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/Info.plist
@@ -0,0 +1,58 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 2.0.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ NSLocationWhenInUseUsageDescription
+ HMCatalog needs your location to search for relevant places in your area.
+ NSHomeKitUsageDescription
+ HMCatalog needs access top your HomeKit devices.
+ UILaunchStoryboardName
+ Launch Screen
+ UIMainStoryboardFile
+ Main
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UIRequiresFullscreen
+
+ UIStatusBarHidden
+
+ UIStatusBarStyle
+ UIStatusBarStyleDefault
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/NSPredicate+Condition.swift b/HomeKitCatalog/HMCatalog/Supporting Files/NSPredicate+Condition.swift
new file mode 100644
index 00000000..67c116dd
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/NSPredicate+Condition.swift
@@ -0,0 +1,162 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `NSPredicate+Condition` properties and methods are used to parse the conditions used in `HMEventTrigger`s.
+*/
+
+import HomeKit
+
+/// Represents condition type in HomeKit with associated values.
+enum HomeKitConditionType {
+ /**
+ Represents a characteristic condition.
+
+ The tuple represents the `HMCharacteristic` and its condition value.
+ For example, "Current gargage door is set to 'Open'".
+ */
+ case Characteristic(HMCharacteristic, NSCopying)
+
+ /**
+ Represents a time condition.
+
+ The tuple represents the time ordering and the sun state.
+ For example, "Before sunset".
+ */
+ case SunTime(TimeConditionOrder, TimeConditionSunState)
+
+ /**
+ Represents an exact time condition.
+
+ The tuple represents the time ordering and time.
+ For example, "At 12:00pm".
+ */
+ case ExactTime(TimeConditionOrder, NSDateComponents)
+
+ /// The predicate is not a HomeKit condition.
+ case Unknown
+}
+
+extension NSPredicate {
+
+ /**
+ Parses the predicate and attempts to generate a characteristic-value `HomeKitConditionType`.
+
+ - returns: An optional characteristic-value tuple.
+ */
+ private func characteristic() -> HomeKitConditionType? {
+ guard let predicate = self as? NSCompoundPredicate else { return nil }
+ guard let subpredicates = predicate.subpredicates as? [NSPredicate] else { return nil }
+ guard subpredicates.count == 2 else { return nil }
+
+ var characteristicPredicate: NSComparisonPredicate? = nil
+ var valuePredicate: NSComparisonPredicate? = nil
+
+ for subpredicate in subpredicates {
+ if let comparison = subpredicate as? NSComparisonPredicate where comparison.leftExpression.expressionType == .KeyPathExpressionType && comparison.rightExpression.expressionType == .ConstantValueExpressionType {
+ switch comparison.leftExpression.keyPath {
+ case HMCharacteristicKeyPath:
+ characteristicPredicate = comparison
+
+ case HMCharacteristicValueKeyPath:
+ valuePredicate = comparison
+
+ default:
+ break
+ }
+ }
+ }
+
+ if let characteristic = characteristicPredicate?.rightExpression.constantValue as? HMCharacteristic,
+ characteristicValue = valuePredicate?.rightExpression.constantValue as? NSCopying {
+ return .Characteristic(characteristic, characteristicValue)
+ }
+ return nil
+ }
+
+ /**
+ Parses the predicate and attempts to generate an order-sunstate `HomeKitConditionType`.
+
+ - returns: An optional order-sunstate tuple.
+ */
+ private func sunState() -> HomeKitConditionType? {
+ guard let comparison = self as? NSComparisonPredicate else { return nil }
+ guard comparison.leftExpression.expressionType == .KeyPathExpressionType else { return nil }
+ guard comparison.rightExpression.expressionType == .FunctionExpressionType else { return nil }
+ guard comparison.rightExpression.function == "now" else { return nil }
+ guard comparison.rightExpression.arguments?.count == 0 else { return nil }
+
+ switch (comparison.leftExpression.keyPath, comparison.predicateOperatorType) {
+ case (HMSignificantEventSunrise, .LessThanPredicateOperatorType):
+ return .SunTime(.After, .Sunrise)
+
+ case (HMSignificantEventSunrise, .LessThanOrEqualToPredicateOperatorType):
+ return .SunTime(.After, .Sunrise)
+
+ case (HMSignificantEventSunrise, .GreaterThanPredicateOperatorType):
+ return .SunTime(.Before, .Sunrise)
+
+ case (HMSignificantEventSunrise, .GreaterThanOrEqualToPredicateOperatorType):
+ return .SunTime(.Before, .Sunrise)
+
+ case (HMSignificantEventSunset, .LessThanPredicateOperatorType):
+ return .SunTime(.After, .Sunset)
+
+ case (HMSignificantEventSunset, .LessThanOrEqualToPredicateOperatorType):
+ return .SunTime(.After, .Sunset)
+
+ case (HMSignificantEventSunset, .GreaterThanPredicateOperatorType):
+ return .SunTime(.Before, .Sunset)
+
+ case (HMSignificantEventSunset, .GreaterThanOrEqualToPredicateOperatorType):
+ return .SunTime(.Before, .Sunset)
+
+ default:
+ return nil
+ }
+ }
+
+ /**
+ Parses the predicate and attempts to generate an order-exacttime `HomeKitConditionType`.
+
+ - returns: An optional order-exacttime tuple.
+ */
+ private func exactTime() -> HomeKitConditionType? {
+ guard let comparison = self as? NSComparisonPredicate else { return nil }
+ guard comparison.leftExpression.expressionType == .FunctionExpressionType else { return nil }
+ guard comparison.leftExpression.function == "now" else { return nil }
+ guard comparison.rightExpression.expressionType == .ConstantValueExpressionType else { return nil }
+ guard let dateComponents = comparison.rightExpression.constantValue as? NSDateComponents else { return nil }
+
+ switch comparison.predicateOperatorType {
+ case .LessThanPredicateOperatorType, .LessThanOrEqualToPredicateOperatorType:
+ return .ExactTime(.Before, dateComponents)
+
+ case .GreaterThanPredicateOperatorType, .GreaterThanOrEqualToPredicateOperatorType:
+ return .ExactTime(.After, dateComponents)
+
+ case .EqualToPredicateOperatorType:
+ return .ExactTime(.At, dateComponents)
+
+ default:
+ return nil
+ }
+ }
+
+ /// - returns: The 'type' of HomeKit condition, with associated value, if applicable.
+ var homeKitConditionType: HomeKitConditionType {
+ if let characteristic = characteristic() {
+ return characteristic
+ }
+ else if let sunState = sunState() {
+ return sunState
+ }
+ else if let exactTime = exactTime() {
+ return exactTime
+ }
+ else {
+ return .Unknown
+ }
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIAlertController+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIAlertController+Convenience.swift
new file mode 100644
index 00000000..e35717bd
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIAlertController+Convenience.swift
@@ -0,0 +1,61 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `UIAlertController+Convenience` methods allow for quick construction of `UIAlertController`s with common structures.
+*/
+
+import UIKit
+
+extension UIAlertController {
+
+ /**
+ A simple `UIAlertController` that prompts for a name, then runs a completion block passing in the name.
+
+ - parameter attributeType: The type of object that will be named.
+ - parameter completion: A block to call, passing in the provided text.
+ - parameter placeholder: An optional string used as text field's placeholder text.
+ - parameter shortType: An optional string used as to form the alert's action title.
+
+ - returns: A `UIAlertController` instance with a UITextField, cancel button, and add button.
+ */
+ convenience init(attributeType: String, completionHandler: (name: String) -> Void, placeholder: String? = nil, shortType: String? = nil) {
+ let title = NSLocalizedString("New", comment: "New") + " \(attributeType)"
+ let message = NSLocalizedString("Enter a name.", comment: "Enter a name.")
+ self.init(title: title, message: message, preferredStyle: .Alert)
+ self.addTextFieldWithConfigurationHandler { textField in
+ textField.placeholder = placeholder ?? attributeType
+ textField.autocapitalizationType = .Words
+ }
+ let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel) { action in
+ self.dismissViewControllerAnimated(true, completion: nil)
+ }
+ let add = NSLocalizedString("Add", comment: "Add")
+ let actionTitle = "\(add) \(shortType ?? attributeType)"
+ let addNewObject = UIAlertAction(title: actionTitle, style: .Default) { action in
+ if let name = self.textFields!.first!.text {
+ let trimmedName = name.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
+ completionHandler(name: trimmedName)
+ }
+ self.dismissViewControllerAnimated(true, completion: nil)
+ }
+ self.addAction(cancelAction)
+ self.addAction(addNewObject)
+ }
+
+ /**
+ A simple `UIAlertController` made to show an error message that's passed in.
+
+ - parameter body: The body of the alert.
+
+ - returns: A `UIAlertController` with an 'Okay' button.
+ */
+ convenience init(title: String, body: String) {
+ self.init(title: title, message: body, preferredStyle: .Alert)
+ let okayAction = UIAlertAction(title: NSLocalizedString("Okay", comment: "Okay"), style: .Default) { action in
+ self.dismissViewControllerAnimated(true, completion: nil)
+ }
+ self.addAction(okayAction)
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIColor+Custom.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIColor+Custom.swift
new file mode 100644
index 00000000..2478408f
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIColor+Custom.swift
@@ -0,0 +1,17 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `UIColor+Custom` method provides a generator method for a custom color.
+*/
+
+import UIKit
+
+extension UIColor {
+
+ /// - returns: A nice blue color which suggests editability.
+ static func editableBlueColor() -> UIColor {
+ return UIColor(red:0/255.0, green:122/255.0, blue:255/255.0, alpha:1.0)
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIStoryboardSegue+IntendedDestination.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIStoryboardSegue+IntendedDestination.swift
new file mode 100644
index 00000000..cf77e4e9
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIStoryboardSegue+IntendedDestination.swift
@@ -0,0 +1,20 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `UIStoryboardSegue+IntendedDestination` method allows for the selection of a segue's destination.
+*/
+
+import UIKit
+
+extension UIStoryboardSegue {
+
+ /// - returns: The intended `UIViewController` from the segue's destination.
+ var intendedDestinationViewController: UIViewController {
+ if let navigationController = self.destinationViewController as? UINavigationController {
+ return navigationController.topViewController!
+ }
+ return self.destinationViewController
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UITableViewController+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UITableViewController+Convenience.swift
new file mode 100644
index 00000000..23c92c6b
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/UITableViewController+Convenience.swift
@@ -0,0 +1,39 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `UITableViewController+Convenience` methods allow for the configuration of a background label.
+*/
+
+import HomeKit
+import UIKit
+
+extension UITableViewController {
+
+ /**
+ Displays or hides a label in the background of the table view.
+
+ - parameter message: The String message to display. The message is hidden
+ if `nil` is provided.
+ */
+ func setBackgroundMessage(message: String?) {
+ if let message = message {
+ // Display a message when the table is empty
+ let messageLabel = UILabel()
+
+ messageLabel.text = message
+ messageLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
+ messageLabel.textColor = UIColor.lightGrayColor()
+ messageLabel.textAlignment = .Center
+ messageLabel.sizeToFit()
+
+ tableView.backgroundView = messageLabel
+ tableView.separatorStyle = .None
+ }
+ else {
+ tableView.backgroundView = nil
+ tableView.separatorStyle = .SingleLine
+ }
+ }
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIViewController+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIViewController+Convenience.swift
new file mode 100644
index 00000000..76e8ebd4
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIViewController+Convenience.swift
@@ -0,0 +1,94 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `UIViewController+Convenience` methods allow for easy presentation of common views.
+*/
+
+import HomeKit
+import UIKit
+
+extension UIViewController {
+
+ /**
+ Displays a `UIAlertController` on the main thread with the error's `localizedDescription` at the body.
+
+ - parameter error: The error to display.
+ */
+ func displayError(error: NSError) {
+ if let errorCode = HMErrorCode(rawValue: error.code) {
+ if self.presentedViewController != nil || errorCode == .OperationCancelled || errorCode == .UserDeclinedAddingUser {
+ print(error.localizedDescription)
+ }
+ else {
+ self.displayErrorMessage(error.localizedDescription)
+ }
+ }
+ else {
+ self.displayErrorMessage(error.description)
+ }
+ }
+
+ /**
+ Displays a collection of errors, separated by newlines.
+
+ - parameter errors: An array of `NSError`s to display.
+ */
+ func displayErrors(errors: [NSError]) {
+ var messages = [String]()
+ for error in errors {
+ if let errorCode = HMErrorCode(rawValue: error.code) {
+ if self.presentedViewController != nil || errorCode == .OperationCancelled || errorCode == .UserDeclinedAddingUser {
+ print(error.localizedDescription)
+ }
+ else {
+ messages.append(error.localizedDescription)
+ }
+ }
+ else {
+ messages.append(error.description)
+ }
+ }
+
+ if messages.count > 0 {
+ // There were errors in the list, reduce the messages into a single one.
+ let collectedMessage = messages.reduce("", combine: { (accumulator, message) -> String in
+ return accumulator + "\n" + message
+ })
+ self.displayErrorMessage(collectedMessage)
+ }
+ }
+
+ /// Displays a `UIAlertController` with the passed-in text and an 'Okay' button.
+ func displayMessage(title: String, message: String) {
+ dispatch_async(dispatch_get_main_queue()) {
+ let alert = UIAlertController(title: title, body: message)
+ self.presentViewController(alert, animated: true, completion: nil)
+ }
+ }
+
+ /**
+ Displays `UIAlertController` with a message and a localized "Error" title.
+
+ - parameter message: The message to display.
+ */
+ private func displayErrorMessage(message: String) {
+ let errorTitle = NSLocalizedString("Error", comment: "Error")
+ displayMessage(errorTitle, message: message)
+ }
+
+ /**
+ Presents a simple `UIAlertController` with a textField, set up to
+ accept a name. Once the name is entered, the completion handler will
+ be called and the name will be passed in.
+
+ - parameter attributeType: The kind of object being added
+ - parameter completion: The block to run when the user taps the add button.
+ */
+ func presentAddAlertWithAttributeType(type: String, placeholder: String? = nil, shortType: String? = nil, completion: (String) -> Void) {
+ let alertController = UIAlertController(attributeType: type, completionHandler: completion, placeholder: placeholder, shortType: shortType)
+ self.presentViewController(alertController, animated: true, completion: nil)
+ }
+
+}
\ No newline at end of file
diff --git a/HomeKitCatalog/HMCatalog/TabBarController.swift b/HomeKitCatalog/HMCatalog/TabBarController.swift
new file mode 100644
index 00000000..fd323fe5
--- /dev/null
+++ b/HomeKitCatalog/HMCatalog/TabBarController.swift
@@ -0,0 +1,43 @@
+/*
+ Copyright (C) 2016 Apple Inc. All Rights Reserved.
+ See LICENSE.txt for this sample’s licensing information
+
+ Abstract:
+ The `TabBarController` maintains the state of the tabs across app launches.
+*/
+import UIKit
+
+/**
+ Saves the current state of the tab so that the app will always open to the
+ appropriate tab on launch.
+*/
+class TabBarController: UITabBarController {
+ // MARK: Types
+
+ static let startingTabIndexKey = "TabBarController-StartingTabIndexKey"
+
+ // MARK: View Methods
+
+ // Load the current tab from `NSUserDefaults`.
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ let userDefaults = NSUserDefaults.standardUserDefaults()
+
+ let startingIndex = userDefaults.objectForKey(TabBarController.startingTabIndexKey) as? Int ?? 2
+
+ selectedIndex = startingIndex
+ }
+
+ // MARK: Tab Bar Methods
+
+ /// Save the current selected tab into defaults.
+ override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
+ if let tabBarItems = tabBar.items, index = tabBarItems.indexOf(item) {
+
+ let userDefaults = NSUserDefaults.standardUserDefaults()
+
+ userDefaults.setObject(index, forKey: TabBarController.startingTabIndexKey)
+ }
+ }
+}
diff --git a/HomeKitCatalog/LICENSE.txt b/HomeKitCatalog/LICENSE.txt
new file mode 100644
index 00000000..7ab93e7f
--- /dev/null
+++ b/HomeKitCatalog/LICENSE.txt
@@ -0,0 +1,42 @@
+Sample code project: HomeKit Catalog: Creating Homes, Pairing and Controlling Accessories, and Setting Up Triggers
+Version: 2.2
+
+IMPORTANT: This Apple software is supplied to you by Apple
+Inc. ("Apple") in consideration of your agreement to the following
+terms, and your use, installation, modification or redistribution of
+this Apple software constitutes acceptance of these terms. If you do
+not agree with these terms, please do not use, install, modify or
+redistribute this Apple software.
+
+In consideration of your agreement to abide by the following terms, and
+subject to these terms, Apple grants you a personal, non-exclusive
+license, under Apple's copyrights in this original Apple software (the
+"Apple Software"), to use, reproduce, modify and redistribute the Apple
+Software, with or without modifications, in source and/or binary forms;
+provided that if you redistribute the Apple Software in its entirety and
+without modifications, you must retain this notice and the following
+text and disclaimers in all such redistributions of the Apple Software.
+Neither the name, trademarks, service marks or logos of Apple Inc. may
+be used to endorse or promote products derived from the Apple Software
+without specific prior written permission from Apple. Except as
+expressly stated in this notice, no other rights or licenses, express or
+implied, are granted by Apple herein, including but not limited to any
+patent rights that may be infringed by your derivative works or by other
+works in which the Apple Software may be incorporated.
+
+The Apple Software is provided by Apple on an "AS IS" basis. APPLE
+MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
+THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
+OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
+
+IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
+MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
+AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
+STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+Copyright (C) 2016 Apple Inc. All Rights Reserved.
diff --git a/HomeKitCatalog/README.md b/HomeKitCatalog/README.md
new file mode 100644
index 00000000..d567efa4
--- /dev/null
+++ b/HomeKitCatalog/README.md
@@ -0,0 +1,50 @@
+# HomeKit Catalog
+
+HomeKit Catalog demonstrates how to use the HomeKit API, to create homes, to associate accessories with homes, to associate accessories with homes, to group the accessories into rooms and zones, to create actions sets to tie together multiple actions, to create timer triggers to fire actions sets at specific times, and to create service groups to group services into contexts.
+
+HomeKit Catalog requires Xcode 8 with the iOS 9.0 SDK to build the application. You can either run the sample code within the iOS Simulator or on a device with iOS 9.0 installed. You can use the HomeKit Accessory Simulator running under OS X to simulate accessories on your local Wi-Fi network. The HomeKit Accessory Simulator is available from the Apple Developer site as part of the Hardware IO Tools disk image.
+
+
+## Using the Sample
+
+To use the sample, you should have HomeKit accessories already associated with the current WiFi LAN with which your device is attached. Alternatively, you can use the HomeKit Accessory Simulator running on you OS X System, to simulate the presence of a variety of HomeKit Accessories. When you launch the app, switch to the Configure tab to add new homes.
+
+You may then select a home and perform the following actions:
+
+1. Define the names of the rooms (Bedroom, Living Room, etc) in the home, define zones as a collection of rooms in the home (first floor),
+2. Define Action Sets (turn off Kitchen lights),
+3. Define Triggers (turn off lights at 10PM),
+4. Define Service Groups (subset of accessories in a room), and
+5. Define other users who can control the accessories in your home.
+
+Note: For information on using the HomeKit Accessory Simulator, please refer to the HomeKit Accessory Simulator Help under the Help menu.
+
+Use the Configure tab to set up the home, associate accessories with each room, and to perform the actions described above. Use the Control button to control the accessories in the home.
+
+## Considerations
+
+HomeKit operates asynchronously. Frequently, you will have to defer some UI response until all operations associated with a particular action are
+finished. For example, when this sample wants to save a trigger, it must:
+
+1. Create a new trigger object
+2. Add the trigger to the home
+3. Add all of the specified Action Sets individually
+4. Update its name
+5. Enable it
+
+This sample makes heavy use of `dispatch_group`s to ensure all actions are completed before confirming with UI.
+
+This sample also includes many convenience functions implemented as categories on HomeKit classes, and provides a very basic, flexible UI that adapts based
+on HMCharacteristic metadata.
+
+## Requirements
+
+### Build
+
+Xcode 8.0 or later; iOS 9.0 SDK or later
+
+### Runtime
+
+iOS 9.0 or later.
+
+Copyright (C) 2016 Apple Inc. All rights reserved.