Adds ControllerInputsViewController to customize external game controller inputs

Includes necessary code changes to use refactored DeltaCore Input logic
This commit is contained in:
Riley Testut 2017-09-27 13:08:41 -07:00
parent c0b3a04110
commit d70105e30e
47 changed files with 5266 additions and 1061 deletions

@ -1 +1 @@
Subproject commit caaf2e8e4bd847a2442382e121b07b4d5f139000
Subproject commit f33f8bd91a9e0b41ba55b9aa8370397dd7e7f809

@ -1 +1 @@
Subproject commit 4eedbd481457e3c236d1f3ffab3ba2f49c6d18df
Subproject commit 56401b9649f8abe971e3a66be1a3bb2cd984a1a0

@ -1 +1 @@
Subproject commit 0e622c7887e781363e36f6c7bfcd67fba3cb00ac
Subproject commit 4e678e7eac511d342aab86522eede9fdb7b29108

@ -1 +1 @@
Subproject commit bdc95aeb5e7f50263c34187cbe38ddddb4a2576e
Subproject commit 394627784bd0643c26e0fc95feafa5c960786a7c

View File

@ -30,6 +30,7 @@
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */; };
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF15AF831F54B43B009B6AAB /* ActionInput.swift */; };
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; };
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */; };
BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; };
@ -39,7 +40,7 @@
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34FA101CF1899D006624C7 /* CheatTextView.swift */; };
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF11C5D7FB000C1184C /* PauseViewController.swift */; };
BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF353FF41C5D837600C1184C /* PauseMenu.storyboard */; };
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF81C5D870B00C1184C /* PauseItem.swift */; };
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FF81C5D870B00C1184C /* MenuItem.swift */; };
BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */; };
BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */; };
BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */; };
@ -68,6 +69,8 @@
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */; };
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */; };
BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF5E7F451B9A652600AE44F8 /* Settings.storyboard */; };
BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */; };
BF6424851F5CBDC900D6AB44 /* UIView+ParentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */; };
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */; };
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF696B7F1D9B2B02009639E0 /* Theme.swift */; };
BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3121EB7E47F008E83CD /* ImportOption.swift */; };
@ -79,8 +82,11 @@
BF70798C1B6B464B0019077C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; };
BF70798D1B6B464B0019077C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; };
BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */; };
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */; };
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */; };
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */; };
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */; };
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */; };
BF930FFD1EB6D6FF00E8DBA0 /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF930FFC1EB6D6FF00E8DBA0 /* System.swift */; };
BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2761E4977BF0030E7AD /* GameMetadata.swift */; };
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */; };
@ -92,10 +98,12 @@
BFA0D1271D3AE1F600565894 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63BDE91D389EEB00FCB040 /* GameViewController.swift */; };
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFAA1FEC1B8AA4FA00495943 /* Settings.swift */; };
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */; };
BFC6F7B81F435BC500221B96 /* Input+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC6F7B71F435BC500221B96 /* Input+Display.swift */; };
BFC9B7391CEFCD34008629BB /* CheatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */; };
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */; };
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; };
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; };
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; };
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; };
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFEC732C1AAECC4A00650035 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@ -144,6 +152,7 @@
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllersSettingsViewController.swift; path = Controllers/ControllersSettingsViewController.swift; sourceTree = "<group>"; };
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewGameViewController.swift; sourceTree = "<group>"; };
BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "EmulatorCore+Cheats.swift"; sourceTree = "<group>"; };
BF15AF831F54B43B009B6AAB /* ActionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionInput.swift; sourceTree = "<group>"; };
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = "<group>"; };
BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SystemControllerSkinsViewController.swift; path = "Controller Skins/SystemControllerSkinsViewController.swift"; sourceTree = "<group>"; };
BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = "<group>"; };
@ -155,7 +164,7 @@
BF34FA101CF1899D006624C7 /* CheatTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatTextView.swift; path = "Pause Menu/Cheats/CheatTextView.swift"; sourceTree = "<group>"; };
BF353FF11C5D7FB000C1184C /* PauseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseViewController.swift; path = "Pause Menu/PauseViewController.swift"; sourceTree = "<group>"; };
BF353FF51C5D837600C1184C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PauseMenu.storyboard; sourceTree = "<group>"; };
BF353FF81C5D870B00C1184C /* PauseItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseItem.swift; path = "Pause Menu/PauseItem.swift"; sourceTree = "<group>"; };
BF353FF81C5D870B00C1184C /* MenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MenuItem.swift; path = "Pause Menu/MenuItem.swift"; sourceTree = "<group>"; };
BF353FFD1C5DA3C500C1184C /* PausePresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PausePresentationController.swift; path = "Pause Menu/Presentation Controller/PausePresentationController.swift"; sourceTree = "<group>"; };
BF353FFE1C5DA3C500C1184C /* PausePresentationControllerContentView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = PausePresentationControllerContentView.xib; path = "Pause Menu/Presentation Controller/PausePresentationControllerContentView.xib"; sourceTree = "<group>"; };
BF3540011C5DA3D500C1184C /* PauseStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseStoryboardSegue.swift; path = "Pause Menu/Segues/PauseStoryboardSegue.swift"; sourceTree = "<group>"; };
@ -187,7 +196,10 @@
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Conveniences.swift"; sourceTree = "<group>"; };
BF5E7F431B9A650B00AE44F8 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
BF5E7F451B9A652600AE44F8 /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ControllerInputsViewController.swift; path = Controllers/ControllerInputsViewController.swift; sourceTree = "<group>"; };
BF63BDE91D389EEB00FCB040 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = "<group>"; };
BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ListMenuViewController.swift; path = "Components/Popover Menu/ListMenuViewController.swift"; sourceTree = "<group>"; };
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+ParentViewController.swift"; sourceTree = "<group>"; };
BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ControllerSkin+Configuring.swift"; sourceTree = "<group>"; };
BF696B7F1D9B2B02009639E0 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = Theming/Theme.swift; sourceTree = "<group>"; };
BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -199,8 +211,10 @@
BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PhotoLibraryImportOption.swift; path = "Importing/Import Options/PhotoLibraryImportOption.swift"; sourceTree = "<group>"; };
BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = "<group>"; };
BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseMenuViewController.swift; path = "Pause Menu/PauseMenuViewController.swift"; sourceTree = "<group>"; };
BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridMenuViewController.swift; path = "Pause Menu/GridMenuViewController.swift"; sourceTree = "<group>"; };
BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Delta.swift"; sourceTree = "<group>"; };
BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PopoverMenuController.swift; path = "Components/Popover Menu/PopoverMenuController.swift"; sourceTree = "<group>"; };
BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = InputCalloutView.swift; path = Controllers/InputCalloutView.swift; sourceTree = "<group>"; };
BF930FFC1EB6D6FF00E8DBA0 /* System.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = System.swift; path = Systems/System.swift; sourceTree = "<group>"; };
BF95E2761E4977BF0030E7AD /* GameMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameMetadata.swift; path = Database/OpenVGDB/GameMetadata.swift; sourceTree = "<group>"; };
BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabaseBrowserViewController.swift; path = Database/OpenVGDB/GamesDatabaseBrowserViewController.swift; sourceTree = "<group>"; };
@ -209,9 +223,11 @@
BFAA1FEC1B8AA4FA00495943 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DeltaCoreProtocol+Delta.swift"; sourceTree = "<group>"; };
BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SNESDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFC6F7B71F435BC500221B96 /* Input+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Input+Display.swift"; sourceTree = "<group>"; };
BFC9B7381CEFCD34008629BB /* CheatsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CheatsViewController.swift; path = "Pause Menu/Cheats/CheatsViewController.swift"; sourceTree = "<group>"; };
BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewControllerContextTransitioning+Conveniences.swift"; sourceTree = "<group>"; };
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PopoverMenuButton.swift; path = "Components/Popover Menu/PopoverMenuButton.swift"; sourceTree = "<group>"; };
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesStoryboardSegue.swift; path = Segues/SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBCDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -262,6 +278,8 @@
BF5942921E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift */,
BFBAB2E21EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift */,
BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */,
BFC6F7B71F435BC500221B96 /* Input+Display.swift */,
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -270,6 +288,8 @@
isa = PBXGroup;
children = (
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */,
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */,
BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */,
);
name = Controllers;
sourceTree = "<group>";
@ -316,6 +336,7 @@
isa = PBXGroup;
children = (
BF5942581E09BB810051894B /* Action.swift */,
BFE0229C1F5B56840052D888 /* Popover Menu */,
BF5942671E09BBB70051894B /* Collection View */,
BF5942601E09BBA80051894B /* Loading */,
);
@ -428,8 +449,8 @@
children = (
BF353FF41C5D837600C1184C /* PauseMenu.storyboard */,
BF353FF11C5D7FB000C1184C /* PauseViewController.swift */,
BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */,
BF353FF81C5D870B00C1184C /* PauseItem.swift */,
BF7AE8041C2E858400B1B5BC /* GridMenuViewController.swift */,
BF353FF81C5D870B00C1184C /* MenuItem.swift */,
BF3540031C5DA6D800C1184C /* Save States */,
BFC9B7371CEFCD08008629BB /* Cheats */,
BF353FFB1C5DA2F600C1184C /* Presentation Controller */,
@ -508,6 +529,16 @@
name = Cheats;
sourceTree = "<group>";
};
BFE0229C1F5B56840052D888 /* Popover Menu */ = {
isa = PBXGroup;
children = (
BF8CA9351F5F651900499FDD /* PopoverMenuController.swift */,
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */,
BF6424821F5B8F3F00D6AB44 /* ListMenuViewController.swift */,
);
name = "Popover Menu";
sourceTree = "<group>";
};
BFEC732F1AAECCBD00650035 /* Resources */ = {
isa = PBXGroup;
children = (
@ -521,8 +552,8 @@
isa = PBXGroup;
children = (
BFFA71D91AAC406100EE9DD1 /* Delta */,
BFEC732F1AAECCBD00650035 /* Resources */,
BF9F4FCD1AAD7B25004C9500 /* Frameworks */,
BFEC732F1AAECCBD00650035 /* Resources */,
BFFA71D81AAC406100EE9DD1 /* Products */,
FD1E8AE87FA2DB8793F7B937 /* Pods */,
);
@ -572,6 +603,7 @@
children = (
BF63BDE91D389EEB00FCB040 /* GameViewController.swift */,
BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */,
BF15AF831F54B43B009B6AAB /* ActionInput.swift */,
);
path = Emulation;
sourceTree = "<group>";
@ -776,6 +808,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BFC6F7B81F435BC500221B96 /* Input+Display.swift in Sources */,
BF59426A1E09BBD00051894B /* GridCollectionViewCell.swift in Sources */,
BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */,
BF59427C1E09BC830051894B /* Cheat.swift in Sources */,
@ -789,24 +822,29 @@
BF59428E1E09BCFB0051894B /* ImportController.swift in Sources */,
BF930FFD1EB6D6FF00E8DBA0 /* System.swift in Sources */,
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */,
BF6424831F5B8F3F00D6AB44 /* ListMenuViewController.swift in Sources */,
BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */,
BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */,
BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */,
BF5942731E09BC700051894B /* Model.xcdatamodel in Sources */,
BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */,
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */,
BF353FF91C5D870B00C1184C /* PauseItem.swift in Sources */,
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */,
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */,
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */,
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */,
BF59426B1E09BBD00051894B /* GridCollectionViewLayout.swift in Sources */,
BF6424851F5CBDC900D6AB44 /* UIView+ParentViewController.swift in Sources */,
BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */,
BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */,
BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */,
BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */,
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */,
BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */,
BFFA4C091E8A24D600D87934 /* GameMetadataTableViewCell.swift in Sources */,
BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */,
BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */,
BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */,
BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */,
BF5942881E09BC8B0051894B /* _Game.swift in Sources */,
@ -819,6 +857,7 @@
BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */,
BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */,
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */,
BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */,
BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */,
BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */,
BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */,
@ -835,13 +874,14 @@
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */,
BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */,
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */,
BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */,
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */,
BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */,
BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */,
BF59425C1E09BB810051894B /* Action.swift in Sources */,
BF696B801D9B2B02009639E0 /* Theme.swift in Sources */,
BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */,
BF7AE8081C2E858400B1B5BC /* GridMenuViewController.swift in Sources */,
BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */,
BFF93AA01E0FB036005EC865 /* InputStreamOutputWriter.swift in Sources */,
BF59427F1E09BC830051894B /* GameCollection.swift in Sources */,

View File

@ -40,7 +40,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate
}
// Controllers
ExternalControllerManager.shared.startMonitoringExternalControllers()
ExternalGameControllerManager.shared.startMonitoring()
return true
}

View File

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11185.3" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.19" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11151.4"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.16"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<customFonts key="customFonts">
<array key="Menlo.ttc">
<string>Menlo-Regular</string>
</array>
</customFonts>
<scenes>
<!--Pause View Controller-->
<scene sceneID="Wst-Dv-TjM">
@ -16,15 +20,17 @@
<viewControllerLayoutGuide type="bottom" id="gF0-0U-kR7"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="oOH-ea-jcb">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p2M-dE-BJs" userLabel="Blur View">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="eyD-0d-RHe" userLabel="Blur Content View">
<frame key="frameInset"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rqN-NB-jbb">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<connections>
<segue destination="sWv-Ky-VGs" kind="embed" identifier="embedNavigationController" id="1Ja-XW-uoT"/>
</connections>
@ -63,7 +69,7 @@
<objects>
<navigationController id="sWv-Ky-VGs" sceneMemberID="viewController">
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="Snh-Z0-9kC">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="Snh-Z0-9kC">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
@ -78,9 +84,9 @@
<!--Paused-->
<scene sceneID="1md-hu-g0J">
<objects>
<collectionViewController id="0jA-NY-mvB" customClass="PauseMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" delaysContentTouches="NO" dataMode="prototypes" id="scc-uc-vaJ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<collectionViewController id="0jA-NY-mvB" customClass="GridMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" misplaced="YES" delaysContentTouches="NO" dataMode="prototypes" id="scc-uc-vaJ">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="yXv-zl-idO" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
@ -91,7 +97,7 @@
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="6XS-Ne-nGZ" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minY="84" width="60" height="80"/>
<rect key="frame" x="0.0" y="20" width="60" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="60" height="80"/>
@ -128,7 +134,7 @@
<objects>
<collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="XgF-OF-CVf">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
@ -139,7 +145,7 @@
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="c3N-1A-ryV" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minX="20" minY="124" width="50" height="50"/>
<rect key="frame" x="20" y="60" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
@ -148,7 +154,7 @@
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Header" id="YeY-W9-CC6" customClass="SaveStatesCollectionHeaderView" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minY="64" width="375" height="50"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="50"/>
<autoresizingMask key="autoresizingMask"/>
</collectionReusableView>
<connections>
@ -174,22 +180,22 @@
<objects>
<tableViewController id="wb8-5o-1jE" customClass="CheatsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="f5S-hV-1yV">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="e8g-ZW-5lQ" customClass="CheatTableViewCell" customModule="Delta" customModuleProvider="target">
<frame key="frameInset" minY="92" width="375" height="44"/>
<rect key="frame" x="0.0" y="28" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="e8g-ZW-5lQ" id="AHE-Jk-ULE">
<frame key="frameInset" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View">
<frame key="frameInset" maxY="-0.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" id="9bA-Tg-Bko">
<frame key="frameInset"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
@ -198,8 +204,9 @@
</vibrancyEffect>
</visualEffectView>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="EdX-fU-x54">
<frame key="frameInset"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</view>
<vibrancyEffect>
@ -245,20 +252,21 @@
<objects>
<tableViewController storyboardIdentifier="editCheatViewController" id="jTR-Oe-YUJ" customClass="EditCheatViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<sections>
<tableViewSection headerTitle="Name" id="QT6-DZ-g70">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ZeC-rg-QFa">
<rect key="frame" x="0.0" y="119.5" width="375" height="44"/>
<rect key="frame" x="0.0" y="55.5" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW">
<frame key="frameInset" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Cheat Name" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="DD1-X0-hg7">
<rect key="frame" x="20" y="0.0" width="560" height="43.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/>
<connections>
@ -280,13 +288,14 @@
<tableViewSection headerTitle="Type" footerTitle="Description" id="rvn-VK-2uH">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Tst-zn-e04">
<rect key="frame" x="0.0" y="227" width="375" height="44"/>
<rect key="frame" x="0.0" y="163" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ">
<frame key="frameInset" width="375" height="43.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q">
<rect key="frame" x="20" y="8" width="560" height="29"/>
<segments>
<segment title="First"/>
<segment title="Second"/>
@ -308,13 +317,14 @@
<tableViewSection headerTitle="Code" footerTitle="Description" id="rHC-nA-ga0">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="210" id="xxc-cz-sb7">
<rect key="frame" x="0.0" y="346.5" width="375" height="210"/>
<rect key="frame" x="0.0" y="282.5" width="600" height="210"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa">
<frame key="frameInset" width="375" height="209.5"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="209.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="600" height="209.5"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>

View File

@ -0,0 +1,64 @@
//
// PopoverMenuViewController.swift
// Delta
//
// Created by Riley Testut on 9/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class ListMenuViewController: UITableViewController
{
var items: [MenuItem] {
get { return self.dataSource.items }
set { self.dataSource.items = newValue }
}
fileprivate let dataSource = RSTArrayTableViewDataSource<MenuItem>(items: [])
override var preferredContentSize: CGSize {
get {
let navigationBarHeight = self.navigationController?.navigationBar.bounds.height ?? 0.0
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
}
set {}
}
init()
{
super.init(style: .plain)
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
cell.textLabel?.text = item.text
cell.accessoryType = item.isSelected ? .checkmark : .none
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.tableView.rowHeight = 44
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
}
}
extension ListMenuViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let item = self.dataSource.item(at: indexPath)
item.isSelected = !item.isSelected
item.action(item)
self.tableView.reloadData()
}
}

View File

