Meilleures approches architecturales pour créer des applications de mise en réseau iOS (clients REST)

323

Je suis développeur iOS avec une certaine expérience et cette question est vraiment intéressante pour moi. J'ai vu beaucoup de ressources et de documents différents sur ce sujet, mais néanmoins je suis toujours confus. Quelle est la meilleure architecture pour une application en réseau iOS? Je veux dire un cadre abstrait de base, des modèles, qui s'adapteront à toutes les applications de mise en réseau, que ce soit une petite application qui ne dispose que de quelques demandes de serveur ou un client REST complexe. Apple recommande d'utiliser MVCune approche architecturale de base pour toutes les applications iOS, mais MVCni les MVVMmodèles plus modernes n'expliquent où placer le code logique du réseau et comment l'organiser en général.

Dois-je développer quelque chose comme MVCS( Spour Service) et dans cette Servicecouche mettre toutes les APIdemandes et autres logiques de mise en réseau, qui en perspective peuvent être vraiment complexes? Après avoir fait quelques recherches, j'ai trouvé deux approches de base pour cela. Ici, il a été recommandé de créer une classe distincte pour chaque demande de réseau vers un service Web API(comme une LoginRequestclasse ou une PostCommentRequestclasse, etc.) qui hérite tous de la classe abstraite de la demande de base AbstractBaseRequestet en plus de créer un gestionnaire de réseau global qui encapsule le code de réseau commun et d'autres préférences (il peut s'agir de AFNetworkingpersonnalisation ouRestKittuning, si nous avons des mappages d'objets complexes et la persistance, ou même une propre implémentation de communication réseau avec une API standard). Mais cette approche me semble une surcharge. Une autre approche est d'avoir un singleton APIrépartiteur ou une classe de gestionnaire comme dans la première approche, mais pas de créer des classes pour chaque demande et au lieu d'encapsuler chaque demande comme méthode publique d'instance de cette classe de gestionnaire comme: fetchContacts, loginUserméthodes, etc. Alors, que est le meilleur et le meilleur moyen? Y a-t-il d'autres approches intéressantes que je ne connais pas encore?

Et dois-je créer une autre couche pour tous ces trucs de réseautage comme Service, ou une NetworkProvidercouche ou quoi que ce soit au-dessus de mon MVCarchitecture, ou cette couche doit être intégrée (injectée) dans des MVCcouches existantes , par exemple Model?

Je sais qu'il existe de belles approches, ou comment ces monstres mobiles comme le client Facebook ou le client LinkedIn gèrent-ils la complexité exponentiellement croissante de la logique de mise en réseau?

Je sais qu'il n'y a pas de réponse exacte et formelle au problème. Le but de cette question est de rassembler les approches les plus intéressantes de développeurs iOS expérimentés . La meilleure approche suggérée sera marquée comme acceptée et récompensée par une prime de réputation, d'autres seront votées. C'est surtout une question théorique et de recherche. Je veux comprendre l'approche architecturale de base, abstraite et correcte pour les applications de mise en réseau dans iOS. J'espère des explications détaillées de la part de développeurs expérimentés.

MainstreamDeveloper00
la source
14
N'est-ce pas une question de "liste de courses"? Je viens d'avoir une question rejetée en enfer et clôturée car il a été déclaré que «ce qui est le meilleur» des questions de type suscitent trop de débats non constructifs. Qu'est-ce qui fait que cette question de liste de courses est une bonne question digne de votes positifs et d'une prime alors que d'autres sont fermés?
Alvin Thompson
1
En règle générale, la logique réseau irait dans le contrôleur, ce qui modifierait un objet modèle et avertirait tout délégué ou observateur.
quellish
1
Questions et réponses très intéressantes. Après 4 ans de codage iOS, et en essayant de trouver la plus belle façon d'ajouter une couche réseau à l'application. Quelle classe devrait avoir la responsabilité de gérer une demande de réseau? Les réponses ci-dessous sont vraiment pertinentes. Merci
darksider
@JoeBlow ce n'est pas vrai. L'industrie des applications mobiles repose encore largement sur les communications serveur-client.
scord

Réponses:

327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: il n'y a pas d' approche "la meilleure" ou "la plus correcte" pour construire une architecture d'application. C'est un travail très créatif. Vous devez toujours choisir l'architecture la plus simple et la plus extensible, qui sera claire pour tout développeur, qui commence à travailler sur votre projet ou pour d'autres développeurs de votre équipe, mais je suis d'accord, qu'il peut y avoir un "bon" et un "mauvais" " architecture.

Vous avez dit:, collect the most interesting approaches from experienced iOS developersje ne pense pas que mon approche soit la plus intéressante ou la plus correcte, mais je l'ai utilisée dans plusieurs projets et j'en suis satisfaite. Il s'agit d'une approche hybride de celles que vous avez mentionnées ci-dessus, ainsi que des améliorations de mes propres efforts de recherche. Je m'intéresse aux problèmes de construction d'approches, qui combinent plusieurs modèles et idiomes bien connus. Je pense que beaucoup de modèles d'entreprise de Fowler peuvent être appliqués avec succès aux applications mobiles. Voici une liste des plus intéressantes, que nous pouvons appliquer pour créer une architecture d'application iOS ( à mon avis ): couche de service , unité de travail , façade distante , objet de transfert de données ,Passerelle , supertype de couche , cas spécial , modèle de domaine . Vous devez toujours concevoir correctement une couche de modèle et ne pas oublier la persistance (cela peut augmenter considérablement les performances de votre application). Vous pouvez utiliser Core Datapour cela. Mais vous ne devez pas oublier, ce Core Datan'est pas un ORM ou une base de données, mais un gestionnaire de graphes d'objets avec la persistance comme une bonne option. Ainsi, très souvent, cela Core Datapeut être trop lourd pour vos besoins et vous pouvez envisager de nouvelles solutions telles que Realm et Couchbase Lite , ou créer votre propre couche de mappage / persistance d'objets légers, basée sur SQLite brut ou LevelDB. Je vous conseille également de vous familiariser avec la conception pilotée par domaine et le CQRS .

