Xcode 5 & Asset Catalog: How to reference the LaunchImage?
Asked Answered
T

14

102

I am using Xcode 5's Asset Catalog, and I would like to use my LaunchImage as the background image of my home view (a pretty common practice to make the transition from 'loading' to 'loaded' look smooth).

I would like to use the same entry in the Asset Catalog to save space and not have to replicate the image in two different Image Sets.

However, calling:

UIImage *image = [UIImage imageNamed:@"LaunchImage"]; //returns nil
Terr answered 1/10, 2013 at 3:39 Comment(0)
P
83

This is the (almost) complete list of the LaunchImage (excluding the iPad images with no status bar):

Papaveraceous answered 18/11, 2013 at 10:11 Comment(8)
Anybody know the iPad images with no status bar?Syrup
@Mohamed Hafez: Pichirichi does actually have them included in his list. They are LaunchImage-Portrait~ipad.png, LaunchImage-Portrait@2x~ipad.png, LaunchImage-Landscape~ipad.png, and LaunchImage-Landscape@2x~ipad.png.Carving
What do numbers 700 and 800 mean?Undies
I catched it: it means iOS 7 & 8Undies
It's incredibly annoying that XCode will automatically create a file name for these image assets and make you jump through hoops to figure out how to directly access them...Lesleelesley
Why so many upvotes? The answer itself might not be incorrect technically but this seems the wrong way to go.Ashcraft
@Jonny, since you can access the launch image in a regular way this provides a workaround for the problem toblerpwn was facing.Papaveraceous
What about iphone-xs max and xr?Unionism
B
67
- (NSString *)splashImageNameForOrientation:(UIInterfaceOrientation)orientation {
    CGSize viewSize = self.view.bounds.size;
    NSString* viewOrientation = @"Portrait";
    if (UIDeviceOrientationIsLandscape(orientation)) {
        viewSize = CGSizeMake(viewSize.height, viewSize.width);
        viewOrientation = @"Landscape";
    }

    NSArray* imagesDict = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];
    for (NSDictionary* dict in imagesDict) {
        CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
        if (CGSizeEqualToSize(imageSize, viewSize) && [viewOrientation isEqualToString:dict[@"UILaunchImageOrientation"]])
            return dict[@"UILaunchImageName"];
    }
    return nil;
}
Balderas answered 6/1, 2015 at 11:43 Comment(13)
Works great. Clever and elegant approach to search main bundle's info dictionary for available launch images and then picking the one with matching resolution!Gallegos
Excellent approach. Congrats.Mulhouse
This wouldn't work with iOS 6 because the view height is smaller than the height of the screen with status bar.Faubert
I'm not arguing this solution is perfect - but as you can see from post bellow it can be made perfect, using the same dictionary its possible to know if status bar is visible during launch and if running on iOS and calculate the size appropriatelyBalderas
This is a brilliant idea, better than mine and also future proof, unless Apple change the structure of info.plist.Sinfonia
fingers crossed that they won't in near feature :-)Balderas
This is a very clever solution. I have multiple targets in my Xcode projects and just using the LaunchImage string does not always return the correct image. Thanks a lot.Belloc
I love you! You are my hero!Diabetic
how likely is it that Apple will change this in the future? in particular, the keys UILaunchImages. UILaunchImageOrientation, UILaunchImageSize, UILaunchImageNameAnaya
@Anaya Although it's possible they will be changed in the future, but your current apps on the App Store won't be affected anyway.Sinfonia
Brilliant idea though. But does not work for screens with opaque status bar. So needed to change self.view.bounds.size to [UIScreen mainScreen].bounds.sizeTull
Elegant approach; nice work. Solved the dilemma I was trying to solve.Unyoke
Great Solution. Small edit required: There's an implicit conversion from UIInterfaceOrientation to UIDeviceOrientation. Use UIInterfaceOrientationIsLandscape() instead.Roxannaroxanne
B
52

The LaunchImages are special, and aren't actually an asset catalog on the device. If you look using iFunBox/iExplorer/etc (or on the simulator, or in the build directory) you can see the final names, and then write code to use them - eg. for an iOS7-only iPhone-only project, this will set the right launch image:

NSString *launchImage;
if  ((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) &&
     ([UIScreen mainScreen].bounds.size.height > 480.0f)) {
    launchImage = @"LaunchImage-700-568h";
} else {
    launchImage = @"LaunchImage-700";
}

[self.launchImageView setImage:[UIImage imageNamed:launchImage]];

I put this into viewDidLoad.

This isn't really ideal, it would be great if Apple would give us a nice API to do this.

Brynn answered 9/10, 2013 at 16:5 Comment(5)
This worked for me, but I really wish there was a simpler way of referencing the launch image.Substructure
Maybe fixed in Xcode 5.0.2 - see below, seems to work for me simply to reference "LaunchImage.png"Forman
@Forman Would love if that was true! I've just tried on iphone5s/xcode5.0.2/ios7.0.4, [UIImage imageNamed:@"LaunchImage.png"] gives me nil.Brynn
@Brynn hmm. Maybe it requires a newly created project? This is a project created in Xcode 5.0.2, only change to defaults was "disabled ARC". It's working great :). I'll see if I can find anything else, but can't think what else I might have changedForman
I was trying similar code but using "Default" and "Default-568h" (the original resource file names). After looking inside the exported app bundle, I realized that Xcode changes the names to "LaunchImage-700*".Wallah
S
27

My app currently only supports iOS 7 and later.

This is how I reference the launch image from the asset catalog:

NSDictionary *dict = @{@"320x480" : @"LaunchImage-700",
                       @"320x568" : @"LaunchImage-700-568h",
                       @"375x667" : @"LaunchImage-800-667h",
                       @"414x736" : @"LaunchImage-800-Portrait-736h"};
NSString *key = [NSString stringWithFormat:@"%dx%d",
    (int)[UIScreen mainScreen].bounds.size.width,
    (int)[UIScreen mainScreen].bounds.size.height];
UIImage *launchImage = [UIImage imageNamed:dict[key]];

You can add more key value pairs if you want to support older iOS versions.

Sinfonia answered 15/9, 2014 at 21:5 Comment(2)
Note that starting with iOS 8, UIScreen.mainScreen.bounds is different depending on the current interface orientation. See https://mcmap.net/q/23452/-is-uiscreen-mainscreen-bounds-size-becoming-orientation-dependent-in-ios8Gui
Thanks for htis, any method to access App Icons?Selfmoving
M
10

Here a category on UIImage based on the solution provided by Cherpak Evgeny above.

UIImage+SplashImage.h:

#import <UIKit/UIKit.h>

/**
 * Category on `UIImage` to access the splash image.
 **/
@interface UIImage (SplashImage)

/**
 * Return the name of the splash image for a given orientation.
 * @param orientation The interface orientation.
 * @return The name of the splash image.
 **/
+ (NSString *)si_splashImageNameForOrientation:(UIInterfaceOrientation)orientation;

/**
 * Returns the splash image for a given orientation.
 * @param orientation The interface orientation.
 * @return The splash image.
 **/
+ (UIImage*)si_splashImageForOrientation:(UIInterfaceOrientation)orientation;

@end

UIImage+SplashImage.m:

#import "UIImage+SplashImage.h"

@implementation UIImage (SplashImage)

+ (NSString *)si_splashImageNameForOrientation:(UIInterfaceOrientation)orientation
{
    CGSize viewSize = [UIScreen mainScreen].bounds.size;

    NSString *viewOrientation = @"Portrait";

    if (UIDeviceOrientationIsLandscape(orientation))
    {
        viewSize = CGSizeMake(viewSize.height, viewSize.width);
        viewOrientation = @"Landscape";
    }

    NSArray* imagesDict = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];

    for (NSDictionary *dict in imagesDict)
    {
        CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
        if (CGSizeEqualToSize(imageSize, viewSize) && [viewOrientation isEqualToString:dict[@"UILaunchImageOrientation"]])
            return dict[@"UILaunchImageName"];
    }
    return nil;
}

