Custom Firebase Data Service Class : Swift 3 - ios

I'm searching for a clean way to retrieve (and sometimes save) data from Firebase in Swift. It's annoying me that all my database calls are written in the middle of the view controller code. So I'm looking for some kind of custom data service class. I found this tutorial that's close to what I want: http://www.mobilecyberpunks.com/?p=82.
They promised a Part II but I cannot find this second part, so I guess this was never made. In this second part they promised to cover retrieving and saving data with this custom data service (which is the most important part of the whole thing for me).
I'm thinking of an API class (like in the tutorial) and when I'm retrieving data, and it finishes retrieving from firebase, I save it in a data set in this api class. Then I will posting a notification with Notification Center. But I'm am not sure whether this is best practice or a good way to do this.
Has anyone an idea how to do this (finishing this tutorial I found or in another way)?
Thanks in advance!

Making a custom Class for the communicating is generally a good idea if you need extensive function's and make numerous calls to your server.
The two preferred methods for this are:-
Protocol-Delegate Method
_completionBlocks:
Below answer contains both.
Custom Class
import Foundation
import Firebase
#objc protocol FIRShowAlertDelegate {
func showFIRAlert(_ message : String)
#objc optional func activityIndic()
}
class FIRController :{
var delegate : FIRShowAlertDelegate!
func loginUser(_ emailAddress : String!, password : String , completionBlock : #escaping ((currentUserID : String!) -> Void)){
FIRAuth.auth()?.signIn(withEmail: emailAddress, password: password,
completion: {(user,err) in
if err != nil{
self.delegate.showFIRAlert("Error logging you in,\(err?.localizedDescription)")
}else{
completionBlock(user!.uid)
}
})
}
func retrieveUserData(_ currentId : String!, completionBlock : #escaping ((_ userName : String?) -> Void)){
FIRDatabase.database().reference().child("Users").child(currentId).observeSingleEvent(of: .value, with: {(userSnap) in
if userSnap.exists(){
if let userDict = userSnap.value! as? [String:AnyObject]{
completionBlock(userDict["username"] as! String
}
}else{
completionBlock(nil, nil)
print("No such user exists: \(currentId)")
}
})
}
}
Your ViewController
class AnyViewController : UIViewController, FIRShowAlertDelegate{
let firebaseControllerHandle : FIRController = FIRController()
override func viewDidLoad() {
super.viewDidLoad()
firebaseControllerHandle.delegate = self
firebaseControllerHandle.loginUser("abc#xyz.com", password: "123454321", completionBlock: { (userID) in
print("user : \(userID), logged in")
})
}
func showFIRAlert(_ message : String){
let alertController : UIAlertController = UIAlertController(title: "MyApp", message: message, preferredStyle: .alert)
let okAction : UIAlertAction = UIAlertAction(title: "Ok", style: .default) { (alert) in
print("User pressed ok function")
}
alertController.addAction(okAction)
alertController.popoverPresentationController?.sourceView = view
alertController.popoverPresentationController?.sourceRect = view.frame
self.present(alertController, animated: true, completion: nil)
}
func activityIndic() {
// Use for showing the activity indicator while the data is being retrieved
}
}

I started to use this solution and polished it a little bit, and I came to a pretty handy solution.
I created a custom class named FirebaseAPI. This is a singleton class. This class contains all the methods for Firebase (Authentication, Database, Storage, ...).
Example:
FirebaseAPI.swift
import FirebaseAuth
import FirebaseDatabase
class FirebaseAPI {
static let shared = FirebaseAPI()
private init() {}
//Authentication
func logInUser(onCompletion: #escaping (String?) -> Void {
FIRAuth.auth().signInAnonymously(completion: {(user, error) in
if error == nil {
onCompletion(user!.uid)
} else {
onCompletion(nil)
}
})
}
//Database
func getObjects(parameter: ParamaterClass, onCompletion: #escaping ([ObjectClass]) -> Void) {
Constants.Firebase.References.Object?.observe(.value, with: { snapshot in
var objects = [ObjectClass]()
if snapshot.exists() {
for child in snapshot.children.allObjects {
let object = Object(snapshot: child as! FIRDataSnapshot)
objects.append(object)
}
}
onCompletion(objects)
})
}
}
Constants.swift
import FirebaseDatabase
struct Constants {
struct Firebase {
static var CurrentUser: FIRDatabaseReference?
static var Objects: FIRDatabaseReference?
}
}
AppDelegate.swift
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FIRApp.configure()
FirebaseAPI.shared.logInUser(onCompletion { uid in
if uid != nil {
Constants.Firebase.References.CurrentUser = FIRDatabase.database().reference().child("users").child(uid!)
Constants.Firebase.References.CurrentUser.keepSynced(true)
Constants.Firebase.References.Objects = FIRDatabase.database().reference().child("objects")
Constants.Firebase.Reference.Objects?.keepSynced(true)
}
})
}
return true
}
I can give you a example of calling methods in the FirebaseAPI in a ViewController, but an example of such a method is given in the code of the AppDelegate.swift up here (the FirebaseAPI.shared.logInUser method).
Used this structure in 3 different projects up till now and it works fluently!