@ -0,0 +1,121 @@
//
// PopoverMenuButton.swift
// Delta
//
// Created by Riley Testut on 9/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
extension UINavigationBar
{
fileprivate var defaultTitleTextAttributes: [String: Any]? {
if let textAttributes = self._defaultTitleTextAttributes
{
return textAttributes
}
// Make "copy" of self.
let navigationBar = UINavigationBar(frame: .zero)
navigationBar.barStyle = self.barStyle
// Set item with title so we can retrieve default text attributes.
let navigationItem = UINavigationItem(title: "Testut")
navigationBar.items = [navigationItem]
navigationBar.isHidden = true
// Must be added to window hierarchy for it to create title UILabel.
self.addSubview(navigationBar)
defer { navigationBar.removeFromSuperview() }
navigationBar.layoutIfNeeded()
let textAttributes = navigationBar._defaultTitleTextAttributes
return textAttributes
}
fileprivate var _defaultTitleTextAttributes: [String: Any]? {
guard self.titleTextAttributes == nil else { return self.titleTextAttributes }
guard
let contentView = self.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("ContentView") || NSStringFromClass(type(of: $0)).contains("ItemView") }),
let titleLabel = contentView.subviews.first(where: { $0 is UILabel }) as? UILabel
else { return nil }
let textAttributes = titleLabel.attributedText?.attributes(at: 0, effectiveRange: nil)
return textAttributes
}
}
class PopoverMenuButton: UIControl
{
var title: String {
get { return self.textLabel.text ?? "" }
set {
self.textLabel.text = newValue
self.updateTextAttributes()
self.invalidateIntrinsicContentSize()
}
}
fileprivate let textLabel: UILabel
fileprivate let arrowLabel: UILabel
fileprivate let stackView: UIStackView
fileprivate var parentNavigationBar: UINavigationBar? {
guard let navigationController = self.parentViewController as? UINavigationController ?? self.parentViewController?.navigationController else { return nil }
guard self.isDescendant(of: navigationController.navigationBar) else { return nil }
return navigationController.navigationBar
}
override var intrinsicContentSize: CGSize {
return self.stackView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
}
init()
{
self.textLabel = UILabel()
self.textLabel.textColor = .black
self.arrowLabel = UILabel()
self.arrowLabel.text = ""
self.arrowLabel.textColor = .black
self.stackView = UIStackView(arrangedSubviews: [self.textLabel, self.arrowLabel])
self.stackView.axis = .horizontal
self.stackView.distribution = .fillProportionally
self.stackView.alignment = .center
self.stackView.spacing = 4.0
self.stackView.isUserInteractionEnabled = false
let intrinsicContentSize = self.stackView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
super.init(frame: CGRect(origin: .zero, size: intrinsicContentSize))
self.addSubview(self.stackView, pinningEdgesWith: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToSuperview()
{
self.updateTextAttributes()
}
}
private extension PopoverMenuButton
{
func updateTextAttributes()
{
guard let parentNavigationBar = self.parentNavigationBar else { return }
guard let textAttributes = parentNavigationBar.defaultTitleTextAttributes else { return }
for label in [self.textLabel, self.arrowLabel]
{
label.attributedText = NSAttributedString(string: label.text ?? "", attributes: textAttributes)
}
}
}

View File

@ -0,0 +1,102 @@
//
// PopoverMenuController.swift
// Delta
//
// Created by Riley Testut on 9/5/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
private var popoverMenuControllerKey: UInt8 = 0
extension UINavigationItem
{
var popoverMenuController: PopoverMenuController? {
get { return objc_getAssociatedObject(self, &popoverMenuControllerKey) as? PopoverMenuController }
set {
self.titleView = newValue?.popoverMenuButton
objc_setAssociatedObject(self, &popoverMenuControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
class PopoverMenuController: NSObject
{
let popoverViewController: UIViewController
let popoverMenuButton: PopoverMenuButton
var isActive: Bool = false {
willSet {
guard newValue != self.isActive else { return }
if newValue
{
self.presentPopoverViewController()
}
else
{
self.dismissPopoverViewController()
}
}
}
init(popoverViewController: UIViewController)
{
self.popoverViewController = popoverViewController
self.popoverMenuButton = PopoverMenuButton()
super.init()
self.popoverMenuButton.addTarget(self, action: #selector(PopoverMenuController.pressedPopoverMenuButton(_:)), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension PopoverMenuController
{
@objc func pressedPopoverMenuButton(_ button: PopoverMenuButton)
{
self.isActive = !self.isActive
}
func presentPopoverViewController()
{
guard !self.isActive else { return }
guard let presentingViewController = self.popoverMenuButton.parentViewController else { return }
self.popoverViewController.modalPresentationStyle = .popover
self.popoverViewController.popoverPresentationController?.delegate = self
self.popoverViewController.popoverPresentationController?.sourceView = self.popoverMenuButton.superview
self.popoverViewController.popoverPresentationController?.sourceRect = self.popoverMenuButton.frame
presentingViewController.present(self.popoverViewController, animated: true, completion: nil)
}
func dismissPopoverViewController()
{
guard self.isActive else { return }
self.popoverViewController.dismiss(animated: true, completion: nil)
}
}
extension PopoverMenuController: UIPopoverPresentationControllerDelegate
{
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
{
// Force popover presentation, regardless of trait collection.
return .none
}
func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController)
{
self.isActive = false
}
}

View File

@ -0,0 +1,68 @@
//
// PopoverMenuViewController.swift
// Delta
//
// Created by Riley Testut on 9/2/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class ListMenuViewController: UITableViewController
{
var items: [MenuItem] {
get { return self.dataSource.items }
set { self.dataSource.items = newValue; }
}
fileprivate let dataSource = RSTArrayTableViewDataSource<MenuItem>(items: [])
override var preferredContentSize: CGSize {
get {
let navigationBarHeight = self.navigationController?.navigationBar.bounds.height ?? 0.0
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
}
set {}
}
init()
{
super.init(style: .plain)
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
cell.textLabel?.text = item.text
cell.accessoryType = item.isSelected ? .checkmark : .none
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.tableView.rowHeight = 44
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
}
}
extension ListMenuViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
self.items.forEach { $0.isSelected = false }
let item = self.dataSource.item(at: indexPath)
item.isSelected = true
item.action(item)
self.tableView.reloadData()
self.dismiss(animated: true, completion: nil)
}
}

View File

@ -88,9 +88,9 @@ extension ControllerSkin: ControllerSkinProtocol
return self.controllerSkin?.image(for: traits, preferredSize: preferredSize)
}
public func inputs(for traits: DeltaCore.ControllerSkin.Traits, point: CGPoint) -> [Input]?
public func inputs(for traits: DeltaCore.ControllerSkin.Traits, at point: CGPoint) -> [Input]?
{
return self.controllerSkin?.inputs(for: traits, point: point)
return self.controllerSkin?.inputs(for: traits, at: point)
}
public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]?

View File

@ -0,0 +1,28 @@
//
// ActionInput.swift
// Delta
//
// Created by Riley Testut on 8/28/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
public extension GameControllerInputType
{
static let action = GameControllerInputType("com.rileytestut.Delta.input.action")
}
enum ActionInput: String
{
case saveState
case loadState
case fastForward
}
extension ActionInput: Input
{
var type: InputType {
return .controller(.action)
}
}

View File

@ -29,6 +29,22 @@ private extension GameViewController
self.gameType = gameType
}
}
struct SustainInputsMapping: GameControllerInputMappingProtocol
{
let gameControllerInputType: GameControllerInputType
let previousInputMapping: GameControllerInputMappingProtocol?
func input(forControllerInput controllerInput: Input) -> Input?
{
if let mappedInput = self.previousInputMapping?.input(forControllerInput: controllerInput), mappedInput == StandardGameControllerInput.menu
{
return mappedInput
}
return controllerInput
}
}
}
class GameViewController: DeltaCore.GameViewController
@ -79,10 +95,8 @@ class GameViewController: DeltaCore.GameViewController
fileprivate var context = CIContext(options: [kCIContextWorkingColorSpace: NSNull()])
// Sustain Buttons
fileprivate var updateSemaphores = Set<DispatchSemaphore>()
fileprivate var sustainedInputs = [ObjectIdentifier: [Input]]()
fileprivate var reactivateSustainedInputsQueue: OperationQueue
fileprivate var selectingSustainedButtons = false
fileprivate var isSelectingSustainedButtons = false
fileprivate var sustainInputsMapping: SustainInputsMapping?
fileprivate var sustainButtonsContentView: UIView!
fileprivate var sustainButtonsBlurView: UIVisualEffectView!
@ -90,9 +104,6 @@ class GameViewController: DeltaCore.GameViewController
required init()
{
self.reactivateSustainedInputsQueue = OperationQueue()
self.reactivateSustainedInputsQueue.maxConcurrentOperationCount = 1
super.init()
self.initialize()
@ -100,9 +111,6 @@ class GameViewController: DeltaCore.GameViewController
required init?(coder aDecoder: NSCoder)
{
self.reactivateSustainedInputsQueue = OperationQueue()
self.reactivateSustainedInputsQueue.maxConcurrentOperationCount = 1
super.init(coder: aDecoder)
self.initialize()
@ -112,8 +120,8 @@ class GameViewController: DeltaCore.GameViewController
{
self.delegate = self
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.updateControllers), name: .externalGameControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.didEnterBackground(with:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.settingsDidChange(with:)), name: .settingsDidChange, object: nil)
}
@ -128,18 +136,13 @@ class GameViewController: DeltaCore.GameViewController
{
super.gameController(gameController, didActivate: input)
guard (input as? ControllerInput) != .menu else { return }
guard self.isSelectingSustainedButtons else { return }
if self.selectingSustainedButtons
guard let pausingGameController = self.pausingGameController, gameController == pausingGameController else { return }
if input != StandardGameControllerInput.menu
{
self.addSustainedInput(input, for: gameController)
}
else if let sustainedInputs = self.sustainedInputs[ObjectIdentifier(gameController)], sustainedInputs.contains(where: { $0.isEqual(input) })
{
// Perform on next run loop
DispatchQueue.main.async {
self.reactivateSustainedInput(input, for: gameController)
}
gameController.sustain(input)
}
}
}
@ -243,22 +246,28 @@ extension GameViewController
pauseViewController.saveStatesViewControllerDelegate = self
pauseViewController.cheatsViewControllerDelegate = self
pauseViewController.fastForwardItem?.selected = (self.emulatorCore?.rate != self.emulatorCore?.deltaCore.supportedRates.lowerBound)
pauseViewController.fastForwardItem?.isSelected = (self.emulatorCore?.rate != self.emulatorCore?.deltaCore.supportedRates.lowerBound)
pauseViewController.fastForwardItem?.action = { [unowned self] item in
guard let emulatorCore = self.emulatorCore else { return }
emulatorCore.rate = item.selected ? emulatorCore.deltaCore.supportedRates.upperBound : emulatorCore.deltaCore.supportedRates.lowerBound
emulatorCore.rate = item.isSelected ? emulatorCore.deltaCore.supportedRates.upperBound : emulatorCore.deltaCore.supportedRates.lowerBound
}
pauseViewController.sustainButtonsItem?.selected = (self.sustainedInputs[ObjectIdentifier(gameController)]?.count ?? 0) > 0
pauseViewController.sustainButtonsItem?.isSelected = gameController.sustainedInputs.count > 0
pauseViewController.sustainButtonsItem?.action = { [unowned self, unowned pauseViewController] item in
self.resetSustainedInputs(for: gameController)
for input in gameController.sustainedInputs
{
gameController.unsustain(input)
}
if item.selected
if item.isSelected
{
self.showSustainButtonView()
pauseViewController.dismiss()
}
// Re-set gameController as pausingGameController.
self.pausingGameController = gameController
}
self.pauseViewController = pauseViewController
@ -346,11 +355,7 @@ private extension GameViewController
{
@objc func updateControllers()
{
var controllers = [GameController]()
controllers.append(self.controllerView)
// We need to map each item as a GameControllerProtocol due to a Swift bug
controllers.append(contentsOf: ExternalControllerManager.shared.connectedControllers.map { $0 as GameController })
let controllers = [self.controllerView as GameController] + ExternalGameControllerManager.shared.connectedControllers
if let index = Settings.localControllerPlayerIndex
{
@ -590,17 +595,6 @@ extension GameViewController: SaveStatesViewControllerDelegate
print(error)
}
// Reactivate sustained inputs
for gameController in self.emulatorCore?.gameControllers ?? []
{
guard let sustainedInputs = self.sustainedInputs[ObjectIdentifier(gameController)] else { continue }
for input in sustainedInputs
{
self.reactivateSustainedInput(input, for: gameController)
}
}
self.pauseViewController?.dismiss()
}
}
@ -625,7 +619,12 @@ private extension GameViewController
{
func showSustainButtonView()
{
self.selectingSustainedButtons = true
guard let gameController = self.pausingGameController else { return }
self.isSelectingSustainedButtons = true
self.sustainInputsMapping = SustainInputsMapping(gameControllerInputType: gameController.inputType, previousInputMapping: gameController.inputMapping)
gameController.inputMapping = self.sustainInputsMapping
let blurEffect = self.sustainButtonsBlurView.effect
self.sustainButtonsBlurView.effect = nil
@ -640,7 +639,18 @@ private extension GameViewController
func hideSustainButtonView()
{
self.selectingSustainedButtons = false
guard let gameController = self.pausingGameController else { return }
self.isSelectingSustainedButtons = false
gameController.inputMapping = self.sustainInputsMapping?.previousInputMapping
self.sustainInputsMapping = nil
// Reactivate all sustained inputs, since they will now be mapped to game inputs.
for input in gameController.sustainedInputs
{
gameController.activate(input)
}
let blurEffect = self.sustainButtonsBlurView.effect
@ -652,96 +662,6 @@ private extension GameViewController
self.sustainButtonsBlurView.effect = blurEffect
}
}
func resetSustainedInputs(for gameController: GameController)
{
if let previousInputs = self.sustainedInputs[ObjectIdentifier(gameController)]
{
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Activate previousInputs without notifying anyone so we can then deactivate them
// We do this because deactivating an already deactivated input has no effect
previousInputs.forEach { gameController.activate($0) }
receivers.forEach { gameController.addReceiver($0) }
// Deactivate previously sustained inputs
previousInputs.forEach { gameController.deactivate($0) }
}
self.sustainedInputs[ObjectIdentifier(gameController)] = []
}
func addSustainedInput(_ input: Input, for gameController: GameController)
{
var inputs = self.sustainedInputs[ObjectIdentifier(gameController)] ?? []
guard !inputs.contains(where: { $0.isEqual(input) }) else { return }
inputs.append(input)
self.sustainedInputs[ObjectIdentifier(gameController)] = inputs
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates
// However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it
gameController.deactivate(input)
receivers.forEach { gameController.addReceiver($0) }
}
func reactivateSustainedInput(_ input: Input, for gameController: GameController)
{
// These MUST be performed serially, or else Bad Things Happen if multiple inputs are reactivated at once
self.reactivateSustainedInputsQueue.addOperation {
// The manual activations/deactivations here are hidden implementation details, so we won't notify ourselves about them
gameController.removeReceiver(self)
// Must deactivate first so core recognizes a secondary activation
gameController.deactivate(input)
let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.sustainButtonsQueue")
dispatchQueue.async {
let semaphore = DispatchSemaphore(value: 0)
self.updateSemaphores.insert(semaphore)
// To ensure the emulator core recognizes us activating the input again, we need to wait at least two frames
// Unfortunately we cannot init DispatchSemaphore with value less than 0
// To compensate, we simply wait twice; once the first wait returns, we wait again
semaphore.wait()
semaphore.wait()
// These MUST be performed serially, or else Bad Things Happen if multiple inputs are reactivated at once
self.reactivateSustainedInputsQueue.addOperation {
self.updateSemaphores.remove(semaphore)
// Ensure we still are not a receiver (to prevent rare race conditions)
gameController.removeReceiver(self)
gameController.activate(input)
let receivers = gameController.receivers
receivers.forEach { gameController.removeReceiver($0) }
// Causes input to be considered deactivated, so gameController won't send a subsequent message to observers when user actually deactivates
// However, at this point the core still thinks it is activated, and is temporarily not a receiver, thus sustaining it
gameController.deactivate(input)
receivers.forEach { gameController.addReceiver($0) }
}
// More Bad Things Happen if we add self as observer before ALL reactivations have occurred (notable, infinite loops)
self.reactivateSustainedInputsQueue.waitUntilAllOperationsAreFinished()
gameController.addReceiver(self)
}
}
}
}
//MARK: - GameViewControllerDelegate -
@ -750,12 +670,17 @@ extension GameViewController: GameViewControllerDelegate
{
func gameViewController(_ gameViewController: DeltaCore.GameViewController, handleMenuInputFrom gameController: GameController)
{
if self.selectingSustainedButtons
if let pausingGameController = self.pausingGameController
{
guard pausingGameController == gameController else { return }
}
if self.isSelectingSustainedButtons
{
self.hideSustainButtonView()
}
if let pauseViewController = self.pauseViewController, !self.selectingSustainedButtons
if let pauseViewController = self.pauseViewController, !self.isSelectingSustainedButtons
{
pauseViewController.dismiss()
}
@ -768,15 +693,7 @@ extension GameViewController: GameViewControllerDelegate
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
{
return (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.selectingSustainedButtons && self.view.window != nil
}
func gameViewControllerDidUpdate(_ gameViewController: DeltaCore.GameViewController)
{
for semaphore in self.updateSemaphores
{
semaphore.signal()
}
return (self.presentedViewController == nil || self.presentedViewController?.isDisappearing == true) && !self.isSelectingSustainedButtons && self.view.window != nil
}
}

View File

@ -0,0 +1,112 @@
//
// Input+Display.swift
// Delta
//
// Created by Riley Testut on 8/15/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import DeltaCore
extension Input
{
// With the default GameControllerInputMapping files, multiple controller inputs may map to the same game input.
// This is because each controller input maps to a unique standard input, but then multiple standard inputs may map to same game input.
// To ensure we only show the most "important" controller input for a game input, we define general "display priorities" for each input.
//
// For example, MFiGameController.down and MFiGameController.leftThumbstickDown both map to a "down" game input.
// However, .down has a higher priority than .leftThumbstickDown, so we show .down instead of .leftThumbstickDown.
var displayPriority: Int {
switch self.type
{
case .game: break
case .controller(.standard): break
case .controller(.mfi):
let input = MFiGameController.Input(input: self)!
switch input
{
case .leftThumbstickUp: return 750
case .leftThumbstickDown: return 750
case .leftThumbstickLeft: return 750
case .leftThumbstickRight: return 750
case .leftShoulder: return 750
case .leftTrigger: return 500
case .rightShoulder: return 750
case .rightTrigger: return 500
default: break
}
default: break
}
return 1000
}
var localizedName: String {
switch self.type
{
case .game: break
case .controller(.standard):
let input = StandardGameControllerInput(input: self)!
switch input
{
case .menu: return NSLocalizedString("Menu", comment: "")
case .up: return NSLocalizedString("Up", comment: "")
case .down: return NSLocalizedString("Down", comment: "")
case .left: return NSLocalizedString("Left", comment: "")
case .right: return NSLocalizedString("Right", comment: "")
case .leftThumbstickUp: return NSLocalizedString("L🕹↑", comment: "")
case .leftThumbstickDown: return NSLocalizedString("L🕹↓", comment: "")
case .leftThumbstickLeft: return NSLocalizedString("L🕹←", comment: "")
case .leftThumbstickRight: return NSLocalizedString("L🕹→", comment: "")
case .rightThumbstickUp: return NSLocalizedString("R🕹↑", comment: "")
case .rightThumbstickDown: return NSLocalizedString("R🕹↓", comment: "")
case .rightThumbstickLeft: return NSLocalizedString("R🕹←", comment: "")
case .rightThumbstickRight: return NSLocalizedString("R🕹→", comment: "")
case .a: return NSLocalizedString("A", comment: "")
case .b: return NSLocalizedString("B", comment: "")
case .x: return NSLocalizedString("X", comment: "")
case .y: return NSLocalizedString("Y", comment: "")
case .start: return NSLocalizedString("Start", comment: "Start button")
case .select: return NSLocalizedString("Select", comment: "Select button")
case .l1: return NSLocalizedString("L1", comment: "")
case .l2: return NSLocalizedString("L2", comment: "")
case .l3: return NSLocalizedString("L3", comment: "")
case .r1: return NSLocalizedString("R1", comment: "")
case .r2: return NSLocalizedString("R2", comment: "")
case .r3: return NSLocalizedString("R3", comment: "")
}
case .controller(.mfi):
let input = MFiGameController.Input(input: self)!
switch input
{
case .menu: return NSLocalizedString("Menu", comment: "")
case .up: return NSLocalizedString("Up", comment: "")
case .down: return NSLocalizedString("Down", comment: "")
case .left: return NSLocalizedString("Left", comment: "")
case .right: return NSLocalizedString("Right", comment: "")
case .leftThumbstickUp: return NSLocalizedString("L🕹↑", comment: "")
case .leftThumbstickDown: return NSLocalizedString("L🕹↓", comment: "")
case .leftThumbstickLeft: return NSLocalizedString("L🕹←", comment: "")
case .leftThumbstickRight: return NSLocalizedString("L🕹→", comment: "")
case .rightThumbstickUp: return NSLocalizedString("R🕹↑", comment: "")
case .rightThumbstickDown: return NSLocalizedString("R🕹↓", comment: "")
case .rightThumbstickLeft: return NSLocalizedString("R🕹←", comment: "")
case .rightThumbstickRight: return NSLocalizedString("R🕹→", comment: "")
case .a: return NSLocalizedString("A", comment: "")
case .b: return NSLocalizedString("B", comment: "")
case .x: return NSLocalizedString("X", comment: "")
case .y: return NSLocalizedString("Y", comment: "")
case .leftShoulder: return NSLocalizedString("L1", comment: "")
case .leftTrigger: return NSLocalizedString("L2", comment: "")
case .rightShoulder: return NSLocalizedString("R1", comment: "")
case .rightTrigger: return NSLocalizedString("R2", comment: "")
}
default: break
}
return ""
}
}

View File

@ -0,0 +1,28 @@
//
// UIView+ParentViewController.swift
// Delta
//
// Created by Riley Testut on 9/3/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
extension UIView
{
var parentViewController: UIViewController? {
var nextResponder = self.next
while nextResponder != nil
{
if let parentViewController = nextResponder as? UIViewController
{
return parentViewController
}
nextResponder = nextResponder?.next
}
return nil
}
}

View File

@ -1,5 +1,5 @@
//
// PauseMenuViewController.swift
// GridMenuViewController.swift
// Delta
//
// Created by Riley Testut on 12/21/15.
@ -9,39 +9,57 @@
import UIKit
import Roxas
class PauseMenuViewController: UICollectionViewController
class GridMenuViewController: UICollectionViewController
{
var items = [PauseItem]() {
didSet
{
guard oldValue != self.items else { return }
if self.items.count > 8
{
fatalError("PauseViewController only supports up to 8 items (for my sanity when laying out on a landscape iPhone 4s")
var items: [MenuItem] {
get { return self.dataSource.items }
set { self.dataSource.items = newValue; self.updateItems() }
}
self.dataSource.items = self.items
}
}
var isVibrancyEnabled = true
override var preferredContentSize: CGSize {
set { }
get { return self.collectionView?.contentSize ?? CGSize.zero }
}
fileprivate let dataSource = RSTArrayCollectionViewDataSource<PauseItem>(items: [])
fileprivate let dataSource = RSTArrayCollectionViewDataSource<MenuItem>(items: [])
fileprivate var prototypeCell = GridCollectionViewCell()
fileprivate var previousIndexPath: IndexPath? = nil
fileprivate var registeredKVOObservers = Set<NSKeyValueObservation>()
init()
{
let collectionViewLayout = GridCollectionViewLayout()
collectionViewLayout.itemSize = CGSize(width: 60, height: 80)
collectionViewLayout.minimumLineSpacing = 20
collectionViewLayout.minimumInteritemSpacing = 10
super.init(collectionViewLayout: collectionViewLayout)
}
extension PauseMenuViewController
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
deinit
{
// Crashes on iOS 10 if not explicitly invalidated.
self.registeredKVOObservers.forEach { $0.invalidate() }
}
}
extension GridMenuViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView?.register(GridCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in
self.configure(cell as! GridCollectionViewCell, for: indexPath)
}
@ -62,55 +80,66 @@ extension PauseMenuViewController
if let indexPath = self.previousIndexPath
{
UIView.animate(withDuration: 0.2) {
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
}
}
}
}
private extension PauseMenuViewController
private extension GridMenuViewController
{
func configure(_ cell: GridCollectionViewCell, for indexPath: IndexPath)
{
let pauseItem = self.items[(indexPath as NSIndexPath).item]
let pauseItem = self.items[indexPath.item]
cell.maximumImageSize = CGSize(width: 60, height: 60)
cell.imageView.image = pauseItem.image
cell.imageView.contentMode = .center
cell.imageView.layer.borderWidth = 2
cell.imageView.layer.borderColor = UIColor.white.cgColor
cell.imageView.layer.borderColor = self.view.tintColor.cgColor
cell.imageView.layer.cornerRadius = 10
cell.textLabel.text = pauseItem.text
cell.textLabel.textColor = UIColor.white
cell.textLabel.textColor = self.view.tintColor
if pauseItem.selected
if pauseItem.isSelected
{
cell.imageView.tintColor = UIColor.black
cell.imageView.backgroundColor = UIColor.white
cell.imageView.backgroundColor = self.view.tintColor
}
else
{
cell.imageView.tintColor = UIColor.white
cell.imageView.tintColor = self.view.tintColor
cell.imageView.backgroundColor = UIColor.clear
}
cell.isImageViewVibrancyEnabled = true
cell.isTextLabelVibrancyEnabled = true
cell.isImageViewVibrancyEnabled = self.isVibrancyEnabled
cell.isTextLabelVibrancyEnabled = self.isVibrancyEnabled
}
func toggleSelectedStateForPauseItemAtIndexPath(_ indexPath: IndexPath)
func updateItems()
{
let pauseItem = self.items[indexPath.item]
pauseItem.selected = !pauseItem.selected
self.registeredKVOObservers.removeAll()
let cell = self.collectionView!.cellForItem(at: indexPath) as! GridCollectionViewCell
for (index, item) in self.items.enumerated()
{
let observer = item.observe(\.isSelected, changeHandler: { [unowned self] (item, change) in
let indexPath = IndexPath(item: index, section: 0)
if let cell = self.collectionView?.cellForItem(at: indexPath) as? GridCollectionViewCell
{
self.configure(cell, for: indexPath)
}
})
self.registeredKVOObservers.insert(observer)
}
}
}
extension PauseMenuViewController: UICollectionViewDelegateFlowLayout
extension GridMenuViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
@ -121,26 +150,27 @@ extension PauseMenuViewController: UICollectionViewDelegateFlowLayout
}
}
extension PauseMenuViewController
extension GridMenuViewController
{
override func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath)
{
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
}
override func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath)
{
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
self.previousIndexPath = indexPath
self.toggleSelectedStateForPauseItemAtIndexPath(indexPath)
let pauseItem = self.items[indexPath.item]
pauseItem.action(pauseItem)
let item = self.items[indexPath.item]
item.isSelected = !item.isSelected
item.action(item)
}
}

View File

@ -0,0 +1,35 @@
//
// MenuItem.swift
// Delta
//
// Created by Riley Testut on 1/30/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
// Must be class for use with Objective-C generics :(
class MenuItem: NSObject
{
var text: String
var image: UIImage?
var action: ((MenuItem) -> Void)
@objc dynamic var isSelected = false
init(text: String, image: UIImage?, action: @escaping ((MenuItem) -> Void))
{
self.image = image
self.text = text
self.action = action
}
}
extension MenuItem
{
override func isEqual(_ object: Any?) -> Bool
{
guard let item = object as? MenuItem else { return false }
return item.image == self.image && item.text == self.text
}
}

View File

@ -1,31 +0,0 @@
//
// PauseItem.swift
// Delta
//
// Created by Riley Testut on 1/30/16.
// Copyright © 2016 Riley Testut. All rights reserved.
//
import UIKit
// Must be class for use with Objective-C generics :(
class PauseItem: Equatable
{
var image: UIImage
var text: String
var action: ((PauseItem) -> Void)
var selected = false
init(image: UIImage, text: String, action: @escaping ((PauseItem) -> Void))
{
self.image = image
self.text = text
self.action = action
}
}
func ==(lhs: PauseItem, rhs: PauseItem) -> Bool
{
return (lhs.image == rhs.image) && (lhs.text == rhs.text)
}

View File