Au début, je pense que nous devrions créer une autre couche pour la mise en réseau, car nous ne voulons pas de gros contrôleurs ou de modèles lourds et dépassés. Je ne crois pas à ces fat model, skinny controllerchoses. Mais je crois en l' skinny everythingapproche, car aucune classe ne devrait être grosse, jamais. Tout réseau peut être généralement résumé comme une logique métier, par conséquent, nous devrions avoir une autre couche, où nous pouvons la mettre. La couche de service est ce dont nous avons besoin:

It encapsulates the application's business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

Dans notre MVCroyaume Service Layerest quelque chose comme un médiateur entre le modèle de domaine et les contrôleurs. Il existe une variante assez similaire de cette approche appelée MVCS où a Storeest en fait notre Servicecouche. Storeinstances de modèle vends et gère le réseau, la mise en cache , etc. Je veux mentionner que vous ne devriez pas écrire tout votre réseau et la logique métier dans votre couche de service. Cela peut également être considéré comme une mauvaise conception. Pour plus d'informations, consultez les modèles de domaine anémique et riche . Certaines méthodes de service et logique métier peuvent être gérées dans le modèle, ce sera donc un modèle "riche" (avec comportement).

J'utilise toujours largement deux bibliothèques: AFNetworking 2.0 et ReactiveCocoa . Je pense que c'est un must pour toute application moderne qui interagit avec le réseau et les services Web ou contient une logique d'interface utilisateur complexe.

ARCHITECTURE

Au début, je crée une APIClientclasse générale , qui est une sous-classe de AFHTTPSessionManager . Il s'agit d'un cheval de bataille de tous les réseaux de l'application: toutes les classes de service lui délèguent des demandes REST réelles. Il contient toutes les personnalisations du client HTTP, dont j'ai besoin dans l'application particulière: épinglage SSL, traitement des erreurs et création d' NSErrorobjets simples avec des raisons d'échec détaillées et des descriptions de toutes APIet des erreurs de connexion (dans ce cas, le contrôleur sera en mesure d'afficher les messages corrects pour l'utilisateur), définir des sérialiseurs de demande et de réponse, des en-têtes http et d'autres éléments liés au réseau. Ensuite , je divise logiquement toutes les demandes de l' API dans subservices ou, plus correctement, microservices : UserSerivces, CommonServices, SecurityServices,FriendsServiceset ainsi de suite, conformément à la logique métier qu'ils mettent en œuvre. Chacun de ces microservices est une classe distincte. Ils forment ensemble un Service Layer. Ces classes contiennent des méthodes pour chaque demande d'API, traitent les modèles de domaine et renvoient toujours un RACSignalavec le modèle de réponse analysé ou NSErrorà l'appelant.

Je veux mentionner que si vous avez une logique de sérialisation de modèle complexe - alors créez une autre couche pour cela: quelque chose comme Data Mapper mais plus général, par exemple JSON / XML -> Model mapper. Si vous avez un cache: créez-le également en tant que couche / service séparé (vous ne devez pas mélanger la logique métier avec la mise en cache). Pourquoi? Parce que la couche de mise en cache correcte peut être assez complexe avec ses propres accrochages. Les gens mettent en œuvre une logique complexe pour obtenir une mise en cache valide et prévisible comme par exemple la mise en cache monoïdale avec des projections basées sur des profuncteurs. Vous pouvez lire sur cette belle bibliothèque appelée Carlos pour en savoir plus. Et n'oubliez pas que Core Data peut vraiment vous aider avec tous les problèmes de mise en cache et vous permettra d'écrire moins de logique. De plus, si vous avez une logique entre NSManagedObjectContextles modèles de demande de serveur et les modèles de serveur, vous pouvez utiliserModèle de référentiel , qui sépare la logique qui récupère les données et les mappe au modèle d'entité de la logique métier qui agit sur le modèle. Donc, je conseille d'utiliser le modèle de référentiel même lorsque vous avez une architecture basée sur les données de base. Référentiel peut des choses abstraites, comme NSFetchRequest, NSEntityDescription, NSPredicateet ainsi de suite à des méthodes simples comme getou put.

Après toutes ces actions dans la couche Service, l'appelant (contrôleur de vue) peut effectuer des tâches asynchrones complexes avec la réponse: manipulations de signaux, chaînage, mappage, etc. à l'aide de ReactiveCocoaprimitives, ou simplement vous y abonner et afficher les résultats dans la vue . J'injectent l' injection de dépendance dans toutes ces classes de service mes APIClient, ce qui se traduira par un appel de service particulier en correspondant GET, POST, PUT, DELETE, etc. demande au point de terminaison REST. Dans ce cas APIClientest passé implicitement à tous les contrôleurs, vous pouvez le rendre explicite avec un paramétré sur APIClientles classes de service. Cela peut avoir un sens si vous souhaitez utiliser différentes personnalisations duAPIClientpour des classes de service particulières, mais si, pour certaines raisons, vous ne voulez pas de copies supplémentaires ou si vous êtes sûr que vous utiliserez toujours une instance particulière (sans personnalisation) de APIClient- faites-en un singleton, mais ne le faites pas, s'il vous plaît DON 'T faire des classes de service comme singletons.

