Unexpected quirky behavior from socket.io in Swift - ios

As per title, I'm having some trouble dealing with socket.io. It connects really well and accordingly in the first view controller but weird things happen when it comes to second controller.
Here's the code:
First Controller: I have declared some global variable for connection purposes between both view controller.
import UIKit
import SocketIOClientSwift
import SwiftyJSON
import CoreData
//declare some global variable
var patientCoreData = [NSManagedObject]()
var numberOfUsersExisting:Int = 0 //assign to 0 by default
var appUserData: Patient? //for specific user
var pSample: Array<Patient> = [] //for all user
//initiate socket globally
let socket = SocketIOClient(socketURL: "localhost:3000", options: [
"reconnects": true
])
func reportStatus(){
socket.on("connect") {data, ack in
print("Report status: View Controller connected")
socket.emit("click", "Client app connected")
}
}
func readDataFromSocket(completion: (data:AnyObject)-> ()){
socket.on("reply") {data, ack in
print("database replied")
completion(data: data)
}//socket
}//readDataFromSOCKET
func importData(){
reportStatus()
socket.connect()
readDataFromSocket(){ data in
let json = JSON(data)
let nou = json[0].count
if nou > 0 {
print("Test(1st VC): grabbing data from database")
for var i=0; i<nou; ++i{
numberOfUsersExisting = nou
pSample += [Patient(id: json[0][i]["ID"].intValue, name: json[0][i]["Name"].stringValue, gender: json[0][i]["Gender"].stringValue, mileage: json[0][i]["Mileage"].doubleValue)]
pSample.sortInPlace({$0.globalPatientMileAge < $1.globalPatientMileAge})
}
print("Successfully grabbed data")
}else{
print("No user in the database")
numberOfUsersExisting = 0
}
}//readDataFromSocket
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout{
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
print("First view appeared")
let prefs = NSUserDefaults.standardUserDefaults()
//if an user has logged in
let isLoggedIn = prefs.integerForKey("ISLOGGEDIN") as Int
if (isLoggedIn != 1){
print("No user currently, so heading to login screen")
socket.disconnect()
self.performSegueWithIdentifier("gotoLogin", sender: self)
}else{
print("ViewDidAppear: An user has been logged in")
let permissionToLoadData = prefs.integerForKey("ISLOGGEDIN")
if (permissionToLoadData != 1) {
print("Please grant permission to get data")
}else{
print("First view: connecting to database")
importData()
}//permission to load data
}
}//end of viewDidAppear
}
Second Controller:
import UIKit
import SocketIOClientSwift
import SwiftyJSON
import CoreData
var nou:Int?
class LoginViewController: UIViewController {
let prefs:NSUserDefaults = NSUserDefaults.standardUserDefaults()
let registeredUserID = NSUserDefaults.standardUserDefaults().stringForKey("registerPatientID")
let appDel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
func displayAlertMessage(userMessage:String){
let alert = UIAlertController(title: "Alert", message: userMessage, preferredStyle: UIAlertControllerStyle.Alert)
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
func successMessage(userMessage:String){
let alert = UIAlertController(title: "Welcome Back", message: userMessage, preferredStyle: UIAlertControllerStyle.Alert)
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
#IBOutlet weak var loginPatientID: UITextField!
#IBAction func LoginButton(sender: AnyObject) {
let logInUserID = loginPatientID.text
if (logInUserID!.isEmpty){
displayAlertMessage("Please enter your Patient ID!")
return
}else{
print("Test: requesting login permission from database")
socket.emit("loginRequest", logInUserID!)
print("Test: requested")
socket.on("loginReply") {data, ack in
let jsonLogin = JSON(data)
if jsonLogin[0].intValue == 1{
print("Test: ID Matched, putting up ViewController")
self.prefs.setObject(logInUserID, forKey: "AppUserID")
self.prefs.setInteger(1, forKey: "ISLOGGEDIN")
self.prefs.synchronize()
let permissionToLoadData = self.prefs.integerForKey("ISLOGGEDIN")
if (permissionToLoadData != 1) {
print("Please grant permission to get data")
}else{
print("First view: connecting to database")
importData()
print("Did you import?")
}//permission to load data
self.loginPatientID.resignFirstResponder()
self.dismissViewControllerAnimated(true, completion: nil)
}else if jsonLogin[0].intValue == 0{
self.displayAlertMessage("Sorry, you are not assigned to this program")
}else if jsonLogin[0].intValue == 3{
print("Test: Query problem")
}else{
print("Test: not getting anything from ID database")
}
}//socket.on
}//else
}//login button
override func viewDidLoad() {
super.viewDidLoad()
print("Login View Controller loaded")
}
override func viewDidAppear(animated: Bool) {
socket.connect()
print("LoginVC: establishing connection")
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
You may have noticed that in First view controller, when the viewDidAppear() is launched, the app will checks if user is login or not. If somebody has already logged in, it's fine. If there is nobody logging in, it will perform a segue(modally segue) to Second view controller.
A login form will be presented in second view controller and once user hits the login button, you might wanna look at the code.
Let's assume that everything goes right until it comes to importData(), the function isn't launched at all but the app just goes on, why?
Here's a screenshot of the console, pay attention to "Did you import?", if the function is launched, the app should return some additional message from 1st view controller.

After struggling for a few days, I think I may have found the correct answer.
Eventually I defined 2 different socket handlers connection as such:
let loginSocket = SocketIOClient(socketURL: "localhost:3000")
let socket = SocketIOClient(socketURL: "localhost:3000", options: [
"reconnects": true
])
for both view controller.
If there is a conclusion I can draw from this conundrum is that we can't use single socket handler for socket methods from different view controller.

Related

AWS Cognito sign in not working (Swift-iOS)

I've integrated cognito into my xcode project. The sign up/password update features are working correctly. However I can't seem to get the sign in process to work. I turned on the logs and I get the following error
{"__type":"NotAuthorizedException","message":"Access Token has expired"}
Domain=com.amazonaws.AWSCognitoIdentityProviderErrorDomain Code=-1000 "Authentication delegate not set" UserInfo={NSLocalizedDescription=Authentication delegate not set}]
I have also implemented the AWSCognitoIdentityInteractiveAuthenticationDelegate delegate in the AppDelegate script.
Here's the AppDelegate code
class AppDelegate: UIResponder, UIApplicationDelegate {
class func defaultUserPool() -> AWSCognitoIdentityUserPool {
return AWSCognitoIdentityUserPool(forKey: userPoolID)
}
var window: UIWindow?
var loginViewController: LoginVC?
var navigationController: UINavigationController?
var storyboard: UIStoryboard?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Warn user if configuration not updated
if (CognitoIdentityUserPoolId == "us-east-1_TavWWBZtI") {
let alertController = UIAlertController(title: "Invalid Configuration",
message: "Please configure user pool constants in Constants.swift file.",
preferredStyle: .alert)
let okAction = UIAlertAction(title: "Ok", style: .default, handler: nil)
alertController.addAction(okAction)
self.window?.rootViewController!.present(alertController, animated: true, completion: nil)
//print("Please configure user pool constants in Constants.swift file.")
}
// setup logging
AWSDDLog.sharedInstance.logLevel = .verbose
AWSDDLog.add(AWSDDTTYLogger.sharedInstance)
// setup service configuration
let serviceConfiguration = AWSServiceConfiguration(region: CognitoIdentityUserPoolRegion, credentialsProvider: nil)
// create pool configuration
let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: CognitoIdentityUserPoolAppClientId,
clientSecret: CognitoIdentityUserPoolAppClientSecret,
poolId: CognitoIdentityUserPoolId)
// initialize user pool client
AWSCognitoIdentityUserPool.register(with: serviceConfiguration, userPoolConfiguration: poolConfiguration, forKey: AWSCognitoUserPoolsSignInProviderKey)
// fetch the user pool client we initialized in above step
let pool = AWSCognitoIdentityUserPool(forKey: AWSCognitoUserPoolsSignInProviderKey)
self.storyboard = UIStoryboard(name: "Main", bundle: nil)
pool.delegate = self
return AWSMobileClient.sharedInstance().interceptApplication(
application, didFinishLaunchingWithOptions:
launchOptions)
//return true
}
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let navigationController = self.window?.rootViewController as? UINavigationController {
if navigationController.visibleViewController is SummaryReportVC ||
navigationController.visibleViewController is GoalStatusReportVC || navigationController.visibleViewController is YearTotalsReportVC || navigationController.visibleViewController is DailyActivityReportVC {
return UIInterfaceOrientationMask.all
} else {
return UIInterfaceOrientationMask.portrait
}
}
return UIInterfaceOrientationMask.portrait
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
extension AppDelegate: AWSCognitoIdentityInteractiveAuthenticationDelegate {
func startPasswordAuthentication() -> AWSCognitoIdentityPasswordAuthentication {
print("Calling signin VC from app delegate")
if (self.navigationController == nil) {
self.navigationController = self.storyboard?.instantiateViewController(withIdentifier: "NCFirst") as? UINavigationController
}
if (self.loginViewController == nil) {
self.loginViewController = self.navigationController?.viewControllers[0] as? LoginVC
}
DispatchQueue.main.async {
self.navigationController!.popToRootViewController(animated: true)
if (!self.navigationController!.isViewLoaded
|| self.navigationController!.view.window == nil) {
self.window?.rootViewController?.present(self.navigationController!,
animated: true,
completion: nil)
}
}
return self.loginViewController!
}
}
Here's my LoginVC code
class LoginVC: UIViewController {
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var forgotPasswordLabel: UILabel!
#IBOutlet weak var signUpLabel: UILabel!
#IBOutlet weak var emailTF: UITextField!
#IBOutlet weak var passwordTF: UITextField!
var passwordAuthenticationCompletion: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>?
let pool = AWSCognitoIdentityUserPool(forKey: AWSCognitoUserPoolsSignInProviderKey)
var usernameText: String?
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.tintColor = UIColor.white
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController!.navigationBar.shadowImage = UIImage()
self.navigationController!.navigationBar.isTranslucent = true
loginButton.addTarget(self, action: #selector(loginUser), for: .touchUpInside)
loginButton.layer.cornerRadius = 18
emailTF.addPadding(.left(35))
passwordTF.addPadding(.left(35))
let tap = UITapGestureRecognizer(target: self, action: #selector(goToForgotPasswordVC))
let tap2 = UITapGestureRecognizer(target: self, action: #selector(goToSignUp1VC))
forgotPasswordLabel.isUserInteractionEnabled = true
forgotPasswordLabel.addGestureRecognizer(tap)
signUpLabel.isUserInteractionEnabled = true
signUpLabel.addGestureRecognizer(tap2)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.passwordTF.text = nil
self.emailTF.text = usernameText
}
#objc func loginUser() {
print("Got inside Login func")
if (self.emailTF.text != nil && self.passwordTF.text != nil) {
print("Calling login method now")
let authDetails = AWSCognitoIdentityPasswordAuthenticationDetails(username: self.emailTF.text!, password: self.passwordTF.text! )
self.passwordAuthenticationCompletion?.set(result: authDetails)
} else {
print("Empty fields")
let alertController = UIAlertController(title: "Missing information",
message: "Please enter a valid user name and password",
preferredStyle: .alert)
let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
alertController.addAction(retryAction)
}
}
#objc func goToActivitySessionsVC() {
let storyboard = UIStoryboard(name: "TabBar", bundle: nil)
let destVC = storyboard.instantiateViewController(withIdentifier: "TabBarVC")
self.navigationController?.pushViewController(destVC, animated: true)
self.navigationController?.isNavigationBarHidden = true
}
#objc func goToForgotPasswordVC() {
let storyboard = UIStoryboard(name: "ForgotPassword", bundle: nil)
let destVC = storyboard.instantiateViewController(withIdentifier: "ForgotPasswordVC")
self.navigationController?.pushViewController(destVC, animated: true)
}
#objc func goToSignUp1VC() {
let storyboard = UIStoryboard(name: "SignUp", bundle: nil)
let destVC = storyboard.instantiateViewController(withIdentifier: "SignUp1VC")
self.navigationController?.pushViewController(destVC, animated: true)
}
/* func checkLoginStatus() {
if !AWSSignInManager.sharedInstance().isLoggedIn {
showSignIn()
}
else {
print("Logged In")
AWSSignInManager.sharedInstance().logout(completionHandler: {(result: Any?, error: Error?) in
self.showSignIn()
print("Sign-out Successful");
})
}
}
}
*/
extension LoginVC: AWSCognitoIdentityPasswordAuthentication {
public func getDetails(_ authenticationInput: AWSCognitoIdentityPasswordAuthenticationInput, passwordAuthenticationCompletionSource: AWSTaskCompletionSource<AWSCognitoIdentityPasswordAuthenticationDetails>) {
print("Get details called")
self.passwordAuthenticationCompletion = passwordAuthenticationCompletionSource
DispatchQueue.main.async {
if (self.usernameText == nil) {
self.usernameText = authenticationInput.lastKnownUsername
}
}
}
public func didCompleteStepWithError(_ error: Error?) {
print("Did commplete step with error called")
DispatchQueue.main.async {
if let error = error as NSError? {
let alertController = UIAlertController(title: error.userInfo["__type"] as? String,
message: error.userInfo["message"] as? String,
preferredStyle: .alert)
let retryAction = UIAlertAction(title: "Retry", style: .default, handler: nil)
alertController.addAction(retryAction)
self.present(alertController, animated: true, completion: nil)
print(error.description)
} else {
self.emailTF.text = nil
self.dismiss(animated: true, completion: nil)
print("Got in else")
}
}
}
}
One other thing to note is that getDetails never gets called and so does the didCompleteStepWithError method. When I click the sign in button, nothing happens.
The AWS documentation is quite confusing. After much trial and error, I was able to successfully set up Cognito, sign up, authenticate on log in, and un-authenticate on sign out. To be quite honest, I don't fully understand why I call certain things. To the best of my ability, I will explain here.
Here's how Cognito works.. First it assumes that the user is already logged in and authenticated. It checks to see if this is true. This is the reason why the entry point for your storyboard is the view controller that users see after they are logged in. This is all done with the code that runs in your AppDelegate on launch. More on what you need to fix in that below.
If the user is not logged in, startPasswordAuthentication() will be called. In your code, (as it should be) this defined in the extension of AppDelegate for the protocol AWSCognitoIdentityInteractiveAuthenticationDelegate. Furthermore, startPasswordAuthentication() is called every time the user needs to log in. If the user is already logged in once the app starts, this is not called.
Another note on your question - getDetails is only called on launch if the user is not signed in. If on launch the user is not signed in, then this is called. It is also called if you are signed in and then you sign out.
So make sure the entry point for your storyboard is the logged-in screen.
On the statement that follows I am not entirely sure, so feel free to correct me if so: AWS automatically accesses the entry point upon successful log-in. Everything you are going in your #objc func loginUser() looks correct to me. That's what I have. But make sure your entry point is not the log-in screen but rather what shows after successful log in. Here is a picture of my storyboard:
Try adding the following. I am not quite sure why exactly this works, but it results in proper authentication:
In your AppDelegate, right after your variable for the storyboard, add a boolean isInitialized as such:
var isInitialized : Bool = false
Then add this code after you set up your Cognito configuration. (right before your return statement in didFinishLaunchingWithOptions) :
let didFinishLaunching = AWSSignInManager.sharedInstance().interceptApplication(application, didFinishLaunchingWithOptions: launchOptions)
if (!self.isInitialized) {
AWSSignInManager.sharedInstance().resumeSession(completionHandler: { (result: Any?, error: Error?) in
print("Result: \(String(describing: result)) \n Error:\(String(describing: error))")
})
self.isInitialized = true
}
Now replace the return statement you currently have for didFinishLaunching with the following:
return didFinishLaunching
Make sure you have this delegate set in your viewDidLoad() method for your login screen (Note you have to import AWSAuthCore):
AWSSignInManager.sharedInstance().delegate = self
and implement the protocol in your log-in VC as such:
extension LoginViewController : AWSSignInDelegate {
func onLogin(signInProvider: AWSSignInProvider, result: Any?, error: Error?) {
if error == nil {
}
}
}
Add these variables as class variables to your view controller that users see after they are logged in. They are referenced below.
var user : AWSCognitoIdentityUser?
var userAttributes : [AWSCognitoIdentityProviderAttributeType]?
/*
* :name: defaultUserPool
* :description: Returns the cognito identity pool for the global pool ID.
* :return: A Cognito Identity pool instantiation
*/
class func defaultUserPool() -> AWSCognitoIdentityUserPool {
return AWSCognitoIdentityUserPool(forKey: userPoolID)
}
Finally, make sure that you are checking the user attributes in the initial screen in viewWillAppear(). For example call the function fetchUserAttributes in this method:
func fetchUserAttributes() {
self.user = AppDelegate.defaultUserPool().currentUser()
self.user?.getDetails().continueOnSuccessWith(block: { [weak self = self] (task) -> Any? in
AWSSignInManager.sharedInstance().resumeSession(completionHandler: { (result: Any?, error: Error?) in
print("Result: \(String(describing: result)) \n Error:\(String(describing: error))")
})
guard task.result != nil else {
// alert error
return nil
}
self?.username = self?.user?.username
self?.userAttributes = task.result?.userAttributes
self?.userAttributes?.forEach({ (attribute) in
print("Name: " + attribute.name!)
})
DispatchQueue.main.async {
self?.setAttributeValues()
}
}
return nil
})
}
func resetAttributeValues() {
self.user = nil
self.userAttributes = nil
}
Finally, here is my code for signing out:
let comp = { [weak self = self] (_ result: Any?, _ error: Error?) -> Void in
if error == nil {
self?.user?.signOut()
self?.resetAttributeValues()
self?.fetchUserAttributes()
}
}
AWSSignInManager.sharedInstance().logout(completionHandler: comp)
I hope this helps. I understand this is really confusing, and to be honest, I was quite confused just writing this.. Good luck and feel free to message me with any questions.
You need to call getDetails.
In the sample app, they call getDetails in UserDetailTableViewController.
Try this line of code below pool.delegate = self in AppDelegate.
self.pool?.currentUser()?.getDetails()
I referred AWS Cognito User Pools in iOS (Swift) app

