Monkeypatching Compatibility to Older iOS Versions

The Inevitability of Progress

Apple wants all of its users to be on the latest iOS version, and it wants all third-party developers to be quick to adopt new APIs, quick to remove deprecated APIs, and quick to drop support for older iOS versions.

While, historically, iOS adoption rates have been rather quick, lately this has not been the case.

For the independent developers who rely on iOS app sales, this isn’t much of a problem. People who buy apps have spare money. People with spare money are on the latest iPhones. The latest iPhones typically have the latest OS. Small, independent software developers (the kind that Apple caters to – I may complain about this in another article) do not have to worry so much about old OS compatibility.

However, enterprise software does not always have the luxury of dropping support for older operating systems and devices.


The Snag

Mobile enterprise software typically comes free with an existing suite of software. Enterprise software may have peripherals that only work on older devices. Enterprise software may target a group of users who do not typically update frequently.  And, to add to all of this, the company may still be ramping up mobile development and only have a few experts, because mobile is not the core business.

For example, let’s say you make inventory software and sell it to various organizations. Your inventory software uses barcoding. The barcoding landscape for Apple mobile devices is complex. Camera barcode recognition is future-safest, but slow. If you’ve walked into an Apple store, you may have noticed they have ‘sleds’ that provide barcoding capabilities to an i-device. The sleds are form-fitting. When the next iPhone or iPod Touch comes out, the sleds must be replaced. Not much of a problem for Apple – but a huge pain for you and your customers.

If you sell an integrated healthcare record database to a large healthcare provider – the doctors will typically have the latest and greatest, but many patients might still be on iOS 6 or earlier due to having an older device. Because your customer is the organization, you are accountable to them, which makes you twice as accountable to the actual users.


Potential Solutions

1. Multiple App Versions

Apple’s AppStore supports only one version of your application* per application name.  You could place multiple versions of your app on the store with different names supporting backwards compatibility to different versions.  Except forking code cascades into tons of extra work for your developers and testers, and strange issues for end-users, and when they report them will they report the right version?

*The AppStore does support multiple versions insofar as you have the option of offering an older version that supported the older iOS version a user might have.  You can’t fix any bugs with these older versions or update them at all.

2. Screw It!

Drop iOS versions as soon as possible.   As noted above, this isn’t a route that many companies in this position can take.

3. Code For The Oldest

The old code doesn’t stop working on new iOS versions, so you can continue to use UIAlertView in iOS 8 or fixed font sizes in iOS 7.

A problem here is that the moment you update your target, you have a million deprecation warnings cluttering your build and causing serious compiler warnings to be ignored.  A rough check shows that I am curating over 200 view controller subclasses in one application.  We do not have enough developers and testers to prioritize a wide-scale fix.  The testers will ask why we’re trying to break everything, the managers will ask why we’re wasting everyone’s time on non-issues, and the developers aren’t all iOS experts who see this as important or can do this easily, either.

Another problem is that it prevents developers from learning the new APIs sooner, and not everyone will simultaneously, magically get the memo to use the new method when they can.

4. Code For Both

a. Riddle your code with branches…

Most answers on Stack Overflow suggest the following. It’s more explicit, but you end up with duplicate code paths and sometimes ugly logic. If you have 1,000 places, it’s simply not feasible.