@ -18,16 +18,16 @@ class PauseViewController: UIViewController, PauseInfoProviding
}
}
var pauseItems: [PauseItem] {
var pauseItems: [MenuItem] {
return [self.saveStateItem, self.loadStateItem, self.cheatCodesItem, self.fastForwardItem, self.sustainButtonsItem].flatMap { $0 }
}
/// Pause Items
var saveStateItem: PauseItem?
var loadStateItem: PauseItem?
var cheatCodesItem: PauseItem?
var fastForwardItem: PauseItem?
var sustainButtonsItem: PauseItem?
var saveStateItem: MenuItem?
var loadStateItem: MenuItem?
var cheatCodesItem: MenuItem?
var fastForwardItem: MenuItem?
var sustainButtonsItem: MenuItem?
/// PauseInfoProviding
var pauseText: String?
@ -85,8 +85,8 @@ extension PauseViewController
self.pauseNavigationController.navigationBar.tintColor = UIColor.deltaPurple
self.pauseNavigationController.view.backgroundColor = UIColor.clear
let pauseMenuViewController = self.pauseNavigationController.topViewController as! PauseMenuViewController
pauseMenuViewController.items = self.pauseItems
let gridMenuViewController = self.pauseNavigationController.topViewController as! GridMenuViewController
gridMenuViewController.items = self.pauseItems
case "saveStates":
let saveStatesViewController = segue.destination as! SaveStatesViewController
@ -135,21 +135,21 @@ private extension PauseViewController
guard self.emulatorCore != nil else { return }
self.saveStateItem = PauseItem(image: #imageLiteral(resourceName: "SaveSaveState"), text: NSLocalizedString("Save State", comment: ""), action: { [unowned self] _ in
self.saveStateItem = MenuItem(text: NSLocalizedString("Save State", comment: ""), image: #imageLiteral(resourceName: "SaveSaveState"), action: { [unowned self] _ in
self.saveStatesViewControllerMode = .saving
self.performSegue(withIdentifier: "saveStates", sender: self)
})
self.loadStateItem = PauseItem(image: #imageLiteral(resourceName: "LoadSaveState"), text: NSLocalizedString("Load State", comment: ""), action: { [unowned self] _ in
self.loadStateItem = MenuItem(text: NSLocalizedString("Load State", comment: ""), image: #imageLiteral(resourceName: "LoadSaveState"), action: { [unowned self] _ in
self.saveStatesViewControllerMode = .loading
self.performSegue(withIdentifier: "saveStates", sender: self)
})
self.cheatCodesItem = PauseItem(image: #imageLiteral(resourceName: "CheatCodes"), text: NSLocalizedString("Cheat Codes", comment: ""), action: { [unowned self] _ in
self.cheatCodesItem = MenuItem(text: NSLocalizedString("Cheat Codes", comment: ""), image: #imageLiteral(resourceName: "CheatCodes"), action: { [unowned self] _ in
self.performSegue(withIdentifier: "cheats", sender: self)
})
self.fastForwardItem = PauseItem(image: #imageLiteral(resourceName: "FastForward"), text: NSLocalizedString("Fast Forward", comment: ""), action: { _ in })
self.sustainButtonsItem = PauseItem(image: #imageLiteral(resourceName: "SustainButtons"), text: NSLocalizedString("Sustain Buttons", comment: ""), action: { _ in })
self.fastForwardItem = MenuItem(text: NSLocalizedString("Fast Forward", comment: ""), image: #imageLiteral(resourceName: "FastForward"), action: { _ in })
self.sustainButtonsItem = MenuItem(text: NSLocalizedString("Sustain Buttons", comment: ""), image: #imageLiteral(resourceName: "SustainButtons"), action: { _ in })
}
}

View File

@ -29,6 +29,9 @@ class PauseStoryboardSegue: UIStoryboardSegue
self.destination.modalPresentationStyle = .custom
self.destination.modalPresentationCapturesStatusBarAppearance = true
// Manually set tint color, since calling layoutIfNeeded will cause view to load, but with default system tint color.
self.destination.view.tintColor = .white
// We need to force layout of destinationViewController.view _before_ animateTransition(using:)
// Otherwise, we'll get "Unable to simultaneously satisfy constraints" errors
self.destination.view.frame = self.source.view.frame

View File

@ -0,0 +1,480 @@
//
// ControllerInputsViewController.swift
// Delta
//
// Created by Riley Testut on 7/1/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
import DeltaCore
import SMCalloutView
class ControllerInputsViewController: UIViewController
{
var gameController: GameController! {
didSet {
self.prepareGameController()
}
}
var system: System = .snes {
didSet {
guard self.system != oldValue else { return }
self.updateSystem()
}
}
fileprivate var inputMapping: GameControllerInputMapping!
fileprivate var previousInputMapping: GameControllerInputMappingProtocol?
fileprivate let supportedActionInputs: [ActionInput] = [.saveState, .loadState, .fastForward]
fileprivate var gameViewController: DeltaCore.GameViewController!
fileprivate var actionsMenuViewController: GridMenuViewController!
fileprivate var calloutViews = [AnyInput: InputCalloutView]()
fileprivate var activeCalloutView: InputCalloutView?
@IBOutlet private var actionsMenuViewControllerHeightConstraint: NSLayoutConstraint!
@IBOutlet private var cancelTapGestureRecognizer: UITapGestureRecognizer!
override func viewDidLoad()
{
super.viewDidLoad()
self.gameViewController.controllerView.addReceiver(self)
self.navigationController?.navigationBar.barStyle = .black
NSLayoutConstraint.activate([self.gameViewController.gameView.centerYAnchor.constraint(equalTo: self.actionsMenuViewController.view.centerYAnchor)])
self.preparePopoverMenuController()
self.updateSystem()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
if self.actionsMenuViewController.preferredContentSize.height > 0
{
self.actionsMenuViewControllerHeightConstraint.constant = self.actionsMenuViewController.preferredContentSize.height
}
}
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if self.calloutViews.isEmpty
{
self.prepareCallouts()
}
}
}
extension ControllerInputsViewController
{
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let identifier = segue.identifier else { return }
switch identifier
{
case "embedGameViewController": self.gameViewController = segue.destination as! DeltaCore.GameViewController
case "embedActionsMenuViewController":
self.actionsMenuViewController = segue.destination as! GridMenuViewController
self.prepareActionsMenuViewController()
case "cancelControllerControls": self.gameController.inputMapping = self.previousInputMapping
case "saveControllerControls": self.gameController.inputMapping = self.inputMapping
default: break
}
}
}
private extension ControllerInputsViewController
{
func updateSystem()
{
guard self.isViewLoaded else { return }
if let popoverMenuButton = self.navigationItem.popoverMenuController?.popoverMenuButton
{
popoverMenuButton.title = self.system.localizedShortName
popoverMenuButton.bounds.size = popoverMenuButton.intrinsicContentSize
self.navigationController?.navigationBar.layoutIfNeeded()
}
self.gameViewController.controllerView.controllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: self.system.gameType)
if self.view.window != nil
{
self.calloutViews.forEach { $1.dismissCallout(animated: true) }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.calloutViews = [:]
self.prepareCallouts()
}
}
}
func prepareGameController()
{
self.gameController.addReceiver(self)
self.previousInputMapping = self.gameController.inputMapping
self.inputMapping = self.gameController.inputMapping as? GameControllerInputMapping ?? GameControllerInputMapping(gameControllerInputType: self.gameController.inputType)
self.inputMapping.name = String.localizedStringWithFormat("Custom %@", self.gameController.name)
self.gameController.inputMapping = nil
}
func preparePopoverMenuController()
{
let listMenuViewController = ListMenuViewController()
listMenuViewController.title = NSLocalizedString("Game System", comment: "")
let navigationController = UINavigationController(rootViewController: listMenuViewController)
let popoverMenuController = PopoverMenuController(popoverViewController: navigationController)
self.navigationItem.popoverMenuController = popoverMenuController
let items = System.supportedSystems.map { [unowned self, weak popoverMenuController, weak listMenuViewController] system -> MenuItem in
let item = MenuItem(text: system.localizedShortName, image: #imageLiteral(resourceName: "CheatCodes")) { [weak popoverMenuController, weak listMenuViewController] item in
listMenuViewController?.items.forEach { $0.isSelected = ($0 == item) }
popoverMenuController?.isActive = false
self.system = system
}
item.isSelected = (system == self.system)
return item
}
listMenuViewController.items = items
}
func prepareActionsMenuViewController()
{
var items = [MenuItem]()
for input in self.supportedActionInputs
{
let image: UIImage
let text: String
switch input
{
case .saveState:
image = #imageLiteral(resourceName: "SaveSaveState")
text = NSLocalizedString("Save State", comment: "")
case .loadState:
image = #imageLiteral(resourceName: "LoadSaveState")
text = NSLocalizedString("Load State", comment: "")
case .fastForward:
image = #imageLiteral(resourceName: "FastForward")
text = NSLocalizedString("Fast Forward", comment: "")
}
let item = MenuItem(text: text, image: image) { (item) in
guard let calloutView = self.calloutViews[AnyInput(input)] else { return }
self.toggle(calloutView)
}
items.append(item)
}
self.actionsMenuViewController.items = items
self.actionsMenuViewController.isVibrancyEnabled = false
}
func prepareCallouts()
{
guard
let controllerView = self.gameViewController.controllerView,
let traits = controllerView.controllerSkinTraits,
let items = controllerView.controllerSkin?.items(for: traits),
let controllerViewInputMapping = controllerView.inputMapping
else { return }
// Implicit assumption that all skins used for controller input mapping don't have multiple items with same input.
let mappedInputs = items.flatMap { $0.inputs.allInputs.flatMap(controllerViewInputMapping.input(forControllerInput:)) } + (self.supportedActionInputs as [Input])
// Create callout view for each on-screen input.
for input in mappedInputs
{
let calloutView = InputCalloutView()
calloutView.delegate = self
self.calloutViews[AnyInput(input)] = calloutView
}
// Update callout views with controller inputs that map to callout views' associated controller skin inputs.
for input in self.inputMapping.supportedControllerInputs
{
let mappedInput = self.mappedInput(for: input)
if let calloutView = self.calloutViews[mappedInput]
{
if let previousInput = calloutView.input
{
// Ensure the input we display has a higher priority.
calloutView.input = (input.displayPriority > previousInput.displayPriority) ? input : previousInput
}
else
{
calloutView.input = input
}
}
}
// Present only callout views that are associated with a controller input.
for calloutView in self.calloutViews.values
{
if let presentationRect = self.presentationRect(for: calloutView), calloutView.input != nil
{
calloutView.presentCallout(from: presentationRect, in: self.view, constrainedTo: self.view, animated: true)
}
}
}
}
private extension ControllerInputsViewController
{
func updateActiveCalloutView(with controllerInput: Input?)
{
guard let activeCalloutView = self.activeCalloutView else { return }
guard let input = self.calloutViews.first(where: { $0.value == activeCalloutView })?.key else { return }
if let controllerInput = controllerInput
{
for (_, calloutView) in self.calloutViews
{
guard let calloutInput = calloutView.input else { continue }
if calloutInput == controllerInput
{
// Hide callout views that previously displayed the controller input.
calloutView.input = nil
calloutView.dismissCallout(animated: true)
}
}
}
for supportedInput in self.inputMapping.supportedControllerInputs
{
let mappedInput = self.mappedInput(for: supportedInput)
if mappedInput == input
{
// Set all existing controller inputs that currently map to "input" to instead map to nil.
self.inputMapping.set(nil, forControllerInput: supportedInput)
}
}
if let controllerInput = controllerInput
{
self.inputMapping.set(input, forControllerInput: controllerInput)
}
activeCalloutView.input = controllerInput
self.toggle(activeCalloutView)
}
func toggle(_ calloutView: InputCalloutView)
{
if let activeCalloutView = self.activeCalloutView, activeCalloutView != calloutView
{
self.toggle(activeCalloutView)
}
let menuItem: MenuItem?
if let input = self.calloutViews.first(where: { $0.value == calloutView })?.key, let index = self.supportedActionInputs.index(where: { $0 == input })
{
menuItem = self.actionsMenuViewController.items[index]
}
else
{
menuItem = nil
}
switch calloutView.state
{
case .normal:
calloutView.state = .listening
menuItem?.isSelected = true
self.activeCalloutView = calloutView
case .listening:
calloutView.state = .normal
menuItem?.isSelected = false
self.activeCalloutView = nil
}
calloutView.dismissCallout(animated: true)
if let presentationRect = self.presentationRect(for: calloutView)
{
if calloutView.state == .listening || calloutView.input != nil
{
calloutView.presentCallout(from: presentationRect, in: self.view, constrainedTo: self.view, animated: true)
}
}
}
}
extension ControllerInputsViewController: UIGestureRecognizerDelegate
{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool
{
return self.activeCalloutView != nil
}
@IBAction private func handleTapGesture(_ tapGestureRecognizer: UITapGestureRecognizer)
{
self.updateActiveCalloutView(with: nil)
}
}
private extension ControllerInputsViewController
{
func mappedInput(for input: Input) -> AnyInput
{
guard let mappedInput = self.inputMapping.input(forControllerInput: input) else {
fatalError("Mapped input for provided input does not exist.")
}
if let standardInput = StandardGameControllerInput(input: mappedInput)
{
if let gameInput = standardInput.input(for: self.system.gameType)
{
return AnyInput(gameInput)
}
}
return AnyInput(mappedInput)
}
func presentationRect(for calloutView: InputCalloutView) -> CGRect?
{
guard let input = self.calloutViews.first(where: { $0.value == calloutView })?.key else { return nil }
guard
let controllerView = self.gameViewController.controllerView,
let traits = controllerView.controllerSkinTraits,
let items = controllerView.controllerSkin?.items(for: traits)
else { return nil }
if let item = items.first(where: { $0.inputs.allInputs.contains(where: { $0.stringValue == input.stringValue })})
{
// Input is a controller skin input.
let itemFrame: CGRect?
switch item.inputs
{
case .standard: itemFrame = item.frame
case let .directional(up, down, left, right):
switch input.stringValue
{
case up.stringValue:
itemFrame = CGRect(x: item.frame.minX + item.frame.width / 3,
y: item.frame.minY,
width: item.frame.width / 3,
height: item.frame.height / 3)
case down.stringValue:
itemFrame = CGRect(x: item.frame.minX + item.frame.width / 3,
y: item.frame.minY + (item.frame.height / 3) * 2,
width: item.frame.width / 3,
height: item.frame.height / 3)
case left.stringValue:
itemFrame = CGRect(x: item.frame.minX,
y: item.frame.minY + (item.frame.height / 3),
width: item.frame.width / 3,
height: item.frame.height / 3)
case right.stringValue:
itemFrame = CGRect(x: item.frame.minX + (item.frame.width / 3) * 2,
y: item.frame.minY + (item.frame.height / 3),
width: item.frame.width / 3,
height: item.frame.height / 3)
default: itemFrame = nil
}
}
if let itemFrame = itemFrame
{
var presentationFrame = itemFrame.applying(CGAffineTransform(scaleX: controllerView.bounds.width, y: controllerView.bounds.height))
presentationFrame = self.view.convert(presentationFrame, from: controllerView)
return presentationFrame
}
}
else if let index = self.supportedActionInputs.index(where: { $0 == input })
{
// Input is an ActionInput.
let indexPath = IndexPath(item: index, section: 0)
if let attributes = self.actionsMenuViewController.collectionViewLayout.layoutAttributesForItem(at: indexPath)
{
let presentationFrame = self.view.convert(attributes.frame, from: self.actionsMenuViewController.view)
return presentationFrame
}
}
else
{
// Input is not an on-screen input.
}
return nil
}
}
extension ControllerInputsViewController: GameControllerReceiver
{
func gameController(_ gameController: GameController, didActivate controllerInput: DeltaCore.Input)
{
switch gameController
{
case self.gameViewController.controllerView:
if let calloutView = self.calloutViews[AnyInput(controllerInput)]
{
self.toggle(calloutView)
}
case self.gameController: self.updateActiveCalloutView(with: controllerInput)
default: break
}
}
func gameController(_ gameController: GameController, didDeactivate input: DeltaCore.Input)
{
}
}
extension ControllerInputsViewController: SMCalloutViewDelegate
{
func calloutViewClicked(_ calloutView: SMCalloutView)
{
guard let calloutView = calloutView as? InputCalloutView else { return }
self.toggle(calloutView)
}
}

View File

@ -9,37 +9,52 @@
import UIKit
import DeltaCore
private enum ControllersSettingsSection: Int
import Roxas
extension ControllersSettingsViewController
{
fileprivate enum Section: Int
{
case none
case localDevice
case externalControllers
case customizeControls
}
}
private class LocalDeviceController: ExternalController
private class LocalDeviceController: NSObject, GameController
{
override var name: String {
var name: String {
return UIDevice.current.name
}
var playerIndex: Int? {
set { Settings.localControllerPlayerIndex = newValue }
get { return Settings.localControllerPlayerIndex }
}
let inputType: GameControllerInputType = .standard
var inputMapping: GameControllerInputMappingProtocol?
}
class ControllersSettingsViewController: UITableViewController
{
var playerIndex: Int? {
didSet
{
if let playerIndex = self.playerIndex
{
self.title = NSLocalizedString("Player \(playerIndex + 1)", comment: "")
}
else
{
self.title = NSLocalizedString("Controllers", comment: "")
}
var playerIndex: Int! {
didSet {
self.title = NSLocalizedString("Player \(self.playerIndex + 1)", comment: "")
}
}
fileprivate var connectedControllers = ExternalControllerManager.shared.connectedControllers.sorted(by: { $0.playerIndex ?? NSIntegerMax < $1.playerIndex ?? NSIntegerMax })
fileprivate var gameController: GameController? {
didSet {
oldValue?.playerIndex = nil
self.gameController?.playerIndex = self.playerIndex
}
}
fileprivate var connectedControllers = ExternalGameControllerManager.shared.connectedControllers.sorted(by: { $0.playerIndex ?? NSIntegerMax < $1.playerIndex ?? NSIntegerMax })
fileprivate lazy var localDeviceController: LocalDeviceController = {
let device = LocalDeviceController()
@ -52,146 +67,76 @@ class ControllersSettingsViewController: UITableViewController
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalControllerDidConnect(_:)), name: .externalControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalControllerDidDisconnect(_:)), name: .externalControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalGameControllerDidConnect(_:)), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ControllersSettingsViewController.externalGameControllerDidDisconnect(_:)), name: .externalGameControllerDidDisconnect, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
}
//MARK: - Storyboards -
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
let gameControllers = [self.localDeviceController as GameController] + self.connectedControllers
for gameController in gameControllers
{
guard let indexPath = self.tableView.indexPathForSelectedRow else { return }
var controllers = self.connectedControllers
controllers.append(self.localDeviceController)
// Reset previous controller
if let playerIndex = self.playerIndex, let index = controllers.index(where: { $0.playerIndex == playerIndex })
if gameController.playerIndex == self.playerIndex
{
let controller = controllers[index]
controller.playerIndex = nil
self.gameController = gameController
break
}
switch ControllersSettingsSection(rawValue: (indexPath as NSIndexPath).section)!
{
case .none: break
case .localDevice: self.localDeviceController.playerIndex = self.playerIndex
case .externalControllers:
let controller = self.connectedControllers[(indexPath as NSIndexPath).row]
controller.playerIndex = self.playerIndex
}
// Updates in case we reset it above, as well as if we updated in the switch statement
Settings.localControllerPlayerIndex = self.localDeviceController.playerIndex
}
}
private extension ControllersSettingsViewController
{
dynamic func externalControllerDidConnect(_ notification: Notification)
{
guard let controller = notification.object as? ExternalController else { return }
if let playerIndex = controller.playerIndex
{
self.connectedControllers.insert(controller, at: playerIndex)
}
else
{
self.connectedControllers.append(controller)
}
if let index = self.connectedControllers.index(of: controller)
{
if self.connectedControllers.count == 1
{
self.tableView.insertSections(IndexSet(integer: ControllersSettingsSection.externalControllers.rawValue), with: .fade)
}
else
{
self.tableView.insertRows(at: [IndexPath(row: index, section: ControllersSettingsSection.externalControllers.rawValue)], with: .automatic)
}
}
}
dynamic func externalControllerDidDisconnect(_ notification: Notification)
{
guard let controller = notification.object as? ExternalController else { return }
if let index = self.connectedControllers.index(of: controller)
{
self.connectedControllers.remove(at: index)
if self.connectedControllers.count == 0
{
self.tableView.deleteSections(IndexSet(integer: ControllersSettingsSection.externalControllers.rawValue), with: .fade)
}
else
{
self.tableView.deleteRows(at: [IndexPath(row: index, section: ControllersSettingsSection.externalControllers.rawValue)], with: .automatic)
}
}
if controller.playerIndex == self.playerIndex
{
self.tableView.reloadSections(IndexSet(integer: ControllersSettingsSection.none.rawValue), with: .none)
}
}
}
extension ControllersSettingsViewController
{
override func numberOfSections(in tableView: UITableView) -> Int
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if self.connectedControllers.count == 0
{
return 2
}
guard let identifier = segue.identifier else { return }
return 3
}
switch identifier
{
case "controllerInputsSegue":
let controllerInputsViewController = (segue.destination as! UINavigationController).topViewController as! ControllerInputsViewController
controllerInputsViewController.gameController = self.gameController
controllerInputsViewController.system = .snes
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch ControllersSettingsSection(rawValue: section)!
{
case .none: return 1
case .localDevice: return 1
case .externalControllers: return self.connectedControllers.count
default: break
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
@IBAction private func unwindFromControllerControlsViewController(_ segue: UIStoryboardSegue)
{
}
}
private extension ControllersSettingsViewController
{
func configure(_ cell: UITableViewCell, for indexPath: IndexPath)
{
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.detailTextLabel?.text = nil
cell.accessoryType = .none
cell.detailTextLabel?.text = nil
cell.textLabel?.textColor = .darkText
if (indexPath as NSIndexPath).section == ControllersSettingsSection.none.rawValue
switch Section(rawValue: indexPath.section)!
{
case .none:
cell.textLabel?.text = NSLocalizedString("None", comment: "")
if Settings.localControllerPlayerIndex != self.playerIndex && !self.connectedControllers.contains(where: { $0.playerIndex == self.playerIndex })
{
cell.accessoryType = .checkmark
}
}
else
{
let controller: ExternalController
if (indexPath as NSIndexPath).section == ControllersSettingsSection.localDevice.rawValue
case .localDevice, .externalControllers:
let controller: GameController
if indexPath.section == Section.localDevice.rawValue
{
controller = self.localDeviceController
}
else if (indexPath as NSIndexPath).section == ControllersSettingsSection.externalControllers.rawValue
else if indexPath.section == Section.externalControllers.rawValue
{
controller = self.connectedControllers[(indexPath as NSIndexPath).row]
controller = self.connectedControllers[indexPath.row]
}
else
{
@ -210,20 +155,177 @@ extension ControllersSettingsViewController
{
cell.detailTextLabel?.text = NSLocalizedString("Player \(playerIndex + 1)", comment: "")
}
}
case .customizeControls:
cell.textLabel?.text = NSLocalizedString("Customize Controls…", comment: "")
cell.textLabel?.textColor = self.view.tintColor
}
}
}
private extension ControllersSettingsViewController
{
dynamic func externalGameControllerDidConnect(_ notification: Notification)
{
guard let controller = notification.object as? GameController else { return }
if let playerIndex = controller.playerIndex
{
// Keep connected controllers sorted.
self.connectedControllers.insert(controller, at: playerIndex)
}
else
{
self.connectedControllers.append(controller)
}
if let index = self.connectedControllers.index(where: { $0 == controller })
{
if self.connectedControllers.count == 1
{
self.tableView.insertSections(IndexSet(integer: Section.externalControllers.rawValue), with: .fade)
}
else
{
self.tableView.insertRows(at: [IndexPath(row: index, section: Section.externalControllers.rawValue)], with: .automatic)
}
}
}
dynamic func externalGameControllerDidDisconnect(_ notification: Notification)
{
guard let controller = notification.object as? GameController else { return }
if let index = self.connectedControllers.index(where: { $0 == controller })
{
self.connectedControllers.remove(at: index)
if self.connectedControllers.count == 0
{
self.tableView.deleteSections(IndexSet(integer: Section.externalControllers.rawValue), with: .fade)
}
else
{
self.tableView.deleteRows(at: [IndexPath(row: index, section: Section.externalControllers.rawValue)], with: .automatic)
}
}
if controller.playerIndex == self.playerIndex
{
self.tableView.reloadSections(IndexSet(integer: Section.none.rawValue), with: .none)
}
}
}
extension ControllersSettingsViewController
{
override func numberOfSections(in tableView: UITableView) -> Int
{
if self.connectedControllers.count == 0
{
return 2
}
if self.gameController == nil || Settings.localControllerPlayerIndex == self.playerIndex
{
return 3
}
return 4
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch Section(rawValue: section)!
{
case .none: return 1
case .localDevice: return 1
case .externalControllers: return self.connectedControllers.count
case .customizeControls: return 1
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: RSTCellContentGenericCellIdentifier, for: indexPath)
self.configure(cell, for: indexPath)
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
switch ControllersSettingsSection(rawValue: section)!
switch Section(rawValue: section)!
{
case .none: return nil
case .localDevice: return NSLocalizedString("Local Device", comment: "")
case .externalControllers: return self.connectedControllers.count > 0 ? NSLocalizedString("External Controllers", comment: "") : ""
case .customizeControls: return nil
}
}
}
extension ControllersSettingsViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let previousIndexPath: IndexPath?
if let gameController = self.gameController
{
if gameController == self.localDeviceController
{
previousIndexPath = IndexPath(row: 0, section: Section.localDevice.rawValue)
}
else if let row = self.connectedControllers.index(where: { $0 == gameController })
{
previousIndexPath = IndexPath(row: row, section: Section.externalControllers.rawValue)
}
else
{
previousIndexPath = nil
}
}
else
{
previousIndexPath = IndexPath(row: 0, section: Section.none.rawValue)
}
switch Section(rawValue: indexPath.section)!
{
case .none: self.gameController = nil
case .localDevice: self.gameController = self.localDeviceController
case .externalControllers: self.gameController = self.connectedControllers[indexPath.row]
case .customizeControls:
guard let cell = tableView.cellForRow(at: indexPath) else { return }
self.performSegue(withIdentifier: "controllerInputsSegue", sender: cell)
return
}
self.tableView.beginUpdates()
if let previousIndexPath = previousIndexPath, let cell = tableView.cellForRow(at: previousIndexPath)
{
// Must configure cell directly, or else a strange animation occurs when reloading row on iOS 11.
self.configure(cell, for: previousIndexPath)
}
self.tableView.reloadRows(at: [indexPath], with: .none)
if self.numberOfSections(in: self.tableView) > self.tableView.numberOfSections
{
self.tableView.insertSections(IndexSet(integer: Section.customizeControls.rawValue), with: .fade)
}
else if self.numberOfSections(in: self.tableView) < self.tableView.numberOfSections
{
self.tableView.deleteSections(IndexSet(integer: Section.customizeControls.rawValue), with: .fade)
}
self.tableView.endUpdates()
}
}

