TechBook #4 : Gestion d’images en Swift 4 et Node.js

Avant de commencer, rappelons que dans l’article précédent nous avons montré comment installer des bibliothèques dans une application iOS et consommer des webservices (sign in/sign up) développés en Node.js via Alamofire.

Aujourd’hui, on va s’intéresser à l’envoi des données multiples à un serveur Node.js. On prend le cas de la modification du profil utilisateur de notre application TechBook. Chaque utilisateur de l’application peut télécharger une image de profil stockée sur notre serveur. Nous allons montrer comment développer avec Node.js un web service qui permet de télécharger des images. On verra également consommer le web service depuis notre application Swift.

Vous pouvez télécharger le code source de l’app et de l’API de service via ce lien Github : https://github.com/moben-technology/TechBook.

L’API de téléchargement de l’image de profil d’un utilisateur

Pour télécharger des fichiers dans un serveur Node.js on peut utiliser le plugin ‘multer’ qu’on peut installer comme suit :

 npm install --save multer

Maintenant on doit configurer le téléchargement des images dans le fichier source users.js (ce fichier contient le code de notre endpoint) : modifier le nom des fichiers (date.now()) pour ne pas avoir des fichiers avec le même nom, préciser la racine du dossier des images téléchargées. Le snippet ci-après montre tout cela en code :

var multer = require('multer');
var fs = require('fs');
var path = require('path');
var storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, 'uploads/images/users/')
    },
    filename: function (req, file, cb) {
        cb(null, Date.now() + path.extname(file.originalname)) //Appending extension
}
});
const upload = multer({storage: storage});
var pathFolder = 'uploads/images/users/';

Enfin voici le code mettant à jour le profil de l’utilisateur. Il fonctionne comme suit : suppression de l’ancien photo de profil s’il y en a une et ajout de la nouvelle image, mise à jour du mot de passe si l’utilisateur veut le modifier…. Vous pouvez adapter ce code selon vos besoins. 

// update user
router.post('/updateUser', upload.single('file'), function (req, res) {
    try {
        if (req.file) {
            User.findOne({_id: req.body.userId}, function (err, user) {
                if (user.photo !== undefined) {
                    var fullPath = pathFolder + user.photo;
                    fs.stat(fullPath, function (err, stats) {
                        if (err) {
                            return console.error(err);
                        }
                        // delete old image profile from folder
                        if (user.photo != "avatar.png"){
                            fs.unlink((fullPath), function (err) {
                                if (err) return console.log(err);
                                console.log('file deleted successfully');
                            });
                        }
                    });
                }
            });
        }
        User.findOne({_id: req.body.userId}, function (err, user) {
            if (err) {
                res.json({
                    status: 0,
                    message: ('Error update user') + err
                });
            } else {
                if (!user) {
                    res.json({
                        status: 0,
                        message: ('user does not exist')
                    });
                } else {
                    try {
                        if (req.body.email) {
                            user.email = req.body.email;
                        }
                        if (req.body.firstName) {
                            user.firstName = req.body.firstName;
                        }
                        if (req.body.lastName) {
                            user.lastName = req.body.lastName;
                        }
                        if (req.body.gender) {
                            user.gender = req.body.gender;
                        }
                        if (req.body.age) {
                            user.age = req.body.age;
                        }
                        if (req.file) {
                            user.photo = req.file.filename;
                        }
                        if (req.body.oldPassword && req.body.newPassword) {
                            // check if password matches
                            user.comparePassword(req.body.oldPassword, function (err, isMatch, next) {
                                if (isMatch && !err) {
                                    user.password = req.body.newPassword;
                                    user.save(function (err, savedUser) {
                                        if (err) {
                                            res.json({
                                                status: 0,
                                                message: ('error Update user ') + err
                                            });
                                        } else {
                                            var token = jwt.sign(savedUser.getUser(), 'MySecret', {expiresIn: 36000});
                                            res.json({
                                                status: 1,
                                                message: 'Update user successfully',
                                                data: {
                                                    user: savedUser.getUser(),
                                                    token: token
                                                }
                                            })
                                        }
                                    });
                                } else {
                                    res.json({
                                        status: 0,
                                        message: 'update user failed. Wrong password.'
                                    });
                                }
                            });
                        } else {
                        user.save(function (err, savedUser) {
                            if (err) {
                                res.json({
                                    status: 0,
                                    message: ('error Update user ') + err
                                });
                            } else {
                                var token = jwt.sign(savedUser.getUser(), 'MySecret', {expiresIn: 3600});
                                res.json({
                                    status: 1,
                                    message: 'Update user successfully',
                                    data: {
                                        user: savedUser.getUser(),
                                        token: token
                                    }
                                })
                            }
                        });
                    }
                } catch (err) {
                    console.log(err);
                    res.json({
                        status: 0,
                        message: '500 Internal Server Error',
                        data: {}
                    })
                }
                }
            }
        });
    } catch (err) {
        console.log(err);
        res.json({
            status: 0,
            message: '500 Internal Server Error',
            data: {}
        })
    }
});

