Flavouring Objective-C iOS app with Storyboard - ios

I have a single-activity iOS app in Objective-C with Storyboard. The app has two build schemes, say Scheme 1 and Scheme 2. The View has just a couple of buttons. I want to differentiate the color of these button depending on the build scheme.
I am new in the iOS world, but I am having a hard time parametrising the Storyboard (e.g. colors, strings, etc.) depending on the build scheme. I am aware of this post, but I'd like something more explicit.
Thank you for your help.

Conditional compilation with Schemes isn't supported by Xcode. You'd need to maintain two targets instead for that, and that gets messy quickly.
To do this with Schemes you need to maintain two asset catalogs with the correct named colors and copy the correct one in at build time. The source .xcasset catalogs would NOT be added to your target.
You need to add a Run Script early on in the Build Phases section of your target.
Luckily the Scheme name is expressed via the CONFIGURATION environment variable. You could do something like this, your paths might vary:
# Copy over the appropriate asset catalog for the scheme
target=${SRCROOT}/Resources/Colors.xcassets
if [ "${CONFIGURATION}" = "Scheme 1" ]; then
sourceassets=${PROJECT_DIR}/Scheme1.xcassets
else
sourceassets =${PROJECT_DIR}/Scheme2.xcassets
fi
if [ -e ${target} ]; then
echo "Assets: Purging ${target}"
rm -rf ${target}
fi
echo "Assets: Copying source=${sourceassets} to destination=${target}"
cp -r ${sourceassets} ${target}
Essentially you are replacing the compiled version of the asset catalog with one of your Scheme specific versions.
Strings would be another issue and you might be able to do this with the same technique for localised strings.
This all gets horrible very quickly and isn't recommended. Configuring the UI at runtime via code using the technique described in your referenced post would be a better bet.
You could construct a shim layer to protect your code from the Scheme changes.
e.g
#interface MyColors : NSObject
+ (UIColor *)buttonBackground;
#end
#implementation MyColors
+ (UIColor *)buttonBackground {
#if SCHEME1
return [UIColor colorNamed:#"scheme1ButtonBackground"];
#else
return [UIColor colorNamed:#"scheme2ButtonBackground"];
#endif
}
#end

Related

How to have 2 languages on debug and only 1 on productio?

On the app that I develop I want to have english and greek in debug mode (because I don't speak greek and the app is for Greece) and only Greek when when in release( because I have a requirement to only support Greek and no english when the app is released).
So, does anyone know how can I do this in iOS?
Solution 1
One easy thing I would do :
Create a new branch for production
Remove the English language in the project while keeping the files
Now every time you merge into prod, it will be up-to-date without the English language available.
NB: Don't forget to switch back to your dev branch while you are coding!
Solution 2
If you can't use another branch, you can create a Build Run Script that will get executed every time you build the app and/or use Fastlane to customise your deployment pipeline
After all I inspired a little from this thread, more exactly this answer.
And here is how my solution looks:
Removed #UIApplicationMain from AppDelegate and created main.swift and added this code:
if !SharedConstants.isDebugEnabled {
let supportedLanguages: [String] = ["el"]
UserDefaults.standard.set(supportedLanguages, forKey: "AppleLanguages")
UserDefaults.standard.synchronize()
}
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))
LATER EDIT:
Later I found the solution to add the below script in Build Phases to remove the english language and I don't need any of the workarounds above.
if [ $CONFIGURATION == 'Release' ]; then
rm -r "${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/en.lproj"
fi;

How to disable font smoothing in Xcode 9?

I've got a great programming font Deccy that only looks good with font smoothing (anti aliasing) disabled in Xcode. With Xcode 8 the following would do the trick:
defaults write com.apple.dt.Xcode NSFontDefaultScreenFontSubstitutionEnabled -bool YES
defaults write com.apple.dt.Xcode AppleAntiAliasingThreshold 24
But this no longer works with Xcode 9.
Would it be possible to disable font smoothing in Xcode 9?
Mischief managed?
Here's a screenshot of my Xcode 9 with Deccy at 13pt:
I believe the above is what you want. Here's stock Xcode displaying the same file:
But how?
I probed deep for a noninvasive way to accomplish this, and failed. As far as I can tell, the text rendering path in Xcode 9 very deliberately turns on font smoothing.
Before going any further, please file a feature request with Apple. It only takes a few minutes, and it's your best hope for an answer that that can be discussed in front of those with sound security instincts and strained cardiovasculature:
https://bugreport.apple.com/
I wrote an Xcode plugin. You might have heard that Xcode 9 uses code signing restrictions to forbid the loading of plugins. This is true, but a few mavericks press on, and tonight we ride with them.
Step one
There is a tool, update_xcode_plugins. You can use it to strip the code signature from your copy of Xcode, and with it the bundle-loading restriction:
$ gem install update_xcode_plugins
$ update_xcode_plugins --unsign
If you change your mind, you can do this to revert to (a backup copy, I think?) of signed Xcode:
$ update_xcode_plugins --restore
Step two
There is another tool, Alcatraz. It's a plugin manager for Xcode. I chose to install it because it provides a plugin which provides a project template for plugins. I followed these instructions to install Alcatraz, which boil down to this:
$ git clone https://github.com/alcatraz/Alcatraz.git
$ cd Alcatraz
$ xcodebuild
I launched Xcode, clicked through the dialog warning me about the new plugin, and then used the newly-added Window > Package Manager to install the "Xcode Plugin" template.
Step three
I made a project with this template.
As I write this, the "Xcode Plugin" template hasn't been updated to support Xcode 9. No worries. I ran this command to grab Xcode 9's compatibility UUID:
$ defaults read /Applications/Xcode.app/Contents/Info DVTPlugInCompatibilityUUID
I visited my new project's Info.plist and added that UUID to the DVTPlugInCompatibilityUUIDs array.
Then, I linked SourceEditor.framework into my project. That was a two-step process:
Visit the target's Build Settings. Add this to Framework Search Paths:
/Applications/Xcode.app/Contents/SharedFrameworks/
Visit the target's Build Phases. Add a new "Link Binary With Libraries" phase. Hit the plus. Navigate to the directory above (you can just press the / key and then paste the path in) and choose SourceEditor.framework. It should appear in the list. The project should still build.
Then, I made my plugin's .mm file look like this (I deleted the .h file, it's unneeded for this PoC):
#import <AppKit/AppKit.h>
#include <dlfcn.h>
extern void CGContextSetAllowsFontAntialiasing(CGContextRef, BOOL);
static void hooked_sourceEditorSetFontSmoothingStyle(CGContextRef ctx) {
CGContextSetAllowsFontAntialiasing(ctx, NO);
}
#interface NoAA : NSObject
#end
#implementation NoAA
+ (void)pluginDidLoad:(NSBundle *)plugin
{
NSArray *allowedLoaders = [plugin objectForInfoDictionaryKey:#"me.delisa.XcodePluginBase.AllowedLoaders"];
if (![allowedLoaders containsObject:[[NSBundle mainBundle] bundleIdentifier]])
return;
Class cls = NSClassFromString(#"SourceEditorScrollView");
NSBundle* bundle = [NSBundle bundleForClass:cls];
void *handle = dlopen(bundle.executablePath.fileSystemRepresentation, RTLD_NOW);
if (!handle)
return;
uint8_t* set_font_smoothing_fn = dlsym(handle, "sourceEditorSetFontSmoothingStyle");
if (!set_font_smoothing_fn)
goto fin;
void* set_font_smoothing_fn_page = (void*)((long)set_font_smoothing_fn & -PAGE_SIZE);
if (mprotect(set_font_smoothing_fn_page, PAGE_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC))
goto fin;
set_font_smoothing_fn[0] = 0xe9; // jmp
uint32_t* jmp_arg = (uint32_t*)(set_font_smoothing_fn + 1);
*jmp_arg = (uint32_t)((long)hooked_sourceEditorSetFontSmoothingStyle - (long)(jmp_arg + 1));
mprotect(set_font_smoothing_fn_page, PAGE_SIZE, PROT_READ | PROT_EXEC);
fin:
dlclose(handle);
}
#end
…I think the gotos add character. Basically, it just defines a function that takes a CGContextRef and turns off text antialiasing for it. Then, it overwrites the beginning of a function inside the SourceEditor framework which ordinarily configures antialiasing settings — don't need that anymore — to jump to our function instead. It does all of this in an incredibly unsafe way, so if something goes wrong, Xcode may politely crash.
Build and run the project, which automatically installs the plugin. Accept the addition of your plugin, and that's that.
What now?
If anything here doesn't work because I messed up, let me know. I'm not planning to roll this into an Alcatraz plugin myself, but you or anyone else should free to do so with credit (after filing a feature request with Apple).
Happy hacking!
If you 'live' in XCode and need a crisp rendering of this TrueType font, then editing XCode application defaults with PrefEdit.app, or defaults write com.apple.dt.Xcode.* has no effect.
Thinking outside the box you might be interested in the following to achieve crispyness all-over your Mac.
Since the Deccy font is best viewed at 12pt, it makes sense to raise the AppleAntiAliasingThreshold in the global domain to 13, the default for this setting is 4.
You can also suggest no AppleFontSmoothing.
defaults write -g AppleFontSmoothing -int 0
defaults write -g AppleAntiAliasingThreshold -int 13
In addition to these tweaks a bit more can be achieved in the Accessibility Preference pane in System Preferences: The Display has 2 checkmarks that you should try: 'Differentiate without color', and 'Increase contrast'.
"Beauty is in the eye of the beholder", I hope this helps.
Here are alternative steps that might work for you.
Try to find the com.apple.dt.Xcode.plist file under Macintosh HD/Library/Preferences.
Copy the file to desktop
Open file and add NSFontDefaultScreenFontSubstitutionEnabled to (Boolean)YES
add AppleAntiAliasingThreshold to (Number)24
Replace this file with preference file
Restart the system and Xcode
Note: For safer side keep backup of the original file.

release-debug configuration in React-Native

Currently in React-Native, according to the documentation, to build your iOS app for production, you need to :
change your scheme to Release
change your AppDelegate.m to load the correct bundle
change your Info.pList for ATS
This is a strong violation of 12 factor config recommandation, and it leads to mistakes being made in a continuous integration process.
RN does not provide either out-of-the box strategies to know the configuration environment in the JS code, leading to the existence of the package react-native-config, which does a great job already, but is not perfect (Xcode is not fully supported).
Why is it so? Is it because there are actually so few RN app in production today that nobody cares about this? Can we do better than react-native-config so that steps listed above are not required? I would like a command line that archives my app in the same way that I can run cd android && ./gradlew assembleRelease, without changing anything to my config.
EDIT:
Fastlane makes deployment a lot easier through its gym command (thank you Daniel Basedow). Apparently, the philosophy of Xcode is to call environments "schemes", only you cannot store variables in them, or know which scheme you're running in your code... Anyway, David K. Hess found a great way to export your scheme name in your Info.plist, and then in your Objective C code, which means I'm now able to chose my bundle according to the current scheme, and not touch my code.
Here is my code:
NSString *schemeName = [[[NSBundle mainBundle] infoDictionary] valueForKey:#"SchemeName"];
if ([schemeName isEqualToString:#"scheme1"]) {
jsCodeLocation = [NSURL URLWithString:#"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
} else if ([schemeName isEqualToString:#"scheme2"]) {
jsCodeLocation = [NSURL URLWithString:#"http://<my_local_ip_address>:8081/index.ios.bundle?platform=ios&dev=true"];
} else if ([schemeName isEqualToString:#"scheme3"]) {
jsCodeLocation = [[NSBundle mainBundle] URLForResource:#"main" withExtension:#"jsbundle"];
}
Now my problem is : I also want to know which scheme I'm running in my JS code. react-native-config's way is self-described as hacky, and overly complicated considering the fact the information is already in my Objective C code. Is there a way to bridge this information to my JS code?
Only knowing which scheme I'm running is not as good as being able to set environment variables, but at least I'll be able to switch between environments only by changing my scheme.
EDIT 2:
I managed to export my scheme to my JS code. I created a cocoa touch class with the following code:
// RNScheme.h
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
#interface RNScheme : NSObject <RCTBridgeModule>
#end
// RNScheme.m
#import "RNScheme.h"
#interface RNScheme()
#end
#implementation RNScheme
{
}
RCT_EXPORT_MODULE()
- (NSDictionary *)constantsToExport
{
NSString *schemeName = [[[NSBundle mainBundle] infoDictionary] valueForKey:#"SchemeName"];
NSLog(#"%#", schemeName);
return #{
#"scheme_name": schemeName,
};
}
#end
and then in my JS code:
import {NativeModules} from 'react-native'
let scheme = NativeModules.RNScheme.scheme_name
EDIT 3:
There is actually another way than using schemes. You can create new "configurations" ("Release" and "Debug" are called configurations) with the following steps (thanks CodePush):
Open up your Xcode project and select your project in the Project
navigator window
Ensure the project node is selected, as opposed to one of your
targets
Select the Info tab
Click the + button within the Configurations section and select
which configuration you want to duplicate
Then you can define keys with different values according to your configuration.
Select your app target
Chose Build Settings
Go to User-Defined section (at the bottom of the scroll area)
You can define constants with a different value according to your configuration (for instance API_ENDPOINT)
You can then reference this value in your Info.plist file :
Open your Info.plist file
Create a new value and give it a name (ApiEndpoint)
Give it the value $(API_ENDPOINT) or whatever name you gave to your constant
Now you can reference this value in your code using the code I gave you in my second edit to this question.
You can create one scheme per configuration to switch quickly from one to the other, or change the build configuration each time (option click on the run button).
In your AppDelegate you can use the correct bundle like this
#ifdef DEBUG
jsCodeLocation = [NSURL URLWithString:#"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:#"main" withExtension:#"jsbundle"];
#endif
When you do a release build, the DEBUG flag is not set. You can also use different files as your Info.plist depending on build type. There will probably be situations where you want an Xcode debug build with a production JS bundle or vice versa. In that case you need to touch code.
Building ios apps from command line can be a bit tricky. The problems you're describing are not specific to react-native but the Xcode build system. If you haven't already, check out fastlane especially the gym command. It is much simpler than using xcodebuild directly.
But you still have to define your schemes.

Setup different target for using different constant in Xcode

I know maybe a question is duplicated, but where I can check information. How to setup different targets to build with different bundle name etc.
Right now I know of course how to create different targets in Xcode, it is very simple to copy it from example from first target that was created automatically when I created project.
But I also have Constant.h and Constant.m files. I want to handle constants depend on which target I build for.
Let's say when build for target A I need to setup NSString const *toEmail = #"a#test.com", but in case if I build for the target B then toEmail = #"b#test";
Do I need to create two difference Constant files say ConstantA.m and ConstantB.m or maybe there is another best practice here. I don't want to recreate a wheel )
I setup Preprocessor Macro in target build settings for just one target. (For Example: TARGET_B)
And in code i check using
// Check if it's target B:
#if TARGET_B
NSString const *toEmail = #"b#test.com"
#else
NSString const *toEmail = #"a#test.com"
#end

iOS: How to add conditionality based on Project Target

I am building a project that will eventually be compiled to make 4 slightly different applications. The main change I have to make are image changes, but I also want to change a few UILabels as well.
I know that I could create multiple XIB files and modify them to be attached to each target, but I would is there a way to use a macro #define if to detect the name of the target?
Example: Project: Project1
Targets: TargetA, TargetB
#define if Target = TargetA{
label.text = #"This is targetA";
}
Is this possible?
Thanks!
I found an easy way to do this... you can access the bundle identifier which is unique for each target:
NSLog(#"%#", [[NSBundle mainBundle] bundleIdentifier]);
C preprocess macro will accomplish this feature.
In Xcode:
TARGET->Build Settings->Preprocess Marcos
And your source code:
#if DEBUG
// debug build
#else
// release build
#endif
This is already defined macro. You can define your own macros in all project targets.

Resources