Symbol not found errors in universal apps

I have posted a few times about universal apps but just when I think I have understood everything another problem pops up to catch me out (or in this case fails to pop up…). The scenario is as follows: a settings button added to a toolbar is used to present a view controller in a popover view. The action method for the button is as follows:

- (void)showSettings:(id)sender {
  if (popoverSettings == nil) {
    SettingsViewController *viewController = [[SettingsViewController alloc]
                            initWithNibName:@"SettingsView" bundle:nil];
      
    // Create a Navigation controller for the settings
    UINavigationController *navController = [[UINavigationController alloc]
                            initWithRootViewController:viewController];
      
    // Create a popover controller to show the navigation controller
    self.popoverSettings = [[UIPopoverController alloc]
                           initWithContentViewController:navController];
      
    [navController release];
    [viewController release];
  }
  
  [popoverSettings presentPopoverFromBarButtonItem:sender
                   permittedArrowDirections:UIPopoverArrowDirectionAny
                   animated:YES];
}

There is nothing really special going on here. I have omitted some code that deals with ensuring that only one popover controller is displayed at a time for brevity. The first time the button is used the settings view controller is created and used to initialise a navigation controller which then in turn acts as the content for a UIPopoverController.

I should also mention that this code is contained in an iPad only view controller that will only be loaded when the application is launched on the iPad. So I was at first surprised when launching this app on an iPhone device running 3.1.3 to get a runtime error:

dyld: Symbol not found: _OBJC_CLASS_$_UIPopoverController
** Referenced from: /var/mobile/Applications/BE668E24-D713-4F84-A959-926CE06B425B/MyApp.app/MyApp**
** Expected in: /System/Library/Frameworks/UIKit.framework/UIKit**

So what is going wrong? The problem is that when the application loads the dynamic linker is trying to resolve all symbols in the application. It does not matter that the reference to a UIPopoverController will not actually be executed (the linker cannot know that). Since the UIPopoverController class does not exist within the UIKit framework installed with version 3.1.3 of the iPhone OS it generates a symbol not found error.

The offending code is the point where the popover controller is allocated [UIPopoverController alloc]. The alloc method is a class level method called on the class UIPopoverController. Objective C classes are actually themselves objects so in this case the linker is looking for a reference to an object (the UIPopoverController class object) that obviously does not exist.

Weak Linking

If you google or search Stack Overflow for this error you will find two suggestions. The quick and easy solution (or maybe I should say quick and dirty) is to weak link the UIKit framework. This effectively tells the linker to ignore any unresolved references to the UIKit framework. Since we have organised the code so that the offending reference is contained in an iPad only controller we are certain that we will never hit a runtime error when running on a non-iPad device.

To specify weak linking of a framework right-click the application target in Xcode and select “Get Info” and then from the General tab change the Type for UIKit.framework from Required to Weak:

This will work but I have to say that I do not feel comfortable with this approach. UIKit is a fundamental framework for an iPhone application so if we have to rely on pretending to the linker that it is not really required something is probably wrong with our code. Also there is a real danger that other errors can be hidden by this approach.

Runtime Checks

The method that I prefer is to include runtime checks for the iPad specific symbols. This allows the UIKit to remain as a strongly linked, required framework and has the added advantage in producing code that is safe to execute on all devices. The previous method is modified as follows:

- (void)showSettings:(id)sender {
  if (popoverSettings == nil) {
    Class cls = NSClassFromString(@"UIPopoverController");
    if (cls != nil) {
      SettingsViewController *viewController = [[SettingsViewController alloc]
                              initWithNibName:@"SettingsView" bundle:nil];
      
      // Create a Navigation controller for the settings
      UINavigationController *navController = [[UINavigationController alloc]
                              initWithRootViewController:viewController];
      
      // Create a popover controller to show the navigation controller
      self.popoverSettings = [[cls alloc]
                             initWithContentViewController:navController];
      
      [navController release];
      [viewController release];
    }
  }
  
  [popoverSettings presentPopoverFromBarButtonItem:sender
                   permittedArrowDirections:UIPopoverArrowDirectionAny
                   animated:YES];
}

The NSClassFromString method is first used to test if the UIPopoverController class is defined and if so a reference to the class is obtained at runtime. This reference is then used to allocate the object ([cls alloc]) rather then referencing the class directly. This way the linker does not have to resolve the symbol when the application is loaded so we do not get an error when running on non iPad devices.