diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h index 648a2fd6c9..d785ae2928 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h @@ -55,6 +55,9 @@ public: getTargetLocationValue() = getDefaultBuildsRootFolder() + (iOS ? "iOS" : "MacOSX"); initialiseDependencyPathValues(); + + if (getScreenOrientationValue().toString().isEmpty()) + getScreenOrientationValue() = "portraitlandscape"; } static XCodeProjectExporter* createForSettings (Project& project, const ValueTree& settings) @@ -78,6 +81,25 @@ public: Value getPreBuildScriptValue() { return getSetting (Ids::prebuildCommand); } String getPreBuildScript() const { return settings [Ids::prebuildCommand]; } + Value getScreenOrientationValue() { return getSetting (Ids::iosScreenOrientation); } + String getScreenOrientationString() const { return settings [Ids::iosScreenOrientation]; } + + Value getCustomResourceFoldersValue() { return getSetting (Ids::customXcodeResourceFolders); } + String getCustomResourceFoldersString() const { return getSettingString (Ids::customXcodeResourceFolders).replaceCharacters ("\r\n", "::"); } + + Value getCustomXcassetsFolderValue() { return getSetting (Ids::customXcassetsFolder); } + String getCustomXcassetsFolderString() const { return settings [Ids::customXcassetsFolder]; } + + Value getInAppPurchasesValue() { return getSetting (Ids::iosInAppPurchases); } + bool isInAppPurchasesEnabled() const { return settings [Ids::iosInAppPurchases]; } + Value getBackgroundAudioValue() { return getSetting (Ids::iosBackgroundAudio); } + bool isBackgroundAudioEnabled() const { return settings [Ids::iosBackgroundAudio]; } + Value getBackgroundBleValue() { return getSetting (Ids::iosBackgroundBle); } + bool isBackgroundBleEnabled() const { return settings [Ids::iosBackgroundBle]; } + + Value getIosDevelopmentTeamIDValue() { return getSetting (Ids::iosDevelopmentTeamID); } + String getIosDevelopmentTeamIDString() const { return settings [Ids::iosDevelopmentTeamID]; } + bool usesMMFiles() const override { return true; } bool isXcode() const override { return true; } bool isOSX() const override { return ! iOS; } @@ -85,19 +107,48 @@ public: void createExporterProperties (PropertyListBuilder& props) override { - if (projectType.isGUIApplication() && ! iOS) + if (iOS) { - props.add (new TextPropertyComponent (getSetting ("documentExtensions"), "Document file extensions", 128, false), - "A comma-separated list of file extensions for documents that your app can open. " - "Using a leading '.' is optional, and the extensions are not case-sensitive."); + props.add (new TextPropertyComponent (getCustomXcassetsFolderValue(), "Custom Xcassets folder", 128, false), + "If this field is not empty, your Xcode project will use the custom xcassets folder specified here " + "for the app icons and launchimages, and will ignore the Icon files specified above."); } - else if (iOS) + + props.add (new TextPropertyComponent (getCustomResourceFoldersValue(), "Custom Xcode Resource folders", 8192, true), + "You can specify a list of custom resource folders here (separated by newlines or whitespace). " + "References to these folders will then be added to the Xcode resources. " + "This way you can specify them for OS X and iOS separately, and modify the content of the resource folders " + "without re-saving the Introjucer project."); + + if (iOS) { + static const char* orientations[] = { "Portrait and Landscape", "Portrait", "Landscape", nullptr }; + static const char* orientationValues[] = { "portraitlandscape", "portrait", "landscape", nullptr }; + + props.add (new ChoicePropertyComponent (getScreenOrientationValue(), "Screen orientation",StringArray (orientations), Array (orientationValues)), + "The screen orientations that this app should support"); + props.add (new BooleanPropertyComponent (getSetting ("UIFileSharingEnabled"), "File Sharing Enabled", "Enabled"), "Enable this to expose your app's files to iTunes."); props.add (new BooleanPropertyComponent (getSetting ("UIStatusBarHidden"), "Status Bar Hidden", "Enabled"), "Enable this to disable the status bar in your app."); + + props.add (new BooleanPropertyComponent (getInAppPurchasesValue(), "In-App purchases capability", "Enabled"), + "Enable this to grant your app the capability for in-app purchases. " + "This option requires that you specify a valid Development Team ID."); + + props.add (new BooleanPropertyComponent (getBackgroundAudioValue(), "Audio background capability", "Enabled"), + "Enable this to grant your app the capability to access audio when in background mode."); + + props.add (new BooleanPropertyComponent (getBackgroundBleValue(), "Bluetooth MIDI background capability", "Enabled"), + "Enable this to grant your app the capability to connect to Bluetooth LE devices when in background mode."); + } + else if (projectType.isGUIApplication()) + { + props.add (new TextPropertyComponent (getSetting ("documentExtensions"), "Document file extensions", 128, false), + "A comma-separated list of file extensions for documents that your app can open. " + "Using a leading '.' is optional, and the extensions are not case-sensitive."); } props.add (new TextPropertyComponent (getPListToMergeValue(), "Custom PList", 8192, true), @@ -114,6 +165,14 @@ public: props.add (new TextPropertyComponent (getPostBuildScriptValue(), "Post-build shell script", 32768, true), "Some shell-script that will be run after a build completes."); + + if (iOS) + { + props.add (new TextPropertyComponent (getIosDevelopmentTeamIDValue(), "Development Team ID", 10, false), + "The Development Team ID to be used for setting up code-signing your iOS app. This is a ten-character " + "string (for example, \"S7B6T5XJ2Q\") that describes the distribution certificate Apple issued to you. " + "You can find this string in the OS X app Keychain Access under \"Certificates\"."); + } } bool launchProject() override @@ -325,6 +384,7 @@ private: void createObjects() const { addFrameworks(); + addCustomResourceFolders(); addMainBuildProduct(); if (xcodeCreatePList) @@ -337,7 +397,14 @@ private: if (iOS) { if (! projectType.isStaticLibrary()) - createiOSAssetsFolder(); + { + String customXcassetsPath = getCustomXcassetsFolderString(); + + if (customXcassetsPath.isEmpty()) + createXcassetsFolderFromIcons(); + else + addCustomResourceFolder (customXcassetsPath, "folder.assetcatalog"); + } } else { @@ -662,22 +729,8 @@ private: // Forcing full screen disables the split screen feature and prevents error ITMS-90475 addPlistDictionaryKeyBool (dict, "UIRequiresFullScreen", true); - static const char* kDefaultiOSOrientationStrings[] = - { - "UIInterfaceOrientationPortrait", - "UIInterfaceOrientationPortraitUpsideDown", - "UIInterfaceOrientationLandscapeLeft", - "UIInterfaceOrientationLandscapeRight", - nullptr - }; - - StringArray iOSOrientations (kDefaultiOSOrientationStrings); - - dict->createNewChildElement ("key")->addTextElement ("UISupportedInterfaceOrientations"); - XmlElement* plistStringArray = dict->createNewChildElement ("array"); - - for (int i = 0; i < iOSOrientations.size(); ++i) - plistStringArray->createNewChildElement ("string")->addTextElement (iOSOrientations[i]); + addIosScreenOrientations (dict); + addIosBackgroundModes (dict); } for (int i = 0; i < xcodeExtraPListEntries.size(); ++i) @@ -689,6 +742,36 @@ private: overwriteFileIfDifferentOrThrow (infoPlistFile, mo); } + void addIosScreenOrientations (XmlElement* dict) const + { + String screenOrientation = getScreenOrientationString(); + StringArray iOSOrientations; + + if (screenOrientation.contains ("portrait")) { iOSOrientations.add ("UIInterfaceOrientationPortrait"); } + if (screenOrientation.contains ("landscape")) { iOSOrientations.add ("UIInterfaceOrientationLandscapeLeft"); iOSOrientations.add ("UIInterfaceOrientationLandscapeRight"); } + + addArrayToPlist (dict, "UISupportedInterfaceOrientations", iOSOrientations); + + } + + void addIosBackgroundModes (XmlElement* dict) const + { + StringArray iosBackgroundModes; + if (isBackgroundAudioEnabled()) iosBackgroundModes.add ("audio"); + if (isBackgroundBleEnabled()) iosBackgroundModes.add ("bluetooth-central"); + + addArrayToPlist (dict, "UIBackgroundModes", iosBackgroundModes); + } + + static void addArrayToPlist (XmlElement* dict, String arrayKey, const StringArray& arrayElements) + { + dict->createNewChildElement ("key")->addTextElement (arrayKey); + XmlElement* plistStringArray = dict->createNewChildElement ("array"); + + for (int i = 0; i < arrayElements.size(); ++i) + plistStringArray->createNewChildElement ("string")->addTextElement (arrayElements[i]); + } + void deleteRsrcFiles() const { for (DirectoryIterator di (getTargetFolder().getChildFile ("build"), true, "*.rsrc", File::findFiles); di.next();) @@ -981,6 +1064,9 @@ private: StringArray s (xcodeFrameworks); s.addTokens (getExtraFrameworksString(), ",;", "\"'"); + if (iOS && isInAppPurchasesEnabled()) + s.addIfNotAlreadyThere ("StoreKit"); + if (project.getConfigFlag ("JUCE_QUICKTIME") == Project::configFlagDisabled) s.removeString ("QuickTime"); @@ -993,6 +1079,31 @@ private: } } + void addCustomResourceFolders() const + { + StringArray crf; + + crf.addTokens (getCustomResourceFoldersString(), ":", ""); + crf.trim(); + + for (int i = 0; i < crf.size(); ++i) + addCustomResourceFolder (crf[i]); + } + + void addCustomResourceFolder (String folderPathRelativeToProjectFolder, const String fileType = "folder") const + { + String folderPath = RelativePath (folderPathRelativeToProjectFolder, RelativePath::projectFolder) + .rebased (projectFolder, getTargetFolder(), RelativePath::buildTargetFolder) + .toUnixStyle(); + + const String fileRefID (createFileRefID (folderPath)); + + addFileOrFolderReference (folderPath, "", fileType); + + resourceIDs.add (addBuildFile (folderPath, fileRefID, false, false)); + resourceFileRefs.add (createFileRefID (folderPath)); + } + //============================================================================== void writeProjectFile (OutputStream& output) const { @@ -1072,11 +1183,18 @@ private: sourceTree = ""; } + String fileType = getFileType (path); + + return addFileOrFolderReference (pathString, sourceTree, fileType); + } + + String addFileOrFolderReference (String pathString, String sourceTree, String fileType) const + { const String fileRefID (createFileRefID (pathString)); ScopedPointer v (new ValueTree (fileRefID)); v->setProperty ("isa", "PBXFileReference", nullptr); - v->setProperty ("lastKnownFileType", getFileType (path), nullptr); + v->setProperty ("lastKnownFileType", fileType, nullptr); v->setProperty (Ids::name, pathString.fromLastOccurrenceOf ("/", false, false), nullptr); v->setProperty ("path", sanitisePath (pathString), nullptr); v->setProperty ("sourceTree", sourceTree, nullptr); @@ -1332,7 +1450,7 @@ private: ValueTree* const v = new ValueTree (createID ("__root")); v->setProperty ("isa", "PBXProject", nullptr); v->setProperty ("buildConfigurationList", createID ("__projList"), nullptr); - v->setProperty ("attributes", "{ LastUpgradeCheck = 0440; }", nullptr); + v->setProperty ("attributes", getProjectObjectAttributes(), nullptr); v->setProperty ("compatibilityVersion", "Xcode 3.2", nullptr); v->setProperty ("hasScannedForEncodings", (int) 0, nullptr); v->setProperty ("mainGroup", createID ("__mainsourcegroup"), nullptr); @@ -1415,6 +1533,23 @@ private: return getiOSAssetContents (images); } + String getProjectObjectAttributes() const + { + String attributes; + + attributes << "{ LastUpgradeCheck = 0440; "; + + if (iOS && isInAppPurchasesEnabled()) + { + attributes << "TargetAttributes = { " << createID ("__target") << " = { "; + attributes << "DevelopmentTeam = " << getIosDevelopmentTeamIDString() << "; "; + attributes << "SystemCapabilities = { com.apple.InAppPurchase = { enabled = 1; }; }; }; };"; + } + + attributes << "}"; + return attributes; + } + //============================================================================== struct ImageType { @@ -1502,7 +1637,7 @@ private: return JSON::toString (var (v)); } - void createiOSAssetsFolder() const + void createXcassetsFolderFromIcons() const { const File assets (getTargetFolder().getChildFile (project.getProjectFilenameRoot()) .getChildFile ("Images.xcassets")); diff --git a/extras/Introjucer/Source/Utility/jucer_PresetIDs.h b/extras/Introjucer/Source/Utility/jucer_PresetIDs.h index 353f34f7d9..3af853396a 100644 --- a/extras/Introjucer/Source/Utility/jucer_PresetIDs.h +++ b/extras/Introjucer/Source/Utility/jucer_PresetIDs.h @@ -80,6 +80,8 @@ namespace Ids DECLARE_ID (systemHeaderPath); DECLARE_ID (libraryPath); DECLARE_ID (customXcodeFlags); + DECLARE_ID (customXcassetsFolder); + DECLARE_ID (customXcodeResourceFolders); DECLARE_ID (cppLanguageStandard); DECLARE_ID (cppLibType); DECLARE_ID (codeSigningIdentity); @@ -156,6 +158,11 @@ namespace Ids DECLARE_ID (androidSharedLibraries); DECLARE_ID (androidNdkPlatformVersion); DECLARE_ID (androidScreenOrientation); + DECLARE_ID (iosScreenOrientation); + DECLARE_ID (iosInAppPurchases); + DECLARE_ID (iosBackgroundAudio); + DECLARE_ID (iosBackgroundBle); + DECLARE_ID (iosDevelopmentTeamID); DECLARE_ID (buildToolsVersion); DECLARE_ID (gradleVersion); DECLARE_ID (gradleWrapperVersion);