The sample app here https://getstream.io/ios-activity-feed/tutorial/ is working good using the sample Client api key, app id, and token. Really uploads and downloads Posts that are viewable in the dashboard. However, when I change the api key, app id, and token to mine (I submitted the email to an online Nodejs get stream backend to produce the token), though I am able to push a Post and view in the dashboard, the download fails with this error message:
JSON decoding error: The data couldn’t be read because it isn’t in the correct format..
I compare the data in the two dashboards online and noted that Post in both are of the same types.
At first, I really thought that the FlatFeedViewController in the sample app was a fast and clean solution.
I used 100% code from this link.
In AppDelegate:
apiKey: "xxxxxxxx",
appId: "xxxx",
token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
Client.shared.getCurrentUser(typeOf: GetStreamActivityFeed.User.self) { result in
if result.error == nil, let viewController = self.window?.rootViewController as? ViewController {
viewController.reloadData()
}
}
In the viewcontroller:
class ViewController: FlatFeedViewController<GetStreamActivityFeed.Activity> {
let textToolBar = TextToolBar.make()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
override func viewDidLoad() {
appDelegate.getStream()
if let feedId = FeedId(feedSlug: "timeline") {
let timelineFlatFeed = Client.shared.flatFeed(feedId)
presenter = FlatFeedPresenter<GetStreamActivityFeed.Activity>(flatFeed: timelineFlatFeed, reactionTypes: [.likes, .comments])
}
super.viewDidLoad()
setupTextToolBar()
subscribeForUpdates()
}
func setupTextToolBar() {
textToolBar.addToSuperview(view, placeholderText: "Share something...")
// Enable URL unfurling.
textToolBar.linksDetectorEnabled = true
// Enable image picker.
textToolBar.enableImagePicking(with: self)
textToolBar.sendButton.addTarget(self, action: #selector(save(_:)), for: .touchUpInside)
}
#objc func save(_ sender: UIButton) {
// Hide the keyboard.
view.endEditing(true)
if textToolBar.isValidContent, let presenter = presenter {
textToolBar.addActivity(to: presenter.flatFeed) { result in
print(result) // It will print the added activity or error.
}
}
}
I expected the uploaded Posts to be downloaded in the app. But the decoding error results. The error is thrown in the public final class Activity, specifically in the function: public required init(from decoder: Decoder) throws {
Please help.
Related
UPDATE at the bottom.
I have followed the UIKit section of this Apple iOS Dev Tutorial, up to and including the Saving New Reminders section. The tutorials provide full code for download at the beginning of each section.
But, I want to get FirebaseFirestore involved. I have some other Firestore projects that work, but I always thought that I was doing something not quite right, so I'm always looking for better examples to learn from.
This is how I found Peter Friese's 3-part YT series, "Build a To-Do list with Swift UI and Firebase". While I'm not using SwiftUI, I figured that the Firestore code should probably work with just a few changes, as he creates a Repository whose sole function is to interface between app and Firestore. No UI involved. So, following his example, I added a ReminderRepository.
It doesn't work, but I'm so close. The UITableView looks empty but I know that the records are being loaded.
Stepping through in the debugger, I see that the first time the numberOfRowsInSection is called, the data hasn't been loaded from the Firestore, so it returns 0. But, eventually the code does load the data. I can see each Reminder as it's being mapped and at the end, all documents are loaded into the reminderRepository.reminders property.
But I can't figure out how to get the loadData() to make the table reload later.
ReminderRepository.swift
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
init() {
loadData()
}
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
}
The only difference from the Apple code is that in the ListDataSource.swift file, I added:
var remindersRepository: ReminderRepository
override init() {
remindersRepository = ReminderRepository()
}
and all reminders references in that file have been changed to
remindersRepository.reminders.
Do I need to provide a callback for the init()? How? I'm still a little iffy on the matter.
UPDATE: Not a full credit solution, but getting closer.
I added two lines to ReminderListViewController.viewDidLoad() as well as the referenced function:
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(refreshTournaments(_:)), for: .valueChanged)
#objc
private func refreshTournaments(_ sender: Any) {
tableView.reloadData()
refreshControl?.endRefreshing()
}
Now, when staring at the initial blank table, I pull down from the top and it refreshes. Now, how can I make it do that automatically?
Firstly create some ReminderRepositoryDelegate protocol, that will handle communication between you Controller part (in your case ReminderListDataSource ) and your model part (in your case ReminderRepository ). Then load data by delegating controller after reminder is set. here are some steps:
creating delegate protocol.
protocol ReminderRepositoryDelegate: AnyObject {
func reloadYourData()
}
Conform ReminderListDataSource to delegate protocol:
class ReminderListDataSource: UITableViewDataSource, ReminderRepositoryDelegate {
func reloadYourData() {
self.tableView.reloadData()
}
}
Add delegate weak variable to ReminderRepository that will weakly hold your controller.
class ReminderRepository {
let remindersCollection = Firestore.firestore()
.collection("reminders").order(by: "date")
var reminders = [Reminder]()
weak var delegate: ReminderRepositoryDelegate?
init() {
loadData()
}
}
set ReminderListDataSource as a delegate when creating ReminderRepository
override init() {
remindersRepository = ReminderRepository()
remindersRepository.delegate = self
}
load data after reminder is set
func loadData() {
print ("loadData")
remindersCollection.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.reminders = querySnapshot.documents.compactMap { document in
do {
let reminder = try document.data(as: Reminder.self)
print ("loadData: ", reminder?.title ?? "Unknown")
delegate?.reloadYourData()
return reminder
} catch {
print (error)
}
return nil
}
}
print ("loadData: ", self.reminders.count)
}
}
Please try changing var reminders = [Reminder]() to
var reminders : [Reminder] = []{
didSet {
self.tableview.reloadData()
}
}
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.
I'm using Firestore together with Swift.
I have a singleton data class UserManager. I call this from my different ViewControllers to get data to populate my tableviews. I want the tableviews to automatically update when the collections are updated so I need to use a SnapshotListener. Everything works fine but I'm not sure how to detach from the listener when the Viewcontroller is closed.
In the singleton class I have methods like this below. The method gives a list of users and will be called from several different places around my app.
I also want to give back a reference to the listener so that I can detach from it when the Viewcontroller is closed. But I can't get it working. The below solution gives compiler error.
I've been trying to look at the reference, for example here
https://firebase.google.com/docs/firestore/query-data/listen but I need to get it working when the data is loaded in a singleton class instead of directly in the Viewcontroller. What is the way to go here?
In UserManager:
func allUsers(completion:#escaping ([User], ListenerRegistration?)->Void) {
let listener = db.collection("users").addSnapshotListener { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users, listener)
}
}
}
In ViewController:
override func viewDidLoad() {
super.viewDidLoad()
UserManager.shared.allUsers(completion: { (users, listener) in
self.users = users
self.listener = listener
self.tableView.reloadData()
})
}
deinit {
self.listener.remove()
}
I guess the compiler error that you see is referring to the fact that you are using listener into it's own defining context.
Try this for a change:
In UserManager:
func allUsers(completion:#escaping ([User])->Void) -> ListenerRegistration? {
return db.collection("users").addSnapshotListener { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users)
}
}
}
In ViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.listener = UserManager.shared.allUsers(completion: { (users) in
self.users = users
self.tableView.reloadData()
})
}
deinit {
self.listener.remove()
}
I think that getDocument instead of addSnapshotListener is what you are looking for.
Using this method the listener is automatically detached at the end of the request...
It will be something similar to
func allUsers(completion:#escaping ([User])->Void) {
db.collection("users").getDocument { querySnapshot, error in
if let documents = querySnapshot?.documents {
var users = [User]()
for document in documents {
let user = User(snapshot: document)
users.append(user)
}
completion(users)
}
} }
I'm trying to link two firebase accounts, usually the user would be logged in with social media or email or anonmus account and then a user would sign in with phone. I need to link these two accounts.
parent view
a function in the parent view would be called to sign up with phone number
class welcomeView: UIViewController, FUIAuthDelegate {
//stores current user
var previosUser = FIRUser()
override func viewDidLoad() {
super.viewDidLoad()
if let user = Auth.auth().currentUser {
previosUser = user
}else{
Auth.auth().signInAnonymously() { (user, error) in
if error != nil {
//handle error
}
previosUser = user!
}
}
}
func signUpWithPhone(){
let vc = signUpWithPhoneView()
vc.authUI?.delegate = self
vc.auth?.languageCode = "ar"
self.present(vc, animated: true)
}
}
in the child view (signUpWithPhoneView) I present the FUIPhoneAuth
phoneProvider.signIn(withPresenting: self)
child view
import UIKit
import FirebaseAuthUI
import FirebasePhoneAuthUI
class signUpWithPhoneView: UIViewController {
fileprivate(set) var authUI = FUIAuth.defaultAuthUI()
var didload = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
if !didload { // stops from looping
didload = !didload
guard let authUI = self.authUI else {return}
let phoneProvider = FUIPhoneAuth.init(authUI: authUI)
self.authUI?.providers = [phoneProvider]
>> phoneProvider.signIn(withPresenting: self)
}
}
}
when the user signs in, the child view will be dismissed automatically and I have didSignInWith function that will be Called in the parents view. I need to link the previous user account and the user phone account
parent view
func authUI(_ authUI: FUIAuth, didSignInWith user: FirebaseAuth.User?, error: Error?) {
if let user = user{
// link the the two accounts
}else{
}
}
I tried to link by using
let provider = PhoneAuthProvider.provider(auth: authUI.auth!)
let credential = PhoneAuthProvider.credential(provider)
previosUser.link(with: credential, completion: { (user, error) in
if error != nil {
print("this is linking two acounts error : " , error)
}
})
but there was error in credential
in ...previosUser.link(with: *credential*, completion: ...
Cannot convert value of type '(String, String) -> PhoneAuthCredential' to expected
argument type 'AuthCredential'
any help would be appreciated
thanks
You are getting the provider with authUI.auth and then not using it to get the credential and you are currently using the static reference of the instance method credential.
The instance method itself takes two strings and returns an AuthCredential which is why you're seeing (String, String) -> AuthCredential. You must create the credential using a verification ID and code that you must collect from the user.
guard let auth = authUI.auth else { return }
let provider = PhoneAuthProvider.provider(auth: auth)
let credential = provider.credential(
withVerificationID: "XXXXXXXXXXXX",
verificationCode: "XXXXXX"
)
// ...
I have a user object, which I would like to store the currently logged in user for, as well as have functions that will allow the user to do things, such as logout, login, etc.
Following various tutorials and plenty of posts on here, I've come up with some code that works well so far when logging in via the login page. Here is that code:
import Foundation
import UIKit
// define the protocol for using this class
protocol LoginProtocol {
func didRecieveLoginResponse(status: Bool, message: String?)
}
class User : APIControllerProtocol {
var delegate: LoginProtocol
var username: String?
var company: String?
var auth_token: String?
// more properties here
// initiate the class, must have a delegate as per the protocol
init(delegate: LoginProtocol, username: String?, school: String?) {
self.delegate = delegate
if(username != nil) {
self.username = username
self.company = company
}
}
// login using the API
func login(password: String) {
print("Logging in as " + username! + " - " + company!)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
let api = APIController(delegate: self)
var postFields = [String: String]()
postFields["username"] = username
postFields["password"] = password
postFields["company"] = company
api.request("login",postData: postFields)
}
// API results were received, log in the user if appropriate
func didRecieveAPIResults(originalRequest: String,apiResponse: APIResponse) {
dispatch_async(dispatch_get_main_queue(), {
if(apiResponse.status == true) {
self.auth_token = details["auth_token"]
// more properties here
self.save_to_user_defaults()
self.delegate.didRecieveLoginResponse(true, message:nil )
} else {
self.delegate.didRecieveLoginResponse(false, message:apiResponse.message )
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
})
}
// save the properties to user defaults
func save_to_user_defaults() {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(self.username, forKey: "username")
defaults.setObject(self.company, forKey: "company")
defaults.setObject(self.auth_token, forKey: "auth_token")
// more properties here
}
// load the properties from user defaults
func load_from_user_defaults() {
let defaults = NSUserDefaults.standardUserDefaults()
self.username = defaults.objectForKey("username") as? String
self.company = defaults.objectForKey("company") as? String
self.auth_token = defaults.objectForKey("auth_token") as? String
// more properties here
}
}
The next stage for me is logging in the user via NSUserDefaults - my plan to do this is via something like this:
let user = User()
user.load_from_user_defaults()
However I'm not quite sure:
Whether I'm on the right track (this is my first complete swift app)
If I am, where to put the above 2 lines of code (perhaps the app delegate?), such that when the app is opened, the user in NSUserDefaults (if there is one) is logged back in
How to allow the rest of the app access to the user (I'd like to be able to be able to reference user data in my view controllers, e.g. let pointsLabel.text = users.points)
Thank you so much in advance!
Hope, my answer will help in improving your app:
NSUserDefaults is not secure, it can be easily opened and read, both on device & to a Mac. So user defaults is a good place for preferences and config info, however it's not a good for sensitive information, like passwords. Check the link for more details
Yes, it should be in app delegate. Load the user data and present the view accordingly.
Either you can pass user data to view controller or just read from persistent storage. Depends upon your requirement.