3

I inherited a large code base (iOS / Objective-C) that is used to build multiple mobile apps. The apps have similar, but not identical UI.

The apps share a lot of common features, but there are features only found in one app. Some features can be later added to other apps.

Apps have relatively independent life cycle. Releases are not simultaneous. It is common to have two or three versions of each app in development at the same time: hotfix for the current published version, next minor version, next major version. Features can move between versions. I.e features can be postponed to the later version or moved to the earlier one.

For now there is a separate scheme for each app. Code contains quite a lot of conditional compilation to separate features. Code changes often transferred between branches (develop -> master, develop -> releaseXYZ) using git cherry-pick and git revert commands.

How can I reduce the pain of maintaining all this? I would really like to simplify feature set management between different versions of the same app and between different apps.

EDIT: Thank you for your answers so far. I understand, that I need to split code base into some kind of modules. I would really appreciate if you tell me more how I can manage these modules efficiently / less painful.

4

I don't know how modular the code is, but in order to handle the complexity of all of this it might be useful to create API layers. The core library has the features/functions in all apps, with feature packs that can be included in each of the final apps. Essentially the dependency graph is intended to be a directed acyclic graph (DAG).

After organizing your features into modules, you may want to include extension points so that applications can inject functionality to take care of the things in the conditional compilation. That way your code won't break just because you make a change and then forget about the part in the condition.

In each specific app, you do all your customization and wiring together.

2

The other answers give good suggestions for architecture and project layout. I wanted to address this specific point:

Code contains quite a lot of conditional compilation to separate features.

I recently ran into this on a project I'm working on and improved it using the following techniques:

Use Constants

Some of our code has been quickly modified with directives and now looks like this:

#if <SOMETHING>
callSomeFunction(aConst);
#else
callSomeFunction(bConst);
#endif

This can by improved by making a single constant that’s defined differently for each path of the #if and only calling the function once, like this:

// At top of file
#if <SOMETHING>
const int kImportantVal = aConst;
#else
const int kImportantVal = bConst;
#endif
…
// In the actual code
callSomeFunction(kImportantVal);

Combining Sections

Often in our haste we simply sprinkle #if directives throughout the code, leaving functions looking like this:

#if SOMETHING
callA();
#else
callB();
#endif

callC();

#if SOMETHING
callD();
#else
callE();
#endif

if callD() and callE() are not dependent on the result of callC(), this can be rewritten as:

#if SOMETHING
callA();
callD();
#else
callB();
callE();
#endif

callC();

Refactored Functions

Functionality can be moved into functions which are grouped together within a single #if or other directive.

If we look at the above example, another way to do it would be the following:

#if SOMETHING
void callA()
{
    // ...whatever callA() above does;
}

void callD()
{
    // ...whatever callD() above does;
}
#else
void callA()
{
    // ...whatever callB() above does;
}

void callD()

{
    // ...whatever callE() above does;
}
#endif

// ...
callA();
callC();
callD();

Use Polymorphism

In object-oriented languages like C++ and Objective-C, it’s fairly simple to create an interface that is implemented by different objects in different builds. For example, if you had this code:

@interface SomeInterface
{
}

- (void)methodA;
@end

@implementation SomeInterface

-(void)methodA
{
#if SOMETHING
    doX();
#else
    doY();
#endif
}
@end 

You would instead make 2 classes - one for each branch of the #if SOMETHING. They might look like this:

@interface SomeInterfaceX : SomeInterface
{
}
@end

@implementation SomeInterfaceX
- (void)methodA
{
    doX();
}
@end

and

@interface SomeInterfaceY : SomeInterface
{
}
@end

@implementation SomeInterfaceY
- (void)methodA
{
    doY();
}
@end

You can do something similar in C++ with derived classes.

Categories

In Objective-C we have the option of adding a category only in the targets that need it. For example, in SomeClass we want one set of functionality in 2 targets, but there's some additional functionality that should only exist in 1 of the targets. As such, we take the methods that are only implemented in the 1 target and put them in a separate file containing a category that implements just those methods. That file is only compiled in the 1 target and not the other.

So you'd have something like:

@interface SomeClass {
    // ... class definition that is common to both targets
}
@end

@implementation SomeClass
// ... class implementation that is common to both targets
@end

Then in a separate file

@interface SomeClass (Additions)
// ... additional methods for target that needs them
@end 

@implementation SomeClass (Additions)
// ... implementation of addition methods for target that needs them
@end
1

If I were you, I'd factor out the common code into separate Xcode projects and compile them as a Framework or library. Then you can separate the different UIs into their own projects as well and include the common code in each one.

Since you also seem to have complexity issues with your SCM situation, I would consider separating all of the applications into their own repositories and the common code into its own as well. You can use something like Cocoapods or Carthage to include the common Framework into each app.

1

You could:

  1. Create one GIT repository containing features and maybe dummy screens that use those features;
  2. Each app will have a separate GIT repository, forked from the repo from Step 1 (since each app evolves differently, share common features, and for other features it slightly modifies them);
  3. Whenever you see appropriate, during each apps maintenance, you could send feature's commits to the base repository, in order to be available to the other apps. The base repo is just to aid you in the maintenance of common features.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.