I want to switch to a new view when the login is successful And it will save the login state, when I close the App and open it again, no need to login again. I use combine . library .Hope you can help me. Thanks
Here is my full code. Hope you will help me
Firebase will tell you... you just need to ask.
This is how you ask.
let user = Auth.auth().currentUser
if let user = user {
// User is logged in
}
These are the FirebaseAuth iOS docs (for further information)
Your should probably call this in the init of your LoginViewModel
init() {
let user = Auth.auth().currentUser
if let user = user {
// User is logged in
session = User(uid: user.uid,
email: user.email)
} else {
session = nil
}
}
Make the LoginViewModel variable session #Published like this
class LoginViewModel: ObservableObject {
...
#Published var session: User?
...
}
read about the Published type here
Update
You should add this block to your signIn() func in the LoginView in the else instead of session.isLoggedIn.toggle().
I also recommended removing the isLoggedin var you should look at the user object in session to check if it is nil or not.
func signIn () {
loading = true
error = false
session.signIn(email: email, password: password) { (result, error) in
self.loading = false
if error != nil {
self.error = true
self.loading = false
} else {
guard let uid = result?.user.uid,
let email = result?.user.email else {
return
}
session.session = User(uid: uid, email: email)
}
}
}
This is the signUp() I added. Although I recommend making it on a separate page.
func signUp (email: String,
password: String,
handler: #escaping AuthDataResultCallback) {
Auth.auth().createUser(withEmail: email, password: password, completion: handler)
}
Finally I would recommend to move all the login logic to the viewModel instead of the view... that's why viewModels exist.
I have a SwiftUI/Firebase project, where I allow users to create and upload content while logged in with anonymous. I also have a Firebase rule that prevent editing data that isn't tagged with the same UID as you're logged in with.
My problem is that, when users log in with Google or Apple login, I don't know where to insert any logic for migrating their content from their old anonymous UID to their Apple/Google UID. (Update: Yes, I can link accounts, but that only works if they haven't previously used their account on a different device).
As far as I can tell, I don't get their new Apple/Google UID until after they're authenticated, and by then, they can no longer modify data tagged with the Anonymous UID.
I've tried linking the accounts, but I get an "Account is already linked" error, so I'm assuming that approach is a dead end?
As an example, here is my code for the Google login with a note where I'm trying to insert my migration logic:
import SwiftUI
import Firebase
import GoogleSignIn
struct GoogleSignInButton: View {
#EnvironmentObject var viewModel: GoogleSignInViewModel
var body: some View {
Button("Sign in with Google") {
viewModel.signIn()
}
.foregroundColor(Color.greyZ)
.padding()
.frame(maxWidth: .infinity)
.background(Color.greyB)
.cornerRadius(5)
.padding()
}
}
struct GoogleSignInButton_Previews: PreviewProvider {
static var previews: some View {
GoogleSignInButton()
}
}
class GoogleSignInViewModel: NSObject, ObservableObject {
enum SignInState {
case signedIn
case signedOut
}
#Published var state: SignInState = .signedOut
override init() {
super.init()
setupGoogleSignIn()
}
func signIn() {
if GIDSignIn.sharedInstance().currentUser == nil {
GIDSignIn.sharedInstance().presentingViewController = UIApplication.shared.windows.first?.rootViewController
GIDSignIn.sharedInstance().signIn()
}
}
func signOut() {
GIDSignIn.sharedInstance().signOut()
do {
try Auth.auth().signOut()
state = .signedOut
} catch let signOutError as NSError {
print(signOutError.localizedDescription)
}
}
private func setupGoogleSignIn() {
GIDSignIn.sharedInstance().delegate = self
}
}
extension GoogleSignInViewModel: GIDSignInDelegate {
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error!) {
if error == nil {
// Get UID of existing user
if let previousUID:String = Auth.auth().currentUser?.uid {
// migrate Firestore data for old uid to new uid
// Firebase rule prevent modifying data if you're logged in with different uid so it has to be before logging in with Google
// But I don't seem to have the new Google UID yet, so what do I migrate it to?
}
// Log in with new user
firebaseAuthentication(withUser: user)
} else {
print(error.debugDescription)
}
}
private func firebaseAuthentication(withUser user: GIDGoogleUser) {
if let authentication = user.authentication {
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken)
Auth.auth().signIn(with: credential) { (_, error) in
if let error = error {
print(error.localizedDescription)
self.state = .signedOut
} else {
self.state = .signedIn
}
}
}
}
}
UPDATE: As requested, here is the Link-function that invariably results in a "This credential is already associated with a different user account" error. I have checked the account in Firebase, and the account already exists, so that is why I assumed the "link" approach is a dead end, and tried migrating the data instead.
private func firebaseAuthentication(withUser user: GIDGoogleUser) {
if let authentication = user.authentication {
let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken)
if let currentUser = Auth.auth().currentUser {
// User already logged in
currentUser.link(with: credential) { result, error in
if let error = error {
print(error.localizedDescription)
} else {
print(result ?? "Success")
}
}
} else {
// User not logged in (shouldn't happen as they're always anonymous
Auth.auth().signIn(with: credential) { (_, error) in
if let error = error {
print(error.localizedDescription)
self.state = .signedOut
} else {
self.state = .signedIn
}
}
}
}
}
Instead of migrating the data, consider linking the user's new Google or Apple credentials to their existing Firebase account by filling the process outlines in linking multiple Auth providers to an account on iOS.
I have integrated the Pusher framework for my application in Swift 3 using cocoa pods [ pod 'PusherSwift' ].
These are the lines of code :
let pusher = Pusher(key: "XXXXXXXXXXXXXXXXXXXX")
// subscribe to channel and bind to event
let channel = pusher.subscribe("test_channel")
let _ = channel.bind(eventName: "my_event", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
if let message = data["message"] as? String {
print(message)
}
}
})
pusher.connect()
The app crashes at pusher.connect() at the line - self.delegate?.debugLog?(message: "[PUSHER DEBUG] Network reachable"). No crash report is shown.
open lazy var reachability: Reachability? = {
let reachability = Reachability.init()
reachability?.whenReachable = { [unowned self] reachability in
self.delegate?.debugLog?(message: "[PUSHER DEBUG] Network reachable")
if self.connectionState == .disconnected || self.connectionState == .reconnectingWhenNetworkBecomesReachable {
self.attemptReconnect()
}
}
reachability?.whenUnreachable = { [unowned self] reachability in
self.delegate?.debugLog?(message: "[PUSHER DEBUG] Network unreachable")
}
return reachability
}()
This looks like you might be getting bitten by the same issue described here.
I think it's that the PusherConnection object is taken as unowned into the reachability closure but because you're not keeping a reference to the Pusher instance outside of the viewDidLoad function then the connection object gets cleaned up whereas the reachability object does not.
So, to fix this you probably need to declare the pusher object outside of the function where you instantiate it, so that it hangs around. e.g.
class ViewController: UIViewController, PusherDelegate {
var pusher: Pusher! = nil
...
and then within viewDidLoad do pusher = Pusher(... as normal.
I don't think you need to use pusher.connect().
See for example detailed docs:
let pusher = Pusher(key: "YOUR_APP_KEY")
let myChannel = pusher.subscribe("my-channel")
myChannel.bind(eventName: "new-price", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
if let price = data["price"] as? String, company = data["company"] as? String {
print("\(company) is now priced at \(price)")
}
}
})
Alternatively try this first and see if it connects:
let pusher = Pusher(key: "XXXXXXXXXXXXXXXXXXXX")
pusher.connect()
Then bind to your channel.
Using Alamofire 4.0 and Swift 3.0 this works:
Alamofire.request("http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json", method: .get).responseJSON {
(response) -> Void in
print("Success: \(response.result)")
}
Success: SUCCESS
However when I try to use the Sessionmanager so I can include a timeoutInterval, my requests always fail
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
let alamofireManager = Alamofire.SessionManager(configuration: configuration)
alamofireManager.request("http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json").validate().responseJSON {
response in
print("Success: \(response.result)")
print("Response String: \(response.result.value)")
}
Success: FAILURE
Would be grateful if someone could help point me in the right direction here.
By printing response.result.error I got:
Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json, NSLocalizedDescription=cancelled, NSErrorFailingURLStringKey=http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json}
Which lead me to this reference:
You need to make sure that the manager is retained. The difference
here is that the initialized manager is not owned, and is deallocated
shortly after it goes out of scope. As a result, any pending tasks are
cancelled.
Solution:
One way to solve the issue your having is by declaring the custom session manager outside of the class declaration as a global variable like so...
let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 15
return SessionManager(configuration: configuration)
}()
Now, within your class you can make the request.
class ViewController: UIViewController {
let url = "http://content.uplynk.com/player/assetinfo/ab19f0dc98dc4b7dbfcf88fa223a6c3b.json"
override func viewDidLoad() {
super.viewDidLoad()
sessionManager.request(url).validate().responseJSON { response in
switch response.result {
case .success:
print(response.result.value as! NSDictionary)
break
case .failure:
print(response.result.error!)
break
}
}
}
}
Which shall give you what you're looking for. Hope that helps!
I'm struggling a bit to figure out how to best test an app that uses Alamofire to help sync with server data.
I want to be able to test my code that uses Alamofire and processes JSON responses from a server.
I'd like to mock those tests so that I can feed the expected response data to those tests without incurring real network traffic.
This blog post (http://nshipster.com/xctestcase/) describes how easy it is to Mock an object in Swift - but I'm not sure how to do that with Alamofire and its chained responses.
Would I mock the Manager? the Request? Response? Any help would be appreciated!
I'm adding another answer since I've just found this approach that in my opinion is easier and really simple to read and use.
I've created a dummy Alamofire class that contains only the functions and the types necessary for tests.
Now I include this file in the test target instead of the real Alamofire.
For example I've created my version of the Request class where I define a couple of static variables that I valorise depending on the test, and for this class I've implemented only the init and the responseJSON function.
public class Request {
var request:String?
struct response{
static var data:NSHTTPURLResponse?
static var json:AnyObject?
static var error:NSError?
}
init (request:String){
self.request = request
}
public func responseJSON(options: NSJSONReadingOptions = .AllowFragments, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self {
completionHandler(NSURLRequest(URL: NSURL(string:self.request!)!), Request.response.data, Request.response.json, Request.response.error)
return self
}
}
Now I can mock a response in a test:
func testMytestFunction(){
var HTMLResponse = NSHTTPURLResponse(URL: NSURL(string: "myurl")!, statusCode: 200, HTTPVersion: "HTTP/1.1", headerFields: nil)
Request.response.data = HTMLResponse
Request.response.json = LoadDataFromJSONFile("MyJsonFile")
request(.POST, "myurl", parameters: nil, encoding: ParameterEncoding.JSON).responseJSON {
(request, response, JSON, error) -> Void in
// the JSON and response variable now contains exactly the data that you have passed to Request.response.data and Request.response.json
}
}
The request function is defined here:
public func request(method: Method, URLString: URLStringConvertible, parameters: [String: AnyObject]? = nil, encoding: ParameterEncoding = .URL) -> Request {
return Request(request: URLString.URLString)
}
public func request(URLRequest: URLRequestConvertible) -> Request {
return Request(request: "fakecall")
}
This question is getting old, but I just encountered the same issue, and the solution is very easy when using OHHTTPStubs.
OHHTTPStubs just mocks the responses you get from NSURLSession, so it works well with Alamofire, and you get very good coverage of your code path.
For example, in your test case, just mock the response using:
OHHTTPStubs.stubRequestsPassingTest({
(request: NSURLRequest) -> Bool in
return request.URL!.host == "myhost.com"
}, withStubResponse: {
(request: NSURLRequest) -> OHHTTPStubsResponse in
let obj = ["status": "ok", "data": "something"]
return OHHTTPStubsResponse(JSONObject: obj, statusCode:200, headers:nil)
})
Waiting for an answer by #mattt I post an example of my code.
Let's say that we have a Client class that is responsible for calling a simple web service. This class implements a function called userSignIn that performs a sign in using the WS.
This is the code for the userSignIn function:
func userSignIn(
#email:String,
password:String,
completionHandler: (Bool, String?, NSError?) -> Void
)-> Void
{
var parameters:[String:AnyObject] = [
"email":email,
"password":password,
]
Alamofire.request(.POST, Client.urlPath, parameters: parameters, encoding: ParameterEncoding.JSON).responseJSON {
(request, response, JSON, responseError) -> Void in
// Setup callback params
// HERE WE INJECT THE "FAKE" DATA--------
var operationComplete = false
var accessToken:String?
var error:NSError?
// --------------------------------------
if let statusCode = response?.statusCode {
// Check for errors and build response data
(operationComplete, accessToken, error) = self.checkSignInResponse(statusCode, JSON: JSON)
}
// Call the completion handler
completionHandler(operationComplete, accessToken, error)
}
}
The aim of the function is to get a token from the web service if the information passed by the user are correct.
The function checkSignInResponse (I don't report its code since it's not useful for the answer) has the role to valorise the 3 variables operationComplete, accessToken and error depending on the JSON response received.
Now that the 3 variables have a value we call the completionHandler using them.
How to mock this function?!
To mock the response I override the userSignIn function directly into the test function (as explained by the NSHipster article).
func testUserSignIn_whenParamsAreInvalid(){
class MockClient:Client {
override func userSignIn(#email: String, password: String, completionHandler:
(Bool, String?, NSError?) -> Void) {
// Set callback params
var operationComplete = false
var accessToken:String? = nil
var error:NSError? = NSError(domain: "Testing", code: 99, userInfo: nil)
completionHandler(operationComplete, accessToken, error)
}
}
signInViewController!.client = MockClient()
signInViewController!.loadView()
fillRegisterFieldsWithDataAndSubmit(femail(), password: fpassword())
XCTAssertNotNil(signInViewController!.error, "Expect error to be not nil")
}
then I substitute the client inside the view controller that I'm testing using my "mocked" client. In this case I'm testing that the controller passes to the function information that are not valid so I check that the error property of the controller is not nil. To force this data I simply set operationComplete to false and I manual generate an NSError.
Does it make any sense to you? I'm not sure that this test is a good test... but at least I can verify the data flow.
I believe I have a solution to this for the newer versions of Alamofire. My Swift and DI skills are a bit noob so this can probably be improved but I thought I'd share. The most challenging part of mocking Alamofire is mocking the method chaining in the Network call (request().responseJSON).
The Network call:
let networkManager: NetworkManagerProtocol!
init(_ networkManager: NetworkManagerProtocol = NetworkManagerTest(SessionManager())) {
self.networkManager = networkManager
}
func create(_ params: [String: Any], completion: #escaping (Response<Success,Fail>) -> Void) {
self.networkManager.manager.request(self.url!, method: .post, parameters: params, encoding: URLEncoding.default, headers: nil).responseJSON {
response in
if response.result.isSuccess {
completion(Success())
} else {
completion(Fail())
}
}
}
The manager that you'll inject into the network call class:
The NetworkManagerProtocol provides the get manager functionality to the various types of network managers.
class NetworkManager: NetworkManagerProtocol {
private let sessionManager: NetworkManagerProtocol
init(_ sessionManager: NetworkManagerProtocol) {
self.sessionManager = sessionManager
}
var manager: SessionManagerProtocol {
get {
return sessionManager.manager
}
set {}
}
}
Extend Alamofire's SessionManager class:
This is where we add the protocols and custom functionality to SessionManager. Note the protocol's request method is a wrapper around Alamofire's request method .
extension SessionManager: NetworkManagerProtocol, SessionManagerProtocol {
private static var _manager = SessionManager()
var manager: SessionManagerProtocol {
get {
return SessionManager._manager
}
set {
let configuration = URLSessionConfiguration.default
SessionManager._manager = Alamofire.SessionManager(configuration: configuration, delegate: SessionManager.default.delegate)
}
}
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol {
let dataRequest: DataRequest = self.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers)
return dataRequest
}
}
Create a SessionManagerMock for the mock api call:
This class creates a SessionManagerMock object and then retrieves the mock data with its request method.
class SessionManagerMock: NetworkManagerProtocol, SessionManagerProtocol {
private static var _manager = SessionManagerMock()
var manager: SessionManagerProtocol {
get {
return SessionManagerMock._manager
}
set {}
}
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol {
return DataRequestMock()
}
}
Extend Alamofire's DataRequest class:
And again, note the protocol's responseJSON class is a wrapper around DataRequests's responseJSON class.
extension DataRequest: DataRequestProtocol {
func responseJSON(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self {
return self.responseJSON(queue: nil, options: .allowFragments, completionHandler: completionHandler)
}
}
DataRequestMock Class:
This class stores the data for the mock request. It could be built out a little more (add request data, etc) but you get the idea.
class DataRequestMock: DataRequestProtocol {
static var statusCode: Int = 200
var dataResponse = DataResponse<Any>(
request: nil,
response: HTTPURLResponse(url: URL(string: "foo.baz.com")!, statusCode: DataRequestMock.statusCode, httpVersion: "1.1", headerFields: nil),
data: nil,
result: Result.success(true), // enum
timeline: Timeline()
)
func response(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self {
completionHandler(dataResponse)
return self
}
func responseJSON(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self {
return response(completionHandler: completionHandler)
}
}
The Protocol Droids:
protocol NetworkManagerProtocol {
var manager: SessionManagerProtocol { get set }
}
protocol SessionManagerProtocol {
func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequestProtocol
}
protocol DataRequestProtocol {
func responseJSON(completionHandler: #escaping (DataResponse<Any>) -> Void) -> Self
}
The test method:
A lot of improvements could be made to make this more dynamic but again you get the idea
var sut: UserService?
override func setUp() {
super.setUp()
sut = UserService(NetworkManagerTest(SessionManagerMock()))
}
func testCreateUser201() {
DataRequestMock.statusCode = 201
let params : [String : String] = ["name": "foo baz", "email": "foobaz#gmail.com", "password": "tester123"]
var resultCode: Int!
sut?.create(params) {(response: Response) in
switch response {
case .success(let resp):
resultCode = resp.statusCode
case .failure(let resp):
resultCode = resp.statusCode
}
}
XCTAssertEqual(resultCode, 201, "Status code is wrong")
}