Related

In Cognito on iOS, handling new password required doesn't ever reach didCompleteNewPasswordStepWithError

I'm trying to implement functionality to respond to FORCE_CHANGE_PASSWORD on my iOS app that uses AWS Cognito. I used this Stack Overflow question which references this sample code. Right now, my code opens a view controller like it's supposed to; however, once on that view controller, I can't get it to do anything. In the sample code, it seems that when you want to submit the password change request you call .set on an instance of AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>, yet when I do this, the protocol function didCompleteNewPasswordStepWithError is never called. Interestingly, the other protocol function getNewPasswordDetails is called quickly after viewDidLoad and I can't tell why. I believe this shouldn't be called until the user has entered their new password, etc and should be in response to .set but I could be wrong.
My code is pretty identical to the sample code and that SO post, so I'm not sure what's going wrong here.
My relevant AppDelegate code is here:
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startNewPasswordRequired() -> AWSCognitoIdentityNewPasswordRequired {
//assume we are presenting from login vc cuz where else would we be presenting that from
DispatchQueue.main.async {
let presentVC = UIApplication.shared.keyWindow?.visibleViewController
TransitionHelperFunctions.presentResetPasswordViewController(viewController: presentVC!)
print(1)
}
var vcToReturn: ResetPasswordViewController?
returnVC { (vc) in
vcToReturn = vc
print(2)
}
print(3)
return vcToReturn!
}
//put this into its own func so we can call it on main thread
func returnVC(completion: #escaping (ResetPasswordViewController) -> () ) {
DispatchQueue.main.sync {
let storyboard = UIStoryboard(name: "ResetPassword", bundle: nil)
let resetVC = storyboard.instantiateViewController(withIdentifier: "ResetPasswordViewController") as? ResetPasswordViewController
completion(resetVC!)
}
}
}
My relevant ResetPasswordViewController code is here:
class ResetPasswordViewController: UIViewController, UITextFieldDelegate {
#IBAction func resetButtonPressed(_ sender: Any) {
var userAttributes: [String:String] = [:]
userAttributes["given_name"] = firstNameField.text!
userAttributes["family_name"] = lastNameField.text!
let details = AWSCognitoIdentityNewPasswordRequiredDetails(proposedPassword: self.passwordTextField.text!, userAttributes: userAttributes)
self.newPasswordCompletion?.set(result: details)
}
}
extension ResetPasswordViewController: AWSCognitoIdentityNewPasswordRequired {
func getNewPasswordDetails(_ newPasswordRequiredInput: AWSCognitoIdentityNewPasswordRequiredInput, newPasswordRequiredCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityNewPasswordRequiredDetails>) {
self.newPasswordCompletion = newPasswordRequiredCompletionSource
}
func didCompleteNewPasswordStepWithError(_ error: Error?) {
DispatchQueue.main.async {
if let error = error as? NSError {
print("error")
print(error)
} else {
// Handle success, in my case simply dismiss the view controller
SCLAlertViewHelperFunctions.displaySuccessAlertView(timeoutValue: 5.0, title: "Success", subTitle: "You can now login with your new passowrd", colorStyle: Constants.UIntColors.emeraldColor, colorTextButton: Constants.UIntColors.whiteColor)
self.dismiss(animated: true, completion: nil)
}
}
}
}
Thank you so much for your help in advance and let me know if you need any more information.

