Running async tcp connection on swiftUi chat app - asynchronous

For a school project, I have to create a swiftUi chat app.
I'm fairly new to swift but managed to use the Network framework to connect to my chat server using the following code :
import Foundation
import Network
class Client {
let connection: Connection
let host: NWEndpoint.Host
let port: NWEndpoint.Port
var queue: DispatchQueue = DispatchQueue.init(label: "keepAlive")
init(host: String, port: UInt16) {
self.host = NWEndpoint.Host(host)
self.port = NWEndpoint.Port(rawValue: port)!
let nwConnection = NWConnection(host: self.host, port: self.port, using: .tcp)
connection = BaburuConnection(nwConnection: nwConnection)
}
func start() {
print("Client started \(host) \(port)")
connection.didStopCallback = didStopCallback(error:)
connection.start()
}
func stop() {
connection.stop()
}
func sendMessage(content: String, channel: UUID) {
let msg = Data(Frame.Message(value: content, channel: channel).serialize())
connection.send(data: msg)
}
func sendCommand(command: CommandKind, args: [String]? = nil) {
let cmd = Data(Frame.Cmd(cmd: Command(kind: command, args: args)).serialize())
connection.send(data: cmd)
}
func isConnected() -> Bool {
return connection.running
}
func didStopCallback(error: Error?) {
if error == nil {
exit(EXIT_SUCCESS)
} else {
exit(EXIT_FAILURE)
}
}
}
import Foundation
import Network
var KeepAlive = 30
var maxReadLength = 4096
class Connection {
let nwConnection: NWConnection
let queue = DispatchQueue(label: "Client connection Q")
var running = false
private var serializedKA = Frame.KeepAlive().serialize()
private lazy var timer = Timer(timeInterval: TimeInterval(KeepAlive), repeats: true) { _ in
self.send(data: Data(Frame.KeepAlive().serialize()))
}
init(nwConnection: NWConnection) {
self.nwConnection = nwConnection
}
var didStopCallback: ((Error?) -> Void)? = nil
func start() {
print("connection will start")
nwConnection.stateUpdateHandler = stateDidChange(to:)
setupReceive()
nwConnection.start(queue: queue)
running = true
DispatchQueue.global(qos: .background).async {
let runLoop = RunLoop.current
runLoop.add(self.timer, forMode: .default)
runLoop.run()
}
}
private func stateDidChange(to state: NWConnection.State) {
switch state {
case .waiting(let error):
connectionDidFail(error: error)
case .ready:
print("Client connection ready")
case .failed(let error):
connectionDidFail(error: error)
default:
break
}
}
private func setupReceive() {
nwConnection.receive(minimumIncompleteLength: 1, maximumLength: maxReadLength) { (data, _, isComplete, error) in
if let data = data, !data.isEmpty {
let message = String(data: data, encoding: .utf8)
print("connection did receive, data: \(data as NSData) string: \(message ?? "-" )")
switch(Frame.parse(reader: BufferedString(str: message!))) {
case .success(let res):
if res is Frame.MessageWrapper {
print("message : ", res)
} else if res is Frame.Cmd {
print("command : ", res)
} else {
print("TODO")
}
case .failure(let err):
print(err)
}
}
if isComplete {
self.connectionDidEnd()
} else if let error = error {
self.connectionDidFail(error: error)
} else {
self.setupReceive()
}
}
}
func send(data: Data) {
nwConnection.send(content: data, completion: .contentProcessed( { error in
if let error = error {
self.connectionDidFail(error: error)
return
}
print("connection did send, data: \(data as NSData)")
}))
}
func stop() {
print("connection will stop")
self.timer.invalidate()
running = false
stop(error: nil)
}
private func connectionDidFail(error: Error) {
print("connection did fail, error: \(error)")
self.stop(error: error)
}
private func connectionDidEnd() {
print("connection did end")
self.stop(error: nil)
}
private func stop(error: Error?) {
self.nwConnection.stateUpdateHandler = nil
self.nwConnection.cancel()
if let didStopCallback = self.didStopCallback {
self.didStopCallback = nil
didStopCallback(error)
}
}
}
The code works fine when I use it with a blocking operation beneath it but I can't think of any good solution for implementing it in mw swiftUi app since I need to keep some sort of reference on the Client object in order to call it's sendMessage and sendCommand methods with a button and notify my ui when I receive a message.
Does anyone has any advises ?
Also, if there is a cleaner way than recursion for async reading, I would gladly change it.

