In building an iPhone app, XCode requires you use a loading storyboard or a loading image. As there are still some major bugs with the storyboard mechanism, safe area padding, using an image asset is a much more supported approach. The problem is that the developer has to support it. In order to do that, a correct asset has to be made in the Assets.xcassets sub-folder along with a Contents.json file that looks like this:
{
"images" : [
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "2688h",
"filename" : "LoadingX_1242w2688h.png",
"minimum-system-version" : "12.0",
"orientation" : "portrait",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "2688h",
"filename" : "LoadingX_2688w1242h.png",
"minimum-system-version" : "12.0",
"orientation" : "landscape",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "1792h",
"filename" : "LoadingX_828w1792h.png",
"minimum-system-version" : "12.0",
"orientation" : "portrait",
"scale" : "2x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "1792h",
"filename" : "LoadingX_1792w828h.png",
"minimum-system-version" : "12.0",
"orientation" : "landscape",
"scale" : "2x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "2436h",
"filename" : "LoadingX_1125w2436h.png",
"minimum-system-version" : "11.0",
"orientation" : "portrait",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "2436h",
"filename" : "LoadingX_2436w1125h.png",
"minimum-system-version" : "11.0",
"orientation" : "landscape",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "736h",
"filename" : "LoadingX_1242w2208h.png",
"minimum-system-version" : "8.0",
"orientation" : "portrait",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "736h",
"filename" : "LoadingX_2208w1242h.png",
"minimum-system-version" : "8.0",
"orientation" : "landscape",
"scale" : "3x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "667h",
"filename" : "LoadingX_750w1334h.png",
"minimum-system-version" : "8.0",
"orientation" : "portrait",
"scale" : "2x"
},
{
"orientation" : "portrait",
"idiom" : "iphone",
"filename" : "LoadingX_640w960h.png",
"extent" : "full-screen",
"minimum-system-version" : "7.0",
"scale" : "2x"
},
{
"extent" : "full-screen",
"idiom" : "iphone",
"subtype" : "retina4",
"filename" : "LoadingX_640w1136h.png",
"minimum-system-version" : "7.0",
"orientation" : "portrait",
"scale" : "2x"
},
{
"orientation" : "portrait",
"idiom" : "iphone",
"filename" : "LoadingX_320w480h.png",
"extent" : "full-screen",
"scale" : "1x"
},
{
"orientation" : "portrait",
"idiom" : "iphone",
"filename" : "LoadingX_640w960h.png",
"extent" : "full-screen",
"scale" : "2x"
},
{
"orientation" : "portrait",
"idiom" : "iphone",
"filename" : "LoadingX_640w1136h.png",
"extent" : "full-screen",
"subtype" : "retina4",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Let's define a structure in C# that can provide this information...
using System;
using System.IO;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
struct ContentAsset
{
public System.Drawing.Size Size;
public int Scale;
public string Extent;
public string Orientation;
public string MinimumSystemVersion;
public string SubType;
public string Device;
}
The goal is not to take an arbitrary image and stretch it to cover the required area without changing aspect ratio. Again, we turn to C# code...
public static partial class SystemDrawingFunctions
{
/// <summary>
/// Scales an image filling a size
/// </summary>
/// <param name="image">The image.</param>
/// <param name="size">The size.</param>
/// <param name="fitAspect">if set to <c>true</c> [fit aspect].</param>
/// <returns>A new image of the desired size</returns>
public static System.Drawing.Image ScaledToFill(
this System.Drawing.Image image,
System.Drawing.Size size,
bool fitAspect = true)
{
double destX = 0;
double destY = 0;
double scaleRatio = 1;
double scaleX = ((double)size.Width / (double)image.Width);
double scaleY = ((double)size.Height / (double)image.Height);
if (!fitAspect)
{
scaleRatio = Math.Min(scaleY, scaleX);
}
else
{
scaleRatio = Math.Max(scaleY, scaleX);
destY = (size.Height - image.Height * scaleRatio) / 2;
destX = (size.Width - image.Width * scaleRatio) / 2;
}
int destWidth = (int)Math.Round(image.Width * scaleRatio);
int destHeight = (int)Math.Round(image.Height * scaleRatio);
System.Drawing.Bitmap returnImage = new System.Drawing.Bitmap(size.Width, size.Height);
using (System.Drawing.Graphics graphic = System.Drawing.Graphics.FromImage(returnImage))
{
graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphic.CompositingQuality = CompositingQuality.HighQuality;
graphic.SmoothingMode = SmoothingMode.HighQuality;
Rectangle to = new System.Drawing.Rectangle(
(int)Math.Round(destX), (int)Math.Round(destY),
destWidth, destHeight);
Rectangle from = new System.Drawing.Rectangle(0, 0, image.Width, image.Height);
graphic.DrawImage(image, to, from, System.Drawing.GraphicsUnit.Pixel);
return returnImage;
}
}
/// <summary>
/// Converts System.Drawing.Image to a byte array.
/// </summary>
/// <param name="image">The image.</param>
/// <returns>a raw byte array</returns>
public static byte[] ToByteArray(this System.Drawing.Image image)
{
using (var ms = new MemoryStream())
{
image.Save(ms, image.RawFormat);
return ms.ToArray();
}
}
/// <summary>
/// Scales an image byte array to fill.
/// </summary>
/// <param name="image">The image.</param>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="fitAspect">if set to <c>true</c> [fit aspect].</param>
/// <returns>a raw byte array of the scaled image</returns>
public static byte[] ScaledToFill(
this byte[] image,
int width = 196,
int height = 196,
bool fitAspect = true)
{
using (var ms = new MemoryStream(image))
{
return Image.FromStream(ms).ScaledToFill(new Size(width, height)).ToByteArray();
}
}
}
Now we can build out the list of images to generate, and get to work!
// Pick a sample image off the desktop
var img = @"C:\Users\admin\Desktop\imageToResize\LaunchImage.png";
var fi = new FileInfo(img);
using (var image = System.Drawing.Image.FromFile(img, true))
{
var sizes = new ContentAsset[] {
new ContentAsset(){ Size = new System.Drawing.Size(1242, 2688),
Orientation="portrait", Extent="full-screen", Scale=3, SubType="2688h",
MinimumSystemVersion ="12.0", Device ="iPhone Xs Max" },
new ContentAsset(){ Size = new System.Drawing.Size(2688, 1242),
Orientation="landscape", Extent="full-screen", Scale=3, SubType="2688h",
MinimumSystemVersion ="12.0", Device ="iPhone Xs Max" },
new ContentAsset(){ Size = new System.Drawing.Size(828, 1792),
Orientation="portrait", Extent="full-screen", Scale=2, SubType="1792h",
MinimumSystemVersion ="12.0", Device ="iPhone Xr Max" },
new ContentAsset(){ Size = new System.Drawing.Size(1792, 828),
Orientation ="landscape", Extent="full-screen", Scale=2, SubType="1792h",
MinimumSystemVersion ="12.0", Device ="iPhone Xr Max" },
new ContentAsset(){ Size = new System.Drawing.Size(1125, 2436),
Orientation="portrait", Extent="full-screen", Scale=3, SubType="2436h",
MinimumSystemVersion ="11.0", Device ="iPhone X" },
new ContentAsset(){ Size = new System.Drawing.Size(2436, 1125),
Orientation="landscape", Extent="full-screen", Scale=3, SubType="2436h",
MinimumSystemVersion ="11.0", Device ="iPhone X Landscape" },
new ContentAsset(){ Size = new System.Drawing.Size(1242, 2208),
Orientation="portrait", Extent="full-screen", Scale=3, SubType="736h",
MinimumSystemVersion ="8.0", Device ="iPhone 6s Plus - 8 Plus" },
new ContentAsset(){ Size = new System.Drawing.Size(2208, 1242),
Orientation="landscape", Extent="full-screen", Scale=3, SubType="736h",
MinimumSystemVersion ="8.0", Device ="Retina HD 5.5" },
new ContentAsset(){ Size = new System.Drawing.Size(750,1334), Extent="full-screen",
Scale =2, SubType="667h", MinimumSystemVersion="8.0", Device ="iPhone 6s - 8" },
new ContentAsset(){ Size = new System.Drawing.Size(640,960),
Orientation="portrait", Extent="full-screen", Scale=2,
MinimumSystemVersion ="7.0", Device ="iPhone 4, 4s" },
new ContentAsset(){ Size = new System.Drawing.Size(640,1136),
Orientation="portrait", Extent="full-screen", Scale=2, SubType="retina4",
MinimumSystemVersion ="7.0", Device ="iPhone 5, 5c, 5s" },
new ContentAsset(){ Size = new System.Drawing.Size(320,480),
Orientation="portrait", Extent="full-screen", Scale=1, Device ="iPhone 1g-3Gs" },
new ContentAsset(){ Size = new System.Drawing.Size(640,960),
Orientation="portrait", Extent="full-screen", Scale=2, Device ="iPhone 4, 4s" },
new ContentAsset(){ Size = new System.Drawing.Size(640, 1136),
Orientation="portrait", Extent="full-screen", Scale=2, SubType="retina4", Device ="Retina 4" }
/// No longer used
/*
new ContentAsset(){ Size = new System.Drawing.Size(768,1024), Device ="iPad, iPad 2, Mini" },
new ContentAsset(){ Size = new System.Drawing.Size(1024,768), Device ="iPad Landscape" },
new ContentAsset(){ Size = new System.Drawing.Size(1536,2048), Device ="iPad Retina" },
new ContentAsset(){ Size = new System.Drawing.Size(2048,1536), Device ="12.9\" iPad Pro" },
*/
};
foreach (var size in sizes)
{
var newImage = ScaledToFill(image, size.Size);
try
{
newImage.Save(Path.Combine(fi.DirectoryName,
"LoadingX_" + size.Size.Width + "w" + size.Size.Height + "h" + fi.Extension));
}
catch { }
}
// This matches the Contents.json file generated above
}
The entire output directory can be zipped, then unzipped into the appropriate .loadingxcassets folder. XCode will recognize it as a loading image - with every component populated correctly.