Mocking iOS Firebase Auth sign-in methods

This question is somewhat similar to Mock third party classes (Firebase) in Swift but different enough to warrant a new question, based on the answers to it.
I'm trying to mock the Auth/FIRAuth method signIn(withEmail email: String, password: String, completion: AuthDataResultCallback?) and am running into difficulties with trying to mock the AuthDataResultCallback object, mainly because it has a User property that I also want to mock. Unfortunately, I'm not able to create my own User or Auth objects because they've been marked as not having an available initializer in Swift.
I have an object (let's call it UserAuthenticationRepository) that's responsible for performing user authentication and database reads. I'd like to inject a Firebase auth object into it to do these things under the hood, but since I want to test this repository object I'd like to be able to inject a Firebase mock object when I go to unit test it.
What I want to do is something like this (simplified slightly for this question):
import FirebaseAuth
protocol FirebaseUserType {
var uid: String { get }
}
extension User: FirebaseUserType {}
protocol FirebaseAuthDataResultType {
var user: FirebaseUserType { get }
}
extension AuthDataResult: FirebaseAuthDataResultType {
var user: FirebaseUserType {
// This is where I'm running into problems because AuthDataResult expects a User object,
// which I also use in the UserAuthenticationRepository signIn(withEmail:) method
}
}
protocol FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: ((FirebaseAuthDataResultType?, Error?) -> Void)?)
}
extension Auth: FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: ((FirebaseAuthDataResultType?, Error?) -> Void)?) {
let completion = completion as AuthDataResultCallback?
signIn(withEmail: email, password: password, completion: completion)
}
}
protocol UserAuthenticationType {
func loginUser(emailAddress: String, password: String) -> Observable<User>
}
class UserAuthenticationRepository: UserAuthenticationType {
private let authenticationService: FirebaseAuthenticationType
private let disposeBag = DisposeBag()
init(authenticationService: FirebaseAuthenticationType = Auth.auth()) {
self.authenticationService = authenticationService
}
func loginUser(emailAddress: String, password: String) -> Observable<User> {
return .create { [weak self] observer in
self?.authenticationService.signIn(withEmail: emailAddress, password: password, completion: { authDataResult, error in
if let error = error {
observer.onError(error)
} else if let authDataResult = authDataResult {
observer.onNext(authDataResult.user)
}
})
return Disposables.create()
}
}
As noted above, I'm running into problems when I try to extend AuthDataResult to conform to my FirebaseAuthDataResultType protocol. Is it possible to do what I'm trying to do? I'd ultimately like to pass back a uid string in my Firebase authentication service when testing UserAuthenticationRepository.
I was eventually able to find a way to mock the Firebase Auth objects necessary for me, but I had to resort to subclassing the Firebase User object, adding new properties to it be used during testing (can't create a User object directly or mutate its properties), and then creating a struct which conforms to FirebaseAuthDataResultType which is initialized with a MockUser object during testing. The protocols and extensions I ended up needing are below:
protocol FirebaseAuthDataResultType {
var user: User { get }
}
extension AuthDataResult: FirebaseAuthDataResultType {}
typealias FirebaseAuthDataResultTypeCallback = (FirebaseAuthDataResultType?, Error?) -> Void
protocol FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: FirebaseAuthDataResultTypeCallback?)
func signOut() throws
func addStateDidChangeListener(_ listener: #escaping AuthStateDidChangeListenerBlock) -> AuthStateDidChangeListenerHandle
func removeStateDidChangeListener(_ listenerHandle: AuthStateDidChangeListenerHandle)
}
extension Auth: FirebaseAuthenticationType {
func signIn(withEmail email: String, password: String, completion: FirebaseAuthDataResultTypeCallback?) {
let completion = completion as AuthDataResultCallback?
signIn(withEmail: email, password: password, completion: completion)
}
}
Below are the mock objects:
class MockUser: User {
let testingUID: String
let testingEmail: String?
let testingDisplayName: String?
init(testingUID: String,
testingEmail: String? = nil,
testingDisplayName: String? = nil) {
self.testingUID = testingUID
self.testingEmail = testingEmail
self.testingDisplayName = testingDisplayName
}
}
struct MockFirebaseAuthDataResult: FirebaseAuthDataResultType {
var user: User
}
An instance of my mock Firebase authentication service with stubs:
class MockFirebaseAuthenticationService: FirebaseAuthenticationType {
typealias AuthDataResultType = (authDataResult: FirebaseAuthDataResultType?, error: Error?)
var authDataResultFactory: (() -> (AuthDataResultType))?
func signIn(withEmail email: String, password: String, completion: FirebaseAuthDataResultTypeCallback?) {
// Mock service logic goes here
}
// ...rest of protocol functions
}
Usage (using RxSwift and RxTest):
func testLoginUserReturnsUserIfSignInSuccessful() {
let firebaseAuthService = MockFirebaseAuthenticationService()
let expectedUID = "aM1RyjpaZcQ4EhaUvDAeCnla3HX2"
firebaseAuthService.authDataResultFactory = {
let user = MockUser(testingUID: expectedUID)
let authDataResult = MockFirebaseAuthDataResult(user: user)
return (authDataResult, nil)
}
let sut = UserSessionRepository(authenticationService: firebaseAuthService)
let userObserver = testScheduler.createObserver(User.self)
sut.loginUser(emailAddress: "john#gmail.com", password: "123456")
.bind(to: userObserver)
.disposed(by: disposeBag)
testScheduler.start()
let user = userObserver.events[0].value.element as? MockUser
// Assert MockUser properties, events, etc.
}
If anyone has any better ideas of how this can be accomplished, please let me know!

Embed Unity inside iOS in own ViewController

Using Unity 2019.3.0f3 and its Unity as a library feature I'm trying to embed a Unity project inside my iOS application.
Unity officially only supports full screen rendering. Nevertheless I'm looking for a way around that restriction.
In previous versions of Unity i successfully used swift-unity to do the integration. Within this approach it is easy to just get the View where Unity is rendering to (using UnityGetGLView()). I had no problems regarding stability or resources.
Using the new library approach, every time I try to access the UnityView, unity forces it's complete Window as keyWindow.
I tried accessing the UnityView in my own ViewController using
if let unityView = UnityFramework.getInstance()?.appController()?.rootViewController.view {
// insert subview at index 0 ensures unity view is behind current UI view
view?.insertSubview(unityView, at: 0)
}
But that immediately activates the complete unity-window and hides my parenting UITabBarController.
Trying to make the UnityFramework.getInstance()?.appController()?.rootViewController a child of my UITabBarController failed with the same result.
Furthermore it is not possible to add a child ViewController. Only adding subviews seems possible.
Does anybody know where that window-behaviour is located or how i can access the UnityView (or the RootViewController) and use it freely?
I found a solution to the problem based on this approach from the unity forum. Using this approach I'm able to use the UnityViewController as a child in my own TabBarController.
The approach is working for Unity 2019.3.0f3, but I'm not sure if it will work in future versions. It feels like Unity tries to actively prevent such use. Then again I found hints in comments in the library-code that would suggest that a modified ViewController-Hierarchy was at least contemplated e.g. in UnityAppController+ViewHandling.h. But the instructions are unclear and methods with the hinted names don't exist.
Solution
1. Create UnityEmbeddedSwift.swift
The official example App provided by Unity is a real mess. I ended up using the UnityEmbeddedSwift.swift from the linked forum post with additions for pausing. This class encapsulates all Unity-related functionality in one clean class.
//
// UnityEmbeddedSwift.swift
// Native
//
// Created by NSWell on 2019/12/19.
// Copyright © 2019 WEACW. All rights reserved.
//
//
// Created by Simon Tysland on 19/08/2019.
// Copyright © 2019 Simon Tysland. All rights reserved.
//
import Foundation
import UnityFramework
class UnityEmbeddedSwift: UIResponder, UIApplicationDelegate, UnityFrameworkListener {
private struct UnityMessage {
let objectName : String?
let methodName : String?
let messageBody : String?
}
private static var instance : UnityEmbeddedSwift!
private var ufw : UnityFramework!
private static var hostMainWindow : UIWindow! // Window to return to when exiting Unity window
private static var launchOpts : [UIApplication.LaunchOptionsKey: Any]?
private static var cachedMessages = [UnityMessage]()
// MARK: - Static functions (that can be called from other scripts)
static func getUnityRootViewController() -> UIViewController! {
return instance.ufw.appController()?.rootViewController
}
static func getUnityView() -> UIView! {
return instance.ufw.appController()?.rootViewController?.view
}
static func setHostMainWindow(_ hostMainWindow : UIWindow?) {
UnityEmbeddedSwift.hostMainWindow = hostMainWindow
let value = UIInterfaceOrientation.landscapeLeft.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
static func setLaunchinOptions(_ launchingOptions : [UIApplication.LaunchOptionsKey: Any]?) {
UnityEmbeddedSwift.launchOpts = launchingOptions
}
static func showUnity() {
if(UnityEmbeddedSwift.instance == nil || UnityEmbeddedSwift.instance.unityIsInitialized() == false) {
UnityEmbeddedSwift().initUnityWindow()
}
else {
UnityEmbeddedSwift.instance.showUnityWindow()
}
}
static func hideUnity() {
UnityEmbeddedSwift.instance?.hideUnityWindow()
}
static func pauseUnity() {
UnityEmbeddedSwift.instance?.pauseUnityWindow()
}
static func unpauseUnity() {
UnityEmbeddedSwift.instance?.unpauseUnityWindow()
}
static func unloadUnity() {
UnityEmbeddedSwift.instance?.unloadUnityWindow()
}
static func sendUnityMessage(_ objectName : String, methodName : String, message : String) {
let msg : UnityMessage = UnityMessage(objectName: objectName, methodName: methodName, messageBody: message)
// Send the message right away if Unity is initialized, else cache it
if(UnityEmbeddedSwift.instance != nil && UnityEmbeddedSwift.instance.unityIsInitialized()) {
UnityEmbeddedSwift.instance.ufw.sendMessageToGO(withName: msg.objectName, functionName: msg.methodName, message: msg.messageBody)
}
else {
UnityEmbeddedSwift.cachedMessages.append(msg)
}
}
// MARK - Callback from UnityFrameworkListener
func unityDidUnload(_ notification: Notification!) {
ufw.unregisterFrameworkListener(self)
ufw = nil
UnityEmbeddedSwift.hostMainWindow?.makeKeyAndVisible()
}
// MARK: - Private functions (called within the class)
private func unityIsInitialized() -> Bool {
return ufw != nil && (ufw.appController() != nil)
}
private func initUnityWindow() {
if unityIsInitialized() {
showUnityWindow()
return
}
ufw = UnityFrameworkLoad()!
ufw.setDataBundleId("com.unity3d.framework")
ufw.register(self)
// NSClassFromString("FrameworkLibAPI")?.registerAPIforNativeCalls(self)
ufw.runEmbedded(withArgc: CommandLine.argc, argv: CommandLine.unsafeArgv, appLaunchOpts: UnityEmbeddedSwift.launchOpts)
sendUnityMessageToGameObject()
UnityEmbeddedSwift.instance = self
}
private func showUnityWindow() {
if unityIsInitialized() {
ufw.showUnityWindow()
sendUnityMessageToGameObject()
}
}
private func hideUnityWindow() {
if(UnityEmbeddedSwift.hostMainWindow == nil) {
print("WARNING: hostMainWindow is nil! Cannot switch from Unity window to previous window")
}
else {
UnityEmbeddedSwift.hostMainWindow?.makeKeyAndVisible()
}
}
private func pauseUnityWindow() {
ufw.pause(true)
}
private func unpauseUnityWindow() {
ufw.pause(false)
}
private func unloadUnityWindow() {
if unityIsInitialized() {
UnityEmbeddedSwift.cachedMessages.removeAll()
ufw.unloadApplication()
}
}
private func sendUnityMessageToGameObject() {
if (UnityEmbeddedSwift.cachedMessages.count >= 0 && unityIsInitialized())
{
for msg in UnityEmbeddedSwift.cachedMessages {
ufw.sendMessageToGO(withName: msg.objectName, functionName: msg.methodName, message: msg.messageBody)
}
UnityEmbeddedSwift.cachedMessages.removeAll()
}
}
private func UnityFrameworkLoad() -> UnityFramework? {
let bundlePath: String = Bundle.main.bundlePath + "/Frameworks/UnityFramework.framework"
let bundle = Bundle(path: bundlePath )
if bundle?.isLoaded == false {
bundle?.load()
}
let ufw = bundle?.principalClass?.getInstance()
if ufw?.appController() == nil {
// unity is not initialized
// ufw?.executeHeader = &mh_execute_header
let machineHeader = UnsafeMutablePointer<MachHeader>.allocate(capacity: 1)
machineHeader.pointee = _mh_execute_header
ufw!.setExecuteHeader(machineHeader)
}
return ufw
}
}
2. Modify AppDelegate.swift
Sets window and launch options needed by UnityEmbeddedSwift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UnityEmbeddedSwift.setHostMainWindow(window)
UnityEmbeddedSwift.setLaunchinOptions(launchOptions)
return true
}
3. Create RootTabBarController.swift
This class sets up the hierarchy.
It is important to use the UnityRootViewController right after calling UnityEmbeddedSwift.showUnity().
The Tab-Switching is not nice, but if it is missing Unity will pause (or freeze?) during loading. The timing seems to depend on the Unity-Projects loading time. It can be faster for small projects and needs more time for larger projects.
import UIKit
class RootTabBarController: UITabBarController, UITabBarControllerDelegate {
var unityNC: UINavigationController?
var nativeNC: UINavigationController?
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
// start unity and immediatly set as rootViewController
// this loophole makes it possible to run unity in the same window
UnityEmbeddedSwift.showUnity()
let unityViewController = UnityEmbeddedSwift.getUnityRootViewController()!
unityViewController.navigationItem.title = "Unity"
unityNC = UINavigationController.init(rootViewController: unityViewController)
unityNC?.tabBarItem.title = "Unity"
let nativeViewController = UIViewController.init()
nativeViewController.view.backgroundColor = UIColor.darkGray
nativeViewController.navigationItem.title = "Native"
nativeNC = UINavigationController.init(rootViewController: nativeViewController)
nativeNC?.tabBarItem.title = "Native"
viewControllers = [unityNC!, nativeNC!]
// select other tab and reselect first tab to unfreeze unity-loading
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
self.selectedIndex = 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01, execute: {
self.selectedIndex = 0
})
})
}
// MARK: - UITabBarControllerDelegate
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
// pause unity if unity-tab is not selected
if viewController != unityNC {
UnityEmbeddedSwift.pauseUnity()
} else {
UnityEmbeddedSwift.unpauseUnity()
}
}
}
4. Modify Main.storyboard
Modify the storyboard to start with the RootTabBarController.
For anyone who is still interested in preventing the freezing, I am building on top of aalmigthy's answer:
You do not need to add a TabBar controller and switch between the tabs. All you need to do is:
Add the Unity view as a subview
Send the subview to back
Here's the modified ViewController class (no need for a tab bar):
import UIKit
class HybridViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UnityEmbeddedSwift.showUnity()
let uView = UnityEmbeddedSwift.getUnityView()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
self.view.addSubview(uView!)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
self.view.sendSubviewToBack(uView!)
})
})
}
}

