Semaine 1: 2023-01-23 ~ 2023-01-29
Mercredi, on a rencontré Lena et les deux autres membres d'équipes (Gaspard et Natacha) pour la première fois, ainsi que Prof. Guy Lapalme. On a eu une rencontre de 1h30 pendant laquelle les informations administrative et pratiques par rapport au projets ont été présentées. Notre rencontre hebdomadaire aura lieu chaque mercredi à 17h30. Cette semaine, j'ai pris du temps pour explorer ce qui est déjà fait pour Mona iOS. Bien sûr, je configure l'environnement de travail, entre autres, le téléchargement de Xcode.
Semaine 2: 2023-01-30 ~ 2023-02-05
Ce lundi, j'ai rencontré Isabel, l'étudiante qui a travaillé sur l'application iOS de Mona avant moi. On a discuté pendant 45 minutes et elle m'a mentionné les choses à améliorer et qui ne fonctionnent pas avec l'application pour l'instant. Ses informations m'ont beaucoup aidé de savoir ce que je dois travailler pour améliorer cette application. Mercredi, l'équipe a rencontré Lena sur le campus pour la première fois. On s'est parlé de beaucoup de problèmes présents dans l'application iOS. Lena nous a demandé de commencer par des petits changements car les gros changements impliquent souvent une collaboration entre le côté serveur et du client. En continu : J'apprends la structure des codes de l'application, Swift et SwiftUI
Les problèmes que j'ai trouvé (Après la discussion avec Isabel et Lena)
- Problème de l'API: Il faut que l'application récupère les informations directement à travers de l'API au lieu de fichier local
- Amélioration du filtrage et le barre de recherche
- Affichage du tutorial pour l'usager lors du premier lancement de l'application
- Découvert du jour: Assurer que les arts ciblées et collectées ne seront pas affichés. Et que le découvert inclut aussi des lieux culturels et des patrimoines
Semaine 3: 2023-02-06 ~ 2023-02-12
Ce mercredi, comme d'habitude, on a eu une rencontre en ligne. Pendant cette rencontre on a relevé beaucoup de changements à faire dans l'application mobile. Ci-dessous est une liste de changements potentiels à faire dans l'application mobile
- Assurer que la mode hors ligne fonctionne. (Problème un peu compliqué: Il faut assurer que les photos seront stockés temporairement. Comment peut-on uploader ces photos en bulk une fois la connexion rétablie ?? Comment peut-on donner le choix à l'usger de se connecter au réseau ou non
- Décider s'il faut retirer la barre de navigation pour le tutoriel dans l'onglet "Autres" (Une décision à prendre ensemble avec le graphiste)
- Problème relevé la semaine précédente: Découvert du jour: Assurer que les arts ciblées et collectées ne seront pas affichés. Et que le découvert inclut aussi des lieux culturels et des patrimoines
Cette semaine j'ai terminé la modification de l'écran du tutoriel pour assurer que l'écran du tutoriel soit lancé pour un nouvel usager, et qu'il y a un bouton de fermer à la dernière page. J'ai aussi ajouté un mécanisme pour assurer que l'usage doive entrer un nom d'usager avant de continuer. Avant, cela n'existait pas. À partir de cette semaine, je vais concentrer sur l'amélioration de l'écran de découvert du jour pour qu'il puisse contenir ces trois types d'arts. Cela est un peu compliqué parce qu'il faut changer beaucoup de choses et il faut une solution adéquate.
Semaine 4: 2023-02-13 ~ 2023-02-19
Cette semaine, je me concentre toujours sur le découvert du jour pour que cela prenne en charge les 3 types d'art. Cela demande aussi un changement dans le code de Swift UI vu que les 3 types d'arts sont dans 3 classes différentes. C'est possible que l'on trouve une meilleure solution pour régler ce problème. Toutefois, pour l'instant, je vais me procéder avec cette solution qui fonctionne sûrment.
On a aussi mentionné le changement de permettre les usagers de choisir une photo déjà pris depuis leur album de photo. Cela est à faire après avoir modifié le découvert du jour
On a aussi discuté avec Gaspard (Développeur d'Android dans notre équipe) pour migrer l'application Mona à un framework (Soit Ionic, soit React Native). Gaspard a bien fait un petit exposé, et Lena et nous, on a bien discuté les enjeux et les problèmes potentiels de cette décision (Temps d'apprentissage, faisabilité avant la fin de notre mandat etc.). À la fin, Lena est d'accord que la migration vers un framework est une solution faisable. Gaspard et moi, on va faire un prototype, nommé "Baby Mona" pour voir comment ça fonctionne dans un framework, tout en continuant à maintenir l'application existante. Plus de détails concernant comment on vas répartir les tâches, et aussi quel framework à choisir sont à décider plus tard avec Gaspard. Ce n'est qu'une décision initiale
Semaine 5: 2023-02-20-2023-02-26
Cette semaine j'ai fini l'amélioration du découvert du jour. Plus précisément, j'ai réorganisé les "DataModel"s. Avant, on avait trois modèles de données individuels: ArtworkDataModel
, PlaceDataModel
, PatrimoineDataModel
. Cela ne facilite pas l'inclusion de 3 types
d'art dans le découvert du jour. Cela n'est pas une solution optimale de créer 3 vues dans Swift UI pour accommoder ces 3 types d'art. Par conséquent, j'ai décidé de créer un modèle de donnée en commun où j'ai extrait les paramètres communs de 3 types d'art, qui s'appelle MonaDataModel
qui est un protocole auquel les 3 autres modèles de donnée se conforment.
En faisant cela, il nous permet de stocker simplement ces 3 types d'art sans besoin de distinguer les types. Seulement quand on aura besoin d'accéder les informations particulières, on va "cast" l'objet de type général ver le type particulier.
Pour faire cela, et grâce à l'inspiration de Lena, j'ai décidé de créer un tableau qui contient des numéros. Chaque fois, on va prendre aléatoirement un numéro, et si le numéro tombe dans un interval spécific, on va ensuite décider quel type d'art est à inclure dans notre découvert du jour. Le numéro aléatoirement choisi sera aussi gardé pour que le découvert du jour ne change pas avant la fin de la journée, et sera aussi utilisé par d'autres classes pour qu'on puisse savoir la type vraie de l'art afin de pouvoir faire le cast.
Dans Swift UI, on a profité de cette nouvelle structure pour pouvoir alléger des parties répétitives afin d'accommoder les trois types d'art. Toutefois, vu qu'il y a sûrment des éléments qui sont propres à chaque type d'art, il faut toujours créer certains éléments particuliers pour cela.
Snippet du Code: MonaModel
//
// Monamodel.swift
// Mona
//
// Created by Yan Zhuang on 2023-02-16.
//
import Foundation
protocol MonaModel: Codable, Identifiable {
var id: Int {get}
var duplicates: [Int]? {get}
var source: String {get}
var territory: String? {get}
var borough: String? {get}
var location: Location {get}
var category: LocalizedString? {get} //For Badges
var collection: String? {get set}
var isCollected: Bool? {get set}
var isTargeted: Bool? {get set}
}
// Different Structs for data type used by ArtworkModel and PlaceModel and PatrimoineModel
struct Accessibilities: Codable {
let fr: [String]
let en: [String]
}
struct Title: Codable {
let fr: String?
let en: String?
}
struct Location: Codable {
let lat, lng: Double
}
struct LocalizedString: Codable {
let fr, en: String
}
Snippet du Code: CollectedViewModel
private init() {
if mainDataPublisher.allArtworks.isEmpty
{ return }
var random = 0
// Make sure that the dailyArtwork doesn't change in the same day.
// Retrieve the already saved random if it is the same date
// If not, generate a new random (A new day, or first time user)
// We use Eastern Time to verify. Data will be stored in Eastern Time time.
if let date = getSavedRandomDate(){
var calender = Calendar.current
calender.timeZone = TimeZone(identifier: "America/Toronto")!
let dateRandom = torontoDateFromDate(date: date)
if calender.isDateInToday(dateRandom){
// Something is wrong if the random number returned is nil
if let randomNumber = getSavedRandom(){
random = randomNumber
} else{
return
}
random = getSavedRandom()!
print("Saved Random is \(random)")
} else{
random = getRandom()
removeSavedDailyArt()
setSavedRandom(random: random)
print("New Random is \(random)")
}
} else{
random = getRandom()
setSavedRandom(random: random)
print("First time Random is \(random)")
}
// Make sure a random is assigned ! 0 is not possible, means something is wrong !!
guard random > 0 else {return}
// Decide if we will be getting Daily Artwork, Place or Patrimoine
switch random{
case 1..<5:
guard let randomArtwork = getRandomArtwork() else {return}
guard let savedDailyArt = getSavedDailyWorkId() else {
dailyWork = randomArtwork
setSavedDailyWork(work: randomArtwork)
print("Daily Artwork in guard is \(String(describing: dailyWork as? ArtworkModel))")
return
}
dailyWork = getAllArtworks().filter{$0.id == savedDailyArt}.first
print("Daily Artwork is \(dailyWork as! ArtworkModel)")
case 5..<10:
guard let randomPlace = getRandomPlace() else {return}
guard let savedDailyArt = getSavedDailyWorkId() else {
dailyWork = randomPlace
setSavedDailyWork(work: randomPlace)
print("Daily Place in guard is \(String(describing: dailyWork as? PlaceModel))")
return
}
dailyWork = getAllPlaces().filter{$0.id == savedDailyArt}.first
print("Daily Place is \(String(describing: dailyWork as? PlaceModel))")
default:
guard let randomPatrimoine = getRandomPatrimoine() else {return}
guard let savedDailyArt = getSavedDailyWorkId() else {
dailyWork = randomPatrimoine
setSavedDailyWork(work: randomPatrimoine)
print("Daily Patrimoine in guard is \(String(describing: dailyWork as? PatrimoineModel))")
return
}
dailyWork = getAllPatrimoines().filter{$0.id == savedDailyArt}.first
print("Daily Patrimoine is \(String(describing: dailyWork as? PatrimoineModel))")
}
}
Semaine 6: 2023-02-27 ~ 2023-03-05
Durant la dernière semaine, on a rencontré la graphiste Barabara qui nous a présenté le Figma et aussi le nouveau tutoriel qu'elle a conçu. Dès que son retour des vacances, elle va travailler sur la conception de l'écran de log-in. Après cela, on pourrait
commencer à développer la fonction de log-in qui permettra les usagers à sauvegarder leurs photos prises et récupérer leurs données si jamais il/elle change un cellulaire. Cette semaine j'ai réglé des bugs dans le "découvert du jour". Le problème est à cause du fuseau horaire différent utilisé
par Date()
et Calender
. Le premier utilise l'horaire de GMT par défaut et le dernier utilise le fuseau horaire d'appareil actuel de l'usager. Cela cause un problème parce qu'on veut que "découvert du jour" ne change pas si on est toujours dans le même jour
Toutefois, Swift ne fournit pas une façon facile pour convertir la date entre des différents fuseaux horaires. Donc j'ai du passé beaucoup de temps là-dessus pour trouver une solution stable. Finalement, je l'ai trouvé. Grâce à cette fonction qui converti l'heure de GMT à l'heure de Tonrot. La date sera stocké
sous format d'un string tout en conservant le fuseau d'horaire de Toronto:
extension Date{
// Make sure the saved date is based on Eastern Time.
// In String format
func torontoDateToString() -> String {
let timeZone = TimeZone(identifier: "America/Toronto")!
let dateFormatter = DateFormatter()
dateFormatter.timeZone = timeZone
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
return dateFormatter.string(from: Date())
}
}
J'ai créé aussi une autre fonction similaire pour pouvoir convertir la date stockée en String vers Date()
aussi en conservant le fuseau d'horaire
// Convert the saved date in String form back to Date type preserving the timezone
private func torontoDateFromDate(date: String) -> Date{
let timeZone = TimeZone(identifier: "America/Toronto")
let dateFormatter = DateFormatter()
dateFormatter.timeZone = timeZone
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
return dateFormatter.date(from: date)!
}
Grâce à la réorganisation de DataModel durant la semaine dernière, j'ai pu supprimer beaucoup de fonctions répétitives dans la class CollectedViewModel()
vu que maintenant il faut simplement sauvegarder les 3 types d'art en tant que MonaModel
au lieu
d'avoir 3 versions différentes. Cela facilite beaucoup de choses
Cette semaine, je vais essayer d'incorporer le nouveau tutoriel en mettant à jour l'interface pour que ça corresponde à celui que Barbara a conçu. Je vais aussi désactiver temporaire le mechanism existant de log-in dans l'application iOS qui génère chaque fois un UUID pour un nouvel usager et les envoie vers le serveur, ce qui cause un problème. (C'était une partie de code qui est censé pour le développement, mais elle est incluse dans la version stable). Finalement, il me faut résoudre le problème avec API pour que l'application puisse obtenir les informations directement du serveur.
Semaine 7: 2023-03-06 ~ 2023-03-12
Cette semaine, on n'a pas eu une rencontre normale. A la place, on est allé à l'exposition au centre phi pour participer à l'exhibition de Ludmila, notre artiste résidente du Mona de l'automne passé.
Par rapport à mon travail, cette semaine, je me concentre sur la construction de l'API pour que l'on puisse télécharger directement les données depuis le serveur. Toutefois, il y a des aspects à considérer quand on incorpore cette fonctionnalité, et ce qui devient le difficulté: 1) Il faut bien considérer la possibilité que l'usager n'ait pas de connexion internet. 2) Il faut bien faire des mises à jour régulières, mais aussi efficace pour qu'on télécharger pas toujours les jeux de données complètes chaque fois.
Aussi, afin de préparer pour l'arrivée éventuelle de l'écran de sign-up, j'ai fait un mock up screen pour que je puisse commencer à écrire le code. On va attendre le retour de Barbara pour finaliser le UI de l'écran de sign-up. Le processus pour compléter l'écran de log-in et les logiques en derrière seront mon focus pour les prochaines semaines. Ce n'est pas facile parce qu'il implique beaucoup de changements dans le code, vu que ce n'était pas une fonction qui existe déjà, et que le code existant ne s'adapte pas bien à notre besoin.
Semaine 8: 2023-03-13 ~ 2023-03-19
Cette semaine, on a rencontré avec le prof Guy Lapalme pour faire un suivi de mi-session. J'ai bien présenté ce que j'ai fait et mon "focus" pour le reste du trimestre: La partie de connectivité de l'application pour qu'elle sois plus moderne avec la possibilité de modifier les photos/commentaires, se connecter/s'inscrire. Toutefois, prof Lapalme nous a suggéré de nous concentrer sur un seul aspect de l'application pour que l'on puisse finir avant la présentation finale. J'ai donc décidé de me concentrer sur la partie d'inscription de l'application, ce qui va prendre un peu de temps
En ce qui concerne le progrès de projet, comme mentionné la semaine passée, je concentre toujours sur la création de l'API pour la fonction de l'inscription. J'ai réussi un écran mock up de login avec des capacités basiques. Il me faut maintenant commencer à réaliser les logiques back-end pour l'inscription. Il me faut aussi tenir compte de futurs changements en créant du code assez flexible et adaptable aux futurs changements. Je vais bien profiter des compétences d'asynchro en Swift pour pouvoir faire des requêtes API. Toutefois, sans avoir une décision finale sur l'UI c'est difficile de déterminer comment on va afficher des informations (Ex: Erreurs, Notifications) parce qu'il faut bien trouver une façon de réagir la réponse du serveur de façon compréhensible pour les usagers. Je vais essayer de créer de code comme placeholder pour qu'on puisse les remplacer une fois que notre conceptrice Barbara aie décidé la conception de l'interface pour l'écran de l'inscription, ainsi que les éléments pertinents (Affichage d'erreur etc.)
Semaine 9: 2023-03-20 ~ 2023-03-26
Cette semaine, on n'a pas de rencontre étant donné que Lena est à une conférence à Paris. On a tous travaillé sur ce qu'on a décidé dans le suivi de mi-session la semaine passée. Je poursuis la création de l'API de la fonction de l'inscription. En profitant des appels asynchro de Swift, la création d'appel API est vraiment simple. Le code devient plus lisible et compréhensible, en comparaison de l'utilisation de "Completion handle" comme avant. Ci-dessous est la fonction responsable de faire l'appel au serveur. On a utilisé simplement un URLRequest pour faire l'appel:
// Returns the response from the server and the status code of the response
private func registerPost(username: String, email: String, password: String, passwordConfirm: String) async throws -> (Dictionary<String, Any>, Int)? {
let url = URL(string: "https://picasso.iro.umontreal.ca/~mona/api/v3/register")
var urlRequest = URLRequest(url: url!)
let headers = [
"Accept" : "application/json",
"Content-Type" : "application/json"
]
urlRequest.allHTTPHeaderFields = headers
urlRequest.httpMethod = "POST"
let parameters = [
"username": username,
"email": email,
"password": password,
"password_confirmation": passwordConfirm
]
do { let requestBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
print(requestBody)
urlRequest.httpBody = requestBody
} catch{
print("error creating data for json")
}
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
config.timeoutIntervalForResource = 60
let (data, response) = try await URLSession(configuration: config).data(for: urlRequest)
do{
let dict = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any]
let responseTest = (response as! HTTPURLResponse).statusCode
return (dict!, responseTest)
} catch {
print("error in do is \(error)")
}
return nil
}
La partie du code en haut est seulement responsable pour faire l'appel. Afin de pouvoir traiter les réponses reçues et les erreurs potentielles, j'ai créé une autre fonction register(...)
.
C'est aussi la fonction que l'interface va appeler.
func register(username: String, email: String, password: String, passwordConfirm: String) async{
do{
if let (data, responseCode) = try await registerPost(username: username, email: email, password: password, passwordConfirm: passwordConfirm){
print("data is : \(data)")
print("--------")
print("responseCode is: \(responseCode)")
var registerErrors: [RegisterError] = []
var errorsAll : [[String]] = []
var flattenedErrorArray: [String] = []
guard responseCode != 404 || responseCode != 500 else{
errorsRegister = [RegisterError.serverError]
hasErrors = true
return
}
if let errors = data["errors"]{
let errorsSequence = errors as! Dictionary <String,[String]>
errorsSequence.map({errorsAll.append($1)})
flattenedErrorArray = errorsAll.flatMap({$0})
registerErrors = convertErrorDescriptionsToRegisterError(errorDescriptionsArray: flattenedErrorArray)
print(registerErrors)
}
for error in registerErrors{
print("error converted is \(error.localizedDescription)")
}
// Make sure the registration proceed without error
guard registerErrors.count == 0 else{
DispatchQueue.main.async {
self.errorsRegister = registerErrors
self.hasErrors = true
}
return
}
if let token = data["token"]{
let keychain = KeychainSwift()
keychain.set(token as! String, forKey: "token")
DispatchQueue.main.async {
self.success = true
}
} else{
// Should either fail previously or has a token. Otherwise there is a problem
DispatchQueue.main.async {
self.errorsRegister = [RegisterError.serverError]
self.hasErrors = true
}
return
}
}
} catch {
print("error is \(error)")
}
}
Il me faut aussi trouver une façon pour pouvoir présenter les erreurs de façon "user-friendly" et dans deux langues différentes. Par conséquent, j'ai décidé de créer une classe d'erreur peronalisée qui s'appelle RegisterError
.
On sera capable de convertir les erreurs reçues du serveur en RegisterError
et ensuite, on peut facilement les afficher dans l'interface. Bien sûr, ici j'ai décidé de les afficher sous forme d'une alerter en concaténant les erreurs reçues.
Comme mentionné en haut, il faut que Barbara prenne une décision créative pour qu'on décide finalement la manière officielle de présenter ces erreurs.
// Customized Error class for potential errors encountered while registering. Localized (In French or in English)
enum RegisterError: String, Error, LocalizedError{
case duplicatedUserName = "The username has already been taken."
case passwordNotStrong = "The password confirmation does not match."
case passwordNotMatch = "The password must be at least 6 characters."
case duplicatedEmail = "The email has already been taken."
case serverError = "Server Error"
case usernameNotValid = "The username may only contain letters, numbers, dashes and underscores."
public var errorDescription: String? {
switch self{
case .duplicatedUserName:
return NSLocalizedString("duplicated_username", comment: "Duplicated Username")
case .passwordNotMatch:
return NSLocalizedString("password_not_match", comment: "Password does not match")
case .passwordNotStrong:
return NSLocalizedString("password_not_strong", comment: "Password is not strong enough")
case .duplicatedEmail:
return NSLocalizedString("duplicated_email", comment: "Duplicated Email")
case .serverError:
return NSLocalizedString("server_error", comment: "Server Error")
case .usernameNotValid:
return NSLocalizedString("username_not_valid", comment: "The username may only contain letters, numbers, dashes and underscores.")
}
}
J'ai bien testé toutes les fonctions que j'ai créées et l'application peut bien réagir face à des erreurs reçues en affichant ces erreurs et en donnant l'usager l'opportunité de réessayer. Au cas où l'inscription soit faite avec succès, on va simplement cacher l'écran d'inscription et procéder avec l'écran de tutoriel. À cause de la limite de temps dans ce cours de projet, on a décidé de laisser la fonction de se connecter ou de se déconnecter pour plus tard.
Semaine 10: 2023-03-27 ~ 2023-04-02
Cette semaine, j'ai décidé de travailler sur la fonctionnalité de "téléchargement de données depuis le serveur". C'est une fonction hyper importante pour MONA. J'ai décidé de travailler d'abord sur les different "DataService" pour les objets (Ex: PatrimoinesDataService).
J'ai dû modifier le mécanisme d'obtenir les données du serveur en profitant du patron "async/await". Ci-dessous est ce que l'on a pour PatrimoinesDataService
// parses JSON already in files
private func getPatrimoinesNewFromFile() throws{
let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let url = directory.appendingPathComponent("patrimoinesInternet.json")
if fileManager.fileExists(atPath: url.path){
let data = try Data(contentsOf: url)
let json = try JSONDecoder().decode(PatrimoineDataModel.self, from: data)
print("patrimoines done")
allPatrimoines = json.data
}
}
func getPatrimoinesNewFromAPI() async throws {
let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let writeURL = directory.appendingPathComponent("patrimoinesInternet.json")
guard let url = URL(string: "https://picasso.iro.umontreal.ca/~mona/api/v3/heritages") else {return}
let (data, _) = try await URLSession.shared.data(from: url)
try data.write(to: writeURL)
print(data)
print("patrimoines done")
try getPatrimoinesNewFromFile()
}
La stratégie est assez directe à comprendre ici. L'application va d'abord télécharger des données brutes (en JSON) depuis le serveur et les écrire dans un fichier local qui sera stocké dans l'appareil de l'usager à moins qu'il supprime l'application. Au lieu de lire les données téléchargées directement, il va ensuite lire les données dans le fichier. Pour le démarrage dans le futur, l'application va toujours lire les données dans le fichier pour générer les objets. Cela seront ces fichiers que l'on va mettre à jour dans le futur aussi pour que l'usage puisse toujours recevoir les dernières données.
Les deux fonctions génèrent bien les erreurs au lieu d'être géré directement dans cette classe. Cela a été fait pour pouvoir faciliter la gestion d'erreur. Je compte de géré les erreurs directement dans la classe APILoadData()
. Je crois que cela va aussi faciliter
l'affichage des erreurs avec SwiftUI. Surtout vu qu'on a déjà le processus d'inscription qui va s'exécuter en même temps, la gestion d'erreur pour n'importe quel processus sera vraiment important pour fournir une bonne expérience d'utilisateur.
Semaine 11: 2023-04-03 ~ 2023-04-09
Je travaille sur l'intégration du processus du téléchargement de données avec l'UI ainsi que la gestion d'erreurs. J'ai décidé de lancer le téléchargement dans APILoadData()
. Vu que ces 4 appels d'API seront exécuté en parallel, cela sera difficile de
savoir exactement d'où provient l'erreur. Donc, j'ai décidé d'afficher un message général aussitôt qu'on reçoit une erreur du téléchargement. Comme suivant:
func downloadDataFromApi() async {
hasErrorDownload = false
do{
try await vmPlaces.getPlacesNewFromAPI()
try await vmArtworks.getArtworksNewFromAPI()
try await vmPatrimoines.getPatrimoinesNewFromAPI()
try await vmBadges.getBadgesNewFromAPI()
} catch {
hasErrorDownload = true
print("normal error is in API \(error)")
}
Ici, la variable hasErrorDownload
est une variable de type publisher. Cela nous donne la possibilité d'informer à l'UI qu'il y a une erreur. Cela va nous permettre d'afficher un message d'erreur à l'usager. J'ai aussi décidé qu'on va lancer le processus d'inscription
d'abord, après on va initier le téléchargement de données. Grâce à ProgressView()
, c'est facile à changer le message affiché sous l'indicateur. Aussi, grâce aux variables de type "Publisher" et la fonction onReceive()
de ProgressView()
,
c'est possible de réévaluer la situation chaque fois qu'on reçoit des nouvelles données (Soit un achèvement de téléchargement soit le succès d'inscription). Comme suivant:
.overlay(!$buttonPressed.wrappedValue ? nil:
Color.gray
.opacity(0.40)
.ignoresSafeArea()
.overlay( //$register.hasErrors.wrappedValue ? nil :
ProgressView(progressViewText)
.onReceive(mainData.$allArtworks){ result in
print("Download Artworks OnReceive \(result.count)")
checkAllDone()
}
.onReceive(mainData.$allPatrimoines){ result in
print("Download Patrimoines OnReceive \(result.count)")
checkAllDone()
}
.onReceive(mainData.$allPlaces){ result in
print("Download Places OnReceive \(result.count)")
checkAllDone()
}
.onReceive(mainData.$allBadges){ result in
print("Download Badges OnReceive \(result.count)")
checkAllDone()
}
.onReceive(register.$success){ result in
print("Register OnReceive \(result)")
if result{
disableTextField = true
}
checkAllDone()
}
)
)
Ici, chaque fois qu'on reçoit une nouvelle valeur, le code dans "closure" sera exécuté. Ici, on a créé une fonction auxiliaire pour nous aider à determiner si le processus d'inscription et le téléchargement sont finis sans problème. Le code de checkAllDone()
est comme suivant:
func checkAllDone(){
if($register.success.wrappedValue && !$mainData.allPatrimoines.wrappedValue.isEmpty && !$mainData.allPlaces.wrappedValue.isEmpty && !$mainData.allArtworks.wrappedValue.isEmpty &&
!$mainData.allBadges.wrappedValue.isEmpty){
firstTimeUser = true
currentUserSignedIn = true
}
}
Au cas d'erreur, il nous faut donc lancer une alerte et arrêter le processus tout de suite. Il nous faut bien tenir en compte de deux situations: 1) L'inscription, ce qui est lancée en premier, a échoué. 2) L'inscription a été faite avec succès (Données dans
la base de données et le token a été généré) mais il y a des erreurs liées avec le téléchargement. La première situation est facile à résoudre, on l'a déjà résolu l'autre semaine. Toutefois, la deuxième est un peu difficile. Tout d'abord, j'ai dû trouver une façon pour pouvoir lancer
l'alerte dans les deux cas. La règle de SwiftUI n'autorise pas de déclarer deux alertes séparées dans la même hiérarchie. Sans vouloir modifier la hiérarchie de View, j'ai dû créer un Binding<Bool>
personnalisé et le contenu d'alerte sera généré à l'aide d'une fonction
auxiliaire comme suivant:
.alert(isPresented: alertPresentCondBiding, content: {onBoardingAlert()})
// Customized Binding to be able to present the Alert whenever error occurs (for register or for downloading)
private var alertPresentCondBiding : Binding<Bool>{
return Binding<Bool>(get: {
return register.hasErrors || loadData.hasErrorDownload}, set: {if !$0 {self.register.hasErrors = false}
})
}
func onBoardingAlert() -> Alert{
if register.hasErrors{
return Alert(title: Text(LocalizedStringKey("error_registration_title")), message: processRegistrationErrors(), dismissButton: .default(Text("OK"), action: {
buttonPressed = false
register.clearErrors()
}))
}
return Alert(title: Text(LocalizedStringKey("error_download")),message: Text(LocalizedStringKey("error_download_msg")), dismissButton: .default((Text("OK")), action:{
buttonPressed = false
}))
}
De plus, vu que le téléchargement ne se démarre qu'après l'inscription. Cela veut dire que s'il y a des erreurs produites lors de téléchargement, l'inscription sera déjà faite avec succès. À cause de cela, j'ai décidé que si l'inscription a été faite avec succès et qu'il y a des erreurs de téléchargement, l'usager ne pourra plus modifier les informations saisies et que le processus d'inscription ne sera pas refaite. Quand l'usager clique le bouton, c'est seulement le téléchargement qui sera exécuté, et ce jusqu'à l'achèvement où l'usager peut accéder au tutoriel de l'application comme avant
La fonctionnalité de téléchargement est presque terminée. Je me suis rendu compte qu'il existe un petit problème avec le téléchargement de données liées avec les Badges
. En effet, le format de JSON (les attributs) sont changés quand l'API a été mise à
jour à Version 3. Pour pouvoir décoder les Badges
, un décodeur personnalisé est créé dans BadgesModel
. Cependant, ce décodeur a été fait basant sur les attributs pour la version 2 de l'API. Cela veut dire que les attributs dans la version 3 ne seront plus reconnus
par le décodeur. Je ne veux pas perturber le système de badges actuel, donc j'ai essayé de trouver les correpondances entre les anciens attributs et ceux nouveaux pour que je puisse réécrire le décodeur sans besoin changer grandement la structure. Suivant est le nouveau décodeur dans
BadgesModel
struct BadgeModel: Decodable, Identifiable {
let id: Int
let type: String?
let title: LocalizedString
let description: LocalizedString
let notification: LocalizedString
let required_count: BadgeRequiredArgs // Required Count
let badgeType: BadgeType
let badgeable: BadgeOptionalArgs // "Badgeable" in v3 data
private enum CodingKeys: String, CodingKey {
case id, type, badgeable, required_count, title, description, notification
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let jsonDecoder = JSONDecoder()
self.id = try container.decode(Int.self, forKey: .id)
self.type = try container.decodeIfPresent(String.self, forKey: .type)
print("type is \(self.type)")
let title : [String:String] = try container.decode([String:String].self, forKey: .title)
self.title = LocalizedString(fr: title["fr"]!, en: title["en"]!)
let description : [String: String] = try container.decode([String:String].self, forKey: .description)
self.description = LocalizedString(fr: description["fr"]!, en: description["en"]!)
let notification : [String: String] = try container.decode([String:String].self, forKey: .notification)
self.notification = LocalizedString(fr: notification["fr"]!, en: notification["en"]!)
let requiredCount : Int = try container.decode(Int.self, forKey: .required_count)
self.required_count = BadgeRequiredArgs(nbArtworks: requiredCount)
let optionalArgs : [String:String]? = try container.decodeIfPresent([String:String].self, forKey: .badgeable)
print("type is \(optionalArgs)")
if let optional = optionalArgs{
let badgeCategory = optional["type"]!
if badgeCategory == "Owner"{
self.badgeType = .collectionBadges
self.badgeable = BadgeOptionalArgs(borough: nil, category: nil, collection: optional["name"]!)
} else if badgeCategory == "Borough"{
self.badgeType = .boroughBadge
self.badgeable = BadgeOptionalArgs(borough: optional["name"]!, category: nil, collection: nil)
} else{
self.badgeType = .categoryBadge
self.badgeable = BadgeOptionalArgs(borough: nil, category: optional["name"]!, collection: nil)
}
} else{
self.badgeType = .generalBadge
self.badgeable = BadgeOptionalArgs(borough: nil, category: nil, collection: nil)
}
}
....
}
Cela conclut bien la fonctionnalité de téléchargement de données du serveur. Il nous reste toujours un autre composant important, c'est de pouvoir mettre à jour les données. Cela demandera encore des efforts vu que l'API retournera les nouveaux objets ainsi que les objets dont les attributs ont été simplement modifiés. Donc, il nous faut bien vérifier quel type il s'agit. S'il s'agit bien d'un objet qui existe bien et dont les attributs sont modifiés, il faut bien modifier ces attributs pour que les données liées à cet objet ne sera pas couvert. Vu que la fin de trimestre s'approche, je vais malheureusement laisser l'implémentation de cette fonctionnalité pour plus tard. Heureusement, on ne s'attend pas à des ajouts majeurs de données dans la base de données de MONA pour l'instant
Semaine 12: 2023-04-10 ~ 2023-04-16
Ce mercredi, on a fait un test interne avec quelques testeurs. Ce test était vraiment utile parce qu'il a relevé bien des problèmes graves que je n'ai pas remarqués avant. Le problème le plus grave était le problème avec DailyWork qui entraîne de crash direct d'application.
En analysant le log de crash, j'ai pu repérer l'erreur. En fait, cela a été une erreur de frappe qui s'est glissée dans le code. La façon dont la logique a été faite cause ce problème n'arrive que de façon
occasionnelle. En fait, un DailyWork
est de type Artwork
quand le random se trouve entre 1 et 4, de type Place
quand le random se trouve entre 5 et 9, et de type Patrimoine
quand il se trouve entre 10 et 15.
Dans le code, j'ai aussi une autre fonction qui va renvoyer le type de DailyWork
selon le nombre de random. Toutefois, dans cette fonction, j'ai utilisé une répartition différente pour ces trois types. Cela cause que l'UI n'arrive pas à caster le DailyWork
en le bon type, et
c'est ce qui a causé le crash.
Un usager, pendant le test interne d'aujourd'hui, a signalé que la couleur utilisée pour le texte dans l'écran d'inscription est très foncée qu'il ne puisse pas lire facilement. Donc, je l'ai bien modifié en blanc si l'usager utilise "Dark Mode" ou en noir sinon. Bien sûr, comme mentionné en haut, l'écran d'inscription doit être modifiée suite au design de Barbara
Suite au test de Guy Lapalme, il nous a signalé des problèmes comme le filtre, le lien non-cliquable dans "À propos" etc. Vu que le filtre et l'écran d'inscription seront à modifier à la prochaine itération, je vais le laisser pour la prochaine itération. J'ai réglé le problème de lien non-cliquable en ajoutant des boutons cliquable à la fin de chaque paragraph pour que les usgaers puissent voir plus en détails dans leur navigateur. J'ai aussi essayé d'adapter le tutoriel aux iphones plus petit, ça devient un peu difficile et j'imagine qu'il faut refaire l'écran tutoriel en construisant dynamiquement le contenu de tutoriel au lieu d'utiliser les images. J'ai trouvé une solution temporaire : Utiliser des flèches pour des iphones plus petites pour qu'ils puissent toujours parcourir les images de tutoriel et quitter à la fin.
Celui-ci est donc le dernier rapport hebdomadaire avant la présentation finale. Pour le reste, je vais concentrer à préparer les diapos pour la présentation et bien répéter pour la présentation. Tout ce qui n'est pas mentionné dans ce rapport sera fait plus tard avec notre contrat en tant que auxiliaire de recherche. Nous discuterons chaque à fois avec Lena pour assurer que nous travaillerons sur les priorités.