Related

How do I load data using ObservableObject in SwiftUI?

I am trying to transition an app from UIKit to SwiftUI which depends on basic DynamoDB resources but I have hit a snag in forcing the view to refresh as data is added to the list. I have been at this set of code for hours trying different things and I thought I might see if anyone might know why the 'SessionsData' seems to be thrown away and will not accumulate the 'Sessions' objects.
Does anyone have any quick thoughts???
class SessionsData: ObservableObject {
let didChange = PassthroughSubject<SessionsData, Never>()
#Published var data: [Sessions] = [] {
didSet {
didChange.send(self)
}
}
init() {
load()
}
func load() {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
var temp : [Sessions] = []
dynamoDBObjectMapper.scan(Sessions.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result {
for session in paginatedOutput.items as! [Sessions] {
print("Item Found")
temp.append(session)
}
DispatchQueue.main.async {
self.data = temp
self.didChange.send(self)
}
}
print(self.data.count)
return true
})
}
}
struct Events: View {
#ObservedObject var sessionsData = SessionsData()
var body: some View {...}
}
Looks like you've over-complicated the code. The PassthroughSubject is unnecessary. Whenever you change a #Published property, it should trigger an update.
class SessionsData: ObservableObject {
#Published var data: [Sessions] = []
init() {
load()
}
func load() {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
var temp : [Sessions] = []
dynamoDBObjectMapper.scan(Sessions.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result {
for session in paginatedOutput.items as! [Sessions] {
print("Item Found")
temp.append(session)
}
DispatchQueue.main.async {
self.data = temp
}
}
print(self.data.count)
return true
})
}
}
I don't have experience with DynamoDB, but here are a few things from SwiftUI / Combine perspective. In ObseravbleObjects have change a significant bit and and are now declared with objectWillChange and then sending newValue in willSet:
class SessionsData: ObservableObject {
public let objectWillChange = PassthroughSubject<[Sessions], Never>()
public private(set) var items: [Sessions] = [] {
willSet {
objectWillChange.send(newValue)
}
}
init() {
self.items = []
}
public func load() {
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.default()
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 20
var temp: [Sessions] = []
dynamoDBObjectMapper
.scan(Sessions.self,
expression: scanExpression)
.continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result,
let sessions = paginatedOutput.items as? [Sessions] {
temp.append(contentsOf: sessions)
}
DispatchQueue.main.async {
self.items = temp
}
}
return true
})
}
}
For the UI part you have to just call your load() method defined above in .onApear() and everything else should happen magically:
struct Events: View {
#ObservedObject var sessionsData: SessionsData
var body: some View {
List {
ForEach(self.sessionsData.items) { session in
Text(session.name) // or something of that kind
}
} .onAppear(perform: { self.sessionsData.load() })
}
}

Cant Create Accout (Upload Image Swift)

