Advertisement
  1. Code
  2. iOS SDK
Code

iOS Succinctly - Asset Management

by
Difficulty:BeginnerLanguages:
This post is part of a series called iOS Succinctly.
iOS Succinctly - Multi-Scene Applications
iOS Succinctly - Localization

Now that we have a basic understanding of iOS scene management, the next big topic to tackle is how to manage the multimedia assets in an application. iOS apps store their assets using the same hierarchical file system as any other modern operating system. Text, image, audio, and video files are organized into folders and accessed using familiar file paths like Documents/SomePicture.png.

In this chapter, we’ll learn about the standard file structure for an app; how to add resources to a project; and how to locate, load, and save files. We’ll also talk about the required assets for all iOS apps.

Throughout the chapter, we’ll talk about files and folders, but keep in mind that the file system should be entirely hidden from iOS users. Instead of showing users the files and folders behind an application, iOS encourages developers to present the file system as user-oriented documents. For example, in a sketching app, drawings should be listed with semantic display names and organized into sketchbooks or a similar abstract organizational structure. You should never show the user file paths like sketchbook-1/your-drawing.svg.


Conceptual Overview

The Application Sandbox

The iOS file system was designed with security in mind. Instead of allowing an app to access a device’s entire file system, iOS gives each application its own separate file system (a sandbox). This means your application doesn’t have access to files generated by other apps. When you need to access information that is not owned by your app (e.g., the user’s contact list), you request it from a mediator (e.g., the Address Book Framework) instead of accessing the files directly.

A sandbox is like a mini file system dedicated solely to your app’s operation. All apps use a canonical file structure consisting of four top-level directories, each of which store a specific type of file.

  • AppName.app, the application bundle that contains your app’s executable and all of its required media assets. You can read from this folder, but you should never write to it. The next section discusses bundles in more detail.
  • Documents/, a folder for user-generated content and other critical data files that cannot be re-created by your app. The contents of this directory are available through iCloud.
  • Library/, a folder for application files that are not used by the user, but still need to persist between launches.
  • tmp/, a folder for temporary files used while your application is running. Files in this folder do not necessarily persist between application launches. iOS will automatically delete temporary files when necessary while your application isn’t running, but you should manually delete temporary files as soon as you’re done with them as a best practice.

When the user installs an app, a new sandbox containing all of these folders is created. After that, your application can dynamically create arbitrary subdirectories in any of these top-level folders. There are also a few pre-defined subdirectories, as described in the following list.

  • Library/Application Support/, a folder for support files that can be re-created if necessary. This includes downloaded and generated content. You should use the com.apple.MobileBackup extended attribute to prevent this folder from being backed up.
  • Library/Cache/, a folder for cache files. These files can be deleted without notice, so your app should be able to re-create them gracefully. This folder is also an appropriate place to store downloaded content.

It’s important to put files in the appropriate folder to make sure they are backed up properly without consuming an unnecessary amount of space on the user’s device. iTunes automatically backs up files in the Documents/ and Library/ folders (with the exception of Library/Cache/). Neither the application bundle nor the tmp/ folder should ever need to be backed up.

Bundles

An iOS application isn’t just an executable. It also contains media, data files, and possibly localized text for different regions. To simplify deployment, Xcode wraps the executable and all of its required files into a special kind of folder called an application bundle. Despite being a folder, an application bundle uses the .app extension. You can think of an application bundle as a ZIP file that runs an app when you open it.

Since your application bundle contains all of your media assets, you’ll need to interact with it while your program is running. The NSBundle class makes it easy to search your application bundle for specific files, which can then be loaded by other classes. For example, you can locate a particular image file using NSBundle, and then add it to a view using the UIImage class. We’ll do this in another section, “The Application Bundle.”


Creating the Example Application

This chapter uses a simple application to explore some of the fundamental methods for accessing files in iOS. First, open Xcode, create a new project, and select Single View Application for the template.

tutorial_image
Figure 81: Creating a new Single View Application

Use AssetManagement for the Product Name, edu.self for the Company Identifier, and make sure Use Storyboards and Use Automatic Reference Counting are selected.

tutorial_image
Figure 82: Configuring the new project

You can save the project wherever you like.


The File System

Before we go into an app’s multimedia assets, we’re going to look at the basic tools for accessing the file system. The upcoming sections discuss how to generate file paths, create plain text files, and load them back into the application.