Ensuite, chaque contrôleur de vue avec le DI injecte la classe de service dont il a besoin, appelle les méthodes de service appropriées et compose leurs résultats avec la logique de l'interface utilisateur. Pour l'injection de dépendance, j'aime utiliser BloodMagic ou un framework Typhoon plus puissant . Je n'utilise jamais de singletons, de cours de Dieu APIManagerWhateverou d'autres mauvaises choses. Parce que si vous appelez votre classe WhateverManager, cela indique que vous ne connaissez pas son objectif et c'est un mauvais choix de conception . Les singletons sont également un anti-modèle, et dans la plupart des cas (sauf les rares), c'est une mauvaise solution. Le singleton ne devrait être envisagé que si les trois critères suivants sont satisfaits:

  1. La propriété de l'instance unique ne peut pas être raisonnablement attribuée;
  2. Une initialisation paresseuse est souhaitable;
  3. L'accès global n'est pas prévu autrement.

Dans notre cas, la propriété de l'instance unique n'est pas un problème et nous n'avons pas non plus besoin d'un accès global après avoir divisé notre gestionnaire de dieux en services, car maintenant un ou plusieurs contrôleurs dédiés ont besoin d'un service particulier (par exemple, UserProfileles besoins des contrôleurs UserServices, etc.) .

Nous devons toujours respecter le Sprincipe dans SOLID et utiliser la séparation des préoccupations , alors ne mettez pas toutes vos méthodes de service et vos appels réseaux dans une seule classe, car c'est fou, surtout si vous développez une application de grande entreprise. C'est pourquoi nous devrions considérer l'injection de dépendance et l'approche des services. Je considère cette approche comme moderne et post-OO . Dans ce cas, nous avons divisé notre application en deux parties: la logique de contrôle (contrôleurs et événements) et les paramètres.

Un type de paramètres serait des paramètres de «données» ordinaires. C'est ce que nous transmettons aux fonctions, manipulons, modifions, persistons, etc. Ce sont des entités, des agrégats, des collections, des classes de cas. L'autre type serait les paramètres de «service». Ce sont des classes qui encapsulent la logique métier, permettent de communiquer avec des systèmes externes, fournissent un accès aux données.

Voici un workflow général de mon architecture par exemple. Supposons que nous ayons un FriendsViewController, qui affiche la liste des amis de l'utilisateur et que nous ayons une option à supprimer des amis. Je crée une méthode dans ma FriendsServicesclasse appelée:

- (RACSignal *)removeFriend:(Friend * const)friend