After importing Firebase to my Project which i want to Create for iOS (Swift)
I tried to create a user, with ProfileImage and Username.
When i tap "Create Account" i get an error message it says "Error signing up".
This is the Error Message in the Terminal
2018-07-20 23:15:50.764062+0200 Collect App[3341:120860] [discovery] errors encountered while discovering extensions: Error Domain=PlugInKit Code=13 "query cancelled" UserInfo={NSLocalizedDescription=query cancelled}
I will paste my code here.
//
// SignUpViewController.swift
import Foundation
import UIKit
import Firebase
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
class SignUpViewController: UIViewController {
#IBOutlet weak var userNameTextField: UITextField!
#IBOutlet weak var userEmailTextField: UITextField!
#IBOutlet weak var userPasswordTextField: UITextField!
#IBOutlet weak var confirmuserPasswordTextField: UITextField!
#IBOutlet weak var profileImageView: UIImageView!
#IBOutlet weak var dismissButton: UIButton!
#IBAction func tapToChangeProfileButton(_ sender: Any) {
}
#IBAction func handleDismissButton(_ sender: Any) {
self.dismiss(animated: false, completion: nil)
}
var imagePicker:UIImagePickerController!
private var authUser : User? {
return Auth.auth().currentUser
}
public func sendVerificationMail() {
if self.authUser != nil && !self.authUser!.isEmailVerified {
self.authUser!.sendEmailVerification(completion: { (error) in
// Notify the user that the mail has sent or couldn't because of an error.
})
}
else {
// Either the user is not available, or the user is already verified.
}
}
override func viewDidLoad() {
super.viewDidLoad()
let imageTap = UITapGestureRecognizer(target: self, action: #selector(openImagePicker))
profileImageView.isUserInteractionEnabled = true
profileImageView.addGestureRecognizer(imageTap)
profileImageView.layer.cornerRadius = profileImageView.bounds.height / 2
profileImageView.clipsToBounds = true
//tapToChangeProfileButton.addTarget(self, action: #selector(openImagePicker), for: .touchUpInside)
imagePicker = UIImagePickerController()
imagePicker.allowsEditing = true
imagePicker.sourceType = .photoLibrary
imagePicker.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#objc func openImagePicker(_ sender:Any) {
// Open Image Picker
self.present(imagePicker, animated: true, completion: nil)
}
#IBAction func CreateAccount(_ sender: Any) {
guard let username = userNameTextField.text else { return }
guard let email = userEmailTextField.text else { return }
guard let pass = userPasswordTextField.text else { return }
guard let image = profileImageView.image else { return }
Auth.auth().createUser(withEmail: email, password: pass) { user, error in
if error == nil && user != nil {
print("User created!")
// 1. Upload the profile image to Firebase Storage
self.uploadProfileImage(image) { url in
if url != nil {
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = username
changeRequest?.photoURL = url
changeRequest?.commitChanges { error in
if error == nil {
print("User display name changed!")
self.saveProfile(username: username, profileImageURL: url!) { success in
if success {
self.dismiss(animated: true, completion: nil)
} else {
self.resetForm()
}
}
} else {
print("Error: \(error!.localizedDescription)")
self.resetForm()
}
}
} else {
self.resetForm()
}
}
} else {
self.resetForm()
}
}
}
func resetForm() {
let alert = UIAlertController(title: "Error signing up", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
func uploadProfileImage(_ image:UIImage, completion: #escaping ((_ url:URL?)->())) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let storageRef = Storage.storage().reference().child("user/\(uid)")
// Create a reference to the file you want to download
let starsRef = storageRef.child("images/stars.jpg")
// Fetch the download URL
starsRef.downloadURL { url, error in
if let error = error {
// Handle any errors
} else {
// Get the download URL for 'images/stars.jpg'
}
}
}
func saveProfile(username:String, profileImageURL:URL, completion: #escaping ((_ success:Bool)->())) {
guard let uid = Auth.auth().currentUser?.uid else { return }
let databaseRef = Database.database().reference().child("users/profile/\(uid)")
let userObject = [
"username": username,
"photoURL": profileImageURL.absoluteString
] as [String:Any]
databaseRef.setValue(userObject) { error, ref in
completion(error == nil)
}
}
#IBAction func AlreadyaccountButtonTapped(_ sender: Any) {
print("Already account Button Tapped")
let LoginViewController = self.storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
self.present(LoginViewController, animated: true)
}
//EmailVerificationViewController
#IBAction func ResendEmailVerificationTapped(_ sender: Any) {
self.sendVerificationMail()
}
#IBAction func LoginButtonTapped(_ sender: Any) {
print("Login Button Tapped")
let LoginViewController = self.storyboard?.instantiateViewController(withIdentifier: "LoginViewController") as! LoginViewController
self.present(LoginViewController, animated: true)
}
func displayMessage(userMessage:String) -> Void {
DispatchQueue.main.async
{
let alertController = UIAlertController(title: "Alert", message: userMessage, 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")
DispatchQueue.main.async
{
self.dismiss(animated: true, completion: nil)
}
}
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion:nil)
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
}
}
extension SignUpViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerEditedImage] as? UIImage {
self.profileImageView.image = pickedImage
}
picker.dismiss(animated: true, completion: nil)
}
}
Thanks for your time !
Now the New Code with another i error that i mentioned in the Comment
#IBAction func CreateAccount(_ sender: Any) {
guard let username = userNameTextField.text else { return }
guard let image = profileImageView.image else { return }
Auth.auth().createUser(withEmail: self.userEmailTextField.text!, password: self.userPasswordTextField.text!) { user, error in
if error == nil && user != nil {
print("User created!")
// 1. Upload the profile image to Firebase Storage
self.uploadProfileImage(image, uid: user.uid) { url in
if url != nil {
let changeRequest = Auth.auth().currentUser?.createProfileChangeRequest()
changeRequest?.displayName = username
changeRequest?.photoURL = url
changeRequest?.commitChanges { error in
if error == nil {
print("User display name changed!")
self.saveProfile(username: username, profileImageURL: url!) { success in
if success {
self.dismiss(animated: true, completion: nil)
} else {
self.resetForm()
}
}
} else {
print("Error: \(error!.localizedDescription)")
self.resetForm()
}
}
} else {
self.resetForm()
}
}
} else {
self.resetForm()
}
}
}
func resetForm() {
let alert = UIAlertController(title: "Error signing up", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
func uploadProfileImage(_ image:UIImage, uid:String, completion: #escaping ((_ url:URL?)->())) {
let storageRef = Storage.storage().reference().child("user/\(uid)")
// Create a reference to the file you want to download
let starsRef = storageRef.child("images/stars.jpg")
// Fetch the download URL
starsRef.downloadURL { url, error in
if let error = error {
// Handle any errors
} else {
// Get the download URL for 'images/stars.jpg'
}
}
}
You are getting that error because your url is nil.
It is nil because you are creating the user, upon successful creation self.uploadProfileImage(image) is called in order to get the image url of the current user but the user is not logged in.
A solution would be:
Sign in the user and upon success then call uploadProfileImage.

Unable to make Graph + iCloud works

what I have to write here?
db = Graph(cloud: "iCloud.com.devname.appname", completion: { (done, error) in
if let errore = error {
debugPrint("Error iCloud: \(errore.localizedDescription)")
return
}
})
or
db = Graph(cloud: "fantasyString", completion: { (done, error) in
if let errore = error {
debugPrint("Errore iCloud: \(errore.localizedDescription)")
return
}
})
I tried everything but I'm unable to make iCloud works
Thank you for your help, Daniel
EDIT:
the way I read data form db:
var customers : [Entity] {
let search = Search<Entity>(graph: db).for(types: "Customers")
return search.sync(completion: nil).sorted { ($0["name"] as! String) < ($1["name"] as! String)}
}
the way I save the record:
func newCustomer(name:String, phone:String, mail:String, closure: #escaping ()->()) {
let cliente = Entity(type: "Customers")
cliente["name"] = name
cliente["phone"] = phone
cliente["mail"] = mail
db.sync { (done, error) in
if let errore = error {
debugPrint("Errore addCustomer: \(errore.localizedDescription)")
return
}
if done { closure() }
}
}
EDIT 2: the GraphDelegate implementation:
extension DataManager: GraphDelegate {
func graphWillPrepareCloudStorage(graph: Graph, transition: GraphCloudStorageTransition) {
debugPrint("graphWillPrepareCloudStorage")
if transition == .initialImportCompleted {
debugPrint("iCloud initialImportCompleted ok")
self.clientiCont?.tableView.reloadData()
}
}
func graphDidPrepareCloudStorage(graph: Graph) {
debugPrint("graphDidPrepareCloudStorage")
self.clientiCont?.tableView.reloadData()
}
func graphWillUpdateFromCloudStorage(graph: Graph) {
debugPrint("graphWillUpdateFromCloudStorage")
self.clientiCont?.tableView.reloadData()
}
func graphDidUpdateFromCloudStorage(graph: Graph) {
debugPrint("graphDidUpdateFromCloudStorage")
// refresh clienti
self.clientiCont?.tableView.reloadData()
// refresh lista ordini
self.gestCliCont?.tableOrder.reloadData()
// refresh oridine
self.gestOrdCont?.showOrder()
self.gestOrdCont?.tableProdotti.reloadData()
}
}
EDIT: the iCloud config
Thanks to one of my students I found the bug:
if you make a record this way everything works fine:
let record = Entity(type: "Names", graph: self.db)
but if you use this init it doesn't: let record = Entity(type: "Names")
so the solution is: make a record this way
let record = Entity(type: "Names", graph: self.db)

Asynchronous Request (Order Matters)

I'm using Alamofire to execute a number of asynchronous requests concurrently, and SwiftyJSON to handle the response.
I need help making sure that appending to moviesByCategory occurs in order.
For example, the "top_rated" data response should be the first element appended to moviesByCategory, not "upcoming".
var moviesByCategory = [[JSON]]()
override func viewDidLoad() {
super.viewDidLoad()
let apiEndPoints = ["top_rated", "popular", "now_playing", "upcoming"]
let dispatchGroup = DispatchGroup()
for endPoint in apiEndPoints {
let endPointURL = URL(string: "https://api.themoviedb.org/3/movie/\(endPoint)?api_key=\(apiKey)&language=en-US&page=1")!
dispatchGroup.enter()
getMoviesFromEndPoint(url: endPointURL)
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.tableView.reloadData()
}
}
func getMoviesFromEndPoint(url: URL, group: dispatchGroup) {
Alamofire.request(url).responseData { response in
if let data = response.result.value {
let json = JSON(data: data)
self.moviesByCategory.append(json["results"].arrayValue)
}
}
}
The purpose for DispatchGroup is to reload the UITableView once all requests have completed.
Any help with this would be tremendously appreciated. Please do point out where I am wrong.
Add a completion handler parameter to getMoviesFromEndPoint:
func getMoviesFromEndPoint(url: URL, completion: () -> Void) { ... }
and leave the group within after the network call completed:
getMoviesFromEndPoint(url: endPointURL) {
dispatchGroup.leave()
}
Complete code:
override func viewDidLoad() {
super.viewDidLoad()
let apiEndPoints = ["top_rated", "popular", "now_playing", "upcoming"]
let dispatchGroup = DispatchGroup()
for endPoint in apiEndPoints {
let endPointURL = URL(string: "https://api.themoviedb.org/3/movie/\(endPoint)?api_key=\(apiKey)&language=en-US&page=1")!
dispatchGroup.enter()
getMoviesFromEndPoint(url: endPointURL) {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.tableView.reloadData()
}
}
func getMoviesFromEndPoint(url: URL, completion: () -> Void) {
Alamofire.request(url).responseData { response in
if let data = response.result.value {
let json = JSON(data: data)
self.moviesByCategory.append(json["results"].arrayValue)
}
completion()
}
}

Sqlite.swift no such table error

I have created a function for mapping json data to a sqlite store using sqlite.swift. I am receiving strange behavior though since the first time I try to run an insert I get a "No such table" error". But the inserts work perfectly after that initial throw. My code works by making a connection to the database onload using SQLData.instance.connect(). Then every time a call to insert is placed it initializes the table to make sure it is set up with all the columns within the data.
//attached to button
let userJSON = JSON(["user_id" : id, "username" : email])
SQLData.instance.insert(table: "users", data: userJSON) { result in
if let result = result{
print(result)
}
}
//sql class
class SQLData {
static let instance = SQLData()
var db: Connection? = nil
func connect() {
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
).first!
do {
self.db = try Connection("\(path)/contacts.sqlite3")
if let db = self.db {
try db.run(Table("users").drop(ifExists: true))
}
print("SQLite connection established.")
} catch {
print("Couldn't create SQLite connection.")
}
}
func initTable(table: String, data: JSON, completionHandler: (Table?) -> Void){
if let db = db {
let t = Table(table)
do {
try db.run(t.create(temporary: false, ifNotExists: true) { r in
r.column(Expression<Int>("id"), primaryKey: true)
r.column(Expression<Bool>("synced"), defaultValue: false)
for (key,val) in data {
if val.bool != nil{
r.column(Expression<Bool>(key), defaultValue: false)
} else if val.int != nil {
r.column(Expression<Int>(key), defaultValue: 0)
} else if val.double != nil {
r.column(Expression<Double>(key), defaultValue: 0.0)
} else {
r.column(Expression<String?>(key), defaultValue: "")
}
}
completionHandler(t)
})
} catch let err {
print(err)
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
func insert(table: String, data: JSON, completionHandler: #escaping (Int64?) -> Void){
self.initTable(table: table, data: data) { t in
if let db = db, let t = t {
var query: [Setter] = []
for (key, val) in data {
if val.bool != nil{
query.append(Expression<Bool>(key) <- val.boolValue)
} else if val.int != nil {
query.append(Expression<Int>(key) <- val.intValue)
} else if val.double != nil {
query.append(Expression<Double>(key) <- val.doubleValue)
} else {
query.append(Expression<String?>(key) <- val.stringValue)
}
}
do {
try db.transaction {
let insert = t.insert(query)
let rowid = try db.run(insert)
completionHandler(rowid)
}
} catch let err {
print(err)
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
}
}
Change your connection function as below
func connect(){
do {
let databaseFilePath = Bundle.main.path(forResource: "contacts", ofType: "sqlite3")!
db = try Connection(databaseFilePath)
} catch {
print(error)
}
}
and bu sure your .sqlite3 (or .db) file in to your project hierarchy and copy bundle resources like images

Resources