Enable iCloud on a Xcode project via script - ios

i am trying to setup a build server for a continuous building on a iOS project.
Since that i need to recreate the Xcode project very often (it is a build from unity), the iCloud will be reset to OFF.
I can copy the entitlement file (with the iCloud key) via script but i still need to click on the actual checkbox to turn iCloud ON.
I managed to change the XC project manually but it is not very safe, due to possible changes on XC project structure.
Do you know a better way to do this?
Cheers!

Apparently the TO solved the problem but since there seem to be more people interested in this, here a possible solution.
You can make a script to edit the project.pbxproj which is inside your xcodeproj file (can be viewed e.g. with "show package contents" options in finder).
In this file there's a section for project settings called PBXProject section. There you can add the capabilities for the targets. You probably need to write custom parsing logic, since this file is written in XCode config format, no anything popular like XML or JSON.
The part which you want to update looks like this:
/* Begin PBXProject section */
EB1DDE9C1A3334EC00D778DE /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0610;
/* ...more settings */
TargetAttributes = {
EB1DDEA31A3334EC00D778DE = {/* this is one target */
CreatedOnToolsVersion = 6.1.1;
DevelopmentTeam = 37QAPDY2PR;
/* ...more settings */
};
EB9F6CE11A8812550038355B = {/* another possible target */
CreatedOnToolsVersion = 6.2;
DevelopmentTeam = 37QAPDY2PR;
/* ...more settings */
};
};
};
/* ...more settings */
};
/* End PBXProject section */
You want to add the iCloud capabilities to the targets. This looks like this:
SystemCapabilities = {
com.apple.iCloud = {
enabled = 1;
};
};
So let's say you want to add iCloud capability to the target EB1DDEA31A3334EC00D778DE, then the TargetAttributes entries will look like this:
TargetAttributes = {
EB1DDEA31A3334EC00D778DE = {/* this is one target */
CreatedOnToolsVersion = 6.1.1;
DevelopmentTeam = 37QAPDY2PR;
SystemCapabilities = {
com.apple.iCloud = {
enabled = 1;
};
};
};
EB9F6CE11A8812550038355B = {/* another possible target */
CreatedOnToolsVersion = 6.2;
DevelopmentTeam = 37QAPDY2PR;
/* ...more settings */
};
};
Now there are some things you need to determine in order to do this:
1. Identify the section
I'd make the script look for the string "Begin PBXProject section" and then for "TargetAttributes" both of which are unique in the file. Theoretically "TargetAttributes" should be enough but better to be safe... and remember to add proper logging to the script and verify the results, because these strings could easily change in future XCode versions (I have seen them unchanged, though, a while already).
2. Identify the target
There are multiple parts in this file where you can see the id of the target associated with the name. I would just look this up myself and hardcode it in the script, as this id will not change unless you re-create the target. If you really need it you can also automate this... by looking for your target's name and the format where it appears associated with the id. There should be also other config files where this association appears (in this file the name just appears as a comment).
3. Handle situation that there is already a SystemCapabilities entry for the target, and also that there is already iCloud entry.
If your target has other capabilities this entry may already exist. Also if you already have iCloud enabled or if you had once iCloud enabled and disabled it, the entry will also exist (with a 0 value). This has to be handled in the script (latest should not be a problem if the project file is new though).
Besides of that, you also probably have to add a reference to the entitlements file. You have to add this to the build configurations of the respective targets. For this:
4. Find the build configuration id(s)
Your target has probably multiple build configurations, e.g. debug and release. You have to find the id of the build configuration(s) for which you want to add a reference to the entitlements file. To do this there's a section called XCConfigurationList (look for /* Begin XCConfigurationList section */). There look for the target id we got in 1., Then find the configuration id(s) for the configurations you need.
5. Look for build configuration id in XCBuildConfiguration section
Go to /* Begin XCBuildConfiguration section */ and look for id(s) found in 5., then add path to entitlements to it's buildSettings. E.g:
E.g. you have sth like
EB9F6CF33A861055BB38355B /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B82B36921BDRI3622B0EC99 /* Pods-mytargetname.debug.xcconfig */;
buildSettings = {
/* build settings... */
CODE_SIGN_ENTITLEMENTS = mytargetname/myentitlements.entitlements; /* <-- add this */
};
name = Debug;
};
Note that XCode should "know" the entitlements file (just like the rest of your project files).