Friendest un objet modèle / domaine (ou il peut être juste un Userobjet s'ils ont des attributs similaires). Underhood cette méthode Parsis Friendà NSDictionarydes paramètres JSON friend_id, name, surname, friend_request_idet ainsi de suite. J'utilise toujours la bibliothèque Mantle pour ce type de passe-partout et pour ma couche de modèle (analyse en avant et en avant, gestion des hiérarchies d'objets imbriquées dans JSON, etc.). Après l' analyse qu'il appelle APIClient DELETEméthode pour faire une demande REST réelle et retourne Responseen RACSignall'appelant ( FriendsViewControllerdans notre cas) pour afficher un message approprié pour l'utilisateur ou autre.

Si notre application est très importante, nous devons séparer notre logique encore plus clairement. Par exemple, il n'est pas toujours bon de mélanger Repositoryou de modéliser la logique avec Serviceune seule. Lorsque j'ai décrit mon approche, j'avais dit que la removeFriendméthode devrait être dans la Servicecouche, mais si nous allons être plus pédants, nous pouvons remarquer qu'elle appartient mieux Repository. Rappelons-nous ce qu'est le référentiel. Eric Evans en a donné une description précise dans son livre [DDD]:

Un référentiel représente tous les objets d'un certain type comme un ensemble conceptuel. Il agit comme une collection, sauf avec une capacité d'interrogation plus élaborée.

Ainsi, a Repositoryest essentiellement une façade qui utilise la sémantique de style Collection (Ajouter, Mettre à jour, Supprimer) pour fournir l'accès aux données / objets. Voilà pourquoi quand vous avez quelque chose comme: getFriendsList, getUserGroups, removeFriendvous pouvez le placer dans le Repository, parce que la collection comme la sémantique est assez clair. Et du code comme:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

est certainement une logique métier, car elle va au-delà des CRUDopérations de base et connecte deux objets de domaine ( Friendet Request), c'est pourquoi elle doit être placée dans la Servicecouche. Je veux aussi remarquer: ne créez pas d'abstractions inutiles . Utilisez judicieusement toutes ces approches. Parce que si vous submergez votre application d'abstractions, cela augmentera sa complexité accidentelle, et la complexité causera plus de problèmes dans les systèmes logiciels qu'autre chose

Je vous décris un "ancien" exemple Objective-C mais cette approche peut être très facilement adaptée au langage Swift avec beaucoup plus d'améliorations, car elle a des fonctionnalités plus utiles et du sucre fonctionnel. Je recommande fortement d'utiliser cette bibliothèque: Moya . Il vous permet de créer une APIClientcouche plus élégante (notre cheval de bataille comme vous vous en souvenez). Désormais, notre APIClientfournisseur sera un type de valeur (enum) avec des extensions conformes aux protocoles et exploitant la mise en correspondance des modèles de déstructuration. L'énumération rapide + la correspondance de motifs nous permet de créer des types de données algébriques comme dans la programmation fonctionnelle classique. Nos microservices utiliseront ce APIClientfournisseur amélioré comme dans l'approche Objective-C habituelle. Pour la couche modèle au lieu de Mantlevous pouvez utiliser la bibliothèque ObjectMapperou j'aime utiliser une bibliothèque Argo plus élégante et fonctionnelle .

J'ai donc décrit mon approche architecturale générale, qui peut être adaptée à n'importe quelle application, je pense. Il peut y avoir bien sûr beaucoup plus d'améliorations. Je vous conseille d'apprendre la programmation fonctionnelle, car vous pouvez en bénéficier beaucoup, mais n'allez pas trop loin avec. L'élimination d'un état mutable global, excessif, partagé, la création d'un modèle de domaine immuable ou la création de fonctions pures sans effets secondaires externes est, en général, une bonne pratique, et un nouveau Swiftlangage encourage cela. Mais rappelez-vous toujours que surcharger votre code avec de lourds modèles fonctionnels purs, les approches théoriques par catégorie est une mauvaise idée, car d' autres développeurs liront et prendront en charge votre code, et ils peuvent être frustrés ou effrayants de laprismatic profunctorset ce genre de choses dans votre modèle immuable. La même chose avec le ReactiveCocoa: ne faites pas tropRACify votre code , car il peut devenir illisible très rapidement, surtout pour les débutants. Utilisez-le quand il peut vraiment simplifier vos objectifs et votre logique.

Ainsi, read a lot, mix, experiment, and try to pick up the best from different architectural approaches. C'est le meilleur conseil que je puisse vous donner.

Oleksandr Karaberov
la source
Aussi une approche intéressante et solide. Merci.
MainstreamDeveloper00
1
@darksider Comme je l' ai déjà écrit dans ma réponse: « ` Je n'utilise singletons, classe Dieu APIManagerWhatever ou d' autres choses de mal, parce que singleton est un anti-modèle, et dans la plupart des cas ( à l' exception des rares) est une mauvaise solution. ". I don't like singletons. I have an opinion that if you decided to use singletons in your project you should have at least three criteria why you do this (I edited my answer). So I inject them (lazy of course and not each time, but fois ') dans chaque contrôleur.
Oleksandr Karaberov
14
Salut @alexander. Avez-vous des exemples de projets sur GitHub? Vous décrivez une approche très intéressante. Merci. Mais je suis un débutant en développement Objective-C. Et pour moi, il est difficile de comprendre certains aspects. Peut-être que vous pouvez télécharger un projet de test sur GitHub et donner un lien?
Denis
1
Bonjour @AlexanderKaraberov, je suis un peu confus en ce qui concerne l'explication du magasin que vous avez donnée. Supposons que j'ai 5 modèles, pour chacun j'ai 2 classes, une qui maintient la mise en réseau et une autre mise en cache des objets. Dois-je maintenant avoir une classe Store distincte pour chaque modèle qui appelle une fonction de mise en réseau et une classe de cache ou une classe Store unique qui a toutes les fonctions pour chaque modèle, de sorte que le contrôleur accède toujours à un seul fichier pour les données.
météores
1
@icodebuster ce projet de démonstration m'a aidé à comprendre bon nombre des concepts décrits ici: github.com/darthpelo/NetworkLayerExample
31

Selon l'objectif de cette question, je voudrais décrire notre approche architecturale.

Approche architecturale

L'architecture générale de notre application iOS repose sur les modèles suivants: couches de service , MVVM , liaison de données d'interface utilisateur , injection de dépendance ; et paradigme de programmation réactive fonctionnelle .

Nous pouvons découper une application standard destinée aux consommateurs en couches logiques suivantes:

  • Assemblée
  • Modèle
  • Prestations de service
  • Espace de rangement
  • Managers
  • Coordinateurs
  • UI
  • Infrastructure

La couche d'assemblage est un point d'amorçage de notre application. Il contient un conteneur d'Injection de dépendances et des déclarations d'objets d'application et de leurs dépendances. Cette couche peut également contenir la configuration de l'application (URL, clés de services tiers, etc.). Pour cela, nous utilisons la bibliothèque Typhoon .

La couche modèle contient des classes de modèles de domaine, des validations, des mappages. Nous utilisons la bibliothèque Mantle pour cartographier nos modèles: elle prend en charge la sérialisation / désérialisation en JSONformat et en NSManagedObjectmodèles. Pour la validation et la représentation sous forme de nos modèles, nous utilisons les bibliothèques FXForms et FXModelValidation .

La couche Services déclare les services que nous utilisons pour interagir avec des systèmes externes afin d'envoyer ou de recevoir des données représentées dans notre modèle de domaine. Nous avons donc généralement des services de communication avec les API de serveur (par entité), des services de messagerie (comme PubNub ), des services de stockage (comme Amazon S3), etc. logique. Pour le réseautage général, nous utilisons la bibliothèque AFNetworking .

L'objectif de la couche de stockage est d'organiser le stockage local des données sur l'appareil. Pour cela, nous utilisons Core Data ou Realm (les deux ont des avantages et des inconvénients, la décision à utiliser est basée sur des spécifications concrètes). Pour la configuration de Core Data, nous utilisons la bibliothèque MDMCoreData et un tas de classes - stockages - (similaires aux services) qui fournissent un accès au stockage local pour chaque entité. Pour Realm, nous utilisons simplement des stockages similaires pour avoir accès au stockage local.

La couche Managers est un endroit où vivent nos abstractions / wrappers.

Dans un rôle de gestionnaire pourrait être:

  • Credentials Manager avec ses différentes implémentations (trousseau, NSDefaults, ...)
  • Gestionnaire de session actuel qui sait conserver et fournir la session utilisateur actuelle
  • Capture Pipeline qui donne accès aux appareils multimédias (enregistrement vidéo, audio, prise de photos)
  • BLE Manager qui donne accès aux services et périphériques Bluetooth
  • Geo Location Manager
  • ...

Ainsi, dans le rôle de gestionnaire, tout objet implémentant la logique d'un aspect particulier ou d'une préoccupation doit être utilisé pour le fonctionnement de l'application.

Nous essayons d'éviter les singletons, mais cette couche est un endroit où ils vivent s'ils sont nécessaires.

La couche Coordinators fournit des objets qui dépendent des objets d'autres couches (Service, Storage, Model) afin de combiner leur logique en une séquence de travail nécessaire pour certains modules (fonctionnalité, écran, user story ou expérience utilisateur). Il enchaîne généralement les opérations asynchrones et sait comment réagir en cas de réussite ou d'échec. À titre d'exemple, vous pouvez imaginer une fonction de messagerie et un MessagingCoordinatorobjet correspondant . La gestion de l'opération d'envoi de messages peut ressembler à ceci:

  1. Valider le message (couche modèle)
  2. Enregistrer le message localement (stockage des messages)
  3. Télécharger la pièce jointe d'un message (service Amazon S3)
  4. Mettre à jour l'état des messages et les URL des pièces jointes et enregistrer le message localement (stockage des messages)
  5. Sérialiser le message au format JSON (couche modèle)
  6. Publier un message sur PubNub (service PubNub)
  7. Mettre à jour l'état et les attributs des messages et les enregistrer localement (stockage des messages)

À chacune des étapes ci-dessus, une erreur est traitée en conséquence.

La couche d'interface utilisateur se compose des sous-couches suivantes:

  1. AfficherModèles
  2. ViewControllers
  3. Vues

Afin d'éviter les contrôleurs de vue massifs, nous utilisons le modèle MVVM et implémentons la logique nécessaire pour la présentation de l'interface utilisateur dans ViewModels. Un ViewModel a généralement des coordinateurs et des gestionnaires comme dépendances. ViewModels utilisés par ViewControllers et certains types de vues (par exemple, les cellules de vue de tableau). La colle entre ViewControllers et ViewModels est le modèle de liaison de données et de commande. Afin de permettre d'avoir cette colle, nous utilisons la bibliothèque ReactiveCocoa .

Nous utilisons également ReactiveCocoa et son RACSignalconcept comme interface et type de valeur de retour de tous les coordinateurs, services, méthodes de stockage. Cela nous permet de chaîner des opérations, de les exécuter parallèlement ou en série, et bien d'autres choses utiles fournies par ReactiveCocoa.

Nous essayons d'implémenter notre comportement d'interface utilisateur de manière déclarative. La liaison de données et la mise en page automatique aident beaucoup à atteindre cet objectif.

La couche infrastructure contient tous les assistants, extensions et utilitaires nécessaires au travail d'application.


Cette approche fonctionne bien pour nous et pour les types d'applications que nous créons habituellement. Mais vous devez comprendre que ce n'est qu'une approche subjective qui doit être adaptée / modifiée pour les besoins de l'équipe concrète.

J'espère que ceci vous aidera!

Vous pouvez également trouver plus d'informations sur le processus de développement iOS dans ce billet de blog Développement iOS en tant que service

Alex Petropavlovsky
la source
J'ai commencé à aimer cette architecture il y a quelques mois, merci Alex de la partager! Je voudrais l'essayer avec RxSwift dans un avenir proche!
ingaham
18

Parce que toutes les applications iOS sont différentes, je pense qu'il y a différentes approches à considérer, mais je procède généralement de cette façon:
Créez une classe de gestionnaire central (singleton) pour gérer toutes les demandes d'API (généralement nommées APICommunicator) et chaque méthode d'instance est un appel d'API . Et il existe une méthode centrale (non publique):

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

Pour mémoire, j'utilise 2 bibliothèques / frameworks majeurs, ReactiveCocoa et AFNetworking. ReactiveCocoa gère parfaitement les réponses de mise en réseau asynchrone, vous pouvez le faire (sendNext:, sendError :, etc.).
Cette méthode appelle l'API, obtient les résultats et les envoie via RAC au format «brut» (comme NSArray ce que retourne AFNetworking).
Ensuite, une méthode comme celle getStuffList:qui a appelé la méthode ci-dessus souscrit à son signal, analyse les données brutes en objets (avec quelque chose comme Motis) et envoie les objets un par un à l'appelant ( getStuffList:et des méthodes similaires renvoient également un signal auquel le contrôleur peut s'abonner) ).
Le contrôleur abonné reçoit les objets par subscribeNext:bloc et les gère.

