My previous approach in Objective-C for unit testing was like following:
Calling a public method: no problem
Calling a private method: create a category of that class in your unit test file and put the signature of the private method into this category
Currently, I have the following:
Class developed in ObjC
Protocol developed in Swift
Unit tests for that class developed in Swift
The class conforms (directly, in its public interface (.h)) to that protocol. I have a class instance in my unit tests but somehow I can not invoke a method declared in the protocol over that instance.
Now, the old Category solution does not work with Extensions. When I put some method signature in the extension I get that error saying "Function body expected in declaration".
How can I call the functions with Swift in the best way?
PS: I do not want to declare the method again in the public interface of the class, that is an ugly solution.
As per the documentation on "Writing Tests with Swift" here:
Xcode provides a two-part solution to this problem:
When you set the Enable Testability build setting to Yes, which is
true by default for test builds in new projects, Xcode includes the
-enable-testing flag during compilation. This makes the Swift entities declared in the compiled module eligible for a higher level of access.
When you add the #testable attribute to an import statement for a
module compiled with testing enabled, you activate the elevated access
for that module in that scope. Classes and class members marked as
internal or public behave as if they were marked open. Other entities
marked as internal act as if they were declared public.
Here is also the example (for MySwiftApp)
Class built for a target with Enable Testability set to yes
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
func foo() {
println("Hello, World!")
}
}
Unit test class
// Importing XCTest because of XCTestCase
import XCTest
// Importing AppKit because of NSApplication
import AppKit
// Importing MySwiftApp because of AppDelegate
#testable import MySwiftApp
class MySwiftAppTests: XCTestCase {
func testExample() {
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
appDelegate.foo()
}
}
Also see the important Note
Note: #testable provides access only for internal functions;
file-private and private declarations are not visible outside of their
usual scope when using #testable.
Related
I have created a class in Swift (which I'm learning) that I use to hold certain values as follows:
#objc public class Book: NSObject {
var title: String = ""
var date: Date = Date()
var sku: Float = 0
}
I would now like to store this class in the equivalent of a property that can be accessed by my Objective-C class.
I tried creating a variable in the View Controller where the class is used as follows:
var lastBookViewed = Book()
However, when I try to save the object to the property with the followng code, I get an error:
//create instance of book
let myBook = Book()
//gather information about book
lastBookViewed = myBook //THROWS COMPILER ERROR 'Use of Unresolved Identifier lastBookViewed
Is there a way to do this?
If easier, storing the object to a property in the Objective-C file would work as well but so far I have not been able to get the Objective-C file to recognize the Book class created in Swift although I put #objc before it as recommended.
Edit
When I try to create a property or variable for storing the custom swift object in a different Swift class (the View Controller class) in the same Swift file with the following syntax:
var lastBookViewed: Book
the compiler gives a warning for the VC class "yourVC has no initializers"
Go to your project’s general settings. Select the proper target for your app. Go to “Build Settings” and switch to “All”, instead of “Basic” which is the default. Here search for the “Packaging” section. Turn on “Defines Module”, by changing “No” to “Yes”.
When this is turned on we will now be able to use swift classes inside
of objective-c files.
Before leaving the “Build Settings” look for “Product Module Name” in the “Packaging” section. This will be important so make sure to take note of and copy the “Product Module Name” exactly.
Next go to an objective-c class where you would like to be able to have access to your swift class. In the implementation or .m file of this class import a header like this:
#import "MyProjectModuleName-Swift.h"
Here the file name of the import must be the exact Project Module Name from the build settings. Any objective-c file where you want access to your swift class should include this import.
Now it is worth mentioning one potential issue that may arise when using a swift class. Objective-c cannot read top-level swift classes. So if you go to use a method or variable from your swift class directly it will not be recognized. There are one simple solution to this issue. It’s to make your class public
#objc public class myClass
Say I have a class called ExampleClass.
Say I then write code like so:
#objc(ExampleClass)
class ExampleClass: NSObject {
#objc class func exampleFunc() -> Void {
}
}
With an Objective-C file header like so:
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#interface RCT_EXTERN_MODULE(ExampleClass, NSObject)
RCT_EXTERN_METHOD(exampleFunc)
#end
Which I then consume in my React Native app like so:
console.log('exampleClass', React.NativeModules.ExampleClass);
console.log('exampleFunc', React.NativeModules.ExampleClass.exampleFunc)
The first console log results in {exampleFunc: f}
The second results in undefined,
Calling the function: React.NativeModules.ExampleClass.exampleFunc() results in an app crash with:
Exception 'exampleFunc is not a recognized Objective-C method.' was thrown while invoking setupLogger on target ExampleClass with params (
While changing only the Swift so that it reads:
#objc(ExampleClass)
class ExampleClass: NSObject {
#obj func exampleFunc() -> Void {
}
}
results in calling the function (which, yes) does nothing at present.
How can I expose class level variables? I am trying to write functional Swift, and I am using class methods to simulate structs.
I believe the problem is that RCT_EXPORT_METHOD() only works on instance methods, not class methods, according to my own similar problem and some discussion here: https://github.com/facebook/react-native/issues/2311
My use case is trying to bridge a getInstance() method for a Swift singleton class. This is problematic because when you reference NativeModules.ExampleClass from javascript, which has been exported with RCT_EXTERN_MODULE(), RN calls init() on its own, which you don't want for a singleton (init() reference: https://samwize.com/2017/02/09/calling-a-view-controller-function-from-react-native/)
The best way I've found to accomplish this is pretty ugly. I have a dummy wrapper class that does nothing except call methods on the singleton, and this is the class I export to Objective C (and therefore to React Native). It's basically like this:
#objc(StupidWrapperClass)
class StupidWrapperClass : NSObject {
#objc(pseudoSingletonSomeMethod)
public func pseudoSingletonSomeMethod() {
let singleton = ActualClass.getInstance()
singleton.someMethod()
}
}
and then in the .m bridge file:
#interface RCT_EXTERN_MODULE(StupidWrapperClass, NSObject)
RCT_EXTERN_METHOD(pseudoSingletonSomeMethod)
#end
You could do something like this for a class method, too:
#objc(StupidWrapperClass)
class StupidWrapperClass : NSObject {
#objc(pseudoClassMethod)
public func pseudoClassMethod() {
ActualClass.theRealClassMethod()
}
}
I know I'm kinda late to the party but I recently faced the same problem and I fixed it using a different approach. Adding to the answer given above by #thejoelpatrol, a different approach would be to store the object's reference created by react native in some static variable that would be accessible by you. Then we can use the variable to access the object created by react-native anytime.
Whenever React Native tries to instantiate the class, it would come to the init. inside the init, we can save the reference to the object created by RN.
#objc public class MyClass {
#objc public static var shared: MyClass?
init() {
MyClass.shared = self
}
}
The .m bridge file is as follows:
#interface RCT_EXTERN_MODULE(MyClass)
RCT_EXTERN_METHOD(myClassMethod)
#end
Im in a situation where I need to use Objective-C category to extend a Swift class. I've done something as follows:
In "SomeClass.swift":
class SomeClass: NSObject {
}
In "SomeClass+Extension.h":
#import "Project-Swift.h"
#interface SomeClass (Extension)
-(void)someMethod();
#end
This has worked well. And if I try to use the SomeClass extension in my Objective C code, it is fine.
The problem is, if I want to use someMethod() in a another Swift class, I will need to put the SomeClass+Extension.h file into my ObjC-BridgingHeader.h file.
But doing this will cause a circular dependency, because SomeClass+Extension.h also imports Project-Swift.h.
Does anyone have a good way to get around this?
Please note that simply forward declaring the class in the category header will not work, as categories cannot use forward declarations for it's own implementation as so:
#class SomeClass without importing Project-Swift.h will give a compile error.
The Bad
i too have been fighting this issue a bunch. unfortunately the documentation pretty explicitly states that this pattern is not allowed:
To avoid cyclical references, don’t import Swift code into an
Objective-C header (.h) file. Instead, you can forward declare a Swift
class or protocol to reference it in an Objective-C interface.
Forward declarations of Swift classes and protocols can only be used
as types for method and property declarations.
also throughout the the linked page you will notice it keeps mentioning to import the generated header specifically into the .m file:
To import Swift code into Objective-C from the same target
Import the Swift code from that target into any Objective-C .m file
within that target
The Good
one solution that may work for you is to create a swift extension that redefines each method you need in the category. it is fragile and ugly, but arguably the cleanest solution.
/**
Add category methods from objc here (since circular references prohibit the ObjC extension file)
*/
extension SomeClass {
#nonobjc func someMethod() {
self.performSelector(Selector("someMethod"))
}
}
adding the #noobjc to the front allows the
same method signature to be used w/o overriding the ObjC implementation
now the import "SomeClass+Extension.h" from the bridging
header can be removed
if support for more than two input params is needed, or tighter type coupling is desired i would recommend using the runtime to call the underlying function. a great description is here.
From the Interoperability guide, we cannot directly access the subclassed / categorized / extensioned Objc-objects for the .swift [SomeClass] class.
But as a turn-around, we can do this:
For Variables , we can do this:
extension Class {
private struct AssociatedKeys {
static var DescriptiveName = "sh_DescriptiveName"
}
var descriptiveName: String? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.DescriptiveName) as? String
}
set {
if let newValue = newValue {
objc_setAssociatedObject(
self,
&AssociatedKeys.DescriptiveName,
newValue as NSString?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
}
For Methods, we can use method_swizzling which is not recommended.
As one simple solution, you can move the extension to your Swift code. Then you won't have any dependency problems.
When you implement a class MyGreatClass in Swift its fully qualified name will by <MyPackageName>.MyGreatClass. This is different to Objective-C, where the fully qualified name of that same class is MyGreatClass.
Unfortunately this introduces a problem for me. When I am using NSUnarchiver and the archive was written with Objective-C objects I cannot unpack it with Swift-classes(see below for a detailed description).
This means I need to find a way to rename the namespace for my Swift classes. How do I do that?
Any help would be great!
Background: Why can't NSUnarchiver see/load my swift class?
I have implemented a small program to read a file, which was archived with NSArchive.
These are my files:
main.swift:
import Foundation
// parse command line - total path to *.trace file (from Apple's Instruments app)
var traceFilePath = Process.arguments[1]
var traceFile = NSURL(fileURLWithPath: traceFilePath)
var error:NSError?
// check if the file exists
if (traceFile?.checkResourceIsReachableAndReturnError(&error) == false){
// file does not exist or cannot be accessed
println("\(error)")
exit(1)
}
var rawData = NSData(contentsOfURL: traceFile!)
var data = NSUnarchiver(forReadingWithData: rawData!)
var decodedObject: AnyObject? = data?.decodeObject()
XRObjectAllocRun.swift:
import Foundation
class XRObjectAllocRun: NSObject {
// class to be implemented
}
When I now run my application on an Instruments-file I am getting the following error: Terminating app due to uncaught exception 'NSArchiverArchiveInconsistency', reason: '*** class error for 'XRObjectAllocRun': class not loaded'.
This is really strange because when I add the exact same class in an Objective-C file with a bridging header file I have no issues.
trace file reader-Bridging-Header.h: is empty.
XRObjectAllocRun.h:
#import <Foundation/Foundation.h>
#interface XRObjectAllocRun : NSObject
#end
XRObjectAllocRun.m:
#import "XRObjectAllocRun.h"
#implementation XRObjectAllocRun
#end
What am I missing? Why is my Objective-C class found, whereas my Swift class is not?
Swift has no issues for example with var x = XRObjectAllocRun() in main.swift, but yet the NSUnarchiver still complaints about a missing XRObjectAllocRun class when I stay purely within Swift. Is the NSUnarchiver looking in the wrong places - does it for some reason only accept Objective-C classes?
If you want to know what I am trying to do check this stackoverflow question out.
Update
This is what apple writes:
Swift classes are namespaced based on the module they are compiled in, even when used from Objective-C code. Unlike Objective-C, where all classes are part of a global namespace
Further more:
For example, when you create a document–based Mac app, you provide the name of your NSDocument subclass in your app’s Info.plist file. In Swift, you must use the full name of your document subclass, including the module name derived from the name of your app or framework.
Yikes, trying to figure out the mess now...
Try this when you declare your class:
#objc(XRObjectAllocRun) class XRObjectAllocRun: NSObject {
// class to be implemented
}
That will give this class the same name as the archived class, namely XRObjectAllocRun, instead of the namespaced Swift name trace_file_reader.XRObjectAllocRun.
This is always a concern when you're translating from Objective-C to Swift and you've got an existing archive to deal with. See Apple's documentation:
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html
Note the discussion under "Exposing Swift Interfaces in Objective-C".
I'm attempting to use Swift classes in my Objective-C code, however my Swift classes don't seem to appear in the generated header. As a result, my build fails with "Use of undeclared identifier 'HelloWorld'".
I used the templates to create a project called TestApp.
I have the following Build Settings in my target:
Product Name : TestApp
Product Module Name : TestAppModule
Defines Module : Yes
Apple's documentation says to use #import <TestApp/TestAppModule-Swift.h> but this doesn't work.
Instead, I'm using #import "TestAppModule-Swift.h" in my ".m" file. It seems to find this.
I'm able to navigate to it, and it looks like this...
// Generated by Swift version 1.0 (swift-600.0.34.4.5)
#if defined(__has_include) && __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
...etc...
but no classes defined in there.
I have a Swift file in the project that looks like this...
class HelloWorld {
func hello() {
println("hello world")
}
}
Why isn't this working using the standard header file location #import <TestApp/TestAppModule-Swift.h>?
How can I get my swift classes in that header file, so I won't get the "undeclared identifier" error?
Here's how I have gotten it to work. You can see a more large-scale answer here.
Change this:
class HelloWorld {
func hello() {
println("hello world")
}
}
To:
#objc class HelloWorld {
class func newInstance() -> HelloWorld {
return HelloWorld()
}
func hello() {
println("hello world")
}
}
Then, In your ObjC file:
#import "TestApp-Swift.h"
And call like this:
HelloWorld * helloWorld = [HelloWorld newInstance];
[helloWorld hello];
tl;dr Ensure you have a bridging header if you're doing any cross-calling between Objective-C and Swift.
I had the exact same problem: I could see the -Swift.h file in DerivedData but it made no mention of my Swift classes. I was importing the header file correctly, the Defines Module setting was YES, and the Product Module Name was correct. I tried deleting and re-adding the Swift files, clean buiild, quitting XCode, etc, with no luck.
Then I realised I had no -Bridging-Header.h file in my project, presumably due to the way I'd cobbled it together from a previous project. Shouldn't be a problem because I was not (yet) calling Objective-C from Swift. But when I added a bridging header, and referred to its path in the build settings (Swift Compiler - Code Generation -> Objective-C Bridging Header), it magically fixed the problem - my -Swift.h file was suddenly full of SWIFT_CLASS() goodness!
So I'm guessing the bridging header is fundamental to the process, even if you're NOT using Objective-C from Swift.
UPDATE: I finally understand this. It is related to public/internal access modifiers. Not sure if I missed this originally or if it's an addition to the Apple docs, but it now clearly states:-
By default, the generated header contains interfaces for Swift
declarations marked with the public modifier. It also contains those
marked with the internal modifier if your app target has an
Objective-C bridging header.
It is proper to use #import "TestAppModule-Swift.h" in your .m files. If you need to reference a class in a .h, use the #class forward declaration.
Further, if you want to use a Swift class from Objective-C, the Swift class must be marked with the #objc attribute. Xcode will only include classes with that attributed in the generated header. See also this documentation.
Class should be declared as #objc public class
A more convenient way would be to inherit from NSObject. Like so:
class HelloWorld: NSObject {
func hello() {
println("hello world")
}
}
In my case, by following Apple guidelines, it did not work until I ran the project. The xcode editor kept flagging the unknown swift class, until i clicked "run". The build succeeded, and the swift method worked.
In my case the class was not being compiled, because I first added it to my test target only... After adding it to my main target (Build Phases -> Compile Sources), it was actual compiled and added to the header file.
So much for TDD ;-)
Maybe you defined a Swift class with the same name as an existing Objective-C class which wouldn't be unusual if you want to refactor your Objective-C code to Swift.
As long as you have a class defined simultaneously in Swift and Objective-C the compiler quietly stops updating the bridging header altogether ("ProductModuleName-Swift.h") - which also affects subseqeuent changes in other bridged Swift files.
For general reference how to import Swift into Objective-C see:
Importing Swift into Objective-C | Apple Developer Documentation