if([UIAlertController class]) {
    // use UIAlertController
} else {
    // use UIAlertView

b. Riddle your code with nonstandard wrappers…

You can write your own infrastructure that calls the appropriate API on each platform. In this case, you run the risk of other developers not using the wrappers, or not understanding the code.

MySpecialAlertController *alert = ...


The Technical Issue

For those who aren’t familiar with iOS development, Apple adds new APIs with new SDKs.  When you update your project to the new SDK, you can use the new APIs and everything compiles fine.  However, when you attempt to run on an older iOS version, at best the code might not work and at worst it will fail.

New SDK on Old iOS Rules

  • Enums – enum values can be used, but may not be supported by the APIs they’re being inserted into
  • Methods – when called, an exception will be raised
  • Classes – references to classes will return nil, and consequentially allocations will be nil
  • Global variables – references to strings will cause a runtime error due to dereferencing a null variable

Again, everything compiles fine, but when it runs, you’re screwed.


Compatibility Layers – a better way?

Could we monkeypatch a compatibility layer so that we can write against the latest API while still using the old API on the old platform?  Let’s explore the types of things that get added to the SDK.

Enums and other header-defined things

These are fine, by definition.  They exist solely in a header file and do not get compiled into anything different.

Inline functions, enums, defines, etc, are all good to go.


As noted, if you call a method that does not exist on an older iOS version, it will throw an exception.  A simple, elegant solution to this is to swizzle.  The Objective-C runtime is runtime-modifiable – you can add methods if they do not exist.  For example:

@implementation UITableViewCell (SDK8Compatibility)
+ (void)load {
    if(![self instancesRespondToSelector:@selector(setSeparatorInset:)]) {
        Method impMethod = class_getInstanceMethod(cls, @selector(sdk7_setSeparatorInset:));
        IMP imp = method_getImplementation(impMethod);
        const char *impType = method_getTypeEncoding(impMethod);
        return class_addMethod(cls, @selector(setSeparatorInset:), imp, impType);

- (void)sdk7_setSeparatorInset:(UIEdgeInsets)insets {
    if(!UIEdgeInsetsEqualToEdgeInsets(insets, UIEdgeInsetsZero)) {
        NSLog(@"warning: iOS6 cannot have separator insets");

Pretty cool! Sometimes it’s not that easy, but it’s generally do-able.

Class methods require a different set of code, but keep in mind that Classes are Objects, and anything you do to an object you can do to a class.

Global Variables

Now, you may have added +[UIFont preferredFontForTextStyle:], but you also need UIFontTextStyleHeadline.  This is defined in a header as:

UIKIT_EXTERN NSString *const UIFontTextStyleHeadline NS_AVAILABLE_IOS(7_0);

I’ll show the hard fix later, but the easy, safe fix is to abuse the pre-processor:

#define UIFontTextStyleHeadline (&UIFontTextStyleHeadline != NULL ? UIFontTextStyleHeadline : @"UICTFontTextStyleHeadline")

It’s not the prettiest… but it accomplishes its goal. Note that the address of the variable is null in the case of iOS 6.


This is where things get complicated.  You can add an arbitrary class to the Objective-C runtime at runtime, but it won’t be compiler-visible.  The reason for this is that the following code is not equivalent.

Class clsA = [UIAlertController class]; // compiler reference
Class clsB = NSClassFromString(@"UIAlertController"); // from ObjC runtime

Let’s handle the simple case first.

Adding a Class at Runtime

The first step is to create the class you want to pose as the class you want.  Because Objective-C supports duck typing, code shouldn’t care as long as it works properly.  If it looks like a duck and quacks like a duck, it’s a duck.

The second step is to add it to the runtime:

if(NSClassFromString("UIAlertController") == Nil) {
    objc_allocateClassPair([KZUIAlertController class], "UIAlertController", 0);
    //NSClassFromString now returns the class we registered

Cool!  Now let’s get back to the hard part.

Compile-time references

Question: What does the compiler actually do when you have the following?

Class clsA = [UIAlertController class];

Answer: it is passing a message – “class” – to an object.  UIAlertController, in this context, is not a type (as in a declaration) but a special variable.  It is a magic compiler variable that you can’t manipulate in code, and it contains a reference to a Class object.  Similarly, for global variables such as UIFontTextStyleHeadline, the variable itself holds the reference to the NSString*, but the reference to the variable exists elsewhere.  Like the variable, a smart developer might have tried to solve the class problem with the preprocessor, but he would have soon realized he could no longer declare UIAlertController variables of that type.

#define UIAlertController NSClassFromString(@"KZUIAlertController")
UIAlertController *variable; // compiler error on preprocessor expansion
variable = [UIAlertController new]; // no compiler error here

To solve this problem, we must pinpoint the difference between running on the older iOS and the latest. Your compiled app binary is identical between the two, so there’s something else going on.


Linking and Dynamic Loading

Obviously, these hidden variables exist.  You just can’t reference them directly in your compiled code.

At compile time, the static SDK8 library is added to your program – it’s basically a shell.  The actual functional pieces then exist on whatever device (or simulator) you are deploying to as dynamically-linked library.  The dynamic library is loaded, your variables are populated, and everything is happy.

In the case of an older iOS version, the slots are there, but they aren’t populated because the dynamic library that exists on the older device does not have those symbols.

Somewhere in your running application there is a table that says “UIAlertController” = null.

Mach-O, where art thou?

If you are interested in this type of thing, you might already know that compiled binaries have very particular formats with different bits to them.  The format for iOS and OSX applications is Mach-O (Mach being the kernel and O standing for object).

A Mach-O file reads like a series of commands, and each command describes something about the file.  The file is broken into segments, and each segment can have sections.

We find our UIAlertController class variable somewhere in the __DATA segment under the __objc_classrefs section.  The section is just an array of pointers to classes.  You can test this by using the following code to replace blank classes and see what happens in your own code!

int printClassPointers() {
    Dl_info dlinfo;

    if (dladdr(printClassPointers, &dlinfo) == 0 || dlinfo.dli_fbase == NULL) {
        return 0;
    uint32_t magic = *((uint32_t*)dlinfo.dli_fbase);

    unsigned long size;
    void **classes;

    if(magic == MH_MAGIC_64) {
        const struct mach_header_64 * header = dlinfo.dli_fbase;
        classes = (void**)getsectiondata(header, SEG_DATA, "__objc_classrefs", &size);
        //size = sect->size;
    } else if(magic == MH_MAGIC) {
        const struct mach_header * header = dlinfo.dli_fbase;
        classes = (void**)getsectiondata(header, SEG_DATA, "__objc_classrefs", &size);
    } else {
        return 0;

    long count = size / sizeof(Class);
    for(int i = 0; i < count; i++) {
        Class cls = (__bridge Class)classes[i];
        NSLog(@"%@ - %@ - %p", @(i), [cls description], &classes[i]);
        if(!cls) {
            // this is our problem! set classes[i] = whatever you want here to play around
    return 0;

We have the variable, but we don’t have its name!  If two classes were nil, we wouldn’t be able to tell their entries apart.


It’s all in the loader

The Mach-O format, aside from having segment commands, has a dynamic load command.  The dynamic load command itself points to a particular segment that contains loading bytecode.  The bytecode, through much intense staring, eventually gives the location of the class variable.  If you’re interested in more Macho-O details, check out MachOView.

It turns out that the dynamic load area containing “binding info” is where we find our mapping of symbol names to hidden variables.  We may then arbitrarily populate these values such that [UIAlertController class] actually does work on an iOS 7 device.

This is a bit involved, so please refer to my code linked at the end.

Make a new library instead?

Since the problem is in the loader, can we use the standard route to add symbols? Can we create a new library to impersonate UIKit?
This was my colleague’s first instinct. Unfortunately, this causes linker errors with symbol conflicts.


Parting Words and Example Code

I recommend using swizzling and defines in production to future-proof code – it’s something I’ve done and has worked well.  I have a large team of developers who cannot focus all of their time and energy on keeping up with the i-joneses.

I am not quite comfortable recommending modifying the loader, at this point, as it seems like a much more fragile mechanism. However, it’s a fascinating concept, and I think that if some brave soul proves its reliability in production, it would be great for there to be a compatibility pack project for iOS.

My code may be found on GitHub here.

Thanks for reading.

Using Google’s maps with MapKit

I had a problem. Apple’s map satellite data is old. Google’s is new. It would probably be violating either of their agreements, but this is how to replace the MapKit tiles with Google’s:

    NSString *template = @"{x}&y={y}&z={z}";
    MKTileOverlay *overlay = [[MKTileOverlay alloc] initWithURLTemplate:template];
    overlay.canReplaceMapContent = YES;
    [_mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];

- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay {
    if([overlay isKindOfClass:[MKTileOverlay class]]) {
        MKTileOverlayRenderer *v = [[MKTileOverlayRenderer alloc] initWithOverlay:overlay];
        return v;

Generate all your iOS App Icons with SVG and Inkscape

Let’s say you have Inkscape installed – a great, free vector-graphics program.

Now let’s also say you have an SVG icon that you want to rasterize for the AppStore and Apple’s million different icon size requirements.

And let’s also assume that you realize that simply resizing the rasterized image could potentially cause aliasing/blurring/etc.

Well, have I got the command line for you!
ruby -e '[58,120,29,40,80,76,152,1024].each { |x| `/Applications/ --export-png icon#{x}.png -w #{x} icon.svg` }'

Why REST APIs aren’t that great

You’re a developer. You’re using to simple RPCs (Remote Procedure Calls) to communicate with your database server, which is behind a HTTP server. And someone tells you “Hey, you should make REST web services.”

You spend a week researching and crafting up your first RESTful API to replace your existing API… and find yourself really no better off than when you started, except maybe the buzzword junkie you work with is super-happy now.  What went wrong?

In theory, REST means one thing. In practice, it means another.

Chances are, the API you had was mostly, technically RESTful to start with.  You have your GET, CREATE (POST), UPDATE (PUT/PATCH), and DELETE custom-application methods, and you didn’t have to think nearly half as hard about designing it.

The major difference is that REST must be thought of in terms of acting on resources, and I would argue that it favors flat, not nested, resources.  Some resources are incredibly complicated and will require much more design than previously.

Surprise!  You are now tied to HTTP.

As it stands, REST, in practice, requires intimate knowledge of HTTP workings: methods, query parameters, response codes, reason phrases, content types, message headers – not to mention, a serialization format (or two, or three…) for the actual payload.

This is OK if you’re building a web application and you have a team full of web-conscious developers.  But what if you are building a mobile application?  What if you have a legacy thick client that connects directly with TCP?  Now your developers have to know several languages and formats to get things done.

What if your developers are not web-experienced?  It’s hard enough getting them on the same page about web service design, and now we cloud the waters with this new way of thinking.

Yes, everything these days should be stateless, and most things should be done over the web, but designing your API around the web just means carrying that baggage to every other transport layer.

Programming Shouldn’t Be About The Transport Layer

Programming is about making a product that works (and hopefully works well).  How that is accomplished should be expedient and robust – REST is an esoteric exercise in HTTP-land.  I am all about esoteric exercises, but I cannot seriously justify them on someone else’s dollar.

To make a ‘pure’ REST API you must do a lot more architecting and churn out a lot more code that you possibly will never use.  When your client could have had their product a month ago, is it worth it?  How do you explain the extra time to your manager?  How do you explain the overhead to junior developers who will most likely struggle with the concept?


REST is great when you want a discoverable API.  It’s also great for forcing a particular way of thinking onto developers (arguably, a good developer would have already made the right decisions).

The Bottom Line

If your consumer is you, do whatever makes the most sense.  If your consumer is someone else, that’s when you should consider REST.



PHP – parsing multipart/form-data the correct way when using non-POST methods

If you are here, then you probably are familiar with the problem.  You want to be a good netizen and make a RESTful web API in PHP, but you want it to be robust and handle typical POST data from other HTTP methods – namely, PUT, PATCH, and DELETE.

This helpful walkthrough of making a REST api in PHP suggests using parse_str.  parse_str will only work on application/x-www-form-urlencoded data – meaning, the other common MIME type, multipart/form-data, is not supported.  What does this mean?  Well, complex data cannot be transmitted using simple key-value coding – particularly, file uploads.

So you put on your programming hat and you have these steps already:

  2. If not POST, check $_SERVER[‘CONTENT_TYPE’]
  3. If application/x-www-form-urlencoded, parse_str
  4. If multipart/form-data, handle multipart (but how?!)
  5. If anything else, treat it as a binary stream upload.

That is exactly where I was.  I could have given up, which would have been the smart thing to do, but instead I banged my head against it until it worked.

At this point, let’s break and talk about testing.  You need software to test your API with otherwise you’ll go mad.  I found RESTClient.

Back to the task at hand – you may have noticed that checking CONTENT_TYPE was annoying since it sometimes has more than just the content type (charset).  For file uploads, we will need to read name and filename from Content-Disposition.  To handle that, I’ve come up with the following little overly-complicated code:

function HttpParseHeaderValue($line) {
    $retval = array();
    $regex  = &lt;&lt;&lt;'EOD'

    $matches = null;
    preg_match_all($regex, $line, $matches, PREG_SET_ORDER);

    for($i = 0; $i &lt; count($matches); $i++) {
        $match = $matches[$i];
        $name = $match['name'];
        $quotedValue = $match['quotedValue'];
        if(empty($quotedValue)) {
            $value = $match['value'];
        } else {
            $value = stripcslashes($quotedValue);
        if(empty($value) &amp;&amp; $i == 0) {
            $value = $name;
            $name = 'value';
        $retval[$name] = $value;
    return $retval;

Great! Step 1 achieved! We can now go back and use this on CONTENT_TYPE. If you’re like me, you probably already saw some incorrect or ugly answers on StackOverflow. One involved a regex that would load copious amounts of data into memory, another was less flawed but more on-track.

If you’re anything like me, you’ll rewrite this anyway, but at least it’s a little better:

function HttpParseMultipart($stream, $boundary, array &amp;$variables, array &amp;$files) {
    if($stream == null) {
        $stream = fopen('php://input');

    $partInfo = null;

    $lineN = fgets($stream);
    while(($lineN = fgets($stream)) !== false) {
        if(strpos($lineN, '--') === 0) {
            if(!isset($boundary)) {
                $boundary = rtrim($lineN);

        $line = rtrim($lineN);

        if($line == '') {
            if(!empty($partInfo['Content-Disposition']['filename'])) {
                HttpParseMultipartFile($stream, $boundary, $partInfo, $files);
            } else {
                HttpParseMultipartVariable($stream, $boundary, $partInfo['Content-Disposition']['name'], $variables);
            $partInfo = null;

        $delim = strpos($line, ':');

        $headerKey = substr($line, 0, $delim);
        $headerVal = ltrim(substr($line, $delim + 1));
        $partInfo[$headerKey] = HttpParseHeaderValue($headerVal);

function HttpParseMultipartVariable($stream, $boundary, $name, &amp;$array) {
    $fullValue = '';
    $lastLine = null;
    while(($lineN = fgets($stream)) !== false &amp;&amp; strpos($lineN, $boundary) !== 0) {
        if($lastLine != null) {
            $fullValue .= $lastLine;
        $lastLine = $lineN;

    if($lastLine != null) {
        $fullValue .= rtrim($lastLine, "\r\n");

    $array[$name] = $fullValue;

function HttpParseMultipartFile($stream, $boundary, $info, &amp;$array) {
    $tempdir = sys_get_temp_dir();
    // we should technically 'clean' name - replace '.' with _, etc
    $name = $info['Content-Disposition']['name'];
    $fileStruct['name'] = $info['Content-Disposition']['filename'];
    $fileStruct['type'] = $info['Content-Type']['value'];

    $array[$name] = &amp;$fileStruct;

    if(empty($tempdir)) {
        $fileStruct['error'] = UPLOAD_ERR_NO_TMP_DIR;

    $tempname = tempnam($tempdir, 'php');
    $outFP = fopen($tempname, 'wb');

    $fileStruct['tmp_name'] = $tempname;
    if($outFP === false) {
        $fileStruct['error'] = UPLOAD_ERR_CANT_WRITE;

    $lastLine = null;
    while(($lineN = fgets($stream, 8096)) !== false &amp;&amp; strpos($lineN, $boundary) !== 0) {
        if($lastLine != null) {
            if(fwrite($outFP, $lastLine) === false) {
                $fileStruct['error'] = UPLOAD_ERR_CANT_WRITE;
        $lastLine = $lineN;

    if($lastLine != null) {
        if(fwrite($outFP, rtrim($lastLine, "\r\n")) === false) {
                $fileStruct['error'] = UPLOAD_ERR_CANT_WRITE;
    $fileStruct['error'] = UPLOAD_ERR_OK;
    $fileStruct['size'] = filesize($tempname);

The only remaining caveats that I can find:

  • HttpParseMultipartVariable does not populate indexed variables
  • HttpParseMultipartFile does not replicate PHP’s POST behavior 100%


Good luck and have fun!


NSTemporaryDirectory behavior changed in SDK 6

I had some code that looked like this to clear out the temp folder:

NSFileManager *fm = [NSFileManager defaultManager];

[fm removeItemAtPath:NSTemporaryDirectory() error:NULL];

[fm createDirectoryAtPath:NSTemporaryDirectory() withIntermediateDirectories:YES attributes:attr error:NULL];


This used to work.  The second call to NSTemporaryDirectory() now returns nil or /var/tmp or something equally different from my app’s temporary directory.

To fix, cache the value of NSTemporaryDirectory

How to change virtual machine settings in VMWare Player

My work is too cheap to spring for a full version of VMWare.  Well, that’s not that fair to say – the people who need it get it, and the rest of us get VMWare Player.  And honestly, I don’t need it since I rarely use VMs.

Point is: The memory and processor settings are locked in VMWare Player, and my virtual machine was lagging.


  • Turn off the VM completely.  No suspend or save state or any of that jazz.
  • Close VMWare Player
  • Open the VM’s .vmx file in a text editor
  • Change or add the numvcpus key to how many CPUs you want.  Ex: numvcpus = “4” (Keep in mind this cannot be more than your actual machine, I think)
  • Change the memsize key to how much memory you wish to allocate.  Ex: memsize = “4096”
  • Save
  • Re-open VM in vmware

This will work with any other settings you need, but those are probably the most useful.  Good luck!

Things that suck in Gnome 3 and how to fix them

I am an advocate of using bleeding-edge software, so I usually don’t turn-tail when things are broken.  I just beat my head against the wall until I find a suitable solution.  I usually post my solutions here so that other people don’t have to endure my pain.


1. Right click context menus automatically select the first item

The reason this happens is because you moved your mouse ever so slightly when you right-clicked, and the right mouse up event triggers selection.

Unfortunately, I’ve found no fix for this.  A lot of people had this symptom with an accessibility feature, but if you never turned on that feature then it’s irrelevant.

Solution: workaround – click faster when you right click.


2. Annoying hot-corners

The upper-left, lower-left, and lower-right corners of the screen activate poppy things that obstruct whatever it is I was doing.  When I want spotlight mode, I’ll purposefully trigger it.  And I use Cairo Dock for application/window management and notifications, so I don’t need the notification panel – in fact, it often obscures Cairo Dock.

I went to Gnome’s extensions website, and all the extensions that might do what I want are broken (because apparently the API changes so frequently, or another extension is interfering, etc).

I made my own extension, but the Gnome extension reviewers seem poised to reject it.

Solution: Install using gnome-tweak-tool or to .local/share/gnome-shell/extensions


3. New windows do not take focus

Since I disabled Gnome’s notifications completely (because they were popping over Cairo Dock), I am not notified of new windows.  Plus, when I open a new window, damnit I want it on top.

This one’s easy.

Solution: Open dconf-editor.  Check


That’s all for now.  I’ll keep this updated as more things annoy me.

ModSecurity: Multipart parsing error: Multipart: Final boundary missing. (Wiki bot running on Dreamhost)

I was running a Wiki Bot on a Dreamhost hosted website.  This was the error I received:

ModSecurity: Multipart parsing error: Multipart: Final boundary missing.


Turning off “added security” in the Dreamhost Panel helped a little bit, but I’m still occasional errors.

How to move installed files and have them still work (Steam games, etc)


With SSD drives, this is going to become more and more of a problem until their capacity gets ramped up. We must manually select what we want on the drive to be fast, and put the other stuff on a cheap, large-capacity, mechanical drive. However, Windows and Windows applications make this very difficult.

For the purposes of this example, I will use Steam. Steam places games in “C:\Program Files (x86)\Steam\steamapps\common\”. Games can be pretty large. So when you get to the limit, you have to delete one of the installed games. For me, this is around 4 or 5 modern games, so it’s not very convenient. There’s a longstanding request on the Steam forums to address this functionality, because you can only install games to the drive Steam is on… or can you?

Windows Vista introduced symbolic links. They’re a feature that other operating systems have had for years. Prior versions of Windows could make “junction points”, and you can find that application if you’re using XP or lower, but for the purposes of this post we will talk about this new feature since junctions can cause unforeseen issues – particularly with system files.


A symbolic link allows you to map a directory or file in one place to another without any applications understanding that it isn’t in the place they think it is.

So let’s say I want to put Borderlands – a 12 GB game – on another drive because I won’t be needing fast loading times since I already beat it. I would move “C:\Program Files (x86)\Steam\steamapps\common\borderlands” to “D:\steam\borderlands”. Now, Steam cannot find it.

The next step is to open an Administrator command prompt (type cmd into the Start Menu search, right click on command prompt, and go to “Run as Administrator”).

mklink /D "C:\Program Files\ (x86)\Steam\steamapps\common\borderlands" "D:\steam\borderlands"

Now Steam will be able to find it! If new DLC comes out and you’ll be playing more often, delete the symbolic link (the folder in steamapps) and move the folder back where it came from.

At some point I might make an app to automate this. Enjoy!


  1. Move the folder elsewhere.
  2. mklink /D [FOLDER] [ACTUAL FOLDER]