Closing and opening my app goes straight to homepage without verifying the user email

When I create a new user it does everything it should do and saves the users detail and goes back to the login page waiting for the email to be verified before allowing it to be used. The coding works so it doesn't allowing you to proceed until email has been verified but I've realised when I slide the app to close it and then reopen it (before verifying the email), it goes straight to the homepage bypassing the login page even if the email hasn't been verified?
import UIKit
import Firebase
import SwiftKeychainWrapper
class ViewController: UIViewController {
#IBOutlet weak var emailField: UITextField!
#IBOutlet weak var passwordField: UITextField!
var userUid: String!
override func viewDidLoad(){
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
func Keychain() {
KeychainWrapper.standard.set(userUid, forKey: "uid")
}
if let _ = KeychainWrapper.standard.string(forKey: "uid"){
LoggedIn()
}
}
func goToCreateUserVC() {
performSegue(withIdentifier: "CreateAProfile", sender: nil)
}
func LoggedIn() {
performSegue(withIdentifier: "LoginSuccessful", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CreateAProfile" {
if let destination = segue.destination as? CreatUsers {
if userUid != nil {
destination.userUid = userUid
}
if emailField.text != nil {
destination.emailField = emailField.text
}
if passwordField.text != nil {
destination.passwordField = passwordField.text
}
}
}
}
func DisplayAlertMessage(MessageToDisplay: String) {
let alertController = UIAlertController(title: "Alert", message: MessageToDisplay, preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: .default) { (action:UIAlertAction!) in
// Code in this block will trigger when OK button tapped.
print("Ok button tapped");
}
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion:nil)
}
#IBAction func signIntapped(_ sender: Any) {
if let email = emailField.text, let password = passwordField.text {
Auth.auth().signIn(withEmail: email, password: password, completion:
{(user,error) in
if let user = Auth.auth().currentUser {
if user.isEmailVerified {
self.userUid = user.uid
print("Email Verified")
self.LoggedIn()
} else {
self.DisplayAlertMessage(MessageToDisplay: "Need To Verify Email Address")
}
} else {
self.DisplayAlertMessage(MessageToDisplay: "Incorrect Username/Password")
}
});
}
}
#IBAction func NotaMemberisTapped(_ sender: Any) {
self.goToCreateUserVC()
}
}
Only happens when I close the app and reopen it to find it cheats its way through - trying to figure out how to prevent it from happening.
You’re calling your LoggedIn() function based on whether or not there is a value in the keychain. I’m assuming you’re saving that information to the keychain whether or not validation has been performed.

