26

Objective-C has an @available expression in XCode 9+ / LLVM 5+ that allows you to guard a block of code to at least a certain OS version so that it won't emit unguarded availability warnings if you use APIs that are only available on that OS version.

The problem is that this availability guarding is that it only works if it's the sole expression in the condition of an if. If you use it in any other context, you get a warning:

@available does not guard availability here; use if (@available) instead

So for example, it doesn't work if you try to AND the availability check with other conditions in the if:

if (@available(iOS 11.0, *) && some_condition) {
  // code to run when on iOS 11+ and some_condition is true
} else {
  // code to run when on older iOS or some_condition is false
}

Any code that uses iOS 11 APIs inside the if block or in some_condition still will generate unguarded availability warnings, even though it is guaranteed that those pieces of code can only be reached when on iOS 11+.

I could turn it into two nested ifs, but then the else code would have to be duplicated, which is bad (especially if it's lots of code):

if (@available(iOS 11.0, *)) {
  if (some_condition) {
    // code to run when on iOS 11+ and some_condition is true
  } else {
    // code to run when on older iOS or some_condition is false
  }
} else {
  // code to run when on older iOS or some_condition is false
}

I can avoid duplication by refactoring the else block code into an anonymous function, but that requires defining the else block before the if, which makes the code flow hard to follow:

void (^elseBlock)(void) = ^{
  // code to run when on older iOS or some_condition is false
};

if (@available(iOS 11.0, *)) {
  if (some_condition) {
    // code to run when on iOS 11+ and some_condition is true
  } else {
    elseBlock();
  }
} else {
  elseBlock();
}

Can anyone come up with a better solution?

user102008
  • 30,736
  • 10
  • 83
  • 104
  • Don't you also need to test against `some_condition` **also** in the `else` block of `if (@available...` ...? – Nicolas Miari Oct 27 '17 at 01:07
  • @NicolasMiari: nope – user102008 Oct 27 '17 at 01:12
  • I think a variation of your last solution is the best, using a method instead of a block so that the method definition can be AFTER all this conditional code. Just replace `elseBlock()` with `[self elseMethod];` – RobP Jun 27 '18 at 14:07

8 Answers8

14

You do what you always do when you have complex conditional code in the middle of a function that makes the flow complex: you hoist it into another function.

- (void)handleThing {
    if (@available(iOS 11.0, *)) {
        if (some_condition) {
            // code to run when on iOS 11+ and some_condition is true
            return;
        }
    }

  // code to run when on older iOS or some_condition is false
}

Or you hoist the check into generic code (see Josh Caswell's; it's better than how I originally wrote this).

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
7
#define SUPPRESS_AVAILABILITY_BEGIN \
    _Pragma("clang diagnostic push") \
    _Pragma("clang diagnostic ignored \"-Wunsupported-availability-guard\"")\
    _Pragma("clang diagnostic ignored \"-Wunguarded-availability-new\"")

#define SUPPRESS_AVAILABILITY_END \
    _Pragma("clang diagnostic pop")

#define AVAILABLE_GUARD(platform, os, future, conditions, codeIfAvailable, codeIfUnavailable) \
    SUPPRESS_AVAILABILITY_BEGIN \
    if (__builtin_available(platform os, future) && conditions) {\
        SUPPRESS_AVAILABILITY_END \
        if (@available(platform os, future)) { \
            codeIfAvailable \
        } \
    } \
    else { \
        SUPPRESS_AVAILABILITY_END \
        codeIfUnavailable \
    }

Usage:

AVAILABLE_GUARD(iOS, 11.0, *, true, {
    printf("IS AVAILABLE");
},
{
    printf("NOT AVAILABLE");
});

It works by using @available as a condition with additional optional conditions. Since you lose the ability to "guard", I suppressed the unguarded warnings but I also added an extra guard there to guard the rest of the code.. This makes it so you essentially lost nothing..

You get the guarding, you get the warnings gone and you get the extra conditions..

Brandon
  • 22,723
  • 11
  • 93
  • 186
2

How about wrapping the AND up in a function?

typedef BOOL (^Predicate)();

BOOL elevenAvailableAnd(Predicate predicate)
{
    if (@available(iOS 11.0, *)) {
        return predicate();
    }
    return NO;
}

Then you only have one branch:

if (elevenAvailableAnd(^{ return someCondition })) {
    // code to run when on iOS 11+ and some_condition is true
}
else {
    // code to run when on older iOS or some_condition is false
}

Or you could do without the Block if you prefer:

BOOL elevenAvailableAnd(BOOL condition)
{
    if (@available(iOS 11.0, *)) {
        return condition;
    }
    return NO;
}
jscs
  • 63,694
  • 13
  • 151
  • 195
  • 3
    But then it would not prevent unguarded availability warnings in the code in the if block that uses iOS 11-only APIs. – user102008 Oct 27 '17 at 00:16
  • Ah. Crap. I guess "file a radar" is probably the right answer then. Unfortunately that doesn't help anytime soon. – jscs Oct 27 '17 at 01:32
1
inline bool iOS13()
{
    if(@available(iOS 13, *))
        return true;
    else
        return false;
}

if(iOS13() && x == y)
    //...
Seva Alekseyev
  • 59,826
  • 25
  • 160
  • 281
  • 2
    But then it would not prevent unguarded availability warnings in the code in the if block (the `//...`) that uses iOS 13-only APIs. – user102008 Dec 12 '19 at 01:10
0

You could also simply use a flag:

BOOL doit = FALSE;

if (@available(iOS 11.0, *)) {
  if (some_condition) {
    doit = TRUE;
  }
}

if (doit) {
  // code to run when on iOS 11+ and some_condition is true
} else {
  // code to run when on older iOS or some_condition is false
}
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • 3
    But then it would not prevent unguarded availability warnings in the code in the `if` block that uses iOS 11-only APIs. – user102008 Oct 27 '17 at 00:15
0

The way I came up with that seems to change the layout of the code the least is:

do {
  if (@available(iOS 11.0, *)) {
    if (some_condition) {
      // code to run when on iOS 11+ and some_condition is true
      break;
    }
  }
  // code to run when on older iOS or some_condition is false
} while (0);

which is still ugly.

user102008
  • 30,736
  • 10
  • 83
  • 104
  • I think you may as well use `goto` at that point; it would be a lot clearer how control was jumping around. – jscs Oct 28 '17 at 16:58
0

You could do the else-code first and store the result somehow, and then do the if-code if needed. Something like this:

/**     
 first make default calculations, the 'else-code'
 */
id resultOfCalculations = ... ;

if (@available(iOS 11.0, *)) {
    if (some_condition) {
        /**
         code to run when on iOS 11+ and some_condition is true
         redo calculations and overwrite object
         */
        resultOfCalculations  = ... ;
    }
}

Then, of course, the calculation has to be done twice by the phone (if the conditions are true), but you don't have to write it twice.

May not be the most elegant solution, but if you want to keep it simple, this is an alternative.

turingtested
  • 6,356
  • 7
  • 32
  • 47
0

Defined

#define AT_AVAILABLE(...) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wunsupported-availability-guard\"") \
_Pragma("clang diagnostic ignored \"-Wunguarded-availability-new\"") \
__builtin_available(__VA_ARGS__) \
_Pragma("clang diagnostic pop")

Usage:

if (AT_AVAILABLE(iOS 11.0, *) && some_condition) {
    // code to run when on iOS 11+ and some_condition is true
}else {
    // code to run when on older iOS or some_condition is false
}

or

import this in PCH file

#pragma clang diagnostic ignored "-Wunsupported-availability-guard"
#pragma clang diagnostic ignored "-Wunguarded-availability-new"

Usage:

if (AT_AVAILABLE(iOS 11.0, *) && some_condition) {
    // code to run when on iOS 11+ and some_condition is true
}else {
    // code to run when on older iOS or some_condition is false
}

Yuxiang
  • 9
  • 2
  • It's great that you've offered a solution. Perhaps you can add some context, like why it is better than what the original poster had? Even if it's obvious, the principle "Don't make me think" should still apply. :) – AmitaiB May 10 '19 at 03:28