+ (UIImage*)si_splashImageForOrientation:(UIInterfaceOrientation)orientation
{
    NSString *imageName = [self si_splashImageNameForOrientation:orientation];
    UIImage *image = [UIImage imageNamed:imageName];
    return image;
}

@end
Mulhouse answered 22/1, 2015 at 17:16 Comment(1)
imageNamed pushes image to system cache, but launch image sometimes is very huge, so it is in memory until cache flushes itMousey
I
9

@codeman's answer updated for Swift 1.2:

func splashImageForOrientation(orientation: UIInterfaceOrientation, size: CGSize) -> String? {
    var viewSize        = size
    var viewOrientation = "Portrait"

    if UIInterfaceOrientationIsLandscape(orientation) {
        viewSize        = CGSizeMake(size.height, size.width)
        viewOrientation = "Landscape"
    }

    if let imagesDict = NSBundle.mainBundle().infoDictionary as? [String: AnyObject] {
        if let imagesArray = imagesDict["UILaunchImages"] as? [[String: String]] {
            for dict in imagesArray {
                if let sizeString = dict["UILaunchImageSize"], let imageOrientation = dict["UILaunchImageOrientation"] {
                    let imageSize = CGSizeFromString(sizeString)
                    if CGSizeEqualToSize(imageSize, viewSize) && viewOrientation == imageOrientation {
                        if let imageName = dict["UILaunchImageName"] {
                            return imageName
                        }
                    }
                }
            }
        }
    }

    return nil

}

To call it, and to support rotation for iOS 8:

override func viewWillAppear(animated: Bool) {
    if let img = splashImageForOrientation(UIApplication.sharedApplication().statusBarOrientation, size: self.view.bounds.size) {
        backgroundImage.image = UIImage(named: img)
    }
}

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    let orientation = size.height > size.width ? UIInterfaceOrientation.Portrait : UIInterfaceOrientation.LandscapeLeft

    if let img = splashImageForOrientation(orientation, size: size) {
        backgroundImage.image = UIImage(named: img)
    }

}

Just what I needed, thanks!

Impend answered 22/4, 2015 at 9:8 Comment(0)
Z
7

I just wrote a general method to get the splash image name for iPhone and iPad (Landscape, Portrait), It worked for me, Hope It helps you as well. I wrote this with help of other SO answers, thanks @Pichirichi for whole list.

+(NSString*)getLaunchImageName
{

 NSArray* images= @[@"LaunchImage.png", @"[email protected]",@"[email protected]",@"[email protected]",@"[email protected]",@"LaunchImage-700-Portrait@2x~ipad.png",@"LaunchImage-Portrait@2x~ipad.png",@"LaunchImage-700-Portrait~ipad.png",@"LaunchImage-Portrait~ipad.png",@"LaunchImage-Landscape@2x~ipad.png",@"LaunchImage-700-Landscape@2x~ipad.png",@"LaunchImage-Landscape~ipad.png",@"LaunchImage-700-Landscape~ipad.png"];

UIImage *splashImage;

if ([self isDeviceiPhone])
{
    if ([self isDeviceiPhone4] && [self isDeviceRetina])
    {
        splashImage = [UIImage imageNamed:images[1]];
        if (splashImage.size.width!=0)
            return images[1];
        else
            return images[2];
    }
    else if ([self isDeviceiPhone5])
    {
        splashImage = [UIImage imageNamed:images[1]];
        if (splashImage.size.width!=0)
            return images[3];
        else
            return images[4];
    }
    else
        return images[0]; //Non-retina iPhone
}
else if ([[UIDevice currentDevice] orientation]==UIDeviceOrientationPortrait || [[UIDevice currentDevice] orientation] == UIDeviceOrientationPortraitUpsideDown)//iPad Portrait
{
    if ([self isDeviceRetina])
    {
        splashImage = [UIImage imageNamed:images[5]];
        if (splashImage.size.width!=0)
            return images[5];
        else
            return images[6];
    }
    else
    {
        splashImage = [UIImage imageNamed:images[7]];
        if (splashImage.size.width!=0)
            return images[7];
        else
            return images[8];
    }

}
else
{
    if ([self isDeviceRetina])
    {
        splashImage = [UIImage imageNamed:images[9]];
        if (splashImage.size.width!=0)
            return images[9];
        else
            return images[10];
    }
    else
    {
        splashImage = [UIImage imageNamed:images[11]];
        if (splashImage.size.width!=0)
            return images[11];
        else
            return images[12];
    }
 }
}