J'ai essayé de nombreuses façons dans différentes applications, mais celle-ci fonctionnait le mieux, donc je l'ai utilisée dans quelques applications récemment, elle convient aux petits et grands projets et elle est facile à étendre et à maintenir si quelque chose doit être modifié.
J'espère que cela vous aidera, j'aimerais entendre les opinions des autres sur mon approche et peut-être comment les autres pensent que cela pourrait être amélioré.

Rickye
la source
2
Merci pour la réponse +1. Bonne approche. Je laisse la question. Peut-être que nous aurons d'autres approches d'autres développeurs.
MainstreamDeveloper00
1
J'aime une variante de cette approche - j'utilise un gestionnaire central d'API qui s'occupe des mécanismes de communication avec l'API. Cependant, j'essaie de rendre toutes les fonctionnalités exposées sur mes objets de modèle. Les modèles fourniront des méthodes comme + (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;et - (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;qui effectuent les préparatifs nécessaires, puis feront appel au gestionnaire d'API.
jsadler
1
Cette approche est simple, mais à mesure que le nombre d'API augmente, il devient plus difficile de maintenir le gestionnaire d'API singleton. Et chaque nouvelle API ajoutée sera liée au gestionnaire, quel que soit le module auquel appartient cette API. Essayez d'utiliser github.com/kevin0571/STNetTaskQueue pour gérer les demandes d'API.
Kevin
Outre le fait de savoir pourquoi vous faites de la publicité pour votre bibliothèque, qui est aussi loin que possible de ma solution et beaucoup plus compliquée, j'ai essayé cette approche sur d'innombrables projets, petits et grands, comme mentionné et je l'ai utilisée exactement même depuis que j'ai écrit cette réponse. Avec des conventions de dénomination intelligentes, il n'est pas du tout difficile à maintenir.
Rickye
8

Dans ma situation, j'utilise généralement la bibliothèque ResKit pour configurer la couche réseau. Il fournit une analyse syntaxique facile à utiliser. Cela réduit mes efforts sur la configuration du mappage pour différentes réponses et autres choses.

J'ajoute seulement du code pour configurer le mappage automatiquement. Je définis la classe de base pour mes modèles (pas de protocole en raison de beaucoup de code pour vérifier si une méthode est implémentée ou non, et moins de code dans les modèles lui-même):

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

Les relations sont des objets qui représentent des objets imbriqués en réponse:

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

Ensuite, je configure le mappage pour RestKit comme ceci:

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

Un exemple d'implémentation de MappableEntry:

User.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

User.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

Maintenant à propos de l'encapsulation des requêtes:

J'ai un fichier d'en-tête avec la définition des blocs, pour réduire la longueur de ligne dans toutes les classes APIRequest:

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

Et Exemple de ma classe APIRequest que j'utilise:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

Et tout ce que vous devez faire dans le code, initialisez simplement l'objet API et appelez-le quand vous en avez besoin:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

Mon code n'est pas parfait, mais il est facile à définir une fois et à utiliser pour différents projets. Si cela intéresse quelqu'un, mb je pourrais passer du temps et en faire une solution universelle quelque part sur GitHub et CocoaPods.

Andrew Cherkashyn
la source
7

À mon avis, toute architecture logicielle est dictée par les besoins. Si c'est à des fins d'apprentissage ou à des fins personnelles, alors décidez de l'objectif principal et laissez-le guider l'architecture. S'il s'agit d'un travail de location, le besoin commercial est primordial. L'astuce consiste à ne pas laisser les choses brillantes vous distraire des besoins réels. Je trouve cela difficile à faire. Il y a toujours de nouvelles choses brillantes qui apparaissent dans cette entreprise et beaucoup d'entre elles ne sont pas utiles, mais vous ne pouvez pas toujours le dire à l'avance. Concentrez-vous sur le besoin et soyez prêt à abandonner les mauvais choix si vous le pouvez.

Par exemple, j'ai récemment réalisé un prototype rapide d'application de partage de photos pour une entreprise locale. Comme l'entreprise avait besoin de faire quelque chose de rapide et de sale, l'architecture a fini par être un code iOS pour faire apparaître une caméra et un code réseau attaché à un bouton d'envoi qui a téléchargé l'image dans un magasin S3 et écrit dans un domaine SimpleDB. Le code était trivial et le coût minime et le client dispose d'une collection de photos évolutive accessible sur le Web avec des appels REST. Bon marché et stupide, l'application avait de nombreux défauts et verrouillerait l'interface utilisateur à l'occasion, mais ce serait un gaspillage d'en faire plus pour un prototype et cela leur permet de se déployer auprès de leur personnel et de générer facilement des milliers d'images de test sans performances ni évolutivité. préoccupations. Une architecture merdique, mais elle répond parfaitement aux besoins et coûte parfaitement.

Un autre projet consistait à implémenter une base de données sécurisée locale qui se synchronise avec le système de l'entreprise en arrière-plan lorsque le réseau est disponible. J'ai créé un synchroniseur d'arrière-plan qui a utilisé RestKit car il semblait avoir tout ce dont j'avais besoin. Mais j'ai dû écrire tellement de code personnalisé pour RestKit pour gérer le JSON idiosyncratique que j'aurais pu le faire plus rapidement en écrivant mon propre JSON dans les transformations CoreData. Cependant, le client voulait apporter cette application en interne et je pensais que RestKit serait similaire aux frameworks qu'ils utilisaient sur d'autres plateformes. J'attends de voir si c'était une bonne décision.

Encore une fois, le problème pour moi est de se concentrer sur le besoin et de laisser cela déterminer l'architecture. J'essaie comme un diable d'éviter d'utiliser des packages tiers car ils entraînent des coûts qui n'apparaissent qu'après un certain temps sur le terrain. J'essaie d'éviter de faire des hiérarchies de classes car elles sont rarement payantes. Si je peux écrire quelque chose dans un délai raisonnable au lieu d'adopter un package qui ne convient pas parfaitement, je le fais. Mon code est bien structuré pour le débogage et commenté de manière appropriée, mais les packages tiers le sont rarement. Cela dit, je trouve AF Networking trop utile pour être ignoré et bien structuré, bien commenté et maintenu et je l'utilise beaucoup! RestKit couvre un grand nombre de cas courants, mais j'ai l'impression que je me suis battu lorsque je l'utilise, et la plupart des sources de données que je rencontre sont pleines de bizarreries et de problèmes qui sont mieux gérés avec du code personnalisé. Dans mes dernières applications, j'utilise simplement les convertisseurs JSON intégrés et j'écris quelques méthodes utilitaires.

Un modèle que j'utilise toujours est de retirer les appels réseau du thread principal. Les 4 à 5 dernières applications que j'ai effectuées ont configuré une tâche de minuterie d'arrière-plan à l'aide de dispatch_source_create qui se réveille de temps en temps et effectue des tâches réseau selon les besoins. Vous devez effectuer un travail de sécurité sur les threads et vous assurer que le code de modification de l'interface utilisateur est envoyé au thread principal. Cela aide également à faire votre intégration / initialisation de manière à ce que l'utilisateur ne se sente pas surchargé ou retardé. Jusqu'à présent, cela a plutôt bien fonctionné. Je suggère d'examiner ces choses.

Enfin, je pense qu'au fur et à mesure que nous travaillons et que le système d'exploitation évolue, nous avons tendance à développer de meilleures solutions. Il m'a fallu des années pour surmonter ma conviction que je dois suivre des modèles et des conceptions qui, selon d'autres, sont obligatoires. Si je travaille dans un contexte où cela fait partie de la religion locale, ahem, je veux dire les meilleures pratiques d'ingénierie du département, alors je respecte les coutumes à la lettre, c'est pour cela qu'ils me paient. Mais je trouve rarement que suivre des conceptions et des modèles plus anciens est la solution optimale. J'essaie toujours de regarder la solution à travers le prisme des besoins de l'entreprise et de construire l'architecture pour l'adapter et garder les choses aussi simples que possible. Quand j'ai l'impression qu'il n'y en a pas assez, mais que tout fonctionne correctement, alors je suis sur la bonne voie.

Fran K.
la source
4

J'utilise l'approche que j'ai obtenue d'ici: https://github.com/Constantine-Fry/Foursquare-API-v2 . J'ai réécrit cette bibliothèque dans Swift et vous pouvez voir l'approche architecturale de ces parties du code:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

Fondamentalement, il existe une sous-classe NSOperation qui effectue la NSURLRequest, analyse la réponse JSON et ajoute le bloc de rappel avec le résultat à la file d'attente. La classe API principale construit NSURLRequest, initialise cette sous-classe NSOperation et l'ajoute à la file d'attente.

bzz
la source
3

Nous utilisons quelques approches en fonction de la situation. Pour la plupart des choses, AFNetworking est l'approche la plus simple et la plus robuste en ce que vous pouvez définir des en-têtes, télécharger des données en plusieurs parties, utiliser GET, POST, PUT & DELETE et il existe un tas de catégories supplémentaires pour UIKit qui vous permettent par exemple de définir une image à partir de une URL. Dans une application complexe avec beaucoup d'appels, nous résumons parfois cela à une méthode pratique qui serait quelque chose comme:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

Il existe quelques situations où AFNetworking n'est pas approprié, par exemple lorsque vous créez un framework ou un autre composant de bibliothèque car AFNetworking peut déjà être dans une autre base de code. Dans cette situation, vous utiliseriez une NSMutableURLRequest soit en ligne si vous effectuez un seul appel, soit résumée dans une classe de demande / réponse.

Martin
la source
Pour moi, c'est la réponse la meilleure et la plus claire, cheers. "C'est si simple". @martin, personnellement, nous utilisons tout le temps NSMutableURLRequest; Y a-t-il une vraie raison d'utiliser AFNetworking?
Fattie
AFNetworking est vraiment très pratique. Pour moi, les blocs de réussite et d'échec valent la peine car cela rend le code plus facile à gérer. Je suis d'accord pour dire que c'est parfois exagéré.
Martin
Un superbe point sur les blocs, merci pour ça. Je suppose que la nature spécifique de tout cela changera avec Swift.
Fattie
2

J'évite les singletons lors de la conception de mes applications. Ils sont typiques pour beaucoup de gens, mais je pense que vous pouvez trouver des solutions plus élégantes ailleurs. En général, ce que je fais est de construire mes entités dans CoreData, puis de mettre mon code REST dans une catégorie NSManagedObject. Si, par exemple, je voulais créer et poster un nouvel utilisateur, je ferais ceci:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

J'utilise RESTKit pour le mappage d'objet et l'initialise au démarrage. Je trouve que le routage de tous vos appels via un singleton est une perte de temps et ajoute beaucoup de passe-partout qui ne sont pas nécessaires.

Dans NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

Dans NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

Pourquoi ajouter des classes d'assistance supplémentaires lorsque vous pouvez étendre les fonctionnalités d'une classe de base commune à travers des catégories?

Si vous êtes intéressé par des informations plus détaillées sur ma solution, faites-le moi savoir. Je suis heureux de partager.

Sandy Chapman
la source
3
Serait certainement intéressé à lire plus en détail sur cette approche dans un article de blog.
Danyal Aytekin
0

Du point de vue de la conception purement de classe, vous aurez généralement quelque chose comme ceci:

  • Vos contrôleurs de vues contrôlant une ou plusieurs vues
  • Classe de modèle de données - Cela dépend vraiment du nombre d'entités distinctes réelles avec lesquelles vous traitez et de la façon dont elles sont liées.

    Par exemple, si vous avez un tableau d'éléments à afficher dans quatre représentations différentes (liste, graphique, graphique, etc.), vous aurez une classe de modèle de données pour la liste des éléments, une de plus pour un élément. La liste des classes d'objets sera partagée par quatre contrôleurs de vue - tous les enfants d'un contrôleur de barre d'onglets ou d'un contrôleur de navigation.

    Les classes de modèles de données seront utiles non seulement pour afficher des données, mais aussi pour les sérialiser, chacune d'elles pouvant exposer son propre format de sérialisation via des méthodes d'exportation JSON / XML / CSV (ou autre).

  • Il est important de comprendre que vous avez également besoin de classes de générateur de demande d'API qui mappent directement avec vos points de terminaison d'API REST. Supposons que vous disposez d'une API qui connecte l'utilisateur - de sorte que votre classe de générateur d'API de connexion créera une charge utile POST JSON pour l'API de connexion. Dans un autre exemple, une classe de générateur de demande d'API pour la liste des éléments de catalogue API créera une chaîne de requête GET pour l'api correspondante et déclenchera la requête REST GET.

    Ces classes de générateur de demande d'API reçoivent généralement des données des contrôleurs de vue et retransmettent également les mêmes données aux contrôleurs de vue pour la mise à jour de l'interface utilisateur / d'autres opérations. Les contrôleurs de vue décideront alors comment mettre à jour les objets du modèle de données avec ces données.

  • Finalement, le cœur du client REST - classe de récupérateur de données API qui est inconscient de toutes sortes de demandes d'API que votre application fait. Cette classe sera plus probablement un singleton, mais comme d'autres l'ont souligné, il n'est pas nécessaire que ce soit un singleton.

    Notez que le lien est juste une implémentation typique et ne prend pas en considération des scénarios comme la session, les cookies, etc., mais il suffit de vous lancer sans utiliser de frameworks tiers.

Nirav Bhatt
la source
0

Cette question a déjà beaucoup de réponses excellentes et détaillées, mais je pense que je dois la mentionner car personne d'autre ne l'a fait.

Alamofire pour Swift. https://github.com/Alamofire/Alamofire

Il est créé par les mêmes personnes que AFNetworking, mais est plus directement conçu avec Swift à l'esprit.

matt.writes.code
la source
0

Je pense que pour le moment le projet moyen utilise l'architecture MVVM et le grand projet utilise l'architecture VIPER et essaie d'atteindre

  • Programmation orientée protocole
  • Modèles de conception de logiciels
  • Principe VENDU
  • Programmation générique
  • Ne vous répétez pas (SEC)

Et approches architecturales pour la création d'applications de mise en réseau iOS (clients REST)

Le souci de séparation pour un code propre et lisible évite la duplication:

import Foundation
enum DataResponseError: Error {
    case network
    case decoding

    var reason: String {
        switch self {
        case .network:
            return "An error occurred while fetching data"
        case .decoding:
            return "An error occurred while decoding data"
        }
    }
}

extension HTTPURLResponse {
    var hasSuccessStatusCode: Bool {
        return 200...299 ~= statusCode
    }
}

enum Result<T, U: Error> {
    case success(T)
    case failure(U)
}

inversion de dépendance

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

Responsable principal:

  final class NHClientHTTPNetworking : NHDataProvider {

        let session: URLSession

        init(session: URLSession = URLSession.shared) {
            self.session = session
        }

        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
                             completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
            let urlRequest = URLRequest(url: url)
            session.dataTask(with: urlRequest, completionHandler: { data, response, error in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.hasSuccessStatusCode,
                    let data = data
                    else {
                        completion(Result.failure(DataResponseError.network))
                        return
                }
                guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
                    completion(Result.failure(DataResponseError.decoding))
                    return
                }
                completion(Result.success(decodedResponse))
            }).resume()
        }
    }

Vous trouverez ici l' architecture GitHub MVVM avec reste API Swift Project

Nazmul Hasan
la source
0

En génie logiciel mobile, les plus utilisés sont les modèles Clean Architecture + MVVM et Redux.

Clean Architecture + MVVM se compose de 3 couches: domaine, présentation, couches de données. Où la couche de présentation et la couche de référentiels de données dépendent de la couche de domaine:

Presentation Layer -> Domain Layer <- Data Repositories Layer

Et la couche de présentation se compose de ViewModels and Views (MVVM):

Presentation Layer (MVVM) = ViewModels + Views
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API (Network) + Persistence DB

Dans cet article, il existe une description plus détaillée de Clean Architecture + MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Oleh Kudinov
la source