View File

@ -0,0 +1,84 @@
//
// InputCalloutView.swift
// Delta
//
// Created by Riley Testut on 7/9/17.
// Copyright © 2017 Riley Testut. All rights reserved.
//
import SMCalloutView
import DeltaCore
extension InputCalloutView
{
enum State
{
case normal
case listening
}
}
class InputCalloutView: SMCalloutView
{
var input: Input? {
didSet {
self.updateState()
}
}
var state: State = .normal {
didSet {
self.updateState()
}
}
fileprivate let textLabel: UILabel
init()
{
self.textLabel = UILabel()
self.textLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
self.textLabel.textAlignment = .center
super.init(frame: CGRect.zero)
self.titleView = self.textLabel
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.updateTintColor()
}
}
private extension InputCalloutView
{
func updateState()
{
switch self.state
{
case .normal: self.textLabel.text = self.input?.localizedName
case .listening: self.textLabel.text = NSLocalizedString("Press Button", comment: "")
}
self.updateTintColor()
self.textLabel.sizeToFit()
}
func updateTintColor()
{
switch self.state
{
case .normal: self.textLabel.textColor = self.tintColor
case .listening: self.textLabel.textColor = .red
}
}
}

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13196" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13173"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -29,7 +29,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tls-Hv-Rx2">
<rect key="frame" x="15" y="12" width="56" height="19.5"/>
<rect key="frame" x="16" y="12" width="56" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -53,7 +53,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="e3u-x9-IEC">
<rect key="frame" x="15" y="12" width="58" height="19.5"/>
<rect key="frame" x="16" y="12" width="58" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -77,7 +77,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Cdn-11-xZe">
<rect key="frame" x="15" y="12" width="58.5" height="19.5"/>
<rect key="frame" x="16" y="12" width="58.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -101,7 +101,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 4" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Hls-3b-EaS">
<rect key="frame" x="15" y="12" width="59" height="19.5"/>
<rect key="frame" x="16" y="12" width="59" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -129,7 +129,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="mBC-YU-BVK">
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -146,7 +146,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Dxs-Me-IVU">
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -163,7 +163,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="o9x-Kn-6bC">
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -184,7 +184,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="whi-If-wFf">
<rect key="frame" x="74" y="7" width="288" height="31"/>
<rect key="frame" x="75" y="7" width="286" height="31"/>
<connections>
<action selector="beginChangingControllerOpacityWith:" destination="eHi-aO-uGS" eventType="touchDown" id="NG9-FX-62d"/>
<action selector="changeControllerOpacityWith:" destination="eHi-aO-uGS" eventType="valueChanged" id="Zci-tN-4uU"/>
@ -194,7 +194,7 @@
</connections>
</slider>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="50%" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zaz-yD-CYG">
<rect key="frame" x="15" y="11" width="46" height="21"/>
<rect key="frame" x="16" y="11" width="46" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="ACD-qY-k0J"/>
<constraint firstAttribute="width" constant="46" id="ZVd-ie-qRm"/>
@ -240,6 +240,78 @@
</objects>
<point key="canvasLocation" x="1555" y="471"/>
</scene>
<!--Controls-->
<scene sceneID="Gi9-m1-y9x">
<objects>
<viewController title="Controls" id="x1g-pH-DnF" customClass="ControllerInputsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="cH1-gu-g2u"/>
<viewControllerLayoutGuide type="bottom" id="Z6c-bc-h6l"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="cPg-qa-ERT">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Wl-el-X30" userLabel="GameViewController">
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
<connections>
<segue destination="LIv-AL-s86" kind="embed" identifier="embedGameViewController" id="2Qg-Jw-0mM"/>
</connections>
</containerView>
<containerView opaque="NO" contentMode="scaleToFill" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="KkE-ji-6Y8" userLabel="GridMenuViewController">
<rect key="frame" x="0.0" y="233.5" width="375" height="200"/>
<constraints>
<constraint firstAttribute="height" constant="200" id="MWA-T4-ROi"/>
</constraints>
<connections>
<segue destination="Jpj-e9-6XW" kind="embed" identifier="embedActionsMenuViewController" id="kfu-fO-l6Z"/>
</connections>
</containerView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="KkE-ji-6Y8" firstAttribute="centerY" secondItem="cPg-qa-ERT" secondAttribute="centerY" placeholder="YES" id="4wi-cL-aCQ"/>
<constraint firstItem="Z6c-bc-h6l" firstAttribute="top" secondItem="6Wl-el-X30" secondAttribute="bottom" id="Bmp-yB-Yf1"/>
<constraint firstAttribute="trailing" secondItem="KkE-ji-6Y8" secondAttribute="trailing" id="Jeb-8K-VYw"/>
<constraint firstItem="6Wl-el-X30" firstAttribute="top" secondItem="cH1-gu-g2u" secondAttribute="bottom" id="TD2-bx-DJC"/>
<constraint firstAttribute="trailing" secondItem="6Wl-el-X30" secondAttribute="trailing" id="Xph-DL-tBk"/>
<constraint firstItem="6Wl-el-X30" firstAttribute="leading" secondItem="cPg-qa-ERT" secondAttribute="leading" id="gcd-77-5wR"/>
<constraint firstItem="KkE-ji-6Y8" firstAttribute="leading" secondItem="cPg-qa-ERT" secondAttribute="leading" id="z7N-Cn-hGs"/>
</constraints>
<connections>
<outletCollection property="gestureRecognizers" destination="4p8-OB-LsR" appends="YES" id="4k4-Oj-XtP"/>
</connections>
</view>
<navigationItem key="navigationItem" id="UeP-Yr-9jA">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="QfC-sf-WbP">
<connections>
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="cancelControllerControls" unwindAction="unwindFromControllerControlsViewController:" id="AkD-Lu-h5b"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="save" id="WHh-7W-jpl">
<connections>
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="saveControllerControls" unwindAction="unwindFromControllerControlsViewController:" id="4xr-OB-4dx"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<connections>
<outlet property="actionsMenuViewControllerHeightConstraint" destination="MWA-T4-ROi" id="itx-dZ-m76"/>
<outlet property="cancelTapGestureRecognizer" destination="4p8-OB-LsR" id="coO-FL-pbp"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="DqP-Jn-rth" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="8l5-7I-Z7e" userLabel="Exit" sceneMemberID="exit"/>
<tapGestureRecognizer delaysTouchesBegan="YES" id="4p8-OB-LsR">
<connections>
<action selector="handleTapGesture:" destination="x1g-pH-DnF" id="8KO-75-4Iy"/>
<outlet property="delegate" destination="x1g-pH-DnF" id="GDY-v6-naf"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="3809" y="471"/>
</scene>
<!--Controllers-->
<scene sceneID="swa-DT-VKS">
<objects>
@ -257,7 +329,7 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Controller Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VBO-V1-Wfu">
<rect key="frame" x="15" y="12" width="118.5" height="19.5"/>
<rect key="frame" x="16" y="12" width="118.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@ -272,9 +344,6 @@
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="HzL-SB-qry" kind="unwind" identifier="unwindControllersSegue" unwindAction="unwindFromControllersSettingsViewController:" id="WJe-ZI-clo"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
@ -284,9 +353,11 @@
</tableView>
<navigationItem key="navigationItem" title="Controllers" id="QK7-oi-2jJ"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<segue destination="0QR-U9-gtx" kind="presentation" identifier="controllerInputsSegue" id="E3Y-yV-zT5"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="owG-Kh-rfn" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="HzL-SB-qry" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2221" y="471"/>
</scene>
@ -416,7 +487,7 @@
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="ssH-mM-uG6" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="Ckw-ES-lkE">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
@ -428,5 +499,76 @@
</objects>
<point key="canvasLocation" x="889" y="471"/>
</scene>
<!--Grid Menu View Controller-->
<scene sceneID="Lgi-Ii-M1W">
<objects>
<collectionViewController id="Jpj-e9-6XW" customClass="GridMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="yGk-jU-wZQ">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="tLr-UM-1BH" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
<size key="itemSize" width="50" height="50"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="Hef-IR-nMO" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="Jpj-e9-6XW" id="iAK-8A-KXA"/>
<outlet property="delegate" destination="Jpj-e9-6XW" id="sbi-az-9kr"/>
</connections>
</collectionView>
<size key="freeformSize" width="375" height="667"/>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="pRg-BA-3KK" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4566" y="-216"/>
</scene>
<!--Game View Controller-->
<scene sceneID="qAz-yz-iOc">
<objects>
<viewController id="LIv-AL-s86" customClass="GameViewController" customModule="DeltaCore" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="9u3-RP-Qcj"/>
<viewControllerLayoutGuide type="bottom" id="XGZ-ro-kQv"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="57g-cn-rbZ">
<rect key="frame" x="0.0" y="0.0" width="375" height="603"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="uQK-ch-9AG" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4566" y="471"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="bwW-s2-fcE">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" sceneMemberID="viewController">
<toolbarItems/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="Y5H-O6-CQ5">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="x1g-pH-DnF" kind="relationship" relationship="rootViewController" id="EOa-ao-vBI"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="D4f-Fb-zfa" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2977" y="471"/>
</scene>
</scenes>
</document>

View File