Locating Standard Directories

One of the most common file system tasks is generating the path to a particular resource. But, before you can gain access to any given file, you need to find the path to the application sandbox or one of the top-level folders discussed previously. The easiest way to do this is via the global NSHomeDirectory() function, which returns the absolute path to the application sandbox. For example, try changing ViewController.m’s viewDidLoad method to the following:

When the app loads in the iOS Simulator, you should see something like the following in Xcode’s output panel.

This path represents the root of your application. If you navigate to this directory in a terminal, you’ll find the following four directories.

Not surprisingly, this is the canonical file structure discussed previously. Of course, NSHomeDirectory() will return a different path when your application is running on an iOS device. The idea behind using NSHomeDirectory() instead of manually generating the path to your application is to make sure you always have the correct root path, regardless of where your application resides.

The related NSTemporaryDirectory() function returns the path to the tmp/ directory. For the other standard application folders, you’ll need to use the NSFileManager class.

As you can see, NSFileManager is implemented as a singleton, and the shared instance should be accessed via the defaultManager class method. The NSSearchPathDirectory enum defines several constants that represent the standard locations used by both OS X and iOS applications. Some of these locations (e.g., NSDesktopDirectory) are not applicable in iOS apps, however, the URLsForDirectory:inDomains: method will still return the appropriate subfolder in the application sandbox. The constants for the directories we’ve discussed are listed as follows.

The URLsForDirectory:inDomains: method returns an NSArray containing NSURL objects, which is an object-oriented representation of a file path.

Generating File Paths

Once you have the location of one of the standard directories, you can manually assemble the path to a specific file using the NSURL instance methods. Note that NSString also provides related utilities, but NSURL is the preferred way to represent file paths.

For example, the URLByAppendingPathComponent: method provides a straightforward way to generate the path to a specific file. The following snippet creates a path to a file called someData.txt in the application’s Library/ directory.

A few of the other useful NSURL instance methods are described in the following list. Together, these provide the basic functionality for navigating a file hierarchy and manually determining file names and types.

  • URLByDeletingLastPathComponent returns a new NSURL representing the parent folder of the receiving path.
  • lastPathComponent returns the final component in the path as a string. This could be either a folder name or a file name, depending on the path.
  • pathExtension returns the file extension of the path as a string. If the path doesn’t contain a period, it returns an empty string, otherwise it returns the last group of characters that follow a period.
  • pathComponents decomposes the path into its component parts and returns them as an NSArray.

Saving and Loading Files

It’s important to understand that NSURL only describes the location of a resource. It does not represent the actual file or folder itself. To get at the file data, you need some way to interpret it. The NSData class provides a low-level API for reading in raw bytes, but most of the time you’ll want to use a higher-level interface for interpreting the contents of a file.

The iOS frameworks include many classes for saving and loading different types of files. For example, NSString can read and write text files, UIImage can display images inside of a view, and AVAudioPlayer can play music loaded from a file. We’ll look at UIImage once we get to the application bundle, but for now, let’s stick with basic text file manipulation.

To save a file with NSString, use the writeToURL:automatically:encoding:error: method. The first argument is an NSURL representing the file path, the second determines whether or not to save it to an auxiliary file first, and the third is one of the constants defined by the NSStringEncoding enum, and the error argument is a reference to an NSError instance that will record error details should the method fail. The following snippet demonstrates writeToURL:automatically:encoding:error: by creating a plain text file called someData.txt in the Library/ folder.

When saving or loading text files, it’s imperative to specify the file encoding, otherwise your text data could be unexpectedly altered when writing or reading a file. A few of the common encoding constants are included here.

When in doubt, you’ll probably want to use NSUnicodeStringEncoding to make sure multi-byte characters are interpreted properly.

To load a text file, you can use the related stringWithContentsOfURL:encoding:error:. This works much the same as writeToURL:automatically:encoding:error:, but it’s implemented as a class method instead of an instance method. The following example loads the text file created by the previous snippet back into the app.

In the real world, you’ll probably save and load data that was dynamically generated instead of hardcoded as a literal string. For instance, you might store template preferences or user information that needs to persist between application launches in a text file. It’s entirely possible to manually load and interpret this data from plain text, but keep in mind that there are several built-in tools for working and storing structured data or even whole Objective-C objects. For example, NSDictionary defines a method called dictionaryWithContentsOfURL: that loads an XML file containing key-value pairs.

Manipulating Directories

The NSString methods described previously combine the creation of a file and the writing of content into a single step, but to create directories, we need to return to the NSFileManager class. It defines several methods that let you alter the contents of a directory.

Creating Directories

The createDirectoryAtURL:withIntermediateDirectories:attributes:error: instance method creates a new directory at the specified path. The second argument is a Boolean value that determines whether or not intermediate directories should be created automatically, attributes lets you define file attributes for the new directory, and the final argument is a reference to an NSError instance that will contain the error details should the method fail.

For instance, if your application uses custom templates downloaded from a server, you might save them in Library/Templates/. To create this folder, you could use something like the following.

Leaving the attributes argument as nil tells the method to use the default group, owner, and permissions for the current process.

Moving Files/Directories

The NSFileManager class can also be used to move or rename files and folders via its moveItemAtURL:toURL:error: instance method. It works as follows.

This renames the someData.txt file we created earlier to someOtherData.txt. If you need to copy files, you can use copyItemAtURL:toURL:error:, which works the same way, but leaves the source file untouched.

Removing Files/Directories

Finally, NSFileManager’s removeItemAtURL:error: method lets you delete files or folders. Simply pass the NSURL instance containing the path you want to remove, as follows.

If the targetURL is a directory, its contents will be removed recursively.

Listing the Contents of a Directory

It’s also possible to list the files and subdirectories in a folder using NSFileManager’s enumeratorAtPath: method. This returns an NSDirectoryEnumerator object that you can use to iterate through each file or folder. For example, you can list the contents of the top-level Documents/ directory as follows:

Note that this enumerator will iterate through all subdirectories. You can alter this behavior by calling the skipDescendents method on the NSDirectoryEnumerator instance, or using the more sophisticated enumeratorAtURL:includingPropertiesForKeys:options:errorHandler: method of NSFileManager.


The Application Bundle

The file access methods discussed previously are typically only used to interact with files that are dynamically created at run time. The standard Library/, Documents/, and tmp/ folders are local to the device on which the application is installed, and they are initially empty. For assets that are an essential part of the application itself (as opposed to being created by the application), we need another tool—an application bundle.

The application bundle represents your entire project, which is composed of the iOS executable, along with all of the images, sounds, videos, and configuration files required by that executable. The application bundle is what actually installs when a user downloads your app, so—unlike the contents of the sandbox directories—you can assume that all of the supporting resources will be present regardless of the device on which your app resides.

Conceptually, a bundle is just a bunch of files, but interacting with it is a little bit different than using the file system methods from the previous section. Instead of manually generating file paths and saving or loading data with methods like writeToURL:automatically:encoding:error:, you use the NSBundle class as a high-level interface to your application’s media assets. This delegates some nitty-gritty details behind media assets to the underlying system.

In addition to custom media assets, the application bundle also contains several required resources, like the app icon that appears on the user’s home screen and important configuration files. We’ll talk about these in the “Required Resources” section.

Adding Assets to the Bundle

Remember that assets in the application bundle are static, so they will always be included at compile-time. To add a media asset to your project, simply drag the file(s) from the Finder into the Project Navigator panel in Xcode. We’re going to add a file called syncfusion-logo.jpg, which you can find in the resource package for this book, but you can use any image you like. In the next section, we’ll learn how to access the bundle to display this image in the view.

tutorial_image
Figure 83: Adding an image to the application bundle

After releasing the mouse, Xcode will present you with a dialog asking for configuration options. The Copy items into destination group’s folder check box should be selected. This tells Xcode to copy the assets into the project folder, which is typically desirable behavior. The Create groups for any added folders option uses the Xcode grouping mechanism. Add to targets is the most important configuration option. It defines which build targets the asset will be a compiled with. If AssetManagement wasn’t selected, it would be like we never added it to the project.

tutorial_image
Figure 84: Selecting configuration options for the new media assets

After clicking Finish, you should see your file in the Xcode Project Navigator:

tutorial_image
Figure 85: The media asset in the Project Navigator

Now that you have a custom resource in your application bundle, you can access it via NSBundle.

Accessing Bundled Resources