Cannot set value conforming to protocol to property with protocol type

I'm trying to create a fake authenticator for my unit tests that can manually set the user as logged in or logged out and bypass the API my code actually uses where I'd need a real accessToken to log the user in.
I've wrapped the Authentication API my app uses in to the following class:
API Wrapper
import OIDC
protocol Authenticator {
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void)
}
struct OIDCAuthenticator: Authenticator {
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void) {
//API call
OIDCHelper.getValidAccessToken { (error, accessToken) in
DispatchQueue.main.async {
completionHandler( error, accessToken)
}
}
}
}
Then I create a Fake Authenticator using the same protocol for testing purposes
Fake/Testing Authenticator
import OIDC
import Foundation
///Mocks the user being logged in our logged out for Testing purposes
struct FakeAuthenticator: Authenticator {
let error: OIDCError?
let accessToken: String?
func getValidAccessToken(completionHandler: #escaping (Error?, String?) -> Void) {
completionHandler(error, accessToken)
}
init(loggedIn: Bool) {
if loggedIn {
error = .tokenNotFoundInKeychainData
accessToken = nil
} else {
error = nil
accessToken = "abcdefg"
}
}
}
Settings the OIDCAuthenticator API Wrapper works fine when settings the authenticator in the ViewController subclass.
TableViewController Implementation
import UIKit
import OIDC
class SettingsPageTableViewController: UITableViewController{
// MARK: - Outlets and variables
var authenticator: Authenticator!
private var isUserLoggedIn = false
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
authenticator = OIDCAuthenticator()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
authenticator.getValidAccessToken { [weak self] (error, _) in
self?.isUserLoggedIn = (error == nil)
self?.setLogginStatus()
}
}
}
However when I try to do the same thing with the FakeAuthenticator in my Unit tests I get the following error:
SettingsViewController Test Class
import XCTest
import UIKit
#testable import MyProject
class SettingsViewControllerTests: XCTestCase {
var viewController: SettingsPageTableViewController!
override func setUp() {
super.setUp()
configureViewControllerForTesting()
}
private func configureViewControllerForTesting() {
let storyboard = UIStoryboard(name: "SettingsPage", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController
viewController = navigationController.topViewController as! SettingsPageTableViewController
_ = viewController.view
}
func testSignInButtonIsAvailableWhenUnauthenticated() {
viewController.authenticator = FakeAuthenticator(loggedIn: false)
}
}
The same things happens when I swap out FakeAuthenticator with OIDCAuthenticator. I've also attempted to cast the FakeAuthenticator to Authenticator but this merely alters the Error to Cannot assign value of type 'Authenticator' to type 'Authenticator!'.
Why am I getting this error and what is the best approach to fixing this?
You need to remove the files from your test target since you're already importing your whole project with #testable.

RxSwift subscribe block not called

I'm playing around with RxSwift and I'm stuck with a simple toy programm. My program essentially contains a model class and a viewcontroller. The model contains an observable that gets updated on the main queue after an asynchronous network call, the viewcontroller subscribes in viewDidLoad(). The AppDelegate initializes the model and passes it to ViewController and triggers the network request.
class GalleryModel {
var galleryCount: BehaviorSubject<Int>
init() {
galleryCount = BehaviorSubject.init(value:0)
}
func refresh() {
doAsyncRequestToAmazonWithCompletion { (response) -> AnyObject! in
var counter = 0
//process response
counter = 12
dispatch_async(dispatch_get_main_queue()) {
self.galleryCount.on(.Next(counter))
}
return nil
}
}
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
var galleryModel: GalleryModel?
override func viewDidLoad() {
super.viewDidLoad()
galleryModel?.galleryCount.subscribe { e in
if let gc = e.element {
self.label.text = String(gc)
}
}
}
}
class AppDelegate: UIResponder, UIApplicationDelegate {
var galleryModel: GalleryModel?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//do amazon setup
galleryModel = GalleryModel()
if let viewController = window?.rootViewController as? ViewController {
viewController.galleryModel = GalleryModel()
}
return true
}
func applicationDidBecomeActive(application: UIApplication) {
galleryModel?.refresh()
}
The label gets updated only one, it shows "0". I expected the label to get updated twice, showing "0" after the first update and showing "12" after the second update after the processing of the network request. A breakpoint in the dispatch_async block gets hit, but it seems that galleryCount lost its observer. Anybody any idea what's happening or how to debug this?
Best
In case reads this anyone is interested. It was an refactoring error, after renaming variables I stopped passing the observable to the ViewController. Instead I created a new one... facepalm
Here are some useful snippets for subscribe in RxSwift (in Japanese)
For example to subscribe to different events:
let source: Observable<Int> = create { (observer: ObserverOf<Int>) in
sendNext(observer, 42)
sendCompleted(observer)
return AnonymousDisposable {
print("disposed")
}
}
let subscription = source.subscribe { (event: Event<Int>) -> Void in
switch event {
case .Next(let element):
print("Next: \(element)")
case .Completed:
print("Completed")
case .Error(let error):
print("Error: \(error)")
}
}
Clean and Build solved the problems for me

Resources