UserDefaults not persisting data in TextView

Edit: I noticed I am using different keys (was playing with the code), but even if they are same, I get similar results.
I have looked all over StackOverflow but not found a solution for this. I have a textview on a second view in my storyboard, and I need it to persist the data without any button press. I should go to the view, see the text and when the app backgrounds (or view disappears), it should persist that data.
I've got it working to the point the text appears on the second view (via a segue), but it doesn't update with new text which is appended via the loop and doesn't persist the data. Also, if I tap a "clear" button to clear the view, it clears the textview but as soon as I go back to the view, the data comes back.
In essence it's stuck on saving just one line of my textview and nothing more. Any help would be appriciated as I am very new to Xcode. (Latest Xcode with Swift 3).
import Foundation
import UIKit
class CalcViewController: UIViewController {
var tape = Array<String>()
#IBOutlet weak var calcTape: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
for eachNewLineInTape in tape {
calcTape.text = calcTape.text + eachNewLineInTape + "\r\n"
}
let sel:Selector = #selector(self.appMovedToBackground)
let notificationCentre = NotificationCenter.default
notificationCentre.addObserver(self, selector: sel, name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
let defaults = UserDefaults.standard
if let userDataNew:AnyObject = defaults.object(forKey: "userDataNew") as AnyObject?{
calcTape.text = (userDataNew as! Array).first
print("SAVED")
}
else {
print("not saved")
}
}
#IBAction func clearAll(_ sender: UIButton) {
// if the tape is not empty...
if (calcTape.text != ""){
// empty it (set to empty string)
calcTape.text = ""
let bundleIdentifier = Bundle.main.bundleIdentifier
UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier!)
} else {
// otherwise, it was already empty, so display a message
let alert = UIAlertController(title: "Alert", message: "Nothing to clear", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
func appMovedToBackground(){
print("App moved to background")
let myText = calcTape.text
UserDefaults.standard.set(myText, forKey: "savedStringKey")
UserDefaults.standard.synchronize()
print("saved")
}
}
I assume that you tested and verified that appMovedToBackground is called.
First, as it was mentioned in the comments, you need to use the same key.
Then, in your saving code:
let myText = calcTape.text
UserDefaults.standard.set(myText, forKey: "savedStringKey")
myText has String type, so it should store it as is.
But then in your loading code, you are doing something really strange. This line should succeed and userDataNew will have your saved string.
if let userDataNew:AnyObject = defaults.object(forKey: "userDataNew") as AnyObject?{
But after that you are force casting it to Array. Your program must crash straight away:
calcTape.text = (userDataNew as! Array).first
The correct way for loading should be just:
calcTape.text = UserDefaults.standard.string(forKey: "savedStringKey")
Slightly off topic, but a few improvements in your code:
Instead of this:
for eachNewLineInTape in tape {
calcTape.text = calcTape.text + eachNewLineInTape + "\r\n"
}
you could write:
calcTape.text = tape.joined(separator: "\r\n")
Sorry for answering my own question (not sure how else to do it and make it clear), but this is how the code looks now:
import Foundation
import UIKit
class ViewControllerTwo: UIViewController {
var tape = Array<String>()
#IBOutlet weak var calcTape: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
calcTape.text = tape.joined(separator: "\r\n")
let sel:Selector = #selector(self.appMovedToBackground)
let notificationCentre = NotificationCenter.default
notificationCentre.addObserver(self, selector: sel, name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
let defaults = UserDefaults.standard
if let _:AnyObject = defaults.object(forKey: "savedStringKey") as AnyObject?{
calcTape.text = UserDefaults.standard.string(forKey: "savedStringKey")
print("SAVED")
}
else {
print("not saved")
}
}
#IBAction func clearAll(_ sender: UIButton) {
// if the tape is not empty...
if (calcTape.text != ""){
// empty it (set to empty string)
calcTape.text = ""
let bundleIdentifier = Bundle.main.bundleIdentifier
UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier!)
} else {
// otherwise, it was already empty, so display a message
let alert = UIAlertController(title: "Alert", message: "Nothing to clear", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
func appMovedToBackground(){
print("App moved to background")
let myText = calcTape.text
UserDefaults.standard.set(myText, forKey: "savedStringKey")
print("saved in bg")
}
}

iOS Swift fetching more data for collection view after having navigated to detail view not working

I am working on an assignment for a job interview. I have finished most of the assignment. There's only a bug i can't figure out. I have been trying for three days now.
I had to make a client app for the Flickr API that allows users to search for photos using specific words. Display the results in a collection view with infinite scroll. And when a photo is selected it should show the details of the photo in a detail view.
The bug:
Everything is working if i stay in the collection view. I can search over and over again and the infinite scroll is also working.As soon as a specific index in the index path is hit. A new request is sent with the same search term. But if i select a photo and then navigate back to the collection view and try a new search nothing comes back and my error handeling returns an error. (the error is not a console error).Also when navigating back from detail to collection. I can still scroll until the index triggers a new request than it also throws an error.
I hope i am explaining it well. I am really getting desperate at the moment. I tried everything i could think of: the request url still works when i try it in the browser.
Please help! If you need more info just ask.
Collection view controller:
import UIKit
// Global variable for holding a search term.
var searchTerm: String?
// Global variable to hold an instance of Reachability.
var reachability: Reachability?
// Enum for changing the textfield placeholder text.
enum TextFieldPlaceHolderText: String {
case Search = "Search"
case Searching = "Searching..."
}
class PhotosViewController: UIViewController {
// MARK: - Outlets
#IBOutlet var collectionView: UICollectionView!
#IBOutlet var searchTextField: UITextField!
// MARK: - Properties
let photoDataSource = PhotoDataSource()
let photoStore = PhotoStore()
// MARK: - View Setup
override func viewDidLoad() {
super.viewDidLoad()
// Sets the data source and delegate.
collectionView.dataSource = photoDataSource
collectionView.delegate = self
// Uses an image to add a pattern to the collection view background.
collectionView.backgroundColor = UIColor(patternImage: UIImage(named: "flickr.png")!)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Checks if the device is connected to the internet.
checkForReachability()
}
// MARK: showAlert
func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
let okAction = UIAlertAction(title: "Ok", style: .Cancel, handler: { (nil) in
self.dismissViewControllerAnimated(true, completion: nil)
})
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
// MARK: - Segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowPhoto" {
if let selectedIndexPath = collectionView.indexPathsForSelectedItems()?.first {
let flickrPhoto = photoDataSource.flickrPhotos[selectedIndexPath.row]
let destinationVC = segue.destinationViewController as! PhotoDetailViewController
destinationVC.flickrPhoto = flickrPhoto
destinationVC.photoStore = photoStore
}
}
}
// MARK: - checkForReachability
func checkForReachability() {
do {
reachability = try Reachability.reachabilityForInternetConnection()
} catch {
print("Unable to create Reachability")
return
}
reachability!.whenReachable = { reachability in
// This is called on a background thread, but UI updates must be on the main thread.
NSOperationQueue.mainQueue().addOperationWithBlock({
if reachability.isReachableViaWiFi() {
print("Reachable via WiFi")
} else {
print("Reachable via Cellular")
}
})
}
reachability!.whenUnreachable = { reachability in
// This is called on a background thread, but UI updates must be on the main thread.
NSOperationQueue.mainQueue().addOperationWithBlock({
print("Not reachable")
self.showAlert("No Internet Connection", message: "Make sure your device is connected to the internet.")
})
}
do {
try reachability!.startNotifier()
} catch {
print("Unable to start notifier")
}
}
}
//MARK: - Extension UICollectionViewDelegate
extension PhotosViewController: UICollectionViewDelegate {
//MARK: - willDisplayCell
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
let flickrPhoto = photoDataSource.flickrPhotos[indexPath.row]
// Downloads the image data for a thumbnail.
photoStore.fetchImageForPhoto(flickrPhoto,thumbnail: true) { (result) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
// The indexpath for the photo might have changed between the time the request started and finished, so find the most recent indeaxpath
let photoIndex = self.photoDataSource.flickrPhotos.indexOf(flickrPhoto)!
let photoIndexPath = NSIndexPath(forRow: photoIndex, inSection: 0)
// When the request finishes, only update the cell if it's still visible
if let cell = collectionView.cellForItemAtIndexPath(photoIndexPath) as? PhotoCollectionViewCell {
cell.updateWithImage(flickrPhoto.image)
}
}
}
}
}
//MARK: - Extension UITextFieldDelegate
extension PhotosViewController : UITextFieldDelegate {
func textFieldShouldReturn(textField: UITextField) -> Bool {
// Checks if the textfield is not empty.
if textField.text!.isEmpty {
self.showAlert("S😉rry", message: "No search term detected, please enter a search term.")
return false
}
else {
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
textField.addSubview(activityIndicator)
activityIndicator.frame = textField.bounds
activityIndicator.startAnimating()
textField.placeholder = TextFieldPlaceHolderText.Searching.rawValue
// Sets the text that the user typed as the value for the searchTerm property.
searchTerm = textField.text!
// Fetches the photos from flickr using the user's search term.
photoStore.fetchPhotosForSearchTerm() {
(photosResult) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
switch photosResult {
case let .Success(photos):
// Checks if photos were found using the search term.
if photos.count == 0 {
self.showAlert("S😞rry", message: "No images found matching your search for: \(searchTerm!), please try again.")
}
activityIndicator.removeFromSuperview()
textField.placeholder = TextFieldPlaceHolderText.Search.rawValue
// Sets the result to the data source array.
self.photoDataSource.flickrPhotos = photos
print("Successfully found \(photos.count) recent photos.")
case let .Failure(error):
self.checkForReachability()
activityIndicator.removeFromSuperview()
textField.placeholder = TextFieldPlaceHolderText.Search.rawValue
self.photoDataSource.flickrPhotos.removeAll()
self.showAlert("", message: "Something went wrong, please try again.")
print("Error fetching photo's for search term: \(searchTerm!), error: \(error)")
}
self.collectionView.reloadSections(NSIndexSet(index: 0))
}
}
textField.text = nil
textField.resignFirstResponder()
self.collectionView?.backgroundColor = UIColor.whiteColor()
return true
}
}
}
The detail view controller:
import UIKit
import Social
class PhotoDetailViewController: UIViewController {
// MARK: - Outlets
#IBOutlet var photoTitleLabel: UILabel!
#IBOutlet var photoIDLabel: UILabel!
#IBOutlet var dateTakenLabel: UILabel!
#IBOutlet var imageView: UIImageView!
// MARK: Properties
var flickrPhoto: FlickrPhoto!
var photoStore: PhotoStore!
let formatter = FlickrAPI.dateFormatter
// MARK: - View Setup
override func viewDidLoad() {
super.viewDidLoad()
//Downloads the image data for large image
photoStore.fetchImageForPhoto(flickrPhoto, thumbnail: false) { (result) -> Void in
switch result {
case let .Success(image):
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.imageView.image = image
}
case let .Failure(error):
print(" Error fetching detail image for photo: \(error)")
}
}
// Formats the date a shorte date that doesn't display the time
formatter.dateStyle = .MediumStyle
formatter.timeStyle = .NoStyle
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Checks if the device is connected to the internet.
checkForReachability()
// Configures the UI.
configureView()
}
// MARK: - checkForReachability
func checkForReachability() {
do {
reachability = try Reachability.reachabilityForInternetConnection()
} catch {
print("Unable to create Reachability")
return
}
reachability!.whenReachable = { reachability in
// this is called on a background thread, but UI updates must be on the main thread, like this:
NSOperationQueue.mainQueue().addOperationWithBlock({
if reachability.isReachableViaWiFi() {
print("Reachable via WiFi")
} else {
print("Reachable via Cellular")
}
})
}
reachability!.whenUnreachable = { reachability in
// this is called on a background thread, but UI updates must be on the main thread, like this:
NSOperationQueue.mainQueue().addOperationWithBlock({
print("Not reachable")
self.showAlert("No Internet Connection", message: "Make sure your device is connected to the internet.")
})
}
do {
try reachability!.startNotifier()
} catch {
print("Unable to start notifier")
}
}
// MARK: - configureView
func configureView() {
photoTitleLabel.text = flickrPhoto.title ?? "No title available"
photoIDLabel.text = flickrPhoto.photoID ?? "ID unknown"
dateTakenLabel.text = formatter.stringFromDate(flickrPhoto.dateTaken) ?? " Date unknown"
}
// MARK: - showShareOptions
#IBAction func showShareOptions(sender: AnyObject) {
// Configure an action sheet to show the sharing options.
let actionSheet = UIAlertController(title: "Share this photo", message: "", preferredStyle: UIAlertControllerStyle.ActionSheet)
let tweetAction = UIAlertAction(title: "Share on Twitter", style: UIAlertActionStyle.Default) { (action) -> Void in
// Check if sharing to Twitter is possible.
if SLComposeViewController.isAvailableForServiceType(SLServiceTypeTwitter) {
let twitterComposeVC = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
twitterComposeVC.addImage(self.imageView.image)
self.presentViewController(twitterComposeVC, animated: true, completion: nil)
}
else {
self.showAlert("Flickr Searcher", message: "You are not logged in to your Twitter account.")
}
}
// Configure a new action to share on Facebook.
let facebookPostAction = UIAlertAction(title: "Share on Facebook", style: UIAlertActionStyle.Default) { (action) -> Void in
if SLComposeViewController.isAvailableForServiceType(SLServiceTypeTwitter) {
let facebookComposeVC = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
facebookComposeVC.addImage(self.imageView.image)
self.presentViewController(facebookComposeVC, animated: true, completion: nil)
}
else {
self.showAlert("Flickr Searcher", message: "You are not logged in to your facebook account.")
}
}
// Configure a new action to show the UIActivityViewController
let moreAction = UIAlertAction(title: "More", style: UIAlertActionStyle.Default) { (action) -> Void in
let activityViewController = UIActivityViewController(activityItems: [self.imageView.image!], applicationActivities: nil)
self.presentViewController(activityViewController, animated: true, completion: nil)
}
let dismissAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Destructive) { (action) -> Void in
}
actionSheet.addAction(tweetAction)
actionSheet.addAction(facebookPostAction)
actionSheet.addAction(moreAction)
actionSheet.addAction(dismissAction)
presentViewController(actionSheet, animated: true, completion: nil)
}
// MARK: showAlert
func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
let okAction = UIAlertAction(title: "Ok", style: .Cancel, handler: { (nil) in
self.dismissViewControllerAnimated(true, completion: nil)
})
alert.addAction(okAction)
self.presentViewController(alert, animated: true, completion: nil)
}
}
My data source:
import UIKit
class PhotoDataSource: NSObject, UICollectionViewDataSource {
//MARK: - Properties
// Array to store the Flickr Photos
var flickrPhotos = [FlickrPhoto]()
// An instance of photoStore.
var photoStore = PhotoStore()
// MARK: - numberOfItemsInSection
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return flickrPhotos.count
}
// MARK: - cellForItemAtIndexPath
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let identifier = "FlickrCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath) as! PhotoCollectionViewCell
let photo = flickrPhotos[indexPath.item]
cell.updateWithImage(photo.image)
print(indexPath.item)
// If you get close to the end of the collection, fetch more photo's.
if indexPath.item == flickrPhotos.count - 20 {
print("Detected the end of the collection")
// Fetch the next batch of photos.
photoStore.fetchPhotosForSearchTerm() {
(photosResult) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
switch photosResult {
case let .Success(photos):
print("Successfully found \(photos.count) recent photos.")
self.flickrPhotos.appendContentsOf(photos)
case let .Failure(error):
self.flickrPhotos.removeAll()
print("Error fetching more photos for search term \(error)")
}
collectionView.reloadSections(NSIndexSet(index: 0))
}
}
}
return cell
}
}
This is the method that throws the error. But only when navigated to the detail view first. Staying in the collection view the method gets call over and over with no problem:
photoStore.fetchPhotosForSearchTerm() {
(photosResult) -> Void in
// Calls the mainthread to update the UI.
NSOperationQueue.mainQueue().addOperationWithBlock() {
switch photosResult {
case let .Success(photos):
print("Successfully found \(photos.count) recent photos.")
self.flickrPhotos.appendContentsOf(photos)
case let .Failure(error):
self.flickrPhotos.removeAll()
print("Error fetching more photos for search term \(error)")
}
collectionView.reloadSections(NSIndexSet(index: 0))
}
}

