Changed +load method order in Xcode 7 - ios

I found out that Xcode 7 (Version 7.0 (7A220)) changed the order in which +load methods for classes and categories are called during unit tests.
If a category belonging to the test target implements a +load method, it is now called at the end, when instances of the class might've already been created and used.
I have an AppDelegate, which implements +load method. The AppDelegate.m file also contains AppDelegate (MainModule) category. Additionally, there is a unit test file LoadMethodTestTests.m, which contains another category – AppDelegate (UnitTest).
Both categories also implement +load method. The first category belongs to the main target, the second one – to the test target.
Code
I made a small test project to demonstrate the issue.
It is an empty default Xcode one view project with only two files changed.
AppDelegate.m:
#import "AppDelegate.h"
#implementation AppDelegate
+(void)load {
NSLog(#"Class load");
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(#"didFinishLaunchingWithOptions");
return YES;
}
#end
#interface AppDelegate (MainModule)
#end
#implementation AppDelegate (MainModule)
+(void)load {
NSLog(#"Main Module +load");
}
#end
And a unit test file (LoadMethodTestTests.m):
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "AppDelegate.h"
#interface LoadMethodTestTests : XCTestCase
#end
#interface AppDelegate (UnitTest)
#end
#implementation AppDelegate (UnitTest)
+(void)load {
NSLog(#"Unit Test +load");
}
#end
#implementation LoadMethodTestTests
-(void)testEmptyTest {
XCTAssert(YES);
}
#end
Testing
I performed Unit Testing of this project (the code and the github link are below) on Xcode 6/7 and got the following +load calls order:
Xcode 6 (iOS 8.4 simulator):
Unit Test +load
Class load
Main Module +load
didFinishLaunchingWithOptions
Xcode 7 (iOS 9 simulator):
Class load
Main Module +load
didFinishLaunchingWithOptions
Unit Test +load
Xcode 7 (iOS 8.4 simulator):
Class load
Main Module +load
didFinishLaunchingWithOptions
Unit Test +load
Question
Xcode 7 runs the test target category +load method (Unit Test +load) in the end, after the AppDelegate has already been created.
Is it a correct behavior or is it a bug that should be sent to Apple?
May be it is not specified, so the compiler/runtime is free to rearrange calls?
I had a look at this SO question as well as on the +load description in the NSObject documentation but I didn't quite understand how the +load method is supposed to work when the category belongs to another target.
Or may be AppDelegate is some sort of a special case for some reason?
Why I'm asking this
Educational purposes.
I used to perform method swizzling in a category inside unit test target. Now, when the call order has changed, applicationDidFinishLaunchingWithOptions is performed before the swizzling takes place. There are other ways to do it, I believe, but it just seems counter-intuitive to me the way it works in Xcode 7. I thought that when a class is loaded into memory, +load of this class and +load methods of all its categories are supposed to be called before we can something with this class (like create an instance and call didFinishLaunching...).

TL,DR: It's xctest's fault, not objc's.
This is because of how the xctest executable (the one that actually runs the unit tests, located at $XCODE_DIR/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Agents/xctest loads its bundle.
Pre-Xcode 7, it loaded all referenced test bundles before running any tests. This can be seen (for those that care), by disassembling the binary for Xcode 6.4, the relevant section can be seen for the symbol -[XCTestTool runTestFromBundle:].
In the Xcode 7 version of xctest, you can see that it delays loading of testing bundles until the actual test is run by XCTestSuite, in the actual XCTest framework, which can be seen in the symbol __XCTestMain, which is only invoked AFTER the test's host application is set-up.
Because the order of these being invoked internally changed, the way that your test's +load methods are invoked is different. There were no changes made to the objective-c-runtime's internals.
If you want to fix this in your application, you can do a few things. First, you could manually load your bundle using +[NSBundle bundleWithPath:], and invoking -load on that.
You could also link your test target back to your test host application (I hope you're using a separate test host than your main application!), which would make it be automatically loaded when xctest loads the host application.
I would not consider it a bug, it's just an implementation detail of XCTest.
Source: Just spend the last 3 days disassembling xctest for a completely unrelated reason.

Xcode 7 has two different load orders in the iOS template project.
Unit Test Case. For Unit Test, the test bundle is injected into the running simulation after the application has launched through to the main screen. The default Unit Test execution sequence is like the following:
Application: AppDelegate initialize()
Application: AppDelegate init()
Application: AppDelegate application(…didFinishLaunchingWithOptions…)
Application: ViewController viewDidLoad()
Application: ViewController viewWillAppear()
Application: AppDelegate applicationDidBecomeActive(…)
Application: ViewController viewDidAppear()
Unit Test: setup()
Unit Test: testExample()
UI Test Case. For UI Test, a separate second process XCTRunner is set up which exercises the application under test. An argument can be passed from the test setUp() ...
class Launch_UITests: XCTestCase {
override func setUp() {
// … other code …
let app = XCUIApplication()
app.launchArguments = ["UI_TESTING_MODE"]
app.launch()
// … other code …
}
... to be received be the AppDelegate ...
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(… didFinishLaunchingWithOptions… ) -> Bool {
// … other code …
let args = NSProcessInfo.processInfo().arguments
if args.contains("UI_TESTING_MODE") {
print("FOUND: UI_TESTING_MODE")
}
// … other code …
The injection into vs the separate process can be observed by printing NSProcessInfo.processInfo().processIdentifier and NSProcessInfo.processInfo().processName in from both the test code and the application code.

Related

Swift 3 How to manage different delegates for different Targets

We have created two Targets (Target_One & Target_Two) for same project.
Target_One contain SDK1 and delegate Target1SDKHelperDelegate
Target_Two contain SDK2 and delegate Target2SDKHelperDelegate
The reason to create two target : we need to upload two apps with same UI but different SDK integration.
As we know, each SDK has their own delegates. So we want to apply delegates specific to the target.
Example: Target_One has a class named MyClass
class MyClass: NSObject, Target1SDKHelperDelegate {
}
In above class, we have implemented Target1SDKHelperDelegate delegate.
We are using same class for Target_Two also and we want to use Target2SDKHelperDelegate for Target_Two.
So how we can put two different delegates to for two different targets?
We also know to manage target we should use below code.
#if Target_One
#else
#endif
But anyone tell us how to manage delegate by using above?
We want to do something like :
class MyClass: NSObject
#if Target_One
, Target1SDKHelperDelegate
#else
, Target2SDKHelperDelegate
#endif
{
}
Can try like this code with use typealias:
#if FREE_VERSION
public typealias DELEGATES = UIViewController & AttributeUDBClientMainDelegate & AttributeSubscriptionHelperDelegate
#else
public typealias DELEGATES = UIViewController
#endif
public final class SettingsViewController: DELEGATES {
Actually it is pretty simple and direct . Lately I faced the issue cause of size of iPA went too big like 20MB . I disabled not needed features from some targets and had to manage the shared files like Appdelegate when it has references to disabled features files . Solved this by simply duplicating Appdelegate file and put it in a specific paths related to specific targets . Then included each appDelegate file under its target. It worked . The idea is the same like if you have firebase push notifications pLists configuration files for multiple targets OR imageAssets folders. Hope this helps.

ObjC class method is not called. GDB playing games?

I have a custom UITableViewController that I am trying to use to manage a UITableView. The flow of my code in the main UIViewController that contains the UITableView goes like below:
_messagesTableVC = [[AllMessagesTableViewController alloc] init];
_allMessageTableView.dataSource = _messagesTableVC;
_allMessageTableView.delegate = _messagesTableVC;
[_allMessageTableView reloadData];
The AllMessagesTableViewController custom UITableViewController class is initialized, it does any processing needed and I set the _allMessageTableView (the UITableView)'s delegates to my custom class.
When I run this code, the program acts as if the custom class is not there but no errors occur. It seems as that NO methods in the custom class are called, no init, no initWithCoder, nothing (I have set breakpoints and checked ;)).
As you can see in the screenshot below, I have set a breakpoint after a custom method refreshData in the custom class that I set to return YES. I assign the return value of refreshData to a local variable test.
In the debugger:
_messagesTableVC custom class does not appear to be nil.
test does not appear to exist.
Not shown here, but when I try to run [_messagesTableVC refreshData] in the debugger it says error: Execution was interrupted, reason: Attempted to dereference an invalid ObjC Object or send it an unrecognized selector.
The process has been returned to the state before expression evaluation.. So is _messagesTableVC actually nil??
What could be causing these problems or is GDB playing games? This is a Messages app extension in case that makes any difference. Thanks.
Update: Here is the code for the custom class init and refreshData
- (instancetype)init {
self = [super init];
if (!self)
return nil;
return self;
}
- (BOOL)refreshData {
return YES;
}
Update2: I created a blank working project and copy pasted the exact files into my original project (because it is an iMessage extension to a big iOS app). It turns out the Xcode is running the older build of the app even after I cleaned the project and changed the UITableView delegate to supply a different text.
I created a Messages Extension for the same project in the past and deleted the target while keeping the files in the project. When I re-added the Messages Extension to the project, Xcode kept running the older build of the app.
I fixed the issue with Xcode running an older version of the app by:
Clean All
Delete Messages Extension target from project
Backup existing Message Extension code to a directory outside the project. Then delete the Messages app group and directory from the project.
Delete the Messages Extension build scheme from project.
Delete project Derived Data directory.
Restart Xcode, create new Messages Extension target in project, and import saved code.

Newly created test case class is not shown under Test Navigator in Xcode

My XCode version is 7.3.1 .
My project already has test project, and there are already some test case classes. I can see them when select the "Test Navigator" tab which is one of this on left top of XCode.
Now, I want to create a new test case, so I selected the new Unit Test Case Class:
And I add very simple code into it:
#import <XCTest/XCTest.h>
#interface MyTest : XCTestCase
#end
#implementation MyTest
- (void)setUp {
[super setUp];
}
- (void)tearDown {
[super tearDown];
}
- (void)testExample {
NSString* expected = #"my-test-string";
XCTAssertEqual(expected, #"my-test-string");
}
#end
I need to run it, so I go to "Test Navigator" again, but I can't see this test case class I just created. Why?
Restarting Xcode is the only thing that worked for me in Xcode 9.0 beta 6.
It appears that Xcode adds the individual testmethods/classes only after the whole unit test target has been run at least once.
So select your test target, run all tests using Command u. After that you should see all tests in the test navigator and you can run them individually.
Sometimes the unit test appears in Test Navigator after you Save (CTRL+S) the unit test's file in XCode.
Remember to keep the test prefix on the test methods, otherwise they are not recognized by the test framework, e.g.:
func test_When_empty_Then_returnsNil() {
not
func when_empty_Then_returnsNil() {

iOS testing with XCTest and separate file

Currently, I write my testing in 1 file (default). Is it possible to have multiple files and test?
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
- (void)testExample {
// This is an example of a functional test case.
XCTAssert(YES, #"Pass");
}
You can have many files and many tests per file. The first one is just the one made by Xcode automatically, but all that makes it special is that it:
Implements a subclass of XCTestCase
Is a member of the testing target (not your app target)
Making another file with the same characteristics is simple.
In Xcode, File -> New -> iOS -> Source -> UI Test Case Class
The convention is to group related tests into a source file, and then name that source file ___Tests.swift (or .m)
For example, SharingTests.swift might contain the methods testSharingToFacebook() and testSharingToTwitter().

Module "MyApp" not found in UnitTest-Swift

Im trying to test some Swift class (#objc class) in my legacy Objc code. I am importing "UnitTests-Swift.h" in my test classes.
Then I get this error:
Module "MyApp" not found in the autogenerated "UnitTests-Swift.h"
This is whats inside the top part of the "UnitTests-Swift.h"
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
#if defined(__has_feature) && __has_feature(modules)
#import XCTest;
#import ObjectiveC;
#import MyApp;
#import CoreData;
#endif
I cleaned the project, checked all the relevant flags ("No such module" when using #testable in Xcode Unit tests, Can't use Swift classes inside Objective-C), removed derived data and so on.. No idea of whats happening, but I m quite sure that #import MyApp shouldnt be there..
Can anyone help me with this?
Just got this issue in my project and after the entire day spent on investigation I finally resolved it.
Basically this happens because you have cyclic dependency between ObjC and Swift. So in my case it was:
Swift Protocol with #obj attribute in a main target;
Swift Class in UnitTest target which inherited this Protocol;
Import UnitTestTarget-Swift.h in any Objective-C class of your UnitTest target
So fairly simple combination leads to this error. In order to fix this you want either:
simply make sure that your Swift Class in UnitTest target is private, so it won't get to UnitTestTarget-Swift.h
or
do not mark your original Swift Protocol as #objc, which will allow you to access your SwiftClass from all ObjectiveC test classes, but those classes won't have any idea about the Swift Protocol.
Because the Swift class to be tested is part of MyApp, you should be importing "MyApp-Swift.h" in the test classes instead of "UnitTests-Swift.h".
You can add a Swift unit test (just create a new unit file and change the extension by .swift).
From that unit test file you can use your Swift classes.
And you can also import that file from your Objective-C unit tests (and the other way around) using the test module bridging headers.
And this would be the default example for your Swift unit test file:
import XCTest
#testable import MyApp
class MyAppSwiftTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
This solution helped me:
Open Tests Target Build Settings
Search for HEADER_SEARCH_PATHS
In the line called "Header Search Paths" set value $CONFIGURATION_TEMP_DIR/myProjectName.build/DerivedSources
Clean and Cmd+U again
Hope it helps!
Many thanks to this article.

Resources