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.
|
@ -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>
|
|
@ -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 */;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `AppDelegate` handles higher-level app events.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A standard app delegate.
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
var window: UIWindow?
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `FavoritesManager` stores and saves characteristics that have been pinned by the user.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/// Handles interactions with `NSUserDefault`s to save the user's favorite accessories.
|
||||
class FavoritesManager {
|
||||
// MARK: Types
|
||||
|
||||
static let accessoryToCharacteristicIdentifierMappingKey = "FavoritesManager.accessoryToCharacteristicIdentifierMappingKey"
|
||||
|
||||
static let accessoryIdentifiersKey = "FavoritesManager.accessoryIdentifiersKey"
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A shared, singleton manager.
|
||||
static let sharedManager = FavoritesManager()
|
||||
|
||||
var home: HMHome? {
|
||||
return HomeStore.sharedStore.home
|
||||
}
|
||||
|
||||
/**
|
||||
An internal mapping of accessory unique identifiers to an array of their
|
||||
favorite characteristic's unique identifiers.
|
||||
*/
|
||||
private var accessoryToCharacteristicIdentifiers = [NSUUID: [NSUUID]]()
|
||||
|
||||
/// An internal array of all favorite accessory unique identifiers.
|
||||
private var accessoryIdentifiers = [NSUUID]()
|
||||
|
||||
/**
|
||||
Loads the unique identifier map and array data from `NSUserDefaults`
|
||||
into internal variables.
|
||||
*/
|
||||
init() {
|
||||
let userDefaults = NSUserDefaults.standardUserDefaults()
|
||||
|
||||
if let mapData = userDefaults.objectForKey(FavoritesManager.accessoryToCharacteristicIdentifierMappingKey) as? NSData,
|
||||
arrayData = userDefaults.objectForKey(FavoritesManager.accessoryIdentifiersKey) as? NSData {
|
||||
|
||||
accessoryToCharacteristicIdentifiers = NSKeyedUnarchiver.unarchiveObjectWithData(mapData) as? [NSUUID: [NSUUID]] ?? [:]
|
||||
|
||||
accessoryIdentifiers = NSKeyedUnarchiver.unarchiveObjectWithData(arrayData) as? [NSUUID] ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: An array of all favorite characteristics.
|
||||
The array is sorted by localized type.
|
||||
*/
|
||||
var favoriteCharacteristics: [HMCharacteristic] {
|
||||
// Find all of the favorite characteristics.
|
||||
let favoriteCharacteristics = HomeStore.sharedStore.homeManager.homes.map { home in
|
||||
return home.allCharacteristics.filter { return $0.isFavorite }
|
||||
}
|
||||
|
||||
// Need to flatten an [[HMCharacteristic]] to an [HMCharacteristic].
|
||||
return favoriteCharacteristics.reduce([], combine: +)
|
||||
.sort(characteristicOrderedBefore)
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: An array of all favorite accessories.
|
||||
The array is sorted by localized name.
|
||||
*/
|
||||
var favoriteAccessories: [HMAccessory] {
|
||||
// Find all of the favorite accessories.
|
||||
let newAccessories = accessoryIdentifiers.map { accessoryIdentifier in
|
||||
return HomeStore.sharedStore.homeManager.homes.map { home in
|
||||
return home.accessories.filter { accessory in
|
||||
return accessory.uniqueIdentifier == accessoryIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Need to flatten [[[HMAccessory]]] to [HMAccessory].
|
||||
return newAccessories.reduce([], combine: +)
|
||||
.reduce([], combine: +)
|
||||
.sortByLocalizedName()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
- returns: An array of tuples representing accessories and
|
||||
all favorite characteristics they contain.
|
||||
The array is sorted by localized type.
|
||||
*/
|
||||
var favoriteGroups:[(accessory: HMAccessory, characteristics: [HMCharacteristic])] {
|
||||
return favoriteAccessories.map { accessory in
|
||||
let favoriteCharacteristics = favoriteCharacteristicsForAccessory(accessory)
|
||||
|
||||
return (accessory: accessory, characteristics: favoriteCharacteristics)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Evaluates whether or not an `HMCharacteristic` is a favorite.
|
||||
|
||||
- parameter characteristic: The `HMCharacteristic` to evaluate.
|
||||
|
||||
- returns: A `Bool`, whether or not the characteristic is a favorite.
|
||||
*/
|
||||
func characteristicIsFavorite(characteristic: HMCharacteristic) -> Bool {
|
||||
guard let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier else {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let characteristicIdentifiers = accessoryToCharacteristicIdentifiers[accessoryIdentifier] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return characteristicIdentifiers.contains(characteristic.uniqueIdentifier)
|
||||
}
|
||||
|
||||
/**
|
||||
Favorites a characteristic.
|
||||
|
||||
- parameter characteristic: The `HMCharacteristic` to favorite.
|
||||
*/
|
||||
func favoriteCharacteristic(characteristic: HMCharacteristic) {
|
||||
if characteristicIsFavorite(characteristic) {
|
||||
return
|
||||
}
|
||||
|
||||
if let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier where accessoryToCharacteristicIdentifiers[accessoryIdentifier] != nil {
|
||||
// Accessory is already favorite, add the characteristic.
|
||||
accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.append(characteristic.uniqueIdentifier)
|
||||
save()
|
||||
}
|
||||
else if let accessory = characteristic.service?.accessory {
|
||||
// New accessory, make a new entry.
|
||||
accessoryIdentifiers.append(accessory.uniqueIdentifier)
|
||||
accessoryToCharacteristicIdentifiers[accessory.uniqueIdentifier] = [characteristic.uniqueIdentifier]
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Provides an array of favorite `HMCharacteristic`s within a given accessory.
|
||||
|
||||
- parameter accessory: The `HMAccessory` to query.
|
||||
|
||||
- returns: An array of `HMCharacteristic`s which are favorites for the provided accessory.
|
||||
*/
|
||||
func favoriteCharacteristicsForAccessory(accessory: HMAccessory) -> [HMCharacteristic] {
|
||||
let characteristics = accessory.services.map { service in
|
||||
return service.characteristics.filter { characteristic in
|
||||
return characteristic.isFavorite
|
||||
}
|
||||
}
|
||||
return characteristics.reduce([], combine: +)
|
||||
.sort(characteristicOrderedBefore)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Unfavorites a characteristic.
|
||||
|
||||
- parameter characteristic: The `HMCharacteristic` to unfavorite.
|
||||
*/
|
||||
func unfavoriteCharacteristic(characteristic: HMCharacteristic) {
|
||||
guard let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier else { return }
|
||||
|
||||
guard let characteristicIdentifiers = accessoryToCharacteristicIdentifiers[accessoryIdentifier] else { return }
|
||||
|
||||
guard let indexOfCharacteristic = characteristicIdentifiers.indexOf(characteristic.uniqueIdentifier) else { return }
|
||||
|
||||
// Remove the characteristic from the mapped collection.
|
||||
accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.removeAtIndex(indexOfCharacteristic)
|
||||
if let indexOfAccessory = accessoryIdentifiers.indexOf(accessoryIdentifier),
|
||||
isEmpty = accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.isEmpty
|
||||
where isEmpty {
|
||||
/*
|
||||
If that was the last characteristic for that accessory, remove
|
||||
the accessory from the internal array.
|
||||
*/
|
||||
accessoryIdentifiers.removeAtIndex(indexOfAccessory)
|
||||
accessoryToCharacteristicIdentifiers.removeValueForKey(accessoryIdentifier)
|
||||
}
|
||||
|
||||
save()
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
First, cleans out the internal identifier structures, then saves
|
||||
the `accessoryToCharacteristicIdentifiers` map and `accessoryIdentifiers`
|
||||
array into `NSUserDefaults`.
|
||||
|
||||
This method should be called whenever a change is made to the internal structures.
|
||||
*/
|
||||
private func save() {
|
||||
removeUnusedIdentifiers()
|
||||
|
||||
let userDefaults = NSUserDefaults.standardUserDefaults()
|
||||
|
||||
let mapData = NSKeyedArchiver.archivedDataWithRootObject(accessoryToCharacteristicIdentifiers)
|
||||
let arrayData = NSKeyedArchiver.archivedDataWithRootObject(accessoryIdentifiers)
|
||||
|
||||
userDefaults.setObject(mapData, forKey: FavoritesManager.accessoryToCharacteristicIdentifierMappingKey)
|
||||
userDefaults.setObject(arrayData, forKey: FavoritesManager.accessoryIdentifiersKey)
|
||||
}
|
||||
|
||||
/**
|
||||
Filters out any accessories or characteristic which are not longer
|
||||
valid in HomeKit.
|
||||
*/
|
||||
private func removeUnusedIdentifiers() {
|
||||
accessoryIdentifiers = accessoryIdentifiers.filter { identifier in
|
||||
return accessoryIdentifierExists(identifier)
|
||||
}
|
||||
|
||||
let filteredPairs = accessoryToCharacteristicIdentifiers.filter { accessoryId, _ in
|
||||
return accessoryIdentifierExists(accessoryId)
|
||||
}
|
||||
|
||||
accessoryToCharacteristicIdentifiers.removeAll()
|
||||
|
||||
for (accessoryId, characteristicIds) in filteredPairs {
|
||||
accessoryToCharacteristicIdentifiers[accessoryId] = characteristicIds
|
||||
}
|
||||
|
||||
for accessoryIdentifier in accessoryToCharacteristicIdentifiers.keys {
|
||||
let filteredCharacteristics = accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.filter { characteristicId in
|
||||
return characteristicIdentifierExists(characteristicId)
|
||||
}
|
||||
|
||||
accessoryToCharacteristicIdentifiers[accessoryIdentifier] = filteredCharacteristics
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if there exists an accessory in HomeKit with the given
|
||||
identifier; `false` otherwise.
|
||||
*/
|
||||
private func accessoryIdentifierExists(identifier: NSUUID) -> Bool {
|
||||
return HomeStore.sharedStore.homeManager.homes.contains { home in
|
||||
return home.accessories.contains { accessory in
|
||||
return accessory.uniqueIdentifier == identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if there exists a characteristic in HomeKit with the given
|
||||
identifier; `false` otherwise.
|
||||
*/
|
||||
private func characteristicIdentifierExists(identifier: NSUUID) -> Bool {
|
||||
return HomeStore.sharedStore.homeManager.homes.contains { home in
|
||||
return home.accessories.contains { accessory in
|
||||
return accessory.services.contains { service in
|
||||
return service.characteristics.contains { characteristic in
|
||||
return characteristic.uniqueIdentifier == identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Evaluates two `HMCharacteristic` objects to determine if the first is ordered before the second.
|
||||
|
||||
- parameter characteristic1: The first `HMCharacteristic` to evaluate.
|
||||
- parameter characteristic2: The second `HMCharacteristic` to evaluate.
|
||||
|
||||
- returns: `true` if the characteristics are localized ordered ascending, `false` otherwise.
|
||||
*/
|
||||
private func characteristicOrderedBefore(characteristic1: HMCharacteristic, characteristic2: HMCharacteristic) -> Bool {
|
||||
let type1 = characteristic1.localizedCharacteristicType
|
||||
let type2 = characteristic2.localizedCharacteristicType
|
||||
|
||||
return type1.localizedCompare(type2) == .OrderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
extension HMCharacteristic {
|
||||
/// A convenience property to favorite, unfavorite, and query the status of a characteristic.
|
||||
var isFavorite: Bool {
|
||||
get {
|
||||
return FavoritesManager.sharedManager.characteristicIsFavorite(self)
|
||||
}
|
||||
|
||||
set {
|
||||
if newValue {
|
||||
FavoritesManager.sharedManager.favoriteCharacteristic(self)
|
||||
}
|
||||
else {
|
||||
FavoritesManager.sharedManager.unfavoriteCharacteristic(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `FavoritesViewController` allows users to control pinned accessories.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
Lists favorite characteristics (grouped by accessory) and allows users to
|
||||
manipulate their values.
|
||||
*/
|
||||
class FavoritesViewController: UITableViewController, UITabBarControllerDelegate, HMAccessoryDelegate, HMHomeManagerDelegate {
|
||||
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let characteristicCell = "CharacteristicCell"
|
||||
static let segmentedControlCharacteristicCell = "SegmentedControlCharacteristicCell"
|
||||
static let switchCharacteristicCell = "SwitchCharacteristicCell"
|
||||
static let sliderCharacteristicCell = "SliderCharacteristicCell"
|
||||
static let textCharacteristicCell = "TextCharacteristicCell"
|
||||
static let serviceTypeCell = "ServiceTypeCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var favoriteAccessories = FavoritesManager.sharedManager.favoriteAccessories
|
||||
|
||||
var cellDelegate = AccessoryUpdateController()
|
||||
|
||||
@IBOutlet weak var editButton: UIBarButtonItem!
|
||||
|
||||
/// If `true`, the characteristic cells should show stars.
|
||||
var showsFavorites = false {
|
||||
didSet {
|
||||
editButton.title = showsFavorites ? NSLocalizedString("Done", comment: "Done") : NSLocalizedString("Edit", comment: "Edit")
|
||||
|
||||
reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the table view and tab bar.
|
||||
override func awakeFromNib() {
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.allowsSelectionDuringEditing = true
|
||||
|
||||
registerReuseIdentifiers()
|
||||
|
||||
tabBarController?.delegate = self
|
||||
}
|
||||
|
||||
/// Prepares HomeKit objects and reloads view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
registerAsDelegate()
|
||||
|
||||
setNotificationsEnabled(true)
|
||||
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// Disables notifications and "unregisters" as the delegate for the home manager.
|
||||
override func viewWillDisappear(animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
setNotificationsEnabled(false)
|
||||
|
||||
// We don't want any more callbacks once the view has disappeared.
|
||||
HomeStore.sharedStore.homeManager.delegate = nil
|
||||
}
|
||||
|
||||
/// Registers for all types of characteristic cells.
|
||||
private func registerReuseIdentifiers() {
|
||||
let characteristicNib = UINib(nibName: Identifiers.characteristicCell, bundle: nil)
|
||||
tableView.registerNib(characteristicNib, forCellReuseIdentifier: Identifiers.characteristicCell)
|
||||
|
||||
let sliderNib = UINib(nibName: Identifiers.sliderCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(sliderNib, forCellReuseIdentifier: Identifiers.sliderCharacteristicCell)
|
||||
|
||||
let switchNib = UINib(nibName: Identifiers.switchCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(switchNib, forCellReuseIdentifier: Identifiers.switchCharacteristicCell)
|
||||
|
||||
let segmentedNib = UINib(nibName: Identifiers.segmentedControlCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(segmentedNib, forCellReuseIdentifier: Identifiers.segmentedControlCharacteristicCell)
|
||||
|
||||
let textNib = UINib(nibName: Identifiers.textCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(textNib, forCellReuseIdentifier: Identifiers.textCharacteristicCell)
|
||||
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.serviceTypeCell)
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
Provides the number of sections based on the favorite accessories count.
|
||||
Also, add/removes the background message, if required.
|
||||
|
||||
- returns: The favorite accessories count.
|
||||
*/
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
let sectionCount = favoriteAccessories.count
|
||||
|
||||
if sectionCount == 0 {
|
||||
let message = NSLocalizedString("No Favorite Characteristics", comment: "No Favorite Characteristics")
|
||||
|
||||
setBackgroundMessage(message)
|
||||
}
|
||||
else {
|
||||
setBackgroundMessage(nil)
|
||||
}
|
||||
|
||||
return sectionCount
|
||||
}
|
||||
|
||||
/// - returns: The number of characteristics for accessory represented by the section index.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let accessory = favoriteAccessories[section]
|
||||
|
||||
let characteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(accessory)
|
||||
|
||||
return characteristics.count
|
||||
}
|
||||
|
||||
/**
|
||||
Dequeues the appropriate characteristic cell for the characteristic at the
|
||||
given index path and configures the cell based on view configurations.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let characteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(favoriteAccessories[indexPath.section])
|
||||
|
||||
let characteristic = characteristics[indexPath.row]
|
||||
|
||||
var reuseIdentifier = Identifiers.characteristicCell
|
||||
|
||||
if characteristic.isReadOnly || characteristic.isWriteOnly {
|
||||
reuseIdentifier = Identifiers.characteristicCell
|
||||
}
|
||||
else if characteristic.isBoolean {
|
||||
reuseIdentifier = Identifiers.switchCharacteristicCell
|
||||
}
|
||||
else if characteristic.hasPredeterminedValueDescriptions {
|
||||
reuseIdentifier = Identifiers.segmentedControlCharacteristicCell
|
||||
}
|
||||
else if characteristic.isNumeric {
|
||||
reuseIdentifier = Identifiers.sliderCharacteristicCell
|
||||
}
|
||||
else if characteristic.isTextWritable {
|
||||
reuseIdentifier = Identifiers.textCharacteristicCell
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CharacteristicCell
|
||||
|
||||
cell.showsFavorites = showsFavorites
|
||||
cell.delegate = cellDelegate
|
||||
cell.characteristic = characteristic
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// - returns: The name of the accessory at the specified index path.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return favoriteAccessories[section].name
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/// Toggles `showsFavorites`, which will also reload the view.
|
||||
@IBAction func didTapEdit(sender: UIBarButtonItem) {
|
||||
showsFavorites = !showsFavorites
|
||||
}
|
||||
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Resets the `favoriteAccessories` array from the `FavoritesManager`,
|
||||
resets the state of the edit button, and reloads the data.
|
||||
*/
|
||||
private func reloadData() {
|
||||
favoriteAccessories = FavoritesManager.sharedManager.favoriteAccessories
|
||||
|
||||
editButton.enabled = !favoriteAccessories.isEmpty
|
||||
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/**
|
||||
Enables or disables notifications for all favorite characteristics which
|
||||
support event notifications.
|
||||
|
||||
- parameter notificationsEnabled: A `Bool` representing enabled or disabled.
|
||||
*/
|
||||
private func setNotificationsEnabled(notificationsEnabled: Bool) {
|
||||
for characteristic in FavoritesManager.sharedManager.favoriteCharacteristics {
|
||||
if characteristic.supportsEventNotification {
|
||||
characteristic.enableNotification(notificationsEnabled) { error in
|
||||
if let error = error {
|
||||
print("HomeKit: Error enabling notification on characteristic \(characteristic): \(error.localizedDescription).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Registers as the delegate for the home manager and all
|
||||
favorite accessories.
|
||||
*/
|
||||
private func registerAsDelegate() {
|
||||
HomeStore.sharedStore.homeManager.delegate = self
|
||||
|
||||
for accessory in favoriteAccessories {
|
||||
accessory.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
/// Update the view to disable cells with unavailable accessories.
|
||||
func accessoryDidUpdateReachability(accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// Search for the cell corresponding to that characteristic and update its value.
|
||||
func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) {
|
||||
guard let accessory = characteristic.service?.accessory else { return }
|
||||
|
||||
guard let indexOfAccessory = favoriteAccessories.indexOf(accessory) else { return }
|
||||
|
||||
let favoriteCharacteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(accessory)
|
||||
|
||||
guard let indexOfCharacteristic = favoriteCharacteristics.indexOf(characteristic) else { return }
|
||||
|
||||
let indexPath = NSIndexPath(forRow: indexOfCharacteristic, inSection: indexOfAccessory)
|
||||
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath) as! CharacteristicCell
|
||||
|
||||
cell.setValue(characteristic.value, notify: false)
|
||||
}
|
||||
|
||||
// MARK: HMHomeManagerDelegate Methods
|
||||
|
||||
/// Reloads views and re-configures characteristics.
|
||||
func homeManagerDidUpdateHomes(manager: HMHomeManager) {
|
||||
registerAsDelegate()
|
||||
setNotificationsEnabled(true)
|
||||
reloadData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `HMCatalogViewController` is a super class which mainly provides easy-access methods for shared HomeKit objects.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
The super class for most table view controllers in this app. It manages home
|
||||
delegate registration and facilitates 'popping back' when it's discovered that
|
||||
a home has been deleted.
|
||||
*/
|
||||
class HMCatalogViewController: UITableViewController, HMHomeDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
var homeStore: HomeStore {
|
||||
return HomeStore.sharedStore
|
||||
}
|
||||
|
||||
var home: HMHome! {
|
||||
return homeStore.home
|
||||
}
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/**
|
||||
Evaluates whether or not the view controller should pop to
|
||||
the list of homes.
|
||||
|
||||
- returns: `true` if this instance is not the root view controller
|
||||
and the `home` is nil; `false` otherwise.
|
||||
*/
|
||||
private func shouldPopViewController() -> Bool {
|
||||
if let rootViewController = navigationController?.viewControllers.first
|
||||
where rootViewController == self {
|
||||
return false
|
||||
}
|
||||
|
||||
return home == nil
|
||||
}
|
||||
|
||||
/// Pops the view controller, if required. Invokes the delegate registration method.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if shouldPopViewController() {
|
||||
// Pop to root view controller if our home was destroyed while we were away.
|
||||
navigationController?.popToRootViewControllerAnimated(true)
|
||||
return
|
||||
}
|
||||
|
||||
registerAsDelegate()
|
||||
}
|
||||
|
||||
// MARK: Delegate Registration
|
||||
|
||||
/**
|
||||
A hierarchical method, to be overriden by superclasses.
|
||||
The base implementation registers as the delegate for the `HomeStore`'s home.
|
||||
Thus, any subclasses may override this, register as the delegate for any
|
||||
objects they please, and then call `super.registerAsDelegate()` to register
|
||||
as the home delegate as well.
|
||||
|
||||
This method will be called when the view appears.
|
||||
*/
|
||||
func registerAsDelegate() {
|
||||
homeStore.home?.delegate = self
|
||||
}
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `AccessoryBrowserViewController` displays new accessories and allows the user to pair with them.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
import ExternalAccessory
|
||||
|
||||
/// Represents an accessory type and encapsulated accessory.
|
||||
enum AccessoryType: Equatable, Nameable {
|
||||
/// A HomeKit object
|
||||
case HomeKit(accessory: HMAccessory)
|
||||
|
||||
/// An external, `EAWiFiUnconfiguredAccessory` object
|
||||
case External(accessory: EAWiFiUnconfiguredAccessory)
|
||||
|
||||
/// The name of the accessory.
|
||||
var name: String {
|
||||
return accessory.name
|
||||
}
|
||||
|
||||
/// The accessory within the `AccessoryType`.
|
||||
var accessory: AnyObject {
|
||||
switch self {
|
||||
case .HomeKit(let accessory):
|
||||
return accessory
|
||||
|
||||
case .External(let accessory):
|
||||
return accessory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Comparison of `AccessoryType`s based on name.
|
||||
func ==(lhs: AccessoryType, rhs: AccessoryType) -> Bool {
|
||||
return lhs.name == rhs.name
|
||||
}
|
||||
|
||||
/**
|
||||
A view controller that displays a list of nearby accessories and allows the
|
||||
user to add them to the provided HMHome.
|
||||
*/
|
||||
class AccessoryBrowserViewController: HMCatalogViewController, ModifyAccessoryDelegate, EAWiFiUnconfiguredAccessoryBrowserDelegate, HMAccessoryBrowserDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let accessoryCell = "AccessoryCell"
|
||||
static let addedAccessoryCell = "AddedAccessoryCell"
|
||||
static let addAccessorySegue = "Add Accessory"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var addedAccessories = [HMAccessory]()
|
||||
var displayedAccessories = [AccessoryType]()
|
||||
let accessoryBrowser = HMAccessoryBrowser()
|
||||
var externalAccessoryBrowser: EAWiFiUnconfiguredAccessoryBrowser?
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the table view and initializes the accessory browsers.
|
||||
override func viewDidLoad() {
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
accessoryBrowser.delegate = self
|
||||
|
||||
#if arch(arm)
|
||||
// We can't use the ExternalAccessory framework on the iPhone simulator.
|
||||
externalAccessoryBrowser = EAWiFiUnconfiguredAccessoryBrowser(delegate: self, queue: dispatch_get_main_queue())
|
||||
#endif
|
||||
|
||||
startBrowsing()
|
||||
}
|
||||
|
||||
/// Reloads the view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/// Stops browsing and dismisses the view controller.
|
||||
@IBAction func dismiss(sender: AnyObject) {
|
||||
stopBrowsing()
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
/// Sets the accessory, home, and delegate of a ModifyAccessoryViewController.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
|
||||
if let sender = sender as? HMAccessory where segue.identifier == Identifiers.addAccessorySegue {
|
||||
let modifyViewController = segue.intendedDestinationViewController as! ModifyAccessoryViewController
|
||||
modifyViewController.accessory = sender
|
||||
modifyViewController.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
Generates the number of rows based on the number of displayed accessories.
|
||||
|
||||
This method will also display a table view background message, if required.
|
||||
|
||||
- returns: The number of rows based on the number of displayed accessories.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let rows = displayedAccessories.count
|
||||
|
||||
if rows == 0 {
|
||||
let message = NSLocalizedString("No Discovered Accessories", comment: "No Discovered Accessories")
|
||||
setBackgroundMessage(message)
|
||||
}
|
||||
else {
|
||||
setBackgroundMessage(nil)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: Creates a cell that lists an accessory, and if it hasn't been added to the home,
|
||||
shows a disclosure indicator instead of a checkmark.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let accessoryType = displayedAccessories[indexPath.row]
|
||||
|
||||
var reuseIdentifier = Identifiers.accessoryCell
|
||||
|
||||
if case let .HomeKit(hmAccessory) = accessoryType where addedAccessories.contains(hmAccessory) {
|
||||
reuseIdentifier = Identifiers.addedAccessoryCell
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
|
||||
cell.textLabel?.text = accessoryType.name
|
||||
|
||||
if let accessory = accessoryType.accessory as? HMAccessory {
|
||||
cell.detailTextLabel?.text = accessory.category.localizedDescription
|
||||
}
|
||||
else {
|
||||
cell.detailTextLabel?.text = NSLocalizedString("External Accessory", comment: "External Accessory")
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Configures the accessory based on its type.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
switch displayedAccessories[indexPath.row] {
|
||||
case .HomeKit(let accessory):
|
||||
configureAccessory(accessory)
|
||||
|
||||
case .External(let accessory):
|
||||
externalAccessoryBrowser?.configureAccessory(accessory, withConfigurationUIOnViewController: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Starts browsing on both HomeKit and External accessory browsers.
|
||||
private func startBrowsing(){
|
||||
accessoryBrowser.startSearchingForNewAccessories()
|
||||
externalAccessoryBrowser?.startSearchingForUnconfiguredAccessoriesMatchingPredicate(nil)
|
||||
}
|
||||
|
||||
/// Stops browsing on both HomeKit and External accessory browsers.
|
||||
private func stopBrowsing(){
|
||||
accessoryBrowser.stopSearchingForNewAccessories()
|
||||
externalAccessoryBrowser?.stopSearchingForUnconfiguredAccessories()
|
||||
}
|
||||
|
||||
/**
|
||||
Concatenates and sorts the discovered and added accessories.
|
||||
|
||||
- returns: A sorted list of all accessories involved with this
|
||||
browser session.
|
||||
*/
|
||||
func allAccessories() -> [AccessoryType] {
|
||||
var accessories = [AccessoryType]()
|
||||
accessories += accessoryBrowser.discoveredAccessories.map { .HomeKit(accessory: $0) }
|
||||
|
||||
accessories += addedAccessories.flatMap { addedAccessory in
|
||||
let accessoryType = AccessoryType.HomeKit(accessory: addedAccessory)
|
||||
|
||||
return accessories.contains(accessoryType) ? nil : accessoryType
|
||||
}
|
||||
|
||||
if let external = externalAccessoryBrowser?.unconfiguredAccessories {
|
||||
let unconfiguredAccessoriesArray = Array(external)
|
||||
|
||||
accessories += unconfiguredAccessoriesArray.flatMap { addedAccessory in
|
||||
let accessoryType = AccessoryType.External(accessory: addedAccessory)
|
||||
|
||||
return accessories.contains(accessoryType) ? nil : accessoryType
|
||||
}
|
||||
}
|
||||
|
||||
return accessories.sortByLocalizedName()
|
||||
}
|
||||
|
||||
/// Updates the displayed accesories array and reloads the table view.
|
||||
private func reloadTable() {
|
||||
displayedAccessories = allAccessories()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// Sends the accessory to the next view.
|
||||
func configureAccessory(accessory: HMAccessory) {
|
||||
if displayedAccessories.contains(.HomeKit(accessory: accessory)) {
|
||||
performSegueWithIdentifier(Identifiers.addAccessorySegue, sender: accessory)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Finds an unconfigured accessory with a specified name.
|
||||
|
||||
- parameter name: The name string of the accessory.
|
||||
|
||||
- returns: An `HMAccessory?` from the search; `nil` if
|
||||
the accessory could not be found.
|
||||
*/
|
||||
func unconfiguredHomeKitAccessoryWithName(name: String) -> HMAccessory? {
|
||||
for type in displayedAccessories {
|
||||
if case let .HomeKit(accessory) = type where accessory.name == name {
|
||||
return accessory
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: ModifyAccessoryDelegate Methods
|
||||
|
||||
/// Adds the accessory to the internal array and reloads the views.
|
||||
func accessoryViewController(accessoryViewController: ModifyAccessoryViewController, didSaveAccessory accessory: HMAccessory) {
|
||||
addedAccessories.append(accessory)
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
// MARK: EAWiFiUnconfiguredAccessoryBrowserDelegate Methods
|
||||
|
||||
// Any updates to the external accessory browser causes a reload in the table view.
|
||||
|
||||
func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didFindUnconfiguredAccessories accessories: Set<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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `AccessoryUpdateController` manages `CharacteristicCell` updates and buffers them up before sending them to HomeKit.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/// An object that responds to `CharacteristicCell` updates and notifies HomeKit of changes.
|
||||
class AccessoryUpdateController: NSObject, CharacteristicCellDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
let updateQueue = dispatch_queue_create("com.sample.HMCatalog.CharacteristicUpdateQueue", DISPATCH_QUEUE_SERIAL)
|
||||
|
||||
lazy var pendingWrites = [HMCharacteristic:AnyObject]()
|
||||
lazy var sentWrites = [HMCharacteristic:AnyObject]()
|
||||
|
||||
// Implicitly unwrapped optional because we need `self` to initialize.
|
||||
var updateValueTimer: NSTimer!
|
||||
|
||||
/// Starts the update timer on creation.
|
||||
override init() {
|
||||
super.init()
|
||||
startListeningForCellUpdates()
|
||||
}
|
||||
|
||||
/// Responds to a cell change, and if the update was marked immediate, updates the characteristics.
|
||||
func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) {
|
||||
pendingWrites[characteristic] = value
|
||||
if immediate {
|
||||
updateCharacteristics()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Reads the characteristic's value and calls the completion with the characteristic's value.
|
||||
|
||||
If there is a pending write request on the same characteristic, the read is ignored to prevent
|
||||
"UI glitching".
|
||||
*/
|
||||
func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) {
|
||||
characteristic.readValueWithCompletionHandler { error in
|
||||
dispatch_sync(self.updateQueue) {
|
||||
if let sentValue = self.sentWrites[characteristic] {
|
||||
completion(sentValue, nil)
|
||||
return
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
completion(characteristic.value, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates and starts the update value timer.
|
||||
func startListeningForCellUpdates() {
|
||||
updateValueTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(AccessoryUpdateController.updateCharacteristics), userInfo: nil, repeats: true)
|
||||
}
|
||||
|
||||
/// Invalidates the update timer.
|
||||
func stopListeningForCellUpdates() {
|
||||
updateValueTimer.invalidate()
|
||||
}
|
||||
|
||||
/// Sends all pending requests in the array.
|
||||
func updateCharacteristics() {
|
||||
dispatch_sync(updateQueue) {
|
||||
for (characteristic, value) in self.pendingWrites {
|
||||
self.sentWrites[characteristic] = value
|
||||
|
||||
characteristic.writeValue(value) { error in
|
||||
if let error = error {
|
||||
print("HomeKit: Could not change value: \(error.localizedDescription).")
|
||||
}
|
||||
|
||||
self.didCompleteWrite(characteristic, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
self.pendingWrites.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Synchronously adds the characteristic-value pair into the `sentWrites` map.
|
||||
|
||||
- parameter characteristic: The `HMCharacteristic` to add.
|
||||
- parameter value: The value of the `characteristic`.
|
||||
*/
|
||||
func didSendWrite(characteristic: HMCharacteristic, value: AnyObject) {
|
||||
dispatch_sync(updateQueue) {
|
||||
self.sentWrites[characteristic] = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Synchronously removes the characteristic-value pair from the `sentWrites` map.
|
||||
|
||||
- parameter characteristic: The `HMCharacteristic` to remove.
|
||||
- parameter value: The value of the `characteristic` (unused, but included for clarity).
|
||||
*/
|
||||
func didCompleteWrite(characteristic: HMCharacteristic, value: AnyObject) {
|
||||
dispatch_sync(updateQueue) {
|
||||
self.sentWrites.removeValueForKey(characteristic)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ControlsTableViewDataSource` provides data for the `ControlsViewController`.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A `UITableViewDataSource` that populates the table in `ControlsViewController`.
|
||||
class ControlsTableViewDataSource: NSObject, UITableViewDataSource {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let serviceCell = "ServiceCell"
|
||||
static let unreachableServiceCell = "UnreachableServiceCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var serviceTable: [String: [HMService]]?
|
||||
var sortedKeys: [String]?
|
||||
|
||||
let tableView: UITableView
|
||||
var home: HMHome? {
|
||||
return HomeStore.sharedStore.home
|
||||
}
|
||||
|
||||
/// Initializes the table view and data source.
|
||||
required init(tableView: UITableView) {
|
||||
self.tableView = tableView
|
||||
super.init()
|
||||
self.tableView.dataSource = self
|
||||
}
|
||||
|
||||
/**
|
||||
Reloads the table, sets the table's dataSource to self,
|
||||
regenerated the service table, creates a sorted list of keys,
|
||||
sets the home's delegate, and reloads the table.
|
||||
*/
|
||||
func reloadTable() {
|
||||
if let home = home {
|
||||
serviceTable = home.serviceTable
|
||||
sortedKeys = serviceTable!.keys.sort()
|
||||
tableView.reloadData()
|
||||
}
|
||||
else {
|
||||
serviceTable = nil
|
||||
sortedKeys = nil
|
||||
}
|
||||
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// - returns: The localized description of the service type for that section.
|
||||
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return sortedKeys?[section]
|
||||
}
|
||||
|
||||
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return sortedKeys?.count ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A message that corresponds to the current most important reason
|
||||
that there are no services in the table. Either "No Accessories"
|
||||
or "No Services".
|
||||
*/
|
||||
func emptyMessage() -> String {
|
||||
if home?.accessories.count == 0 {
|
||||
return NSLocalizedString("No Accessories", comment: "No Accessories")
|
||||
}
|
||||
else {
|
||||
return NSLocalizedString("No Services", comment: "No Services")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: The number of services matching the service type in that section.
|
||||
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return serviceTable![sortedKeys![section]]!.count
|
||||
}
|
||||
|
||||
/// - returns: A `ServiceCell` set for the service at the provided index path.
|
||||
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let service = serviceForIndexPath(indexPath)!
|
||||
|
||||
let reuseIdentifier = service.accessory!.reachable ? Identifiers.serviceCell : Identifiers.unreachableServiceCell
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! ServiceCell
|
||||
|
||||
cell.service = service
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// - returns: The service represented at the index path in the table.
|
||||
func serviceForIndexPath(indexPath: NSIndexPath) -> HMService? {
|
||||
if let sortedKeys = sortedKeys,
|
||||
serviceTable = serviceTable,
|
||||
services = serviceTable[sortedKeys[indexPath.section]] {
|
||||
return services[indexPath.row]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ControlsViewController` lists services in the selected home.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller which displays a list of `HMServices`, separated by Service Type.
|
||||
class ControlsViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let showServiceSegue = "Show Service"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var tableViewDataSource: ControlsTableViewDataSource!
|
||||
var cellController = AccessoryUpdateController()
|
||||
|
||||
@IBOutlet weak var addButton: UIBarButtonItem!
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Sends the selected service into the destination view controller.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
if segue.identifier == Identifiers.showServiceSegue {
|
||||
if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) {
|
||||
let characteristicsViewController = segue.intendedDestinationViewController as! CharacteristicsViewController
|
||||
|
||||
if let selectedService = tableViewDataSource.serviceForIndexPath(indexPath) {
|
||||
characteristicsViewController.service = selectedService
|
||||
}
|
||||
|
||||
characteristicsViewController.cellDelegate = cellController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the table view data source.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableViewDataSource = ControlsTableViewDataSource(tableView: tableView)
|
||||
}
|
||||
|
||||
/// Reloads the view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationItem.title = home.name
|
||||
reloadData()
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
private func reloadData() {
|
||||
tableViewDataSource.reloadTable()
|
||||
let sections = tableViewDataSource.numberOfSectionsInTableView(tableView)
|
||||
|
||||
if sections == 0 {
|
||||
setBackgroundMessage(tableViewDataSource.emptyMessage())
|
||||
}
|
||||
else {
|
||||
setBackgroundMessage(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Delegate Registration
|
||||
|
||||
/// Registers as the delegate for the current home and all accessories in the home.
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
for accessory in home.accessories {
|
||||
accessory.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Any delegate methods which could change data will reload the
|
||||
table view data source.
|
||||
*/
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
|
||||
accessory.delegate = self
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
func accessoryDidUpdateReachability(accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func accessory(accessory: HMAccessory, didUpdateAssociatedServiceTypeForService service: HMService) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func accessoryDidUpdateServices(accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func accessoryDidUpdateName(accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,388 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ModifyAccessoryViewController` allows the user to modify a HomeKit accessory.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents the sections in the `ModifyAccessoryViewController`.
|
||||
enum AddAccessoryTableViewSection: Int {
|
||||
case Name, Rooms, Identify
|
||||
|
||||
static let count = 3
|
||||
}
|
||||
|
||||
/// Contains a method for notifying the delegate that the accessory was saved.
|
||||
protocol ModifyAccessoryDelegate {
|
||||
func accessoryViewController(accessoryViewController: ModifyAccessoryViewController, didSaveAccessory accessory: HMAccessory)
|
||||
}
|
||||
|
||||
/// A view controller that allows for renaming, reassigning, and identifying accessories before and after they've been added to a home.
|
||||
class ModifyAccessoryViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let roomCell = "RoomCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
// Update this if the acessory failed in any way.
|
||||
private var didEncounterError = false
|
||||
|
||||
private var selectedIndexPath: NSIndexPath?
|
||||
private var selectedRoom: HMRoom!
|
||||
|
||||
@IBOutlet weak var nameField: UITextField!
|
||||
private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
|
||||
|
||||
private let saveAccessoryGroup = dispatch_group_create()
|
||||
|
||||
private var editingExistingAccessory = false
|
||||
|
||||
// Strong reference, because we will replace the button with an activity indicator.
|
||||
@IBOutlet /* strong */ var addButton: UIBarButtonItem!
|
||||
var delegate: ModifyAccessoryDelegate?
|
||||
var rooms = [HMRoom]()
|
||||
|
||||
var accessory: HMAccessory!
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the table view and initializes view elements.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
|
||||
selectedRoom = accessory.room ?? home.roomForEntireHome()
|
||||
|
||||
// If the accessory belongs to the home already, we are in 'edit' mode.
|
||||
editingExistingAccessory = accessoryHasBeenAddedToHome()
|
||||
if editingExistingAccessory {
|
||||
// Show 'save' instead of 'add.'
|
||||
addButton.title = NSLocalizedString("Save", comment: "Save")
|
||||
}
|
||||
else {
|
||||
/*
|
||||
If we're not editing an existing accessory, then let the back
|
||||
button show in the left.
|
||||
*/
|
||||
navigationItem.leftBarButtonItem = nil
|
||||
}
|
||||
|
||||
// Put the accessory's name in the 'name' field.
|
||||
resetNameField()
|
||||
|
||||
// Register a cell for the rooms.
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.roomCell)
|
||||
}
|
||||
|
||||
/**
|
||||
Registers as the delegate for the current home
|
||||
and the accessory.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
accessory.delegate = self
|
||||
}
|
||||
|
||||
/// Replaces the activity indicator with the 'Add' or 'Save' button.
|
||||
func hideActivityIndicator() {
|
||||
activityIndicator.stopAnimating()
|
||||
navigationItem.rightBarButtonItem = addButton
|
||||
}
|
||||
|
||||
/// Temporarily replaces the 'Add' or 'Save' button with an activity indicator.
|
||||
func showActivityIndicator() {
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityIndicator)
|
||||
activityIndicator.startAnimating()
|
||||
}
|
||||
|
||||
/**
|
||||
Called whenever the user taps the 'add' button.
|
||||
|
||||
This method:
|
||||
1. Adds the accessory to the home, if not already added.
|
||||
2. Updates the accessory's name, if necessary.
|
||||
3. Assigns the accessory to the selected room, if necessary.
|
||||
*/
|
||||
@IBAction func didTapAddButton() {
|
||||
let name = trimmedName
|
||||
showActivityIndicator()
|
||||
|
||||
if editingExistingAccessory {
|
||||
home(home, assignAccessory: accessory, toRoom: selectedRoom)
|
||||
updateName(name, forAccessory: accessory)
|
||||
}
|
||||
else {
|
||||
dispatch_group_enter(saveAccessoryGroup)
|
||||
home.addAccessory(accessory) { error in
|
||||
if let error = error {
|
||||
self.hideActivityIndicator()
|
||||
self.displayError(error)
|
||||
self.didEncounterError = true
|
||||
}
|
||||
else {
|
||||
// Once it's successfully added to the home, add it to the room that's selected.
|
||||
self.home(self.home, assignAccessory:self.accessory, toRoom: self.selectedRoom)
|
||||
self.updateName(name, forAccessory: self.accessory)
|
||||
}
|
||||
dispatch_group_leave(self.saveAccessoryGroup)
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_group_notify(saveAccessoryGroup, dispatch_get_main_queue()) {
|
||||
self.hideActivityIndicator()
|
||||
if !self.didEncounterError {
|
||||
self.dismiss(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Informs the delegate that the accessory has been saved, and
|
||||
dismisses the view controller.
|
||||
*/
|
||||
@IBAction func dismiss(sender: AnyObject?) {
|
||||
delegate?.accessoryViewController(self, didSaveAccessory: accessory)
|
||||
if editingExistingAccessory {
|
||||
presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
else {
|
||||
navigationController?.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if the accessory has already been added to
|
||||
the home; `false` otherwise.
|
||||
*/
|
||||
func accessoryHasBeenAddedToHome() -> Bool {
|
||||
return home.accessories.contains(accessory)
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the accessories name. This function will enter and leave the saved dispatch group.
|
||||
If the accessory's name is already equal to the passed-in name, this method does nothing.
|
||||
|
||||
- parameter name: The new name for the accessory.
|
||||
- parameter accessory: The accessory to rename.
|
||||
*/
|
||||
func updateName(name: String, forAccessory accessory: HMAccessory) {
|
||||
if accessory.name == name {
|
||||
return
|
||||
}
|
||||
dispatch_group_enter(saveAccessoryGroup)
|
||||
accessory.updateName(name) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
self.didEncounterError = true
|
||||
}
|
||||
dispatch_group_leave(self.saveAccessoryGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Assigns the given accessory to the provided room. This method will enter and leave the saved dispatch group.
|
||||
|
||||
- parameter home: The home to assign.
|
||||
- parameter accessory: The accessory to be assigned.
|
||||
- parameter room: The room to which to assign the accessory.
|
||||
*/
|
||||
func home(home: HMHome, assignAccessory accessory: HMAccessory, toRoom room: HMRoom) {
|
||||
if accessory.room == room {
|
||||
return
|
||||
}
|
||||
dispatch_group_enter(saveAccessoryGroup)
|
||||
home.assignAccessory(accessory, toRoom: room) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
self.didEncounterError = true
|
||||
}
|
||||
dispatch_group_leave(self.saveAccessoryGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells the current accessory to identify itself.
|
||||
func identifyAccessory() {
|
||||
accessory.identifyWithCompletionHandler { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables the name field if the accessory's name changes.
|
||||
func resetNameField() {
|
||||
var action: String
|
||||
if editingExistingAccessory {
|
||||
action = NSLocalizedString("Edit %@", comment: "Edit Accessory")
|
||||
}
|
||||
else {
|
||||
action = NSLocalizedString("Add %@", comment: "Add Accessory")
|
||||
}
|
||||
navigationItem.title = NSString(format: action, accessory.name) as String
|
||||
nameField.text = accessory.name
|
||||
nameField.enabled = home.isAdmin
|
||||
enableAddButtonIfApplicable()
|
||||
}
|
||||
|
||||
/// Enables the save button if the name field is not empty.
|
||||
func enableAddButtonIfApplicable() {
|
||||
addButton.enabled = home.isAdmin && trimmedName.characters.count > 0
|
||||
}
|
||||
|
||||
/// - returns: The `nameField`'s text, trimmed of newline and whitespace characters.
|
||||
var trimmedName: String {
|
||||
return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
}
|
||||
|
||||
/// Enables or disables the add button.
|
||||
@IBAction func nameFieldDidChange(sender: AnyObject) {
|
||||
enableAddButtonIfApplicable()
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of `AddAccessoryTableViewSection`s.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return AddAccessoryTableViewSection.count
|
||||
}
|
||||
|
||||
/// - returns: The number rows for the rooms section. All other sections are static.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch AddAccessoryTableViewSection(rawValue: section) {
|
||||
case .Rooms?:
|
||||
return home.allRooms.count
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: `UITableViewAutomaticDimension` for dynamic cell, super otherwise.
|
||||
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
|
||||
switch AddAccessoryTableViewSection(rawValue: indexPath.section) {
|
||||
case .Rooms?:
|
||||
return UITableViewAutomaticDimension
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A 'room cell' for the rooms section, super otherwise.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch AddAccessoryTableViewSection(rawValue: indexPath.section) {
|
||||
case .Rooms?:
|
||||
return self.tableView(tableView, roomCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a cell with the name of each room within the home, displaying a checkmark if the room
|
||||
is the currently selected room.
|
||||
*/
|
||||
func tableView(tableView: UITableView, roomCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath)
|
||||
let room = home.allRooms[indexPath.row] as HMRoom
|
||||
|
||||
cell.textLabel?.text = home.nameForRoom(room)
|
||||
|
||||
// Put a checkmark on the selected room.
|
||||
cell.accessoryType = room == selectedRoom ? .Checkmark : .None
|
||||
if !home.isAdmin {
|
||||
cell.selectionStyle = .None
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
/// Handles row selection based on the section.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
switch AddAccessoryTableViewSection(rawValue: indexPath.section) {
|
||||
case .Rooms?:
|
||||
guard home.isAdmin else { return }
|
||||
|
||||
selectedRoom = home.allRooms[indexPath.row]
|
||||
|
||||
let sections = NSIndexSet(index: AddAccessoryTableViewSection.Rooms.rawValue)
|
||||
|
||||
tableView.reloadSections(sections, withRowAnimation: .Automatic)
|
||||
|
||||
case .Identify?:
|
||||
identifyAccessory()
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AddAccessoryTableViewSection` raw value.")
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
/// Required override.
|
||||
override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
|
||||
return super.tableView(tableView, indentationLevelForRowAtIndexPath: NSIndexPath(forRow: 0, inSection: indexPath.section))
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
// All home changes reload the view.
|
||||
|
||||
func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didAddRoom room: HMRoom) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom) {
|
||||
if selectedRoom == room {
|
||||
// Reset the selected room if ours was deleted.
|
||||
selectedRoom = homeStore.home!.roomForEntireHome()
|
||||
}
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
|
||||
/*
|
||||
Bridged accessories don't call the original completion handler if their
|
||||
bridges are added to the home. We must respond to `HMHomeDelegate`'s
|
||||
`home(_:didAddAccessory:)` and assign bridged accessories properly.
|
||||
*/
|
||||
if selectedRoom != nil {
|
||||
self.home(home, assignAccessory: accessory, toRoom: selectedRoom)
|
||||
}
|
||||
}
|
||||
|
||||
func home(home: HMHome, didUnblockAccessory accessory: HMAccessory) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
/// If the accessory's name changes, we update the name field.
|
||||
func accessoryDidUpdateName(accessory: HMAccessory) {
|
||||
resetNameField()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
`CharacteristicCell` is a superclass which represents the state of a HomeKit characteristic.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Methods for handling cell reads and updates.
|
||||
protocol CharacteristicCellDelegate {
|
||||
|
||||
/**
|
||||
Called whenever the control within the cell updates its value.
|
||||
|
||||
- parameter cell: The cell which has updated its value.
|
||||
- parameter newValue: The new value represented by the cell's control.
|
||||
- parameter characteristic: The characteristic the cell represents.
|
||||
- parameter immediate: Whether or not to update external values immediately.
|
||||
|
||||
For example, Slider cells should not update immediately upon value change,
|
||||
so their values are cached and updates are coalesced. Subclasses can decide
|
||||
whether or not their values are meant to be updated immediately.
|
||||
*/
|
||||
func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool)
|
||||
|
||||
/**
|
||||
Called when the characteristic cell needs to reload its value from an external source.
|
||||
Consider using this call to look up values in memory or query them from an accessory.
|
||||
|
||||
- parameter cell: The cell requesting a value update.
|
||||
- parameter characteristic: The characteristic for whose value the cell is asking.
|
||||
- parameter completion: The closure that the cell provides to be called when values have been read successfully.
|
||||
*/
|
||||
func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void)
|
||||
}
|
||||
|
||||
/**
|
||||
A `UITableViewCell` subclass that displays the current value of an `HMCharacteristic` and
|
||||
notifies its delegate of changes. Subclasses of this class will provide additional controls
|
||||
to display different kinds of data.
|
||||
*/
|
||||
class CharacteristicCell: UITableViewCell {
|
||||
/// An alpha percentage used when disabling cells.
|
||||
static let DisabledAlpha: CGFloat = 0.4
|
||||
|
||||
/// Required init.
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
/// Subclasses can return false if they have many frequent updates that should be deferred.
|
||||
class var updatesImmediately: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var typeLabel: UILabel!
|
||||
@IBOutlet weak var valueLabel: UILabel!
|
||||
@IBOutlet weak var favoriteButton: UIButton!
|
||||
|
||||
@IBOutlet weak var favoriteButtonWidthConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var favoriteButtonHeightContraint: NSLayoutConstraint!
|
||||
|
||||
/**
|
||||
Show / hide the favoriteButton and adjust the constraints
|
||||
to ensure proper layout.
|
||||
*/
|
||||
var showsFavorites = false {
|
||||
didSet {
|
||||
if showsFavorites {
|
||||
favoriteButton.hidden = false
|
||||
favoriteButtonWidthConstraint.constant = favoriteButtonHeightContraint.constant
|
||||
}
|
||||
else {
|
||||
favoriteButton.hidden = true
|
||||
favoriteButtonWidthConstraint.constant = 15.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if the represented characteristic is reachable;
|
||||
`false` otherwise.
|
||||
*/
|
||||
var enabled: Bool {
|
||||
return (characteristic.service?.accessory?.reachable ?? false)
|
||||
}
|
||||
|
||||
/**
|
||||
The value currently represented by the cell.
|
||||
|
||||
This is not necessarily the value of this cell's characteristic,
|
||||
because the cell's value changes independently of the characteristic.
|
||||
*/
|
||||
var value: AnyObject?
|
||||
|
||||
/// The delegate that will respond to cell value changes.
|
||||
var delegate: CharacteristicCellDelegate?
|
||||
|
||||
/**
|
||||
The characteristic represented by this cell.
|
||||
|
||||
When this is set, the cell populates based on
|
||||
the characteristic's value and requests an initial value
|
||||
from its delegate.
|
||||
*/
|
||||
var characteristic: HMCharacteristic! {
|
||||
didSet {
|
||||
typeLabel.text = characteristic.localizedCharacteristicType
|
||||
|
||||
selectionStyle = characteristic.isIdentify ? .Default : .None
|
||||
|
||||
setValue(characteristic.value, notify: false)
|
||||
|
||||
if characteristic.isWriteOnly {
|
||||
// Don't read the value for write-only characteristics.
|
||||
return
|
||||
}
|
||||
|
||||
// Set initial state of the favorite button
|
||||
favoriteButton.selected = characteristic.isFavorite
|
||||
|
||||
// "Enable" the cell if the accessory is reachable or we are displaying the favorites.
|
||||
|
||||
// Configure the views.
|
||||
typeLabel.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
|
||||
valueLabel?.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
|
||||
|
||||
if enabled {
|
||||
delegate?.characteristicCell(self, readInitialValueForCharacteristic: characteristic) { value, error in
|
||||
if let error = error {
|
||||
print("HomeKit: Error reading value for characteristic \(self.characteristic): \(error.localizedDescription).")
|
||||
}
|
||||
else {
|
||||
self.setValue(value, notify: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the value label to the localized description from HMCharacteristic+Readability.
|
||||
func resetValueLabel() {
|
||||
if let value = value {
|
||||
valueLabel?.text = characteristic.localizedDescriptionForValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Toggles the star button and saves the favorite status
|
||||
of the characteristic in the FavoriteManager.
|
||||
*/
|
||||
@IBAction func didTapFavoriteButton(sender: UIButton) {
|
||||
sender.selected = !sender.selected
|
||||
characteristic.isFavorite = sender.selected
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the cell's value and resets the label.
|
||||
|
||||
- parameter newValue: The new value.
|
||||
- parameter notify: If true, the cell notifies its delegate of the change.
|
||||
*/
|
||||
func setValue(newValue: AnyObject?, notify: Bool) {
|
||||
value = newValue
|
||||
if let newValue = newValue {
|
||||
resetValueLabel()
|
||||
/*
|
||||
We do not allow the setting of nil values from the app,
|
||||
but we do have to deal with incoming nil values.
|
||||
*/
|
||||
if notify {
|
||||
delegate?.characteristicCell(self, didUpdateValue: newValue, forCharacteristic: characteristic, immediate: self.dynamicType.updatesImmediately)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `SegmentedControlCharacteristicCell` displays characteristics with associated values.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A `CharacteristicCell` subclass that contains a `UISegmentedControl`.
|
||||
|
||||
Used for `HMCharacteristic`s which have associated, non-numeric values, like Lock Management State.
|
||||
*/
|
||||
class SegmentedControlCharacteristicCell: CharacteristicCell {
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var segmentedControl: UISegmentedControl!
|
||||
|
||||
/**
|
||||
Calls the super class's didSet, and also computes a list
|
||||
of possible values.
|
||||
*/
|
||||
override var characteristic: HMCharacteristic! {
|
||||
didSet {
|
||||
segmentedControl.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
|
||||
segmentedControl.userInteractionEnabled = enabled
|
||||
|
||||
if let values = characteristic.allPossibleValues as? [Int] {
|
||||
possibleValues = values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The possible values for this characteristic.
|
||||
When this is set, adds localized descriptions to the segmented control.
|
||||
*/
|
||||
var possibleValues = [Int]() {
|
||||
didSet {
|
||||
segmentedControl.removeAllSegments()
|
||||
for index in 0..<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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `SliderCharacteristicCell` displays characteristics with a continuous range of options.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A `CharacteristicCell` subclass that contains a slider.
|
||||
Used for numeric characteristics that have a continuous range of options.
|
||||
*/
|
||||
class SliderCharacteristicCell: CharacteristicCell {
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var valueSlider: UISlider!
|
||||
|
||||
override var characteristic: HMCharacteristic! {
|
||||
didSet {
|
||||
valueSlider.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
|
||||
valueSlider.userInteractionEnabled = enabled
|
||||
}
|
||||
|
||||
willSet(newCharacteristic) {
|
||||
// These are sane defaults in case the max and min are not set.
|
||||
valueSlider.minimumValue = newCharacteristic.metadata?.minimumValue as? Float ?? 0.0
|
||||
valueSlider.maximumValue = newCharacteristic.metadata?.maximumValue as? Float ?? 100.0
|
||||
}
|
||||
}
|
||||
|
||||
/// If notify is false, sets the valueSlider's represented value.
|
||||
override func setValue(newValue: AnyObject?, notify: Bool) {
|
||||
super.setValue(newValue, notify: notify)
|
||||
if let newValue = newValue as? NSNumber where !notify {
|
||||
valueSlider.value = newValue.floatValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Restricts a value to the step value provided in the cell's
|
||||
characteristic's metadata.
|
||||
|
||||
- parameter sliderValue: The provided value.
|
||||
|
||||
- returns: The value adjusted to align with a step value.
|
||||
*/
|
||||
func roundedValueForSliderValue(value: Float) -> Float {
|
||||
if let metadata = characteristic.metadata,
|
||||
stepValue = metadata.stepValue as? Float
|
||||
where stepValue > 0 {
|
||||
let newStep = roundf(value / stepValue)
|
||||
let stepped = newStep * stepValue
|
||||
return stepped
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Sliders don't update immediately, because sliders generate many updates.
|
||||
override class var updatesImmediately: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Responds to a slider change and sets the cell's value.
|
||||
|
||||
- parameter slider: The slider that changed.
|
||||
*/
|
||||
func didChangeSliderValue(slider: UISlider) {
|
||||
let value = roundedValueForSliderValue(slider.value)
|
||||
setValue(value, notify: true)
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `SwitchCharacteristicCell` displays Boolean characteristics.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A `CharacteristicCell` subclass that contains a single switch.
|
||||
Used for Boolean characteristics.
|
||||
*/
|
||||
class SwitchCharacteristicCell: CharacteristicCell {
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var valueSwitch: UISwitch!
|
||||
|
||||
override var characteristic: HMCharacteristic! {
|
||||
didSet {
|
||||
valueSwitch.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
|
||||
valueSwitch.userInteractionEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
/// If notify is false, sets the switch to the value.
|
||||
override func setValue(newValue: AnyObject?, notify: Bool) {
|
||||
super.setValue(newValue, notify: notify)
|
||||
|
||||
if !notify {
|
||||
if let boolValue = newValue as? Bool {
|
||||
valueSwitch.setOn(boolValue, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Responds to the switch updating and sets the
|
||||
value to the switch's value.
|
||||
|
||||
- parameter valueSwitch: The switch that updated.
|
||||
*/
|
||||
func didChangeSwitchValue(valueSwitch: UISwitch) {
|
||||
setValue(valueSwitch.on, notify: true)
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TextCharacteristicCell` represents text-input characteristics.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A `CharacteristicCell` subclass that contains a text field.
|
||||
Used for text-input characteristics.
|
||||
*/
|
||||
class TextCharacteristicCell: CharacteristicCell, UITextFieldDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var textField: UITextField!
|
||||
|
||||
override var characteristic: HMCharacteristic! {
|
||||
didSet {
|
||||
textField.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha
|
||||
textField.userInteractionEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
/// If notify is false, sets the text field's text from the value.
|
||||
override func setValue(newValue: AnyObject?, notify: Bool) {
|
||||
super.setValue(newValue, notify: notify)
|
||||
if !notify {
|
||||
if let newStringValue = newValue as? String {
|
||||
textField.text = newStringValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismiss the keyboard when "Go" is clicked
|
||||
func textFieldShouldReturn(textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
/// Sets the value of the characteristic when editing is complete.
|
||||
func textFieldDidEndEditing(textField: UITextField) {
|
||||
setValue(textField.text, notify: true)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `CharacteristicsTableViewDataSource` provides the data for the `CharacteristicsViewController`.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents the sections in the `CharacteristicsViewController`.
|
||||
enum CharacteristicTableViewSection: Int {
|
||||
case Characteristics, AssociatedServiceType
|
||||
}
|
||||
|
||||
/// A `UITableViewDataSource` that populates a `CharacteristicsViewController`.
|
||||
class CharacteristicsTableViewDataSource: NSObject, UITableViewDelegate, UITableViewDataSource {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let characteristicCell = "CharacteristicCell"
|
||||
static let sliderCharacteristicCell = "SliderCharacteristicCell"
|
||||
static let switchCharacteristicCell = "SwitchCharacteristicCell"
|
||||
static let segmentedControlCharacteristicCell = "SegmentedControlCharacteristicCell"
|
||||
static let textCharacteristicCell = "TextCharacteristicCell"
|
||||
static let serviceTypeCell = "ServiceTypeCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var service: HMService
|
||||
var tableView: UITableView
|
||||
var delegate: CharacteristicCellDelegate
|
||||
var showsFavorites: Bool
|
||||
var allowsAllWrites: Bool
|
||||
|
||||
/// Sets up properties from specified values, configures the table view, and cell reuse identifiers.
|
||||
required init(service: HMService, tableView: UITableView, delegate: CharacteristicCellDelegate, showsFavorites: Bool = false, allowsAllWrites: Bool = false) {
|
||||
self.service = service
|
||||
self.tableView = tableView
|
||||
self.delegate = delegate
|
||||
self.showsFavorites = showsFavorites
|
||||
self.allowsAllWrites = allowsAllWrites
|
||||
super.init()
|
||||
tableView.dataSource = self
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.estimatedRowHeight = 50.0
|
||||
registerReuseIdentifiers()
|
||||
}
|
||||
|
||||
/// Registers all of the characteristic cell reuse identifiers with this table.
|
||||
func registerReuseIdentifiers() {
|
||||
let characteristicNib = UINib(nibName: Identifiers.characteristicCell, bundle: nil)
|
||||
tableView.registerNib(characteristicNib, forCellReuseIdentifier: Identifiers.characteristicCell)
|
||||
|
||||
let sliderNib = UINib(nibName: Identifiers.sliderCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(sliderNib, forCellReuseIdentifier: Identifiers.sliderCharacteristicCell)
|
||||
|
||||
let switchNib = UINib(nibName: Identifiers.switchCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(switchNib, forCellReuseIdentifier: Identifiers.switchCharacteristicCell)
|
||||
|
||||
let segmentedNib = UINib(nibName: Identifiers.segmentedControlCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(segmentedNib, forCellReuseIdentifier: Identifiers.segmentedControlCharacteristicCell)
|
||||
|
||||
let textNib = UINib(nibName: Identifiers.textCharacteristicCell, bundle: nil)
|
||||
tableView.registerNib(textNib, forCellReuseIdentifier: Identifiers.textCharacteristicCell)
|
||||
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.serviceTypeCell)
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: The number of sections, computed from whether or not
|
||||
the services supports an associated service type.
|
||||
*/
|
||||
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return service.supportsAssociatedServiceType ? 2 : 1
|
||||
}
|
||||
|
||||
/**
|
||||
The characteristics section uses the services count to generate the number of rows.
|
||||
The associated service type uses the valid associated service types.
|
||||
*/
|
||||
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch CharacteristicTableViewSection(rawValue: section) {
|
||||
case .Characteristics?:
|
||||
return service.characteristics.count
|
||||
|
||||
case .AssociatedServiceType?:
|
||||
// For 'None'.
|
||||
return HMService.validAssociatedServiceTypes.count + 1
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Looks up the appropriate service type for the row in the list and returns a localized version,
|
||||
or 'None' if the row doesn't correspond to any valid service type.
|
||||
|
||||
- parameter row: The row to look up.
|
||||
|
||||
- returns: The localized service type in that row, or 'None'.
|
||||
*/
|
||||
func displayedServiceTypeForRow(row: Int) -> String {
|
||||
let serviceTypes = HMService.validAssociatedServiceTypes
|
||||
if row < serviceTypes.count {
|
||||
return HMService.localizedDescriptionForServiceType(serviceTypes[row])
|
||||
}
|
||||
|
||||
return NSLocalizedString("None", comment: "None")
|
||||
}
|
||||
|
||||
/**
|
||||
Evaluates whether or not a service type is selected for a given row.
|
||||
|
||||
- parameter row: The selected row.
|
||||
|
||||
- returns: `true` if the current row is a valid service type, `false` otherwise
|
||||
*/
|
||||
func serviceTypeIsSelectedForRow(row: Int) -> Bool {
|
||||
let serviceTypes = HMService.validAssociatedServiceTypes
|
||||
if row >= serviceTypes.count {
|
||||
return service.associatedServiceType == nil
|
||||
}
|
||||
|
||||
if let associatedServiceType = service.associatedServiceType {
|
||||
return serviceTypes[row] == associatedServiceType
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// Generates a cell for an associated service.
|
||||
private func tableView(tableView: UITableView, associatedServiceTypeCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceTypeCell, forIndexPath: indexPath)
|
||||
|
||||
cell.textLabel?.text = displayedServiceTypeForRow(indexPath.row)
|
||||
cell.accessoryType = serviceTypeIsSelectedForRow(indexPath.row) ? .Checkmark : .None
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Generates a characteristic cell based on the type of characteristic
|
||||
located at the specified index path.
|
||||
*/
|
||||
private func tableView(tableView: UITableView, characteristicCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let characteristic = service.characteristics[indexPath.row]
|
||||
|
||||
var reuseIdentifier = Identifiers.characteristicCell
|
||||
|
||||
if (characteristic.isReadOnly || characteristic.isWriteOnly) && !allowsAllWrites {
|
||||
reuseIdentifier = Identifiers.characteristicCell
|
||||
}
|
||||
else if characteristic.isBoolean {
|
||||
reuseIdentifier = Identifiers.switchCharacteristicCell
|
||||
}
|
||||
else if characteristic.hasPredeterminedValueDescriptions {
|
||||
reuseIdentifier = Identifiers.segmentedControlCharacteristicCell
|
||||
}
|
||||
else if characteristic.isNumeric {
|
||||
reuseIdentifier = Identifiers.sliderCharacteristicCell
|
||||
}
|
||||
else if characteristic.isTextWritable {
|
||||
reuseIdentifier = Identifiers.textCharacteristicCell
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CharacteristicCell
|
||||
|
||||
cell.showsFavorites = showsFavorites
|
||||
cell.delegate = delegate
|
||||
cell.characteristic = characteristic
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Uses convenience methods to generate a cell based on the index path's section.
|
||||
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch CharacteristicTableViewSection(rawValue: indexPath.section) {
|
||||
case .Characteristics?:
|
||||
return self.tableView(tableView, characteristicCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .AssociatedServiceType?:
|
||||
return self.tableView(tableView, associatedServiceTypeCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A localized string for the section.
|
||||
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch CharacteristicTableViewSection(rawValue: section) {
|
||||
case .Characteristics?:
|
||||
return NSLocalizedString("Characteristics", comment: "Characteristics")
|
||||
|
||||
case .AssociatedServiceType?:
|
||||
return NSLocalizedString("Associated Service Type", comment: "Associated Service Type")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `CharacteristicsViewController` displays characteristics within a service.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller that displays a list of characteristics within an `HMService`.
|
||||
class CharacteristicsViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
var service: HMService!
|
||||
var cellDelegate: CharacteristicCellDelegate!
|
||||
private var tableViewDataSource: CharacteristicsTableViewDataSource!
|
||||
var showsFavorites = false
|
||||
var allowsAllWrites = false
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Initializes the data source.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableViewDataSource = CharacteristicsTableViewDataSource(service: service, tableView: tableView, delegate: cellDelegate, showsFavorites: showsFavorites, allowsAllWrites: allowsAllWrites)
|
||||
}
|
||||
|
||||
/// Reloads the view and enabled notifications for all relevant characteristics.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
title = service.name
|
||||
setNotificationsEnabled(true)
|
||||
reloadTableView()
|
||||
}
|
||||
|
||||
/// Disables notifications for characteristics.
|
||||
override func viewWillDisappear(animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
setNotificationsEnabled(false)
|
||||
}
|
||||
|
||||
/**
|
||||
Registers as the delegate for the current home and
|
||||
the service's accessory.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
service.accessory?.delegate = self
|
||||
}
|
||||
|
||||
/**
|
||||
Enables or disables notifications on all characteristics within this service.
|
||||
|
||||
- parameter notificationsEnabled: A `Bool`; whether to enable or disable.
|
||||
*/
|
||||
func setNotificationsEnabled(notificationsEnabled: Bool) {
|
||||
for characteristic in service.characteristics {
|
||||
if characteristic.supportsEventNotification {
|
||||
characteristic.enableNotification(notificationsEnabled) { error in
|
||||
if let error = error {
|
||||
print("HomeKit: Error enabling notification on charcteristic '\(characteristic)': \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the table view and stops the refresh control.
|
||||
func reloadTableView() {
|
||||
setNotificationsEnabled(true)
|
||||
tableViewDataSource.service = service
|
||||
refreshControl?.endRefreshing()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
switch CharacteristicTableViewSection(rawValue: indexPath.section) {
|
||||
case .Characteristics?:
|
||||
let characteristic = service.characteristics[indexPath.row]
|
||||
didSelectCharacteristic(characteristic, atIndexPath: indexPath)
|
||||
|
||||
case .AssociatedServiceType?:
|
||||
didSelectAssociatedServiceTypeAtIndexPath(indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `CharacteristicTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
If a characteristic is selected, and it is the 'Identify' characteristic,
|
||||
perform an identify on that accessory.
|
||||
*/
|
||||
private func didSelectCharacteristic(characteristic: HMCharacteristic, atIndexPath indexPath: NSIndexPath) {
|
||||
if characteristic.isIdentify {
|
||||
service.accessory?.identifyWithCompletionHandler { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handles selection of one of the associated service types in the list.
|
||||
|
||||
- parameter indexPath: The selected index path.
|
||||
*/
|
||||
private func didSelectAssociatedServiceTypeAtIndexPath(indexPath: NSIndexPath) {
|
||||
let serviceTypes = HMService.validAssociatedServiceTypes
|
||||
var newServiceType: String?
|
||||
if indexPath.row < serviceTypes.count {
|
||||
newServiceType = serviceTypes[indexPath.row]
|
||||
}
|
||||
service.updateAssociatedServiceType(newServiceType) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
|
||||
self.didUpdateAssociatedServiceType()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the associated service section in the table view.
|
||||
private func didUpdateAssociatedServiceType() {
|
||||
let associatedServiceTypeIndexSet = NSIndexSet(index: CharacteristicTableViewSection.AssociatedServiceType.rawValue)
|
||||
|
||||
tableView.reloadSections(associatedServiceTypeIndexSet, withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// If our accessory was removed, pop to root view controller.
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
if accessory == service.accessory {
|
||||
navigationController?.popToRootViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
/// If our accessory becomes unreachable, pop to root view controller.
|
||||
func accessoryDidUpdateReachability(accessory: HMAccessory) {
|
||||
if accessory == service.accessory && !accessory.reachable {
|
||||
navigationController?.popToRootViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Search for the cell corresponding to that characteristic and
|
||||
update its value.
|
||||
*/
|
||||
func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) {
|
||||
if let index = service.characteristics.indexOf(characteristic) {
|
||||
let indexPath = NSIndexPath(forRow: index, inSection: 0)
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath) as! CharacteristicCell
|
||||
cell.setValue(characteristic.value, notify: false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ServiceCell` displays a service and information about it.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A `UITableViewCell` subclass for displaying a service and the room and accessory where it resides.
|
||||
class ServiceCell: UITableViewCell {
|
||||
|
||||
// MARK: Properties
|
||||
var includeAccessoryText = true
|
||||
|
||||
/// Required init.
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
/**
|
||||
The cell's service.
|
||||
|
||||
When the service is set, the cell's `textLabel` will contain the service's name
|
||||
or the accessory's name if the service has no name.
|
||||
The detail text will contain information about where this service lives.
|
||||
*/
|
||||
var service: HMService? {
|
||||
didSet {
|
||||
if let service = service,
|
||||
accessory = service.accessory {
|
||||
textLabel?.text = service.name ?? accessory.name
|
||||
let accessoryName = accessory.name
|
||||
let roomName = accessory.room!.name
|
||||
if includeAccessoryText {
|
||||
let inIdentifier = NSLocalizedString("%@ in %@", comment: "Accessory in Room")
|
||||
detailTextLabel?.text = String(format: inIdentifier, accessoryName, roomName)
|
||||
}
|
||||
else {
|
||||
detailTextLabel?.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ServicesViewController` displays an accessory's services.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents the sections in the `ServicesViewController`.
|
||||
enum AccessoryTableViewSection: Int {
|
||||
case Services, BridgedAccessories
|
||||
}
|
||||
|
||||
/**
|
||||
A view controller which displays all the services of a provided accessory, and
|
||||
passes its cell delegate onto a `CharacteristicsViewController`.
|
||||
*/
|
||||
class ServicesViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let accessoryCell = "AccessoryCell"
|
||||
static let serviceCell = "ServiceCell"
|
||||
static let showServiceSegue = "Show Service"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var accessory: HMAccessory!
|
||||
lazy var cellDelegate: CharacteristicCellDelegate = AccessoryUpdateController()
|
||||
var showsFavorites = false
|
||||
var allowsAllWrites = false
|
||||
var onlyShowsControlServices = false
|
||||
var displayedServices = [HMService]()
|
||||
var bridgedAccessories = [HMAccessory]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures table view.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
}
|
||||
|
||||
/// Reloads the view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
updateTitle()
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// Pops the view controller, if required.
|
||||
override func viewDidAppear(animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if shouldPopViewController() {
|
||||
navigationController?.popToRootViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Passes the `CharacteristicsViewController` the service from the cell and
|
||||
configures the view controller.
|
||||
*/
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
|
||||
guard segue.identifier == Identifiers.showServiceSegue else { return }
|
||||
|
||||
if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) {
|
||||
let selectedService = displayedServices[indexPath.row]
|
||||
let characteristicsViewController = segue.intendedDestinationViewController as! CharacteristicsViewController
|
||||
characteristicsViewController.showsFavorites = showsFavorites
|
||||
characteristicsViewController.allowsAllWrites = allowsAllWrites
|
||||
characteristicsViewController.service = selectedService
|
||||
characteristicsViewController.cellDelegate = cellDelegate
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if our accessory is no longer in the
|
||||
current home's list of accessories.
|
||||
*/
|
||||
private func shouldPopViewController() -> Bool {
|
||||
for accessory in homeStore.home!.accessories {
|
||||
if accessory == accessory {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Delegate Registration
|
||||
|
||||
/**
|
||||
Registers as the delegate for the current home
|
||||
and for the current accessory.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
accessory.delegate = self
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// Two sections if we're showing bridged accessories.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
if accessory.uniqueIdentifiersForBridgedAccessories != nil {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
Section 1 contains the services within the accessory.
|
||||
Section 2 contains the bridged accessories.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch AccessoryTableViewSection(rawValue: section) {
|
||||
case .Services?:
|
||||
return displayedServices.count
|
||||
|
||||
case .BridgedAccessories?:
|
||||
return bridgedAccessories.count
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AccessoryTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A Service or Bridged Accessory Cell based
|
||||
on the section.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch AccessoryTableViewSection(rawValue: indexPath.section) {
|
||||
case .Services?:
|
||||
return self.tableView(tableView, serviceCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .BridgedAccessories?:
|
||||
return self.tableView(tableView, bridgedAccessoryCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AccessoryTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A cell containing the name of a bridged
|
||||
accessory at a given index path.
|
||||
*/
|
||||
func tableView(tableView: UITableView, bridgedAccessoryCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.accessoryCell, forIndexPath: indexPath)
|
||||
let accessory = bridgedAccessories[indexPath.row]
|
||||
cell.textLabel?.text = accessory.name
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A cell containing the name of a service at
|
||||
a given index path, as well as a localized
|
||||
description of its service type.
|
||||
*/
|
||||
func tableView(tableView: UITableView, serviceCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath)
|
||||
let service = displayedServices[indexPath.row]
|
||||
|
||||
// Inherit the name from the accessory if the Service doesn't have one.
|
||||
cell.textLabel?.text = service.name ?? service.accessory?.name
|
||||
cell.detailTextLabel?.text = service.localizedDescription
|
||||
return cell
|
||||
}
|
||||
|
||||
/// - returns: A title string for the section.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch AccessoryTableViewSection(rawValue: section) {
|
||||
case .Services?:
|
||||
return NSLocalizedString("Services", comment: "Services")
|
||||
|
||||
case .BridgedAccessories?:
|
||||
return NSLocalizedString("Bridged Accessories", comment: "Bridged Accessories")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `AccessoryTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A localized description of the accessories bridged status.
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
if accessory.bridged && AccessoryTableViewSection(rawValue: section)! == .Services {
|
||||
let formatString = NSLocalizedString("This accessory is being bridged into HomeKit by %@.", comment: "Bridge Description")
|
||||
if let bridge = home.bridgeForAccessory(accessory) {
|
||||
return String(format: formatString, bridge.name)
|
||||
}
|
||||
else {
|
||||
return NSLocalizedString("This accessory is being bridged into HomeKit.", comment: "Bridge Description Without Bridge")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Updates the navigation bar's title.
|
||||
func updateTitle() {
|
||||
navigationItem.title = accessory.name
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the title, resets the displayed services based on
|
||||
view controller configurations, reloads the bridge accessory
|
||||
array and reloads the table view.
|
||||
*/
|
||||
private func reloadData() {
|
||||
displayedServices = accessory.services.sortByLocalizedName()
|
||||
if onlyShowsControlServices {
|
||||
// We are configured to only show control services, filter the array.
|
||||
displayedServices = displayedServices.filter { service -> Bool in
|
||||
return service.isControlType
|
||||
}
|
||||
}
|
||||
|
||||
if let identifiers = accessory.uniqueIdentifiersForBridgedAccessories {
|
||||
bridgedAccessories = home.accessoriesWithIdentifiers(identifiers).sortByLocalizedName()
|
||||
}
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
/// Reloads the title based on the accessories new name.
|
||||
func accessoryDidUpdateName(accessory: HMAccessory) {
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
/// Reloads the cell for the specified service.
|
||||
func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
|
||||
if let index = displayedServices.indexOf(service) {
|
||||
let path = NSIndexPath(forRow: index, inSection: AccessoryTableViewSection.Services.rawValue)
|
||||
tableView.reloadRowsAtIndexPaths([path], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the view.
|
||||
func accessoryDidUpdateServices(accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// If our accessory has become unreachable, go back the previous view.
|
||||
func accessoryDidUpdateReachability(accessory: HMAccessory) {
|
||||
if self.accessory == accessory {
|
||||
navigationController?.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ActionCell` displays a characteristic and a target value.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A `UITableViewCell` subclass that displays a characteristic's 'target value'.
|
||||
class ActionCell: UITableViewCell {
|
||||
/// Ignores the passed-in style and overrides it with `.Subtitle`.
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .None
|
||||
detailTextLabel?.textColor = UIColor.lightGrayColor()
|
||||
accessoryType = .None
|
||||
}
|
||||
|
||||
/// Required init.
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the cell's text to represent a characteristic and target value.
|
||||
For example, "Brightness → 60%"
|
||||
Sets the subtitle to the service and accessory that this characteristic represents.
|
||||
|
||||
- parameter characteristic: The characteristic this cell represents.
|
||||
- parameter targetValue: The target value from this action.
|
||||
*/
|
||||
func setCharacteristic(characteristic: HMCharacteristic, targetValue: AnyObject) {
|
||||
let targetDescription = "\(characteristic.localizedDescription) → \(characteristic.localizedDescriptionForValue(targetValue))"
|
||||
textLabel?.text = targetDescription
|
||||
|
||||
let contextDescription = NSLocalizedString("%@ in %@", comment: "Service in Accessory")
|
||||
if let service = characteristic.service, accessory = service.accessory {
|
||||
detailTextLabel?.text = String(format: contextDescription, service.name, accessory.name)
|
||||
}
|
||||
else {
|
||||
detailTextLabel?.text = NSLocalizedString("Unknown Characteristic", comment: "Unknown Characteristic")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ActionSetCreator` builds `HMActionSet`s.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/// A `CharacteristicCellDelegate` that builds an `HMActionSet` when it receives delegate callbacks.
|
||||
class ActionSetCreator: CharacteristicCellDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
var actionSet: HMActionSet?
|
||||
var home: HMHome
|
||||
|
||||
var saveError: NSError?
|
||||
|
||||
/// The structure we're going to use to hold the target values.
|
||||
let targetValueMap = NSMapTable.strongToStrongObjectsMapTable()
|
||||
|
||||
/// A dispatch group to wait for all of the individual components of the saving process.
|
||||
let saveActionSetGroup = dispatch_group_create()
|
||||
|
||||
required init(actionSet: HMActionSet?, home: HMHome) {
|
||||
self.actionSet = actionSet
|
||||
self.home = home
|
||||
}
|
||||
|
||||
/**
|
||||
If there is an action set, saves the action set and then updates its name.
|
||||
Otherwise creates a new action set and adds all actions to it.
|
||||
|
||||
- parameter name: The new name for the action set.
|
||||
- parameter completionHandler: A closure to call once the action set has been completely saved.
|
||||
*/
|
||||
func saveActionSetWithName(name: NSString, completionHandler: (error: NSError?) -> Void) {
|
||||
if let actionSet = actionSet {
|
||||
saveActionSet(actionSet)
|
||||
updateNameIfNecessary(name)
|
||||
}
|
||||
else {
|
||||
createActionSetWithName(name)
|
||||
}
|
||||
dispatch_group_notify(saveActionSetGroup, dispatch_get_main_queue()) {
|
||||
completionHandler(error: self.saveError)
|
||||
self.saveError = nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds all of the actions that have been requested to the Action Set, then runs a completion block.
|
||||
|
||||
- parameter completion: A closure to be called when all of the actions have been added.
|
||||
*/
|
||||
func saveActionSet(actionSet: HMActionSet) {
|
||||
let actions = actionsFromMapTable(targetValueMap)
|
||||
for action in actions {
|
||||
dispatch_group_enter(saveActionSetGroup)
|
||||
addAction(action, toActionSet: actionSet) { error in
|
||||
if let error = error {
|
||||
print("HomeKit: Error adding action: \(error.localizedDescription)")
|
||||
self.saveError = error
|
||||
}
|
||||
dispatch_group_leave(self.saveActionSetGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the name of an existing action set.
|
||||
|
||||
- parameter name: The new name for the action set.
|
||||
*/
|
||||
func updateNameIfNecessary(name: NSString) {
|
||||
if actionSet?.name == name {
|
||||
return
|
||||
}
|
||||
dispatch_group_enter(saveActionSetGroup)
|
||||
actionSet?.updateName(name as String) { error in
|
||||
if let error = error {
|
||||
print("HomeKit: Error updating name: \(error.localizedDescription)")
|
||||
self.saveError = error
|
||||
}
|
||||
dispatch_group_leave(self.saveActionSetGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates and saves an action set with the provided name.
|
||||
|
||||
- parameter name: The name for the new action set.
|
||||
*/
|
||||
func createActionSetWithName(name: NSString) {
|
||||
dispatch_group_enter(saveActionSetGroup)
|
||||
home.addActionSetWithName(name as String) { actionSet, error in
|
||||
if let error = error {
|
||||
print("HomeKit: Error creating action set: \(error.localizedDescription)")
|
||||
self.saveError = error
|
||||
}
|
||||
else {
|
||||
// There is no error, so the action set has a value.
|
||||
self.saveActionSet(actionSet!)
|
||||
}
|
||||
dispatch_group_leave(self.saveActionSetGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks to see if an action already exists to modify the same characteristic
|
||||
as the action passed in. If such an action exists, the method tells the
|
||||
existing action to update its target value. Otherwise, the new action is
|
||||
simply added to the action set.
|
||||
|
||||
- parameter action: The action to add or update.
|
||||
- parameter actionSet: The action set to which to add the action.
|
||||
- parameter completion: A closure to call when the addition has finished.
|
||||
*/
|
||||
func addAction(action: HMCharacteristicWriteAction, toActionSet actionSet: HMActionSet, completion: (NSError?) -> Void) {
|
||||
if let existingAction = existingActionInActionSetMatchingAction(action) {
|
||||
existingAction.updateTargetValue(action.targetValue, completionHandler: completion)
|
||||
}
|
||||
else {
|
||||
actionSet.addAction(action, completionHandler: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks to see if there is already an HMCharacteristicWriteAction in
|
||||
the action set that matches the provided action.
|
||||
|
||||
- parameter action: The action in question.
|
||||
|
||||
- returns: The existing action that matches the characteristic or nil if
|
||||
there is no existing action.
|
||||
*/
|
||||
func existingActionInActionSetMatchingAction(action: HMCharacteristicWriteAction) -> HMCharacteristicWriteAction? {
|
||||
if let actionSet = actionSet {
|
||||
for existingAction in Array(actionSet.actions) as! [HMCharacteristicWriteAction] {
|
||||
if action.characteristic == existingAction.characteristic {
|
||||
return existingAction
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Iterates over a map table of HMCharacteristic -> id objects and creates
|
||||
an array of HMCharacteristicWriteActions based on those targets.
|
||||
|
||||
- parameter table: An NSMapTable mapping HMCharacteristics to id's.
|
||||
|
||||
- returns: An array of HMCharacteristicWriteActions.
|
||||
*/
|
||||
func actionsFromMapTable(table: NSMapTable) -> [HMCharacteristicWriteAction] {
|
||||
return targetValueMap.keyEnumerator().allObjects.map { characteristic in
|
||||
let targetValue = targetValueMap.objectForKey(characteristic) as! NSCopying
|
||||
return HMCharacteristicWriteAction(characteristic: characteristic as! HMCharacteristic, targetValue: targetValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if the characteristic count is greater than zero;
|
||||
`false` otherwise.
|
||||
*/
|
||||
var containsActions: Bool {
|
||||
return !allCharacteristics.isEmpty
|
||||
}
|
||||
|
||||
/**
|
||||
All existing characteristics within `HMCharacteristiWriteActions`
|
||||
and target values in the target value map.
|
||||
*/
|
||||
var allCharacteristics: [HMCharacteristic] {
|
||||
var characteristics = Set<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,334 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ActionSetViewController` allows users to create and modify action sets.
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents table view sections of the `ActionSetViewController`.
|
||||
enum ActionSetTableViewSection: Int {
|
||||
case Name, Actions, Accessories
|
||||
}
|
||||
|
||||
/**
|
||||
A view controller that facilitates creation of Action Sets.
|
||||
|
||||
It contains a cell for a name, and lists accessories within a home.
|
||||
If there are actions within the action set, it also displays a list of ActionCells displaying those actions.
|
||||
It owns an `ActionSetCreator` and routes events to the creator as appropriate.
|
||||
*/
|
||||
class ActionSetViewController: HMCatalogViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let accessoryCell = "AccessoryCell"
|
||||
static let unreachableAccessoryCell = "UnreachableAccessoryCell"
|
||||
static let actionCell = "ActionCell"
|
||||
static let showServiceSegue = "Show Services"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var nameField: UITextField!
|
||||
@IBOutlet weak var saveButton: UIBarButtonItem!
|
||||
|
||||
var actionSet: HMActionSet?
|
||||
var actionSetCreator: ActionSetCreator!
|
||||
var displayedAccessories = [HMAccessory]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/**
|
||||
Creates the action set creator, registers the appropriate reuse identifiers in the table,
|
||||
and sets the `nameField` if appropriate.
|
||||
*/
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
actionSetCreator = ActionSetCreator(actionSet: actionSet, home: home)
|
||||
displayedAccessories = home.sortedControlAccessories
|
||||
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.accessoryCell)
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.unreachableAccessoryCell)
|
||||
tableView.registerClass(ActionCell.self, forCellReuseIdentifier: Identifiers.actionCell)
|
||||
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
|
||||
if let actionSet = actionSet {
|
||||
nameField.text = actionSet.name
|
||||
nameFieldDidChange(nameField)
|
||||
}
|
||||
|
||||
if !home.isAdmin {
|
||||
nameField.enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the data and view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
tableView.reloadData()
|
||||
enableSaveButtonIfNecessary()
|
||||
}
|
||||
|
||||
/// Dismisses the view controller if our data is invalid.
|
||||
override func viewDidAppear(animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if shouldPopViewController() {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismisses the keyboard when we dismiss.
|
||||
override func viewWillDisappear(animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
resignFirstResponder()
|
||||
}
|
||||
|
||||
/// Passes our accessory into the `ServicesViewController`.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
if segue.identifier == Identifiers.showServiceSegue {
|
||||
let servicesViewController = segue.intendedDestinationViewController as! ServicesViewController
|
||||
servicesViewController.onlyShowsControlServices = true
|
||||
servicesViewController.cellDelegate = actionSetCreator
|
||||
|
||||
let index = tableView.indexPathForCell(sender as! UITableViewCell)!.row
|
||||
|
||||
servicesViewController.accessory = displayedAccessories[index]
|
||||
servicesViewController.cellDelegate = actionSetCreator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/// Dismisses the view controller.
|
||||
@IBAction func dismiss() {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
/// Saves the action set, adds it to the home, and dismisses the view.
|
||||
@IBAction func saveAndDismiss() {
|
||||
saveButton.enabled = false
|
||||
|
||||
actionSetCreator.saveActionSetWithName(trimmedName) { error in
|
||||
self.saveButton.enabled = true
|
||||
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
else {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prompts an update to the save button enabled state.
|
||||
@IBAction func nameFieldDidChange(field: UITextField) {
|
||||
enableSaveButtonIfNecessary()
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// We do not allow the creation of action sets in a shared home.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return home.isAdmin ? 3 : 2
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: In the Actions section: the number of actions this set will contain upon saving.
|
||||
In the Accessories section: The number of accessories in the home.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch ActionSetTableViewSection(rawValue: section) {
|
||||
case .Name?:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
|
||||
case .Actions?:
|
||||
return max(actionSetCreator.allCharacteristics.count, 1)
|
||||
|
||||
case .Accessories?:
|
||||
return displayedAccessories.count
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `ActionSetTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Required override to allow for a tableView with both static and dynamic content.
|
||||
Basically, since the superclass's indentationLevelForRowAtIndexPath is only
|
||||
expecting 1 row per section, just call the super class's implementation
|
||||
for the first row.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
|
||||
return super.tableView(tableView, indentationLevelForRowAtIndexPath: NSIndexPath(forRow: 0, inSection: indexPath.section))
|
||||
}
|
||||
|
||||
/// Removes the action associated with the index path.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
let characteristic = actionSetCreator.allCharacteristics[indexPath.row]
|
||||
actionSetCreator.removeTargetValueForCharacteristic(characteristic) {
|
||||
if self.actionSetCreator.containsActions {
|
||||
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
else {
|
||||
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: `true` for the Actions section; `false` otherwise.
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
return ActionSetTableViewSection(rawValue: indexPath.section) == .Actions && home.isAdmin
|
||||
}
|
||||
|
||||
/// - returns: `UITableViewAutomaticDimension` for dynamic sections, otherwise the superclass's implementation.
|
||||
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
|
||||
switch ActionSetTableViewSection(rawValue: indexPath.section) {
|
||||
case .Name?:
|
||||
return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
|
||||
|
||||
case .Actions?, .Accessories?:
|
||||
return UITableViewAutomaticDimension
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `ActionSetTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: An action cell for the actions section, an accessory cell for the accessory section, or the superclass's implementation.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch ActionSetTableViewSection(rawValue: indexPath.section) {
|
||||
case .Name?:
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .Actions?:
|
||||
if actionSetCreator.containsActions {
|
||||
return self.tableView(tableView, actionCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
else {
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
|
||||
case .Accessories?:
|
||||
return self.tableView(tableView, accessoryCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `ActionSetTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Enables the save button if there is a valid name and at least one action.
|
||||
private func enableSaveButtonIfNecessary() {
|
||||
saveButton.enabled = home.isAdmin && trimmedName.characters.count > 0 && actionSetCreator.containsActions
|
||||
}
|
||||
|
||||
/// - returns: The contents of the nameField, with whitespace trimmed from the beginning and end.
|
||||
private var trimmedName: String {
|
||||
return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if there are no accessories in the home, we have no set action set,
|
||||
or if our home no longer exists; `false` otherwise
|
||||
*/
|
||||
private func shouldPopViewController() -> Bool {
|
||||
if homeStore.home?.accessories.count == 0 && actionSet == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return !homeStore.homeManager.homes.contains { $0 == homeStore.home }
|
||||
}
|
||||
|
||||
/// - returns: An `ActionCell` instance with the target value for the characteristic at the specified index path.
|
||||
private func tableView(tableView: UITableView, actionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.actionCell, forIndexPath: indexPath) as! ActionCell
|
||||
let characteristic = actionSetCreator.allCharacteristics[indexPath.row] as HMCharacteristic
|
||||
|
||||
if let target = actionSetCreator.targetValueForCharacteristic(characteristic) {
|
||||
cell.setCharacteristic(characteristic, targetValue: target)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// - returns: An Accessory cell that contains an accessory's name.
|
||||
private func tableView(tableView: UITableView, accessoryCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
/*
|
||||
These cells are static, the identifiers are defined in the Storyboard,
|
||||
but they're not recognized here. In viewDidLoad:, we're registering
|
||||
`UITableViewCell` as the class for "AccessoryCell" and "UnreachableAccessoryCell".
|
||||
We must configure these cells manually, the cells in the Storyboard
|
||||
are just for reference.
|
||||
*/
|
||||
|
||||
let accessory = displayedAccessories[indexPath.row]
|
||||
let cellIdentifier = accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
|
||||
cell.textLabel?.text = accessory.name
|
||||
|
||||
if accessory.reachable {
|
||||
cell.textLabel?.textColor = UIColor.darkTextColor()
|
||||
cell.accessoryType = .DisclosureIndicator
|
||||
cell.selectionStyle = .Default
|
||||
}
|
||||
else {
|
||||
cell.textLabel?.textColor = UIColor.lightGrayColor()
|
||||
cell.accessoryType = .None
|
||||
cell.selectionStyle = .None
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Shows the services in the selected accessory.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath)!
|
||||
if cell.selectionStyle == .None {
|
||||
return
|
||||
}
|
||||
|
||||
if ActionSetTableViewSection(rawValue: indexPath.section) == .Accessories {
|
||||
performSegueWithIdentifier(Identifiers.showServiceSegue, sender: cell)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/**
|
||||
Pops the view controller if our configuration is invalid;
|
||||
reloads the view otherwise.
|
||||
*/
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
if shouldPopViewController() {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
else {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the table view data.
|
||||
func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// If our action set was removed, dismiss the view.
|
||||
func home(home: HMHome, didRemoveActionSet actionSet: HMActionSet) {
|
||||
if actionSet == self.actionSet {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `HomeKitObjectCollection` is a model object for the `HomeViewController`.
|
||||
It manages arrays of HomeKit objects.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/// Represents the all different types of HomeKit objects.
|
||||
enum HomeKitObjectSection: Int {
|
||||
case Accessory, Room, Zone, User, ActionSet, Trigger, ServiceGroup
|
||||
|
||||
static let count = 7
|
||||
}
|
||||
|
||||
/**
|
||||
Manages internal lists of HomeKit objects to allow for
|
||||
save insertion into a table view.
|
||||
*/
|
||||
class HomeKitObjectCollection {
|
||||
// MARK: Properties
|
||||
|
||||
var accessories = [HMAccessory]()
|
||||
var rooms = [HMRoom]()
|
||||
var zones = [HMZone]()
|
||||
var actionSets = [HMActionSet]()
|
||||
var triggers = [HMTrigger]()
|
||||
var serviceGroups = [HMServiceGroup]()
|
||||
|
||||
/**
|
||||
Adds an object to the collection by finding its corresponding
|
||||
array and appending the object to it.
|
||||
|
||||
- parameter object: The HomeKit object to append.
|
||||
*/
|
||||
func append(object: AnyObject) {
|
||||
switch object {
|
||||
case let actionSet as HMActionSet:
|
||||
actionSets.append(actionSet)
|
||||
actionSets = actionSets.sortByTypeAndLocalizedName()
|
||||
|
||||
case let accessory as HMAccessory:
|
||||
accessories.append(accessory)
|
||||
accessories = accessories.sortByLocalizedName()
|
||||
|
||||
case let room as HMRoom:
|
||||
rooms.append(room)
|
||||
rooms = rooms.sortByLocalizedName()
|
||||
|
||||
case let zone as HMZone:
|
||||
zones.append(zone)
|
||||
zones = zones.sortByLocalizedName()
|
||||
|
||||
case let trigger as HMTrigger:
|
||||
triggers.append(trigger)
|
||||
triggers = triggers.sortByLocalizedName()
|
||||
|
||||
case let serviceGroup as HMServiceGroup:
|
||||
serviceGroups.append(serviceGroup)
|
||||
serviceGroups = serviceGroups.sortByLocalizedName()
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an `NSIndexPath` representing the location of the
|
||||
HomeKit object in the table view.
|
||||
|
||||
- parameter object: The HomeKit object to find.
|
||||
|
||||
- returns: The `NSIndexPath` representing the location of
|
||||
the HomeKit object in the table view.
|
||||
*/
|
||||
func indexPathOfObject(object: AnyObject) -> NSIndexPath? {
|
||||
switch object {
|
||||
case let actionSet as HMActionSet:
|
||||
if let index = actionSets.indexOf(actionSet) {
|
||||
return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.ActionSet.rawValue)
|
||||
}
|
||||
|
||||
case let accessory as HMAccessory:
|
||||
if let index = accessories.indexOf(accessory) {
|
||||
return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Accessory.rawValue)
|
||||
}
|
||||
|
||||
case let room as HMRoom:
|
||||
if let index = rooms.indexOf(room) {
|
||||
return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Room.rawValue)
|
||||
}
|
||||
|
||||
case let zone as HMZone:
|
||||
if let index = zones.indexOf(zone) {
|
||||
return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Zone.rawValue)
|
||||
}
|
||||
|
||||
case let trigger as HMTrigger:
|
||||
if let index = triggers.indexOf(trigger) {
|
||||
return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Trigger.rawValue)
|
||||
}
|
||||
|
||||
case let serviceGroup as HMServiceGroup:
|
||||
if let index = serviceGroups.indexOf(serviceGroup) {
|
||||
return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.ServiceGroup.rawValue)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a HomeKit object from the collection.
|
||||
|
||||
- parameter object: The HomeKit object to remove.
|
||||
*/
|
||||
func remove(object: AnyObject) {
|
||||
switch object {
|
||||
case let actionSet as HMActionSet:
|
||||
if let index = actionSets.indexOf(actionSet) {
|
||||
actionSets.removeAtIndex(index)
|
||||
}
|
||||
|
||||
case let accessory as HMAccessory:
|
||||
if let index = accessories.indexOf(accessory) {
|
||||
accessories.removeAtIndex(index)
|
||||
}
|
||||
|
||||
case let room as HMRoom:
|
||||
if let index = rooms.indexOf(room) {
|
||||
rooms.removeAtIndex(index)
|
||||
}
|
||||
|
||||
case let zone as HMZone:
|
||||
if let index = zones.indexOf(zone) {
|
||||
zones.removeAtIndex(index)
|
||||
}
|
||||
|
||||
case let trigger as HMTrigger:
|
||||
if let index = triggers.indexOf(trigger) {
|
||||
triggers.removeAtIndex(index)
|
||||
}
|
||||
|
||||
case let serviceGroup as HMServiceGroup:
|
||||
if let index = serviceGroups.indexOf(serviceGroup) {
|
||||
serviceGroups.removeAtIndex(index)
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Provides the array of `NSObject`s corresponding to the provided section.
|
||||
|
||||
- parameter section: A `HomeKitObjectSection`.
|
||||
|
||||
- returns: An array of `NSObject`s corresponding to the provided section.
|
||||
*/
|
||||
func objectsForSection(section: HomeKitObjectSection) -> [NSObject] {
|
||||
switch section {
|
||||
case .ActionSet:
|
||||
return actionSets
|
||||
|
||||
case .Accessory:
|
||||
return accessories
|
||||
|
||||
case .Room:
|
||||
return rooms
|
||||
|
||||
case .Zone:
|
||||
return zones
|
||||
|
||||
case .Trigger:
|
||||
return triggers
|
||||
|
||||
case .ServiceGroup:
|
||||
return serviceGroups
|
||||
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Provides an `HomeKitObjectSection` for a given object.
|
||||
|
||||
- parameter object: A HomeKit object.
|
||||
|
||||
- returns: The corrosponding `HomeKitObjectSection`
|
||||
*/
|
||||
class func sectionForObject(object: AnyObject?) -> HomeKitObjectSection? {
|
||||
switch object {
|
||||
case is HMActionSet:
|
||||
return .ActionSet
|
||||
|
||||
case is HMAccessory:
|
||||
return .Accessory
|
||||
|
||||
case is HMZone:
|
||||
return .Zone
|
||||
|
||||
case is HMRoom:
|
||||
return .Room
|
||||
|
||||
case is HMTrigger:
|
||||
return .Trigger
|
||||
|
||||
case is HMServiceGroup:
|
||||
return .ServiceGroup
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Reloads all internal structures based on the provided home.
|
||||
|
||||
- parameter home: The `HMHome` with which to reset the collection.
|
||||
*/
|
||||
func resetWithHome(home: HMHome) {
|
||||
accessories = home.accessories.sortByLocalizedName()
|
||||
rooms = home.allRooms
|
||||
zones = home.zones.sortByLocalizedName()
|
||||
actionSets = home.actionSets.sortByTypeAndLocalizedName()
|
||||
triggers = home.triggers.sortByLocalizedName()
|
||||
serviceGroups = home.serviceGroups.sortByLocalizedName()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `HomeListConfigurationViewController` allows for the creation and deletion of homes.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
// Represents the sections in the `HomeListConfigurationViewController`.
|
||||
enum HomeListSection: Int {
|
||||
case Homes, PrimaryHome
|
||||
|
||||
static let count = 2
|
||||
}
|
||||
|
||||
/**
|
||||
A `HomeListViewController` subclass which allows the user to add and remove
|
||||
homes and set the primary home.
|
||||
*/
|
||||
class HomeListConfigurationViewController: HomeListViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let addHomeCell = "AddHomeCell"
|
||||
static let noHomesCell = "NoHomesCell"
|
||||
static let primaryHomeCell = "PrimaryHomeCell"
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of sections in the `HomeListSection` enum.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return HomeListSection.count
|
||||
}
|
||||
|
||||
/// Provides the number of rows in the section using the internal home's list.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch HomeListSection(rawValue: section) {
|
||||
// Add row.
|
||||
case .Homes?:
|
||||
return homes.count + 1
|
||||
|
||||
// 'No homes' row.
|
||||
case .PrimaryHome?:
|
||||
return max(homes.count, 1)
|
||||
|
||||
case nil: fatalError("Unexpected `HomeListSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generates and configures either a content cell or an add cell using the
|
||||
provided index path.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
if indexPathIsAdd(indexPath) {
|
||||
return tableView.dequeueReusableCellWithIdentifier(Identifiers.addHomeCell, forIndexPath: indexPath)
|
||||
}
|
||||
else if homes.isEmpty {
|
||||
return tableView.dequeueReusableCellWithIdentifier(Identifiers.noHomesCell, forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
let reuseIdentifier: String
|
||||
|
||||
switch HomeListSection(rawValue: indexPath.section) {
|
||||
case .Homes?:
|
||||
reuseIdentifier = Identifiers.homeCell
|
||||
|
||||
case .PrimaryHome?:
|
||||
reuseIdentifier = Identifiers.primaryHomeCell
|
||||
|
||||
case nil: fatalError("Unexpected `HomeListSection` raw value.")
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
|
||||
let home = homes[indexPath.row]
|
||||
|
||||
cell.textLabel!.text = home.name
|
||||
cell.detailTextLabel?.text = sharedTextForHome(home)
|
||||
|
||||
// Mark the primary home with checkmark.
|
||||
if HomeListSection(rawValue: indexPath.section) == .PrimaryHome {
|
||||
if home == homeManager.primaryHome {
|
||||
cell.accessoryType = .Checkmark
|
||||
}
|
||||
else {
|
||||
cell.accessoryType = .None
|
||||
}
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Homes in the list section can be deleted. The add row cannot be deleted.
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
return HomeListSection(rawValue: indexPath.section) == .Homes && !indexPathIsAdd(indexPath)
|
||||
}
|
||||
|
||||
/// Only the 'primary home' section has a title.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
if HomeListSection(rawValue: section) == .PrimaryHome {
|
||||
return NSLocalizedString("Primary Home", comment: "Primary Home")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Provides subtext about the use of designating a "primary home".
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
if section == HomeListSection.PrimaryHome.rawValue {
|
||||
return NSLocalizedString("The primary home is used by Siri to route commands if the home is not specified.", comment: "Primary Home Description")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
If selecting a regular home, a segue will be performed.
|
||||
If this method is called, the user either selected the 'add' row,
|
||||
a primary home cell, or the `No Homes` cell.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
if indexPathIsAdd(indexPath) {
|
||||
addNewHome()
|
||||
}
|
||||
else if indexPathIsNone(indexPath) {
|
||||
return
|
||||
}
|
||||
else if HomeListSection(rawValue: indexPath.section) == .PrimaryHome {
|
||||
let newPrimaryHome = homes[indexPath.row]
|
||||
updatePrimaryHome(newPrimaryHome)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the home from HomeKit if the row is deleted.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
removeHomeAtIndexPath(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Updates the primary home in HomeKit and reloads the view.
|
||||
If the home is already selected, no action is taken.
|
||||
|
||||
- parameter newPrimaryHome: The new `HMHome` to set as the primary home.
|
||||
*/
|
||||
private func updatePrimaryHome(newPrimaryHome: HMHome) {
|
||||
guard newPrimaryHome != homeManager.primaryHome else { return }
|
||||
|
||||
homeManager.updatePrimaryHome(newPrimaryHome) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
|
||||
self.didUpdatePrimaryHome()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the 'primary home' section.
|
||||
private func didUpdatePrimaryHome() {
|
||||
let primaryIndexSet = NSIndexSet(index: HomeListSection.PrimaryHome.rawValue)
|
||||
|
||||
tableView.reloadSections(primaryIndexSet, withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/**
|
||||
Removed the home at the specified index path from HomeKit and updates the view.
|
||||
|
||||
- parameter indexPath: The `NSIndexPath` of the home to remove.
|
||||
*/
|
||||
private func removeHomeAtIndexPath(indexPath: NSIndexPath) {
|
||||
let home = homes[indexPath.row]
|
||||
|
||||
// Remove the home from the data structure. If it fails, put it back.
|
||||
didRemoveHome(home)
|
||||
homeManager.removeHome(home) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
self.didAddHome(home)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Presents an alert controller so the user can provide a name. If committed,
|
||||
the home is created.
|
||||
*/
|
||||
private func addNewHome() {
|
||||
let attributedType = NSLocalizedString("Home", comment: "Home")
|
||||
let placeholder = NSLocalizedString("Apartment", comment: "Apartment")
|
||||
|
||||
presentAddAlertWithAttributeType(attributedType, placeholder: placeholder) { name in
|
||||
self.addHomeWithName(name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a home from the internal structure and updates the view.
|
||||
|
||||
- parameter home: The `HMHome` to remove.
|
||||
*/
|
||||
override func didRemoveHome(home: HMHome) {
|
||||
guard let index = homes.indexOf(home) else { return }
|
||||
|
||||
let indexPath = NSIndexPath(forRow: index, inSection: HomeListSection.Homes.rawValue)
|
||||
homes.removeAtIndex(index)
|
||||
let primaryIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.PrimaryHome.rawValue)
|
||||
|
||||
/*
|
||||
If there aren't any homes, we still want one cell to display 'No Homes'.
|
||||
Just reload.
|
||||
*/
|
||||
tableView.beginUpdates()
|
||||
if homes.isEmpty {
|
||||
tableView.reloadRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Fade)
|
||||
}
|
||||
else {
|
||||
tableView.deleteRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
tableView.endUpdates()
|
||||
|
||||
}
|
||||
|
||||
/// Adds the home to the internal structure and updates the view.
|
||||
override func didAddHome(home: HMHome) {
|
||||
homes.append(home)
|
||||
sortHomes()
|
||||
guard let newHomeIndex = homes.indexOf(home) else { return }
|
||||
|
||||
let indexPath = NSIndexPath(forRow: newHomeIndex, inSection: HomeListSection.Homes.rawValue)
|
||||
|
||||
let primaryIndexPath = NSIndexPath(forRow: newHomeIndex, inSection: HomeListSection.PrimaryHome.rawValue)
|
||||
|
||||
tableView.beginUpdates()
|
||||
|
||||
if homes.count == 1 {
|
||||
tableView.reloadRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Fade)
|
||||
}
|
||||
else {
|
||||
tableView.insertRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
tableView.endUpdates()
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new home with the provided name, adds the home to HomeKit
|
||||
and reloads the view.
|
||||
*/
|
||||
private func addHomeWithName(name: String) {
|
||||
homeManager.addHomeWithName(name) { newHome, error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
|
||||
self.didAddHome(newHome!)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// - returns: `true` if the index path is the 'add row'; `false` otherwise.
|
||||
private func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
|
||||
return HomeListSection(rawValue: indexPath.section) == .Homes &&
|
||||
indexPath.row == homes.count
|
||||
}
|
||||
|
||||
/// - returns: `true` if the index path is the 'No Homes' cell; `false` otherwise.
|
||||
private func indexPathIsNone(indexPath: NSIndexPath) -> Bool {
|
||||
return HomeListSection(rawValue: indexPath.section) == .PrimaryHome && homes.isEmpty
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// Finds the home in the internal structure and reloads the corresponding row.
|
||||
override func homeDidUpdateName(home: HMHome) {
|
||||
if let index = homes.indexOf(home) {
|
||||
let listIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.Homes.rawValue)
|
||||
|
||||
let primaryIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.PrimaryHome.rawValue)
|
||||
|
||||
tableView.reloadRowsAtIndexPaths([listIndexPath, primaryIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
else {
|
||||
// Just reload the data since we don't know the index path.
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeManagerDelegate Methods
|
||||
|
||||
/// Reloads the 'primary home' section.
|
||||
func homeManagerDidUpdatePrimaryHome(manager: HMHomeManager) {
|
||||
didUpdatePrimaryHome()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `HomeListViewController` is a superclass that lists the user's homes.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
|
||||
/// A generic view controller for displaying a list of homes in a home manager.
|
||||
class HomeListViewController: HMCatalogViewController, HMHomeManagerDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let homeCell = "HomeCell"
|
||||
static let showHomeSegue = "Show Home"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var homes = [HMHome]()
|
||||
|
||||
var homeManager: HMHomeManager {
|
||||
return homeStore.homeManager
|
||||
}
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the table view.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
}
|
||||
|
||||
/// Resets the list of homes (which will update the view).
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
resetHomesList()
|
||||
}
|
||||
|
||||
// MARK: Delegate Registration
|
||||
|
||||
/**
|
||||
Registers as the delegate for the home manager and all homes in the internal
|
||||
homes list.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
homeManager.delegate = self
|
||||
|
||||
for home in homes {
|
||||
home.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the home store's current home based on which cell was selected.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
|
||||
if segue.identifier == Identifiers.showHomeSegue {
|
||||
if sender === self {
|
||||
// Don't update the selected home if we sent ourselves here.
|
||||
return
|
||||
}
|
||||
|
||||
if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) {
|
||||
homeStore.home = homes[indexPath.row]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
Provides the number of sections based on the home array count.
|
||||
Updates the background message for the table view.
|
||||
|
||||
- returns: The number of homes in the internal array.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let rows = homes.count
|
||||
|
||||
if rows == 0 {
|
||||
let message = NSLocalizedString("No Homes", comment: "No Homes")
|
||||
setBackgroundMessage(message)
|
||||
}
|
||||
else {
|
||||
setBackgroundMessage(nil)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
Generates a basic cell for a home.
|
||||
Subtext is provided to tell the user if the home is shared or owned by the user.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.homeCell, forIndexPath: indexPath)
|
||||
let home = homes[indexPath.row]
|
||||
|
||||
cell.textLabel?.text = home.name
|
||||
cell.detailTextLabel?.text = sharedTextForHome(home)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Provides an ordering for homes.
|
||||
|
||||
Homes are first ordered by their 'shared' status, then by name.
|
||||
|
||||
- parameter home1: The first `HMHome`.
|
||||
- parameter home2: The second `HMHome`.
|
||||
|
||||
- returns: `true` if `home1` is ordered before `home2`; `false` otherwise.
|
||||
*/
|
||||
private func orderHomes(home1: HMHome, home2: HMHome) -> Bool {
|
||||
if home1.isAdmin == home2.isAdmin {
|
||||
/*
|
||||
We are comparing two shared homes or two of our homes, just compare
|
||||
names.
|
||||
*/
|
||||
return home1.name.localizedCompare(home2.name) == .OrderedAscending
|
||||
}
|
||||
else {
|
||||
/*
|
||||
We are comparing a shared home and one of our homes, if home1 is
|
||||
ours, put it first.
|
||||
*/
|
||||
return home1.isAdmin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Regenerates the list of homes using list provided by the home manager.
|
||||
The list is then sorted and the view is reloaded.
|
||||
*/
|
||||
private func resetHomesList() {
|
||||
homes = homeManager.homes.sort(orderHomes)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// Sorts the list of homes (without reloading from the home manager).
|
||||
func sortHomes() {
|
||||
homes.sortInPlace(orderHomes)
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a new home into the internal homes array and inserts the new
|
||||
row into the table view.
|
||||
|
||||
- parameter home: The new `HMHome` that's been added.
|
||||
*/
|
||||
func didAddHome(home: HMHome) {
|
||||
homes.append(home)
|
||||
|
||||
sortHomes()
|
||||
|
||||
if let newHomeIndex = homes.indexOf(home) {
|
||||
let indexPathOfNewHome = NSIndexPath(forRow: newHomeIndex, inSection: 0)
|
||||
|
||||
tableView.insertRowsAtIndexPaths([indexPathOfNewHome], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a home from the internal homes array (if it exists) and
|
||||
deletes corresponding row from the table view.
|
||||
|
||||
- parameter home: The `HMHome` to remove.
|
||||
*/
|
||||
func didRemoveHome(home: HMHome) {
|
||||
guard let removedHomeIndex = homes.indexOf(home) else { return }
|
||||
|
||||
homes.removeAtIndex(removedHomeIndex)
|
||||
let indexPath = NSIndexPath(forRow: removedHomeIndex, inSection: 0)
|
||||
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A localized description of who owns the provided home.
|
||||
|
||||
- parameter home: The `HMHome` to describe.
|
||||
*/
|
||||
func sharedTextForHome(home: HMHome) -> String {
|
||||
if !home.isAdmin {
|
||||
return NSLocalizedString("Shared with Me", comment: "Shared with Me")
|
||||
}
|
||||
else {
|
||||
return NSLocalizedString("My Home", comment: "My Home")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// Finds the cell with corresponds to the provided home and reloads it.
|
||||
func homeDidUpdateName(home: HMHome) {
|
||||
if let homeIndex = homes.indexOf(home) {
|
||||
let indexPath = NSIndexPath(forRow: homeIndex, inSection: 0)
|
||||
|
||||
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeManagerDelegate Methods
|
||||
|
||||
/**
|
||||
Reloads data and view.
|
||||
|
||||
This view controller, in most cases, will remain the home manager delegate.
|
||||
For this reason, this method will close all modal views and pop all detail views
|
||||
if the home store's current home is no longer in the home manager's list of homes.
|
||||
*/
|
||||
func homeManagerDidUpdateHomes(manager: HMHomeManager) {
|
||||
registerAsDelegate()
|
||||
resetHomesList()
|
||||
|
||||
if let home = homeStore.home where !manager.homes.contains(home) {
|
||||
// Close all modal and detail views.
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
navigationController?.popToRootViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers for the delegate of the new home and updates the view.
|
||||
func homeManager(manager: HMHomeManager, didAddHome home: HMHome) {
|
||||
home.delegate = self
|
||||
|
||||
didAddHome(home)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the home from the current list of homes and updates the view.
|
||||
|
||||
If the removed home was the current home, this view controller will dismiss
|
||||
all modals views and pop all detail views.
|
||||
*/
|
||||
func homeManager(manager: HMHomeManager, didRemoveHome home: HMHome) {
|
||||
didRemoveHome(home)
|
||||
|
||||
guard let selectedHome = homeStore.home where home == selectedHome else { return }
|
||||
|
||||
homeStore.home = nil
|
||||
|
||||
// Close all modal and detail views.
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
navigationController?.popToRootViewControllerAnimated(true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `HomeStore` class is a simple singleton class which holds a home manager and the current selected home.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/// A static, singleton class which holds a home manager and the current home.
|
||||
class HomeStore: NSObject, HMHomeManagerDelegate {
|
||||
static let sharedStore = HomeStore()
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The current 'selected' home.
|
||||
var home: HMHome?
|
||||
|
||||
/// The singleton home manager.
|
||||
var homeManager = HMHomeManager()
|
||||
}
|
|
@ -0,0 +1,929 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `HomeViewController` displays all of the HomeKit objects in a selected home.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Distinguishes between the three types of cells in the `HomeViewController`.
|
||||
enum HomeCellType {
|
||||
/// Represents an actual object in HomeKit.
|
||||
case Object
|
||||
|
||||
/// Represents an "Add" row for users to select to create an object in HomeKit.
|
||||
case Add
|
||||
|
||||
/// The cell is displaying text to show the user that no objects exist in this section.
|
||||
case None
|
||||
}
|
||||
|
||||
/**
|
||||
A view controller that displays all elements within a home.
|
||||
It contains separate sections for Accessories, Rooms, Zones, Action Sets,
|
||||
Triggers, and Service Groups.
|
||||
*/
|
||||
class HomeViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let addCell = "AddCell"
|
||||
static let disabledAddCell = "DisabledAddCell"
|
||||
static let accessoryCell = "AccessoryCell"
|
||||
static let unreachableAccessoryCell = "UnreachableAccessoryCell"
|
||||
static let roomCell = "RoomCell"
|
||||
static let zoneCell = "ZoneCell"
|
||||
static let userCell = "UserCell"
|
||||
static let actionSetCell = "ActionSetCell"
|
||||
static let triggerCell = "TriggerCell"
|
||||
static let serviceGroupCell = "ServiceGroupCell"
|
||||
static let addTimerTriggerSegue = "Add Timer Trigger"
|
||||
static let addCharacteristicTriggerSegue = "Add Characteristic Trigger"
|
||||
static let addLocationTriggerSegue = "Add Location Trigger"
|
||||
static let addActionSetSegue = "Add Action Set"
|
||||
static let addAccessoriesSegue = "Add Accessories"
|
||||
static let showRoomSegue = "Show Room"
|
||||
static let showZoneSegue = "Show Zone"
|
||||
static let showActionSetSegue = "Show Action Set"
|
||||
static let showServiceGroupSegue = "Show Service Group"
|
||||
static let showAccessorySegue = "Show Accessory"
|
||||
static let modifyAccessorySegue = "Modify Accessory"
|
||||
static let showTimerTriggerSegue = "Show Timer Trigger"
|
||||
static let showLocationTriggerSegue = "Show Location Trigger"
|
||||
static let showCharacteristicTriggerSegue = "Show Characteristic Trigger"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// A structure to maintain internal arrays of HomeKit objects.
|
||||
private var objectCollection = HomeKitObjectCollection()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/**
|
||||
Determines the destination of the segue and passes the correct
|
||||
HomeKit object onto the next view controller.
|
||||
*/
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
guard let sender = sender as? UITableViewCell else { return }
|
||||
guard let indexPath = tableView.indexPathForCell(sender) else { return }
|
||||
|
||||
let homeKitObject = homeKitObjectAtIndexPath(indexPath)
|
||||
let destinationViewController = segue.intendedDestinationViewController
|
||||
|
||||
switch segue.identifier! {
|
||||
case Identifiers.showRoomSegue:
|
||||
let roomVC = destinationViewController as! RoomViewController
|
||||
roomVC.room = homeKitObject as? HMRoom
|
||||
|
||||
case Identifiers.showZoneSegue:
|
||||
let zoneViewController = destinationViewController as! ZoneViewController
|
||||
zoneViewController.homeZone = homeKitObject as? HMZone
|
||||
|
||||
case Identifiers.showActionSetSegue:
|
||||
let actionSetVC = destinationViewController as! ActionSetViewController
|
||||
actionSetVC.actionSet = homeKitObject as? HMActionSet
|
||||
|
||||
case Identifiers.showServiceGroupSegue:
|
||||
let serviceGroupVC = destinationViewController as! ServiceGroupViewController
|
||||
serviceGroupVC.serviceGroup = homeKitObject as? HMServiceGroup
|
||||
|
||||
case Identifiers.showAccessorySegue:
|
||||
let detailVC = destinationViewController as! ServicesViewController
|
||||
/*
|
||||
The services view controller is generic, we need to provide
|
||||
`showsFavorites` to display the stars next to characteristics.
|
||||
*/
|
||||
detailVC.accessory = homeKitObject as? HMAccessory
|
||||
detailVC.showsFavorites = true
|
||||
detailVC.cellDelegate = AccessoryUpdateController()
|
||||
|
||||
case Identifiers.modifyAccessorySegue:
|
||||
let addAccessoryVC = destinationViewController as! ModifyAccessoryViewController
|
||||
addAccessoryVC.accessory = homeKitObject as? HMAccessory
|
||||
|
||||
case Identifiers.showTimerTriggerSegue:
|
||||
let triggerVC = destinationViewController as! TimerTriggerViewController
|
||||
triggerVC.trigger = homeKitObject as? HMTimerTrigger
|
||||
|
||||
case Identifiers.showLocationTriggerSegue:
|
||||
let triggerVC = destinationViewController as! LocationTriggerViewController
|
||||
triggerVC.trigger = homeKitObject as? HMEventTrigger
|
||||
|
||||
case Identifiers.showCharacteristicTriggerSegue:
|
||||
let triggerVC = destinationViewController as! CharacteristicTriggerViewController
|
||||
triggerVC.trigger = homeKitObject as? HMEventTrigger
|
||||
|
||||
default:
|
||||
print("Received unknown segue identifier: \(segue.identifier).")
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures the table view.
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
}
|
||||
|
||||
/// Sets the navigation title and reloads view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationItem.title = home.name
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
// MARK: Delegate Registration
|
||||
|
||||
/**
|
||||
Registers as the delegate for the home store's current home
|
||||
and all accessories in the home.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
|
||||
for accessory in home.accessories {
|
||||
accessory.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Resets the object collection and reloads the view.
|
||||
private func reloadTable() {
|
||||
objectCollection.resetWithHome(home)
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/**
|
||||
Determines the type of the cell based on the index path.
|
||||
|
||||
- parameter indexPath: The `NSIndexPath` of the cell.
|
||||
|
||||
- returns: The `HomeCellType` for cell.
|
||||
*/
|
||||
private func cellTypeForIndexPath(indexPath: NSIndexPath) -> HomeCellType {
|
||||
guard let section = HomeKitObjectSection(rawValue: indexPath.section) else { return .None }
|
||||
|
||||
let objectCount = objectCollection.objectsForSection(section).count
|
||||
|
||||
if objectCount == 0 {
|
||||
// No objects -- this is either an 'Add Row' or a 'None Row'.
|
||||
return home.isAdmin ? .Add : .None
|
||||
}
|
||||
else if indexPath.row == objectCount {
|
||||
return .Add
|
||||
}
|
||||
else {
|
||||
return .Object
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the trigger section.
|
||||
private func updateTriggerAddRow() {
|
||||
let triggerSection = NSIndexSet(index: HomeKitObjectSection.Trigger.rawValue)
|
||||
|
||||
tableView.reloadSections(triggerSection, withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/// Reloads the action set section.
|
||||
private func updateActionSetSection() {
|
||||
let actionSetSection = NSIndexSet(index: HomeKitObjectSection.ActionSet.rawValue)
|
||||
|
||||
tableView.reloadSections(actionSetSection, withRowAnimation: .Automatic)
|
||||
|
||||
updateTriggerAddRow()
|
||||
}
|
||||
|
||||
/// - returns: `true` if there are accessories within the home; `false` otherwise.
|
||||
private var canAddActionSet: Bool {
|
||||
return !objectCollection.accessories.isEmpty
|
||||
}
|
||||
|
||||
/// - returns: `true` if there are action sets (with actions) within the home; `false` otherwise.
|
||||
private var canAddTrigger: Bool {
|
||||
return objectCollection.actionSets.contains { actionSet in
|
||||
return !actionSet.actions.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Provides the 'HomeKit object' (`AnyObject?`) at the specified index path.
|
||||
|
||||
- parameter indexPath: The `NSIndexPath` of the object.
|
||||
|
||||
- returns: The HomeKit object.
|
||||
*/
|
||||
private func homeKitObjectAtIndexPath(indexPath: NSIndexPath) -> AnyObject? {
|
||||
if cellTypeForIndexPath(indexPath) != .Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let section = HomeKitObjectSection(rawValue: indexPath.section) {
|
||||
return objectCollection.objectsForSection(section)[indexPath.row]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of `HomeKitObjectSection`s.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return HomeKitObjectSection.count
|
||||
}
|
||||
|
||||
/// - returns: Localized titles for each of the HomeKit sections.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch HomeKitObjectSection(rawValue: section) {
|
||||
case .Accessory?:
|
||||
return NSLocalizedString("Accessories", comment: "Accessories")
|
||||
|
||||
case .Room?:
|
||||
return NSLocalizedString("Rooms", comment: "Rooms")
|
||||
|
||||
case .Zone?:
|
||||
return NSLocalizedString("Zones", comment: "Zones")
|
||||
|
||||
case .User?:
|
||||
return NSLocalizedString("Users", comment: "Users")
|
||||
|
||||
case .ActionSet?:
|
||||
return NSLocalizedString("Scenes", comment: "Scenes")
|
||||
|
||||
case .Trigger?:
|
||||
return NSLocalizedString("Triggers", comment: "Triggers")
|
||||
|
||||
case .ServiceGroup?:
|
||||
return NSLocalizedString("Service Groups", comment: "Service Groups")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `HomeKitObjectSection` raw value.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// - returns: Localized text for the 'add row'.
|
||||
private func titleForAddRowInSection(section: HomeKitObjectSection) -> String {
|
||||
switch section {
|
||||
case .Accessory:
|
||||
return NSLocalizedString("Add Accessory…", comment: "Add Accessory")
|
||||
|
||||
case .Room:
|
||||
return NSLocalizedString("Add Room…", comment: "Add Room")
|
||||
|
||||
case .Zone:
|
||||
return NSLocalizedString("Add Zone…", comment: "Add Zone")
|
||||
|
||||
case .User:
|
||||
return NSLocalizedString("Manage Users…", comment: "Manage Users")
|
||||
|
||||
case .ActionSet:
|
||||
return NSLocalizedString("Add Scene…", comment: "Add Scene")
|
||||
|
||||
case .Trigger:
|
||||
return NSLocalizedString("Add Trigger…", comment: "Add Trigger")
|
||||
|
||||
case .ServiceGroup:
|
||||
return NSLocalizedString("Add Service Group…", comment: "Add Service Group")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: Localized text for the 'none row'.
|
||||
private func titleForNoneRowInSection(section: HomeKitObjectSection) -> String {
|
||||
switch section {
|
||||
case .Accessory:
|
||||
return NSLocalizedString("No Accessories…", comment: "No Accessories")
|
||||
|
||||
case .Room:
|
||||
return NSLocalizedString("No Rooms…", comment: "No Rooms")
|
||||
|
||||
case .Zone:
|
||||
return NSLocalizedString("No Zones…", comment: "No Zones")
|
||||
|
||||
case .User:
|
||||
// We only ever list 'Manage Users'.
|
||||
return NSLocalizedString("Manage Users…", comment: "Manage Users")
|
||||
|
||||
case .ActionSet:
|
||||
return NSLocalizedString("No Scenes…", comment: "No Scenes")
|
||||
|
||||
case .Trigger:
|
||||
return NSLocalizedString("No Triggers…", comment: "No Triggers")
|
||||
|
||||
case .ServiceGroup:
|
||||
return NSLocalizedString("No Service Groups…", comment: "No Service Groups")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: Localized descriptions for HomeKit object types.
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch HomeKitObjectSection(rawValue: section) {
|
||||
case .Zone?:
|
||||
return NSLocalizedString("Zones are optional collections of rooms.", comment: "Zones Description")
|
||||
|
||||
case .User?:
|
||||
return NSLocalizedString("Users can control the accessories in your home. You can share your home with anybody with an iCloud account.", comment: "Users Description")
|
||||
|
||||
case .ActionSet?:
|
||||
return NSLocalizedString("Scenes (action sets) represent a state of your home. You must have at least one paired accessory to create a scene.", comment: "Scenes Description")
|
||||
|
||||
case .Trigger?:
|
||||
return NSLocalizedString("Triggers set scenes at specific times, when you get to locations, or when a characteristic is in a specific state. You must have created at least one scene with an action to create a trigger.", comment: "Trigger Description")
|
||||
|
||||
case .ServiceGroup?:
|
||||
return NSLocalizedString("Service groups organize services in a custom way. For example, add a subset of lights in your living room to control them without controlling all the lights in the living room.", comment: "Service Group Description")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `HomeKitObjectSection` raw value.")
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Provides the number of rows in each HomeKit object section.
|
||||
Most sections just return the object count, but we also handle special cases.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let sectionEnum = HomeKitObjectSection(rawValue: section)!
|
||||
|
||||
// Only "Manage Users" button is in the Users section
|
||||
if sectionEnum == .User {
|
||||
return 1
|
||||
}
|
||||
|
||||
let objectCount = objectCollection.objectsForSection(sectionEnum).count
|
||||
if home.isAdmin {
|
||||
// For add row.
|
||||
return objectCount + 1
|
||||
}
|
||||
else {
|
||||
// Always show at least one row in the section.
|
||||
return max(objectCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a cell based on it's computed type.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch cellTypeForIndexPath(indexPath) {
|
||||
case .Add:
|
||||
return self.tableView(tableView, addCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .Object:
|
||||
return self.tableView(tableView, homeKitObjectCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .None:
|
||||
return self.tableView(tableView, noneCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a 'none cell' with a localized title.
|
||||
private func tableView(tableView: UITableView, noneCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.disabledAddCell, forIndexPath: indexPath)
|
||||
|
||||
let section = HomeKitObjectSection(rawValue: indexPath.section)!
|
||||
|
||||
cell.textLabel!.text = titleForNoneRowInSection(section)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Generates an 'add cell' with a localized title.
|
||||
|
||||
In some cases, the 'add cell' will be 'disabled' because the user is not
|
||||
allowed to perform the action.
|
||||
*/
|
||||
private func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
var reuseIdentifier = Identifiers.addCell
|
||||
|
||||
let section = HomeKitObjectSection(rawValue: indexPath.section)
|
||||
|
||||
if (!canAddActionSet && section == .ActionSet) ||
|
||||
(!canAddTrigger && section == .Trigger) || !home.isAdmin {
|
||||
reuseIdentifier = Identifiers.disabledAddCell
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
|
||||
|
||||
cell.textLabel!.text = titleForAddRowInSection(section!)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Produces the cell reuse identifier based on the section.
|
||||
|
||||
- parameter indexPath: The `NSIndexPath` of the cell.
|
||||
|
||||
- returns: The cell reuse identifier.
|
||||
*/
|
||||
private func reuseIdentifierForIndexPath(indexPath: NSIndexPath) -> String {
|
||||
switch HomeKitObjectSection(rawValue: indexPath.section) {
|
||||
case .Accessory?:
|
||||
let accessory = homeKitObjectAtIndexPath(indexPath) as! HMAccessory
|
||||
return accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell
|
||||
|
||||
case .Room?:
|
||||
return Identifiers.roomCell
|
||||
|
||||
case .Zone?:
|
||||
return Identifiers.zoneCell
|
||||
|
||||
case .User?:
|
||||
return Identifiers.userCell
|
||||
|
||||
case .ActionSet?:
|
||||
return Identifiers.actionSetCell
|
||||
|
||||
case .Trigger?:
|
||||
return Identifiers.triggerCell
|
||||
|
||||
case .ServiceGroup?:
|
||||
return Identifiers.serviceGroupCell
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `HomeKitObjectSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a cell for the HomeKit object at the specified index path.
|
||||
private func tableView(tableView: UITableView, homeKitObjectCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
// Grab the object associated with this indexPath.
|
||||
let homeKitObject = homeKitObjectAtIndexPath(indexPath)
|
||||
|
||||
// Get the name of the object.
|
||||
let name: String
|
||||
switch HomeKitObjectSection(rawValue: indexPath.section) {
|
||||
case .Accessory?:
|
||||
let accessory = homeKitObject as! HMAccessory
|
||||
name = accessory.name
|
||||
|
||||
case .Room?:
|
||||
let room = homeKitObject as! HMRoom
|
||||
name = self.home.nameForRoom(room)
|
||||
|
||||
case .Zone?:
|
||||
let zone = homeKitObject as! HMZone
|
||||
name = zone.name
|
||||
case .User?:
|
||||
name = ""
|
||||
|
||||
case .ActionSet?:
|
||||
let actionSet = homeKitObject as! HMActionSet
|
||||
name = actionSet.name
|
||||
|
||||
case .Trigger?:
|
||||
let trigger = homeKitObject as! HMTrigger
|
||||
name = trigger.name
|
||||
|
||||
case .ServiceGroup?:
|
||||
let serviceGroup = homeKitObject as! HMServiceGroup
|
||||
name = serviceGroup.name
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `HomeKitObjectSection` raw value.")
|
||||
}
|
||||
|
||||
|
||||
// Grab the appropriate reuse identifier for this index path.
|
||||
let reuseIdentifier = reuseIdentifierForIndexPath(indexPath)
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
|
||||
cell.textLabel?.text = name
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Allows users to remove HomeKit object rows if they are the admin of the home.
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
let homeKitObject = homeKitObjectAtIndexPath(indexPath)
|
||||
|
||||
if !home.isAdmin {
|
||||
return false
|
||||
}
|
||||
|
||||
if let actionSet = homeKitObject as? HMActionSet where actionSet.isBuiltIn {
|
||||
// We cannot remove built-in action sets.
|
||||
return false
|
||||
}
|
||||
|
||||
// Any row that is not an 'add' row, and is not the roomForEntireHome, can be removed.
|
||||
return !(homeKitObject as? NSObject == home.roomForEntireHome() || cellTypeForIndexPath(indexPath) == .Add)
|
||||
}
|
||||
|
||||
/// Removes the HomeKit object at the specified index path.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
let homeKitObject = homeKitObjectAtIndexPath(indexPath)!
|
||||
|
||||
// Remove the object from the data structure. If it fails put it back.
|
||||
didRemoveHomeKitObject(homeKitObject)
|
||||
removeHomeKitObject(homeKitObject) { error in
|
||||
guard let error = error else { return }
|
||||
|
||||
self.displayError(error)
|
||||
self.didAddHomeKitObject(homeKitObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles cell selection based on the cell type.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath)!
|
||||
|
||||
guard cell.selectionStyle != .None else { return }
|
||||
|
||||
guard let section = HomeKitObjectSection(rawValue: indexPath.section) else {
|
||||
fatalError("Unexpected `HomeKitObjectSection` raw value.")
|
||||
}
|
||||
|
||||
if cellTypeForIndexPath(indexPath) == .Add{
|
||||
switch section {
|
||||
case .Accessory:
|
||||
browseForAccessories()
|
||||
|
||||
case .Room:
|
||||
addNewRoom()
|
||||
|
||||
case .Zone:
|
||||
addNewZone()
|
||||
|
||||
case .User:
|
||||
manageUsers()
|
||||
|
||||
case .ActionSet:
|
||||
addNewActionSet()
|
||||
|
||||
case .Trigger:
|
||||
addNewTrigger()
|
||||
|
||||
case .ServiceGroup:
|
||||
addNewServiceGroup()
|
||||
}
|
||||
}
|
||||
else if section == .ActionSet {
|
||||
let selectedActionSet = homeKitObjectAtIndexPath(indexPath) as! HMActionSet
|
||||
executeActionSet(selectedActionSet)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an accessory button tap based on the section.
|
||||
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath)
|
||||
|
||||
if HomeKitObjectSection(rawValue: indexPath.section) == .Trigger {
|
||||
let trigger = homeKitObjectAtIndexPath(indexPath)
|
||||
|
||||
switch trigger {
|
||||
case is HMTimerTrigger:
|
||||
performSegueWithIdentifier(Identifiers.showTimerTriggerSegue, sender: cell)
|
||||
|
||||
case let eventTrigger as HMEventTrigger:
|
||||
if eventTrigger.isLocationEvent {
|
||||
performSegueWithIdentifier(Identifiers.showLocationTriggerSegue, sender: cell)
|
||||
}
|
||||
else {
|
||||
performSegueWithIdentifier(Identifiers.showCharacteristicTriggerSegue, sender: cell)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Action Methods
|
||||
|
||||
/// Presents an alert controller to allow the user to choose a trigger type.
|
||||
private func addNewTrigger() {
|
||||
let title = NSLocalizedString("Add Trigger", comment: "Add Trigger")
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .ActionSheet)
|
||||
|
||||
// Timer trigger
|
||||
let timeAction = UIAlertAction(title: NSLocalizedString("Time", comment: "Time"), style: .Default) { _ in
|
||||
self.performSegueWithIdentifier(Identifiers.addTimerTriggerSegue, sender: self)
|
||||
}
|
||||
alertController.addAction(timeAction)
|
||||
|
||||
// Characteristic trigger
|
||||
let eventAction = UIAlertAction(title: NSLocalizedString("Characteristic", comment: "Characteristic"), style: .Default) { _ in
|
||||
self.performSegueWithIdentifier(Identifiers.addCharacteristicTriggerSegue, sender: self)
|
||||
}
|
||||
alertController.addAction(eventAction)
|
||||
|
||||
// Location trigger
|
||||
let locationAction = UIAlertAction(title: NSLocalizedString("Location", comment: "Location"), style: .Default) { _ in
|
||||
self.performSegueWithIdentifier(Identifiers.addLocationTriggerSegue, sender: self)
|
||||
}
|
||||
alertController.addAction(locationAction)
|
||||
|
||||
// Cancel
|
||||
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
// Present alert
|
||||
presentViewController(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/// Navigates into the action set view controller.
|
||||
private func addNewActionSet() {
|
||||
performSegueWithIdentifier(Identifiers.addActionSetSegue, sender: self)
|
||||
}
|
||||
|
||||
/// Navigates into the browse accessory view controller.
|
||||
private func browseForAccessories() {
|
||||
performSegueWithIdentifier(Identifiers.addAccessoriesSegue, sender: self)
|
||||
}
|
||||
|
||||
// MARK: Dialog Creation Methods
|
||||
|
||||
/// Presents a dialog to name a new room and generates the HomeKit object if committed.
|
||||
private func addNewRoom() {
|
||||
presentAddAlertWithAttributeType(NSLocalizedString("Room", comment: "Room"),
|
||||
placeholder: NSLocalizedString("Living Room", comment: "Living Room")) { roomName in
|
||||
self.addRoomWithName(roomName)
|
||||
}
|
||||
}
|
||||
|
||||
/// Presents a dialog to name a new service group and generates the HomeKit object if committed.
|
||||
private func addNewServiceGroup() {
|
||||
presentAddAlertWithAttributeType(NSLocalizedString("Service Group", comment: "Service Group"),
|
||||
placeholder: NSLocalizedString("Group", comment: "Group")) { groupName in
|
||||
self.addServiceGroupWithName(groupName)
|
||||
}
|
||||
}
|
||||
|
||||
/// Presents a dialog to name a new zone and generates the HomeKit object if committed.
|
||||
private func addNewZone() {
|
||||
presentAddAlertWithAttributeType(NSLocalizedString("Zone", comment: "Zone"),
|
||||
placeholder: NSLocalizedString("Upstairs", comment: "Upstairs")) { zoneName in
|
||||
self.addZoneWithName(zoneName)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HomeKit Object Creation and Deletion
|
||||
|
||||
/**
|
||||
Switches based on the type of object attempts to remove the HomeKit object
|
||||
from the curret home.
|
||||
|
||||
- parameter object: The HomeKit object to remove.
|
||||
- parameter completionHandler: The closure to invote when the removal has been completed.
|
||||
*/
|
||||
private func removeHomeKitObject(object: AnyObject, completionHandler: NSError? -> Void) {
|
||||
switch object {
|
||||
case let actionSet as HMActionSet:
|
||||
home.removeActionSet(actionSet) { error in
|
||||
completionHandler(error)
|
||||
self.updateActionSetSection()
|
||||
}
|
||||
|
||||
case let accessory as HMAccessory:
|
||||
home.removeAccessory(accessory, completionHandler: completionHandler)
|
||||
|
||||
case let room as HMRoom:
|
||||
home.removeRoom(room, completionHandler: completionHandler)
|
||||
|
||||
case let zone as HMZone:
|
||||
home.removeZone(zone, completionHandler: completionHandler)
|
||||
|
||||
case let trigger as HMTrigger:
|
||||
home.removeTrigger(trigger, completionHandler: completionHandler)
|
||||
|
||||
case let serviceGroup as HMServiceGroup:
|
||||
home.removeServiceGroup(serviceGroup, completionHandler: completionHandler)
|
||||
|
||||
default:
|
||||
fatalError("Attempted to remove unknown HomeKit object.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a room to the current home.
|
||||
|
||||
- parameter name: The name of the new room.
|
||||
*/
|
||||
private func addRoomWithName(name: String) {
|
||||
home.addRoomWithName(name) { newRoom, error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
|
||||
self.didAddHomeKitObject(newRoom)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a service group to the current home.
|
||||
|
||||
- parameter name: The name of the new service group.
|
||||
*/
|
||||
private func addServiceGroupWithName(name: String) {
|
||||
home.addServiceGroupWithName(name) { newGroup, error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
|
||||
self.didAddHomeKitObject(newGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a zone to the current home.
|
||||
|
||||
- parameter name: The name of the new zone.
|
||||
*/
|
||||
private func addZoneWithName(name: String) {
|
||||
home.addZoneWithName(name) { newZone, error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
|
||||
self.didAddHomeKitObject(newZone)
|
||||
}
|
||||
}
|
||||
|
||||
/// Presents modal view for managing users.
|
||||
private func manageUsers() {
|
||||
home.manageUsersWithCompletionHandler { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks to see if an action set has any actions.
|
||||
If actions exists, the action set will be executed.
|
||||
Otherwise, the user will be alerted.
|
||||
|
||||
- parameter actionSet: The `HMActionSet` to evaluate and execute.
|
||||
*/
|
||||
private func executeActionSet(actionSet: HMActionSet) {
|
||||
if actionSet.actions.isEmpty {
|
||||
let alertTitle = NSLocalizedString("Empty Scene", comment: "Empty Scene")
|
||||
|
||||
let alertMessage = NSLocalizedString("This scene is empty. To set this scene, first add some actions to it.", comment: "Empty Scene Description")
|
||||
|
||||
displayMessage(alertTitle, message: alertMessage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
home.executeActionSet(actionSet) { error in
|
||||
guard let error = error else { return }
|
||||
|
||||
self.displayError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds the HomeKit object into the object collection and inserts the new row into the section.
|
||||
|
||||
- parameter object: The HomeKit object to add.
|
||||
*/
|
||||
private func didAddHomeKitObject(object: AnyObject?) {
|
||||
if let object = object {
|
||||
objectCollection.append(object)
|
||||
if let newObjectIndexPath = objectCollection.indexPathOfObject(object) {
|
||||
tableView.insertRowsAtIndexPaths([newObjectIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the `NSIndexPath` of the specified object and reloads it in the table view.
|
||||
|
||||
- parameter object: The HomeKit object that was modified.
|
||||
*/
|
||||
private func didModifyHomeKitObject(object: AnyObject?) {
|
||||
if let object = object,
|
||||
objectIndexPath = objectCollection.indexPathOfObject(object) {
|
||||
tableView.reloadRowsAtIndexPaths([objectIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the HomeKit object from the object collection and then deletes the row from the section.
|
||||
|
||||
- parameter object: The HomeKit object to remove.
|
||||
*/
|
||||
private func didRemoveHomeKitObject(object: AnyObject?) {
|
||||
if let object = object,
|
||||
objectIndexPath = objectCollection.indexPathOfObject(object) {
|
||||
objectCollection.remove(object)
|
||||
tableView.deleteRowsAtIndexPaths([objectIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The following methods call the above helper methds to handle
|
||||
the addition, removal, and modification of HomeKit objects.
|
||||
*/
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
func homeDidUpdateName(home: HMHome) {
|
||||
navigationItem.title = home.name
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
|
||||
didAddHomeKitObject(accessory)
|
||||
accessory.delegate = self
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
didRemoveHomeKitObject(accessory)
|
||||
}
|
||||
|
||||
// MARK: Triggers
|
||||
|
||||
func home(home: HMHome, didAddTrigger trigger: HMTrigger) {
|
||||
didAddHomeKitObject(trigger)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveTrigger trigger: HMTrigger) {
|
||||
didRemoveHomeKitObject(trigger)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didUpdateNameForTrigger trigger: HMTrigger) {
|
||||
didModifyHomeKitObject(trigger)
|
||||
}
|
||||
|
||||
// MARK: Service Groups
|
||||
|
||||
func home(home: HMHome, didAddServiceGroup group: HMServiceGroup) {
|
||||
didAddHomeKitObject(group)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) {
|
||||
didRemoveHomeKitObject(group)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didUpdateNameForServiceGroup group: HMServiceGroup) {
|
||||
didModifyHomeKitObject(group)
|
||||
}
|
||||
|
||||
// MARK: Action Sets
|
||||
|
||||
func home(home: HMHome, didAddActionSet actionSet: HMActionSet) {
|
||||
didAddHomeKitObject(actionSet)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveActionSet actionSet: HMActionSet) {
|
||||
didRemoveHomeKitObject(actionSet)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didUpdateNameForActionSet actionSet: HMActionSet) {
|
||||
didModifyHomeKitObject(actionSet)
|
||||
}
|
||||
|
||||
// MARK: Zones
|
||||
|
||||
func home(home: HMHome, didAddZone zone: HMZone) {
|
||||
didAddHomeKitObject(zone)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveZone zone: HMZone) {
|
||||
didRemoveHomeKitObject(zone)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didUpdateNameForZone zone: HMZone) {
|
||||
didModifyHomeKitObject(zone)
|
||||
}
|
||||
|
||||
// MARK: Rooms
|
||||
|
||||
func home(home: HMHome, didAddRoom room: HMRoom) {
|
||||
didAddHomeKitObject(room)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom) {
|
||||
didRemoveHomeKitObject(room)
|
||||
}
|
||||
|
||||
func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
|
||||
didModifyHomeKitObject(room)
|
||||
}
|
||||
|
||||
// MARK: Accessories
|
||||
|
||||
func accessoryDidUpdateReachability(accessory: HMAccessory) {
|
||||
didModifyHomeKitObject(accessory)
|
||||
}
|
||||
|
||||
func accessoryDidUpdateName(accessory: HMAccessory) {
|
||||
didModifyHomeKitObject(accessory)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `RoomViewController` lists the accessory within a room.
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller that lists the accessories within a room.
|
||||
class RoomViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let accessoryCell = "AccessoryCell"
|
||||
static let unreachableAccessoryCell = "UnreachableAccessoryCell"
|
||||
static let modifyAccessorySegue = "Modify Accessory"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var room: HMRoom! {
|
||||
didSet {
|
||||
navigationItem.title = room.name
|
||||
}
|
||||
}
|
||||
|
||||
var accessories = [HMAccessory]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reloadData()
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of accessories within this room.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let rows = accessories.count
|
||||
if rows == 0 {
|
||||
let message = NSLocalizedString("No Accessories", comment: "No Accessories")
|
||||
setBackgroundMessage(message)
|
||||
}
|
||||
else {
|
||||
setBackgroundMessage(nil)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/// - returns: `true` if the current room is not the home's roomForEntireHome; `false` otherwise.
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
return room != home.roomForEntireHome()
|
||||
}
|
||||
|
||||
/// - returns: Localized "Unassign".
|
||||
override func tableView(tableView: UITableView, titleForDeleteConfirmationButtonForRowAtIndexPath indexPath: NSIndexPath) -> String? {
|
||||
return NSLocalizedString("Unassign", comment: "Unassign")
|
||||
}
|
||||
|
||||
/// Assigns the 'deleted' room to the home's roomForEntireHome.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
unassignAccessory(accessories[indexPath.row])
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A cell representing an accessory.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let accessory = accessories[indexPath.row]
|
||||
|
||||
var reuseIdentifier = Identifiers.accessoryCell
|
||||
|
||||
if !accessory.reachable {
|
||||
reuseIdentifier = Identifiers.unreachableAccessoryCell
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
|
||||
|
||||
cell.textLabel?.text = accessory.name
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// - returns: A localized description, "Accessories" if there are accessories to list.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
if accessories.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSLocalizedString("Accessories", comment: "Accessories")
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Updates the internal array of accessories and reloads the table view.
|
||||
private func reloadData() {
|
||||
accessories = room.accessories.sortByLocalizedName()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// Sorts the internal list of accessories by localized name.
|
||||
private func sortAccessories() {
|
||||
accessories = accessories.sortByLocalizedName()
|
||||
}
|
||||
|
||||
/**
|
||||
Registers as the delegate for the current home and
|
||||
all accessories in our room.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
for accessory in room.accessories {
|
||||
accessory.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the accessory and home of the modifyAccessoryViewController that will be presented.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
|
||||
if segue.identifier == Identifiers.modifyAccessorySegue {
|
||||
let modifyViewController = segue.intendedDestinationViewController as! ModifyAccessoryViewController
|
||||
modifyViewController.accessory = room.accessories[indexPath.row]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds an accessory into the internal list of accessories
|
||||
and inserts the row into the table view.
|
||||
|
||||
- parameter accessory: The `HMAccessory` to add.
|
||||
*/
|
||||
private func didAssignAccessory(accessory: HMAccessory) {
|
||||
accessories.append(accessory)
|
||||
sortAccessories()
|
||||
if let newAccessoryIndex = accessories.indexOf(accessory) {
|
||||
let newAccessoryIndexPath = NSIndexPath(forRow: newAccessoryIndex, inSection: 0)
|
||||
tableView.insertRowsAtIndexPaths([newAccessoryIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes an accessory from the internal list of accessory (if it
|
||||
exists) and deletes the row from the table view.
|
||||
|
||||
- parameter accessory: The `HMAccessory` to remove.
|
||||
*/
|
||||
private func didUnassignAccessory(accessory: HMAccessory) {
|
||||
if let accessoryIndex = accessories.indexOf(accessory) {
|
||||
accessories.removeAtIndex(accessoryIndex)
|
||||
let accessoryIndexPath = NSIndexPath(forRow: accessoryIndex, inSection: 0)
|
||||
tableView.deleteRowsAtIndexPaths([accessoryIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Assigns an accessory to the current room.
|
||||
|
||||
- parameter accessory: The `HMAccessory` to assign to the room.
|
||||
*/
|
||||
private func assignAccessory(accessory: HMAccessory) {
|
||||
didAssignAccessory(accessory)
|
||||
home.assignAccessory(accessory, toRoom: room) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
self.didUnassignAccessory(accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Assigns the current room back into `roomForEntireHome`.
|
||||
|
||||
- parameter accessory: The `HMAccessory` to reassign.
|
||||
*/
|
||||
private func unassignAccessory(accessory: HMAccessory) {
|
||||
didUnassignAccessory(accessory)
|
||||
home.assignAccessory(accessory, toRoom: home.roomForEntireHome()) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
self.didAssignAccessory(accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Finds an accessory in the internal array of accessories
|
||||
and updates its row in the table view.
|
||||
|
||||
- parameter accessory: The `HMAccessory` to reload.
|
||||
*/
|
||||
func didModifyAccessory(accessory: HMAccessory){
|
||||
if let index = accessories.indexOf(accessory) {
|
||||
let indexPaths = [
|
||||
NSIndexPath(forRow: index, inSection: 0)
|
||||
]
|
||||
|
||||
tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// If the accessory was added to this room, insert it.
|
||||
func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
|
||||
if accessory.room == room {
|
||||
accessory.delegate = self
|
||||
didAssignAccessory(accessory)
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the accessory from our room, if required.
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
didUnassignAccessory(accessory)
|
||||
}
|
||||
|
||||
/**
|
||||
Handles the update.
|
||||
|
||||
We act based on one of three options:
|
||||
|
||||
1. A new accessory is being added to this room.
|
||||
2. An accessory is being assigned from this room to another room.
|
||||
3. We can ignore this message.
|
||||
*/
|
||||
func home(home: HMHome, didUpdateRoom room: HMRoom, forAccessory accessory: HMAccessory) {
|
||||
if room == self.room {
|
||||
didAssignAccessory(accessory)
|
||||
}
|
||||
else if accessories.contains(accessory) {
|
||||
didUnassignAccessory(accessory)
|
||||
}
|
||||
}
|
||||
|
||||
/// If our room was removed, pop back.
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom) {
|
||||
if room == self.room {
|
||||
navigationController!.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// If our room was renamed, reload our title.
|
||||
func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
|
||||
if room == self.room {
|
||||
navigationItem.title = room.name
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
// Accessory updates will reload the cell for the accessory.
|
||||
|
||||
func accessoryDidUpdateReachability(accessory: HMAccessory) {
|
||||
didModifyAccessory(accessory)
|
||||
}
|
||||
|
||||
func accessoryDidUpdateName(accessory: HMAccessory) {
|
||||
didModifyAccessory(accessory)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `AddServicesViewController` allows users to add services to a service group.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A view controller that provides a list of services and lets the user select services to be added to the provided Service Group.
|
||||
|
||||
The services are not added to the service group until the 'Done' button is pressed.
|
||||
*/
|
||||
class AddServicesViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let serviceCell = "ServiceCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
lazy private var displayedAccessories = [HMAccessory]()
|
||||
lazy private var displayedServicesForAccessory = [HMAccessory: [HMService]]()
|
||||
lazy private var selectedServices = [HMService]()
|
||||
|
||||
var serviceGroup: HMServiceGroup!
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Reloads internal data and view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
selectedServices = []
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
/// Registers as the delegate for the home and all accessories.
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
for accessory in homeStore.home!.accessories {
|
||||
accessory.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of displayed accessories.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return displayedAccessories.count
|
||||
}
|
||||
|
||||
/// - returns: The number of displayed services for the provided accessory.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let accessory = displayedAccessories[section]
|
||||
return displayedServicesForAccessory[accessory]!.count
|
||||
}
|
||||
|
||||
/// - returns: A configured `ServiceCell`.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) as! ServiceCell
|
||||
|
||||
let service = serviceAtIndexPath(indexPath)
|
||||
|
||||
cell.includeAccessoryText = false
|
||||
cell.service = service
|
||||
cell.accessoryType = selectedServices.contains(service) ? .Checkmark : .None
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
When an indexPath is selected, this function either adds or removes the selected service from the
|
||||
service group.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
// Get the service associated with this index.
|
||||
let service = serviceAtIndexPath(indexPath)
|
||||
|
||||
// Call the appropriate add/remove operation with the closure from above.
|
||||
if let index = selectedServices.indexOf(service) {
|
||||
selectedServices.removeAtIndex(index)
|
||||
}
|
||||
else {
|
||||
selectedServices.append(service)
|
||||
}
|
||||
|
||||
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/// - returns: The name of the displayed accessory at the given section.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return displayedAccessories[section].name
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Adds the selected services to the service group.
|
||||
|
||||
Calls the provided completion handler once all services have been added.
|
||||
*/
|
||||
func addSelectedServicesWithCompletionHandler(completion: () -> Void) {
|
||||
// Create a dispatch group for each of the service additions.
|
||||
let addServicesGroup = dispatch_group_create()
|
||||
for service in selectedServices {
|
||||
dispatch_group_enter(addServicesGroup)
|
||||
serviceGroup.addService(service) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
dispatch_group_leave(addServicesGroup)
|
||||
}
|
||||
}
|
||||
dispatch_group_notify(addServicesGroup, dispatch_get_main_queue(), completion)
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the service at a specific index path.
|
||||
|
||||
- parameter indexPath: An `NSIndexPath`
|
||||
|
||||
- returns: The `HMService` at the given index path.
|
||||
*/
|
||||
private func serviceAtIndexPath(indexPath: NSIndexPath) -> HMService {
|
||||
let accessory = displayedAccessories[indexPath.section]
|
||||
let services = displayedServicesForAccessory[accessory]!
|
||||
return services[indexPath.row]
|
||||
}
|
||||
|
||||
/**
|
||||
Commits the changes to the service group
|
||||
and dismisses the view.
|
||||
*/
|
||||
@IBAction func dismiss() {
|
||||
addSelectedServicesWithCompletionHandler {
|
||||
self.dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets internal data and view.
|
||||
func reloadTable() {
|
||||
resetDisplayedServices()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/**
|
||||
Updates internal array of accessories and the mapping
|
||||
of accessories to selected services.
|
||||
*/
|
||||
func resetDisplayedServices() {
|
||||
displayedAccessories = []
|
||||
let allAccessories = home.accessories.sortByLocalizedName()
|
||||
displayedServicesForAccessory = [:]
|
||||
for accessory in allAccessories {
|
||||
var displayedServices = [HMService]()
|
||||
for service in accessory.services {
|
||||
if !serviceGroup.services.contains(service) && service.serviceType != HMServiceTypeAccessoryInformation {
|
||||
displayedServices.append(service)
|
||||
}
|
||||
}
|
||||
|
||||
// Only add the accessory if it has displayed services.
|
||||
if !displayedServices.isEmpty {
|
||||
displayedServicesForAccessory[accessory] = displayedServices.sortByLocalizedName()
|
||||
displayedAccessories.append(accessory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// Dismisses the view controller if our service group was removed.
|
||||
func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) {
|
||||
if serviceGroup == group {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reloads the view if an accessory was added to HomeKit.
|
||||
func home(home: HMHome, didAddAccessory accessory: HMAccessory) {
|
||||
reloadTable()
|
||||
accessory.delegate = self
|
||||
}
|
||||
|
||||
/// Dismisses the view controller if we no longer have accesories.
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
if home.accessories.isEmpty {
|
||||
navigationController?.dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
// MARK: HMAccessoryDelegate Methods
|
||||
|
||||
// Accessory changes reload the data and view.
|
||||
|
||||
func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
func accessoryDidUpdateServices(accessory: HMAccessory) {
|
||||
reloadTable()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ServiceGroupViewController` allows users to modify service groups.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller that allows the user to add services to a service group.
|
||||
class ServiceGroupViewController: HMCatalogViewController, HMAccessoryDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let serviceCell = "ServiceCell"
|
||||
static let addServicesSegue = "Add Services Plus"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var plusButton: UIBarButtonItem!
|
||||
|
||||
var serviceGroup: HMServiceGroup!
|
||||
lazy private var accessories = [HMAccessory]()
|
||||
lazy private var servicesForAccessory = [HMAccessory: [HMService]]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Reloads the view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
title = serviceGroup.name
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// Pops the view controller if our data is invalid.
|
||||
override func viewDidAppear(animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if shouldPopViewController() {
|
||||
navigationController?.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
Generates the number of sections and adds a table view
|
||||
back ground message, if required.
|
||||
|
||||
- returns: The number of accessories in the service group.
|
||||
*/
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
let sections = accessories.count
|
||||
if sections == 0 {
|
||||
setBackgroundMessage(NSLocalizedString("No Services", comment: "No Services"))
|
||||
}
|
||||
else {
|
||||
setBackgroundMessage(nil)
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
/// - returns: The number of services for the accessory at the specified section.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let accessory = accessories[section]
|
||||
let services = servicesForAccessory[accessory]
|
||||
|
||||
return services?.count ?? 0
|
||||
}
|
||||
|
||||
/// - returns: The name of the accessory at the specified section.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return accessories[section].name
|
||||
}
|
||||
|
||||
/// All cells in the table view represent services and can be deleted.
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
/// - returns: A `ServiceCell` with the service at the given index path.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) as! ServiceCell
|
||||
let service = serviceAtIndexPath(indexPath)
|
||||
cell.includeAccessoryText = false
|
||||
cell.service = service
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if there are any services not already in the service group;
|
||||
`false` otherwise.
|
||||
*/
|
||||
private func shouldEnableAdd() -> Bool {
|
||||
let unAddedServices = home.servicesNotAlreadyInServiceGroup(serviceGroup)
|
||||
return unAddedServices.count != 0
|
||||
}
|
||||
|
||||
/// Deleting a cell removes the corresponding service from the service group.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
removeServiceAtIndexPath(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the service associated with the cell at a given index path.
|
||||
|
||||
- parameters indexPath: The `NSIndexPath` to remove.
|
||||
*/
|
||||
private func removeServiceAtIndexPath(indexPath: NSIndexPath) {
|
||||
let service = serviceAtIndexPath(indexPath)
|
||||
serviceGroup.removeService(service) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
|
||||
self.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the service at a given index path.
|
||||
|
||||
- parameter indexPath: An `NSIndexPath`.
|
||||
|
||||
- returns: The service at the given index path
|
||||
*/
|
||||
private func serviceAtIndexPath(indexPath: NSIndexPath) -> HMService {
|
||||
let accessory = accessories[indexPath.section]
|
||||
let services = servicesForAccessory[accessory]!
|
||||
return services[indexPath.row]
|
||||
}
|
||||
|
||||
/// Passes the service group into the `AddServicesViewController`
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
if segue.identifier == Identifiers.addServicesSegue {
|
||||
let addServicesVC = segue.intendedDestinationViewController as! AddServicesViewController
|
||||
addServicesVC.serviceGroup = serviceGroup
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Resets accessory and service lists, resets the plus
|
||||
button's enabled status and reloads the table view.
|
||||
*/
|
||||
private func reloadData() {
|
||||
resetLists()
|
||||
plusButton.enabled = shouldEnableAdd()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/**
|
||||
Resets the accessories array and the service-accessory mapping
|
||||
using the original HomeKit objects.
|
||||
*/
|
||||
private func resetLists() {
|
||||
accessories = []
|
||||
servicesForAccessory = [:]
|
||||
|
||||
for service in serviceGroup.services {
|
||||
if let accessory = service.accessory {
|
||||
if servicesForAccessory[accessory] == nil {
|
||||
accessories.append(accessory)
|
||||
servicesForAccessory[accessory] = [service]
|
||||
}
|
||||
else {
|
||||
servicesForAccessory[accessory]?.append(service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all service lists.
|
||||
for accessory in accessories {
|
||||
servicesForAccessory[accessory] = servicesForAccessory[accessory]?.sortByLocalizedName()
|
||||
}
|
||||
|
||||
// Sort accessory list.
|
||||
accessories = accessories.sortByLocalizedName()
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if our service group is not
|
||||
in the home any more; `false` otherwise.
|
||||
*/
|
||||
private func shouldPopViewController() -> Bool {
|
||||
guard let home = homeStore.home else { return true }
|
||||
|
||||
return !home.serviceGroups.contains { group in
|
||||
return group == serviceGroup
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Registers as the delegate for the home and
|
||||
all accessories which are related to our service group.
|
||||
*/
|
||||
override func registerAsDelegate() {
|
||||
super.registerAsDelegate()
|
||||
|
||||
for service in serviceGroup.services {
|
||||
service.accessory?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// Pops the view controller if our service group has been deleted.
|
||||
func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) {
|
||||
if group == serviceGroup {
|
||||
navigationController?.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Home and accessory changes result in a full data reload.
|
||||
|
||||
func home(home: HMHome, didAddService service: HMService, toServiceGroup group: HMServiceGroup) {
|
||||
if serviceGroup == group {
|
||||
reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveService service: HMService, fromServiceGroup group: HMServiceGroup) {
|
||||
if serviceGroup == group {
|
||||
reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func accessoryDidUpdateServices(accessory: HMAccessory) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) {
|
||||
reloadData()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `CharacteristicSelectionViewController` allows for the selection of characteristics.
|
||||
This is mainly used for creating characteristic events and conditions
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
Allows for the selection of characteristics.
|
||||
This is mainly used for creating characteristic events and conditions
|
||||
*/
|
||||
class CharacteristicSelectionViewController: HMCatalogViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let accessoryCell = "AccessoryCell"
|
||||
static let unreachableAccessoryCell = "UnreachableAccessoryCell"
|
||||
static let showServicesSegue = "Show Services"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var eventTrigger: HMEventTrigger?
|
||||
var triggerCreator: EventTriggerCreator!
|
||||
|
||||
/// An internal copy of all controllable accessories in the home.
|
||||
private var accessories = [HMAccessory]()
|
||||
|
||||
@IBOutlet weak var saveButton: UIBarButtonItem!
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Resets the internal array of accessories from the home.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
// Only take accessories which have one control service.
|
||||
accessories = home.sortedControlAccessories
|
||||
}
|
||||
|
||||
/// Configures the `ServicesViewController` and passes it the correct accessory.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
if segue.identifier == Identifiers.showServicesSegue {
|
||||
let senderCell = sender as! UITableViewCell
|
||||
let servicesVC = segue.intendedDestinationViewController as! ServicesViewController
|
||||
let cellIndex = tableView.indexPathForCell(senderCell)!.row
|
||||
servicesVC.allowsAllWrites = true
|
||||
servicesVC.onlyShowsControlServices = true
|
||||
servicesVC.accessory = accessories[cellIndex]
|
||||
servicesVC.cellDelegate = triggerCreator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/**
|
||||
Updates the predicates in the trigger creator and then
|
||||
dismisses the view controller.
|
||||
*/
|
||||
@IBAction func didTapSave(sender: UIBarButtonItem) {
|
||||
/*
|
||||
We should not save the trigger completely, the user still has a chance to bail out.
|
||||
Instead, we generate all of the predicates that were in the map.
|
||||
*/
|
||||
triggerCreator.updatePredicates()
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// Single section view controller.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
/// - returns: The number of accessories. If there are none, will return 1 (for the 'none row').
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return max(accessories.count, 1)
|
||||
}
|
||||
|
||||
/// - returns: An Accessory cell that contains an accessory's name.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let accessory = accessories.sortByLocalizedName()[indexPath.row]
|
||||
let cellIdentifier = accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
|
||||
cell.textLabel?.text = accessory.name
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Shows the services in the selected accessory.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath)!
|
||||
if cell.selectionStyle == .None {
|
||||
return
|
||||
}
|
||||
performSegueWithIdentifier(Identifiers.showServicesSegue, sender: cell)
|
||||
}
|
||||
|
||||
/// - returns: Localized "Accessories" string.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return NSLocalizedString("Accessories", comment: "Accessories")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `CharacteristicTriggerCreator` creates characteristic triggers.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents modes for a `CharacteristicTriggerCreator`.
|
||||
enum CharacteristicTriggerCreatorMode: Int {
|
||||
case Event, Condition
|
||||
}
|
||||
|
||||
/**
|
||||
An `EventTriggerCreator` subclass which allows for the creation
|
||||
of characteristic triggers.
|
||||
*/
|
||||
class CharacteristicTriggerCreator: EventTriggerCreator {
|
||||
// MARK: Properties
|
||||
|
||||
var eventTrigger: HMEventTrigger? {
|
||||
return self.trigger as? HMEventTrigger
|
||||
}
|
||||
|
||||
/**
|
||||
This object will be a characteristic cell delegate and will therefore
|
||||
be receiving updates when UI elements change value. However, this object
|
||||
can construct both characteristic events and characteristic triggers.
|
||||
Setting the `mode` determines how this trigger creator will handle
|
||||
cell delegate callbacks.
|
||||
*/
|
||||
var mode: CharacteristicTriggerCreatorMode = .Event
|
||||
|
||||
/**
|
||||
Contains the new pending mapping of `HMCharacteristic`s to their trigger (`NSCopying`) values.
|
||||
When `saveTriggerWithName(name:completion:)` is called, all of these mappings will be converted
|
||||
into `HMCharacteristicEvent`s and added to the `HMEventTrigger`.
|
||||
*/
|
||||
private let targetValueMap = NSMapTable.strongToStrongObjectsMapTable()
|
||||
|
||||
/// `HMCharacteristicEvent`s that should be removed if `saveTriggerWithName(name:completion:)` is called.
|
||||
private var removalCharacteristicEvents = [HMCharacteristicEvent]()
|
||||
|
||||
// MARK: Trigger Creator Methods
|
||||
|
||||
/// Syncs the stored event trigger using internal values.
|
||||
override func updateTrigger() {
|
||||
guard let eventTrigger = eventTrigger else { return }
|
||||
matchEventsFromTriggerIfNecessary()
|
||||
removePendingEventsFromTrigger()
|
||||
for (characteristic, triggerValue) in pairsFromMapTable(targetValueMap) {
|
||||
let newEvent = HMCharacteristicEvent(characteristic: characteristic, triggerValue: triggerValue)
|
||||
dispatch_group_enter(self.saveTriggerGroup)
|
||||
eventTrigger.addEvent(newEvent) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
savePredicate()
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A new `HMEventTrigger` with the pending
|
||||
characteristic events and constructed predicate.
|
||||
*/
|
||||
override func newTrigger() -> HMTrigger? {
|
||||
return HMEventTrigger(name: name, events: pendingCharacteristicEvents, predicate: newPredicate())
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all objects from the map so they don't show up
|
||||
in the `events` computed array.
|
||||
*/
|
||||
override func cleanUp() {
|
||||
targetValueMap.removeAllObjects()
|
||||
}
|
||||
|
||||
/**
|
||||
Removes an event from the map table if it's a new event and
|
||||
queues it for removal if it already existed in the event trigger.
|
||||
|
||||
- parameter event: `HMCharacteristicEvent` to be removed.
|
||||
*/
|
||||
func removeEvent(event: HMCharacteristicEvent) {
|
||||
if targetValueMap.objectForKey(event.characteristic) != nil {
|
||||
// Remove the characteristic from the target value map.
|
||||
targetValueMap.removeObjectForKey(event.characteristic)
|
||||
}
|
||||
|
||||
if let characteristicEvents = eventTrigger?.characteristicEvents where characteristicEvents.contains(event) {
|
||||
// If the given event is in the event array, queue it for removal.
|
||||
removalCharacteristicEvents.append(event)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Any characteristic events in the map table that have not yet been
|
||||
added to the trigger.
|
||||
*/
|
||||
var pendingCharacteristicEvents: [HMCharacteristicEvent] {
|
||||
return pairsFromMapTable(targetValueMap).map { (characteristic, triggerValue) -> HMCharacteristicEvent in
|
||||
return HMCharacteristicEvent(characteristic: characteristic, triggerValue: triggerValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Loops through the characteristic events in the trigger.
|
||||
If any characteristics in our map table are also in the event,
|
||||
replace the value with the one we have stored and remove that entry from
|
||||
our map table.
|
||||
*/
|
||||
private func matchEventsFromTriggerIfNecessary() {
|
||||
guard let eventTrigger = eventTrigger else { return }
|
||||
for event in eventTrigger.characteristicEvents {
|
||||
// Find events who's characteristic is in our map table.
|
||||
if let triggerValue = targetValueMap.objectForKey(event.characteristic) as? NSCopying {
|
||||
dispatch_group_enter(self.saveTriggerGroup)
|
||||
event.updateTriggerValue(triggerValue) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes all `HMCharacteristicEvent`s from the `removalCharacteristicEvents`
|
||||
array and stores any errors that accumulate.
|
||||
*/
|
||||
private func removePendingEventsFromTrigger() {
|
||||
guard let eventTrigger = eventTrigger else { return }
|
||||
for event in removalCharacteristicEvents {
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
eventTrigger.removeEvent(event) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
removalCharacteristicEvents.removeAll()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// All `HMCharacteristic`s in the `targetValueMap`.
|
||||
private var allCharacteristics: [HMCharacteristic] {
|
||||
var characteristics = Set<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `CharacteristicTriggerViewController` allows the user to create a characteristic trigger.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller which facilitates the creation of characteristic triggers.
|
||||
class CharacteristicTriggerViewController: EventTriggerViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let selectCharacteristicSegue = "Select Characteristic"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private var characteristicTriggerCreator: CharacteristicTriggerCreator {
|
||||
return triggerCreator as! CharacteristicTriggerCreator
|
||||
}
|
||||
|
||||
var eventTrigger: HMEventTrigger? {
|
||||
return trigger as? HMEventTrigger
|
||||
}
|
||||
|
||||
/// An internal array of `HMCharacteristicEvent`s to save into the trigger.
|
||||
private var events = [HMCharacteristicEvent]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Creates the trigger creator.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
triggerCreator = CharacteristicTriggerCreator(trigger: eventTrigger, home: home)
|
||||
}
|
||||
|
||||
/// Reloads the internal data.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// Passes our event trigger and trigger creator to the `CharacteristicSelectionViewController`
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
if segue.identifier == Identifiers.selectCharacteristicSegue {
|
||||
if let destinationVC = segue.intendedDestinationViewController as? CharacteristicSelectionViewController {
|
||||
destinationVC.eventTrigger = eventTrigger
|
||||
destinationVC.triggerCreator = characteristicTriggerCreator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
- returns: The characteristic events for the Characteristics section.
|
||||
Defaults to super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sectionForIndex(section) {
|
||||
case .Characteristics?:
|
||||
// Plus one for the add row.
|
||||
return events.count + 1
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Switches based on cell type to generate the correct cell for the index path.
|
||||
Defaults to super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
if indexPathIsAdd(indexPath) {
|
||||
return self.tableView(tableView, addCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Characteristics?:
|
||||
return self.tableView(tableView, conditionCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A 'condition cell' with the event at the specified index path.
|
||||
private func tableView(tableView: UITableView, conditionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.conditionCell, forIndexPath: indexPath) as! ConditionCell
|
||||
let event = events[indexPath.row]
|
||||
cell.setCharacteristic(event.characteristic, targetValue: event.triggerValue!)
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: An 'add cell' with localized text.
|
||||
Defaults to super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Characteristics?:
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.addCell, forIndexPath: indexPath)
|
||||
cell.textLabel?.text = NSLocalizedString("Add Characteristic…", comment: "Add Characteristic")
|
||||
cell.textLabel?.textColor = UIColor.editableBlueColor()
|
||||
return cell
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, addCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handles the selection of characteristic events.
|
||||
Defaults to super implementation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Characteristics?:
|
||||
if indexPathIsAdd(indexPath) {
|
||||
addEvent()
|
||||
return
|
||||
}
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath)
|
||||
performSegueWithIdentifier(Identifiers.selectCharacteristicSegue, sender: cell)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` for characteristic cells,
|
||||
otherwise defaults to super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
if indexPathIsAdd(indexPath) {
|
||||
return false
|
||||
}
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Characteristics?:
|
||||
return true
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, canEditRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes events from the trigger creator.
|
||||
Defaults to super implementation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Characteristics?:
|
||||
characteristicTriggerCreator.removeEvent(events[indexPath.row])
|
||||
events = characteristicTriggerCreator.events
|
||||
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
super.tableView(tableView, commitEditingStyle: editingStyle, forRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A localized description of characteristic events
|
||||
Defaults to super implementation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch sectionForIndex(section) {
|
||||
case .Characteristics?:
|
||||
return NSLocalizedString("This trigger will activate when any of these characteristics change to their value. For example, 'run when the garage door is opened'.", comment: "Characteristic Trigger Description")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, titleForFooterInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Resets the internal events array from the trigger creator.
|
||||
private func reloadData() {
|
||||
events = characteristicTriggerCreator.events
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// Performs a segue to the `CharacteristicSelectionViewController`.
|
||||
private func addEvent() {
|
||||
characteristicTriggerCreator.mode = .Event
|
||||
self.performSegueWithIdentifier(Identifiers.selectCharacteristicSegue, sender: nil)
|
||||
}
|
||||
|
||||
/// - returns: `true` if the section is the Characteristic 'add row'; otherwise defaults to super implementation.
|
||||
override func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Characteristics?:
|
||||
return indexPath.row == events.count
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.indexPathIsAdd(indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Trigger Controller Methods
|
||||
|
||||
/**
|
||||
- parameter index: The section index.
|
||||
|
||||
- returns: The `TriggerTableViewSection` for the given index.
|
||||
*/
|
||||
override func sectionForIndex(index: Int) -> TriggerTableViewSection? {
|
||||
switch index {
|
||||
case 0:
|
||||
return .Name
|
||||
|
||||
case 1:
|
||||
return .Enabled
|
||||
|
||||
case 2:
|
||||
return .Characteristics
|
||||
|
||||
case 3:
|
||||
return .Conditions
|
||||
|
||||
case 4:
|
||||
return .ActionSets
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ConditionCell` displays characteristic and location conditions.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A `UITableViewCell` subclass that displays a trigger condition.
|
||||
class ConditionCell: UITableViewCell {
|
||||
/// A static, short date formatter.
|
||||
static let dateFormatter: NSDateFormatter = {
|
||||
let dateFormatter = NSDateFormatter()
|
||||
dateFormatter.dateStyle = .NoStyle
|
||||
dateFormatter.timeStyle = .ShortStyle
|
||||
dateFormatter.locale = NSLocale.currentLocale()
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
/// Ignores the passed-in style and overrides it with .Subtitle.
|
||||
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .None
|
||||
detailTextLabel?.textColor = UIColor.lightGrayColor()
|
||||
accessoryType = .None
|
||||
}
|
||||
|
||||
/// Required because we overwrote a designated initializer.
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the cell's text to represent a characteristic and target value.
|
||||
For example, "Brightness → 60%"
|
||||
Sets the subtitle to the service and accessory that this characteristic represents.
|
||||
|
||||
- parameter characteristic: The characteristic this cell represents.
|
||||
- parameter targetValue: The target value from this action.
|
||||
*/
|
||||
func setCharacteristic(characteristic: HMCharacteristic, targetValue: AnyObject) {
|
||||
let targetDescription = "\(characteristic.localizedDescription) → \(characteristic.localizedDescriptionForValue(targetValue))"
|
||||
textLabel?.text = targetDescription
|
||||
|
||||
let contextDescription = NSLocalizedString("%@ in %@", comment: "Service in Accessory")
|
||||
if let service = characteristic.service, accessory = service.accessory {
|
||||
detailTextLabel?.text = String(format: contextDescription, service.name, accessory.name)
|
||||
}
|
||||
else {
|
||||
detailTextLabel?.text = NSLocalizedString("Unknown Characteristic", comment: "Unknown Characteristic")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the cell's text to represent an ordered time with a set context string.
|
||||
|
||||
- parameter order: A `TimeConditionOrder` which will map to a localized string.
|
||||
- parameter timeString: The localized time string.
|
||||
- parameter contextString: A localized string describing the time type.
|
||||
*/
|
||||
private func setOrder(order: TimeConditionOrder, timeString: String, contextString: String) {
|
||||
let formatString: String
|
||||
switch order {
|
||||
case .Before:
|
||||
formatString = NSLocalizedString("Before %@", comment: "Before Time")
|
||||
|
||||
case .After:
|
||||
formatString = NSLocalizedString("After %@", comment: "After Time")
|
||||
|
||||
case .At:
|
||||
formatString = NSLocalizedString("At %@", comment: "At Time")
|
||||
}
|
||||
textLabel?.text = String(format: formatString, timeString)
|
||||
detailTextLabel?.text = contextString
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the cell's text to represent an exact time condition.
|
||||
|
||||
- parameter order: A `TimeConditionOrder` which will map to a localized string.
|
||||
- parameter dateComponents: The date components of the exact time.
|
||||
*/
|
||||
func setOrder(order: TimeConditionOrder, dateComponents: NSDateComponents) {
|
||||
let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents)
|
||||
let timeString = ConditionCell.dateFormatter.stringFromDate(date!)
|
||||
setOrder(order, timeString: timeString, contextString: NSLocalizedString("Relative to Time", comment: "Relative to Time"))
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the cell's text to represent a solar event time condition.
|
||||
|
||||
- parameter order: A `TimeConditionOrder` which will map to a localized string.
|
||||
- parameter sunState: A `TimeConditionSunState` which will map to localized string.
|
||||
*/
|
||||
func setOrder(order: TimeConditionOrder, sunState: TimeConditionSunState) {
|
||||
let timeString: String
|
||||
switch sunState {
|
||||
case .Sunrise:
|
||||
timeString = NSLocalizedString("Sunrise", comment: "Sunrise")
|
||||
|
||||
case .Sunset:
|
||||
timeString = NSLocalizedString("Sunset", comment: "Sunset")
|
||||
}
|
||||
setOrder(order, timeString: timeString , contextString: NSLocalizedString("Relative to sun", comment: "Relative to Sun"))
|
||||
}
|
||||
|
||||
/// Sets the cell's text to indicate the given condition is not handled by the app.
|
||||
func setUnknown() {
|
||||
let unknownString = NSLocalizedString("Unknown Condition", comment: "Unknown Condition")
|
||||
detailTextLabel?.text = unknownString
|
||||
textLabel?.text = unknownString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `SegmentedTimeCell` has a segmented control, used for selecting the time type.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
/// A `UITableViewCell` subclass with a `UISegmentedControl`, used for selecting the time type.
|
||||
class SegmentedTimeCell: UITableViewCell {
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var segmentedControl: UISegmentedControl!
|
||||
}
|
|
@ -0,0 +1,350 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TimeConditionViewController` allows the user to create a new time condition.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents a section in the `TimeConditionViewController`.
|
||||
enum TimeConditionTableViewSection: Int {
|
||||
/**
|
||||
This section contains the segmented control to
|
||||
choose a time condition type.
|
||||
*/
|
||||
case TimeOrSun
|
||||
|
||||
/**
|
||||
This section contains cells to allow the selection
|
||||
of 'before', 'after', or 'at'. 'At' is only available
|
||||
when the exact time is specified.
|
||||
*/
|
||||
case BeforeOrAfter
|
||||
|
||||
/**
|
||||
If the condition type is exact time, this section will
|
||||
only have one cell, the date picker cell.
|
||||
|
||||
If the condition type is relative to a solar event,
|
||||
this section will have two cells, one for 'sunrise' and
|
||||
one for 'sunset.
|
||||
*/
|
||||
case Value
|
||||
|
||||
static let count = 3
|
||||
}
|
||||
|
||||
/**
|
||||
Represents the type of time condition.
|
||||
|
||||
The condition can be an exact time, or relative to a solar event.
|
||||
*/
|
||||
enum TimeConditionType: Int {
|
||||
case Time, Sun
|
||||
}
|
||||
|
||||
/**
|
||||
Represents the type of solar event.
|
||||
|
||||
This can be sunrise or sunset.
|
||||
*/
|
||||
enum TimeConditionSunState: Int {
|
||||
case Sunrise, Sunset
|
||||
}
|
||||
|
||||
/**
|
||||
Represents the condition order.
|
||||
|
||||
Conditions can be before, after, or exactly at a given time.
|
||||
*/
|
||||
enum TimeConditionOrder: Int {
|
||||
case Before, After, At
|
||||
}
|
||||
|
||||
/// A view controller that facilitates the creation of time conditions for triggers.
|
||||
class TimeConditionViewController: HMCatalogViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let selectionCell = "SelectionCell"
|
||||
static let timePickerCell = "TimePickerCell"
|
||||
static let segmentedTimeCell = "SegmentedTimeCell"
|
||||
}
|
||||
|
||||
static let timeOrSunTitles = [
|
||||
NSLocalizedString("Relative to time", comment: "Relative to time"),
|
||||
NSLocalizedString("Relative to sun", comment: "Relative to sun")
|
||||
]
|
||||
|
||||
static let beforeOrAfterTitles = [
|
||||
NSLocalizedString("Before", comment: "Before"),
|
||||
NSLocalizedString("After", comment: "After"),
|
||||
NSLocalizedString("At", comment: "At")
|
||||
]
|
||||
|
||||
static let sunriseSunsetTitles = [
|
||||
NSLocalizedString("Sunrise", comment: "Sunrise"),
|
||||
NSLocalizedString("Sunset", comment: "Sunset")
|
||||
]
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private var timeType: TimeConditionType = .Time
|
||||
private var order: TimeConditionOrder = .Before
|
||||
private var sunState: TimeConditionSunState = .Sunrise
|
||||
|
||||
private var datePicker: UIDatePicker?
|
||||
|
||||
var triggerCreator: EventTriggerCreator?
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the table view.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of `TimeConditionTableViewSection`s.
|
||||
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
|
||||
return TimeConditionTableViewSection.count
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: The number rows based on the `TimeConditionTableViewSection`
|
||||
and the `timeType`.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch TimeConditionTableViewSection(rawValue: section) {
|
||||
case .TimeOrSun?:
|
||||
return 1
|
||||
|
||||
case .BeforeOrAfter?:
|
||||
// If we're choosing an exact time, we add the 'At' row.
|
||||
return (timeType == .Time) ? 3 : 2
|
||||
|
||||
case .Value?:
|
||||
// Date picker cell or sunrise/sunset selection cells
|
||||
return (timeType == .Time) ? 1 : 2
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Switches based on the section to generate a cell.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch TimeConditionTableViewSection(rawValue: indexPath.section) {
|
||||
case .TimeOrSun?:
|
||||
return self.tableView(tableView, segmentedCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .BeforeOrAfter?:
|
||||
return self.tableView(tableView, selectionCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .Value?:
|
||||
switch timeType {
|
||||
case .Time:
|
||||
return self.tableView(tableView, datePickerCellForRowAtIndexPath: indexPath)
|
||||
case .Sun:
|
||||
return self.tableView(tableView, selectionCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A localized string describing the section.
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch TimeConditionTableViewSection(rawValue: section) {
|
||||
case .TimeOrSun?:
|
||||
return NSLocalizedString("Condition Type", comment: "Condition Type")
|
||||
|
||||
case .BeforeOrAfter?:
|
||||
return nil
|
||||
|
||||
case .Value?:
|
||||
if timeType == .Time {
|
||||
return NSLocalizedString("Time", comment: "Time")
|
||||
}
|
||||
else {
|
||||
return NSLocalizedString("Event", comment: "Event")
|
||||
}
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A localized description for condition type section; `nil` otherwise.
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch TimeConditionTableViewSection(rawValue: section) {
|
||||
case .TimeOrSun?:
|
||||
return NSLocalizedString("Time conditions can relate to specific times or special events, like sunrise and sunset.", comment: "Condition Type Description")
|
||||
|
||||
case .BeforeOrAfter?:
|
||||
return nil
|
||||
|
||||
case .Value?:
|
||||
return nil
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates internal values based on row selection.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
let cell = tableView.cellForRowAtIndexPath(indexPath)!
|
||||
if cell.selectionStyle == .None {
|
||||
return
|
||||
}
|
||||
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
|
||||
switch TimeConditionTableViewSection(rawValue: indexPath.section) {
|
||||
case .TimeOrSun?:
|
||||
timeType = TimeConditionType(rawValue: indexPath.row)!
|
||||
reloadDynamicSections()
|
||||
return
|
||||
|
||||
case .BeforeOrAfter?:
|
||||
order = TimeConditionOrder(rawValue: indexPath.row)!
|
||||
tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
|
||||
|
||||
case .Value?:
|
||||
if timeType == .Sun {
|
||||
sunState = TimeConditionSunState(rawValue: indexPath.row)!
|
||||
}
|
||||
tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Generates a selection cell based on the section.
|
||||
Ordering and sun-state sections have selections.
|
||||
*/
|
||||
private func tableView(tableView: UITableView, selectionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.selectionCell, forIndexPath: indexPath)
|
||||
switch TimeConditionTableViewSection(rawValue: indexPath.section) {
|
||||
case .BeforeOrAfter?:
|
||||
cell.textLabel?.text = TimeConditionViewController.beforeOrAfterTitles[indexPath.row]
|
||||
cell.accessoryType = (order.rawValue == indexPath.row) ? .Checkmark : .None
|
||||
|
||||
case .Value?:
|
||||
if timeType == .Sun {
|
||||
cell.textLabel?.text = TimeConditionViewController.sunriseSunsetTitles[indexPath.row]
|
||||
cell.accessoryType = (sunState.rawValue == indexPath.row) ? .Checkmark : .None
|
||||
}
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TimeConditionTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Generates a date picker cell and sets the internal date picker when created.
|
||||
private func tableView(tableView: UITableView, datePickerCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.timePickerCell, forIndexPath: indexPath) as! TimePickerCell
|
||||
// Save the date picker so we can get the result later.
|
||||
datePicker = cell.datePicker
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Generates a segmented cell and sets its target when created.
|
||||
private func tableView(tableView: UITableView, segmentedCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.segmentedTimeCell, forIndexPath: indexPath) as! SegmentedTimeCell
|
||||
cell.segmentedControl.selectedSegmentIndex = timeType.rawValue
|
||||
cell.segmentedControl.removeTarget(nil, action: nil, forControlEvents: .AllEvents)
|
||||
cell.segmentedControl.addTarget(self, action: #selector(TimeConditionViewController.segmentedControlDidChange(_:)), forControlEvents: .ValueChanged)
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Creates date components from the date picker's date.
|
||||
var dateComponents: NSDateComponents? {
|
||||
guard let datePicker = datePicker else { return nil }
|
||||
let flags: NSCalendarUnit = [.Hour, .Minute]
|
||||
return NSCalendar.currentCalendar().components(flags, fromDate: datePicker.date)
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the time type and reloads dynamic sections.
|
||||
|
||||
- parameter segmentedControl: The segmented control that changed.
|
||||
*/
|
||||
func segmentedControlDidChange(segmentedControl: UISegmentedControl) {
|
||||
if let segmentedControlType = TimeConditionType(rawValue: segmentedControl.selectedSegmentIndex) {
|
||||
timeType = segmentedControlType
|
||||
}
|
||||
reloadDynamicSections()
|
||||
}
|
||||
|
||||
/// Reloads the BeforeOrAfter and Value section.
|
||||
private func reloadDynamicSections() {
|
||||
if timeType == .Sun && order == .At {
|
||||
order = .Before
|
||||
}
|
||||
let reloadIndexSet = NSIndexSet(indexesInRange: NSMakeRange(TimeConditionTableViewSection.BeforeOrAfter.rawValue, 2))
|
||||
tableView.reloadSections(reloadIndexSet, withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/**
|
||||
Generates a predicate based on the stored values, adds
|
||||
the condition to the trigger, then dismisses the view.
|
||||
*/
|
||||
@IBAction func saveAndDismiss(sender: UIBarButtonItem) {
|
||||
var predicate: NSPredicate?
|
||||
switch timeType {
|
||||
case .Time:
|
||||
switch order {
|
||||
case .Before:
|
||||
predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringBeforeDateWithComponents(dateComponents!)
|
||||
|
||||
case .After:
|
||||
predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringAfterDateWithComponents(dateComponents!)
|
||||
|
||||
case .At:
|
||||
predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringOnDateWithComponents(dateComponents!)
|
||||
}
|
||||
|
||||
case .Sun:
|
||||
let significantEventString = (sunState == .Sunrise) ? HMSignificantEventSunrise : HMSignificantEventSunset
|
||||
switch order {
|
||||
case .Before:
|
||||
predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringBeforeSignificantEvent(significantEventString, applyingOffset: nil)
|
||||
|
||||
case .After:
|
||||
predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringAfterSignificantEvent(significantEventString, applyingOffset: nil)
|
||||
|
||||
case .At:
|
||||
// Significant events must be specified 'before' or 'after'.
|
||||
break
|
||||
}
|
||||
}
|
||||
if let predicate = predicate {
|
||||
triggerCreator?.addCondition(predicate)
|
||||
}
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
/// Cancels the creation of the conditions and exits.
|
||||
@IBAction func dismiss(sender: UIBarButtonItem) {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TimePickerCell` has a date picker, used for selecting a specific time of day.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A `UITableViewCell` subclass with a `UIDatePicker`, used for selecting a specific time of day.
|
||||
class TimePickerCell: UITableViewCell {
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var datePicker: UIDatePicker!
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `EventTriggerCreator` is a superclass that creates Characteristic and Location triggers.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A superclass for event trigger creators.
|
||||
|
||||
These classes manage the state for characteristic trigger conditions.
|
||||
*/
|
||||
class EventTriggerCreator: TriggerCreator, CharacteristicCellDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
/// A mapping of `HMCharacteristic`s to their values.
|
||||
private let conditionValueMap = NSMapTable.strongToStrongObjectsMapTable()
|
||||
|
||||
private var eventTrigger: HMEventTrigger? {
|
||||
return trigger as? HMEventTrigger
|
||||
}
|
||||
|
||||
/**
|
||||
An array of top-level `NSPredicate` objects.
|
||||
|
||||
Currently, HMCatalog only supports top-level `NSPredicate`s
|
||||
which have type `AndPredicateType`.
|
||||
*/
|
||||
var originalConditions: [NSPredicate] {
|
||||
if let compoundPredicate = eventTrigger?.predicate as? NSCompoundPredicate,
|
||||
subpredicates = compoundPredicate.subpredicates as? [NSPredicate] {
|
||||
return subpredicates
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/// An array of new conditions which will be written when the trigger is saved.
|
||||
lazy var conditions: [NSPredicate] = self.originalConditions
|
||||
|
||||
/**
|
||||
Adds a predicate to the pending conditions.
|
||||
|
||||
- parameter predicate: The new `NSPredicate` to add.
|
||||
*/
|
||||
func addCondition(predicate: NSPredicate) {
|
||||
conditions.append(predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a predicate from the pending conditions.
|
||||
|
||||
- parameter predicate: The `NSPredicate` to remove.
|
||||
*/
|
||||
func removeCondition(predicate: NSPredicate) {
|
||||
if let index = conditions.indexOf(predicate) {
|
||||
conditions.removeAtIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: The new `NSCompoundPredicate`, generated from
|
||||
the pending conditions.
|
||||
*/
|
||||
func newPredicate() -> NSPredicate {
|
||||
return NSCompoundPredicate(type: .AndPredicateType, subpredicates: conditions)
|
||||
}
|
||||
|
||||
/// Handles the value update and stores the value in the condition map.
|
||||
func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) {
|
||||
conditionValueMap.setObject(value, forKey: characteristic)
|
||||
}
|
||||
|
||||
/**
|
||||
Tries to use the value from the condition-value map, but falls back
|
||||
to reading the characteristic's value from HomeKit.
|
||||
*/
|
||||
func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) {
|
||||
if let value = conditionValueMap.objectForKey(characteristic) {
|
||||
completion(value, nil)
|
||||
return
|
||||
}
|
||||
|
||||
characteristic.readValueWithCompletionHandler { error in
|
||||
/*
|
||||
The user may have updated the cell value while the
|
||||
read was happening. We check the map one more time.
|
||||
*/
|
||||
if let value = self.conditionValueMap.objectForKey(characteristic) {
|
||||
completion(value, nil)
|
||||
}
|
||||
else {
|
||||
completion(characteristic.value, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Updates the predicates and saves the new, generated
|
||||
predicate to the event trigger.
|
||||
*/
|
||||
func savePredicate() {
|
||||
updatePredicates()
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
eventTrigger?.updatePredicate(newPredicate()) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates predicates from the characteristic-value map and adds them to the pending conditions.
|
||||
func updatePredicates() {
|
||||
for (characteristic, value) in pairsFromMapTable(conditionValueMap) {
|
||||
let predicate = HMEventTrigger.predicateForEvaluatingTriggerWithCharacteristic(characteristic, relatedBy: .EqualToPredicateOperatorType, toValue: value)
|
||||
addCondition(predicate)
|
||||
}
|
||||
|
||||
conditionValueMap.removeAllObjects()
|
||||
}
|
||||
|
||||
/**
|
||||
- parameter table: The `NSMapTable` from which to generate the pairs.
|
||||
|
||||
- returns: Tuples representing `HMCharacteristic`s and their associated return trigger values.
|
||||
*/
|
||||
func pairsFromMapTable(table: NSMapTable) -> [(HMCharacteristic, NSCopying)] {
|
||||
return table.keyEnumerator().allObjects.map { object in
|
||||
let characteristic = object as! HMCharacteristic
|
||||
let triggerValue = table.objectForKey(object) as! NSCopying
|
||||
return (characteristic, triggerValue)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `EventTriggerViewController` is a superclass that helps users create Characteristic and Location triggers.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A superclass for all event-based view controllers.
|
||||
|
||||
It handles the process of creating and managing trigger conditions.
|
||||
*/
|
||||
class EventTriggerViewController: TriggerViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let addCell = "AddCell"
|
||||
static let conditionCell = "ConditionCell"
|
||||
static let showTimeConditionSegue = "Show Time Condition"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private var eventTriggerCreator: EventTriggerCreator {
|
||||
return triggerCreator as! EventTriggerCreator
|
||||
}
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Registers table view for cells.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier:Identifiers.addCell)
|
||||
tableView.registerClass(ConditionCell.self, forCellReuseIdentifier:Identifiers.conditionCell)
|
||||
}
|
||||
|
||||
/// Hands off the trigger creator to the condition view controllers.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
switch segue.intendedDestinationViewController {
|
||||
case let timeVC as TimeConditionViewController:
|
||||
timeVC.triggerCreator = eventTriggerCreator
|
||||
|
||||
case let characteristicEventVC as CharacteristicSelectionViewController:
|
||||
let characteristicTriggerCreator = triggerCreator as! EventTriggerCreator
|
||||
characteristicEventVC.triggerCreator = characteristicTriggerCreator
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
- returns: In the conditions section: the number of conditions, plus one
|
||||
for the add row. Defaults to the super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sectionForIndex(section) {
|
||||
case .Conditions?:
|
||||
// Add row.
|
||||
return eventTriggerCreator.conditions.count + 1
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Launchs "Add Condition" if the 'add index path' is selected.
|
||||
Defaults to the super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Conditions?:
|
||||
if indexPathIsAdd(indexPath) {
|
||||
addCondition()
|
||||
}
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Switches to select the correct type of cell for the section.
|
||||
Defaults to the super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
if indexPathIsAdd(indexPath) {
|
||||
return self.tableView(tableView, addCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Conditions?:
|
||||
return self.tableView(tableView, conditionCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The conditions can be removed, the 'add index path' cannot.
|
||||
For all others, default to super implementation.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
if indexPathIsAdd(indexPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Conditions?:
|
||||
return true
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the selected condition from the trigger creator.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
let predicate = eventTriggerCreator.conditions[indexPath.row]
|
||||
eventTriggerCreator.removeCondition(predicate)
|
||||
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: An 'add cell' with 'Add Condition' text.
|
||||
func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.addCell, forIndexPath: indexPath)
|
||||
let cellText: String
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Conditions?:
|
||||
cellText = NSLocalizedString("Add Condition…", comment: "Add Condition")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
cellText = NSLocalizedString("Add…", comment: "Add")
|
||||
}
|
||||
|
||||
cell.textLabel?.text = cellText
|
||||
cell.textLabel?.textColor = UIColor.editableBlueColor()
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// - returns: A localized description of a trigger. Falls back to super implementation.
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch sectionForIndex(section) {
|
||||
case .Conditions?:
|
||||
return NSLocalizedString("When a trigger is activated by an event, it checks these conditions. If all of them are true, it will set its scenes.", comment: "Trigger Conditions Description")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, titleForFooterInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// - returns: A 'condition cell', which displays information about the condition.
|
||||
private func tableView(tableView: UITableView, conditionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.conditionCell) as! ConditionCell
|
||||
let condition = eventTriggerCreator.conditions[indexPath.row]
|
||||
|
||||
switch condition.homeKitConditionType {
|
||||
case .Characteristic(let characteristic, let value):
|
||||
cell.setCharacteristic(characteristic, targetValue: value)
|
||||
|
||||
case .ExactTime(let order, let dateComponents):
|
||||
cell.setOrder(order, dateComponents: dateComponents)
|
||||
|
||||
case .SunTime(let order, let sunState):
|
||||
cell.setOrder(order, sunState: sunState)
|
||||
|
||||
case .Unknown:
|
||||
cell.setUnknown()
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Presents an alert controller to choose the type of trigger.
|
||||
private func addCondition() {
|
||||
let title = NSLocalizedString("Add Condition", comment: "Add Condition")
|
||||
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .ActionSheet)
|
||||
|
||||
// Time Condition.
|
||||
let timeAction = UIAlertAction(title: NSLocalizedString("Time", comment: "Time"), style: .Default) { _ in
|
||||
self.performSegueWithIdentifier(Identifiers.showTimeConditionSegue, sender: self)
|
||||
}
|
||||
alertController.addAction(timeAction)
|
||||
|
||||
// Characteristic trigger.
|
||||
let eventActionTitle = NSLocalizedString("Characteristic", comment: "Characteristic")
|
||||
|
||||
let eventAction = UIAlertAction(title: eventActionTitle, style: .Default, handler: { _ in
|
||||
if let triggerCreator = self.triggerCreator as? CharacteristicTriggerCreator {
|
||||
triggerCreator.mode = .Condition
|
||||
}
|
||||
self.performSegueWithIdentifier("Select Characteristic", sender: self)
|
||||
})
|
||||
|
||||
alertController.addAction(eventAction)
|
||||
|
||||
// Cancel.
|
||||
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
// Present alert.
|
||||
presentViewController(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
/// - returns: `true` if the index path is the 'add row'; `false` otherwise.
|
||||
func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Conditions?:
|
||||
return indexPath.row == eventTriggerCreator.conditions.count
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `LocationTriggerCreator` creates Location triggers.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
import MapKit
|
||||
|
||||
/**
|
||||
An `EventTriggerCreator` subclass which allows for the creation
|
||||
of location triggers.
|
||||
*/
|
||||
class LocationTriggerCreator: EventTriggerCreator, MapViewControllerDelegate {
|
||||
// MARK: Properties
|
||||
|
||||
var eventTrigger: HMEventTrigger? {
|
||||
return trigger as? HMEventTrigger
|
||||
}
|
||||
var locationEvent: HMLocationEvent?
|
||||
var targetRegion: CLCircularRegion?
|
||||
var targetRegionStateIndex = 0
|
||||
|
||||
// MARK: Trigger Creator Methods
|
||||
|
||||
/// Initializes location event, target region, and region state.
|
||||
required init(trigger: HMTrigger?, home: HMHome) {
|
||||
super.init(trigger: trigger, home: home)
|
||||
if let eventTrigger = eventTrigger {
|
||||
self.locationEvent = eventTrigger.locationEvent
|
||||
if let region = locationEvent?.region as? CLCircularRegion {
|
||||
self.targetRegion = region
|
||||
}
|
||||
self.targetRegionStateIndex = (self.targetRegion?.notifyOnEntry ?? true) ? 0 : 1
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a new region and updates the location event.
|
||||
override func updateTrigger() {
|
||||
if let region = targetRegion {
|
||||
prepareRegion()
|
||||
if let locationEvent = locationEvent {
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
locationEvent.updateRegion(region) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.savePredicate()
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A new `HMEventTrigger` with a new generated
|
||||
location event and predicate.
|
||||
*/
|
||||
override func newTrigger() -> HMTrigger? {
|
||||
var events = [HMLocationEvent]()
|
||||
if let region = targetRegion {
|
||||
prepareRegion()
|
||||
events.append(HMLocationEvent(region: region))
|
||||
}
|
||||
return HMEventTrigger(name: name, events: events, predicate: newPredicate())
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Sets the `notifyOnEntry` and `notifyOnExit` region
|
||||
properties based on the selected state.
|
||||
*/
|
||||
private func prepareRegion() {
|
||||
if let region = targetRegion {
|
||||
region.notifyOnEntry = (targetRegionStateIndex == 0)
|
||||
region.notifyOnExit = !region.notifyOnEntry
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the target region from the one provided
|
||||
by the delegate.
|
||||
|
||||
- parameter region: A new `CLCircularRegion`, provided by the delegate.
|
||||
*/
|
||||
func mapViewDidUpdateRegion(region: CLCircularRegion) {
|
||||
targetRegion = region
|
||||
}
|
||||
}
|
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `LocationTriggerViewController` allows the user to modify and create Location triggers.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import MapKit
|
||||
import HomeKit
|
||||
import AddressBookUI
|
||||
import Contacts
|
||||
|
||||
/// A view controller which facilitates the creation of a location trigger.
|
||||
class LocationTriggerViewController: EventTriggerViewController {
|
||||
|
||||
struct Identifiers {
|
||||
static let locationCell = "LocationCell"
|
||||
static let regionStatusCell = "RegionStatusCell"
|
||||
static let selectLocationSegue = "Select Location"
|
||||
}
|
||||
|
||||
static let geocoder = CLGeocoder()
|
||||
|
||||
static let regionStatusTitles = [
|
||||
NSLocalizedString("When I Enter The Area", comment: "When I Enter The Area"),
|
||||
NSLocalizedString("When I Leave The Area", comment: "When I Leave The Area")
|
||||
]
|
||||
|
||||
var locationTriggerCreator: LocationTriggerCreator {
|
||||
return triggerCreator as! LocationTriggerCreator
|
||||
}
|
||||
|
||||
var localizedAddress: String?
|
||||
|
||||
var viewIsDisplayed = false
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Initializes a trigger creator and registers for table view cells.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
triggerCreator = LocationTriggerCreator(trigger: trigger, home: home)
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.locationCell)
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.regionStatusCell)
|
||||
}
|
||||
|
||||
/**
|
||||
Generates an address string for the current region location and
|
||||
reloads the table view.
|
||||
*/
|
||||
override func viewDidAppear(animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
viewIsDisplayed = true
|
||||
if let region = locationTriggerCreator.targetRegion {
|
||||
let centerLocation = CLLocation(latitude: region.center.latitude, longitude: region.center.longitude)
|
||||
LocationTriggerViewController.geocoder.reverseGeocodeLocation(centerLocation) { placemarks, error in
|
||||
if !self.viewIsDisplayed {
|
||||
// The geocoder took too long, we're not on this view any more.
|
||||
return
|
||||
}
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
return
|
||||
}
|
||||
if let mostLikelyPlacemark = placemarks?.first {
|
||||
let address = CNMutablePostalAddress(placemark: mostLikelyPlacemark)
|
||||
let addressFormatter = CNPostalAddressFormatter()
|
||||
let addressString = addressFormatter.stringFromPostalAddress(address)
|
||||
self.localizedAddress = addressString.stringByReplacingOccurrencesOfString("\n", withString: ", ")
|
||||
let section = NSIndexSet(index: 2)
|
||||
self.tableView.reloadSections(section, withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
}
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// Passes the trigger creator and region into the `MapViewController`.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
if segue.identifier == Identifiers.selectLocationSegue {
|
||||
guard let destinationVC = segue.intendedDestinationViewController as? MapViewController else { return }
|
||||
// Give the map the previous target region (if exists).
|
||||
destinationVC.targetRegion = locationTriggerCreator.targetRegion
|
||||
destinationVC.delegate = locationTriggerCreator
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
viewIsDisplayed = false
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
- returns: The number of rows in the Region section;
|
||||
defaults to the super implementation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sectionForIndex(section) {
|
||||
case .Region?:
|
||||
return 2
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generates a cell based on the section.
|
||||
Handles Region and Location sections, defaults to
|
||||
super implementations for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Region?:
|
||||
return self.tableView(tableView, regionStatusCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case .Location?:
|
||||
return self.tableView(tableView, locationCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the single location cell.
|
||||
private func tableView(tableView: UITableView, locationCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.locationCell, forIndexPath: indexPath)
|
||||
cell.accessoryType = .DisclosureIndicator
|
||||
|
||||
if locationTriggerCreator.targetRegion != nil {
|
||||
cell.textLabel?.text = localizedAddress ?? NSLocalizedString("Update Location", comment: "Update Location")
|
||||
}
|
||||
else {
|
||||
cell.textLabel?.text = NSLocalizedString("Set Location", comment: "Set Location")
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Generates the cell which allow the user to select either 'on enter' or 'on exit'.
|
||||
private func tableView(tableView: UITableView, regionStatusCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.regionStatusCell, forIndexPath: indexPath)
|
||||
cell.textLabel?.text = LocationTriggerViewController.regionStatusTitles[indexPath.row]
|
||||
cell.accessoryType = (locationTriggerCreator.targetRegionStateIndex == indexPath.row) ? .Checkmark : .None
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Allows the user to select a location or change the region status.
|
||||
Defaults to the super implmentation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Location?:
|
||||
performSegueWithIdentifier(Identifiers.selectLocationSegue, sender: self)
|
||||
|
||||
case .Region?:
|
||||
locationTriggerCreator.targetRegionStateIndex = indexPath.row
|
||||
let reloadIndexSet = NSIndexSet(index: indexPath.section)
|
||||
tableView.reloadSections(reloadIndexSet, withRowAnimation: .Automatic)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A localized title for the Location and Region sections.
|
||||
Defaults to the super implmentation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch sectionForIndex(section) {
|
||||
case .Location?:
|
||||
return NSLocalizedString("Location", comment: "Location")
|
||||
|
||||
case .Region?:
|
||||
return NSLocalizedString("Region Status", comment: "Region Status")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, titleForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: A localized description of the region status.
|
||||
Defaults to the super implmentation for other sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch sectionForIndex(section) {
|
||||
case .Region?:
|
||||
return NSLocalizedString("This trigger can activate when you enter or leave a region. For example, when you arrive at home or when you leave work.", comment: "Location Region Description")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, titleForFooterInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Trigger Controller Methods
|
||||
|
||||
/**
|
||||
- parameter index: The section index.
|
||||
|
||||
- returns: The `TriggerTableViewSection` for the given index.
|
||||
*/
|
||||
override func sectionForIndex(index: Int) -> TriggerTableViewSection? {
|
||||
switch index {
|
||||
case 0:
|
||||
return .Name
|
||||
|
||||
case 1:
|
||||
return .Enabled
|
||||
|
||||
case 2:
|
||||
return .Location
|
||||
|
||||
case 3:
|
||||
return .Region
|
||||
|
||||
case 4:
|
||||
return .Conditions
|
||||
|
||||
case 5:
|
||||
return .ActionSets
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `MapOverlayView` draws the circle over the `MapViewController`.
|
||||
*/
|
||||
|
||||
import MapKit
|
||||
|
||||
/**
|
||||
A simple `UIView` subclass to draw a selection circle over
|
||||
a MKMapView of the same size.
|
||||
*/
|
||||
class MapOverlayView: UIView {
|
||||
|
||||
/**
|
||||
Draws a dashed circle in the center of the `rect` with
|
||||
a radius 1/4th of the `rect`'s smallest side.
|
||||
*/
|
||||
override func drawRect(rect: CGRect) {
|
||||
super.drawRect(rect)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
|
||||
let strokeColor = UIColor.blueColor()
|
||||
|
||||
let circleDiameter: CGFloat = min(rect.width, rect.height) / 2.0
|
||||
let circleRadius = circleDiameter / 2.0
|
||||
let cirlceRect = CGRect(x: rect.midX - circleRadius, y: rect.midY - circleRadius, width: circleDiameter, height: circleDiameter)
|
||||
let circlePath = UIBezierPath(ovalInRect: cirlceRect)
|
||||
|
||||
strokeColor.setStroke()
|
||||
circlePath.lineWidth = 3
|
||||
CGContextSaveGState(context!)
|
||||
CGContextSetLineDash(context!, 0, [6, 6], 2)
|
||||
circlePath.stroke()
|
||||
CGContextRestoreGState(context!)
|
||||
}
|
||||
|
||||
/// - returns: `false` to accept no touches.
|
||||
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `MapViewController` allow the user to select a location using the map.
|
||||
This location will be passed back to the sender when the user saves the view.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import MapKit
|
||||
|
||||
/**
|
||||
Allows the sender to get notified when there
|
||||
have been changes to the region.
|
||||
*/
|
||||
protocol MapViewControllerDelegate {
|
||||
/**
|
||||
Notifies the delegate that the `MapViewController`'s
|
||||
region has been updated.
|
||||
*/
|
||||
func mapViewDidUpdateRegion(region: CLCircularRegion)
|
||||
}
|
||||
|
||||
/**
|
||||
A view controller which allows the selection of a
|
||||
circular region on a map.
|
||||
*/
|
||||
class MapViewController: UIViewController, UISearchBarDelegate, CLLocationManagerDelegate, MKMapViewDelegate {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let circularRegion = "MapViewController.Region"
|
||||
}
|
||||
|
||||
/// When the view loads, we'll zoom to this longitude/latitude span delta.
|
||||
static let InitialZoomDelta: Double = 0.0015
|
||||
|
||||
/// When the view loads, we'll zoom into this span.
|
||||
static let InitialZoomSpan = MKCoordinateSpan(latitudeDelta: MapViewController.InitialZoomDelta, longitudeDelta: MapViewController.InitialZoomDelta)
|
||||
|
||||
// The inverse of the percentage of the map view that should be captured in the region.
|
||||
static let MapRegionFraction: Double = 4.0
|
||||
|
||||
// The size of the query region with respect to the map's zoom.
|
||||
static let RegionQueryDegreeMultiplier: Double = 5.0
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var overlayView: MapOverlayView!
|
||||
@IBOutlet weak var searchBar: UISearchBar!
|
||||
@IBOutlet weak var mapView: MKMapView!
|
||||
|
||||
var delegate: MapViewControllerDelegate?
|
||||
|
||||
var targetRegion: CLCircularRegion?
|
||||
|
||||
var circleOverlay: MKCircle? {
|
||||
didSet {
|
||||
// Remove the old overlay (if exists)
|
||||
if let oldOverlay = oldValue {
|
||||
mapView.removeOverlay(oldOverlay)
|
||||
}
|
||||
|
||||
// Add the new overlay (if exists)
|
||||
if let overlay = circleOverlay {
|
||||
mapView.addOverlay(overlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var locationManager = CLLocationManager()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the map view, search bar and location manager.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
searchBar.delegate = self
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.pitchEnabled = false
|
||||
locationManager.delegate = self
|
||||
}
|
||||
|
||||
/// Loads the user's location and zooms the target region.
|
||||
override func viewDidAppear(animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
locationManager.requestLocation()
|
||||
|
||||
if let region = targetRegion {
|
||||
annotateAndZoomToRegion(region)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the overlay when the orientation changes.
|
||||
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
|
||||
overlayView.setNeedsDisplay()
|
||||
}
|
||||
|
||||
// MARK: Button Actions
|
||||
|
||||
/**
|
||||
Generates a map region based on the map's position
|
||||
and zoom, then notifies the delegate that the region has changed.
|
||||
This will dismiss the view.
|
||||
*/
|
||||
@IBAction func didTapSaveButton(sender: UIBarButtonItem) {
|
||||
let circleDegreeDelta: CLLocationDegrees
|
||||
let pointOnCircle: CLLocation
|
||||
|
||||
if mapView.region.span.latitudeDelta > mapView.region.span.longitudeDelta {
|
||||
circleDegreeDelta = mapView.region.span.longitudeDelta / MapViewController.MapRegionFraction
|
||||
pointOnCircle = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude - circleDegreeDelta)
|
||||
}
|
||||
else {
|
||||
circleDegreeDelta = mapView.region.span.latitudeDelta / MapViewController.MapRegionFraction
|
||||
pointOnCircle = CLLocation(latitude: mapView.region.center.latitude - circleDegreeDelta, longitude: mapView.region.center.longitude)
|
||||
}
|
||||
|
||||
|
||||
let mapCenterLocation = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude)
|
||||
let distance = pointOnCircle.distanceFromLocation(mapCenterLocation)
|
||||
let genericRegion = CLCircularRegion(center: mapView.region.center, radius: distance, identifier: Identifiers.circularRegion)
|
||||
|
||||
circleOverlay = MKCircle(centerCoordinate: genericRegion.center, radius: genericRegion.radius)
|
||||
delegate?.mapViewDidUpdateRegion(genericRegion)
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
/// Dismisses the view without notifying the delegate.
|
||||
@IBAction func didTapCancelButton(sender: UIBarButtonItem) {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Search Bar Methods
|
||||
|
||||
/**
|
||||
Dismisses the keyboard and runs a new search from the
|
||||
search bar.
|
||||
*/
|
||||
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
mapView.removeAnnotations(mapView.annotations)
|
||||
performSearch()
|
||||
}
|
||||
|
||||
// MARK: Location Manager Methods
|
||||
|
||||
/// Zooms to the user's location if the region is not set.
|
||||
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let lastLocation = locations.last else { return }
|
||||
if targetRegion != nil {
|
||||
// Do not zoom to the user's location if there is already a target region.
|
||||
return
|
||||
}
|
||||
let newRegion = MKCoordinateRegion(center: lastLocation.coordinate, span: MapViewController.InitialZoomSpan)
|
||||
mapView.setRegion(newRegion, animated: true)
|
||||
}
|
||||
|
||||
/**
|
||||
The method is required.
|
||||
Simply logs the error.
|
||||
*/
|
||||
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
|
||||
print("System: Location Manager Error: \(error)")
|
||||
}
|
||||
|
||||
/**
|
||||
When the user updates the authorization status, we want to
|
||||
zoom to their current location by asking for it.
|
||||
*/
|
||||
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
|
||||
locationManager.requestLocation()
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Updates the region overlay and zooms the map region
|
||||
|
||||
- parameter region: The new `CLCircularRegion`.
|
||||
*/
|
||||
private func annotateAndZoomToRegion(region: CLCircularRegion) {
|
||||
circleOverlay = MKCircle(centerCoordinate: region.center, radius: region.radius)
|
||||
let multiplier = MapViewController.MapRegionFraction
|
||||
let mapRegion = MKCoordinateRegionMakeWithDistance(region.center, region.radius*multiplier, region.radius*multiplier)
|
||||
mapView.setRegion(mapRegion, animated: false)
|
||||
}
|
||||
|
||||
/**
|
||||
Performs a natural language search for locations
|
||||
in the map's region that match the `searchBar`'s text.
|
||||
*/
|
||||
private func performSearch() {
|
||||
let request = MKLocalSearchRequest()
|
||||
request.naturalLanguageQuery = searchBar.text
|
||||
let multiplier = MapViewController.RegionQueryDegreeMultiplier
|
||||
let querySpan = MKCoordinateSpan(latitudeDelta: mapView.region.span.latitudeDelta*multiplier, longitudeDelta: mapView.region.span.longitudeDelta*multiplier)
|
||||
request.region = MKCoordinateRegion(center: mapView.region.center, span: querySpan)
|
||||
|
||||
let search = MKLocalSearch(request: request)
|
||||
|
||||
var matchingItems = [MKMapItem]()
|
||||
|
||||
search.startWithCompletionHandler { response, error in
|
||||
let mapItems: [MKMapItem] = response?.mapItems ?? []
|
||||
for item in mapItems {
|
||||
matchingItems.append(item)
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = item.placemark.coordinate
|
||||
annotation.title = item.name
|
||||
self.mapView.addAnnotation(annotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: An `MKOverlayRenderer` with our custom stroke and fill.
|
||||
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
|
||||
let circleRenderer = MKCircleRenderer(overlay: overlay)
|
||||
circleRenderer.fillColor = UIColor.blueColor().colorWithAlphaComponent(0.2)
|
||||
circleRenderer.strokeColor = UIColor.blackColor()
|
||||
circleRenderer.lineWidth = 2.0
|
||||
return circleRenderer
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TimerTriggerCreator` creates Timer triggers.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A `TriggerCreator` subclass which allows for the creation
|
||||
of timer triggers.
|
||||
*/
|
||||
class TimerTriggerCreator: TriggerCreator {
|
||||
static let RecurrenceComponents: [NSCalendarUnit] = [
|
||||
.Hour,
|
||||
.Day,
|
||||
.WeekOfYear
|
||||
]
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var timerTrigger: HMTimerTrigger? {
|
||||
return trigger as? HMTimerTrigger
|
||||
}
|
||||
|
||||
var selectedRecurrenceIndex = NSNotFound
|
||||
|
||||
var rawFireDate = NSDate()
|
||||
var fireDate: NSDate {
|
||||
let flags: NSCalendarUnit = [.Year, .Weekday, .Month, .Day, .Hour, .Minute]
|
||||
let dateComponents = NSCalendar.currentCalendar().components(flags, fromDate: self.rawFireDate)
|
||||
let probableDate = NSCalendar.currentCalendar().dateFromComponents(dateComponents)
|
||||
return probableDate ?? rawFireDate
|
||||
}
|
||||
|
||||
// MARK: Trigger Creator Methods
|
||||
|
||||
/// Configures raw fire date and selected recurrence index.
|
||||
required init(trigger: HMTrigger?, home: HMHome) {
|
||||
super.init(trigger: trigger, home: home)
|
||||
if let timerTrigger = timerTrigger {
|
||||
rawFireDate = timerTrigger.fireDate
|
||||
selectedRecurrenceIndex = recurrenceIndexFromDateComponents(timerTrigger.recurrence)
|
||||
}
|
||||
}
|
||||
|
||||
/// - returns: A new `HMTimerTrigger` with the stored configurations.
|
||||
override func newTrigger() -> HMTrigger? {
|
||||
return HMTimerTrigger(name: name, fireDate: fireDate, timeZone: NSCalendar.currentCalendar().timeZone, recurrence: recurrenceComponents, recurrenceCalendar: nil)
|
||||
}
|
||||
|
||||
/// Updates the fire date and recurrence of the trigger.
|
||||
override func updateTrigger() {
|
||||
updateFireDateIfNecessary()
|
||||
updateRecurrenceIfNecessary()
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Creates an NSDateComponent for the selected recurrence type.
|
||||
|
||||
- returns: An NSDateComponent where either `weekOfYear`,
|
||||
`hour`, or `day` is set to 1.
|
||||
*/
|
||||
var recurrenceComponents:NSDateComponents? {
|
||||
if selectedRecurrenceIndex == NSNotFound {
|
||||
return nil
|
||||
}
|
||||
let recurrenceComponents = NSDateComponents()
|
||||
let unit = TimerTriggerCreator.RecurrenceComponents[selectedRecurrenceIndex]
|
||||
switch unit {
|
||||
case NSCalendarUnit.WeekOfYear:
|
||||
recurrenceComponents.weekOfYear = 1
|
||||
|
||||
case NSCalendarUnit.Hour:
|
||||
recurrenceComponents.hour = 1
|
||||
|
||||
case NSCalendarUnit.Day:
|
||||
recurrenceComponents.day = 1
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
return recurrenceComponents
|
||||
}
|
||||
|
||||
/**
|
||||
Maps the possible calendar units associated with recurrence titles, so we can properly
|
||||
set our recurrenceUnit when an index is selected.
|
||||
|
||||
- parameter components: An optional `NSDateComponents` to query.
|
||||
|
||||
- returns: An index for the date components.
|
||||
*/
|
||||
func recurrenceIndexFromDateComponents(components: NSDateComponents?) -> Int {
|
||||
guard let components = components else { return NSNotFound }
|
||||
var unit: NSCalendarUnit?
|
||||
if components.day == 1 {
|
||||
unit = NSCalendarUnit.Day
|
||||
}
|
||||
else if components.weekOfYear == 1 {
|
||||
unit = NSCalendarUnit.WeekOfYear
|
||||
}
|
||||
else if components.hour == 1 {
|
||||
unit = NSCalendarUnit.Hour
|
||||
}
|
||||
if let unit = unit {
|
||||
return TimerTriggerCreator.RecurrenceComponents.indexOf(unit) ?? NSNotFound
|
||||
}
|
||||
return NSNotFound
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the trigger's fire date, entering and leaving the dispatch group if necessary.
|
||||
If the trigger's fire date is already equal to the passed-in fire date, this method does nothing.
|
||||
|
||||
- parameter fireDate: The trigger's new fire date.
|
||||
*/
|
||||
private func updateFireDateIfNecessary() {
|
||||
if timerTrigger?.fireDate == fireDate {
|
||||
return
|
||||
}
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
timerTrigger?.updateFireDate(fireDate) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the trigger's recurrence components, entering and leaving the dispatch group if necessary.
|
||||
If the trigger's components are already equal to the passed-in components, this method does nothing.
|
||||
|
||||
- parameter recurrenceComponents: The trigger's new recurrence components.
|
||||
*/
|
||||
private func updateRecurrenceIfNecessary() {
|
||||
if recurrenceComponents == timerTrigger?.recurrence {
|
||||
return
|
||||
}
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
timerTrigger?.updateRecurrence(recurrenceComponents) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TimerTriggerViewController` allows the user to create Timer triggers.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller which facilitates the creation of timer triggers.
|
||||
class TimerTriggerViewController: TriggerViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let recurrenceCell = "RecurrenceCell"
|
||||
}
|
||||
|
||||
static let RecurrenceTitles = [
|
||||
NSLocalizedString("Every Hour", comment: "Every Hour"),
|
||||
NSLocalizedString("Every Day", comment: "Every Day"),
|
||||
NSLocalizedString("Every Week", comment: "Every Week")
|
||||
]
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var datePicker: UIDatePicker!
|
||||
|
||||
/**
|
||||
Sets the stored fireDate to the new value.
|
||||
HomeKit only accepts dates aligned with minute boundaries,
|
||||
so we use NSDateComponents to only get the appropriate pieces of information from that date.
|
||||
Eventually we will end up with a date following this format: "MM/dd/yyyy hh:mm"
|
||||
*/
|
||||
|
||||
var timerTrigger: HMTimerTrigger? {
|
||||
return trigger as? HMTimerTrigger
|
||||
}
|
||||
|
||||
var timerTriggerCreator: TimerTriggerCreator {
|
||||
return triggerCreator as! TimerTriggerCreator
|
||||
}
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Configures the views and registers for table view cells.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
triggerCreator = TimerTriggerCreator(trigger: trigger, home: home)
|
||||
datePicker.date = timerTriggerCreator.fireDate
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.recurrenceCell)
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/// Reset our saved fire date to the date in the picker.
|
||||
@IBAction func didChangeDate(picker: UIDatePicker) {
|
||||
timerTriggerCreator.rawFireDate = picker.date
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/**
|
||||
- returns: The number of rows in the Recurrence section;
|
||||
defaults to the super implementation for other sections
|
||||
*/
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch sectionForIndex(section) {
|
||||
case .Recurrence?:
|
||||
return TimerTriggerViewController.RecurrenceTitles.count
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generates a recurrence cell.
|
||||
Defaults to the super implementation for other sections
|
||||
*/
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Recurrence?:
|
||||
return self.tableView(tableView, recurrenceCellForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a cell that represents a recurrence type.
|
||||
func tableView(tableView: UITableView, recurrenceCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.recurrenceCell, forIndexPath: indexPath)
|
||||
let title = TimerTriggerViewController.RecurrenceTitles[indexPath.row]
|
||||
cell.textLabel?.text = title
|
||||
|
||||
// The current preferred recurrence style should have a check mark.
|
||||
if indexPath.row == timerTriggerCreator.selectedRecurrenceIndex {
|
||||
cell.accessoryType = .Checkmark
|
||||
}
|
||||
else {
|
||||
cell.accessoryType = .None
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
Tell the tableView to automatically size the custom rows, while using the superclass's
|
||||
static sizing for the static cells.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Recurrence?:
|
||||
return UITableViewAutomaticDimension
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handles recurrence cell selection.
|
||||
Defaults to the super implementation for other sections
|
||||
*/
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Recurrence?:
|
||||
self.tableView(tableView, didSelectRecurrenceComponentAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
super.tableView(tableView, didSelectRowAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handles selection of a recurrence cell.
|
||||
|
||||
If the newly selected recurrence component is the previously selected
|
||||
recurrence component, reset the current selected component to `NSNotFound`
|
||||
and deselect that row.
|
||||
*/
|
||||
func tableView(tableView: UITableView, didSelectRecurrenceComponentAtIndexPath indexPath: NSIndexPath) {
|
||||
if indexPath.row == timerTriggerCreator.selectedRecurrenceIndex {
|
||||
timerTriggerCreator.selectedRecurrenceIndex = NSNotFound
|
||||
}
|
||||
else {
|
||||
timerTriggerCreator.selectedRecurrenceIndex = indexPath.row
|
||||
}
|
||||
tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/**
|
||||
- parameter index: The section index.
|
||||
|
||||
- returns: The `TriggerTableViewSection` for the given index.
|
||||
*/
|
||||
override func sectionForIndex(index: Int) -> TriggerTableViewSection? {
|
||||
switch index {
|
||||
case 0:
|
||||
return .Name
|
||||
|
||||
case 1:
|
||||
return .Enabled
|
||||
|
||||
case 2:
|
||||
return .DateAndTime
|
||||
|
||||
case 3:
|
||||
return .Recurrence
|
||||
|
||||
case 4:
|
||||
return .ActionSets
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TriggerCreator` is a superclass that builds triggers.
|
||||
*/
|
||||
|
||||
import HomeKit
|
||||
|
||||
/**
|
||||
A superclass for all trigger creators.
|
||||
|
||||
These classes manage the temporary state of the trigger
|
||||
and unify some of the saving processes.
|
||||
*/
|
||||
class TriggerCreator {
|
||||
// MARK: Properties
|
||||
|
||||
internal var home: HMHome
|
||||
internal var trigger: HMTrigger?
|
||||
internal var name = ""
|
||||
internal let saveTriggerGroup = dispatch_group_create()
|
||||
internal var errors = [NSError]()
|
||||
|
||||
/**
|
||||
Initializes a trigger creator from an existing trigger (if it exists),
|
||||
and the current home.
|
||||
|
||||
- parameter trigger: An `HMTrigger` or `nil`, if creation is desired.
|
||||
- parameter home: The `HMHome` into which this trigger will go.
|
||||
*/
|
||||
required init(trigger: HMTrigger?, home: HMHome) {
|
||||
self.home = home
|
||||
self.trigger = trigger
|
||||
}
|
||||
|
||||
/**
|
||||
Completes one of two actions based on the current status of the `trigger` object:
|
||||
|
||||
1. Updates the existing trigger.
|
||||
2. Creates a new trigger.
|
||||
|
||||
- parameter name: The name to set for the new or updated trigger.
|
||||
- parameter actionSets: The new list of action sets to set for the trigger
|
||||
- parameter completion: The closure to call when all configurations have been completed.
|
||||
*/
|
||||
func saveTriggerWithName(name: String, actionSets: [HMActionSet], completion: (HMTrigger?, [NSError]) -> Void) {
|
||||
self.name = name
|
||||
if trigger != nil {
|
||||
// Let the subclass update the trigger.
|
||||
updateTrigger()
|
||||
updateNameIfNecessary()
|
||||
configureWithActionSets(actionSets)
|
||||
}
|
||||
else {
|
||||
self.trigger = newTrigger()
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
home.addTrigger(trigger!) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
else {
|
||||
self.configureWithActionSets(actionSets)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Call the completion block with our event trigger and any accumulated errors
|
||||
from the saving process.
|
||||
*/
|
||||
dispatch_group_notify(saveTriggerGroup, dispatch_get_main_queue()) {
|
||||
self.cleanUp()
|
||||
completion(self.trigger, self.errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Updates the trigger's internals.
|
||||
Action sets and the trigger name need not be configured.
|
||||
|
||||
Implemented by subclasses.
|
||||
*/
|
||||
internal func updateTrigger() { }
|
||||
|
||||
/**
|
||||
Creates a new trigger to be added to the home.
|
||||
Action sets and the trigger name need not be configured.
|
||||
|
||||
Implemented by subclasses.
|
||||
|
||||
- returns: A new, generated `HMTrigger`.
|
||||
*/
|
||||
internal func newTrigger() -> HMTrigger? {
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Cleans up an internal structures after the trigger has been saved.
|
||||
|
||||
Implemented by subclasses.
|
||||
*/
|
||||
internal func cleanUp() {}
|
||||
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/**
|
||||
Syncs the trigger's action sets with the specified array of action sets.
|
||||
|
||||
- parameter actionSets: Array of `HMActionSet`s to match.
|
||||
*/
|
||||
private func configureWithActionSets(actionSets: [HMActionSet]) {
|
||||
guard let trigger = trigger else { return }
|
||||
/*
|
||||
Save a standard completion handler to use when we either add or remove
|
||||
an action set.
|
||||
*/
|
||||
let defaultCompletion: NSError? -> Void = { error in
|
||||
// Leave the dispatch group, to notify that we've finished this task.
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
|
||||
// First pass, remove the action sets that have been deselected.
|
||||
for actionSet in trigger.actionSets {
|
||||
if actionSets.contains(actionSet) {
|
||||
continue
|
||||
}
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
trigger.removeActionSet(actionSet, completionHandler: defaultCompletion)
|
||||
}
|
||||
|
||||
// Second pass, add the new action sets that were just selected.
|
||||
for actionSet in actionSets {
|
||||
if trigger.actionSets.contains(actionSet) {
|
||||
continue
|
||||
}
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
trigger.addActionSet(actionSet, completionHandler: defaultCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the trigger's name from the stored name, entering and leaving the dispatch group if necessary.
|
||||
func updateNameIfNecessary() {
|
||||
if trigger?.name == self.name {
|
||||
return
|
||||
}
|
||||
dispatch_group_enter(saveTriggerGroup)
|
||||
trigger?.updateName(name) { error in
|
||||
if let error = error {
|
||||
self.errors.append(error)
|
||||
}
|
||||
dispatch_group_leave(self.saveTriggerGroup)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `TriggerViewController` is a superclass which allows users to create triggers.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// Represents all possible sections in a `TriggerViewController` subclass.
|
||||
enum TriggerTableViewSection: Int {
|
||||
// All triggers have these sections.
|
||||
case Name, Enabled, ActionSets
|
||||
|
||||
// Timer triggers only.
|
||||
case DateAndTime, Recurrence
|
||||
|
||||
// Location and Characteristic triggers only.
|
||||
case Conditions
|
||||
|
||||
// Location triggers only.
|
||||
case Location, Region
|
||||
|
||||
// Characteristic triggers only.
|
||||
case Characteristics
|
||||
}
|
||||
|
||||
/**
|
||||
A superclass for all trigger view controllers.
|
||||
|
||||
It manages the name, enabled state, and action set components of the view,
|
||||
as these are shared components.
|
||||
*/
|
||||
class TriggerViewController: HMCatalogViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let actionSetCell = "ActionSetCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@IBOutlet weak var saveButton: UIBarButtonItem!
|
||||
@IBOutlet weak var nameField: UITextField!
|
||||
@IBOutlet weak var enabledSwitch: UISwitch!
|
||||
|
||||
var trigger: HMTrigger?
|
||||
var triggerCreator: TriggerCreator?
|
||||
|
||||
/// An internal array of all action sets in the home.
|
||||
var actionSets: [HMActionSet]!
|
||||
|
||||
/**
|
||||
An array of all action sets that the user has selected.
|
||||
This will be used to save the trigger when it is finalized.
|
||||
*/
|
||||
lazy var selectedActionSets = [HMActionSet]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Resets internal data, sets initial UI, and configures the table view.
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
let filteredActionSets = home.actionSets.filter { actionSet in
|
||||
return !actionSet.actions.isEmpty
|
||||
}
|
||||
|
||||
actionSets = filteredActionSets.sortByTypeAndLocalizedName()
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
tableView.estimatedRowHeight = 44.0
|
||||
|
||||
/*
|
||||
If we have a trigger, set the saved properties to the current properties
|
||||
of the passed-in trigger.
|
||||
*/
|
||||
if let trigger = trigger {
|
||||
selectedActionSets = trigger.actionSets
|
||||
nameField.text = trigger.name
|
||||
enabledSwitch.on = trigger.enabled
|
||||
}
|
||||
|
||||
enableSaveButtonIfApplicable()
|
||||
|
||||
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.actionSetCell)
|
||||
}
|
||||
|
||||
// MARK: IBAction Methods
|
||||
|
||||
/**
|
||||
Any time the name field changed, reevaluate whether or not
|
||||
to enable the save button.
|
||||
*/
|
||||
@IBAction func nameFieldDidChange(sender: UITextField) {
|
||||
enableSaveButtonIfApplicable()
|
||||
}
|
||||
|
||||
/// Saves the trigger and dismisses this view controller.
|
||||
@IBAction func saveAndDismiss() {
|
||||
saveButton.enabled = false
|
||||
triggerCreator?.saveTriggerWithName(trimmedName, actionSets: selectedActionSets) { trigger, errors in
|
||||
self.trigger = trigger
|
||||
self.saveButton.enabled = true
|
||||
|
||||
if !errors.isEmpty {
|
||||
self.displayErrors(errors)
|
||||
return
|
||||
}
|
||||
|
||||
self.enableTrigger(self.trigger!) {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func dismiss() {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Subclass Methods
|
||||
|
||||
/**
|
||||
Generates the section for the index.
|
||||
|
||||
This allows for the subclasses to lay out their content in different sections
|
||||
while still maintaining common code in the `TriggerViewController`.
|
||||
|
||||
- parameter index: The index of the section
|
||||
|
||||
- returns: The `TriggerTableViewSection` for the provided index.
|
||||
*/
|
||||
func sectionForIndex(index: Int) -> TriggerTableViewSection? {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Enable the trigger if necessary.
|
||||
func enableTrigger(trigger: HMTrigger, completion: Void -> Void) {
|
||||
if trigger.enabled == enabledSwitch.on {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
trigger.enable(enabledSwitch.on) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Enables the save button if:
|
||||
|
||||
1. The name field is not empty, and
|
||||
2. There will be at least one action set in the trigger after saving.
|
||||
*/
|
||||
private func enableSaveButtonIfApplicable() {
|
||||
saveButton.enabled = !trimmedName.characters.isEmpty &&
|
||||
(!selectedActionSets.isEmpty || trigger?.actionSets.count > 0)
|
||||
}
|
||||
|
||||
/// - returns: The name from the `nameField`, stripping newline and whitespace characters.
|
||||
var trimmedName: String {
|
||||
return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// Creates a cell that represents either a selected or unselected action set cell.
|
||||
private func tableView(tableView: UITableView, actionSetCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.actionSetCell, forIndexPath: indexPath)
|
||||
let actionSet = actionSets[indexPath.row]
|
||||
|
||||
if selectedActionSets.contains(actionSet) {
|
||||
cell.accessoryType = .Checkmark
|
||||
}
|
||||
else {
|
||||
cell.accessoryType = .None
|
||||
}
|
||||
|
||||
cell.textLabel?.text = actionSet.name
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
/// Only handles the ActionSets case, defaults to super.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if sectionForIndex(section) == .ActionSets {
|
||||
return actionSets.count ?? 0
|
||||
}
|
||||
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
}
|
||||
|
||||
/// Only handles the ActionSets case, defaults to super.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
if sectionForIndex(indexPath.section) == .ActionSets {
|
||||
return self.tableView(tableView, actionSetCellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
|
||||
return super.tableView(tableView, cellForRowAtIndexPath: indexPath)
|
||||
}
|
||||
|
||||
/**
|
||||
This is necessary for mixing static and dynamic table view cells.
|
||||
We return a fake index path because otherwise the superclass's implementation (which does not
|
||||
know about the extra cells we're adding) will cause an error.
|
||||
|
||||
- returns: The superclass's indentationLevel for the first row in the provided section,
|
||||
instead of the provided row.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int {
|
||||
let newIndexPath = NSIndexPath(forRow: 0, inSection: indexPath.section)
|
||||
|
||||
return super.tableView(tableView, indentationLevelForRowAtIndexPath: newIndexPath)
|
||||
}
|
||||
|
||||
/**
|
||||
Tell the tableView to automatically size the custom rows, while using the superclass's
|
||||
static sizing for the static cells.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
|
||||
switch sectionForIndex(indexPath.section) {
|
||||
case .Name?, .Enabled?:
|
||||
return super.tableView(tableView, heightForRowAtIndexPath: indexPath)
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return UITableViewAutomaticDimension
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles row selction for action sets, defaults to super implementation.
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
tableView.deselectRowAtIndexPath(indexPath, animated: true)
|
||||
if sectionForIndex(indexPath.section) == .ActionSets {
|
||||
self.tableView(tableView, didSelectActionSetAtIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Manages footer titles for higher-level sections. Superclasses should fall back
|
||||
on this implementation after attempting to handle any special trigger sections.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch sectionForIndex(section) {
|
||||
case .ActionSets?:
|
||||
return NSLocalizedString("When this trigger is activated, it will set these scenes. You can only select scenes which have at least one action.", comment: "Scene Trigger Description")
|
||||
|
||||
case .Enabled?:
|
||||
return NSLocalizedString("This trigger will only activate if it is enabled. You can disable triggers to temporarily stop them from running.", comment: "Trigger Enabled Description")
|
||||
|
||||
case nil:
|
||||
fatalError("Unexpected `TriggerTableViewSection` raw value.")
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, titleForFooterInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Handle selection of an action set cell. If the action set is already part of the selected action sets,
|
||||
then remove it from the selected list. Otherwise, add it to the selected list.
|
||||
*/
|
||||
func tableView(tableView: UITableView, didSelectActionSetAtIndexPath indexPath: NSIndexPath) {
|
||||
let actionSet = actionSets[indexPath.row]
|
||||
if let index = selectedActionSets.indexOf(actionSet) {
|
||||
selectedActionSets.removeAtIndex(index)
|
||||
}
|
||||
else {
|
||||
selectedActionSets.append(actionSet)
|
||||
}
|
||||
|
||||
enableSaveButtonIfApplicable()
|
||||
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/**
|
||||
If our trigger has been removed from the home,
|
||||
dismiss the view controller.
|
||||
*/
|
||||
func home(home: HMHome, didRemoveTrigger trigger: HMTrigger) {
|
||||
if self.trigger == trigger{
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// If our trigger has been updated, reload our data.
|
||||
func home(home: HMHome, didUpdateTrigger trigger: HMTrigger) {
|
||||
if self.trigger == trigger{
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `AddRoomViewController` allows the user to add rooms to a zone.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller that lists rooms within a home and allows the user to add the rooms to a provided zone.
|
||||
class AddRoomViewController: HMCatalogViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let roomCell = "RoomCell"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var homeZone: HMZone!
|
||||
|
||||
lazy var displayedRooms = [HMRoom]()
|
||||
lazy var selectedRooms = [HMRoom]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
title = homeZone.name
|
||||
resetDisplayedRooms()
|
||||
}
|
||||
|
||||
/// Adds the selected rooms to the zone and dismisses the view.
|
||||
@IBAction func dismiss(sender: AnyObject) {
|
||||
addSelectedRoomsToZoneWithCompletionHandler {
|
||||
self.dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a dispatch group, adds all of the rooms to the zone,
|
||||
and runs the provided completion once all rooms have been added.
|
||||
|
||||
- parameter completion: A closure to call once all rooms have been added.
|
||||
*/
|
||||
func addSelectedRoomsToZoneWithCompletionHandler(completion: () -> Void) {
|
||||
let group = dispatch_group_create()
|
||||
for room in selectedRooms {
|
||||
dispatch_group_enter(group)
|
||||
homeZone.addRoom(room) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
}
|
||||
dispatch_group_leave(group)
|
||||
}
|
||||
}
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), completion)
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of displayed rooms.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return displayedRooms.count
|
||||
}
|
||||
|
||||
/// - returns: A cell that includes the name of a room and a checkmark if it's intended to be added to the zone.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath)
|
||||
|
||||
let room = displayedRooms[indexPath.row]
|
||||
|
||||
cell.textLabel?.text = room.name
|
||||
cell.accessoryType = selectedRooms.contains(room) ? .Checkmark : .None
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/// Adds the selected room to the selected rooms array and reloads that cell
|
||||
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
|
||||
let room = displayedRooms[indexPath.row]
|
||||
|
||||
if let index = selectedRooms.indexOf(room) {
|
||||
selectedRooms.removeAtIndex(index)
|
||||
}
|
||||
else {
|
||||
selectedRooms.append(room)
|
||||
}
|
||||
|
||||
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/// Resets the list of displayed rooms and reloads the table.
|
||||
func resetDisplayedRooms() {
|
||||
displayedRooms = home.roomsNotAlreadyInZone(homeZone, includingRooms: selectedRooms)
|
||||
if displayedRooms.isEmpty {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
else {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// If our zone was removed, dismiss this view.
|
||||
func home(home: HMHome, didRemoveZone zone: HMZone) {
|
||||
if zone == homeZone {
|
||||
dismissViewControllerAnimated(true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// If our zone was renamed, reset our title.
|
||||
func home(home: HMHome, didUpdateNameForZone zone: HMZone) {
|
||||
if zone == homeZone {
|
||||
title = zone.name
|
||||
}
|
||||
}
|
||||
|
||||
// All home updates reset the displayed homes and reload the view.
|
||||
|
||||
func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
|
||||
resetDisplayedRooms()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didAddRoom room: HMRoom) {
|
||||
resetDisplayedRooms()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom) {
|
||||
resetDisplayedRooms()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didAddRoom room: HMRoom, toZone zone: HMZone) {
|
||||
resetDisplayedRooms()
|
||||
}
|
||||
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom, fromZone zone: HMZone) {
|
||||
resetDisplayedRooms()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
Copyright (C) 2016 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
The `ZoneViewController` lists the rooms in a zone.
|
||||
*/
|
||||
|
||||
import UIKit
|
||||
import HomeKit
|
||||
|
||||
/// A view controller that lists the rooms within a provided zone.
|
||||
class ZoneViewController: HMCatalogViewController {
|
||||
// MARK: Types
|
||||
|
||||
struct Identifiers {
|
||||
static let roomCell = "RoomCell"
|
||||
static let addCell = "AddCell"
|
||||
static let disabledAddCell = "DisabledAddCell"
|
||||
static let addRoomsSegue = "Add Rooms"
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
var homeZone: HMZone!
|
||||
var rooms = [HMRoom]()
|
||||
|
||||
// MARK: View Methods
|
||||
|
||||
/// Reload the data and configure the view.
|
||||
override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
title = homeZone.name
|
||||
reloadData()
|
||||
}
|
||||
|
||||
/// If our data is invalid, pop the view controller.
|
||||
override func viewDidAppear(animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if shouldPopViewController() {
|
||||
navigationController?.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the zone to `AddRoomViewController`.
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
super.prepareForSegue(segue, sender: sender)
|
||||
if segue.identifier == Identifiers.addRoomsSegue {
|
||||
let addViewController = segue.intendedDestinationViewController as! AddRoomViewController
|
||||
addViewController.homeZone = homeZone
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
/// Resets the internal list of rooms and reloads the table view.
|
||||
private func reloadData() {
|
||||
rooms = homeZone.rooms.sortByLocalizedName()
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
/// Sorts the internal list of rooms by localized name.
|
||||
private func sortRooms() {
|
||||
rooms = rooms.sortByLocalizedName()
|
||||
}
|
||||
|
||||
/// - returns: The `NSIndexPath` where the 'Add Cell' should be located.
|
||||
private var addIndexPath: NSIndexPath {
|
||||
return NSIndexPath(forRow: rooms.count, inSection: 0)
|
||||
}
|
||||
|
||||
/**
|
||||
- parameter indexPath: The index path in question.
|
||||
|
||||
- returns: `true` if the indexPath should contain
|
||||
an 'add' cell, `false` otherwise
|
||||
*/
|
||||
private func indexPathIsAdd(indexPath: NSIndexPath) -> Bool {
|
||||
return indexPath.row == addIndexPath.row
|
||||
}
|
||||
|
||||
/**
|
||||
Reloads the `addIndexPath`.
|
||||
|
||||
This is typically used when something has changed to allow
|
||||
the user to add a room.
|
||||
*/
|
||||
private func reloadAddIndexPath() {
|
||||
tableView.reloadRowsAtIndexPaths([addIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a room to the internal array of rooms and inserts new row
|
||||
into the table view.
|
||||
|
||||
- parameter room: The new `HMRoom` to add.
|
||||
*/
|
||||
private func didAddRoom(room: HMRoom) {
|
||||
rooms.append(room)
|
||||
|
||||
sortRooms()
|
||||
|
||||
if let newRoomIndex = rooms.indexOf(room) {
|
||||
let newRoomIndexPath = NSIndexPath(forRow: newRoomIndex, inSection: 0)
|
||||
tableView.insertRowsAtIndexPaths([newRoomIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
reloadAddIndexPath()
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a room from the internal array of rooms and deletes
|
||||
the row from the table view.
|
||||
|
||||
- parameter room: The `HMRoom` to remove.
|
||||
*/
|
||||
private func didRemoveRoom(room: HMRoom) {
|
||||
if let roomIndex = rooms.indexOf(room) {
|
||||
rooms.removeAtIndex(roomIndex)
|
||||
let roomIndexPath = NSIndexPath(forRow: roomIndex, inSection: 0)
|
||||
tableView.deleteRowsAtIndexPaths([roomIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
|
||||
reloadAddIndexPath()
|
||||
}
|
||||
|
||||
/**
|
||||
Reloads the cell corresponding a given room.
|
||||
|
||||
- parameter room: The `HMRoom` to reload.
|
||||
*/
|
||||
private func didUpdateRoom(room: HMRoom) {
|
||||
if let roomIndex = rooms.indexOf(room) {
|
||||
let roomIndexPath = NSIndexPath(forRow: roomIndex, inSection: 0)
|
||||
tableView.reloadRowsAtIndexPaths([roomIndexPath], withRowAnimation: .Automatic)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a room from HomeKit and updates the view.
|
||||
|
||||
- parameter room: The `HMRoom` to remove.
|
||||
*/
|
||||
private func removeRoom(room: HMRoom) {
|
||||
didRemoveRoom(room)
|
||||
homeZone.removeRoom(room) { error in
|
||||
if let error = error {
|
||||
self.displayError(error)
|
||||
self.didAddRoom(room)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if our current home no longer
|
||||
exists, `false` otherwise.
|
||||
*/
|
||||
private func shouldPopViewController() -> Bool {
|
||||
for zone in home.zones {
|
||||
if zone == homeZone {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if more rooms can be added to this zone;
|
||||
`false` otherwise.
|
||||
*/
|
||||
private var canAddRoom: Bool {
|
||||
return rooms.count < home.rooms.count
|
||||
}
|
||||
|
||||
// MARK: Table View Methods
|
||||
|
||||
/// - returns: The number of rooms in the zone, plus 1 for the 'add' row.
|
||||
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return rooms.count + 1
|
||||
}
|
||||
|
||||
/// - returns: A cell containing the name of an HMRoom.
|
||||
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
|
||||
if indexPathIsAdd(indexPath) {
|
||||
let reuseIdentifier = home.isAdmin && canAddRoom ? Identifiers.addCell : Identifiers.disabledAddCell
|
||||
|
||||
return tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath)
|
||||
|
||||
cell.textLabel?.text = rooms[indexPath.row].name
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `true` if the cell is anything but an 'add' cell;
|
||||
`false` otherwise.
|
||||
*/
|
||||
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
return home.isAdmin && !indexPathIsAdd(indexPath)
|
||||
}
|
||||
|
||||
/// Deletes the room at the provided index path.
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
|
||||
if editingStyle == .Delete {
|
||||
let room = rooms[indexPath.row]
|
||||
|
||||
removeRoom(room)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: HMHomeDelegate Methods
|
||||
|
||||
/// If our zone was removed, pop the view controller.
|
||||
func home(home: HMHome, didRemoveZone zone: HMZone) {
|
||||
if zone == homeZone{
|
||||
navigationController?.popViewControllerAnimated(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// If our zone was renamed, update the title.
|
||||
func home(home: HMHome, didUpdateNameForZone zone: HMZone) {
|
||||
if zone == homeZone {
|
||||
title = zone.name
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the row for the room.
|
||||
func home(home: HMHome, didUpdateNameForRoom room: HMRoom) {
|
||||
didUpdateRoom(room)
|
||||
}
|
||||
|
||||
/**
|
||||
A room has been added, we may be able to add it to the zone.
|
||||
Reload the 'addIndexPath'
|
||||
*/
|
||||
func home(home: HMHome, didAddRoom room: HMRoom) {
|
||||
reloadAddIndexPath()
|
||||
}
|
||||
|
||||
/**
|
||||
A room has been removed, attempt to remove it from the room.
|
||||
This will always reload the 'addIndexPath'.
|
||||
*/
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom) {
|
||||
didRemoveRoom(room)
|
||||
}
|
||||
|
||||
/// If the room was added to our zone, add it to the view.
|
||||
func home(home: HMHome, didAddRoom room: HMRoom, toZone zone: HMZone) {
|
||||
if zone == homeZone {
|
||||
didAddRoom(room)
|
||||
}
|
||||
}
|
||||
|
||||
/// If the room was removed from our zone, remove it from the view.
|
||||
func home(home: HMHome, didRemoveRoom room: HMRoom, fromZone zone: HMZone) {
|
||||
if zone == homeZone {
|
||||
didRemoveRoom(room)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 186 KiB |
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf
vendored
Normal file
12
HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Configure.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
12
HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "Control.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf
vendored
Normal file
12
HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "FavoriteTabIcon.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf
vendored
Normal file
12
HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SelectedConfigureTabIcon.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
12
HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SelectedControlTabIcon.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf
vendored
Normal file
12
HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "SelectedFavoriteTabIcon.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
23
HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png
vendored
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png
vendored
Normal file
After Width: | Height: | Size: 2.7 KiB |
23
HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png
vendored
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png
vendored
Normal file
After Width: | Height: | Size: 3.7 KiB |
|
@ -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>
|