HomeKitCatalog: Version 2.2, 2016-09-13

Update to Swift 2.3

HomeKit Catalog demonstrates how to use the HomeKit framework. Use this project as a reference for interacting with objects and performing common tasks such as creating homes, pairing with and controlling accessories, and setting up triggers to automate actions.
This commit is contained in:
Liu Lantao 2016-12-24 12:14:52 +08:00
parent 7e393bcabb
commit 5280d30f1e
117 changed files with 14389 additions and 0 deletions

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.homekit</key>
<true/>
<key>com.apple.external-accessory.wireless-configuration</key>
<true/>
</dict>
</plist>

View File

@ -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 = "<group>"; };
6A10AEE81AF84BEA000A2CD6 /* HMCatalog.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = HMCatalog.entitlements; sourceTree = "<group>"; };
6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HMCatalogViewController.swift; sourceTree = "<group>"; };
6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SegmentedTimeCell.swift; sourceTree = "<group>"; tabWidth = 4; };
6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FavoritesViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TriggerCreator.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimerTriggerCreator.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = LocationTriggerCreator.swift; sourceTree = "<group>"; tabWidth = 4; };
6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = EventTriggerViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimeConditionViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = TimePickerCell.swift; sourceTree = "<group>"; tabWidth = 4; };
6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = EventTriggerCreator.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "NSPredicate+Condition.swift"; sourceTree = "<group>"; tabWidth = 4; };
6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HMActionSet+BuiltIn.swift"; sourceTree = "<group>"; };
6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicSelectionViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicTriggerCreator.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6A96D1201B051326004DD072 /* UIColor+Custom.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Custom.swift"; sourceTree = "<group>"; tabWidth = 4; };
6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FavoritesManager.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Convenience.swift"; sourceTree = "<group>"; };
6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LocationTriggerViewController.swift; sourceTree = "<group>"; 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 = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicTriggerViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MapOverlayView.swift; sourceTree = "<group>"; tabWidth = 4; };
6ACBCF591B02DAA000851BD3 /* MapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = "<group>"; tabWidth = 4; };
6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimerTriggerViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TriggerViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "HMEventTrigger+Convenience.swift"; sourceTree = "<group>"; tabWidth = 4; };
6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ConditionCell.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6AD5641B1AFA792A00321F78 /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = "<group>"; };
6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeKitObjectCollection.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextCharacteristicCell.xib; sourceTree = "<group>"; };
6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCharacteristicCell.swift; sourceTree = "<group>"; };
6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CNMutablePostalAddress+Convenience.swift"; sourceTree = "<group>"; };
DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ServiceGroupViewController.swift; sourceTree = "<group>"; tabWidth = 4; };
DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AddServicesViewController.swift; sourceTree = "<group>"; 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 = "<group>"; 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 = "<group>"; 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 = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC1310F81A1438CB004E5DB5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Convenience.swift"; sourceTree = "<group>"; tabWidth = 4; };
DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "UIViewController+Convenience.swift"; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSetViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSetCreator.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC53509E1A1D71F2000A8F0E /* ActionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionCell.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ServicesViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicsViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CharacteristicCell.xib; sourceTree = "<group>"; };
DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SegmentedControlCharacteristicCell.xib; sourceTree = "<group>"; };
DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderCharacteristicCell.xib; sourceTree = "<group>"; };
DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchCharacteristicCell.xib; sourceTree = "<group>"; };
DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicCell.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlCharacteristicCell.swift; sourceTree = "<group>"; tabWidth = 4; };
DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = SliderCharacteristicCell.swift; sourceTree = "<group>"; tabWidth = 3; };
DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SwitchCharacteristicCell.swift; sourceTree = "<group>"; tabWidth = 4; };
DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicsTableViewDataSource.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccessoryUpdateController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCD480601A16B31C001BFEE3 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = "<group>"; };
DCD480621A16B371001BFEE3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeListViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCD480661A16B4C3001BFEE3 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = "<group>"; tabWidth = 4; };
DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMHome+Properties.swift"; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ControlsViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ControlsTableViewDataSource.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCD480731A16C948001BFEE3 /* ServiceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ServiceCell.swift; sourceTree = "<group>"; tabWidth = 4; };
DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeListConfigurationViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboardSegue+IntendedDestination.swift"; sourceTree = "<group>"; tabWidth = 4; };
DCF046C31A1A939F002DBFBF /* RoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = RoomViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ZoneViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AddRoomViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccessoryBrowserViewController.swift; sourceTree = "<group>"; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ModifyAccessoryViewController.swift; sourceTree = "<group>"; 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 = "<group>";
};
6A5D852D1B0D2134008DF524 /* Event */ = {
isa = PBXGroup;
children = (
6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */,
6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */,
6A5D85301B0D3017008DF524 /* Conditions */,
6A8347511B0573C60055198E /* Characteristic */,
6A8347501B0573A00055198E /* Location */,
);
path = Event;
sourceTree = "<group>";
};
6A5D85301B0D3017008DF524 /* Conditions */ = {
isa = PBXGroup;
children = (
6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */,
6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */,
6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */,
6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */,
);
path = Conditions;
sourceTree = "<group>";
};
6A8347501B0573A00055198E /* Location */ = {
isa = PBXGroup;
children = (
6ACBCF631B02DB2400851BD3 /* Mapping */,
6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */,
6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */,
);
path = Location;
sourceTree = "<group>";
};
6A8347511B0573C60055198E /* Characteristic */ = {
isa = PBXGroup;
children = (
6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */,
6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */,
6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */,
);
path = Characteristic;
sourceTree = "<group>";
};
6ACBCF5B1B02DAA000851BD3 /* Timer */ = {
isa = PBXGroup;
children = (
6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */,
6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */,
);
path = Timer;
sourceTree = "<group>";
};
6ACBCF631B02DB2400851BD3 /* Mapping */ = {
isa = PBXGroup;
children = (
6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */,
6ACBCF591B02DAA000851BD3 /* MapViewController.swift */,
);
path = Mapping;
sourceTree = "<group>";
};
DC1310B51A14201E004E5DB5 = {
isa = PBXGroup;
children = (
3E7CACDF1B1E4AAB00891CE0 /* README.md */,
6A10AEE81AF84BEA000A2CD6 /* HMCatalog.entitlements */,
DC1310ED1A1438CB004E5DB5 /* HMCatalog */,
DC1310BF1A14201E004E5DB5 /* Products */,
);
sourceTree = "<group>";
};
DC1310BF1A14201E004E5DB5 /* Products */ = {
isa = PBXGroup;
children = (
DC1310BE1A14201E004E5DB5 /* HMCatalog.app */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
DCF046CA1A1AB0B9002DBFBF /* Rooms */ = {
isa = PBXGroup;
children = (
DCF046C31A1A939F002DBFBF /* RoomViewController.swift */,
);
path = Rooms;
sourceTree = "<group>";
};
DCF046CB1A1AB0C1002DBFBF /* Zones */ = {
isa = PBXGroup;
children = (
DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */,
DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */,
);
path = Zones;
sourceTree = "<group>";
};
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 = "<group>";
};
DCF046CD1A1AB0CB002DBFBF /* Services */ = {
isa = PBXGroup;
children = (
DCD480731A16C948001BFEE3 /* ServiceCell.swift */,
DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */,
DCF046CE1A1AB0D2002DBFBF /* Characteristic Cells */,
DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */,
DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */,
);
path = Services;
sourceTree = "<group>";
};
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 = "<group>";
};
DCF046CF1A1AB0D9002DBFBF /* Service Groups */ = {
isa = PBXGroup;
children = (
DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */,
DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */,
);
path = "Service Groups";
sourceTree = "<group>";
};
DCF046D01A1AB0E5002DBFBF /* Action Sets */ = {
isa = PBXGroup;
children = (
DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */,
DC53509E1A1D71F2000A8F0E /* ActionCell.swift */,
DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */,
);
path = "Action Sets";
sourceTree = "<group>";
};
DCF046D11A1AB0EC002DBFBF /* Triggers */ = {
isa = PBXGroup;
children = (
6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */,
6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */,
6A5D852D1B0D2134008DF524 /* Event */,
6ACBCF5B1B02DAA000851BD3 /* Timer */,
);
path = Triggers;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,17 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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?
}

View File

@ -0,0 +1,300 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}
}

View File

@ -0,0 +1,256 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,73 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,307 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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<EAWiFiUnconfiguredAccessory>) {
reloadTable()
}
func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didRemoveUnconfiguredAccessories accessories: Set<EAWiFiUnconfiguredAccessory>) {
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)
}
}
}

View File

@ -0,0 +1,110 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,109 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,119 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,388 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,182 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}
}

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="CharacteristicCell" id="745-pU-8vE" customClass="CharacteristicCell" customModule="HMCatalog" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="55"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="745-pU-8vE" id="5CE-gw-hhr">
<rect key="frame" x="0.0" y="0.0" width="320" height="54"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="Type" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cWs-3B-sD0">
<rect key="frame" x="44" y="8" width="268" height="15"/>
<constraints>
<constraint firstAttribute="height" constant="15" id="Xkx-eT-s8M"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hFJ-dz-Aug">
<rect key="frame" x="0.0" y="5" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="FBW-dh-qoz"/>
<constraint firstAttribute="width" constant="44" id="XOF-UV-AsJ"/>
</constraints>
<state key="normal" image="StarNotFavorite"/>
<state key="selected" image="StarFavorite"/>
<connections>
<action selector="didTapFavoriteButton:" destination="745-pU-8vE" eventType="touchUpInside" id="znj-bK-Qg1"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="248" verticalCompressionResistancePriority="749" text="No Value" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jo4-VR-HYz">
<rect key="frame" x="44" y="28" width="263" height="18"/>
<constraints>
<constraint firstAttribute="height" constant="18" id="MlC-Er-rwE"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
<variation key="default">
<mask key="constraints">
<exclude reference="MlC-Er-rwE"/>
</mask>
</variation>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottomMargin" secondItem="Jo4-VR-HYz" secondAttribute="bottom" id="2Z5-Zz-CIe"/>
<constraint firstItem="Jo4-VR-HYz" firstAttribute="top" secondItem="cWs-3B-sD0" secondAttribute="bottom" constant="5" id="Sx3-Ku-zct"/>
<constraint firstItem="hFJ-dz-Aug" firstAttribute="leading" secondItem="5CE-gw-hhr" secondAttribute="leading" id="X6E-63-coo"/>
<constraint firstItem="cWs-3B-sD0" firstAttribute="leading" secondItem="Jo4-VR-HYz" secondAttribute="leading" id="YW4-vv-lHV"/>
<constraint firstItem="hFJ-dz-Aug" firstAttribute="centerY" secondItem="5CE-gw-hhr" secondAttribute="centerY" id="bye-5q-iZU"/>
<constraint firstAttribute="trailingMargin" secondItem="cWs-3B-sD0" secondAttribute="trailing" id="jYO-WZ-gCf"/>
<constraint firstAttribute="trailingMargin" secondItem="Jo4-VR-HYz" secondAttribute="trailing" constant="5" id="lDB-rD-tfp"/>
<constraint firstItem="Jo4-VR-HYz" firstAttribute="leading" secondItem="hFJ-dz-Aug" secondAttribute="trailing" id="ujc-AD-UpY"/>
<constraint firstItem="cWs-3B-sD0" firstAttribute="top" secondItem="5CE-gw-hhr" secondAttribute="topMargin" id="vli-jJ-1zJ"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="favoriteButton" destination="hFJ-dz-Aug" id="fa3-1y-zEg"/>
<outlet property="favoriteButtonHeightContraint" destination="FBW-dh-qoz" id="hx7-uk-TjA"/>
<outlet property="favoriteButtonWidthConstraint" destination="XOF-UV-AsJ" id="LCR-KZ-8he"/>
<outlet property="typeLabel" destination="cWs-3B-sD0" id="jdp-9p-eWS"/>
<outlet property="valueLabel" destination="Jo4-VR-HYz" id="0Mt-5E-6gS"/>
</connections>
<point key="canvasLocation" x="665" y="547.5"/>
</tableViewCell>
</objects>
<resources>
<image name="StarFavorite" width="25" height="25"/>
<image name="StarNotFavorite" width="25" height="25"/>
</resources>
</document>

View File

@ -0,0 +1,86 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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..<possibleValues.count {
let value: AnyObject = possibleValues[index]
let title = characteristic.localizedDescriptionForValue(value)
segmentedControl.insertSegmentWithTitle(title, atIndex: index, animated: false)
}
resetSelectedIndex()
}
}
/**
Responds to the segmented control's segment changing.
Sets the value and notifies its delegate.
- parameter sender: The segmented control that changed.
*/
func segmentedControlDidChange(sender: UISegmentedControl) {
let value = possibleValues[sender.selectedSegmentIndex]
setValue(value, notify: true)
}
/**
If notify is false, then this is an external change,
so update the selected segment on the segmented control.
- parameter newValue: The new value.
- parameter notify: Whether or not to notify the delegate.
*/
override func setValue(newValue: AnyObject?, notify: Bool) {
super.setValue(newValue, notify: notify)
if !notify {
resetSelectedIndex()
}
}
/// Sets the segmented control based on the set value.
func resetSelectedIndex() {
if let intValue = value as? Int, index = possibleValues.indexOf(intValue) {
segmentedControl.selectedSegmentIndex = index
}
}
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="SegmentedControlCharacteristicCell" rowHeight="64" id="oEt-UX-TSu" customClass="SegmentedControlCharacteristicCell" customModule="HMCatalog" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="64"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="oEt-UX-TSu" id="ADi-W2-7Ww">
<rect key="frame" x="0.0" y="0.0" width="320" height="63"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalCompressionResistancePriority="749" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="ENy-PW-l7F">
<rect key="frame" x="44" y="27" width="263" height="26"/>
<segments>
<segment title="First"/>
<segment title="Second"/>
</segments>
<connections>
<action selector="segmentedControlDidChange:" destination="oEt-UX-TSu" eventType="valueChanged" id="MvQ-am-7ZX"/>
</connections>
</segmentedControl>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="WUu-DS-hHd">
<rect key="frame" x="0.0" y="10" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="l8I-3r-lq5"/>
<constraint firstAttribute="width" constant="44" id="lTL-3T-M2R"/>
</constraints>
<state key="normal" image="StarNotFavorite"/>
<state key="selected" image="StarFavorite"/>
<connections>
<action selector="didTapFavoriteButton:" destination="oEt-UX-TSu" eventType="touchUpInside" id="eqL-ek-E4v"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Type" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fm8-d3-Vgc">
<rect key="frame" x="44" y="8" width="263" height="15"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="WUu-DS-hHd" firstAttribute="leading" secondItem="ADi-W2-7Ww" secondAttribute="leading" id="3H0-dx-Gte"/>
<constraint firstAttribute="trailingMargin" secondItem="ENy-PW-l7F" secondAttribute="trailing" constant="5" id="52K-x4-L5S"/>
<constraint firstItem="fm8-d3-Vgc" firstAttribute="leading" secondItem="WUu-DS-hHd" secondAttribute="trailing" id="RD0-UO-VQI"/>
<constraint firstAttribute="bottomMargin" secondItem="ENy-PW-l7F" secondAttribute="bottom" constant="3" id="SMK-cX-3LF"/>
<constraint firstItem="fm8-d3-Vgc" firstAttribute="top" secondItem="ADi-W2-7Ww" secondAttribute="topMargin" id="Y5F-fM-g0g"/>
<constraint firstItem="WUu-DS-hHd" firstAttribute="centerY" secondItem="ADi-W2-7Ww" secondAttribute="centerY" id="ZaQ-BN-C1D"/>
<constraint firstItem="fm8-d3-Vgc" firstAttribute="trailing" secondItem="ENy-PW-l7F" secondAttribute="trailing" id="cWX-90-S8U"/>
<constraint firstItem="ENy-PW-l7F" firstAttribute="top" secondItem="fm8-d3-Vgc" secondAttribute="bottom" constant="4" id="hZc-0o-nK1"/>
<constraint firstItem="fm8-d3-Vgc" firstAttribute="leading" secondItem="ENy-PW-l7F" secondAttribute="leading" id="n3U-1W-1Ef"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="favoriteButton" destination="WUu-DS-hHd" id="qSf-Ro-wQn"/>
<outlet property="favoriteButtonHeightContraint" destination="l8I-3r-lq5" id="m8C-tO-FYD"/>
<outlet property="favoriteButtonWidthConstraint" destination="lTL-3T-M2R" id="3LW-H0-AAl"/>
<outlet property="segmentedControl" destination="ENy-PW-l7F" id="l9I-2H-mOb"/>
<outlet property="typeLabel" destination="fm8-d3-Vgc" id="ZcW-8s-GrP"/>
</connections>
<point key="canvasLocation" x="669" y="535"/>
</tableViewCell>
</objects>
<resources>
<image name="StarFavorite" width="25" height="25"/>
<image name="StarNotFavorite" width="25" height="25"/>
</resources>
</document>

View File

@ -0,0 +1,77 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" rowHeight="75" id="dlO-6Z-2O3" customClass="SliderCharacteristicCell" customModule="HMCatalog" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="75"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="dlO-6Z-2O3" id="xDz-3H-C4l">
<rect key="frame" x="0.0" y="0.0" width="320" height="74"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="zgu-bB-aWn">
<rect key="frame" x="108" y="31" width="201" height="31"/>
<connections>
<action selector="didChangeSliderValue:" destination="dlO-6Z-2O3" eventType="valueChanged" id="kin-5E-etQ"/>
</connections>
</slider>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="y5a-CT-yzT">
<rect key="frame" x="0.0" y="15" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="bsw-bd-PkT"/>
<constraint firstAttribute="width" constant="44" id="fUZ-yC-i2O"/>
</constraints>
<state key="normal" image="StarNotFavorite"/>
<state key="selected" image="StarFavorite"/>
<connections>
<action selector="didTapFavoriteButton:" destination="dlO-6Z-2O3" eventType="touchUpInside" id="Y8W-rG-8NT"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Type" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="tZM-x5-5Nk">
<rect key="frame" x="44" y="8" width="261" height="24"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="249" verticalCompressionResistancePriority="751" text="0" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RxI-7e-LD9">
<rect key="frame" x="44" y="28" width="56" height="38"/>
<constraints>
<constraint firstAttribute="width" constant="56" id="7C1-An-a8f"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="tZM-x5-5Nk" firstAttribute="leading" secondItem="y5a-CT-yzT" secondAttribute="trailing" id="7O2-UC-43s"/>
<constraint firstItem="zgu-bB-aWn" firstAttribute="leading" secondItem="RxI-7e-LD9" secondAttribute="trailing" constant="10" id="Aum-VS-J2k"/>
<constraint firstAttribute="bottomMargin" secondItem="RxI-7e-LD9" secondAttribute="bottom" id="KYC-84-yq3"/>
<constraint firstItem="RxI-7e-LD9" firstAttribute="leading" secondItem="tZM-x5-5Nk" secondAttribute="leading" id="SKK-PR-1xS"/>
<constraint firstAttribute="bottomMargin" secondItem="zgu-bB-aWn" secondAttribute="bottom" constant="5" id="W3I-l0-Zak"/>
<constraint firstItem="zgu-bB-aWn" firstAttribute="trailing" secondItem="tZM-x5-5Nk" secondAttribute="trailing" constant="2" id="ayZ-eS-zGk"/>
<constraint firstItem="tZM-x5-5Nk" firstAttribute="top" secondItem="xDz-3H-C4l" secondAttribute="topMargin" id="eeY-rO-xUx"/>
<constraint firstItem="y5a-CT-yzT" firstAttribute="leading" secondItem="xDz-3H-C4l" secondAttribute="leading" id="hL0-p6-ezx"/>
<constraint firstAttribute="trailingMargin" secondItem="zgu-bB-aWn" secondAttribute="trailing" constant="5" id="kgW-FA-ru8"/>
<constraint firstItem="RxI-7e-LD9" firstAttribute="top" secondItem="tZM-x5-5Nk" secondAttribute="bottom" constant="-4" id="moj-5S-Km2"/>
<constraint firstItem="zgu-bB-aWn" firstAttribute="centerY" secondItem="RxI-7e-LD9" secondAttribute="centerY" constant="-1" id="zPJ-gG-2BU"/>
<constraint firstItem="y5a-CT-yzT" firstAttribute="centerY" secondItem="xDz-3H-C4l" secondAttribute="centerY" id="zYW-R5-3mZ"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="favoriteButton" destination="y5a-CT-yzT" id="WXq-RR-edf"/>
<outlet property="favoriteButtonHeightContraint" destination="bsw-bd-PkT" id="Lz7-WV-tdX"/>
<outlet property="favoriteButtonWidthConstraint" destination="fUZ-yC-i2O" id="729-sE-iiV"/>
<outlet property="typeLabel" destination="tZM-x5-5Nk" id="7eu-uO-G9x"/>
<outlet property="valueLabel" destination="RxI-7e-LD9" id="1Pr-gz-tTb"/>
<outlet property="valueSlider" destination="zgu-bB-aWn" id="mc9-C6-Usa"/>
</connections>
<point key="canvasLocation" x="669" y="542.5"/>
</tableViewCell>
</objects>
<resources>
<image name="StarFavorite" width="25" height="25"/>
<image name="StarNotFavorite" width="25" height="25"/>
</resources>
</document>

View File

@ -0,0 +1,49 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="SwitchCharacteristicCell" id="KR3-99-3M0" customClass="SwitchCharacteristicCell" customModule="HMCatalog" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="55"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KR3-99-3M0" id="a75-D9-Kbw">
<rect key="frame" x="0.0" y="0.0" width="320" height="54"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Type" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l6e-Uo-CYy">
<rect key="frame" x="44" y="8" width="268" height="15"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pIm-OQ-A3u">
<rect key="frame" x="0.0" y="5" width="44" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="qIu-xk-XF0"/>
<constraint firstAttribute="width" constant="44" id="wib-Qc-Zx2"/>
</constraints>
<state key="normal" image="StarNotFavorite"/>
<state key="selected" image="StarFavorite"/>
<connections>
<action selector="didTapFavoriteButton:" destination="KR3-99-3M0" eventType="touchUpInside" id="mvJ-BW-pfa"/>
</connections>
</button>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="35n-x9-T91">
<rect key="frame" x="263" y="11" width="51" height="31"/>
<constraints>
<constraint firstAttribute="height" constant="31" id="6QX-Hw-DKe"/>
</constraints>
<connections>
<action selector="didChangeSwitchValue:" destination="KR3-99-3M0" eventType="valueChanged" id="tbC-wi-JP8"/>
</connections>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="0" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GHz-DE-s0i">
<rect key="frame" x="44" y="26" width="214" height="20"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="pIm-OQ-A3u" firstAttribute="leading" secondItem="a75-D9-Kbw" secondAttribute="leading" id="2ZC-Cd-QvT"/>
<constraint firstAttribute="bottomMargin" secondItem="GHz-DE-s0i" secondAttribute="bottom" id="AcQ-cW-er2"/>
<constraint firstItem="GHz-DE-s0i" firstAttribute="leading" secondItem="l6e-Uo-CYy" secondAttribute="leading" id="KW9-E3-ewx"/>
<constraint firstItem="pIm-OQ-A3u" firstAttribute="centerY" secondItem="a75-D9-Kbw" secondAttribute="centerY" id="PtN-N2-geS"/>
<constraint firstAttribute="trailingMargin" secondItem="35n-x9-T91" secondAttribute="trailing" id="U3Q-ts-bJz"/>
<constraint firstItem="35n-x9-T91" firstAttribute="trailing" secondItem="l6e-Uo-CYy" secondAttribute="trailing" id="Xsl-Aa-eB5"/>
<constraint firstItem="GHz-DE-s0i" firstAttribute="leading" secondItem="pIm-OQ-A3u" secondAttribute="trailing" id="aBj-bI-hc6"/>
<constraint firstItem="35n-x9-T91" firstAttribute="leading" secondItem="GHz-DE-s0i" secondAttribute="trailing" constant="5" id="npw-fC-Lrm"/>
<constraint firstItem="l6e-Uo-CYy" firstAttribute="top" secondItem="a75-D9-Kbw" secondAttribute="topMargin" id="qnL-uw-ga0"/>
<constraint firstAttribute="centerY" secondItem="35n-x9-T91" secondAttribute="centerY" id="rpL-M7-LYK"/>
<constraint firstItem="GHz-DE-s0i" firstAttribute="top" secondItem="l6e-Uo-CYy" secondAttribute="bottom" constant="3" id="rud-Nm-Rq7"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="favoriteButton" destination="pIm-OQ-A3u" id="GKI-ei-bbc"/>
<outlet property="favoriteButtonHeightContraint" destination="qIu-xk-XF0" id="Lda-oo-HPi"/>
<outlet property="favoriteButtonWidthConstraint" destination="wib-Qc-Zx2" id="pty-Ks-naA"/>
<outlet property="typeLabel" destination="l6e-Uo-CYy" id="TsG-l5-SUb"/>
<outlet property="valueLabel" destination="GHz-DE-s0i" id="T1Y-xF-PCu"/>
<outlet property="valueSwitch" destination="35n-x9-T91" id="UrO-FD-QUu"/>
</connections>
<point key="canvasLocation" x="669" y="497.5"/>
</tableViewCell>
</objects>
<resources>
<image name="StarFavorite" width="25" height="25"/>
<image name="StarNotFavorite" width="25" height="25"/>
</resources>
</document>

View File

@ -0,0 +1,48 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178f" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="TextCharacteristicCell" id="Rs6-mT-GPl" customClass="TextCharacteristicCell" customModule="HMCatalog" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="55"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Rs6-mT-GPl" id="qJb-Bt-pXg">
<rect key="frame" x="0.0" y="0.0" width="320" height="54"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Type" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HjB-gL-HbI">
<rect key="frame" x="44" y="8" width="232" height="15"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="gIl-q7-Tnk">
<rect key="frame" x="44" y="24" width="232" height="22"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="18" id="oQp-n7-Md2"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardType="emailAddress" returnKeyType="go" enablesReturnKeyAutomatically="YES"/>
<connections>
<outlet property="delegate" destination="Rs6-mT-GPl" id="di3-CD-Ib7"/>
</connections>
</textField>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="n4W-Tu-WXx">
<rect key="frame" x="0.0" y="5" width="44" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="44" id="1G2-da-Kf0"/>
<constraint firstAttribute="height" constant="44" id="irh-ek-8Th"/>
</constraints>
<state key="normal" image="StarNotFavorite"/>
<state key="selected" image="StarFavorite"/>
<connections>
<action selector="didTapFavoriteButton:" destination="Rs6-mT-GPl" eventType="touchUpInside" id="Ret-zF-aAn"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottomMargin" secondItem="gIl-q7-Tnk" secondAttribute="bottom" id="Dkm-ns-PR0"/>
<constraint firstItem="HjB-gL-HbI" firstAttribute="top" secondItem="qJb-Bt-pXg" secondAttribute="topMargin" id="F3Y-An-AWN"/>
<constraint firstItem="gIl-q7-Tnk" firstAttribute="top" secondItem="HjB-gL-HbI" secondAttribute="bottom" constant="1" id="MnX-CQ-6v0"/>
<constraint firstItem="n4W-Tu-WXx" firstAttribute="centerY" secondItem="qJb-Bt-pXg" secondAttribute="centerY" id="OKZ-hU-gpr"/>
<constraint firstItem="gIl-q7-Tnk" firstAttribute="leading" secondItem="HjB-gL-HbI" secondAttribute="leading" id="P9v-QO-nSg"/>
<constraint firstItem="gIl-q7-Tnk" firstAttribute="centerX" secondItem="qJb-Bt-pXg" secondAttribute="centerX" id="beU-vQ-Qhd"/>
<constraint firstItem="n4W-Tu-WXx" firstAttribute="leading" secondItem="qJb-Bt-pXg" secondAttribute="leading" id="qm6-QR-kzz"/>
<constraint firstItem="gIl-q7-Tnk" firstAttribute="leading" secondItem="n4W-Tu-WXx" secondAttribute="trailing" id="stN-XH-s0C"/>
<constraint firstItem="gIl-q7-Tnk" firstAttribute="trailing" secondItem="HjB-gL-HbI" secondAttribute="trailing" id="uOs-yv-r2r"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="favoriteButton" destination="n4W-Tu-WXx" id="24Y-x1-lql"/>
<outlet property="favoriteButtonHeightContraint" destination="irh-ek-8Th" id="Elv-Hk-6Dh"/>
<outlet property="favoriteButtonWidthConstraint" destination="1G2-da-Kf0" id="27J-h7-7XL"/>
<outlet property="textField" destination="gIl-q7-Tnk" id="o8b-go-TqT"/>
<outlet property="typeLabel" destination="HjB-gL-HbI" id="76N-3I-E3x"/>
</connections>
<point key="canvasLocation" x="837" y="633.5"/>
</tableViewCell>
</objects>
<resources>
<image name="StarFavorite" width="25" height="25"/>
<image name="StarNotFavorite" width="25" height="25"/>
</resources>
</document>

View File

@ -0,0 +1,207 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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.")
}
}
}

View File

@ -0,0 +1,167 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,47 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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 = ""
}
}
}
}
}

View File

@ -0,0 +1,259 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,47 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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")
}
}
}

View File

@ -0,0 +1,294 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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<HMCharacteristic>()
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<HMCharacteristicWriteAction> {
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)
}
}
}
}

View File

@ -0,0 +1,334 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,236 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,306 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,257 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,22 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}

View File

@ -0,0 +1,929 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,266 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,209 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,246 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,110 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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")
}
}

View File

@ -0,0 +1,254 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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<HMCharacteristic>()
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)
}
}
}
}

View File

@ -0,0 +1,265 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}
}

View File

@ -0,0 +1,116 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,15 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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!
}

View File

@ -0,0 +1,350 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,16 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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!
}

View File

@ -0,0 +1,140 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,250 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}
}

View File

@ -0,0 +1,94 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,248 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}
}

View File

@ -0,0 +1,44 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,227 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,153 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,195 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}
}

View File

@ -0,0 +1,161 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -0,0 +1,304 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}
}

View File

@ -0,0 +1,143 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}

View File

@ -0,0 +1,264 @@
/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Configure.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Control.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "FavoriteTabIcon.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "SelectedConfigureTabIcon.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "SelectedControlTabIcon.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "SelectedFavoriteTabIcon.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178r" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="O5X-MM-FpH">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="aMX-aC-zap">
<objects>
<viewController id="O5X-MM-FpH" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="JPw-BJ-VxA"/>
<viewControllerLayoutGuide type="bottom" id="Jpd-Wj-kTD"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="ZsZ-wB-cQS">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="calibratedRGB"/>
</view>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="x1m-3X-cxD" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1478" y="911"/>
</scene>
</scenes>
</document>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More