singleton property set with async\await func is nil after called later - swift

a property from a singleton is nil when checkin is value.
calling this function from viewDidLoad like this
Task {
do {
try await CXOneChat.shared.connect(environment: .NA1, brandId: 1111, channelId: "keyID")
self.checkForConfig()
} catch {
print(error.localizedDescription)
}
}
this connect function checks for several issues and load from network the config
public func connect(environment: Environment, brandId: Int, channelId: String) async throws {
self.environment = environment
self.brandId = brandId
self.channelId = channelId
try connectToSocket()
channelConfig = try await loadChannelConfiguration()
generateDestinationId()
generateVisitor()
if customer == nil {
customer = Customer(senderId: UUID().uuidString, displayName: "")
try await authorizeCustomer()
} else if authorizationCode.isEmpty{
try await reconnectCustomer()
} else {
try await authorizeCustomer()
}
}
this work ok the self.checkForConfig does some stuff with config in singleton object.
after tapping a button and go for another ViewController. call this func
func loadThread() {
do {
if CXOneChat.shared.getChannelConfiguration()?.settings.hasMultipleThreadsPerEndUser ?? false {
try CXOneChat.shared.loadThreads()
} else {
try CXOneChat.shared.loadThread()
}
} catch {
print(error)
}
}
depending on the config call one or another config value load one or the function. in this case the getChannelConfiguration() is nil and call to loadThread().
func loadThread(threadId: UUID? = nil) throws {
guard let config = channelConfig else {
throw CXOneChatError.missingChannelConfig
}
let eventType = config.isLiveChat ? EventType.recoverLivechat : EventType.recoverThread
guard let brandId = brandId else { throw CXOneChatError.invalidBrandId }
guard let channelId = channelId else { throw CXOneChatError.invalidChannelId }
guard let id = getIdentity(with: false) else { throw CXOneChatError.invalidCustomerId }
let retrieveThread = EventFactory.shared.recoverLivechatThreadEvent(brandId: brandId, channelId: channelId, customer: id, eventType: eventType, threadId: threadId)
guard let data = getDataFrom(retrieveThread) else { throw CXOneChatError.invalidData }
let string = getStringFromData(data)
socketService.send(message: string)
}
here checks for he config in singleton object but throws error because config is nil.
my question is why is nil config in this check. the config is downloaded and store. but when get the values got a null value. Im missing some code here or what I is wrong with this.

Related

Firebase Storage listAll() body not executed

I am new to Firebase and Swift. My previous question was very vague due to a misunderstanding on my part. In a class named "A" for example I am trying to create an object of class "B" that contains the fetchARImageTargets function that I have below. I am trying to assign the array ARImageTargets to a var in class "A" however, the listAll completion is not returned in time, which results in the var being empty. Is there a way that I can edit my function or class to avoid the var being set prematurely?
let ARImageTargetStorageRef = Storage.storage().reference().child("ImageTargets")
self.fetchARImageTargets(ref: ARImageTargetStorageRef)
func fetchARImageTargets(ref: StorageReference) {
ref.listAll { (result, error) in
if let error = error {
print(error)
}
for prefix in result.prefixes {
self.fetchARImageTargets(ref: prefix)
}
for item in result.items {
item.getMetadata { (metadata, error) in
if let error = error {
print(error)
} else {
var imageTarget = ARImageTarget()
item.downloadURL(completion: { (url, error) in
imageTarget.ImageURL = url
})
imageTarget.Id = metadata?.customMetadata?["Id"] as String?
let width = metadata?.customMetadata?["PhysicalWidth"] as String?
imageTarget.PhysicalWidth = CGFloat(truncating: NumberFormatter().number(from: width!)!)
self.ARImageTargets.append(imageTarget)
}
}
}
}
}

Future Combine sink does not recieve any values

I want to add a value to Firestore. When finished I want to return the added value. The value does get added to Firestore successfully. However, the value does not go through sink.
This is the function that does not work:
func createPremium(user id: String, isPremium: Bool) -> AnyPublisher<Bool,Never> {
let dic = ["premium":isPremium]
return Future<Bool,Never> { promise in
self.db.collection(self.dbName).document(id).setData(dic, merge: true) { error in
if let error = error {
print(error.localizedDescription)
} else {
/// does get called
promise(.success(isPremium))
}
}
}.eraseToAnyPublisher()
}
I made a test function that works:
func test() -> AnyPublisher<Bool,Never> {
return Future<Bool,Never> { promise in
promise(.success(true))
}.eraseToAnyPublisher()
}
premiumRepository.createPremium(user: userID ?? "1234", isPremium: true)
.sink { receivedValue in
/// does not get called
print(receivedValue)
}.cancel()
test()
.sink { recievedValue in
/// does get called
print("Test", recievedValue)
}.cancel()
Also I have a similar code snippet that works:
func loadExercises(category: Category) -> AnyPublisher<[Exercise], Error> {
let document = store.collection(category.rawValue)
return Future<[Exercise], Error> { promise in
document.getDocuments { documents, error in
if let error = error {
promise(.failure(error))
} else if let documents = documents {
var exercises = [Exercise]()
for document in documents.documents {
do {
let decoded = try FirestoreDecoder().decode(Exercise.self, from: document.data())
exercises.append(decoded)
} catch let error {
promise(.failure(error))
}
}
promise(.success(exercises))
}
}
}.eraseToAnyPublisher()
}
I tried to add a buffer but it did not lead to success.
Try to change/remove .cancel() method on your subscriptions. Seems you subscribe to the publisher, and then immediately cancel the subscription. The better option is to retain and store all your subscriptions in the cancellable set.

How to always return an array in Vapor 3 and Fluent (even for single entity requests)

I'd like to have an index controller function that returns an array of entities if no request parameter is set or a single entity if the id parameter is set. However, I'd like to always receive an array, in the latter case it just contains only one element.
Here's my function:
final class AddressController {
func index(_ req: Request) throws -> Future<[Address]> {
if let id = try? req.query.get(UUID.self, at: "id") {
// THIS IS NOT WORKING...
return Address.find(id, on: req)
} else {
return Address.query(on: req).all()
}
}
}
final class AddressController {
func index(_ req: Request) throws -> Future<[Address]> {
if let id = try? req.query.get(UUID.self, at: "id") {
return Address.find(id, on: req).map {
guard let address = $0 else { return [] }
return [address]
}
} else {
return Address.query(on: req).all()
}
}
}

How to merge nil cases to Failing case

import MVVMC
import RxSwift
import RxCocoa
import RTVModel
import RTVWebAPI
public class SettingsViewModel: ViewModel {
public var fetchedNotifications: Driver<[NotificationItem]> = .empty()
public var fetchedNotificationsFailed: Driver<String> = .empty()
public var notificationCount: Driver<Int> = .empty()
'''''''''''''''
public var userLoginName: Driver<String> = .empty()
///// userLoginName getting is a optional String.
'''''''''''''''''
public var fetchedUserLoginNameFailed: Driver<String> = .empty()
public func bindNotificationEvents(with trigger: Driver<Void>) {
let webService: Driver<RTVInformationListWebService> = trigger
.map { RTVInformationListParameters() }
.webService()
let result = webService.request()
notificationCount = result.success().map { $0.informationList.maxCount }
fetchedNotifications = result.success()
.map {$0.informationList.notifications}
-------> .map {$0.map {NotificationItem.init(notification: $0)}}
///////////////////////////////////////////////////////////////
Error (Value of optional type 'String?' must be unwrapped to a value of type 'String')
///////////////////////////////////////////////////////////////
fetchedNotificationsFailed = Driver.merge(fetchedNotificationsFailed, result.error().map { $0.message })
}
public func bindUserInfoEvents(with trigger: Driver<Void>) {
let webService: Driver<RTVMobileMenuWebService> = trigger
.map { RTVMobileMenuParameters() }
.webService()
let result = webService.request()
userLoginName = result.success().map { ($0.mobileMenuInfo.username) }
fetchedUserLoginNameFailed = Driver.merge(fetchedUserLoginNameFailed, result.error().map { $0.message })
}
}
extension RTVAPIError {
fileprivate var message: String {
var message = "\(self.localizedDescription)"
if let codeNumber = self.codeNumber {
message += "\n(\(codeNumber))"
}
return message
}
}
This is not really the way you should be using it, since the point of Driver is not to error, but you obviously have an error state, therefore, Observable or Signal would be better.
However, you need to split your signal into successful ones and error ones, something like this:
fetchedNotifications = result.success()
.map {$0.informationList.notifications}
.share(replay: 1)
let success = fetchedNotifications
.filter { $0 != nil }
.map { $0.map { NotificationItem.init(notification: $0) } }
let error = fetchedNotifications
.filter { $0 == nil } // Here would be your "error" state which you can merge later
I might be off with the syntax, I wrote this from memory.
I fixed it by using the catchOnNil
.catchOnNil { return }

Vapor 3 - How to check for similar email before saving object

I would like to create a route to let users update their data (e.g. changing their email or their username). To make sure a user cannot use the same username as another user, I would like to check if a user with the same username already exists in the database.
I have already made the username unique in the migrations.
I have a user model that looks like this:
struct User: Content, SQLiteModel, Migration {
var id: Int?
var username: String
var name: String
var email: String
var password: String
var creationDate: Date?
// Permissions
var staff: Bool = false
var superuser: Bool = false
init(username: String, name: String, email: String, password: String) {
self.username = username
self.name = name
self.email = email
self.password = password
self.creationDate = Date()
}
}
This is the piece of code where I want to use it:
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
// Check if `userRequest.email` already exists
// If if does -> throw Abort(.badRequest, reason: "Email already in use")
// Else -> Go on with creation
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
I could do it like this (see next snippet) but it seems a strange option as it requires a lot of nesting when more checks for e.g. uniqueness would have to be performed (for instance in the case of updating a user).
func create(_ req: Request) throws -> EventLoopFuture<User> {
return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
let userID = userRequest.email
return User.query(on: req).filter(\.userID == userID).first().flatMap { existingUser in
guard existingUser == nil else {
throw Abort(.badRequest, reason: "A user with this email already exists")
}
let digest = try req.make(BCryptDigest.self)
let hashedPassword = try digest.hash(userRequest.password)
let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)
return persistedUser.save(on: req)
}
}
}
As one of the answers suggested I've tried to add Error middleware (see next snippet) but this does not correctly catch the error (maybe I am doing something wrong in the code - just started with Vapor).
import Vapor
import FluentSQLite
enum InternalError: Error {
case emailDuplicate
}
struct EmailDuplicateErrorMiddleware: Middleware {
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is SQLiteError {
response = request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
return response.catchFlatMap { error in
if let response = error as? ResponseEncodable {
do {
return try response.encode(for: request)
} catch {
return request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
}
} else {
return request.eventLoop.newFailedFuture(error: error)
}
}
}
}
The quick way of doing it is to do something like User.query(on: req).filter(\.email == email).count() and check that equals 0 before attempting the save.
However, whilst this will work fine for almost everyone, you still risk edge cases where two users try to register with the same username at the exact same time - the only way to handle this is to catch the save failure, check if it was because the unique constraint on the email and return the error to the user. However the chances of you actually hitting that are pretty rare, even for big apps.
I would make the field unique in the model using a Migration such as:
extension User: Migration {
static func prepare(on connection: SQLiteConnection) -> Future<Void> {
return Database.create(self, on: connection) { builder in
try addProperties(to: builder)
builder.unique(on: \.email)
}
}
}
If you use a default String as the field type for email, then you will need to reduce it as this creates a field VARCHAR(255) which is too big for a UNIQUE key. I would then use a bit of custom Middleware to trap the error that arises when a second attempt to save a record is made using the same email.
struct DupEmailErrorMiddleware: Middleware
{
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response>
{
let response: Future<Response>
do {
response = try next.respond(to: request)
} catch is MySQLError {
// needs a bit more sophistication to check the specific error
response = request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
return response.catchFlatMap
{
error in
if let response = error as? ResponseEncodable
{
do
{
return try response.encode(for: request)
}
catch
{
return request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
}
} else
{
return request.eventLoop.newFailedFuture(error: error )
}
}
}
}
EDIT:
Your custom error needs to be something like:
enum InternalError: Debuggable, ResponseEncodable
{
func encode(for request: Request) throws -> EventLoopFuture<Response>
{
let response = request.response()
let eventController = EventController()
//TODO make this return to correct view
eventController.message = reason
return try eventController.index(request).map
{
html in
try response.content.encode(html)
return response
}
}
case dupEmail
var identifier:String
{
switch self
{
case .dupEmail: return "dupEmail"
}
}
var reason:String
{
switch self
{
case .dupEmail: return "Email address already used"
}
}
}
In the code above, the actual error is displayed to the user by setting a value in the controller, which is then picked up in the view and an alert displayed. This method allows a general-purpose error handler to take care of displaying the error messages. However, in your case, it might be that you could just create the response in the catchFlatMap.