Comment accéder à la galerie ou prendre une photo à partir de la caméra en Swift 4

Il faut tout d’abord ajouter une permission d’accès dans le fichier « info.plist » comme suit :

Ensuite on doit ajouter les déclarations des deux protocoles à l’entête de la classe pour la délégation des méthodes d’utilisation de la caméra et de la galerie : 

class UpdateProfileViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

Maintenant, on traite le clic sur le button update profile qui permet de montrer «actionSheet» pour vous donner le choix de télécharger une image à partir de la galerie ou bien de la camera.

    @IBAction func btnUpdateImageProfile(_ sender: Any) {
        let actionSheet = UIAlertController(title: "Select image", message: nil, preferredStyle: .actionSheet)
        let openGalleryAction = UIAlertAction(title: "Gallerie", style: .default) { action in
            if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary) {
                let imagePicker = UIImagePickerController()
                imagePicker.delegate = self
                imagePicker.sourceType = UIImagePickerController.SourceType.photoLibrary;
                imagePicker.allowsEditing = true
                self.present(imagePicker, animated: true, completion: nil)
            }
        let openCameraAction = UIAlertAction(title: "Camera", style: .default) { action in
        }
            if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
                let imagePicker = UIImagePickerController()
                imagePicker.sourceType = UIImagePickerController.SourceType.camera;
                imagePicker.delegate = self
                imagePicker.allowsEditing = true
                self.present(imagePicker, animated: true, completion: nil)
            }
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)
        actionSheet.addAction(openGalleryAction)
        actionSheet.addAction(openCameraAction)
        actionSheet.addAction(cancelAction)
        // this code allow to show action Sheet in ipad
        if let popoverController = actionSheet.popoverPresentationController {
            popoverController.sourceView = self.view
            popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
            popoverController.permittedArrowDirections = []
        }
        present(actionSheet, animated: true, completion: nil)
    }

Après la sélection d’une image on doit développer la méthode prédéfini pour afficher la nouvelle image :

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        var image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage
        if image == nil {
            image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage
        }
        //print(info)
        self.is_image_profile_changed = true
        self.imageProfile.image = image
        self.dismiss(animated: true, completion: nil);
    }

Consommer le web service via Alamofire en envoyant des données multipartFormData.

Rappel : Si on des données de même type ([String :Any]) on doit utiliser la méthode Alamofire.request() . Vous pouvez revoir le précédent article si vous souhaitez en savoir sur l’appel d’API avec Alamofire.

Vous trouvez ci-après le test du web service à travers Postman :