@ -46,8 +46,8 @@ class SettingsViewController: UITableViewController
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalControllerDidConnect(_:)), name: .externalControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalControllerDidDisconnect(_:)), name: .externalControllerDidDisconnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalGameControllerDidConnect(_:)), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalGameControllerDidDisconnect(_:)), name: .externalGameControllerDidDisconnect, object: nil)
}
override func viewDidLoad()
@ -64,6 +64,13 @@ class SettingsViewController: UITableViewController
if let indexPath = self.tableView.indexPathForSelectedRow
{
if indexPath.section == Section.controllers.rawValue
{
// Update and temporarily re-select selected row.
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.none)
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
}
@ -111,18 +118,6 @@ private extension SettingsViewController
}
}
private extension SettingsViewController
{
@IBAction func unwindFromControllersSettingsViewController(_ segue: UIStoryboardSegue)
{
let indexPath = self.tableView.indexPathForSelectedRow
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
self.tableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.none)
}
}
private extension SettingsViewController
{
@IBAction func beginChangingControllerOpacity(with sender: UISlider)
@ -154,12 +149,12 @@ private extension SettingsViewController
private extension SettingsViewController
{
dynamic func externalControllerDidConnect(_ notification: Notification)
dynamic func externalGameControllerDidConnect(_ notification: Notification)
{
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
}
dynamic func externalControllerDidDisconnect(_ notification: Notification)
dynamic func externalGameControllerDidDisconnect(_ notification: Notification)
{
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
}
@ -190,9 +185,9 @@ extension SettingsViewController
{
cell.detailTextLabel?.text = UIDevice.current.name
}
else if let index = ExternalControllerManager.shared.connectedControllers.index(where: { $0.playerIndex == indexPath.row })
else if let index = ExternalGameControllerManager.shared.connectedControllers.index(where: { $0.playerIndex == indexPath.row })
{
let controller = ExternalControllerManager.shared.connectedControllers[index]
let controller = ExternalGameControllerManager.shared.connectedControllers[index]
cell.detailTextLabel?.text = controller.name
}
else

View File

@ -9,6 +9,7 @@ target 'Delta' do
pod 'SDWebImage', '~> 3.8'
pod 'Fabric', '~> 1.6.0'
pod 'Crashlytics', '~> 3.8.0'
pod 'SMCalloutView'
end
post_install do |installer|

View File

@ -6,6 +6,7 @@ PODS:
- SDWebImage (3.8.2):
- SDWebImage/Core (= 3.8.2)
- SDWebImage/Core (3.8.2)
- SMCalloutView (2.1.5)
- SQLite.swift (0.11.3):
- SQLite.swift/standard (= 0.11.3)
- SQLite.swift/standard (0.11.3)
@ -15,6 +16,7 @@ DEPENDENCIES:
- Fabric (~> 1.6.0)
- FileMD5Hash (~> 2.0.0)
- SDWebImage (~> 3.8)
- SMCalloutView
- SQLite.swift (~> 0.11.0)
SPEC CHECKSUMS:
@ -22,8 +24,9 @@ SPEC CHECKSUMS:
Fabric: 5911403591946b8228ab1c51d98f1d7137e863c6
FileMD5Hash: 3ed69cc19a21ff4d30ae8833fc104275ad2c7de0
SDWebImage: '098e97e6176540799c27e804c96653ee0833d13c'
SMCalloutView: 5c0ee363dc8e7204b2fda17dfad38c93e9e23481
SQLite.swift: 99b36c22084427f0abbeb957556ce1528cf10bb3
PODFILE CHECKSUM: de6e2bf57dcf8e9fe6b622b181fd87d3855641e6
PODFILE CHECKSUM: 598f830560ac5b18bbe0eb40134a1719f38f12f1
COCOAPODS: 1.2.1

5
Pods/Manifest.lock generated
View File

@ -6,6 +6,7 @@ PODS:
- SDWebImage (3.8.2):
- SDWebImage/Core (= 3.8.2)
- SDWebImage/Core (3.8.2)
- SMCalloutView (2.1.5)
- SQLite.swift (0.11.3):
- SQLite.swift/standard (= 0.11.3)
- SQLite.swift/standard (0.11.3)
@ -15,6 +16,7 @@ DEPENDENCIES:
- Fabric (~> 1.6.0)
- FileMD5Hash (~> 2.0.0)
- SDWebImage (~> 3.8)
- SMCalloutView
- SQLite.swift (~> 0.11.0)
SPEC CHECKSUMS:
@ -22,8 +24,9 @@ SPEC CHECKSUMS:
Fabric: 5911403591946b8228ab1c51d98f1d7137e863c6
FileMD5Hash: 3ed69cc19a21ff4d30ae8833fc104275ad2c7de0
SDWebImage: '098e97e6176540799c27e804c96653ee0833d13c'
SMCalloutView: 5c0ee363dc8e7204b2fda17dfad38c93e9e23481
SQLite.swift: 99b36c22084427f0abbeb957556ce1528cf10bb3
PODFILE CHECKSUM: de6e2bf57dcf8e9fe6b622b181fd87d3855641e6
PODFILE CHECKSUM: 598f830560ac5b18bbe0eb40134a1719f38f12f1
COCOAPODS: 1.2.1

File diff suppressed because it is too large Load Diff

176
Pods/SMCalloutView/LICENSE generated Normal file
View File

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

99
Pods/SMCalloutView/README.md generated Normal file
View File

@ -0,0 +1,99 @@
![Example Screenshot](SampleAssets/CalloutScreenshot.png)
Overview
--------
SMCalloutView aims to be an exact replica of the private UICalloutView system
control.
We all love those "bubbles" you get when clicking pins in MKMapView. But
sadly, it's impossible to present this bubble-style "Callout" UI anywhere
outside MKMapView. Phooey! So this class _painstakingly_ recreates this handy
control for your pleasure.
Usage
-----
To use SMCalloutView in your own projects, simply copy the files
`SMCalloutView.h` and `SMCalloutView.m`.
SMCalloutView, by default, will render in the new style introduced with
iOS 7. If you need the old style, simply include `SMClassicCalloutView.h`
and `SMClassicCalloutView.m` in your project as well. There is a special
class constructor, `+[SMCalloutView platformCalloutView]` which will
automatically select the appropriate callout class for the current platform.
The comments in `SMCalloutView.h` do a lot of explaining on how to use the
class, but the main function you'll need is `presentCalloutFromRect:`. You'll
specify the view you'd like to add the callout to, as well as the rect
defining the "target" that the popup should point at. The target rect should
be _in the coordinate system of the target view_ (just like the similarly-
named `UIPopover` method). Most likely this will be `target.frame` if you're
adding the callout view as a sibling of the target view, or it would be
`target.bounds` if you're adding the callout view to the target itself.
You can study the included project's UIViewController subclasses for a working
example.
Questions
---------
#### How do I change the height of the callout?
If you use only the `title/titleView/subtitle/subtitleView` properties, the
callout will always be the "system standard" height. If you assign the
`contentView` property however, then the callout will size to fit the
`contentView` and the other properties are ignored.
[#29]: https://github.com/nfarina/calloutview/issues/29
#### Can I customize the background graphics?
Yes, the callout background is an instance of `SMCalloutBackgroundView`. You
can set your own custom `View` subclass to be the background, or you can use
one of the built-in subclasses:
- `SMCalloutMaskedBackgroundView` renders an iOS-7 style background.
- `SMCalloutImageBackgroundView` lets you specify each of the image
"fragments" that make up a horizontally-stretchable background.
- `SMCalloutDrawnBackgroundView` draws the background at any size using
CoreGraphics methods. You can copy the `-drawRect` method and change the
parameters to suit your needs.
#### Can I use the callout with the Google Maps iOS SDK?
Check out [ryanmaxwell's demo project][googlemaps] for an example of one way
to do this. ([More discussion on this topic][#25])
[googlemaps]: https://github.com/ryanmaxwell/GoogleMapsCalloutView
[#25]: https://github.com/nfarina/calloutview/issues/25
#### Have you recreated more of MapKit?
Nope, but other intrepid coders have!
- For an awesome replacement of the pulsing blue "Current Location" dot, check
out [Sam Vermette's SVPulsingAnnotationView][dot].
- And for the outdoor map data and tiles themselves, check out [Mapbox's iOS
SDK][mapbox], a complete open-source solution for custom maps. They even use
`SMCalloutView` out of the box!
[dot]: https://github.com/samvermette/SVPulsingAnnotationView
[mapbox]: https://www.mapbox.com/mobile/
More Info
---------
You can read more info if you wish in the [blog post][].
[blog post]: http://nfarina.com/post/78014139253/smcalloutview-for-ios-7

200
Pods/SMCalloutView/SMCalloutView.h generated Executable file
View File

@ -0,0 +1,200 @@
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
/*
SMCalloutView
-------------
Created by Nick Farina (nfarina@gmail.com)
Version 2.1.5
*/
/// options for which directions the callout is allowed to "point" in.
typedef NS_OPTIONS(NSUInteger, SMCalloutArrowDirection) {
SMCalloutArrowDirectionUp = 1 << 0,
SMCalloutArrowDirectionDown = 1 << 1,
SMCalloutArrowDirectionAny = SMCalloutArrowDirectionUp | SMCalloutArrowDirectionDown
};
/// options for the callout present/dismiss animation
typedef NS_ENUM(NSInteger, SMCalloutAnimation) {
/// the "bounce" animation we all know and love from @c UIAlertView
SMCalloutAnimationBounce,
/// a simple fade in or out
SMCalloutAnimationFade,
/// grow or shrink linearly, like in the iPad Calendar app
SMCalloutAnimationStretch
};
NS_ASSUME_NONNULL_BEGIN
/// when delaying our popup in order to scroll content into view, you can use this amount to match the
/// animation duration of UIScrollView when using @c -setContentOffset:animated.
extern NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView;
@protocol SMCalloutViewDelegate;
@class SMCalloutBackgroundView;
//
// Callout view.
//
@interface SMCalloutView : UIView
@property (nonatomic, weak, nullable) id<SMCalloutViewDelegate> delegate;
/// title/titleView relationship mimics UINavigationBar.
@property (nonatomic, copy, nullable) NSString *title;
@property (nonatomic, copy, nullable) NSString *subtitle;
/// Left accessory view for the call out
@property (nonatomic, strong, nullable) UIView *leftAccessoryView;
/// Right accessoty view for the call out
@property (nonatomic, strong, nullable) UIView *rightAccessoryView;
/// Default @c SMCalloutArrowDirectionDown
@property (nonatomic, assign) SMCalloutArrowDirection permittedArrowDirection;
/// The current arrow direction
@property (nonatomic, readonly) SMCalloutArrowDirection currentArrowDirection;
/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets.
@property (nonatomic, assign) UIEdgeInsets constrainedInsets;
/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView
@property (nonatomic, strong) SMCalloutBackgroundView *backgroundView;
/**
@brief Custom title view.
@disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view
may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
@warning If this is set, the respective @c title property will be ignored.
*/
@property (nonatomic, strong, nullable) UIView *titleView;
/**
@brief Custom subtitle view.
@discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view
may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized.
@warning If this is set, the respective @c subtitle property will be ignored.
*/
@property (nonatomic, strong, nullable) UIView *subtitleView;
/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored.
@property (nonatomic, retain, nullable) UIView *contentView;
/// Custom content view margin
@property (nonatomic, assign) UIEdgeInsets contentViewInset;
/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown.
@property (nonatomic, assign) CGPoint calloutOffset;
/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively
@property (nonatomic, assign) SMCalloutAnimation presentAnimation, dismissAnimation;
/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available.
+ (SMCalloutView *)platformCalloutView;
/**
@brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds.
@discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins)
if @c -delegate is set and responds to @c -delayForRepositionWithSize.
@param rect @c CGRect to present the view from
@param view view to 'constrain' the @c constrainedView to
@param constrainedView @c UIView to be constrainted in @c view
@param animated @c BOOL if presentation should be animated
*/
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated;
/**
@brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds
@discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead.
@note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events.
@param rect @c CGRect to present the view from
@param layer layer to 'constrain' the @c constrainedLayer to
@param constrainedLayer @c CALayer to be constrained in @c layer
@param animated @c BOOL if presentation should be animated
*/
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated;
/**
Dismiss the callout view
@param animated @c BOOL if dismissal should be animated
*/
- (void)dismissCalloutAnimated:(BOOL)animated;
/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout.
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting;
@end
//
// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow).
//
/// Abstract base class
@interface SMCalloutBackgroundView : UIView
/// indicates where the tip of the arrow should be drawn, as a pixel offset
@property (nonatomic, assign) CGPoint arrowPoint;
/// will be set by the callout when the callout is in a highlighted state
@property (nonatomic, assign) BOOL highlighted;
/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView )
@property (nonatomic, assign) CALayer *contentMask;
/// height of the callout "arrow"
@property (nonatomic, assign) CGFloat anchorHeight;
/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right
@property (nonatomic, assign) CGFloat anchorMargin;
@end
/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view.
/// Accessories are masked by the shape of the callout (including the arrow itself).
@interface SMCalloutMaskedBackgroundView : SMCalloutBackgroundView
@end
//
// Delegate methods
//
@protocol SMCalloutViewDelegate <NSObject>
@optional
/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below.
/// Not honored by @c SMClassicCalloutView.
- (BOOL)calloutViewShouldHighlight:(SMCalloutView *)calloutView;
/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView.
- (void)calloutViewClicked:(SMCalloutView *)calloutView;
/**
Called when the callout view detects that it will be outside the constrained view when it appears,
or if the target rect was already outside the constrained view. You can implement this selector
to respond to this situation by repositioning your content first in order to make everything visible.
The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin).
It expects you to return the amount of time you need to reposition things so the popup can be delayed.
Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:].
@param calloutView the @c SMCalloutView to reposition
@param offset caluclated offset necessary to make everything visible
@returns @c NSTimeInterval to delay the repositioning
*/
- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset;
/// Called before the callout view appears on screen, or before the appearance animation will start.
- (void)calloutViewWillAppear:(SMCalloutView *)calloutView;
/// Called after the callout view appears on screen, or after the appearance animation is complete.
- (void)calloutViewDidAppear:(SMCalloutView *)calloutView;
/// Called before the callout view is removed from the screen, or before the disappearance animation is complete.
- (void)calloutViewWillDisappear:(SMCalloutView *)calloutView;
/// Called after the callout view is removed from the screen, or after the disappearance animation is complete.
- (void)calloutViewDidDisappear:(SMCalloutView *)calloutView;
NS_ASSUME_NONNULL_END
@end

859
Pods/SMCalloutView/SMCalloutView.m generated Executable file
View File

@ -0,0 +1,859 @@
#import "SMCalloutView.h"
//
// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable.
//
@interface UIView (SMFrameAdditions)
@property (nonatomic, assign) CGPoint frameOrigin;
@property (nonatomic, assign) CGSize frameSize;
@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties
@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect
@end
//
// Callout View.
//
#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow
#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present)
#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout
#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories
#define TITLE_TOP 11 // the top of the title view when no subtitle is present
#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present
#define TITLE_HEIGHT 21 // title height, fixed
#define SUBTITLE_TOP 28 // the top of the subtitle, when present
#define SUBTITLE_HEIGHT 15 // subtitle height, fixed
#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present
#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything.
#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect
NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0;
@interface SMCalloutView ()
@property (nonatomic, strong) UIButton *containerView; // for masking and interaction
@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel;
@property (nonatomic, assign) SMCalloutArrowDirection currentArrowDirection;
@property (nonatomic, assign) BOOL popupCancelled;
@end
@implementation SMCalloutView
+ (SMCalloutView *)platformCalloutView {
// if you haven't compiled SMClassicCalloutView into your app, then we can't possibly create an instance of it!
if (!NSClassFromString(@"SMClassicCalloutView"))
return [SMCalloutView new];
// ok we have both - so choose the best one based on current platform
if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1)
return [SMCalloutView new]; // iOS 7+
else
return [NSClassFromString(@"SMClassicCalloutView") new];
}
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.permittedArrowDirection = SMCalloutArrowDirectionDown;
self.presentAnimation = SMCalloutAnimationBounce;
self.dismissAnimation = SMCalloutAnimationFade;
self.backgroundColor = [UIColor clearColor];
self.containerView = [UIButton new];
self.containerView.isAccessibilityElement = NO;
self.isAccessibilityElement = NO;
self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12);
[self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside];
[self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside];
[self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (BOOL)supportsHighlighting {
if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)])
return NO;
if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)])
return [self.delegate calloutViewShouldHighlight:self];
return YES;
}
- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; }
- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; }
- (void)calloutClicked {
if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)])
[self.delegate calloutViewClicked:self];
}
- (UIView *)titleViewOrDefault {
if (self.titleView)
// if you have a custom title view defined, return that.
return self.titleView;
else {
if (!self.titleLabel) {
// create a default titleView
self.titleLabel = [UILabel new];
self.titleLabel.frameHeight = TITLE_HEIGHT;
self.titleLabel.opaque = NO;
self.titleLabel.backgroundColor = [UIColor clearColor];
self.titleLabel.font = [UIFont systemFontOfSize:17];
self.titleLabel.textColor = [UIColor blackColor];
}
return self.titleLabel;
}
}
- (UIView *)subtitleViewOrDefault {
if (self.subtitleView)
// if you have a custom subtitle view defined, return that.
return self.subtitleView;
else {
if (!self.subtitleLabel) {
// create a default subtitleView
self.subtitleLabel = [UILabel new];
self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT;
self.subtitleLabel.opaque = NO;
self.subtitleLabel.backgroundColor = [UIColor clearColor];
self.subtitleLabel.font = [UIFont systemFontOfSize:12];
self.subtitleLabel.textColor = [UIColor blackColor];
}
return self.subtitleLabel;
}
}
- (SMCalloutBackgroundView *)backgroundView {
// create our default background on first access only if it's nil, since you might have set your own background anyway.
return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]);
}
- (SMCalloutBackgroundView *)defaultBackgroundView {
return [SMCalloutMaskedBackgroundView new];
}
- (void)rebuildSubviews {
// remove and re-add our appropriate subviews in the appropriate order
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self setNeedsDisplay];
[self addSubview:self.backgroundView];
[self addSubview:self.containerView];
if (self.contentView) {
[self.containerView addSubview:self.contentView];
}
else {
if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault];
if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault];
}
if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView];
if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView];
}
// Accessory margins. Accessories are centered vertically when shorter
// than the callout, otherwise they grow from the upper corner.
- (CGFloat)leftAccessoryVerticalMargin {
if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight)
return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2);
else
return 0;
}
- (CGFloat)leftAccessoryHorizontalMargin {
return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN);
}
- (CGFloat)rightAccessoryVerticalMargin {
if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight)
return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2);
else
return 0;
}
- (CGFloat)rightAccessoryHorizontalMargin {
return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN);
}
- (CGFloat)innerContentMarginLeft {
if (self.leftAccessoryView)
return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN;
else
return self.contentViewInset.left;
}
- (CGFloat)innerContentMarginRight {
if (self.rightAccessoryView)
return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN;
else
return self.contentViewInset.right;
}
- (CGFloat)calloutHeight {
return self.calloutContainerHeight + self.backgroundView.anchorHeight;
}
- (CGFloat)calloutContainerHeight {
if (self.contentView)
return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top;
else if (self.subtitleView || self.subtitle.length > 0)
return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT;
else
return CALLOUT_DEFAULT_CONTAINER_HEIGHT;
}
- (CGSize)sizeThatFits:(CGSize)size {
// calculate how much non-negotiable space we need to reserve for margin and accessories
CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight;
// how much room is left for text?
CGFloat availableWidthForText = size.width - margin - 1;
// no room for text? then we'll have to squeeze into the given size somehow.
if (availableWidthForText < 0)
availableWidthForText = 0;
CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)];
CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)];
// total width we'd like
CGFloat preferredWidth;
if (self.contentView) {
// if we have a content view, then take our preferred size directly from that
preferredWidth = self.contentView.frameWidth + margin;
}
else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) {
// if we have a title or subtitle, then our assumed margins are valid, and we can apply them
preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin;
}
else {
// ok we have no title or subtitle to speak of. In this case, the system callout would actually not display
// at all! But we can handle it.
preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin;
if (self.leftAccessoryView && self.rightAccessoryView)
preferredWidth += BETWEEN_ACCESSORIES_MARGIN;
}
// ensure we're big enough to fit our graphics!
preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH);
// ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle.
return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight);
}
- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect {
CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect));
CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect));
CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect));
CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect));
return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom);
}
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated];
}
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated];
}
// this private method handles both CALayer and UIView parents depending on what's passed.
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
// Sanity check: dismiss this callout immediately if it's displayed somewhere
if (self.layer.superlayer) [self dismissCalloutAnimated:NO];
// cancel all animations that may be in progress
[self.layer removeAnimationForKey:@"present"];
[self.layer removeAnimationForKey:@"dismiss"];
// figure out the constrained view's rect in our popup view's coordinate system
CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer];
// apply our edge constraints
constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets);
constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN);
// form our subviews based on our content set so far
[self rebuildSubviews];
// apply title/subtitle (if present
self.titleLabel.text = self.title;
self.subtitleLabel.text = self.subtitle;
// size the callout to fit the width constraint as best as possible
self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)];
// how much room do we have in the constraint box, both above and below our target rect?
CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect);
CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect);
// we prefer to point our arrow down.
SMCalloutArrowDirection bestDirection = SMCalloutArrowDirectionDown;
// we'll point it up though if that's the only option you gave us.
if (self.permittedArrowDirection == SMCalloutArrowDirectionUp)
bestDirection = SMCalloutArrowDirectionUp;
// or, if we don't have enough space on the top and have more space on the bottom, and you
// gave us a choice, then pointing up is the better option.
if (self.permittedArrowDirection == SMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace)
bestDirection = SMCalloutArrowDirectionUp;
self.currentArrowDirection = bestDirection;
// we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our
// target view's coordinate system. make sure to offset the anchor point as requested if necessary.
CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect);
CGFloat anchorY = self.calloutOffset.y + (bestDirection == SMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect));
// we prefer to sit centered directly above our anchor
CGFloat calloutX = roundf(anchorX - self.frameWidth / 2);
// but not if it's going to get too close to the edge of our constraints
if (calloutX < constrainedRect.origin.x)
calloutX = constrainedRect.origin.x;
if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth)
calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth;
// what's the farthest to the left and right that we could point to, given our background image constraints?
CGFloat minPointX = calloutX + self.backgroundView.anchorMargin;
CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin;
// we may need to scoot over to the left or right to point at the correct spot
CGFloat adjustX = 0;
if (anchorX < minPointX) adjustX = anchorX - minPointX;
if (anchorX > maxPointX) adjustX = anchorX - maxPointX;
// add the callout to the given layer (or view if possible, to receive touch events)
if (view)
[view addSubview:self];
else
[layer addSublayer:self.layer];
CGPoint calloutOrigin = {
.x = calloutX + adjustX,
.y = bestDirection == SMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY
};
self.frameOrigin = calloutOrigin;
// now set the *actual* anchor point for our layer so that our "popup" animation starts from this point.
CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer];
// pass on the anchor point to our background view so it knows where to draw the arrow
self.backgroundView.arrowPoint = anchorPoint;
// adjust it to unit coordinates for the actual layer.anchorPoint property
anchorPoint.x /= self.frameWidth;
anchorPoint.y /= self.frameHeight;
self.layer.anchorPoint = anchorPoint;
// setting the anchor point moves the view a bit, so we need to reset
self.frameOrigin = calloutOrigin;
// make sure our frame is not on half-pixels or else we may be blurry!
CGFloat scale = [UIScreen mainScreen].scale;
self.frameX = floorf(self.frameX*scale)/scale;
self.frameY = floorf(self.frameY*scale)/scale;
// layout now so we can immediately start animating to the final position if needed
[self setNeedsLayout];
[self layoutIfNeeded];
// if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position.
// consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view.
CGRect contentRect = CGRectUnion(self.frame, rect);
CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect];
NSTimeInterval delay = 0;
self.popupCancelled = NO; // reset this before calling our delegate below
if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero))
delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset];
// there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that
// happened then we need to bail!
if (self.popupCancelled) return;
// now we want to mask our contents to our background view (if requested) to match the iOS 7 style
self.containerView.layer.mask = self.backgroundView.contentMask;
// if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup
self.hidden = YES;
// create the appropriate animation, even if we're not animated
CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES];
// nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks
if (!animated)
animation.duration = 0.0000001; // can't be zero or the animation won't "run"
animation.beginTime = CACurrentMediaTime() + delay;
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"present"];
}
- (void)animationDidStart:(CAAnimation *)anim {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting) {
if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)])
[_delegate calloutViewWillAppear:(id)self];
// ok, animation is on, let's make ourselves visible!
self.hidden = NO;
}
else if (!presenting) {
if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)])
[_delegate calloutViewWillDisappear:(id)self];
}
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting && finished) {
if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)])
[_delegate calloutViewDidAppear:(id)self];
}
else if (!presenting && finished) {
[self removeFromParent];
[self.layer removeAnimationForKey:@"dismiss"];
if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)])
[_delegate calloutViewDidDisappear:(id)self];
}
}
- (void)dismissCalloutAnimated:(BOOL)animated {
// cancel all animations that may be in progress
[self.layer removeAnimationForKey:@"present"];
[self.layer removeAnimationForKey:@"dismiss"];
self.popupCancelled = YES;
if (animated) {
CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO];
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"dismiss"];
}
else {
[self removeFromParent];
}
}
- (void)removeFromParent {
if (self.superview)
[self removeFromSuperview];
else {
// removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable.
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.layer removeFromSuperlayer];
[CATransaction commit];
}
}
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting {
CAAnimation *animation = nil;
if (type == SMCalloutAnimationBounce) {
CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.duration = 0.23;
fade.fromValue = presenting ? @0.0 : @1.0;
fade.toValue = presenting ? @1.0 : @0.0;
fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
bounce.duration = 0.23;
bounce.fromValue = presenting ? @0.7 : @1.0;
bounce.toValue = presenting ? @1.0 : @0.7;
bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814];
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[fade, bounce];
group.duration = 0.23;
animation = group;
}
else if (type == SMCalloutAnimationFade) {
CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"];
fade.duration = 1.0/3.0;
fade.fromValue = presenting ? @0.0 : @1.0;
fade.toValue = presenting ? @1.0 : @0.0;
animation = fade;
}
else if (type == SMCalloutAnimationStretch) {
CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
stretch.duration = 0.1;
stretch.fromValue = presenting ? @0.0 : @1.0;
stretch.toValue = presenting ? @1.0 : @0.0;
animation = stretch;
}
// CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods
[animation setValue:@(presenting) forKey:@"presenting"];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
return animation;
}
- (void)layoutSubviews {
self.containerView.frame = self.bounds;
self.backgroundView.frame = self.bounds;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = self.currentArrowDirection == SMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0;
self.titleViewOrDefault.frameX = self.innerContentMarginLeft;
self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy;
self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight;
self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX;
self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy;
self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth;
self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin;
self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy;
self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth;
self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy;
if (self.contentView) {
self.contentView.frameX = self.innerContentMarginLeft;
self.contentView.frameY = self.contentViewInset.top + dy;
}
}
#pragma mark - Accessibility
- (NSInteger)accessibilityElementCount {
return (!!self.leftAccessoryView + !!self.titleViewOrDefault +
!!self.subtitleViewOrDefault + !!self.rightAccessoryView);
}
- (id)accessibilityElementAtIndex:(NSInteger)index {
if (index == 0) {
return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault;
}
if (index == 1) {
return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault;
}
if (index == 2) {
return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView;
}
if (index == 3) {
return self.leftAccessoryView ? self.rightAccessoryView : nil;
}
return nil;
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == nil) return NSNotFound;
if (element == self.leftAccessoryView) return 0;
if (element == self.titleViewOrDefault) {
return self.leftAccessoryView ? 1 : 0;
}
if (element == self.subtitleViewOrDefault) {
return self.leftAccessoryView ? 2 : 1;
}
if (element == self.rightAccessoryView) {
return self.leftAccessoryView ? 3 : 2;
}
return NSNotFound;
}
@end
// import this known "private API" from SMCalloutBackgroundView
@interface SMCalloutBackgroundView (EmbeddedImages)
+ (UIImage *)embeddedImageNamed:(NSString *)name;
@end
//
// Callout Background View.
//
@interface SMCalloutMaskedBackgroundView ()
@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView;
@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView;
@end
static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil;
@implementation SMCalloutMaskedBackgroundView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// Here we're mimicking the very particular (and odd) structure of the system callout view.
// The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app
self.containerView = [UIView new];
self.containerView.backgroundColor = [UIColor whiteColor];
self.containerView.alpha = 0.96;
self.containerView.layer.cornerRadius = 8;
self.containerView.layer.shadowRadius = 30;
self.containerView.layer.shadowOpacity = 0.1;
self.containerBorderView = [UIView new];
self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor;
self.containerBorderView.layer.borderWidth = 0.5;
self.containerBorderView.layer.cornerRadius = 8.5;
if (!blackArrowImage) {
blackArrowImage = [SMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"];
whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]];
grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]];
}
self.anchorHeight = 13;
self.anchorMargin = 27;
self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)];
self.arrowView.alpha = 0.96;
self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage];
self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage];
self.arrowHighlightedImageView.hidden = YES;
self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage];
self.arrowBorderView.alpha = 0.1;
self.arrowBorderView.frameY = 0.5;
[self addSubview:self.containerView];
[self.containerView addSubview:self.containerBorderView];
[self addSubview:self.arrowView];
[self.arrowView addSubview:self.arrowBorderView];
[self.arrowView addSubview:self.arrowImageView];
[self.arrowView addSubview:self.arrowHighlightedImageView];
}
return self;
}
// Make sure we relayout our images when our arrow point changes!
- (void)setArrowPoint:(CGPoint)arrowPoint {
[super setArrowPoint:arrowPoint];
[self setNeedsLayout];
}
- (void)setHighlighted:(BOOL)highlighted {
[super setHighlighted:highlighted];
self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor];
self.arrowImageView.hidden = highlighted;
self.arrowHighlightedImageView.hidden = !highlighted;
}
- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color {
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
CGRect imageRect = (CGRect){.size=image.size};
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(c, 0, image.size.height);
CGContextScaleCTM(c, 1, -1);
CGContextClipToMask(c, imageRect, image.CGImage);
[color setFill];
CGContextFillRect(c, imageRect);
UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return whiteImage;
}
- (void)layoutSubviews {
BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0;
self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5);
self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5);
self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2);
if (pointingUp) {
self.arrowView.frameY = 1;
self.arrowView.transform = CGAffineTransformMakeRotation(M_PI);
}
else {
self.arrowView.frameY = self.containerView.frameHeight - 0.5;
self.arrowView.transform = CGAffineTransformIdentity;
}
}
- (CALayer *)contentMask {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CALayer *layer = [CALayer layer];
layer.frame = self.bounds;
layer.contents = (id)maskImage.CGImage;
return layer;
}
@end
@implementation SMCalloutBackgroundView
+ (NSData *)dataWithBase64EncodedString:(NSString *)string {
//
// NSData+Base64.m
//
// Version 1.0.2
//
// Created by Nick Lockwood on 12/01/2012.
// Copyright (C) 2012 Charcoal Design
//
// Distributed under the permissive zlib License
// Get the latest version from here:
//
// https://github.com/nicklockwood/Base64
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would be
// appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not be
// misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
const char lookup[] = {
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
};
NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
long long inputLength = [inputData length];
const unsigned char *inputBytes = [inputData bytes];
long long maxOutputLength = (inputLength / 4 + 1) * 3;
NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength];
unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
int accumulator = 0;
long long outputLength = 0;
unsigned char accumulated[] = {0, 0, 0, 0};
for (long long i = 0; i < inputLength; i++) {
unsigned char decoded = lookup[inputBytes[i] & 0x7F];
if (decoded != 99) {
accumulated[accumulator] = decoded;
if (accumulator == 3) {
outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
}
accumulator = (accumulator + 1) % 4;
}
}
//handle left-over data
if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
if (accumulator > 2) outputLength++;
//truncate data to match actual output length
outputData.length = (NSUInteger)outputLength;
return outputLength? outputData: nil;
}
+ (UIImage *)embeddedImageNamed:(NSString *)name {
CGFloat screenScale = [UIScreen mainScreen].scale;
if (screenScale > 1.0) {
name = [name stringByAppendingString:@"_2x"];
screenScale = 2.0;
}
SEL selector = NSSelectorFromString(name);
if (![(id)self respondsToSelector:selector]) {
NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name);
return nil;
}
// We need to hush the compiler here - but we know what we're doing!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
NSString *base64String = [(id)self performSelector:selector];
#pragma clang diagnostic pop
UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]];
return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp];
}
+ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; }
+ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; }
@end
//
// Our UIView frame helpers implementation
//
@implementation UIView (SMFrameAdditions)
- (CGPoint)frameOrigin { return self.frame.origin; }
- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; }
- (CGFloat)frameX { return self.frame.origin.x; }
- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; }
- (CGFloat)frameY { return self.frame.origin.y; }
- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; }
- (CGSize)frameSize { return self.frame.size; }
- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; }
- (CGFloat)frameWidth { return self.frame.size.width; }
- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; }
- (CGFloat)frameHeight { return self.frame.size.height; }
- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; }
- (CGFloat)frameLeft { return self.frame.origin.x; }
- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; }
- (CGFloat)frameTop { return self.frame.origin.y; }
- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; }
- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; }
- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; }
- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; }
- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; }
@end

View File

@ -0,0 +1,39 @@
#import <UIKit/UIKit.h>
#import "SMCalloutView.h"
/*
SMClassicCalloutView
--------------------
Created by Nick Farina (nfarina@gmail.com)
Version 1.1
*/
@protocol SMCalloutViewDelegate;
@class SMCalloutBackgroundView;
//
// Classic Callout view.
//
@interface SMClassicCalloutView : SMCalloutView
// One thing to note about the classic callout is that it will ignore the "constrainedInsets" property. That property is designed for iOS-7
// style presentation where your target view surface may be operlapped by navigation bars, tab bars, etc.
@end
//
// Classes responsible for drawing the background graphic with the pointy arrow.
//
// Draws a background composed of stretched prerendered images that you can customize. Uses the embedded iOS 6 graphics by default.
@interface SMCalloutImageBackgroundView : SMCalloutBackgroundView
@property (nonatomic, strong) UIImage *leftCapImage, *rightCapImage, *topAnchorImage, *bottomAnchorImage, *backgroundImage;
@end
// Draws a custom background matching the system background but can grow in height.
@interface SMCalloutDrawnBackgroundView : SMCalloutBackgroundView
@end