Other utility methods are

+(BOOL)isDeviceiPhone
{
 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
 {
     return TRUE;
 }

 return FALSE;
}

+(BOOL)isDeviceiPhone4
{
 if ([[UIScreen mainScreen] bounds].size.height==480)
    return TRUE;

 return FALSE;
}


+(BOOL)isDeviceRetina
{
 if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)] &&
    ([UIScreen mainScreen].scale == 2.0))        // Retina display
 {
    return TRUE;
 } 
 else                                          // non-Retina display
 {
     return FALSE;
 }
}


+(BOOL)isDeviceiPhone5
{
 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && [[UIScreen mainScreen] bounds].size.height>480)
 {
    return TRUE;
 }
 return FALSE;
}
Zipah answered 30/4, 2014 at 6:9 Comment(2)
There's actually a slight bug in this code for isDeviceiPhone4: [[UIScreen mainScreen] bounds] now changes based on what orientation you are in under iOS 8. You'll need to explicitly convert it to a portrait bounds doing something like: [screen.coordinateSpace convertRect:screen.bounds toCoordinateSpace:screen.fixedCoordinateSpace], but make sure to test if you are on iOS 8 first otherwise that will crash.Syrup
Thanks @Hafez for pointing that out, I will test it for iOS 8 and update the answer soon.Zipah
A
7

Swift version of Cherpak Evgeny's answer:

    func splashImageForOrientation(orientation: UIInterfaceOrientation) -> String {
        var viewSize = self.view.bounds.size
        var viewOrientation = "Portrait"
        if UIInterfaceOrientationIsLandscape(orientation) {
           viewSize = CGSizeMake(viewSize.height, viewSize.width)
           viewOrientation = "Landscape"
        }
        let imagesDict = NSBundle.mainBundle().infoDictionary as Dictionary<NSObject,AnyObject>!
        let imagesArray = imagesDict["UILaunchImages"] as NSArray
        for dict in imagesArray {
            let dictNSDict = dict as NSDictionary
            let imageSize = CGSizeFromString(dictNSDict["UILaunchImageSize"] as String)
            if CGSizeEqualToSize(imageSize, viewSize) && viewOrientation == (dictNSDict["UILaunchImageOrientation"] as String) {
                return dictNSDict["UILaunchImageName"] as String
            }
        }
        return ""
    }
Acanthoid answered 19/2, 2015 at 19:56 Comment(0)
F
5

Following @Pichirich's answer, I referenced my launchimage in InterfaceBuilder as:

"LaunchImage.png"

...and with Xcode 5.0.2, it's automatically plucking the appropriate image straight out of the Asset Catalog.

This is what I'd expect - except for Apple's viciously nasty move of silently renaming "Default.png" to "LaunchImage.png" :)

Forman answered 20/11, 2013 at 12:40 Comment(1)
One more thing should be noted. These images' sizes should be exactly like Apple recommends (320x480 for LaunchImage for iOS 5-6 iPhone 3GS for example), otherwise it would be nil after given initialisationNadabus
U
3

In the documentation there is clearly stated:

"Each set in an asset catalog has a name. You can use that name to programmatically load any individual image contained in the set. To load an image, call the UIImage:ImageNamed: method, passing the name of the set that contains the image."

Using Pichirichi's list helps to solve this inconsistency.

Uranie answered 25/4, 2014 at 18:56 Comment(5)
Note the "name of the set" part. Looking at my asset catalog, I have a set called "LaunchImage". To load the launch image, then, I called: UIImageView *myView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"LaunchImage"]]; Works great!Modish
No need to use Pichirichi's list (thought that's still awesome info to know) - just use the name of the asset catalog's "set".Modish
Well, for me this does not work for the Launch Image on Xcode 6.0.1 and iOS 8. The LaunchImage seems to be special as the images end up individually in the compiled app bundle and do not remain inside the xcasset bundle folder.Dimarco
What happens if there are two different asset catalogs containing identically named sets? How would [UIImage imageNamed:..] know which one to pick?Navigate
For me this doesn't work, XCode 6.0.1 iOS 7 iPod TouchIndusium
W
3