Related

Swift - Localizable file working properly in all except one iPad device

I am currently working on an Xcode project that has several different targets, each one with their own strings defined for specific parts of the app. That is why we are using different string files, which we call depending on the target we are running.
I came across a strange problem which I still could not find a way to reproduce. One of the devices of a client is showing the name of the key instead of showing the string defined for that target or the default string in the file "default.string". The problem is that all other test devices (simulator and physical devices) we used show the strings correctly. The device that is not showing strings properly (and just showing the key) is an iPad 7th Generation, with iPadOS 14.
I am using the following code to show the string on the view (note that I am using "section2HomeTitle" as an example here):
FactoryView.sectionHeaderView(with: Strings.localizedString(forKey: Strings.LocalizedStringKey.section2HomeTitle), icon: Constants.Config.useIconSectionsHomeTitle ? #imageLiteral(resourceName: "imbatibles"): nil )
The key section2HomeTitle is defined in a file for Constants the following way:
struct Strings {
struct LocalizedStringKey {
static let section2HomeTitle = "section2HomeTitle"
}}
Also in the Constants file, I have the following function defined, which looks for the definition of key "section2HomeTitle" in the localized files:
static func localizedString(forKey key: String) -> String {
var result = Bundle.main.localizedString(forKey: key, value: nil, table: Constants.Config.store)
if result == key {
result = Bundle.main.localizedString(forKey: key, value: nil, table: "default")
}
return result
}
That function looks for the current target's definition for that particular key using Constants.Config.store. If it does not find the key in that target's file, it will look for it in the default file.
In this case I am dealing with Target store "xyz", and therefore I define its keys in file xyz.string which has Localization language set to Spanish and Target Membership set to "xyz" target. The key is defined in that file with the following string:
"section2HomeTitle" = "REBAJAS";
In case the file is not found, I have the "default" file defined as default.string which has Localization language set to Spanish and Target Membershipt set to "xyz" target and all other targets in the project. The key definition here is:
"section2HomeTitle" = "Ofertas";
As I mentioned before, the output shows up as "REBAJAS" in all devices we tested for that target, except in that one iPad 7th Gen from the client, where it shows:section2HomeTitle
My questions are:
Why is this key showing up correctly in all devices we test it, except on the client's iPad 7th Gen
What can I do to correct this issue?
Thank you.

Testing for GVfs metadata support in C

I am trying to add support for per-directory viewing settings to the Thunar file browser of the Xfce desktop. So for example if a user chooses to view the contents of a directory as a list rather than as a grid of icons, this setting is remembered for that directory and will be used whenever that directory is viewed.
Now Thunar is built on GLib, and the mechanism we have chosen to use to implement this is to store metadata using GFile attributes, using methods like g_file_set_attributes_async to store
keys with names such as "metadata::thunar-view-type". The per-directory feature can be turned on or off by the user via a checkbox in a preferences dialog. My knowledge of GIO and GLib is pretty limited, but I have now managed to get this all working as desired (you can see my merge request here if you are interested).
Now as I understand it, the functionality that I am using here relies on something called "GVfs metadata", and as I understand it this might not be available on all systems. On systems where GVfs metadata is not available, I want to turn this functionality off and in particular make the checkbox in the preferences dialog insensitive (i.e. greyed out). Thus I need to write a function to detect if gvfs metadata support is available, by which I mean whether I can use functions like g_file_set_attributes_async to successfully save metadata so that it will be available in future.
Thunar is written in C, so this function needs to be written in C using the C API for GLib, GIO, etc.
The function I have come up with (from much reading of API documentation, modifying code scraps I have found, and experimentation) is as follows.
gboolean
thunar_g_vfs_metadata_is_supported (void)
{
GDBusMessage *send, *reply;
GDBusConnection *conn;
GVariant *v1, *v2;
GError *error = NULL;
const gchar **service_names;
gboolean metadata_found;
/* connect to the session bus */
conn = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
/* check that the connection was opened sucessfully */
if (error != NULL)
{
g_error_free (error);
return FALSE;
}
/* create the message to send to list the available services */
send = g_dbus_message_new_method_call ("org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"ListNames");
/* send the message and wait for the reply */
reply = g_dbus_connection_send_message_with_reply_sync (conn, send, G_DBUS_SEND_MESSAGE_FLAGS_NONE,
-1, NULL, NULL, &error);
/* release the connection and the sent message */
g_object_unref (send);
g_object_unref (conn);
/* check if we got a sucessful reply */
if (error != NULL)
{
g_error_free (error);
return FALSE;
}
/* extract the GVariant with the array of strings describing the available services */
v1 = g_dbus_message_get_body (reply); /* v1 belongs to reply and must not be freed */
if (v1 == NULL || !g_variant_is_container (v1) || g_variant_n_children (v1) < 1)
{
g_object_unref (reply);
return FALSE;
}
v2 = g_variant_get_child_value (v1, 0);
g_object_unref (reply);
/* check that the GVariant we have been given does contain an array of strings */
if (!g_variant_is_of_type (v2, G_VARIANT_TYPE_STRING_ARRAY))
{
g_variant_unref (v2);
return FALSE;
}
/* search through the list of service names to see if gvfs metadata is present */
metadata_found = FALSE;
service_names = g_variant_get_strv (v2, NULL);
for (int i=0; service_names[i] != NULL; i++)
if (g_strcmp0 (service_names[i], "org.gtk.vfs.Metadata") == 0)
metadata_found = TRUE;
g_free (service_names);
g_variant_unref (v2);
return metadata_found;
}
As you can see, this function uses DBus to query service names to see if the necessary service is available. Now, as far as I have been able to test it, this function works as I want it to. However, during a code review it has been questioned whether this can be done without relying on DBus (which might itself not be available even though GVfs metadata is).
Thus (at last!) my question: what is the best (i.e. most robust and accurate) way to test for GVfs metadata support via the C API for GLib, GIO, etc?. As I said above, by "GVfs metadata support" I mean "can I use functions like g_file_set_attributes_async to successfully save metadata so that it will be available in future?".
One method I have considered is looking at the list of running processes for the name "gvfsd-metadata", but that seems a bit kludgy to me.
Also, as mentioned above I am very much a novice with these technologies, so I is absolutely possible that I have misunderstood stuff here, so if you spot any errors in the assertions I have made above, please let me know.
Thanks!
(And yes, usual story, I'm a long time reader of SO & co, but a first time asker, so please feel free to edit or let me know if I've done something wrong/bad)
Call g_file_query_settable_attributes() and g_file_query_writable_namespaces() on the GFile, as described in the GFileInfo documentation:
However, not all attributes can be changed in the file. For instance, the actual size of a file cannot be changed via g_file_info_set_size(). You may call g_file_query_settable_attributes() and g_file_query_writable_namespaces() to discover the settable attributes of a particular file at runtime.

GraphQL Object mapping Apollo with different schema by target

I have a project with multiple targets.
I have implemented Apollo to request database with GraphQL. Targets have slightly the same schema with some differences: +1 or 2 fields depending targets.
I can successfully build my app because I have changed my build phases to build only *common.graphql and *targetname.graphql
So when target A has AnnuaireFragment like that:
fragment AnnuaireFragment on Directory {
id
test
}
and target B has AnnuaireFragment like that:
fragment AnnuaireFragment on Directory {
id
}
Everything can build, fragments are separated.
I would like that when I use translater I can build too. Can I use only one translater for the two targets?
static func translateFromAnnuaireNode(annuaireNode: GetAnnuaireFromSearchQuery.Data.Directory.Edge.Node) -> ContactModel {
let contactModel = ContactModel()
contactModel._id = annuaireNode.id
// Here that doesn't build for target B because annuaireNode.test doesn't exist
contactModel.test = annuaireNode.test
return contactModel
}
How can I successfully build my app for target A and B ?
When building for target B, the compiler doesn't know about existence of different class definition of GetAnnuaireFromSearchQuery.Data.Directory.Edge.Node, which is specific to target A. Which means that this error is compile-time error, and the only possible way of solving it is addressing it in compile-time.
The possible solution I can come up with is to use different compilation conditions for different targets.
The setup is following:
Under build settings on one of your targets place TARGET_A and under another one place TARGET_B
Use #if to differentiate targets during compile-time
static func translateFromAnnuaireNode(annuaireNode: GetAnnuaireFromSearchQuery.Data.Directory.Edge.Node) -> ContactModel {
let contactModel = ContactModel()
contactModel._id = annuaireNode.id
#if TARGET_A
//The code here will only be compiled when building target A
contactModel.test = annuaireNode.test
#endif
return contactModel
}

Can I auto-increment the CFBundleVersion value in the Info.plist file using Visual Studio?

I've seen solutions to doing this with Xcode and even Xamarin Studio, but nothing with Visual Studio.
Ideally, I'd like for every single build of the project to auto-increment the CFBundleVersion value within the Info.plist file.
<key>CFBundleVersion</key>
<string>9</string>
I don't even know where to start and haven't been able to find an article / blog post / tutorial on anything that includes Visual Studio.
Is this possible?
Just wanted to add that I am using Visual Studio 2015 on Windows 8.1.
Being in the same boat as you, as in not finding a proper solution, I decided to create my own. Maybe better late than never! :)
In my case I used the very useful Automatic Versions Settings tool (available on NuGet) to automatically update my assembly info, but wanted that to also update the Info.plist information as that's what HockeyApp uses to track and notify of new releases.
In the end, I kludged together a minimal C# program, which reads AssemblyInfo.cs, grabs the version info from there and edits the Info.plist XML file and writes it back.
It'd be a 20 line program if I hadn't put in a lot of paranoid checks, so as not to risk mangling Info.plist irretrievably (and even then it creates a backup of that file).
The "magic" comes down to two methods, the first one which I found here on SO:
Read AssemblyInfo.cs:
private static string GetVersionFromAssemblyInfo(string infile)
{
var version = String.Empty;
var readText = File.ReadAllLines(infile);
var versionInfoLines = readText.Where(t => t.Contains("[assembly: AssemblyVersion"));
foreach (var item in versionInfoLines)
{
version = item.Substring(item.IndexOf('(') + 2, item.LastIndexOf(')') - item.IndexOf('(') - 3);
}
return version;
}
Edit Info.plist, where the first 3 elements of the assembly info tuple becomes the CFBundleShortVersionString and the last element becomes CFBundleVersion which HockeyApp uses for build number.
The wonkiness in the LINQ is due to the slight weirdness of Apple's way of presenting the key/value pairs in that file:
private static bool SetVersionInInfoPlist(string infoplistFile, string version, string number)
{
var xelements = XDocument.Load(infoplistFile);
var dict = from el in xelements.Root?.Elements() select el;
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (dict == null) return false;
var cfshortversion =
from el in dict.Descendants("key")
where el.Value == "CFBundleShortVersionString"
select el.ElementsAfterSelf().FirstOrDefault();
;
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (cfshortversion == null) return false;
cfshortversion.FirstOrDefault()?.SetValue(version);
var cfversion =
from el in dict.Descendants("key")
where el.Value == "CFBundleVersion"
select el.ElementsAfterSelf().FirstOrDefault();
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (cfversion == null) return false;
cfversion.FirstOrDefault()?.SetValue(number);
// Make backup
try
{
File.Copy(infoplistFile, $"{infoplistFile}-backup", true);
}
catch (Exception)
{
Console.WriteLine($"Failed to create backup of {infoplistFile}. Will not edit.");
return false;
}
try
{
using (StringWriter sw = new StringWriter())
{
using (XmlWriter xWrite = XmlWriter.Create(sw))
{
xelements.Save(xWrite);
}
}
xelements.Save(infoplistFile);
}
catch (Exception)
{
Console.WriteLine($"Failed to save the edited {infoplistFile}.");
return false;
}
Console.WriteLine($"Successfully edited and saved new {infoplistFile}.");
return true;
}
EDIT: I should have also added that I use Bamboo for CI and build automation. This program therefore becomes a capability for the remote build agent and then I can add it as a Task in the Bamboo build Plan.

How to update a custom TFS field programmatically

We have a custom build process (not using MS Build) and during that process I am adding a "fake" build to the global builds list. The reason I am doing that is so that you can select the build for a given work item (found in build). We have a custom field, build included, which is intended to show which build that work item was fixed in. I am having trouble figuring out how to update this field programmatically. The idea is I will have a small app that does this that I will call during the build process, finding all work items since the last build, then updating the field for those work items. Any ideas?
Something like this should work for you:
public void UpdateTFSValue(string tfsServerUrl, string fieldToUpdate,
string valueToUpdateTo, int workItemID)
{
// Connect to the TFS Server
TfsTeamProjectCollection tfs = new TfsTeamProjectCollection(new Uri(tfsUri));
// Connect to the store of work items.
_store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
// Grab the work item we want to update
WorkItem workItem = _store.GetWorkItem(workItemId);
// Open it up for editing. (Sometimes PartialOpen() works too and takes less time.)
workItem.Open();
// Update the field.
workItem.Fields[fieldToUpdate] = valueToUpdateTo;
// Save your changes. If there is a constraint on the field and your value does not
// meet it then this save will fail. (Throw an exception.) I leave that to you to
// deal with as you see fit.
workItem.Save();
}
An example of calling this would be:
UpdateTFSValue("http://tfs2010dev:8080/tfs", "Integration Build", "Build Name", 1234);
The variable fieldToUpdate should be the name of the field, not the refname (ie. Integration Build, not Microsoft.VSTS.Build.IntegrationBuild)
You could probably get away with using PartialOpen(), but I am not sure.
You will probably need to add Microsoft.TeamFoundation.Client to your project. (And maybe Microsoft.TeamFoundation.Common)
This has changed for TFS 2012, basicly you have to add workItem.Fields[fieldToUpdate].Value
Updated Version of what #Vaccano wrote.
public void UpdateTFSValue(string tfsServerUrl, string fieldToUpdate,
string valueToUpdateTo, int workItemID)
{
// Connect to the TFS Server
TfsTeamProjectCollection tfs = new TfsTeamProjectCollection(new Uri(tfsUri));
// Connect to the store of work items.
_store = (WorkItemStore)tfs.GetService(typeof(WorkItemStore));
// Grab the work item we want to update
WorkItem workItem = _store.GetWorkItem(workItemId);
// Open it up for editing. (Sometimes PartialOpen() works too and takes less time.)
workItem.Open();
// Update the field.
workItem.Fields[fieldToUpdate].Value = valueToUpdateTo;
// Save your changes. If there is a constraint on the field and your value does not
// meet it then this save will fail. (Throw an exception.) I leave that to you to
// deal with as you see fit.
workItem.Save();
}

Resources