871
Pods/SMCalloutView/SMClassicCalloutView.m generated Normal file
View File

@ -0,0 +1,871 @@
#import "SMClassicCalloutView.h"
#import <QuartzCore/QuartzCore.h>
//
// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable.
//
@interface UIView (SMFrameAdditions)
@property (nonatomic, assign) CGPoint frameOrigin;
@property (nonatomic, assign) CGSize frameSize;
@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties
@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect
@end
//
// Callout View.
//
#define CALLOUT_DEFAULT_MIN_WIDTH 75 // our image-based background graphics limit us to this minimum width...
#define CALLOUT_DEFAULT_HEIGHT 70 // ...and allow only for this exact height.
#define CALLOUT_DEFAULT_WIDTH 153 // default "I give up" width when we are asked to present in a space less than our min width
#define TITLE_MARGIN 17 // the title view's normal horizontal margin from the edges of our callout view
#define TITLE_TOP 11 // the top of the title view when no subtitle is present
#define TITLE_SUB_TOP 3 // the top of the title view when a subtitle IS present
#define TITLE_HEIGHT 22 // title height, fixed
#define SUBTITLE_TOP 25 // the top of the subtitle, when present
#define SUBTITLE_HEIGHT 16 // subtitle height, fixed
#define TITLE_ACCESSORY_MARGIN 6 // the margin between the title and an accessory if one is present (on either side)
#define ACCESSORY_MARGIN 14 // the accessory's margin from the edges of our callout view
#define ACCESSORY_TOP 8 // the top of the accessory "area" in which accessory views are placed
#define ACCESSORY_HEIGHT 32 // the "suggested" maximum height of an accessory view. shorter accessories will be vertically centered
#define BETWEEN_ACCESSORIES_MARGIN 7 // if we have no title or subtitle, but have two accessory views, then this is the space between them
#define ANCHOR_MARGIN 39 // the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right
#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything.
#define BOTTOM_ANCHOR_MARGIN 10 // if using a bottom anchor, we'll need to account for the shadow below the "tip"
#define REPOSITION_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect
#define TOP_SHADOW_BUFFER 2 // height offset buffer to account for top shadow
#define BOTTOM_SHADOW_BUFFER 5 // height offset buffer to account for bottom shadow
#define OFFSET_FROM_ORIGIN 5 // distance to offset vertically from the rect origin of the callout
#define ANCHOR_HEIGHT 14 // height to use for the anchor
#define ANCHOR_MARGIN_MIN 24 // the smallest possible distance from the edge of our control to the edge of the anchor, from either left or right
@interface SMCalloutView (PrivateMethods)
@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel;
@property (nonatomic, assign) SMCalloutArrowDirection currentArrowDirection;
@property (nonatomic, assign) BOOL popupCancelled;
//@property (nonatomic, strong) UIImageView *leftCap, *rightCap, *topAnchor, *bottomAnchor, *leftBackground, *rightBackground;
@end
@interface SMClassicCalloutView ()
@end
@implementation SMClassicCalloutView
- (UIView *)titleViewOrDefault {
if (self.titleView)
// if you have a custom title view defined, return that.
return self.titleView;
else {
if (!self.titleLabel) {
// create a default titleView
self.titleLabel = [UILabel new];
self.titleLabel.frameHeight = TITLE_HEIGHT;
self.titleLabel.opaque = NO;
self.titleLabel.backgroundColor = [UIColor clearColor];
self.titleLabel.font = [UIFont boldSystemFontOfSize:17];
self.titleLabel.textColor = [UIColor whiteColor];
self.titleLabel.shadowColor = [UIColor colorWithWhite:0 alpha:0.5];
self.titleLabel.shadowOffset = CGSizeMake(0, -1);
}
return self.titleLabel;
}
}
- (UIView *)subtitleViewOrDefault {
if (self.subtitleView)
// if you have a custom subtitle view defined, return that.
return self.subtitleView;
else {
if (!self.subtitleLabel) {
// create a default subtitleView
self.subtitleLabel = [UILabel new];
self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT;
self.subtitleLabel.opaque = NO;
self.subtitleLabel.backgroundColor = [UIColor clearColor];
self.subtitleLabel.font = [UIFont systemFontOfSize:12];
self.subtitleLabel.textColor = [UIColor whiteColor];
self.subtitleLabel.shadowColor = [UIColor colorWithWhite:0 alpha:0.5];
self.subtitleLabel.shadowOffset = CGSizeMake(0, -1);
}
return self.subtitleLabel;
}
}
- (SMCalloutBackgroundView *)defaultBackgroundView {
return [SMCalloutDrawnBackgroundView new];
}
- (void)rebuildSubviews {
// remove and re-add our appropriate subviews in the appropriate order
[self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[self setNeedsDisplay];
[self addSubview:self.backgroundView];
if (self.contentView) {
[self addSubview:self.contentView];
}
else {
if (self.titleViewOrDefault) [self addSubview:self.titleViewOrDefault];
if (self.subtitleViewOrDefault) [self addSubview:self.subtitleViewOrDefault];
}
if (self.leftAccessoryView) [self addSubview:self.leftAccessoryView];
if (self.rightAccessoryView) [self addSubview:self.rightAccessoryView];
}
- (CGFloat)innerContentMarginLeft {
if (self.leftAccessoryView)
return ACCESSORY_MARGIN + self.leftAccessoryView.frameWidth + TITLE_ACCESSORY_MARGIN;
else
return TITLE_MARGIN;
}
- (CGFloat)innerContentMarginRight {
if (self.rightAccessoryView)
return ACCESSORY_MARGIN + self.rightAccessoryView.frameWidth + TITLE_ACCESSORY_MARGIN;
else
return TITLE_MARGIN;
}
- (CGFloat)calloutHeight {
if (self.contentView)
return self.contentView.frameHeight + TITLE_TOP*2 + ANCHOR_HEIGHT + BOTTOM_ANCHOR_MARGIN;
else
return CALLOUT_DEFAULT_HEIGHT;
}
- (CGSize)sizeThatFits:(CGSize)size {
// odd behavior, but mimicking the system callout view
if (size.width < CALLOUT_DEFAULT_MIN_WIDTH)
return CGSizeMake(CALLOUT_DEFAULT_WIDTH, self.calloutHeight);
// calculate how much non-negotiable space we need to reserve for margin and accessories
CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight;
// how much room is left for text?
CGFloat availableWidthForText = size.width - margin;
// no room for text? then we'll have to squeeze into the given size somehow.
if (availableWidthForText < 0)
availableWidthForText = 0;
CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)];
CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)];
// total width we'd like
CGFloat preferredWidth;
if (self.contentView) {
// if we have a content view, then take our preferred size directly from that
preferredWidth = self.contentView.frameWidth + margin;
}
else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) {
// if we have a title or subtitle, then our assumed margins are valid, and we can apply them
preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin;
}
else {
// ok we have no title or subtitle to speak of. In this case, the system callout would actually not display
// at all! But we can handle it.
preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + ACCESSORY_MARGIN*2;
if (self.leftAccessoryView && self.rightAccessoryView)
preferredWidth += BETWEEN_ACCESSORIES_MARGIN;
}
// ensure we're big enough to fit our graphics!
preferredWidth = fmaxf(preferredWidth, CALLOUT_DEFAULT_MIN_WIDTH);
// ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle.
return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight);
}
- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect {
CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect));
CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect));
CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect));
CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect));
return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom);
}
- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated];
}
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
[self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated];
}
// this private method handles both CALayer and UIView parents depending on what's passed.
- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated {
// Sanity check: dismiss this callout immediately if it's displayed somewhere
if (self.layer.superlayer) [self dismissCalloutAnimated:NO];
// figure out the constrained view's rect in our popup view's coordinate system
CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer];
// apply our edge constraints
constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets);
// form our subviews based on our content set so far
[self rebuildSubviews];
// apply title/subtitle (if present
self.titleLabel.text = self.title;
self.subtitleLabel.text = self.subtitle;
// size the callout to fit the width constraint as best as possible
self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)];
// how much room do we have in the constraint box, both above and below our target rect?
CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect);
CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect);
// we prefer to point our arrow down.
SMCalloutArrowDirection bestDirection = SMCalloutArrowDirectionDown;
// we'll point it up though if that's the only option you gave us.
if (self.permittedArrowDirection == SMCalloutArrowDirectionUp)
bestDirection = SMCalloutArrowDirectionUp;
// or, if we don't have enough space on the top and have more space on the bottom, and you
// gave us a choice, then pointing up is the better option.
if (self.permittedArrowDirection == SMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace)
bestDirection = SMCalloutArrowDirectionUp;
// we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our
// target view's coordinate system. make sure to offset the anchor point as requested if necessary.
CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect);
CGFloat anchorY = self.calloutOffset.y + (bestDirection == SMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect));
// we prefer to sit in the exact center of our constrained view, so we have visually pleasing equal left/right margins.
CGFloat calloutX = roundf(CGRectGetMidX(constrainedRect) - self.frameWidth / 2);
// what's the farthest to the left and right that we could point to, given our background image constraints?
CGFloat minPointX = calloutX + ANCHOR_MARGIN;
CGFloat maxPointX = calloutX + self.frameWidth - ANCHOR_MARGIN;
// we may need to scoot over to the left or right to point at the correct spot
CGFloat adjustX = 0;
if (anchorX < minPointX) adjustX = anchorX - minPointX;
if (anchorX > maxPointX) adjustX = anchorX - maxPointX;
// add the callout to the given layer (or view if possible, to receive touch events)
if (view)
[view addSubview:self];
else
[layer addSublayer:self.layer];
CGPoint calloutOrigin = {
.x = calloutX + adjustX,
.y = bestDirection == SMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight + BOTTOM_ANCHOR_MARGIN) : anchorY
};
self.currentArrowDirection = bestDirection;
self.frameOrigin = calloutOrigin;
// now set the *actual* anchor point for our layer so that our "popup" animation starts from this point.
CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer];
// pass on the anchor point to our background view so it knows where to draw the arrow
self.backgroundView.arrowPoint = anchorPoint;
// adjust it to unit coordinates for the actual layer.anchorPoint property
anchorPoint.x /= self.frameWidth;
anchorPoint.y /= self.frameHeight;
self.layer.anchorPoint = anchorPoint;
// setting the anchor point moves the view a bit, so we need to reset
self.frameOrigin = calloutOrigin;
// make sure our frame is not on half-pixels or else we may be blurry!
self.frame = CGRectIntegral(self.frame);
// layout now so we can immediately start animating to the final position if needed
[self setNeedsLayout];
[self layoutIfNeeded];
// if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position.
// consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view.
CGRect contentRect = CGRectUnion(self.frame, CGRectInset(rect, -REPOSITION_MARGIN, -REPOSITION_MARGIN));
CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect];
NSTimeInterval delay = 0;
self.popupCancelled = NO; // reset this before calling our delegate below
if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero))
delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset];
// there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that
// happened then we need to bail!
if (self.popupCancelled) return;
// if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup
self.hidden = YES;
// create the appropriate animation, even if we're not animated
CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES];
// nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks
if (!animated)
animation.duration = 0.0000001; // can't be zero or the animation won't "run"
animation.beginTime = CACurrentMediaTime() + delay;
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"present"];
}
- (void)animationDidStart:(CAAnimation *)anim {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting) {
if ([self.delegate respondsToSelector:@selector(calloutViewWillAppear:)])
[self.delegate calloutViewWillAppear:(id)self];
// ok, animation is on, let's make ourselves visible!
self.hidden = NO;
}
else if (!presenting) {
if ([self.delegate respondsToSelector:@selector(calloutViewWillDisappear:)])
[self.delegate calloutViewWillDisappear:(id)self];
}
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished {
BOOL presenting = [[anim valueForKey:@"presenting"] boolValue];
if (presenting) {
if ([self.delegate respondsToSelector:@selector(calloutViewDidAppear:)])
[self.delegate calloutViewDidAppear:(id)self];
}
else if (!presenting) {
[self removeFromParent];
[self.layer removeAnimationForKey:@"dismiss"];
if ([self.delegate respondsToSelector:@selector(calloutViewDidDisappear:)])
[self.delegate calloutViewDidDisappear:(id)self];
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// we want to match the system callout view, which doesn't "capture" touches outside the accessory areas. This way you can click on other pins and things *behind* a translucent callout.
return
[self.leftAccessoryView pointInside:[self.leftAccessoryView convertPoint:point fromView:self] withEvent:nil] ||
[self.rightAccessoryView pointInside:[self.rightAccessoryView convertPoint:point fromView:self] withEvent:nil] ||
[self.contentView pointInside:[self.contentView convertPoint:point fromView:self] withEvent:nil] ||
(!self.contentView && [self.titleView pointInside:[self.titleView convertPoint:point fromView:self] withEvent:nil]) ||
(!self.contentView && [self.subtitleView pointInside:[self.subtitleView convertPoint:point fromView:self] withEvent:nil]);
}
- (void)dismissCalloutAnimated:(BOOL)animated {
[self.layer removeAnimationForKey:@"present"];
self.popupCancelled = YES;
if (animated) {
CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO];
animation.delegate = self;
[self.layer addAnimation:animation forKey:@"dismiss"];
}
else [self removeFromParent];
}
- (void)removeFromParent {
if (self.superview)
[self removeFromSuperview];
else {
// removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable.
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.layer removeFromSuperlayer];
[CATransaction commit];
}
}
- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting {
CAAnimation *animation = nil;
if (type == SMCalloutAnimationBounce) {
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
CAMediaTimingFunction *easeInOut = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
bounceAnimation.values = @[@0.05, @1.11245, @0.951807, @1.0];
bounceAnimation.keyTimes = @[@0, @(4.0/9.0), @(4.0/9.0+5.0/18.0), @1.0];
bounceAnimation.duration = 1.0/3.0; // the official bounce animation duration adds up to 0.3 seconds; but there is a bit of delay introduced by Apple using a sequence of callback-based CABasicAnimations rather than a single CAKeyframeAnimation. So we bump it up to 0.33333 to make it feel identical on the device
bounceAnimation.timingFunctions = @[easeInOut, easeInOut, easeInOut, easeInOut];
if (!presenting)
bounceAnimation.values = [[bounceAnimation.values reverseObjectEnumerator] allObjects]; // reverse values
animation = bounceAnimation;
}
else if (type == SMCalloutAnimationFade) {
CABasicAnimation *fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeAnimation.duration = 1.0/3.0;
fadeAnimation.fromValue = presenting ? @0.0 : @1.0;
fadeAnimation.toValue = presenting ? @1.0 : @0.0;
animation = fadeAnimation;
}
else if (type == SMCalloutAnimationStretch) {
CABasicAnimation *stretchAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
stretchAnimation.duration = 0.1;
stretchAnimation.fromValue = presenting ? @0.0 : @1.0;
stretchAnimation.toValue = presenting ? @1.0 : @0.0;
animation = stretchAnimation;
}
// CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods
[animation setValue:@(presenting) forKey:@"presenting"];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
return animation;
}
- (CGFloat)centeredPositionOfView:(UIView *)view ifSmallerThan:(CGFloat)height {
return view.frameHeight < height ? floorf(height/2 - view.frameHeight/2) : 0;
}
- (CGFloat)centeredPositionOfView:(UIView *)view relativeToView:(UIView *)parentView {
return roundf((parentView.frameHeight - view.frameHeight) / 2);
}
- (void)layoutSubviews {
self.backgroundView.frame = self.bounds;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = self.currentArrowDirection == SMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0;
self.titleViewOrDefault.frameX = self.innerContentMarginLeft;
self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy;
self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight;
self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX;
self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy;
self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth;
self.leftAccessoryView.frameX = ACCESSORY_MARGIN;
if (self.contentView)
self.leftAccessoryView.frameY = TITLE_TOP + [self centeredPositionOfView:self.leftAccessoryView relativeToView:self.contentView] + dy;
else
self.leftAccessoryView.frameY = ACCESSORY_TOP + [self centeredPositionOfView:self.leftAccessoryView ifSmallerThan:ACCESSORY_HEIGHT] + dy;
self.rightAccessoryView.frameX = self.frameWidth-ACCESSORY_MARGIN-self.rightAccessoryView.frameWidth;
if (self.contentView)
self.rightAccessoryView.frameY = TITLE_TOP + [self centeredPositionOfView:self.rightAccessoryView relativeToView:self.contentView] + dy;
else
self.rightAccessoryView.frameY = ACCESSORY_TOP + [self centeredPositionOfView:self.rightAccessoryView ifSmallerThan:ACCESSORY_HEIGHT] + dy;
if (self.contentView) {
self.contentView.frameX = self.innerContentMarginLeft;
self.contentView.frameY = TITLE_TOP + dy;
}
}
@end
// import this known "private API" from SMCalloutView.m
@interface SMCalloutBackgroundView (EmbeddedImages)
+ (UIImage *)embeddedImageNamed:(NSString *)name;
@end
//
// Callout background assembled from predrawn stretched images.
//
@implementation SMCalloutImageBackgroundView {
UIImageView *leftCap, *rightCap, *topAnchor, *bottomAnchor, *leftBackground, *rightBackground;
}
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
leftCap = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 17, 57)];
rightCap = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 17, 57)];
topAnchor = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 41, 70)];
bottomAnchor = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 41, 70)];
leftBackground = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 1, 57)];
rightBackground = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 1, 57)];
[self addSubview:leftCap];
[self addSubview:rightCap];
[self addSubview:topAnchor];
[self addSubview:bottomAnchor];
[self addSubview:leftBackground];
[self addSubview:rightBackground];
}
return self;
}
- (UIImage *)leftCapImage { return _leftCapImage ? _leftCapImage : [[SMCalloutBackgroundView embeddedImageNamed:@"SMCalloutViewLeftCap"] stretchableImageWithLeftCapWidth:16 topCapHeight:20]; }
- (UIImage *)rightCapImage { return _rightCapImage ? _rightCapImage : [[SMCalloutBackgroundView embeddedImageNamed:@"SMCalloutViewRightCap"] stretchableImageWithLeftCapWidth:0 topCapHeight:20]; }
- (UIImage *)topAnchorImage { return _topAnchorImage ? _topAnchorImage : [[SMCalloutBackgroundView embeddedImageNamed:@"SMCalloutViewTopAnchor"] stretchableImageWithLeftCapWidth:0 topCapHeight:33]; }
- (UIImage *)bottomAnchorImage { return _bottomAnchorImage ? _bottomAnchorImage : [[SMCalloutBackgroundView embeddedImageNamed:@"SMCalloutViewBottomAnchor"] stretchableImageWithLeftCapWidth:0 topCapHeight:20]; }
- (UIImage *)backgroundImage { return _backgroundImage ? _backgroundImage : [[SMCalloutBackgroundView embeddedImageNamed:@"SMCalloutViewBackground"] stretchableImageWithLeftCapWidth:0 topCapHeight:20]; }
// Make sure we relayout our images when our arrow point changes!
- (void)setArrowPoint:(CGPoint)arrowPoint {
[super setArrowPoint:arrowPoint];
[self setNeedsLayout];
}
- (void)layoutSubviews {
// apply our background graphics
leftCap.image = self.leftCapImage;
rightCap.image = self.rightCapImage;
topAnchor.image = self.topAnchorImage;
bottomAnchor.image = self.bottomAnchorImage;
leftBackground.image = self.backgroundImage;
rightBackground.image = self.backgroundImage;
// stretch the images to fill our vertical space. The system background images aren't really stretchable,
// but that's OK because you'll probably be using title/subtitle rather than contentView if you're using the
// system images, and in that case the height will match the system background heights exactly and no stretching
// will occur. However, if you wish to define your own custom background using prerendered images, you could
// define stretchable images using -stretchableImageWithLeftCapWidth:TopCapHeight and they'd get stretched
// properly here if necessary.
leftCap.frameHeight = rightCap.frameHeight = leftBackground.frameHeight = rightBackground.frameHeight = self.frameHeight - 13;
topAnchor.frameHeight = bottomAnchor.frameHeight = self.frameHeight;
BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
// show the correct anchor based on our direction
topAnchor.hidden = !pointingUp;
bottomAnchor.hidden = pointingUp;
// if we're pointing up, we'll need to push almost everything down a bit
CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0;
leftCap.frameY = rightCap.frameY = leftBackground.frameY = rightBackground.frameY = dy;
leftCap.frameX = 0;
rightCap.frameX = self.frameWidth - rightCap.frameWidth;
// move both anchors, only one will have been made visible in our -popup method
CGFloat anchorX = roundf(self.arrowPoint.x - bottomAnchor.frameWidth / 2);
topAnchor.frameOrigin = CGPointMake(anchorX, 0);
// make sure the anchor graphic isn't overlapping with an endcap
if (topAnchor.frameLeft < leftCap.frameRight) topAnchor.frameX = leftCap.frameRight;
if (topAnchor.frameRight > rightCap.frameLeft) topAnchor.frameX = rightCap.frameLeft - topAnchor.frameWidth; // don't stretch it
bottomAnchor.frameOrigin = topAnchor.frameOrigin; // match
leftBackground.frameLeft = leftCap.frameRight;
leftBackground.frameRight = topAnchor.frameLeft;
rightBackground.frameLeft = topAnchor.frameRight;
rightBackground.frameRight = rightCap.frameLeft;
}
@end
@implementation SMCalloutBackgroundView (ClassicEmbeddedImages)
//
// I didn't want this class to require adding any images to your Xcode project. So instead the images needed are embedded below.
//
+ (NSString *)SMCalloutViewBackground { return @"iVBORw0KGgoAAAANSUhEUgAAAAEAAAA5CAYAAAD3PEFJAAAAHGlET1QAAAACAAAAAAAAAB0AAAAoAAAAHQAAABwAAACkF8Y1LgAAAHBJREFUGBlMy6ENQyEARdGXJh2CBEZAgAABhoQgEASDQBCWYqy3wt/lt80XFfe4CwCvT3gjpXThnEPsvYm1FjHnJMYYRO+daK0RtVailELknPndiBjjQwiB8N4TzrkHay3xwxhDaK0JpRQhpfxHCMEbAAD//2TbkE8AAAA9SURBVGOQlJQ8w0AJIScnd4ZBQ0PjDIOysvJdBiCIARFhIMIPRLiACEsQoQMiFEGEBIgQABFcIIIVRDABAL6ePTfutWY6AAAAAElFTkSuQmCC"; }
+ (NSString *)SMCalloutViewBackground_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAAIAAAByCAYAAAB5lADlAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAADkAAAAoAAAAOQAAADkAAAD3bjNTjgAAAMNJREFUOBGkz72JhGAAhOGpwyZUVESFVfAPFFRcFfypwmJsa4KNtxaPucAvuuiCZ3jTwX3fkN/5I7Iso2Acx4/guq6v4DzPj+A4Dgr2faeY2LaNgifWdaVgWRYK5nmmmJimiYL3+03BMAwU9H1PMdF1HQVt21LQNA3FRF3XFFRVRUFZlhQURUFBnucUPJfTNKXgidfrRUGSJBTEcUwxEUURBWEYUhAEAcWE7/sUeJ5HMeG6LgWO41Bg2zbFhGVZFPwvfgAAAP//7zgpOQAAAGxJREFU5cYhCkJBAEXRqyAYLJOECTLFaDXafjNZDIKgLniWNL73eXxwDYZzudRau/HP01rrxjKllG4sA3yCt8Z4BU+N8QjuGuMWXDXGFFw0xjk4aYxjcNAY+6BojF2w1RibYK0xVrMxBjbnZ75mdyKsdyBPxAAAAABJRU5ErkJggg=="; }
+ (NSString *)SMCalloutViewBottomAnchor { return @"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"; }
+ (NSString *)SMCalloutViewBottomAnchor_2x { return @"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"; }
+ (NSString *)SMCalloutViewLeftCap { return @"iVBORw0KGgoAAAANSUhEUgAAABEAAAA5CAYAAADQksChAAAAHGlET1QAAAACAAAAAAAAAB0AAAAoAAAAHQAAABwAAAKS6krCNQAAAl5JREFUSA2klNmKGkEUhs0yM9nIglnctQc3zKioqOCKwQVXFBRUcLnxwvtci88gPkpeIlATEshVSM2rmP9UUo3dajuQi49TVX3OZ3mquk273c50DpPJ9OAID7H2l2MCXYFMfoT1xzouML842MU/gabQ4XAUo9Ho10KhcFcqlbgejWRPIH/10ul0fq7X67/X6/X3zWZzu91umR5VohPQNq/MZvOndrv9a7VafVssFmw+n7PZbKYi50JyTIC1Zz6f78tyufwxnU7ZZDI5iV5CjbsiAXiZy+V+jsfjW8BGo9FJ9iXUh0vwlATAnEwm74bDIRsMBoZICZ2G3MULjN8ASzgc5v1+n3W73QN6vR4j6BlyxUUiCTVT7uIdxo5QKMQpqdPpaGi1WmIuI3KFRP6V55iLXSAqgUCAUyKOmDWbTdZoNESUY5oT+xJqKP0VM7ADL06HU0GtVlOpVqtMIteRK3ZC/XgCqKFvgRMEvF4vp4JyuWwIcg8k77HmBiFFUXilUmG45oaQRJ4MNfUV+AA84KPH46H3hBWLRZbP50WUY5pLkKuRvMbcAhRwQxIqwqUzBLlCQsdLt1QjcblcnASZTEZDNptV5zQ+JbnGgxuSUFI6nTbESBLGZ4DTLlKplGBfJtconpVQId4hVUTj/flZCb5oPBaLsXg8LkgkEuwYhjux2+0cF06VkExKZaS1sxJcOOb3+0UxFR7DUGK1WjlJcEqMYjAYZPg8sEgkouGsxO12M5wSQ380kdYk95KQwIj/kthsNiE3lFgsFk79oGRZIMcy0g7vJUGDVZEslpGe/QEAAP//KqClBgAAAfBJREFU7ZTRatNQHIfjdFaj1El1prW2Sbti2aZeKEMFB+pAEFREFAa79d5n8YF8gkzUi93M7VXq9x08I82y7QH04iNpOP34/37nJEmSJAuwCCksQQYjuJdl2cFgMCh7vV6g2+2WVXzub9aeLan+sen+v2S+3H+12Hioms7IWZ2ss+AwntiTBKdJCk6hkr08z3f7/f7cca8L44m9wM1liO9OkHQ6nW/j8fjncDgsFZ0Uqy65xoNbkMNamqafEexPJpPvo9GoNFoTrE3OgZNcgjYswxBWYaPdbn9lmt/T6fQH7EJZh3XHJDd4dgfuwkPYbLVaX4jyqyiKQ4QHdVgTJOe5tuAqdOA2rMADeAov4DW8h0+wDTsVjiQXeXgFroMfJstdg0fwDLZA0Tv4AB9BoQRJ/Lq5Q/ZyE/owgfuwAYpewitQ9gbeBmazGdfwdbPcGClOY8F2o8iJjLYJz0Gh020lfyXukL0YKU5jN11Q5ERGsyPLdrLH8CRQkRgpTpNybyxF9mM0O1oBJ1sFpeuBmsRp/PIbK4qMZkfK3DW33+lyUFyEOKeIjOa2O5UyJ1O4DJ5sxdmRpEFkNDvyJCtz+6NQqa+I79rSnKQmsiPjKTOiQmMqjWLl6TFJReSOibKqUGlE+WKjRFEVFkZh9RrlC38A3S8C8jPZQY0AAAAASUVORK5CYII="; }
+ (NSString *)SMCalloutViewLeftCap_2x { return @"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"; }
+ (NSString *)SMCalloutViewRightCap { return @"iVBORw0KGgoAAAANSUhEUgAAABEAAAA5CAYAAADQksChAAAAHGlET1QAAAACAAAAAAAAAB0AAAAoAAAAHQAAABwAAAKdevXfpAAAAmlJREFUSA2UlNlqWlEUhredJzpgB41zcAi2JiRiBE0UiwNxQsGACibeeJH7XovPEPIofYnCSmmhV6XJq6T/f3SfHg/xmF58rH22rm+vvfY+Ryml7llwYbzEzc2NWgdy1MMFDxCt3MezXsAU3yZUpVLpyk6hULje2dn55vf7iwvxktAuUhcXF2Ln/Pz8cjab/Tg6OvoTCAS+QPTIJnNZRer09FTG47Ewavg8mUxkOp1+b7Vav91u92dIHgNuXVdlitRoNJJVnJycyNnZ2c9YLPYVyc9WidRgMJBVDIdDAZcHBwe/IHhpEfEA2HSjGnV8fCxO9Pt9yWQy10hwA4qeAvaI25pLOp2OdLtdA47t9Ho9SaVSV0jwgDfgBWB/jGrYYNVut6XZbIo1cqyhNJlMUuIH74Cuhk02tqTq9bpoGo2GED4z4oiNBRKJBCURoKt5jrG5JVWr1YRUq1UTPcdIGU6HkijwAfZGb8noiyqXy+IE5dFolJIECIC3gFt6AtgXF6+9OFGpVCQSiVCSBCHwHixLDg8PxUqxWBTCOUYuEA6HKfkIwuADeAV41PP7goskTlC0kHxCkm7u6yVJPp+XXC5nYB3rOS4QDAZZiV3C12B+zNlsVpyg2CLZRCKPmZX8k+zv74vGKtNzrAifA1aSAusleEf4nphSjileK0mn03Ibe3t7QnZ3dwVfOOdKrH/WSXqOERdNfD6fs4QrrSIej/OirZdsb2+LFbz2srW1ZSTjVIzo9XqdK0HT2DgT7N8Y6xgKhWSthH924s6SjY2NlaL/klCkYWV6zL54PB7nnmC/ZoJO1JG/3UXyFwAA//9HuzCQAAAB80lEQVTtlc1qU1EURk9iNXqVqERrEmNykzQY/B0oooKCPyAIbRGpIDh17rP4QD7BragDJ9q+SlwrZsd7NRQFhx0szulhf9/Z+7uHJnU6naLb7RauZTyTfr9ftNvtvZTSdRhBG85ABkehnsrCVftDk2q4ZnSYSTWT//LYwoSA93mZ1+DfX2yv1yvyPN/F5OvCZMj6d8/eDjQYDAbFeDz+3Gq1PqwwOcHZGtST72AVo9GomEwmHzH6lmXZW4qvQg4X4DT8MplOp8UKdjn7RBffm83mewR34AoMYB2acBzspJYo3Pud4XC4z0hfGo3GO4oewi24DJfgHFRNOHhT4jX7V/ACnsNjuA83YQMuQgtOQQOOQA3mIoU78BK2QYOn8ABug3nElznL/iQcg6XJFn/IJih+Bk9AA7O4ARPowXlwFEP9+V9t0Yk3isJHYAaOYAcamIWB+j7sIkZZY1+fzWYsKd1bcJfVmw3RDBzBDjTogFlEF8tRwsTnLIr8jN68AWbgCHYQBhl7A513wVoLE4slB2/1M/oVFJuBI9hBGJiFgdZhaWKx+BLXQaE3h9gMDNIO/jCITvwNEZ+yN4pCP6NiX6YZOEKlAw3CxDYtDoEib1XozWXxcoQwCBMLo1hBELfOhZzXpCyOPefzgCysFCuQKDxo/QHd3ALyX+9lLwAAAABJRU5ErkJggg=="; }
+ (NSString *)SMCalloutViewRightCap_2x { return @"iVBORw0KGgoAAAANSUhEUgAAACIAAAByCAYAAAA2yQM1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAADkAAAAoAAAAOQAAADkAAARyl43hJQAABD5JREFUaAXsmFlLa1cUx3eH27m9rbXaem1R6lCHOqDiPOGEA84DjqCoKIIPivokqC/6LijavBX6IfoRCmUZE25vB9v7Tez/f3DJIU002TvF2+LDj5UEcvYva629zzox19fX5lXglZBgIh5FItvBtLS0SCxaW1t/Bj+As+bm5vna2tpnkRdI1nszPDwc8jM0NBTq7++/xMISDUgHmpqaypMloNcxJycnV8rp6emVcnZ2dnV+fv5nIBD4C7w8PDy86ujouJWDzE5FRcUTvZBrNBsbG6FobG5uhra2tsI7Ozvh3d3d5xD59fj4+I/9/f3fNVOQCZSVlX1sjHnNWWRmZkaiMTc3J2R+fv5icXExuLa2dgm58N7e3i9HR0e/tbe3a3a+z8zMfJcyLkJmenpa4oGylFpdXQ1ub2+HDw4OXrBUyIo0NjbuQuJ1lbERiluEspRBli6Wl5eDKGcYQs8pQqqqqopuZG6FEimXmZqakkSZnZ29WFpa8mR6e3uloaGBnEPkDVsZKxGKMzMrKyvB9fX1EM4ZT+YmK2/6hOLuGzM5OSk2UIY9A5nLnp4eqa+vl7q6unVIPAEJy5jx8XGxBT1zsbCwEBwZGaGE1NTUfAeJt30y7BevZ+7rFzM2Nia28AewRIwUAT9iYW5lldGeubdETiL8ASwRS4v7EPkJEh/EkrkrK2Z0dFRc0P66ERFIPAXvg3fAW4D9cm+JDOvrwsTEhJDq6moPLJoCPgLvgX+UKFZWzODgoLig/YWty0ONGfkM8P6jJfJnJeY9yQwMDIgLUUQ+h8SnIKGs/BsizyCRBj4B/qzc7qBo5TEYgsQFbXRfab6CQLSsaNNGLY/hvcIFTHRCMCR5QCIbZADtFe4gf9NGF+Hx7II2uk8kBwtngsjy8Oj3yhO1NC4S/K42enl5uRAslge0PNzKHwKettw9sUW6urrEBe0vjIxCsNg3IAt8Afy7526Rzs5OcaGvr09IaWmpBxYvANonqXjNk1YPt5gNazjuuaCl9YkUYuGvAbdxZMPGFuEQ7IKKlJSUCMHiHBkpog3LU1Z3TmyRtrY2caG7u1tIcXGxBxYtBv6do8c9b4LcOVHnkzsfOWM9ivo/1/4qKioS4hP5Eq/TgZ6wd4vow5Jt1P4qLCwUgoW/BczIf1REn0tsozZ6REZykREeavGXhtO3C3yUIAUFBR5YnKVJXORm6NXhN+GojZufny/kwUS0yfPy8oRYi+jQaxv9Irm5ufYiOvTaRm1ySmRlZdmL6GRlG7XRc3JyBP+TPJyINjtF0tLS7EUqKyvFBRVhWVJTU+1FdMSzjdrk6enpDy/CWYTZcMqIzpouUcviJKKzpk3kAZadne1tW+eM6IiXSOTcwSxkZGR4MSkZiVeAizMDKkCJpIroBW0jxZKSEVsB/d6jiGZC42NGNBMaNSMpKSlCrAcjvaBt/N+J/A0AAP//+DyM3gAABEZJREFU7ZnLblNXGIVNXAdaYmIbG+M4JAZKgd6g3IQo94u4tiqUXmgLpe0LdMQb8AqoQ2YMmCC1YsCEB2DgR0q/z+JHm01ixwQIgxPpk30S+6x11r/29iWlmZmZ/kro9Xp9aTQaA0ql0hewA+agDXWYgnVQgQlYs7CwUEoprcSEzy2M5AkWiRSJ5Ankx0VHikTyBPLjoiNFInkC+XHRkSKRPIH8+L3qyPz8fL9er/ue9RnvR1fvPWsYqdVqT1fFSLfbHbx5np2dHSRCKg9Wxcjc3NzASKfTGRipVqt3V8VIFLXVag2MTE5Ofv/OjdgLjUQa09PTDzFx6LmRj7ndAm/3A1aYiJK6Ykjj13dqJDURI6Eb/2DiGByEz+HtJOLqiGI6Ds2ECZbsvwhfhNzILL/bBDUY/dk3CrfcWzvhB27HgYlH5XL5B4TOwlE4AJ/BdkiNrOd4LXwAi38IH2XAJNwn2u12v9lsxn7RZxz3Oamr5BKcga9hP3wKGulCC0xktBGvbBxYHY8p5h1Ofh2+BcdyGo7APtgNW2EGmjANH8HwREaZQPgJI/hvamrqXqVS+ZsTujp+gmvwDZyHk3AY9sIu6EEHNsIG0MgklGEi/V4k7vP70l9D+JO/yR/wO9yEG2AvvoPLcA6Og3vIl/AJ+CXNZmhAFT6EkUZCLL9VXG5DmIg07IZjuQCnIfphUWPpumLi2yKN+G3R0ERSwds8OEUDt8AkfoGfwW6kaZzgOB3LVo7tR17UMPLK11aOxx+FchSW38AUHIe9CBNXuG83ToFpHID0fYhjSfsxtKhhRCHxilMUDwN2wnGYRJhwJO4ddmMPuFq2QRfysbzoB39bMhGvNPiR+6KwmICrQwN2wnKahCaOgSP5CqIbUdI8jRjLBI9d0ohCwVXui8Ih7hLVgMV0hTgOk9DEPvC1xZXSg7QbbuuxWpbcUdPla9SBgoE7puImoAFTsJh2wnGYhCZ2ggV1S29DA2LvsBsj04iOKJLi64acAcVPgmPQgClYTDvhOEwiNeFI3EnXQ57GkmMJI8d5UqCgGL3CR0BxE9gPe8HVYTHdL3oQSWiiBjGSZacRRhQJDnJfvGqF7YDi7pgm4Pa9A7aBxbQTjmOkCR6zaEnTjniFgTMXRX0V9coVdwTbwTFowCW6GVpgJxxHnkQUdOhIUiNeoTGnKOpVK9wDxR2BCWhgE0QKFjM6EePQRBkGJrgdmkaMRpEtCQqKV61wB0LcBDRQBw2Ygq+s6+C1TYQRZ5zi1YqiTVDY+BW3jKkBV4YG3DkrYApjJZGORoEUxQJnr3AVvHpHYAK5gXQUyx5HmIhEFMhRMERDOMTTBBY1wHNHdiI1EUacb45igbFH9MYf4jECE3itFFIznGMwWwVSFAti7nEbwi/EeezYCaQmIpH0xMPur1EwJT/ZSo4578snT49XcuJxn/vSfxrHffKbfHxhJE/zfxa16jD5HvXsAAAAAElFTkSuQmCC"; }
+ (NSString *)SMCalloutViewTopAnchor { return @"iVBORw0KGgoAAAANSUhEUgAAACkAAABGCAYAAABRwr15AAAAHGlET1QAAAACAAAAAAAAACMAAAAoAAAAIwAAACMAAAQYDCloiwAAA+RJREFUaAXMVslKHFEUfSaaaPIFunDaulJj2/QQBxxwVtCFC9GFCxVRUHHAARVx1pWf9QKVNCGb5FvMPUWf5vazqu0qFVwc7nTuvaeedr0yj4+P5qUwxlQoVCq/4qWz0R9boBLyQXwfzc3N31paWn7CMpe3/kPEFRxLZF4gxX2UuLKpqSkxOTn56/T09DcsYuQFqJMb62Qji5SFOBUs9cWJ/dTY2JiEsPv7e+/s7OwHLGLkURdosZGFRhIpyygQS6sE1Q0NDampqakchMkpWgIx8qiDl+ejDw8YSWjZIjE4vwCLcDrV9fX1aQi5u7vzBZ6cnBREQiyFggd+vi+y0LJEynBXYI0szsif1BcIccfHx0+APB4APPBlTk0coc+KDBM4MTGRu7299SDk6OgoFKiDB35coSVFhgkcGxvLXV1deRB3eHhoDw4OiixyGjjlm5sbD31xhIaKDBM4Ojqau7y89AXu7+/bvb09C/sc8EDoQ39UoYEiwwQODw/nLi4uPJzc7u5uZKAP/SMjI5GEPhEZJnBoaCh3fn7uQdzW1pbd3t72resjZg4cgnn0Yw7mlXuiRSKDBNbV1WUwUF4p3s7Ojt3Y2CjC5uamH8NqHzzmXIs5mIe5mC97S/7qpV64svQt8lnyX2pra7MDAwO+QCxaW1vzsb6+bgk3FxSTqy3myS/fw3zswT4B9uI9qq9S6PJvDtweeEGDhKf6Ko3f+/v7c/LL9DB8dXX11YG5mI892Ie9+f3QAT3QVWV6enr+uejt7f07PT39R14j3srKil1aWirC8vJyUezW3bgUH/OxB/uw19WC2Dw8PNggXF9f+0IWFxftWwMPhX1BOpAzCwsL1sX8/LwFmKdPyzxsGJecoB7Wwix7aM3c3Jx97zCzs7P2vcPMzMxY+ae1sHERpd/lunGQBiOfUVa+CS2si7C85mlOmB+2o1y+GR8ft4Bc/EXWzSOWz60iDnuCuMwFWfbBBvnoYR6+kY8GqyGXvx/D0medsVuT660wgxy3h7Fr2cs+WvJQN3It2cHBQR/wCZ2DX4rHGnvd2J3lztN13Uue6evrs4C87X0bFjP/mtbdGTYb16Iluru7C36pHGvavqRXz6Gv55lsNmuJrq4uq4E8YteSwz7WycUCnWMeljW3l3zWdY/JZDL2vcOkUikbhGQyWcin0+mCH8SNmtPz6Ot97jzT2dlpCRDpw7qxrr2l7+41iUTCamA5Y/raUhw5sMyR59Y0R9eYD8txXpHIjo4OXyCtbg7K6Tr953il6m6NsWlvb7floq2trWyunhm3jzMMBgShtbXVAlFq5MfpxR7dz73I/QcAAP//R6Pt0AAAASZJREFU7VK7asNAEBwCblKltISkImrUWEhl+tQuU+WT7xfyL94TjNksdzIkZ7yGMwyzj9m9YWWs6xpuYVmWm5rcjhKzmOc55BAfYE/HrO2x1ut4b4Y9rY8x+r4PFl3XhQjWGVtmX/M9ZsGlnnkz2bbtdjkaZU5mPcVWY/PUDGvUWmafjCjQIuY5joO2l6pZTcxTulTNzl5NNk0TiCjKxXaB1sYZndv4r7PQi/VSmiTzAeZkzpBZ1/zf2c0kF+49RE2O7zn7y2TOwKPr1WSpL1AvWS9Z6gKl9tT/ZL1kqQuU2oNhGIJ3YJqm4B0Yx/HHOyC/7ycAvsSkd+AsJr0Dn2LSO/AhJr0DJzHpHXgXk96Bo5j0DryJSe/Aq5j0DhzEpHfgRUy6xgX1iUkAQX47jAAAAABJRU5ErkJggg=="; }
+ (NSString *)SMCalloutViewTopAnchor_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAFIAAACMCAYAAADvGP7EAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo5QUY0RkNERjZENDMxMUUyQTAzNEREMUIxRjIzOEVCNSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo5QUY0RkNFMDZENDMxMUUyQTAzNEREMUIxRjIzOEVCNSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjdFQUM5NTk0NkJGQjExRTJBMDM0REQxQjFGMjM4RUI1IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjlBRjRGQ0RFNkQ0MzExRTJBMDM0REQxQjFGMjM4RUI1Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+zk9a9AAABhtJREFUeNrsnclO41gUhm3HDGEo5jGAQLwDsxjFICR2JVFVza4fAVFdD9H1ALxA9yPAhg0S6170mnmeCVMSyNDnt3LT7jSIQJzEgf9IVkJw7OuP/5zz3+sAeiwW0xjph0EEHwTkzMzMl+np6W8EmSZEKT2/GYax4HaYhtsh1tfXa9jcDtOVICcnJ78qiCMjIxVNTU12mL+4ccy6G7q2LqGeT0xMfCkoKPgOcH19faUyPl0AahsbG7eHh4faycmJFo1Gf19aWvpDvSfmgovIKUg7QHwpEGcBsaamRuvq6vIGAoHow8ND1OPx6GVlZZ69vb3AwcGBdn5+rmD+CY5uAJozkDaIuk2JCwri/f195Pb2NhIOh2PYtbS01PPp0yfTDjMSifxcXl5WyozlEqbhFoiiuoXq6moL4t3dXeTm5iYSCoWi2ESVMUD1+/1hn8/nRc3EvnKY+ampqW/2YyWp/P2CTIKoK4i1tbVad3e3BRHQkNJQIwQmaRx7fHyM4XvX19fhlpYWb3Nzs4b32GDquYSZ1dR+AuIsICKdFURswWAwKmkbE4D//sSl4cimS/rrJSUlnsrKSnN3dzeABnR2doaURpqrmpn1NM8ayKcgyuMCurNdiYAIJT53HDQe0zSt5qNgomaim6Nmrqys5ASmkU8QEVAq9sH+V1dX4dbWVivNcSyBPD82NvY1F2mecUUmQxSDPSuKWqirq9N6e3u9AKIgAlIq48EhlTLRze1pfnx8jJqadWUa+QYxDiWhTKhZKRPdvKGhAbU068rMmCKTIY6Pj3+Wxx+A2NPT44134ER3tjeWlFUgDciuzKqqKnN7e9tS5unpaVaVmRGQyRCHh4c/ywX/gF2BEmG2FUTYmnTGgFOhmxcWFloNqKKiwtzZ2QkcHR1ZaS7H/7m6uppxmEY+Q1RpDp9pN+1tbW3exsZGK83FLs0PDg5mPM0dVeRzEOET+/r6EjMWzKGTfWLaioj7zKKiIkuZmE6qNIfPzLQyHQP5HESpW1p/f/9/IL5kcdIJ1EzALC8vT8BUc/NMwnQE5FMQ5SVLiYCourOk9au6czo1U8FEzdza2grs7+8nFjoyAdN4TxDtNTMUCsWQAaiZ7e3tXp/Pp2FMMO2ZqJl6mh3zRYi4mGxBfM60S4pb1mhjY8OqmZlQ5ptBJkMUcFZNhE+0Q1Q1UQae9aUtGU9iocMOE9ZIzc3X1tYcgfkmkE9BlAFbFmdgYCDRWOTREYvjhDLhMxXM9fX1ADxm/LaFIzCN9wzRPp2Ez4R/vby8DHd2dnrhMeM31OblGtKuma9S5HMQYXGGhoasdMZgAVEtyrollDJVN6+urraUqXxmuso0nYIIFdohai4LMFFzenluFWwoUx4CeC4woUwtDtN6C645VZh6istW/4OIBQhAHB0d9YrFgM2wIMJ6ODljcXxObFtpr6ysTChT+UyJNykTUP56y4A6OjoM3KiSkz/mC8TnYEp9NzFusUOBOMzXl465ubm/X9oJJ5ST4YQFXq/XIy/BF4YvLi7CqrHkyuKkY41QM3FtUjMN8b6mdPUCfAu+F2ucku5huTaI42VFLi4urqc6WxBQUXWLVLpfojOjI+bj5yztDai4uFiX6aSnpKTEgFhg5BFQbkrNZnNz8z7FYm3BwtQrGAxalgLPFeh8DCUQZJNciw5hCEQLLNL/NVYIn1wIax88ABSZJZsmM7E3qcLkR58damBE4EyY+WBXqMiPpEjWSKY2FckayWBqM7XzBWQ+rdhQkWw2DCqSXZuKJEgGU5sgaX8YrJEEybk2Fclgs6H9IUgGayRrJBXJGsmgIgmSIAmSQftDRdL+MJjaBOnW1MZv0TOoSIIkSAZBEiRBEiSDIAmSIBkESZAESZAMgiRIgiRIBkESJEEyCJIgCZIgGQRJkARJkAyCJEiCZBAkQRIkQTJSD7OwsJAUqEgXKdLv95MCFemewL9h+pUYHEhtLf6//xgE6RqQ/HN9BOkukGFicAbkIzE4A/KBGJwBGSIGgnQVyCAxOAMyQAxUJGskQTLoIzmz4Vz7Y4Hk6o8DgVsNvG/jQHiIwCFF8i+aOhNMa4J0V/wjwADkbTd31/iGkwAAAABJRU5ErkJggg=="; }
@end
//
// Custom-drawn flexible-height background implementation.
// Contributed by Nicholas Shipes: https://github.com/u10int
//
@implementation SMCalloutDrawnBackgroundView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
}
return self;
}
// Make sure we redraw our graphics when the arrow point changes!
- (void)setArrowPoint:(CGPoint)arrowPoint {
[super setArrowPoint:arrowPoint];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2;
CGSize anchorSize = CGSizeMake(27, ANCHOR_HEIGHT);
CGFloat anchorX = roundf(self.arrowPoint.x - anchorSize.width / 2);
CGRect anchorRect = CGRectMake(anchorX, 0, anchorSize.width, anchorSize.height);
// make sure the anchor is not too close to the end caps
if (anchorRect.origin.x < ANCHOR_MARGIN_MIN)
anchorRect.origin.x = ANCHOR_MARGIN_MIN;
else if (anchorRect.origin.x + anchorRect.size.width > self.frameWidth - ANCHOR_MARGIN_MIN)
anchorRect.origin.x = self.frameWidth - anchorRect.size.width - ANCHOR_MARGIN_MIN;
// determine size
CGFloat stroke = 1.0;
CGFloat radius = [UIScreen mainScreen].scale == 1 ? 4.5 : 6.0;
rect = CGRectMake(self.bounds.origin.x, self.bounds.origin.y + TOP_SHADOW_BUFFER, self.bounds.size.width, self.bounds.size.height - ANCHOR_HEIGHT);
rect.size.width -= stroke + 14;
rect.size.height -= stroke * 2 + TOP_SHADOW_BUFFER + BOTTOM_SHADOW_BUFFER + OFFSET_FROM_ORIGIN;
rect.origin.x += stroke / 2.0 + 7;
rect.origin.y += pointingUp ? ANCHOR_HEIGHT - stroke / 2.0 : stroke / 2.0;
// General Declarations
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = UIGraphicsGetCurrentContext();
// Color Declarations
UIColor* fillBlack = [UIColor colorWithRed: 0.11 green: 0.11 blue: 0.11 alpha: 1];
UIColor* shadowBlack = [UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 0.47];
UIColor* glossBottom = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 0.2];
UIColor* glossTop = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 0.85];
UIColor* strokeColor = [UIColor colorWithRed: 0.199 green: 0.199 blue: 0.199 alpha: 1];
UIColor* innerShadowColor = [UIColor colorWithRed: 1 green: 1 blue: 1 alpha: 0.4];
UIColor* innerStrokeColor = [UIColor colorWithRed: 0.821 green: 0.821 blue: 0.821 alpha: 0.04];
UIColor* outerStrokeColor = [UIColor colorWithRed: 0 green: 0 blue: 0 alpha: 0.35];
// Gradient Declarations
NSArray* glossFillColors = [NSArray arrayWithObjects:
(id)glossBottom.CGColor,
(id)glossTop.CGColor, nil];
CGFloat glossFillLocations[] = {0, 1};
CGGradientRef glossFill = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)glossFillColors, glossFillLocations);
// Shadow Declarations
UIColor* baseShadow = shadowBlack;
CGSize baseShadowOffset = CGSizeMake(0.1, 6.1);
CGFloat baseShadowBlurRadius = 6;
UIColor* innerShadow = innerShadowColor;
CGSize innerShadowOffset = CGSizeMake(0.1, 1.1);
CGFloat innerShadowBlurRadius = 1;
CGFloat backgroundStrokeWidth = 1;
CGFloat outerStrokeStrokeWidth = 1;
// Frames
CGRect frame = rect;
CGRect innerFrame = CGRectMake(frame.origin.x + backgroundStrokeWidth, frame.origin.y + backgroundStrokeWidth, frame.size.width - backgroundStrokeWidth * 2, frame.size.height - backgroundStrokeWidth * 2);
CGRect glossFrame = CGRectMake(frame.origin.x - backgroundStrokeWidth / 2, frame.origin.y - backgroundStrokeWidth / 2, frame.size.width + backgroundStrokeWidth, frame.size.height / 2 + backgroundStrokeWidth + 0.5);
//// CoreGroup ////
{
CGContextSaveGState(context);
CGContextSetAlpha(context, 0.83);
CGContextBeginTransparencyLayer(context, NULL);
// Background Drawing
UIBezierPath* backgroundPath = [UIBezierPath bezierPath];
[backgroundPath moveToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMinY(frame) + radius)];
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMaxY(frame) - radius)]; // left
[backgroundPath addArcWithCenter:CGPointMake(CGRectGetMinX(frame) + radius, CGRectGetMaxY(frame) - radius) radius:radius startAngle:M_PI endAngle:M_PI / 2 clockwise:NO]; // bottom-left corner
// pointer down
if (!pointingUp) {
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect), CGRectGetMaxY(frame))];
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect) + anchorRect.size.width / 2, CGRectGetMaxY(frame) + anchorRect.size.height)];
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorRect), CGRectGetMaxY(frame))];
}
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMaxX(frame) - radius, CGRectGetMaxY(frame))]; // bottom
[backgroundPath addArcWithCenter:CGPointMake(CGRectGetMaxX(frame) - radius, CGRectGetMaxY(frame) - radius) radius:radius startAngle:M_PI / 2 endAngle:0.0f clockwise:NO]; // bottom-right corner
[backgroundPath addLineToPoint: CGPointMake(CGRectGetMaxX(frame), CGRectGetMinY(frame) + radius)]; // right
[backgroundPath addArcWithCenter:CGPointMake(CGRectGetMaxX(frame) - radius, CGRectGetMinY(frame) + radius) radius:radius startAngle:0.0f endAngle:-M_PI / 2 clockwise:NO]; // top-right corner
// pointer up
if (pointingUp) {
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorRect), CGRectGetMinY(frame))];
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect) + anchorRect.size.width / 2, CGRectGetMinY(frame) - anchorRect.size.height)];
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect), CGRectGetMinY(frame))];
}
[backgroundPath addLineToPoint:CGPointMake(CGRectGetMinX(frame) + radius, CGRectGetMinY(frame))]; // top
[backgroundPath addArcWithCenter:CGPointMake(CGRectGetMinX(frame) + radius, CGRectGetMinY(frame) + radius) radius:radius startAngle:-M_PI / 2 endAngle:M_PI clockwise:NO]; // top-left corner
[backgroundPath closePath];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, baseShadowOffset, baseShadowBlurRadius, baseShadow.CGColor);
[fillBlack setFill];
[backgroundPath fill];
// Background Inner Shadow
CGRect backgroundBorderRect = CGRectInset([backgroundPath bounds], -innerShadowBlurRadius, -innerShadowBlurRadius);
backgroundBorderRect = CGRectOffset(backgroundBorderRect, -innerShadowOffset.width, -innerShadowOffset.height);
backgroundBorderRect = CGRectInset(CGRectUnion(backgroundBorderRect, [backgroundPath bounds]), -1, -1);
UIBezierPath* backgroundNegativePath = [UIBezierPath bezierPathWithRect: backgroundBorderRect];
[backgroundNegativePath appendPath: backgroundPath];
backgroundNegativePath.usesEvenOddFillRule = YES;
CGContextSaveGState(context);
{
CGFloat xOffset = innerShadowOffset.width + round(backgroundBorderRect.size.width);
CGFloat yOffset = innerShadowOffset.height;
CGContextSetShadowWithColor(context,
CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)),
innerShadowBlurRadius,
innerShadow.CGColor);
[backgroundPath addClip];
CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(backgroundBorderRect.size.width), 0);
[backgroundNegativePath applyTransform: transform];
[[UIColor grayColor] setFill];
[backgroundNegativePath fill];
}
CGContextRestoreGState(context);
CGContextRestoreGState(context);
[strokeColor setStroke];
backgroundPath.lineWidth = backgroundStrokeWidth;
[backgroundPath stroke];
// Inner Stroke Drawing
CGFloat innerRadius = radius - 1.0;
CGRect anchorInnerRect = anchorRect;
anchorInnerRect.origin.x += backgroundStrokeWidth / 2;
anchorInnerRect.origin.y -= backgroundStrokeWidth / 2;
anchorInnerRect.size.width -= backgroundStrokeWidth;
anchorInnerRect.size.height -= backgroundStrokeWidth / 2;
UIBezierPath* innerStrokePath = [UIBezierPath bezierPath];
[innerStrokePath moveToPoint:CGPointMake(CGRectGetMinX(innerFrame), CGRectGetMinY(innerFrame) + innerRadius)];
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(innerFrame), CGRectGetMaxY(innerFrame) - innerRadius)]; // left
[innerStrokePath addArcWithCenter:CGPointMake(CGRectGetMinX(innerFrame) + innerRadius, CGRectGetMaxY(innerFrame) - innerRadius) radius:innerRadius startAngle:M_PI endAngle:M_PI / 2 clockwise:NO]; // bottom-left corner
// pointer down
if (!pointingUp) {
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorInnerRect), CGRectGetMaxY(innerFrame))];
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorInnerRect) + anchorInnerRect.size.width / 2, CGRectGetMaxY(innerFrame) + anchorInnerRect.size.height)];
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorInnerRect), CGRectGetMaxY(innerFrame))];
}
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMaxX(innerFrame) - innerRadius, CGRectGetMaxY(innerFrame))]; // bottom
[innerStrokePath addArcWithCenter:CGPointMake(CGRectGetMaxX(innerFrame) - innerRadius, CGRectGetMaxY(innerFrame) - innerRadius) radius:innerRadius startAngle:M_PI / 2 endAngle:0.0f clockwise:NO]; // bottom-right corner
[innerStrokePath addLineToPoint: CGPointMake(CGRectGetMaxX(innerFrame), CGRectGetMinY(innerFrame) + innerRadius)]; // right
[innerStrokePath addArcWithCenter:CGPointMake(CGRectGetMaxX(innerFrame) - innerRadius, CGRectGetMinY(innerFrame) + innerRadius) radius:innerRadius startAngle:0.0f endAngle:-M_PI / 2 clockwise:NO]; // top-right corner
// pointer up
if (pointingUp) {
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorInnerRect), CGRectGetMinY(innerFrame))];
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorInnerRect) + anchorRect.size.width / 2, CGRectGetMinY(innerFrame) - anchorInnerRect.size.height)];
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorInnerRect), CGRectGetMinY(innerFrame))];
}
[innerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(innerFrame) + innerRadius, CGRectGetMinY(innerFrame))]; // top
[innerStrokePath addArcWithCenter:CGPointMake(CGRectGetMinX(innerFrame) + innerRadius, CGRectGetMinY(innerFrame) + innerRadius) radius:innerRadius startAngle:-M_PI / 2 endAngle:M_PI clockwise:NO]; // top-left corner
[innerStrokePath closePath];
[innerStrokeColor setStroke];
innerStrokePath.lineWidth = backgroundStrokeWidth;
[innerStrokePath stroke];
//// GlossGroup ////
{
CGContextSaveGState(context);
CGContextSetAlpha(context, 0.45);
CGContextBeginTransparencyLayer(context, NULL);
CGFloat glossRadius = radius + 0.5;
// Gloss Drawing
UIBezierPath* glossPath = [UIBezierPath bezierPath];
[glossPath moveToPoint:CGPointMake(CGRectGetMinX(glossFrame), CGRectGetMinY(glossFrame))];
[glossPath addLineToPoint:CGPointMake(CGRectGetMinX(glossFrame), CGRectGetMaxY(glossFrame) - glossRadius)]; // left
[glossPath addArcWithCenter:CGPointMake(CGRectGetMinX(glossFrame) + glossRadius, CGRectGetMaxY(glossFrame) - glossRadius) radius:glossRadius startAngle:M_PI endAngle:M_PI / 2 clockwise:NO]; // bottom-left corner
[glossPath addLineToPoint:CGPointMake(CGRectGetMaxX(glossFrame) - glossRadius, CGRectGetMaxY(glossFrame))]; // bottom
[glossPath addArcWithCenter:CGPointMake(CGRectGetMaxX(glossFrame) - glossRadius, CGRectGetMaxY(glossFrame) - glossRadius) radius:glossRadius startAngle:M_PI / 2 endAngle:0.0f clockwise:NO]; // bottom-right corner
[glossPath addLineToPoint: CGPointMake(CGRectGetMaxX(glossFrame), CGRectGetMinY(glossFrame) - glossRadius)]; // right
[glossPath addArcWithCenter:CGPointMake(CGRectGetMaxX(glossFrame) - glossRadius, CGRectGetMinY(glossFrame) + glossRadius) radius:glossRadius startAngle:0.0f endAngle:-M_PI / 2 clockwise:NO]; // top-right corner
// pointer up
if (pointingUp) {
[glossPath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorRect), CGRectGetMinY(glossFrame))];
[glossPath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect) + roundf(anchorRect.size.width / 2), CGRectGetMinY(glossFrame) - anchorRect.size.height)];
[glossPath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect), CGRectGetMinY(glossFrame))];
}
[glossPath addLineToPoint:CGPointMake(CGRectGetMinX(glossFrame) + glossRadius, CGRectGetMinY(glossFrame))]; // top
[glossPath addArcWithCenter:CGPointMake(CGRectGetMinX(glossFrame) + glossRadius, CGRectGetMinY(glossFrame) + glossRadius) radius:glossRadius startAngle:-M_PI / 2 endAngle:M_PI clockwise:NO]; // top-left corner
[glossPath closePath];
CGContextSaveGState(context);
[glossPath addClip];
CGRect glossBounds = glossPath.bounds;
CGContextDrawLinearGradient(context, glossFill,
CGPointMake(CGRectGetMidX(glossBounds), CGRectGetMaxY(glossBounds)),
CGPointMake(CGRectGetMidX(glossBounds), CGRectGetMinY(glossBounds)),
0);
CGContextRestoreGState(context);
CGContextEndTransparencyLayer(context);
CGContextRestoreGState(context);
}
CGContextEndTransparencyLayer(context);
CGContextRestoreGState(context);
}
// Outer Stroke Drawing
UIBezierPath* outerStrokePath = [UIBezierPath bezierPath];
[outerStrokePath moveToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMinY(frame) + radius)];
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(frame), CGRectGetMaxY(frame) - radius)]; // left
[outerStrokePath addArcWithCenter:CGPointMake(CGRectGetMinX(frame) + radius, CGRectGetMaxY(frame) - radius) radius:radius startAngle:M_PI endAngle:M_PI / 2 clockwise:NO]; // bottom-left corner
// pointer down
if (!pointingUp) {
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect), CGRectGetMaxY(frame))];
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect) + anchorRect.size.width / 2, CGRectGetMaxY(frame) + anchorRect.size.height)];
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorRect), CGRectGetMaxY(frame))];
}
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMaxX(frame) - radius, CGRectGetMaxY(frame))]; // bottom
[outerStrokePath addArcWithCenter:CGPointMake(CGRectGetMaxX(frame) - radius, CGRectGetMaxY(frame) - radius) radius:radius startAngle:M_PI / 2 endAngle:0.0f clockwise:NO]; // bottom-right corner
[outerStrokePath addLineToPoint: CGPointMake(CGRectGetMaxX(frame), CGRectGetMinY(frame) + radius)]; // right
[outerStrokePath addArcWithCenter:CGPointMake(CGRectGetMaxX(frame) - radius, CGRectGetMinY(frame) + radius) radius:radius startAngle:0.0f endAngle:-M_PI / 2 clockwise:NO]; // top-right corner
// pointer up
if (pointingUp) {
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMaxX(anchorRect), CGRectGetMinY(frame))];
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect) + anchorRect.size.width / 2, CGRectGetMinY(frame) - anchorRect.size.height)];
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(anchorRect), CGRectGetMinY(frame))];
}
[outerStrokePath addLineToPoint:CGPointMake(CGRectGetMinX(frame) + radius, CGRectGetMinY(frame))]; // top
[outerStrokePath addArcWithCenter:CGPointMake(CGRectGetMinX(frame) + radius, CGRectGetMinY(frame) + radius) radius:radius startAngle:-M_PI / 2 endAngle:M_PI clockwise:NO]; // top-left corner
[outerStrokePath closePath];
CGContextSaveGState(context);
CGContextSetShadowWithColor(context, baseShadowOffset, baseShadowBlurRadius, baseShadow.CGColor);
CGContextRestoreGState(context);
[outerStrokeColor setStroke];
outerStrokePath.lineWidth = outerStrokeStrokeWidth;
[outerStrokePath stroke];
//// Cleanup
CGGradientRelease(glossFill);
CGColorSpaceRelease(colorSpace);
}
@end

