I have an app that was originally developed in Swift 2 / Xcode 7.3.1 using the Firebase iOS SDK v2.5.1 which is currently being updated to the latest versions of Swift/Xcode, which entails also updating the Firebase SDK.
We have hundreds of unit tests that were developed that, for the most part have been easy to update, but there are a few in particular that are tricky due to the way the old Firebase SDK was structured (and thus how it was used).
Originally, because nearly all Firebase database functionality was encompassed in one "Firebase" class, we created a custom class that inherited from the Firebase class that overrode several methods. In the latest SDK, functionality has been split further into different classes (namely the separate of Database and DatabaseReference), and most of the methods that were overridden are now a part of the DatabaseReference class in the FirebaseDatabase framework.
To make this a bit more tangible.
Old code:
class FirebaseTest: Firebase {
override func childByAutoId() {
//generate and save Auto ID internally to be used later for testing/validation
super.childByAutoID();
}
//many other overridden methods
}
//Unit tests
let fbref = FirebaseTest(url: "my-firebase.firebaseio.com/test");
// etc
The old Firebase SDK would return a Firebase reference to the base URL provided in the constructor, and because many of the properties we used were members of Firebase, all of our testing worked.
Following the SDK migration guide, I end up with the following code (simply replacing Firebase with DatabaseReference):
class FirebaseTest: DatabaseReference {
override func childByAutoId() {
//generate and save Auto ID internally to be used later for testing/validation
super.childByAutoID();
}
//many other overridden methods
}
//Unit tests
let fbref = FirebaseTest(fromUrl: "my-firebase.firebaseio.com/test")
// etc
Now, immediately, there is a problem with the above code that prevents it from compiling: the DatabaseReference constructor does not take any arguments. I can change it to the following
let fbref = FirebaseTest();
but now I know that, while it will compile, the tests will absolutely fail. The tricky part is that, in the latest SDK, database references are only supposed to be retrieved from a Database as follows:
//don't do this!
let fbref = DatabaseReference(); //will compile, but will break at runtime. Upon inspection in the debugger, there are several internal properties that should be set that are null. Those internal properties are likely all set correctly if the database reference is obtained correctly
//do it one of these ways
let fbref2 = Database.database().reference();
let fbref3 = Database.database().reference(fromUrl: "my-firebase.firebaseio.com/test") //also have access to this "reference(fromUrl: )" method from Database.database()
let fbref4 = Database.database().reference(withPath: "/test") //also have access to this "reference(withPath: )" method from Database.database()
So, hopefully my conundrum is becoming clear -- because I can only retrieve a correctly-initalized DatabaseReference from a Database, rather than my version of DatabaseReference that has overridden methods for testing, I'm fairly stuck.
Also, I have looked into creating an extension of Database itself as follows:
class FirebaseTest: DatabaseReference {
override func childByAutoId() {
//generate and save Auto ID internally to be used later for testing/validation
super.childByAutoID();
}
//many other overridden methods
}
class FirebaseDatabaseTest: Database {
override func reference(fromUrl: String) {
return FirebaseTest();
}
}
let fbref = FirebaseDatabaseTest().reference(fromUrl: "my-firebase.firebaseio.com/test")
But, even though I am intercepting the url in the overridden reference method, there's no clear way for me to "set up" the DatabaseReference the correct way using that URL.
Thoughts?
Related
First of all, i am not asking if its possible to pass data between view controllers. I am specifically asking if there is anyway to pass some values from a viewController thats in my project to a viewController inside a pod library !
The issue i am facing is that, i am using a payment SDK and the SDK doesn't support pay by saved card info. Normally , whenever i invoke a function from the SDK using a delegate in the sdk, a payment view is presented and it accepts inputs and the SDK works with those inputs.
But what i am trying to do is, pass values from saved card data (name number etc) to the viewController thats inside the pod library.
I tried creating a delegate on my class and tried to access it on the pod library viewController and it returned an error saying that it cannot find the item in scope. I tried creating a global variable and that also returned the same error.
So, this is my question. Is there anyway to pass data from my viewController to the viewController thats present in the SDK ?
You can, although it will be pointless to. Whenever you try to edit a file in the /Pods directory, it will warn you with something similar to the following message
You can press Unlock and append your code, or in your case, a delegate. But the next time you run pod install, it would override everything you've written.
The real pain starts when collaborating, as the /Pods directory does not get pushed into VCS.
I actually found a way to this. I made this happen by using sharedInstance
This is how i did it, here i created a sharedInstance variable.
#objc public final class NISdk: NSObject {
#objc public static let sharedInstance = NISdk()
//We will write the next function here//
}
Then i made a function that accepts parameters
#objc public func SavedCardPayment(cardToken : String,expiryMonth : String,
expiryYear : String,cvv : String,cardholderName : String,useSavingCards : Int)
{ // we will write some ways to store the values we just got as parameters// }
On this function
{
//You must declare these global variables somewhere inside the pod !
ViewControllerInsidePod.savedCardToken = cardToken
ViewControllerInsidePod.savedExpiryMonth = expiryMonth
ViewControllerInsidePod.savedExpiryYear = expiryYear
ViewControllerInsidePod.savedCvv = cvv
ViewControllerInsidePod.savedName = cardholderName
}
Now all info which user entered on the screen will be passed to SDK and then stored in these global variables (which are only available inside the pod n not in the project). Then i used these values to call a function of the pod from itself.
Now i will go back to my viewController in my project, and then call the function using sharedInstance
let sharedSDKInstance = NISdk.sharedInstance
let sharedSDKInstance = NISdk.sharedInstance
sharedSDKInstance.SavedCardPayment(cardToken : "\(token)",
expiryMonth : "\(expiryM)",expiryYear : "\(expiryY)",cvv : "\(cvv)",
cardholderName : "\(name)",useSavingCards : 1)
Hope this helps someone, i dunno if this is the right way to do it but this is how i passed data from a viewController that was outside of scope!
REMEMBER THIS
Also as #Visal Rajapakse , this will only work as long as you never update that specific pod !
I wish to support rearranging a UITableView. I have seen other answers here, here, and here recommend using a another class to manage the realm objects. The only problem is as soon as I add the class I cannot successfully open a Realm.
import RealmSwift
class Data: Object {
dynamic var id = ""
}
// Adding this class causes issues
class DataList: Object {
let list dataList = List<Data>()
}
Any ideas on what is going wrong here? When I attempt to open the Realm it just hangs: no error thrown.
Edit:
From the realm doc it says they should be declared as let.
Thanks to Tj3n for the solution. Migrate your schema or reinstall the app fixes the issue. Here is some doc on that.
I have added functionality to my project that downloads JSON and compares the version numbers in there with the currently installed app version to determine whether a feature should be enabled or not. However, I am now trying to unit test this and I am not sure how to mock the current app version.
Can I inject a value into the info.plist in a test?
Can I completely mock the info.plist in a test?
Or should I:
Add a function in my class to retrieve the version number from the info.plist file and then mock that function?
On app startup, store the version number in NSUserDefaults and the mock this?
I would definitely go with the function which retrieves version number. This way you can get it from info.plist in production code and mock whatever you want in tests. Additionally you will be able to test the retrieval of app version as well :)
Or even better, create another class which gets the application number and inject instance to the class which downloads JSONs. You'll be then able to mock this however you want.
protocol AppVersionProvider {
func getAppVersion() -> String
}
class JSONDownloader {
private let appVersionProvider: AppVersionProvider
public init(appVersionProvider: AppVersionProvider) {
self.appVersionProvider = appVersionProvider
}
public func downloadJSON() {
if appVersionProvider.getAppVersion() != networkingCallResult.appVersion {
...
}
}
}
There, you can mock AppVersionProvider protocol in test with some stub and use info.plist provider for production.
I'm trying to get an example of running OpenEars with the RapidEars plugin running in Swift 2.2 (XCode 7.3.1). However, I suspect I'm having a larger issue with using Objective-C interfaces with extensions in a Swift project (or my understanding of how that works).
The OpenEars code is Obj-C. However I was able to get it running in my swift project through the standard Obj-C -> Swift translation techniques.
Abbreviated code follows. The full example is on a forked Github and updated to Swift-2.2: https://github.com/SuperTango/OpenEars-with-Swift-
This following example is working great. You can see the entire project by checkng out the "working-opears-swift2.2" tag.
OpenEarsTest-Bridging-Header.h:
#import <OpenEars/OELanguageModelGenerator.h>
#import <OpenEars/OEAcousticModel.h>
#import <OpenEars/OEPocketsphinxController.h>
#import <OpenEars/OEAcousticModel.h>
#import <OpenEars/OEEventsObserver.h>
ViewController.swift:
class ViewController: UIViewController, OEEventsObserverDelegate {
var openEarsEventsObserver = OEEventsObserver()
override func viewDidLoad() {
super.viewDidLoad()
loadOpenEars()
}
func loadOpenEars() {
self.openEarsEventsObserver = OEEventsObserver()
self.openEarsEventsObserver.delegate = self
var lmGenerator: OELanguageModelGenerator = OELanguageModelGenerator()
addWords()
var name = "LanguageModelFileStarSaver"
lmGenerator.generateLanguageModelFromArray(words, withFilesNamed: name, forAcousticModelAtPath: OEAcousticModel.pathToModel("AcousticModelEnglish"))
lmPath = lmGenerator.pathToSuccessfullyGeneratedLanguageModelWithRequestedName(name)
dicPath = lmGenerator.pathToSuccessfullyGeneratedDictionaryWithRequestedName(name)
}
func startListening() {
do {
try OEPocketsphinxController.sharedInstance().setActive(true)
OEPocketsphinxController.sharedInstance().startListeningWithLanguageModelAtPath(lmPath, dictionaryAtPath: dicPath, acousticModelAtPath: OEAcousticModel.pathToModel("AcousticModelEnglish"), languageModelIsJSGF: false)
} catch {
NSLog("Error!")
}
}
// A whole bunch more OEEventsObserverDelegate methods that are all working fine...
func pocketsphinxDidStartListening() {
print("Pocketsphinx is now listening.")
statusTextView.text = "Pocketsphinx is now listening."
}
Up until this point, everything is working great.
However, In order to use the "RapidEars" plugin, the documentation (http://www.politepix.com/rapidears/) says to:
Add the framework to the project and ensure it's being included properly.
import two new files (that are both "categories" to existing OpenEars classes):
#import <RapidEarsDemo/OEEventsObserver+RapidEars.h>
#import <RapidEarsDemo/OEPocketsphinxController+RapidEars.h>
Change methods that used: startListeningWithLanguageModelAtPath to use startRealtimeListeningWithLanguageModelAtPath
add two new OEEventsObservableDelegate methods.
func rapidEarsDidReceiveLiveSpeechHypothesis(hypothesis: String!, recognitionScore: String!)
func rapidEarsDidReceiveFinishedSpeechHypothesis(hypothesis: String!, recognitionScore: String!)
The new code can be found by checking out the rapidears-notworking-stackoverflow tag from the above github repo
Problem 1:
When doing completion in the XCode editor, the editor sees WILL perform autocompletion on the startRealtimeListeningWithLanguageModelAtPath method, however when the code is run, it always fails with the error:
[OEPocketsphinxController startRealtimeListeningWithLanguageModelAtPath:dictionaryAtPath:acousticModelAtPath:]: unrecognized selector sent to instance 0x7fa27a7310e0
Problem 2:
When doing auto completion in the XCode editor, it doesn't see the two new delegate methods defined in RapidEarsDemo/OEPocketsphinxController+RapidEars.h.
I have a feeling that these are related, and also related to the fact that they failing methods are defined as Categories to Objective-C classes. But that's only a guess at this point.
I've made sure that the RapidEars framework is imported and in the framework search path.
Can anyone tell me why this is happening? Or if there's some Swift magic incantation that I missed?
The problem could be the one described in the link below, where category methods in a static library produce selector not recognized runtime errors.
Technical Q&A QA1490: Building Objective-C static libraries with categories
How can I define global variables according to whether or not Xcode UI Tests are running? I'm trying to do this:
#if UITESTS
let api = StubbedAPI()
#else
let api = RealAPI()
#endif
These are global variables, so I can't call NSProcessInfo.processInfo().environment or NSProcessInfo.processInfo().arguments at file scope.
The UI Testing target runs as a separate process from your application. This means you can't set preprocessor macros in the test target and expect the app to know about them. The only way the tests can communicate with the app is via the two processInfo settings you mention.
Using these is dynamic, whereas your proposed solution is static. However, it is still possible to do what you are trying to do with the tools Apple has given us.
First, create a protocol that both StubbedAPI and RealAPI conform to.
protocol API {
// ... //
}
class RealAPI: API {
// ... //
}
class StubbedAPI: API {
// ... //
}
Next, create a configuration class. This will be used to tell your code which API to use at run time.
struct Config {
var api: APIProtocol {
get {
return UITesting() ? RealAPI() : StubbedAPI()
}
}
}
private func UITesting() -> Bool {
return NSProcessInfo.processInfo().arguments.contains("UI-TESTING")
}
Then, retrieve a reference to an implementation of API via the configuration.
class FooService {
private let api = Config().api;
}
Finally, set the processInfo argument before you launch the app under UI Testing.
class UITests: TestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
app.launchArguments = ["UI-TESTING"]
app.launch()
}
}
The api property will be set to the real API when running production code and the stubbed one under UI Testing.
There are some downsides to this approach. First, you are introducing the actual "stubbed" API to your production code. This has the potential downside of a developer actually using this in production. Second, you are required to create the API protocol to only have one "real" object implement it. Unfortunately, this is the best solution I've come up with given the current state of UI Testing and Swift.