One can easily access Launch image by one line of code.

 UIImage *myAppsLaunchImage = [UIImage launchImage];

Please follow steps given below to achieve functionality depicted above.

Step 1. Extend UIImage class by creating a category & add following method to it.

+ (UIImage *)launchImage {
    NSDictionary *dOfLaunchImage = [NSDictionary dictionaryWithObjectsAndKeys:
                                    @"[email protected]",@"568,320,2,8,p", // ios 8 - iphone 5 - portrait
                                    @"[email protected]",@"568,320,2,8,l", // ios 8 - iphone 5 - landscape
                                    @"[email protected]",@"568,320,2,7,p", // ios 7 - iphone 5 - portrait
                                    @"[email protected]",@"568,320,2,7,l", // ios 7 - iphone 5 - landscape
                                    @"LaunchImage-700-Landscape@2x~ipad.png",@"1024,768,2,7,l", // ios 7 - ipad retina - landscape
                                    @"LaunchImage-700-Landscape~ipad.png",@"1024,768,1,7,l", // ios 7 - ipad regular - landscape
                                    @"LaunchImage-700-Portrait@2x~ipad.png",@"1024,768,2,7,p", // ios 7 - ipad retina - portrait
                                    @"LaunchImage-700-Portrait~ipad.png",@"1024,768,1,7,p", // ios 7 - ipad regular - portrait
                                    @"[email protected]",@"480,320,2,7,p", // ios 7 - iphone 4/4s retina - portrait
                                    @"[email protected]",@"480,320,2,7,l", // ios 7 - iphone 4/4s retina - landscape
                                    @"LaunchImage-Landscape@2x~ipad.png",@"1024,768,2,8,l", // ios 8 - ipad retina - landscape
                                    @"LaunchImage-Landscape~ipad.png",@"1024,768,1,8,l", // ios 8 - ipad regular - landscape
                                    @"LaunchImage-Portrait@2x~ipad.png",@"1024,768,2,8,p", // ios 8 - ipad retina - portrait
                                    @"LaunchImage-Portrait~ipad.png",@"1024,768,1,8,l", // ios 8 - ipad regular - portrait
                                    @"LaunchImage.png",@"480,320,1,7,p", // ios 6 - iphone 3g/3gs - portrait
                                    @"LaunchImage.png",@"480,320,1,7,l", // ios 6 - iphone 3g/3gs - landscape
                                    @"[email protected]",@"480,320,2,8,p", // ios 6,7,8 - iphone 4/4s - portrait
                                    @"[email protected]",@"480,320,2,8,l", // ios 6,7,8 - iphone 4/4s - landscape
                                    @"[email protected]",@"667,375,2,8,p", // ios 8 - iphone 6 - portrait
                                    @"[email protected]",@"667,375,2,8,l", // ios 8 - iphone 6 - landscape
                                    @"[email protected]",@"736,414,3,8,p", // ios 8 - iphone 6 plus - portrait
                                    @"[email protected]",@"736,414,3,8,l", // ios 8 - iphone 6 plus - landscape
                                    nil];
    NSInteger width = ([UIScreen mainScreen].bounds.size.width>[UIScreen mainScreen].bounds.size.height)?[UIScreen mainScreen].bounds.size.width:[UIScreen mainScreen].bounds.size.height;
    NSInteger height = ([UIScreen mainScreen].bounds.size.width>[UIScreen mainScreen].bounds.size.height)?[UIScreen mainScreen].bounds.size.height:[UIScreen mainScreen].bounds.size.width;
    NSInteger os = [[[[[UIDevice currentDevice] systemVersion] componentsSeparatedByString:@"."] objectAtIndex:0] integerValue];
    NSString *strOrientation = UIDeviceOrientationIsLandscape([[UIDevice currentDevice] orientation])?@"l":@"p";
    NSString *strImageName = [NSString stringWithFormat:@"%li,%li,%li,%li,%@",width,height,(NSInteger)[UIScreen mainScreen].scale,os,strOrientation];
    UIImage *imageToReturn = [UIImage imageNamed:[dOfLaunchImage valueForKey:strImageName]];
    if([strOrientation isEqualToString:@"l"] && [strImageName rangeOfString:@"Landscape"].length==0) {
        imageToReturn = [UIImage rotate:imageToReturn orientation:UIImageOrientationRight];
    }
    return imageToReturn;
}