View File

@ -239,6 +239,185 @@ THE SOFTWARE.
## SMCalloutView
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
## SQLite.swift
(The MIT License)

View File

@ -274,6 +274,191 @@ THE SOFTWARE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string> Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS</string>
<key>License</key>
<string>Apache License, Version 2.0</string>
<key>Title</key>
<string>SMCalloutView</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>(The MIT License)

View File

@ -91,11 +91,13 @@ strip_invalid_archs() {
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "$BUILT_PRODUCTS_DIR/FileMD5Hash/FileMD5Hash.framework"
install_framework "$BUILT_PRODUCTS_DIR/SDWebImage/SDWebImage.framework"
install_framework "$BUILT_PRODUCTS_DIR/SMCalloutView/SMCalloutView.framework"
install_framework "$BUILT_PRODUCTS_DIR/SQLite.swift/SQLite.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "$BUILT_PRODUCTS_DIR/FileMD5Hash/FileMD5Hash.framework"
install_framework "$BUILT_PRODUCTS_DIR/SDWebImage/SDWebImage.framework"
install_framework "$BUILT_PRODUCTS_DIR/SMCalloutView/SMCalloutView.framework"
install_framework "$BUILT_PRODUCTS_DIR/SQLite.swift/SQLite.framework"
fi
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then

View File

@ -1,10 +1,10 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash" "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage" "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift" "${PODS_ROOT}/Crashlytics/iOS" "${PODS_ROOT}/Fabric/iOS"
FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash" "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage" "$PODS_CONFIGURATION_BUILD_DIR/SMCalloutView" "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift" "${PODS_ROOT}/Crashlytics/iOS" "${PODS_ROOT}/Fabric/iOS"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Crashlytics" "${PODS_ROOT}/Headers/Public/Fabric"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash/FileMD5Hash.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage/SDWebImage.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift/SQLite.framework/Headers" -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/Crashlytics" -isystem "${PODS_ROOT}/Headers/Public/Fabric"
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"z" -framework "Crashlytics" -framework "Fabric" -framework "FileMD5Hash" -framework "SDWebImage" -framework "SQLite" -framework "Security" -framework "SystemConfiguration" -framework "UIKit"
OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash/FileMD5Hash.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage/SDWebImage.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SMCalloutView/SMCalloutView.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift/SQLite.framework/Headers" -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/Crashlytics" -isystem "${PODS_ROOT}/Headers/Public/Fabric"
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"z" -framework "Crashlytics" -framework "Fabric" -framework "FileMD5Hash" -framework "SDWebImage" -framework "SMCalloutView" -framework "SQLite" -framework "Security" -framework "SystemConfiguration" -framework "UIKit"
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

View File

@ -1,10 +1,10 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash" "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage" "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift" "${PODS_ROOT}/Crashlytics/iOS" "${PODS_ROOT}/Fabric/iOS"
FRAMEWORK_SEARCH_PATHS = $(inherited) "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash" "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage" "$PODS_CONFIGURATION_BUILD_DIR/SMCalloutView" "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift" "${PODS_ROOT}/Crashlytics/iOS" "${PODS_ROOT}/Fabric/iOS"
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Crashlytics" "${PODS_ROOT}/Headers/Public/Fabric"
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash/FileMD5Hash.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage/SDWebImage.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift/SQLite.framework/Headers" -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/Crashlytics" -isystem "${PODS_ROOT}/Headers/Public/Fabric"
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"z" -framework "Crashlytics" -framework "Fabric" -framework "FileMD5Hash" -framework "SDWebImage" -framework "SQLite" -framework "Security" -framework "SystemConfiguration" -framework "UIKit"
OTHER_CFLAGS = $(inherited) -iquote "$PODS_CONFIGURATION_BUILD_DIR/FileMD5Hash/FileMD5Hash.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SDWebImage/SDWebImage.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SMCalloutView/SMCalloutView.framework/Headers" -iquote "$PODS_CONFIGURATION_BUILD_DIR/SQLite.swift/SQLite.framework/Headers" -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/Crashlytics" -isystem "${PODS_ROOT}/Headers/Public/Fabric"
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"z" -framework "Crashlytics" -framework "Fabric" -framework "FileMD5Hash" -framework "SDWebImage" -framework "SMCalloutView" -framework "SQLite" -framework "Security" -framework "SystemConfiguration" -framework "UIKit"
OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)

