Among the tasks of the mobile developer, in addition to the most frequent (writing, in fact, applications) periodically appears such as the creation of sdk.
Examples of such a task could be the creation of sdk using the REST API of a service (advertising, analytics, weather), a library of algorithm implementations, image processing ... The list is almost unlimited.
Errors in such a product are much more difficult to correct than when developing applications. In the case of an application, it is enough to update it in the AppStore, wait for the moderation and update by the user. In the case of sdk, the chain grows by an additional step - wait for it to be updated by the developer.
')
For some types of tasks, the situation can be aggravated by the fact that not all application developers using our sdk can be sufficiently qualified. In this regard, integration should cause a minimum of difficulties. This is especially true for advertising sdk and analytics, since they are often embedded much later than the main stage of creating an application and not always the original developer.
Any such sdk usually consists of many components: a library, a test application, documentation, plug-ins for other tools. In this article I will talk about building a library in the form of a framework, some techniques and design features.
For a start, a small remark about the problem to be solved. Our goal is to get the library in a format that requires a minimum of effort and knowledge to integrate, and also has no side effects after it.
Such a solution could be, for example, CocoaPods, which, in particular, allows you to distribute packages with closed source code. There are quite a few articles (for example,
here or
here ) that describe how to publish our libraries in CocoaPods, and we probably will not repeat.
For our sdk, designed for a wide range of developers, this method of distribution is obviously not enough: some developers do not use it in principle (“what if they block github again?”), Some have not heard of it at all.
Focus on building sdk in a framework format. All that the user needs for integration is to drag him to his project.
Pro dynamic frameworks iOS8
With the release of iOS 8 and its corresponding version of iOS SDK, a new type of target appeared when creating a project - a dynamic framework. Let's try to find out if it suits us.
The appearance of this type of target was due, primarily, to the introduction of extensions with which the main application has to share common code and resources. The resulting dynamic libraries are similar to the real ones, which are present, for example, in OS X only partly. The fact is that the library still remains in the application sandbox and can be used only for extension and main application.
Is it possible to use such frameworks to distribute our sdk? At the moment, the situation is ambiguous. From the
Apple documentation, it follows that the embedded framework can only be used on iOS8. The application may still have Deployment Target below, but it will not be possible to use such a framework. Practice shows a slightly different situation. Embedded framework is still able to run on devices with iOS version below 8, but this joy quickly passes when you try to publish an application using such framework in the AppStore. At the stage of automatic validation, an error is issued:
The MinimumOSVersion of the framework "..." is invalid. The minimum value is iOS 8.0;
Setting the minimum version for the framework framework to 8.0, in turn, leads to an error at the linking stage of the project:
ld: embedded dylibs / frameworks are only supported on iOS 8.0 and later
A summary of dynamic frameworks at the moment is this: it is possible to use and distribute them, but only if the minimum version of the iOS SDK starts from 8.
Pro build SDK
Let's try to use the same template of the dynamic framework that is provided by Xcode 6, but now to build its static version. It turns out that after some modifications it is quite possible. Let's get started
We will create a new project, where we will select Cocoa Touch Framework as the type of target. Name choose any. For definiteness, our name is MySDK.
We add our sdk classes to the project. The header files, which should be publicly available, are marked as Public and imported into the umbrella-header, which by default was created with the project. In our case, it is called MySDK.h
Next, go to our target's BuildSettings and in the Linking section change Mach-O Type to the Static Library.
In the settings of our target we change the launch configuration to Release. This action is necessary for the build to occur for all available architectures, not just for the active one.
A developer using our library will probably want to run code on all possible processor architectures for iOS. And we have to take care of this. At the moment, the current list of architectures includes 3 for devices (armv7, armv7s, arm64) and 2 for simulators (i386, x86_64). To do this, you need to build a so-called fat binary library, which will include layers for each of the supported architectures. To do this, create in the project a target of the Aggregate type. Let's call it MySDKUniversal.
Add the Run Script phase to the BuildPhases target with the following script:
FRAMEWORK_NAME="${PROJECT_NAME}" SIMULATOR_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework" DEVICE_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework" UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal" FRAMEWORK_PATH="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework" xcodebuild -project ${PROJECT_NAME}.xcodeproj -sdk iphonesimulator -target ${FRAMEWORK_NAME} -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator ARCHS='i386 x86_64' xcodebuild -project ${PROJECT_NAME}.xcodeproj -sdk iphoneos -target ${FRAMEWORK_NAME} -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos ARCHS='arm64 armv7 armv7s' rm -rf "${UNIVERSAL_LIBRARY_DIR}" mkdir "${UNIVERSAL_LIBRARY_DIR}" mkdir "${FRAMEWORK_PATH}" cp -r "${DEVICE_LIBRARY_PATH}/." "${FRAMEWORK_PATH}" lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${FRAMEWORK_PATH}/${FRAMEWORK_NAME}"
Here we call the assembly of our framework twice and assemble the resulting libraries into one fat binary using the lipo utility. Note the last ARCHS parameter when calling xcodebuild. If you do not specify it, the assembly for the device will use the default list of architectures described in the variable ARCHS_STANDARD. The fact is that Apple, apparently, deliberately excluded from the variable ARCHS_STANDARD armv7s for Xcode6, leaving only armv7, arm64. In general, this is not a big problem for the developer, and in the application, he can almost always exclude armv7s. The application without any problems, although, probably, with less optimization, will work on A6, A6X processors. But, developing sdk, you should not make such a decision for the developer. It is much easier to list the architectures explicitly.
About dependencies
Rarely what code can do without third-party dependencies. For example, network libraries, various parsers, categories for classes of system frameworks, etc. As a result, all these classes fall into our sdk, and the developer who uses it will be able to run into problems while building their project. These problems will start as soon as he wants to use one of those libraries embedded into sdk. The compiler will generate a bunch of “duplicate symbols” when building its application. It would seem that this is bad: the developer just needs to remove this library from the dependencies, since its classes already exist in our sdk. The hitch is that it loses the opportunity to update this library and becomes dependent on the availability of our sdk.
The solution, which, with some reservations, can be called the best available, is to add prefixes to all imported symbols of the libraries used. Another step is added to the build process of our framework. To do this, create a separate project target in which we will build a static library with all dependencies. Let's call it ext.
Add all our dependencies to the CompileSources section of the list of phases of the assembly. The last phase is to add a custom script. We take the script
here . Do not forget to correct the prefix, which we will add to the symbols from the library, and the path to the header file, which we will generate. In our case it is:
header="${SRCROOT}/${PROJECT_NAME}/ext/NamespacedDependencies.h" prefix="MYSDK"
We start assembly of our target `ext`. We won't need the resulting library. Of interest is the resulting header file NamespacedDependencies.h. Make sure that it is not empty and contains macros that replace the names of the characters in our dependency library. After that, add the resulting header file to our main target MySDK.
NamespacedDependencies.h must be imported before importing any of the dependency headers. The most suitable place for this is the PCH file. If it does not already exist in the project, then you need to create it. Do not forget to specify the path to it in the settings of the target.
Now when building our framework, all symbols from dependency libraries will be provided with the specified prefix. This fact can be verified by running the nm utility.
nm -a /Users/mik/Library/Developer/Xcode/DerivedData/Build/Products/Release-iphoneuniversal/MySDK.framework/MySDK | grep MYSDK 00000000000022be t +[MYSDK_AFURLConnectionOperation batchOfRequestOperations:progressBlock:completionBlock:] 0000000000000000 t +[MYSDK_AFURLConnectionOperation networkRequestThreadEntryPoint:] ...
As a result of this trick, our sdk will not cause conflicts when using the same dependencies in the final project, although, of course, it will increase the size of the assembled application.
About resources
In addition to the compiled library, there is often a need to distribute it along with resources: such as images, sounds, xib files, etc. And the framework structure may contain a folder with resources. However, Xcode simply ignores it.
To be able to add resources to your project, you need to distribute along with the framework either a separate bundle with resources, or a symbolic link to the resources located inside the framework itself.
Method 1
For resources we will create a separate bundle, which we will place inside the framework.
We will place all our resources in it, and its, in turn, already inside the framework.
Due to the fact that we used the Bundle template for OS X, it is important not to forget to make a few more settings in the bundle target.
- install Base SDK in Latest iOS (default is Latest OS X)
- remove Compile Sources section from Build Phases
- Set the Combine High Resolution Artwork setting to NO. Otherwise, resources for different screen densities will be collected in one TIFF file.
After this setup, you can add MySDKResources.bundle to the Copy Bundle Resources section of our main target MySDK and the target MySDKResources in Target Dependencies.
It remains to add a symbolic link to the bundle inside the framework. To do this, we add to the script that collects our universal framework of the line.
pushd ${UNIVERSAL_LIBRARY_DIR} ln -sfh "${FRAMEWORK_NAME}.framework/${FRAMEWORK_NAME}Resources.bundle" "${FRAMEWORK_NAME}Resources.bundle" popd
Now, to use sdk, in addition to the framework, you also need to add this symbolic link to the project.
Method 2
A significant disadvantage of the first method is that our framework ceases to be integral. And this is very important when spreading sdk. For cases where this is applicable, you can use such a technique as embedding resources in the sdk code. This method is convenient for storing resources of small size. For example, it can be sounds, images of buttons, logos or text resources. Naturally, the list is not limited to this set, resources can be absolutely any.
This is where the xxd utility comes in, which has a great output option in the format of a static C-array. I use such an additional resource generation script in the Build Phases project that I need to add as a Run Script before the Compile Sources.
RESOURCES_FILE="${SRCROOT}/${PROJECT_NAME}/MySDKResources.h" rm -f ${RESOURCES_FILE} pushd "${SRCROOT}/${PROJECT_NAME}Resources/images" for filename in *.png; do xxd -i $filename >> "${RESOURCES_FILE}" done popd
The resulting MySDKResources.h is added to the project. After that, resources can be used to create an NSData object, and from it, in turn, any other object, for example, a UIImage for images.
NSData *pngData = [NSData dataWithBytesNoCopy:close_png length:close_png_len freeWhenDone:NO];
About versioning
When distributing sdk it is very convenient for each build to have a unique version number. Convenience is mainly noticeable when contacting customer support or when taking into account errors in the bugtracker. There are many ways to generate a version number. I stopped at this. We use the format MAJOR.MINOR.BUILD, where MAJOR.MINOR are set manually depending on the release policy, backward compatibility of versions, etc. The number of commits in the current repository branch is taken as BUILD (release builds always come from one branch). To automate this process, a small script is used, which is added to the BuildPhases target before the build phase of the framework. After the first generation of this file, we do not forget to add it to the project with the public label and to our umbrella-header:
VERSION=`git rev-list HEAD --count` echo "#define MYSDK_VERSION @\"1.0.${VERSION}\"" > ${SRCROOT}/MYSDKVersion.h
After this, the version number can be used by sdk itself. For example, as parameters of a request to an API or for output to a log.
Instead of conclusion
So, in this article we managed to cover some important aspects of sdk development, mainly related to the build. But the range of issues related to sdk is far from exhausted. Such interesting things as the sdk build automation, documentation, sdk testing automation, support for old iOS versions, plugin development for tools such as Unity and Adobe Air remained intact. But this is already a topic for a separate and, perhaps, not one article.