App is performing segue automatically (Swift 2.0, Firebase 3)

Been smashing my face against the wall all day trying to upgrade my app to the Firebase 3.x code.
I was having a ton of trouble with updating my original userAuth code and decided to just start from scratch. I haven't really been able to test it though because when I run the app it is calling the segue immediately upon loading the initial VC. Obviously I don't want it to do this and I don't know what is causing it.
I've tried deleting the app from the simulator and when I load it back up I get the same result.
Here is my code for the VC:
import UIKit
import FirebaseAuth
class SignInViewController: UIViewController {
#IBOutlet weak var emailField: UITextField!
#IBOutlet weak var passwordField: UITextField!
override func viewDidAppear(animated: Bool) {
if let user = FIRAuth.auth()?.currentUser {
self.signedIn(user)
}
}
#IBAction func didTapSignIn(sender: AnyObject) {
// Sign In with credentials.
let email = emailField.text
let password = passwordField.text
FIRAuth.auth()?.signInWithEmail(email!, password: password!) { (user, error) in
if let error = error {
print(error.localizedDescription)
return
}
self.signedIn(user!)
}
}
#IBAction func didTapSignUp(sender: AnyObject) {
let email = emailField.text
let password = passwordField.text
FIRAuth.auth()?.createUserWithEmail(email!, password: password!) { (user, error) in
if let error = error {
print(error.localizedDescription)
return
}
self.setDisplayName(user!)
}
}
func setDisplayName(user: FIRUser) {
let changeRequest = user.profileChangeRequest()
changeRequest.displayName = user.email!.componentsSeparatedByString("#")[0]
changeRequest.commitChangesWithCompletion(){ (error) in
if let error = error {
print(error.localizedDescription)
return
}
self.signedIn(FIRAuth.auth()?.currentUser)
}
}
#IBAction func didRequestPasswordReset(sender: AnyObject) {
let prompt = UIAlertController.init(title: nil, message: "Email:", preferredStyle: UIAlertControllerStyle.Alert)
let okAction = UIAlertAction.init(title: "OK", style: UIAlertActionStyle.Default) { (action) in
let userInput = prompt.textFields![0].text
if (userInput!.isEmpty) {
return
}
FIRAuth.auth()?.sendPasswordResetWithEmail(userInput!) { (error) in
if let error = error {
print(error.localizedDescription)
return
}
}
}
prompt.addTextFieldWithConfigurationHandler(nil)
prompt.addAction(okAction)
presentViewController(prompt, animated: true, completion: nil);
}
func signedIn(user: FIRUser?) {
MeasurementHelper.sendLoginEvent()
AppState.sharedInstance.displayName = user?.displayName ?? user?.email
AppState.sharedInstance.photoUrl = user?.photoURL
AppState.sharedInstance.signedIn = true
NSNotificationCenter.defaultCenter().postNotificationName(Constants.NotificationKeys.SignedIn, object: nil, userInfo: nil)
performSegueWithIdentifier(Constants.Segues.SignInToFp, sender: nil)
}
}
Can someone please help? Thank you in advance.

Resources