Le code source de la fonction de lancer le web service :

    func confirmUpdateProfile(){
        // execute web service
        self.validationFormLabel.text = ""
        let postParameters = [
            "email": self.emailTxtField.text!,
            "firstName": self.firstNameTxtField.text!,
            "lastName": self.lastNameTxtField.text!,
            "age": self.ageTxtField.text!,
            "gender": self.lastTagGender,
            "userId": self.userConnected._id!,
            "oldPassword": self.oldPasswordTxtField.text ?? "",
            "newPassword": self.newPasswordTxtField.text ?? "",
            ] as [String : Any]
        Alamofire.upload(multipartFormData: { (multipartFormData) in
            for (key, value) in postParameters {
                if value is String {
                    multipartFormData.append((value as AnyObject).data(using: String.Encoding.utf8.rawValue)!, withName: key)
                }
            }
            if (self.is_image_profile_changed == true) {
                multipartFormData.append(self.imageProfile.image!.jpegData(compressionQuality: 0.75)!, withName: "file", fileName: "swift_file.jpeg", mimeType: "image/jpeg")
            }
        }, to:"http://localhost:2500/users/updateUser" , headers: nil)
        { (result) in
            switch result {
            case .success(let upload, _, _):
                upload.uploadProgress(closure: { (progress) in
                    // print (progress)
                })
                upload.responseJSON { response in
                    if response.result.isFailure == true {
                        print("errror upload")
                    }
                    if let result = response.result.value as? [String:Any] {
                        let responseServer = result["status"] as? NSNumber
                        if responseServer == 1 {
                            if  let data = result["data"] as? [String:Any]{
                                if  let userData = data["user"] as? [String:Any] {
                                    self.userConnected = User(userData)
                                    // save object user in NSUserDefaults
                                    self.defaults.value(forKey: "objectUser")
                                    self.defaults.set(userData, forKey: "objectUser")
                                    self.defaults.synchronize()
                                    self.showToast(message: (result["message"] as? String)!)
                                    self.navigationController?.popViewController(animated: true)
                                }
                            }
                        }
                    }
                }
            case .failure(let error):
                print("error from server : ",error)
                break
            }
        }
    }

Dans ce cas, nous avons utilisé la méthode Alamofire.upload() qui prend comme paramètres multipartFormData contenant un tableau de type [String:Any] (clé : valeur) et pour envoyer le fichier on lui a ajouté l’image sous la clé ‘file’ comme indique la capture de Postman ci-dessus.

Afficher Toast en iOS

Si vous remarquez, lors du succès de l’exécution du web service on a appelé une fonction showToast(message : String). En iOS le toast n’est pas un composant prédéfini comme Android Alors on doit manipuler un label avec un design et des fonctionnalités (durée d’affichage).

Vous pouvez utiliser cette fonction dans vos projets Swift 4 pour faire des toasts ! N’hésitez pas à la modifier afin de l’adapter à vos besoins :

    func showToast(message: String) {
        guard let window = UIApplication.shared.keyWindow else {
            return
        }
        let toastLbl = UILabel()
        toastLbl.text = message
        toastLbl.textAlignment = .center
        toastLbl.font = UIFont.systemFont(ofSize: 18)
        toastLbl.textColor = UIColor.white
        toastLbl.backgroundColor = UIColor.black.withAlphaComponent(0.6)
        toastLbl.numberOfLines = 0
        let textSize = toastLbl.intrinsicContentSize
        let labelHeight = ( textSize.width / window.frame.width ) * 30
        let labelWidth = min(textSize.width, window.frame.width - 40)
        let adjustedHeight = max(labelHeight, textSize.height + 20)
        toastLbl.frame = CGRect(x: 20, y: (window.frame.height - 90 ) - adjustedHeight, width: labelWidth + 20, height: adjustedHeight)
        toastLbl.center.x = window.center.x
        toastLbl.layer.cornerRadius = 10
        toastLbl.layer.masksToBounds = true
        window.addSubview(toastLbl)
        UIView.animate(withDuration: 5.0, animations: {
            toastLbl.alpha = 0
        }) { (_) in
            toastLbl.removeFromSuperview()
        }
    }

Enfin voici un sreenshot de notre application qui commence à prendre forme !

Perspective

Dans le prochain article nous travaillerons sur les tableView en Swfit 4 avec différentes méthodes. A bientôt.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *