In one of my view controllers I have a function to establish a tcp connection , send and receive messages which is as follows.
func tcpConnection(host: NSString){
var client = TCPClient(addr: String(host) as String, port: Config.tcpPort)
let (success, err) = client.connect(timeout: Config.tcpConnectionTimeout)
log?.debug("Connected \(success)")
guard success else { log?.error("Cannot Connect \(err)"); return}
let (successMsg, failmsg) = client.send(str: self.jsonString)
let data = client.read(Config.expectedByteLength)
guard let d = data else { return }
guard let recievedMsg = String(bytes: d, encoding: NSUTF8StringEncoding) else { return }
self.recivedMsgFromServer = recievedMsg
log?.debug("Recieved msg\(recievedMsg)")
let (clientClosed, errormsg) = client.close()
guard clientClosed else { return }
}
I am using this piece of code twice in the same view controller. So I want to have a generic class for this function . Also I have many guards which I want to replace with a single try catch block.
Also after receiving a message I am doing different things in both the tcp connection functions.
Following is what I have tried until now.
class TcpService{
var jsonString : String = ""
func tcpConnection(host: NSString){
do {
var client = try TCPClient(addr: String(host) as String, port: Config.tcpPort)
let (success, err) = client.connect(timeout: Config.tcpConnectionTimeout)
log?.debug("Connected \(success)")
guard success else { log?.error("Cannot Connect \(err)"); return}
let (successMsg, failmsg) = client.send(str: self.jsonString)
let data = client.read(Config.expectedByteLength)
guard let d = data else { return }
guard let recievedMsg = String(bytes: d, encoding: NSUTF8StringEncoding) else { return }
log?.debug("Recieved msg\(recievedMsg)")
/*Do Something different in every viewController
//For Example
self.Info = Mapper<Info>().map(recievedMsg)
log?.debug("Status\(self.Info?.Status)")
*/
let (clientClosed, errormsg) = client.close()
guard clientClosed else { return }
} catch {
let fetchError = error as NSError
print(fetchError)
// Handle Error
}
}
}
When I try to call this function from the new class in my view controller it does not behave in the same way the function in the view controller did.
Is there anything I should change in the way I have created the class?
Any help will be appreciated as I am very new to swift. Thank you
This is a way your problem could be solved. Skip the custom error throwing part if you don't actually need it. The way to refactor it is using callback function, just send your function into this one, i.e.:
TCPService.connect("some_host", message: "your_message") { response in
Mapper<Info>().map(response)
}
Code:
import Foundation
extension NSError {
static func error(with localized: String) -> NSError {
return NSError(domain: "error", code: 2, userInfo: [NSLocalizedDescriptionKey: localized])
}
}
class TCPService {
static func connect(host: String, andSend message: String, onComplete: String -> ()) {
do {
let client = try TCPClient(addr: host, port: Config.tcpPort)
let (successfulConnection, error) = client.connect(timeout: Config.tcpConnectionTimeout)
log?.debug("Connected \(successfulConnection)")
guard successfulConnection else {
throw NSError.error(with: error)
log?.error("Cannot Connect \(error)")
return
}
let (successfullySent, failureMessage) = client.send(str: message)
let data = client.read(Config.expectedByteLength)
guard successfullySent,
let d = data,
receivedMessage = String(bytes: d, encoding: NSUTF8StringEncoding) else {
throw NSError.error(with: failureMessage)
return
}
log?.debug("Received msg\(receivedMessage)")
onComplete(receivedMessage)
let (clientClosed, errorMessage) = client.close()
guard clientClosed else {
throw NSError.error(with: errorMessage)
return
}
} catch let error as NSError {
let fetchError = error as NSError
print(fetchError)
// Handle errors
}
}
}
Related
I want to return a tuple using an observable but
I get this error in the last return line : " Cannot convert return expression of type '(PublishSubject, Bool)' to return type 'Observable<(MessageStatus, Bool)>' "
struct MessageStatus {
let message: String
let code: Int
}
class ServiceNetwork: Service {
func getStatus() -> Observable<(MessageStatus, Bool)>{
let query = Bundle.main.query ?? ""
let responseMessage = PublishSubject<MessageStatus>()
let isSuccess = PublishSubject<Bool>()
let body = ["query": "\(query)"]
_ = response(body: body)
.map { try JSONDecoder().decode(Data.self, from: $0) }
.subscribe(onNext: { response in
guard response.data != nil else {
let error = response.errors?.data?.first
let failure = MessageStatus(message: error.message , code: error.code)
responseMessage.onNext(failure)
isSuccess.onNext(false)
return
}
let sucess = MessageStatus(message: "Success", code: 200)
responseMessage.onNext(success)
isSuccess.onNext(true)
}) return (responseMessage, isSuccess) // error here
}
}
Just swap the subscribe out for a map. Something like the below:
func getStatus() -> Observable<(MessageStatus, Bool)> {
let query = Bundle.main.query ?? ""
let body = ["query": "\(query)"]
return response(body: body)
.map { try JSONDecoder().decode(Data.self, from: $0) }
.map { response in
guard response.data != nil else {
let error = response.errors?.data?.first
let failure = MessageStatus(message: error.message , code: error.code)
return (failure, false)
}
let success = MessageStatus(message: "Success", code: 200)
return (success, true)
}
}
You have to return Observable stream. Can do it like in below example.
class ServiceNetwork: Service {
func getStatus() -> Observable<(MessageStatus, Bool)>{
Observable.create { subscriber in
let query = Bundle.main.query ?? ""
let body = ["query": "\(query)"]
let disposable = response(body: body)
.map { try JSONDecoder().decode(Data.self, from: $0) }
.subscribe(onNext: { response in
guard response.data != nil else {
let error = response.errors?.data?.first
let failure = MessageStatus(message: error.message , code: error.code)
subscriber.onNext((failure, false))
return
}
let success = MessageStatus(message: "Success", code: 200)
subscriber.onNext((success, true))
})
return Disposables.create {
disposable.dispose()
}
}
}
}
Little advice, think about error handling outside of this function. And, additional, handle onError case.
UPDATED WITH PROPOSED SOLUTION AND ADDITIONAL QUESTION
I'm officially stuck and also in callback hell. I have a call to Firebase retrieving all articles in the FireStore. Inside each article object is a an Image filename that translates into a storage reference location that needs to be passed to a function to get the absolute URL back. I'd store the URL in the data, but it could change. The problem is the ArticleListener function is prematurely returning the closure (returnArray) without all the data and I can't figure out what I'm missing. This was working fine before I added the self.getURL code, but now it's returning the array back empty and then doing all the work.
If anyone has some bonus tips here on chaining the methods together without resorting to PromiseKit or GCD that would be great, but open to all suggestions to get this to work as is
and/or refactoring for more efficiency / readability!
Proposed Solution with GCD and updated example
This is calling the Author init after the Article is being created. I am trying to transform the dataDict dictionary so it get's used during the Author init for key ["author"]. I think I'm close, but not 100% sure if my GCD enter/leave calls are happening in the right order
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
print("Error in setting up snapshot listener - \(error)")
} else {
let fireStoreDispatchGrp = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
//NEW EXAMPLE WITH ADDITIONAL TASK HERE
if let author = $0.data()["author"] as? DocumentReference {
author.getDocument() {(authorSnapshot, error) in
fireStoreDispatchGrp.enter() //1
if let error = error {
print("Error getting Author from snapshot inside Article getDocumentFunction - leaving dispatch group and returning early")
fireStoreDispatchGrp.leave()
return
}
if let newAuthor = authorSnapshot.flatMap(Author.init) {
print("Able to build new author \(newAuthor)")
dataDict["author"] = newAuthor
dataDict["authorId"] = authorSnapshot?.documentID
print("Data Dict successfully mutated \(dataDict)")
}
fireStoreDispatchGrp.leave() //2
}
}
///END OF NEW EXAMPLE
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
fireStoreDispatchGrp.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
dataDict["image"] = url.absoluteString
case .failure(let error):
print("Error getting URL for author: \n Error: \(error) \n forReference: \(reference) \n forArticleID: \(id)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
returnArray.append(newArticle)
}
fireStoreDispatchGrp.leave() ///3
}
}
}
//Completion block
print("Exiting dispatchGroup all data should be setup correctly")
fireStoreDispatchGrp.notify(queue: .main) { ///4
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
Original Code
Calling Setup Code
self.manager.SetupArticleListener() { [weak self] articles in
print("๐๐๐๐๐๐๐In closure function to update articles๐๐๐๐๐๐๐")
self?.articles = articles
}
Article Listener
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
}
}
}
print("๐๐๐๐๐๐๐ sending back completion array \(returnArray)๐๐๐๐๐๐๐")
completion(returnArray)
}
}
updateListeners(for: listener)
}
GetURL
private func getURL(reference: StorageReference, _ result: #escaping (Result<URL, Error>) -> Void) {
reference.downloadURL() { (url, error) in
if let url = url {
result(.success(url))
} else {
if let error = error {
print("error")
result(.failure(error))
}
}
}
}
You need dispatch group as the for loop contains multiple asynchronous calls
public func SetupArticleListener(completion: #escaping ([Article]) -> Void) {
var returnArray = [Article]()
let db = FIRdb.articles.reference()
let listener = db.addSnapshotListener() { (querySnapshot, error) in
returnArray = [] // nil this out every time
if let error = error {
printLog("Error retrieving documents while adding snapshotlistener, Error: \(error.localizedDescription)")
} else {
let g = DispatchGroup() /// 1
querySnapshot?.documents.forEach {
var dataDict = $0.data() //mutable copy of the dictionary data
let id = $0.documentID
if let imageURL = $0.data()["image"] as? String {
let reference = FIRStorage.articles.referenceForFile(filename: imageURL)
g.enter() /// 2
self.getURL(reference: reference){ result in
switch result {
case .success(let url) :
print("Success in getting url from reference \(url)")
dataDict["image"] = url.absoluteString
print("Dictionary XFORM")
case .failure(let error):
print("Error retrieving URL from reference \(error)")
}
if let newArticle = Article(id: id, dictionary: dataDict) {
printLog("Success in creating Article with xformed url")
returnArray.append(newArticle)
}
g.leave() /// 3
}
}
}
g.notify(queue:.main) { /// 4
print("๐๐๐๐๐๐๐ sending back completion array \(returnArray)๐๐๐๐๐๐๐")
completion(returnArray)
}
}
}
updateListeners(for: listener)
}
I try use nested DispatchGroup, there is my code :
class TranslateService {
private let myGroup = DispatchGroup()
func translateText(text:[String],closure:#escaping ((_ success:String?,_ error:Error?) -> Void)) {
var translateString: String = ""
var responseError: Error?
for index in 0...text.count - 1 {
let urlString = "https://translate.yandex.net/api/v1.5/tr.json/translate?key=trnsl.1.1.20171105T134956Z.795c7a0141d3061b.dc25bae76fa5740b2cdecb02396644dea58edd24&text=\(text[index])&lang=fa&format=plain&options=1"
if let allowString = Utilities.shareInstance.getQueryAllowedString(url: urlString) {
if let url = URL(string:allowString){
myGroup.enter()
Alamofire.request(url).responseJSON { response in
guard let responseData = response.data else {
self.myGroup.leave()
return
}
do {
let json = try JSONSerialization.jsonObject(with: responseData, options: [])
if let res = json as? [String:Any] {
if let code = res["code"] as? Int {
if code == 200 {
if let textArr = res["text"] as? [AnyObject] {
let flattArr = Utilities.shareInstance.flatStringMapArray(textArr)
if flattArr.count > 0 {
translateString += "ุ" + flattArr[0]
}
}
}
}
self.myGroup.leave()
}
}catch {
responseError = error
self.myGroup.leave()
}
}
self.myGroup.notify(queue: .main) {
print("Finished all requests.")
print(translateString)
closure(translateString, responseError)
}
}
}
}
}
}
class AddressService {
private let translateService: TranslateService = TranslateService()
private let myGroup = DispatchGroup()
func fetchAddressFromGeographicalLocation(latitude: Double, longitude: Double,closure:#escaping ((_ success:String,_ name:String,_ error:Error?) -> Void)) {
var address: String = ""
let name: String = ""
var responseError: Error?
if let url = URL(string:"https://maps.googleapis.com/maps/api/geocode/json?latlng=\(latitude),\(longitude)&key=AIzaSyAdEzHZfZWyjLMuuW92w5fkR86S3-opIF0&language=fa®ion=IR&locale=fa"){
self.myGroup.enter()
Alamofire.request(url).responseJSON { response in
guard let responseData = response.data else {
self.myGroup.leave()
return
}
do {
let json = try JSONSerialization.jsonObject(with: responseData, options: [])
if let addressDic = json as? [String:Any] {
if let result = addressDic["results"] as? [AnyObject] {
if result.count > 0 {
let flattRes = Utilities.shareInstance.flatMapArray(result)
let item = flattRes[0]
address = item["formatted_address"] as? String ?? ""
var res = address
if res.isContainEnglishCharachter {
self.myGroup.enter()
let resArr = res.components(separatedBy: ",")
var all : [String] = []
for item in resArr {
if item != " " {
all.append(item)
}}
self.translateService.translateText(text: all, closure: {stringAdd,error in
self.myGroup.enter()
if error != nil {
self.myGroup.leave()
}else {
address = stringAdd ?? ""
self.myGroup.leave()
} })
}else {
self.myGroup.leave()
}
}
}
}
}catch {
responseError = error
self.myGroup.leave()
}
self.myGroup.notify(queue: .main) {
// print("Finished all requests.")
closure(address, name, responseError)
}
}
}
}
}
All I want is that the myGroup I put in the class AddressService waiting for the myGroup that I put in the class TranslateService.
but now self.myGroup.notify not call in the AddressService class, So closure not work.
How can solve this problem, Thank you for all the answers.
I think you are over complicating it.
If I understood a bit, what you want to do is the following:
Get an address from the Address service.
Translate some words of that address, one by one, using the translation service.
When using the Address service there is only one call being done, so there is no need to use Dispatch Groups at this point. Dispatch Groups are used to synchronize more than one async call.
For your Translation service you can make good use of the Dispatch groups, since you are doing calls to the service inside a for loop. The problem here is, that the implementation is slightly wrong. You are setting the notification block inside the for loop, and it should be outside, so that it gets only triggered once, when all the calls inside the loop are done.
So move this block outside the for loop in the Translation service:
self.myGroup.notify(queue: .main) {
print("Finished all requests.")
print(translateString)
closure(translateString, responseError)
}
Now "Finished all requests." will only be printed once, when all requests are done.
In the address service you do not need dispatch groups at all. Just wait until the completion block is called.
self.translateService.translateText(text: all, closure: {stringAdd,error in
Everything is done here already.
}
I want to change the following if statements to guards. Doing so throws the following error
Initializer for conditional binding must have Optional type, not '(Bool, String)'
Any idea how I should go about it?
Any help will be appreciated. Thank you
dispatch_async(backgroundQueue, {
let (success, errmsg) = client.connect(timeout: 5)
print("Connected",success)
if success {
let (success, errmsg) = client.send(str: self.jsonString)
print("sent",success)
if success {
let data = client.read(ApplicationConfig.expectedByteLength)
if let d = data {
if let recievedMsg = String(bytes: d, encoding: NSUTF8StringEncoding){
print(recievedMsg)
let (success, errormsg) = client.close()
}
}
}else {
print("Message not sent", errmsg)
}
}
})
}
Because you declare the variable without the ? (non optional) and also given it default value so the guard assume it never nil thats why u got the error, u still can use guard like this guard let object: Type = data else {...}
It sounds like you might be converting if statements into guard let statements, which require an Optional type.
You should be able to use just a straight guard something like:
dispatch_async(backgroundQueue, {
let (success, errmsg) = client.connect(timeout: 5)
print("Connected",success)
guard success else {
return
}
let (success, errmsg) = client.send(str: self.jsonString)
print("sent",success)
guard success else {
print("Message not sent", errmsg)
return
}
let data = client.read(ApplicationConfig.expectedByteLength)
guard let d = data else {
return
}
guard let recievedMsg = String(bytes: d, encoding: NSUTF8StringEncoding) else {
return
}
print(recievedMsg)
let (success, errormsg) = client.close()
})
NOTE: This doesn't actually compile because the code is trying to redefine the success and errormsg constants, but it should give you the general idea.
I am using nsurlsession on RxSwift.
I am facing two problems about nsurlsession on RxSwift.
I created Custom Observable.
This Observable has used nsurlsession.
nsurlsession.datataskwithrequst was canceled everytime on RxSwift.
My code is here
func getWorkInfo(request:NSURLRequest,type1:C.Type,type2:W.Type? = nil) -> Observable<(C?,[W]?, NSHTTPURLResponse)>{
return Observable.create { observer in
var d: NSDate?
if Logging.URLRequests(request) {
d = NSDate()
}
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { (data, response, error) in
guard let response = response, data = data else {
ColorLogger.defaultInstance?.error(error)
observer.on(.Error(error ?? RxCocoaURLError.Unknown))
return
}
guard let httpResponse = response as? NSHTTPURLResponse else {
observer.on(.Error(RxCocoaURLError.NonHTTPResponse(response: response)))
return
}
guard let jsonString: String = NSString(data:data, encoding:NSUTF8StringEncoding) as? String else{
observer.on(.Error(ApiError.FormatError))
return
}
//ใซใฆใณใ็ณป
let countObj = Mapper<C>().map(jsonString)
//ไธ่ฆง็ณป
//ไบใค็ฎใๆๅฎใใฆใชใๅ ดๅใฏnilใ่ฟใ
if type2 != nil{
let aryObj = Mapper<W>().mapArray(jsonString)
observer.on(.Next(countObj,aryObj, httpResponse))
}else{
observer.on(.Next(countObj,nil, httpResponse))
}
observer.on(.Completed)
}
let t = task
t.resume()
return AnonymousDisposable{task.cancel()}
}
}
Above method was called by here.
func getWorkCount(dicParam: NSDictionary) -> Observable<WorkCount?> {
// URLไฝๆ
let strParam = dicParam.urlEncodedString()
let strUrl = Const.ShiftApiBase.SFT_API_DOMAIN+Const.ApiUrl.WORK_COUNT+"?"+strParam
// ๆฑไบบใชในใใใผใฟใๅๅพ
let url = NSURL(string: strUrl)!
let request = NSURLRequest(URL: url)
let client = WorkClient<WorkCount,Work>()
ColorLogger.defaultInstance?.debug(strUrl)
return client.getWorkInfo(request, type1: WorkCount.self)
.observeOn(Dependencies.sharedDependencies.backgroundWorkScheduler)
.catchError{(error) -> Observable<(WorkCount?,[Work]?, NSHTTPURLResponse)> in
print("error")
ColorLogger.defaultInstance?.error("UnknownError")
return Observable.empty()
}
.map { countObj,workObj,httpResponse in
if httpResponse.statusCode != 200 {
throw ApiError.Bad
}
return countObj
}
.observeOn(Dependencies.sharedDependencies.mainScheduler)
}
And My subscribe is here.
/**
ๆค็ดขไปถๆฐใๅๅพ
- parameter param: <#param description#>
*/
func getSearchCount(param: [String:String]){
let dicParam = NSDictionary(dictionary: param)
api.getWorkCount(dicParam)
.catchError{
error -> Observable<WorkCount?> in
switch error{
case ApiError.Bad:
ColorLogger.defaultInstance?.error("status error")
break
case ApiError.FormatError:
ColorLogger.defaultInstance?.error("FormatError")
break
case ApiError.NoResponse:
ColorLogger.defaultInstance?.error("NoResponse")
break
default:
ColorLogger.defaultInstance?.error("UnKnownError")
break
}
return Observable.just(nil)
}
.subscribeNext { [weak self] countObj in
self?.count.value = countObj?.returned_count
}
.addDisposableTo(disposeBag)
}
I have two problems.
1:nsurlsession was canceled every time.I don know reason.
2:Even if I got error on NSURLSession,I could not catch error on "CatchError".
By the way,when i try to use the following code,But nsurlsession might be canceled.
It might be a base nsurlsession on RxSwift.
func getWorkCount4(dicParam: NSDictionary) -> Observable<WorkCount?> {
// URLไฝๆ
let strParam = dicParam.urlEncodedString()
let strUrl = Const.ShiftApiBase.SFT_API_DOMAIN+Const.ApiUrl.WORK_COUNT+"?"+strParam
// ๆฑไบบใชในใใใผใฟใๅๅพ
let url = NSURL(string: strUrl)!
let request = NSURLRequest(URL: url)
let session = ApiBase.sharedObj.createNSURLSession()
return NSURLSession.sharedSession().rx_response(request)
.observeOn(Dependencies.sharedDependencies.backgroundWorkScheduler)
.map { data,httpResponse in
if httpResponse.statusCode != Const.HTTP_RESPONSE.HTTP_STATUS_CODE_OK {
throw ApiError.Bad
}
guard let jsonString: String = NSString(data:data, encoding:NSUTF8StringEncoding) as? String else{
throw ApiError.FormatError
}
let countObj = Mapper<WorkCount>().map(jsonString)
return countObj
}
.observeOn(Dependencies.sharedDependencies.mainScheduler)
}
What is this problem?
I could resolve by myself.
Above method does not have any problem.
Below code has problem.
// MARK: - ๆค็ดขใซใฆใณใ
extension SearchCount{
/// ๆค็ดข็จใฎViewModel
var searchViewModel:SearchViewModel {
return SearchViewModel()
}
/**
ๆค็ดขใฎไปถๆฐใๅๅพ
*/
func getSearchCount(){
setApiParameter()
searchViewModel.getSearchCount(apiParam)
}
}
I defined searchViewModel on Class,the i could resolve.