Instead of manually creating file paths with NSURL, you should always use the NSBundle class as the programmatic interface to your application bundle. It provides optimized search functionality and built-in internationalization capabilities, which we’ll talk about in the next chapter.

The mainBundle class method returns the NSBundle instance that represents your application bundle. Once you have that, you can locate resources using the pathForResource:ofType: instance method. iOS uses special file naming conventions that make it possible for NSBundle to return different files depending on how the resource is going to be used. Separating the file name from the extension allows pathForResource:ofType: to figure out which file to use automatically, and it allows NSBundle to automatically select localized files.

For example, the following code locates a JPEG called syncfusion-logo in the application bundle:

Like text files, locating an image and loading it are separate actions. The first NSLog() call should display something like /path/to/sandbox/AssetManagement.app/your-image.jpg in the Xcode Output Panel.

The UIImage class represents the contents of an image file, and it works with virtually any type of image format (JPEG, PNG, GIF, TIFF, BMP, and a few others). Once you’ve gotten the image location with NSBundle, you can pass it to the initWithContentsOfFile: method of UIImage. If the file loaded successfully, you’ll be able to access the image’s dimensions through the size property, which is a CGSize struct containing the width and height floating-point fields.

While UIImage does define a few methods for drawing the associated image to the screen (namely, drawAtPoint: and drawInRect:), it’s often easier to display it using the UIImageView class. Since it’s a subclass of UIView, it can be added to the existing view hierarchy using the addSubview: method common to all view instances. UIImageView also provides a convenient interface for controlling animation playback. To display UIImage in the root view object, change the viewDidLoad method of ViewController.m to the following.

When you compile the project, you should see your image in the top-left corner of the screen. By default, the image will not be scaled. So, if your image is bigger than the screen, it will be cropped accordingly.

tutorial_image
Figure 86: A cropped UIImageView object

To change how your UIImageView scales its content, you should use the contentMode property of UIView. It takes a value of type UIViewContentMode, which is an enum defining the following behaviors.

  • UIViewContentModeScaleToFill, scale to fill the view’s dimensions, changing the image’s aspect ratio if necessary.
  • UIViewContentModeScaleAspectFit, scale to fit into the view’s dimensions, maintaining the image’s aspect ratio.
  • UIViewContentModeScaleAspectFill, scale to fill the view’s dimensions, maintaining the image’s aspect ratio. This may cause part of the image to be clipped.
  • UIViewContentModeCenter, use the original image’s size, but center it horizontally and vertically.
  • UIViewContentModeTop, use the original image’s size, but center it horizontally and align it to the top of the view.
  • UIViewContentModeBottom, use the original image’s size, but center it horizontally and align it to the bottom of the view.
  • UIViewContentModeLeft, use the original image’s size, but center it vertically and align it to the left of the view.
  • UIViewContentModeRight, use the original image’s size, but center it vertically and align it to the right of the view.
  • UIViewContentModeTopLeft, use the original image’s size, but align it to the top-left corner of the view.
  • UIViewContentModeTopRight, use the original image’s size, but align it to the top-right corner of the view.
  • UIViewContentModeBottomLeft, use the original image’s size, but align it to the bottom-left corner of the view.
  • UIViewContentModeBottomRight, use the original image’s size, but align it to the bottom-right corner of the view.

For example, if you want your image to shrink to fit into the width of the screen while maintaining its aspect ratio, you would use UIViewContentModeScaleAspectFit for the contentMode, and then change the width of the image view’s dimensions to match the width of the screen (available via the [UIScreen mainScreen] object). Add the following to the viewDidLoad method from the previous example after the [[self view] addSubview:imageView]; line.

The frame property of all UIView instances defines the position and the visible area of the view (i.e. its dimensions.). After changing the width of the frame, the UIViewContentModeScaleAspectFit behavior automatically calculated the height of the frame, resulting in the following image.

tutorial_image
Figure 87: Shrinking an image to fit the screen width

While this section extracted an image from the bundle, remember that it’s just as easy to access other types of media. In the previous section, we saw how text files can be loaded with NSString; this works the same way with bundles. A video works in a similar fashion to an image in that iOS provides a dedicated class (MPMoviePlayerController) for incorporating it into an existing view hierarchy. Audio files are slightly different, since playback is not necessarily linked to a dedicated view. We’ll discuss the audio capabilities of iOS later in this book. For now, let’s go back to the application bundle.

Required Resources

In addition to any custom media assets your app might need, there are also three files that are required to be in your application bundle. These are described as follows.

  • Information property list, the configuration file defining critical options like required device capabilities, supported orientations, etc.
  • App icon, the icon that appears on the user’s home screen. This is what the user taps to launch your application.
  • Launch image. After the user launches your application, this is the image that briefly appears while it’s loading.

Information Property List

A property list (also known as a “plist”) is a convenient format for storing structured data. It makes it easy to save arrays, dictionaries, dates, strings, and numbers into a persistent file and load it back into an application at run time. If you’ve ever worked with JSON data, it’s the same idea.

The information property list is a special kind of property list stored in a file called Info.plist in your application bundle. You can think of it as a file-based NSDictionary whose key-value pairs define the configuration options for your app. The Info.plist for our example app is generated from a file called AssetManagement-Info.plist, and you can edit it directly in Xcode by selecting it from the Supporting Files folder in the Project Navigator. When you open it, you should see something like the following.

tutorial_image
Figure 88: Opening the Info.plist file in Xcode

These are all of the configuration options provided by the template. The left-most column contains the option name, and the right-most one contains its value. Note that values can be Boolean, numbers, strings, arrays, or even entire dictionaries.

By default, keys are shown with human-readable titles, but it helps—especially when learning iOS for the first time—to see the raw keys that are referenced by the official documentation. To display the raw keys, Ctrl+click anywhere in the Info.plist editor and select Show Raw Keys/Values. These are the strings to use when you want to access configuration options programmatically (as discussed in the next section).

tutorial_image
Figure 89: Displaying raw keys

The left-most column should now show keys like CFBundleDevelopmentRegion, CFBundleDisplayName, and so on. The following keys are required by all Info.plist files.

  • UIRequiredDeviceCapabilities is an array containing the device requirements for your app. This is one way that Apple determines which users can view your application in the App Store. Possible values are listed in the UIRequiredDeviceCapabilities section of the iOS Keys Reference.
  • UISupportedInterfaceOrientations is an array defining the orientations your app supports. Acceptable values include UIInterfaceOrientationPortrait, UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight, and UIInterfaceOrientationPortraitUpsideDown.
  • CFBundleIconFile is an array containing the file names of all your app icons. We’ll talk more about app icons in a moment.

The template provides defaults for the device requirements and the supported orientations, as shown in Figure 90.

tutorial_image
Figure 90: Default values for device requirements and supported orientations

Accessing Configuration Options

Most of the configuration options defined in Info.plist are used internally by iOS, but you may occasionally need to access them manually. You can get an NSDictionary representation of Info.plist through the infoDictionary method of NSBundle. For example, if you wanted to perform a custom launch behavior based on a key in Info.plist, you could do a quick check in the application:didFinishLaunchingWithOptions: method of AppDelegate.m, like so:

App Icon(s)

Your app bundle must include at least one icon to display on the home screen, but it’s also possible to specify multiple icons for different situations. For example, you might want to use a different design for the smaller icon that appears in search results, or use a high-resolution image for devices with Retina displays. Xcode takes care of all of this for you.

The CFBundleIconFiles key in Info.plist should be an array containing the file names of all your app icons. With the exception of the App Store icon, you can use any name you like for the files. Note that you do not need to specify the intended usage of each file—iOS will select the appropriate icon automatically based on the dimensions.

tutorial_image
Figure 91: Custom iPhone app icons with high-resolution versions for Retina displays

If your app supports devices with Retina displays, you should also include a high-resolution version of each icon and give it the same file name with @2x appended to it. For example, if your main icon file was called app-icon.png, the Retina display version should be called app-icon@2x.png.

The following table lists the standard iOS app icons, but be sure to visit the iOS Human Interface Guidelines for a more detailed discussion. This document also provides extensive guidelines for creating icon graphics. All icons should use the PNG format.

Icon Type Platform Required Standard Size Retina Size Description/
App Store Icon iPhone and iPad Yes 512 × 512 1024 × 1024 The image presented to customers in iTunes. This file must be called iTunesArtwork or iTunesArtwork@2x (with no extension).
Main Icon iPhone Yes 57 × 57 114 × 114 The icon that appears on the iPhone home screen.
Main Icon iPad Yes 72 × 72 144 × 144 The icon that appears on the iPad home screen.
Small Icon iPhone No 29 × 29 58 × 58 The icon displayed next to search results and in the Settings app for iPhone.
Small Icon iPad No 50 × 50 100 × 100 The icon displayed next to search results and in the Settings app for iPad.

Next, you’re going to add an icon to the example application. In the resource package for this book, you’ll find four sample icons called app-icon.png, app-icon@2x.png, app-icon-small.png, and app-icon-small@2x.png. Drag all of these into the Project Navigator to add them to your application bundle, and then open AssetManagement-Info.plist and make sure you’re looking at raw keys or values. Add a new row and type CFBundleIconFiles for the key (not to be confused with CFBundleIcons or CFBundleIconFile). This will automatically add an empty item to the array, which you can view by clicking the triangle next to the CFBundleIconFiles item. Add all four of the icon files to the array, so it looks like the following. The order doesn’t matter.

tutorial_image
Figure 92: Adding icon files to Info.plist

Now, when you run your project, you should see a custom app icon in the home screen. You may need to restart the iOS Simulator for this to work.

tutorial_image
Figure 93: The custom app icon in the home screen

Try dragging the home screen to the right a few times until you reach the search screen. If you start typing “assetmanagement,” you should see the small version of the icon in the search results, as shown in Figure 94:

tutorial_image
Figure 94: The small app icon used in search results

And that’s all there is to customizing the icons for your iPhone application.

Launch Image(s)

The final required media asset for any iOS application is a launch image. A launch image is displayed immediately when the user opens your application. The idea is to give the user the impression that your app launched immediately, even though it may take a few seconds to load. Apple discourages developers from using launch images as an about page or a splash screen. Instead, it should be a skeleton of your app’s initial screen.

For example, consider the master-detail application we built in the previous chapter. The ideal launch image would simply be an empty master list:

tutorial_image
Figure 95: Appropriate launch image for a master-detail application

As you can see from previous chapters, a launch image is essentially a screenshot of your app’s initial screen, minus any dynamic data. This avoids any abrupt changes in the UI. Again, the idea is to downplay the application launch by making the transition as seamless as possible from app selection, to launch image, to the initial screen. The iOS Simulator has a convenient screen capture tool for creating launch images. Once your application is done, run it in the simulator and navigate to Edit > Copy Screen.

Like app icons, it’s possible to have multiple launch images depending on the device or screen resolution. Launch images use the same @2x affix for high-resolution images, but it’s also possible to target specific devices by appending a usage modifier immediately after the base name. For example, iPhone 5 has different screen dimensions than previous generations, and thus requires its own launch image. To tell iOS to use a particular file for iPhone 5 devices, you would append -568h to its base name, giving you something like launch-image-568h@2x.png (note that iPhone 5 has a Retina display, so the associated launch image will always have @2x in its filename).

The following table lists the dimension requirements for iOS launch images.

Platform Standard Size Retina Size
iPhone (up to 4th generation) 320 × 480 640 × 960 iPhone (5th generation) 640 × 1136 640 × 1136 iPad 768 × 1004 1536 × 2008

Once you have your launch image, adding it to your application is very similar to configuring app icons. First, add the files to the top level of your application bundle, and then add the base name to the UILaunchImageFile key of your Info.plist. For example, if your launch images were called launch-image.png, launch-image@2x.png, and launch-image-568h@2x.png, you would use launch-image as the value for UILaunchImageFile.

If you don’t specify a UILaunchImageFile, iOS will use the files Default.png, Default@2x.png, and Default-568h@2x.png. These default launch images are provided by the Xcode templates.


Summary

This chapter covered many of the built-in asset management tools in iOS. We talked about an app’s sandbox file system and how to read and write dynamic files from various predefined directories. We also looked at the application bundle, which contains all of the resources that will be distributed with the app. In addition to any custom media assets, the application bundle must include an information property list, app icons, and launch images.

Bundles are a convenient way to distribute an application, but they also provide built-in internationalization capabilities. In the next chapter, we’ll learn how to show different files to different users based on their language settings. And, thanks to NSBundle, this will require very little additional code.

This lesson represents a chapter from iOS Succinctly, a free eBook from the team at Syncfusion.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.