View File

@ -0,0 +1,26 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.1.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${CURRENT_PROJECT_VERSION}</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>

View File

@ -0,0 +1,5 @@
#import <Foundation/Foundation.h>
@interface PodsDummy_SMCalloutView : NSObject
@end
@implementation PodsDummy_SMCalloutView
@end

View File

@ -0,0 +1,12 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif

View File

@ -0,0 +1,18 @@
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif
#import "SMCalloutView.h"
#import "SMClassicCalloutView.h"
FOUNDATION_EXPORT double SMCalloutViewVersionNumber;
FOUNDATION_EXPORT const unsigned char SMCalloutViewVersionString[];

View File

@ -0,0 +1,6 @@
framework module SMCalloutView {
umbrella header "SMCalloutView-umbrella.h"
export *
module * { export * }
}

View File

@ -0,0 +1,9 @@
CONFIGURATION_BUILD_DIR = $PODS_CONFIGURATION_BUILD_DIR/SMCalloutView
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Crashlytics" "${PODS_ROOT}/Headers/Public/Fabric"
PODS_BUILD_DIR = $BUILD_DIR
PODS_CONFIGURATION_BUILD_DIR = $PODS_BUILD_DIR/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
PODS_ROOT = ${SRCROOT}
PODS_TARGET_SRCROOT = ${PODS_ROOT}/SMCalloutView
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
SKIP_INSTALL = YES