Step 2. Above method should be working by adding following code also into same category of UIImage

static inline double radians (double degrees) {return degrees * M_PI/180;}

+ (UIImage *)rotate:(UIImage*)src orientation:(UIImageOrientation) orientation {
    UIGraphicsBeginImageContext(src.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    if (orientation == UIImageOrientationRight) {
        CGContextRotateCTM (context, radians(90));
    } else if (orientation == UIImageOrientationLeft) {
        CGContextRotateCTM (context, radians(-90));
    } else if (orientation == UIImageOrientationDown) {
        // NOTHING
    } else if (orientation == UIImageOrientationUp) {
        CGContextRotateCTM (context, radians(90));
    }
    [src drawAtPoint:CGPointMake(0, 0)];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
Witchhunt answered 21/11, 2014 at 11:12 Comment(1)
Whats the name for the iPhone X launch image now?Clubbable
J
2

I realize that this is not necessarily the best solution for everyone but the easiest (and least error-prone, IMHO) way to do this is by making a separate entry in your Images.xcassets catalog. I called it SplashImage.

When you go to add a new entry, make sure not to select "New Launch Image" as an option. Instead, select the generic "New Image Set". Next, open up the inspector and select the relevant options. If you're building for only retina devices, as I was, you can select the following:

image inspector

This will leave you with four entries (iPhone 4S, iPhone 5(s,c), iPhone 6, and iPhone 6 Plus).

images

The files corresponding the the images are as follows:

| Resolution (Xcode entry) | Launch Image name   |   Device         |
|--------------------------|---------------------|------------------|
| 1x                       | Default-750.png     | iPhone 6         |
| 2x                       | [email protected]      | iPhone 4S        |
| Retina 4 2x              | [email protected] | iPhone 5, 5s, 5c |
| 3x                       | Default-1242.png    | iPhone 6 Plus    |

Of course, after you've done this you can simply use [UIImage imageNamed:@"SplashImage"]

Juanajuanita answered 11/9, 2014 at 18:7 Comment(2)
Interesting idea, but it doesn't work on iPhone 6. It still loads the [email protected] image on iPhone 6 simulator.Sinfonia
Using this approach you should take care about set of launch images for landscape orientation as well.Nuke
S
2

With help of Pichirichi's answer I've implemented the following category (iOS 7+) : UIImage+AssetLaunchImage

It's actually little more than generating name on the fly, but probably will be helpful.

Sagitta answered 26/11, 2014 at 18:59 Comment(0)
D
0

Updated to latest Swift syntax (Swift 5)

   func splashImageForOrientation(orientation: UIInterfaceOrientation) -> String? {

    var viewSize = screenSize
    var viewOrientation = "Portrait"
    if orientation.isLandscape {
        viewSize = CGSize(width: viewSize.height, height: viewSize.width)
        viewOrientation = "Landscape"
    }
    if let infoDict = Bundle.main.infoDictionary, let launchImagesArray = infoDict["UILaunchImages"] as? [Any] {
        for launchImage in launchImagesArray {
            if let launchImage = launchImage as? [String: Any], let nameString = launchImage["UILaunchImageName"] as? String, let sizeString = launchImage["UILaunchImageSize"] as? String, let orientationString = launchImage["UILaunchImageOrientation"] as? String {
                let imageSize = NSCoder.cgSize(for: sizeString)
                if imageSize.equalTo(viewSize) && viewOrientation == orientationString {
                    return nameString
                }
            }
        }
    }
    return nil
}
Darius answered 15/4, 2019 at 10:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.