Najlepsze podejścia architektoniczne do budowania aplikacji sieciowych iOS (klienci REST)

323

Jestem programistą iOS z pewnym doświadczeniem i to pytanie jest dla mnie bardzo interesujące. Widziałem wiele różnych zasobów i materiałów na ten temat, ale nadal jestem zdezorientowany. Jaka jest najlepsza architektura dla aplikacji sieciowej na iOS? Mam na myśli podstawowe abstrakcyjne ramy, wzorce, które pasują do każdej aplikacji sieciowej, niezależnie od tego, czy jest to mała aplikacja, która ma tylko kilka żądań serwera, czy złożony klient REST. Apple zaleca stosowanie MVCjako podstawowego podejścia architektonicznego do wszystkich aplikacji na iOS, ale MVCani bardziej nowoczesnegoMVVM wzorce nie wyjaśniają, gdzie umieścić kod logiki sieci i jak go ogólnie zorganizować.

Czy muszę opracować coś takiego MVCS( Sdla Service) i na tej Servicewarstwie umieścić wszystkie APIżądania i inną logikę sieci, która z perspektywy może być naprawdę złożona? Po przeprowadzeniu badań znalazłem dwa podstawowe podejścia do tego. W tym przypadku zalecono utworzenie osobnej klasy dla każdego żądania sieciowego do usługi sieci web API(takiej jak LoginRequestklasa lub PostCommentRequestklasa itd.), Która wszystkie dziedziczy po abstrakcyjnych klas żądań podstawowychAbstractBaseRequest a także utworzenie globalnego menedżera sieci, który zawiera wspólny kod sieciowy i inne preferencje (może to być AFNetworkingpersonalizacja lubRestKitstrojenie, jeśli mamy złożone odwzorowania obiektów i trwałość, a nawet własną implementację komunikacji sieciowej ze standardowym API). Ale takie podejście wydaje mi się narzutem. Innym rozwiązaniem jest mieć jakiś singleton APIdyspozytora lub klasy menedżera jak w pierwszym podejściu, ale nie do tworzenia klas dla każdego żądania i zamiast do hermetyzacji każde żądanie jako metody instancji publicznej tej klasy menedżera, takich jak: fetchContacts, loginUsermetod, itd. Tak, to jakie jest najlepszy i poprawny sposób? Czy są jeszcze inne ciekawe podejścia, których jeszcze nie znam?

I czy powinienem utworzyć kolejną warstwę dla wszystkich takich rzeczy sieciowych Service, lub NetworkProviderwarstwy lub czegokolwiek na mojej MVCarchitekturze, czy też ta warstwa powinna zostać zintegrowana (wstrzyknięta) z istniejącymi MVCwarstwami np. Model?

Wiem, że istnieją piękne podejścia lub jak takie mobilne potwory, jak klient Facebooka lub klient LinkedIn, radzą sobie z wykładniczo rosnącą złożonością logiki sieci?

Wiem, że nie ma dokładnej i formalnej odpowiedzi na problem. Celem tego pytania jest zebranie najciekawszych podejść od doświadczonych programistów iOS . Najlepsze sugerowane podejście zostanie oznaczone jako zaakceptowane i nagrodzone nagrodą za reputację, inne zostaną pozytywnie ocenione. Jest to głównie pytanie teoretyczne i badawcze. Chcę zrozumieć podstawowe, abstrakcyjne i poprawne podejście architektoniczne do aplikacji sieciowych w iOS. Mam nadzieję na szczegółowe wyjaśnienia od doświadczonych programistów.

MainstreamDeveloper00
źródło
14
Czy to nie jest pytanie z „listą zakupów”? Właśnie zadałem pytanie do głosowania do piekła i zamknięte, ponieważ stwierdzono, że pytania typu „co jest najlepsze” wywołują zbyt niekonstruktywną debatę. Co sprawia, że ​​pytanie o listę zakupów jest dobrym pytaniem godnym pochwały i nagrody, podczas gdy inni się zamykają?
Alvin Thompson,
1
Zwykle logika sieci wchodzi do kontrolera, co zmienia obiekt modelu i powiadamia każdego delegata lub obserwatora.
quellish
1
Bardzo ciekawe pytania i odpowiedzi. Po 4 latach kodowania iOS i próbie znalezienia najpiękniejszego sposobu na dodanie warstwy sieciowej do aplikacji. Która klasa powinna być odpowiedzialna za zarządzanie żądaniem sieci? Poniższe odpowiedzi są naprawdę trafne. Dziękuję
Darksider,
@JoeBlow to nie jest prawda. Branża aplikacji mobilnych nadal w dużej mierze opiera się na komunikacji serwer-klient.
scord

Odpowiedzi:

327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: nie ma „najlepszego” lub „najbardziej poprawnego” podejścia do budowania architektury aplikacji. To bardzo kreatywna praca. Zawsze powinieneś wybierać najbardziej prostą i rozszerzalną architekturę, która będzie zrozumiała dla każdego programisty, który zacznie pracować nad twoim projektem lub dla innych programistów w zespole, ale zgadzam się, że może być „dobra” i „zła” „architektura.

Powiedziałeś: „ collect the most interesting approaches from experienced iOS developersNie sądzę, że moje podejście jest najbardziej interesujące lub poprawne, ale wykorzystałem je w kilku projektach i jestem z niego zadowolony. Jest to hybrydowe podejście, o którym wspomniałeś powyżej, a także ulepszenia z moich własnych badań. Interesują mnie problemy budowania podejść, które łączą kilka dobrze znanych wzorców i idiomów. Myślę, że wiele wzorców korporacyjnych Fowlera można z powodzeniem zastosować w aplikacjach mobilnych. Oto lista najciekawszych, które możemy złożyć wniosku o utworzenie architektury aplikacji iOS ( moim zdaniem ): Warstwa usługi , Jednostka pracy , Zdalna fasada , obiektu przesyłania danych ,Brama , typ warstwy , przypadek specjalny , model domeny . Zawsze należy poprawnie zaprojektować warstwę modelu i zawsze nie zapominać o trwałości (może to znacznie zwiększyć wydajność aplikacji). Możesz Core Datado tego użyć . Nie należy jednak zapominać, że Core Datanie jest to ORM ani baza danych, ale menedżer grafów obiektowych z trwałością jako dobrą opcją. Tak więc bardzo często Core Datamoże być zbyt ciężki dla twoich potrzeb i możesz spojrzeć na nowe rozwiązania, takie jak Realm i Couchbase Lite , lub zbudować własną lekką warstwę mapowania / trwałości obiektów, opartą na surowym SQLite lub LevelDB. Radzę również zapoznać się z projektami opartymi na domenach i CQRS .

Na początku myślę, że powinniśmy stworzyć kolejną warstwę dla sieci, ponieważ nie chcemy kontrolerów tłuszczu ani ciężkich, przytłoczonych modeli. Nie wierzę w te fat model, skinny controllerrzeczy. Ale wierzę w skinny everythingpodejście, ponieważ żadna klasa nie powinna być gruba, nigdy. Wszystkie sieci można generalnie wyodrębnić jako logikę biznesową, w związku z czym powinniśmy mieć kolejną warstwę, w której możemy to umieścić. Warstwa serwisowa jest tym, czego potrzebujemy:

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

W naszym MVCkrólestwie Service Layerjest coś w rodzaju pośrednika między modelem domeny a kontrolerami. Istnieje dość podobna odmiana tego podejścia zwana MVCS, gdzie a Storejest w rzeczywistości naszą Servicewarstwą. Storevending instancje modelu i obsługuje tworzenie sieci, buforowanie itp. Chcę wspomnieć, że nie powinieneś pisać całej sieci i logiki biznesowej w warstwie usług. Można to również uznać za zły projekt. Aby uzyskać więcej informacji, zobacz modele domen Anemic i Rich . Niektóre metody serwisowe i logika biznesowa mogą być obsługiwane w modelu, więc będzie to model „bogaty” (z zachowaniem).

Zawsze intensywnie korzystam z dwóch bibliotek: AFNetworking 2.0 i ReactiveCocoa . Myślę, że jest to niezbędne dla każdej nowoczesnej aplikacji, która współdziała z siecią i usługami internetowymi lub zawiera złożoną logikę interfejsu użytkownika.

ARCHITEKTURA

Najpierw tworzę APIClientklasę ogólną , która jest podklasą AFHTTPSessionManager . Jest to koń roboczy całej sieci w aplikacji: wszystkie klasy usług delegują do niego rzeczywiste żądania REST. Zawiera wszystkie dostosowania klienta HTTP, których potrzebuję w konkretnej aplikacji: przypinanie SSL, przetwarzanie błędów i tworzenie prostych NSErrorobiektów ze szczegółowymi przyczynami niepowodzenia i opisami wszystkich APIbłędów oraz błędów połączenia (w takim przypadku kontroler będzie mógł wyświetlać poprawne komunikaty dla użytkownika), ustawianie serializatorów żądań i odpowiedzi, nagłówków http i innych rzeczy związanych z siecią. Potem logicznie podzielić wszystkie wnioski API język subservices lub, bardziej poprawnie, microservices : UserSerivces, CommonServices, SecurityServices,FriendsServicesi tak dalej, zgodnie z logiką biznesową, którą wdrażają. Każda z tych mikrousług jest odrębną klasą. Razem tworzą Service Layer. Klasy te zawierają metody dla każdego żądania API, przetwarzają modele domen i zawsze zwracają a RACSignalz analizowanym modelem odpowiedzi lub NSErrorwywołującym.

Chcę wspomnieć, że jeśli masz złożoną logikę serializacji modelu - utwórz dla niej kolejną warstwę: coś takiego jak Data Mapper, ale bardziej ogólnie, np. JSON / XML -> Model mapper. Jeśli masz pamięć podręczną: utwórz ją również jako osobną warstwę / usługę (nie należy mieszać logiki biznesowej z buforowaniem). Dlaczego? Ponieważ poprawna warstwa buforująca może być dość złożona z własnymi problemami. Ludzie stosują złożoną logikę, aby uzyskać prawidłowe, przewidywalne buforowanie, takie jak np. Buforowanie monoidalne z projekcjami opartymi na profpraktorach. Możesz przeczytać o tej pięknej bibliotece o nazwie Carlos, aby dowiedzieć się więcej. I nie zapominaj, że Core Data może naprawdę pomóc ci we wszystkich problemach z buforowaniem i pozwoli ci napisać mniej logiki. Ponadto, jeśli masz trochę logiki między repozytoriumNSManagedObjectContext modelami żądań serwera a serwerami, możesz użyćWzorzec , który oddziela logikę, która pobiera dane i mapuje je do modelu encji od logiki biznesowej, która działa na model. Dlatego radzę używać wzorca repozytorium, nawet jeśli masz architekturę opartą na podstawowych danych. Repozytorium może abstrakcyjne rzeczy, jak NSFetchRequest, NSEntityDescription, NSPredicatei tak dalej do prostych metod, takich jak getczy put.

Po wszystkich tych czynnościach w warstwie usług osoba wywołująca (kontroler widoku) może wykonać złożone złożone operacje asynchroniczne z odpowiedzią: manipulowanie sygnałem, tworzenie łańcuchów, mapowanie itp. Za pomocą operacji ReactiveCocoapodstawowych lub po prostu subskrybować i wyświetlać wyniki w widoku . I wstrzyknąć z wstrzykiwania zależności We wszystkich tych klas usług moi APIClient, co przełoży konkretnego zgłoszenia serwisowego w odpowiednie GET, POST, PUT, DELETE, itd żądania do punktu końcowego REST. W takim przypadku APIClientjest niejawnie przekazywany do wszystkich kontrolerów, można to jednoznacznie sparametryzować poprzez APIClientklasy usług. Może to mieć sens, jeśli chcesz użyć różnych dostosowańAPIClientdla określonych klas usług, ale jeśli z jakichś powodów nie chcesz dodatkowych kopii lub masz pewność, że zawsze użyjesz jednej konkretnej instancji (bez dostosowań) APIClient- zrób to singleton, ale NIE, proszę NIE „Czyń klasy usług jako singletony.

Następnie każdy kontroler widoku ponownie z DI wstrzykuje potrzebną klasę usługi, wywołuje odpowiednie metody usługi i zestawia ich wyniki z logiką interfejsu użytkownika. Do wstrzykiwania zależności lubię używać BloodMagic lub bardziej zaawansowanego frameworka Typhoon . Nigdy nie używam singletonów, APIManagerWhateverklasy Bożej ani innych niewłaściwych rzeczy. Ponieważ jeśli zadzwonisz do swojej klasy WhateverManager, oznacza to, że nie znasz jej celu i jest to zły wybór projektu . Singletony to również anty-wzór, aw większości przypadków (z wyjątkiem rzadkich) jest złym rozwiązaniem. Singleton należy rozważyć tylko wtedy, gdy wszystkie trzy z następujących kryteriów są spełnione:

  1. Własności pojedynczej instancji nie można racjonalnie przypisać;
  2. Pożądana jest leniwa inicjalizacja;
  3. Globalny dostęp nie jest inaczej przewidziany.

W naszym przypadku własność pojedynczej instancji nie stanowi problemu, a także nie potrzebujemy globalnego dostępu po podzieleniu naszego boga managera na usługi, ponieważ teraz tylko jeden lub kilka dedykowanych kontrolerów potrzebuje konkretnej usługi (np. UserProfilePotrzebuje kontrolera UserServicesitd.) .

Zawsze powinniśmy szanować Szasadę w SOLID i stosować separację problemów , więc nie umieszczaj wszystkich metod obsługi i połączeń sieciowych w jednej klasie, ponieważ to szalone, szczególnie jeśli tworzysz dużą aplikację dla przedsiębiorstw. Dlatego powinniśmy rozważyć zastrzyk zależności i podejście do usług. Uważam to podejście za nowoczesne i post-OO . W tym przypadku podzieliliśmy naszą aplikację na dwie części: logikę sterowania (sterowniki i zdarzenia) oraz parametry.

Jednym rodzajem parametrów byłyby zwykłe parametry „danych”. To właśnie przekazujemy funkcjom, manipulujemy, modyfikujemy, utrzymujemy itp. Są to byty, agregaty, kolekcje, klasy spraw. Drugi rodzaj to parametry „serwisowe”. Są to klasy, które zawierają logikę biznesową, umożliwiają komunikację z systemami zewnętrznymi, zapewniają dostęp do danych.

Oto ogólny przepływ pracy mojej architektury według przykładów. Załóżmy, że mamy FriendsViewController, która wyświetla listę znajomych użytkownika i mamy opcję usunięcia z listy znajomych. W mojej FriendsServicesklasie tworzę metodę o nazwie:

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

gdzie Friendjest obiekt modelu / domeny (lub może to być tylko Userobiekt, jeśli mają podobne atrybuty). Underhood tej metody parsowań Frienddo NSDictionaryparametrów JSON friend_id, name, surname, friend_request_idi tak dalej. Zawsze używam biblioteki Mantle dla tego rodzaju szablonów i dla mojej warstwy modelu (parsowanie w przód iw tył, zarządzanie hierarchiami obiektów zagnieżdżonych w JSON i tak dalej). Po parsowania wywołuje APIClient DELETEmetodę dokonania faktycznej żądania REST i powraca Responsew RACSignalosobie dzwoniącej ( FriendsViewControllerw naszym przypadku), aby wyświetlić odpowiedni komunikat dla użytkownika lub cokolwiek.

Jeśli nasza aplikacja jest bardzo duża, musimy rozdzielić naszą logikę jeszcze wyraźniej. Np. Nie zawsze dobrze jest łączyć Repositorylub modelować logikę z Servicejednym. Kiedy opisałem swoje podejście, powiedziałem, że removeFriendmetoda powinna znajdować się w Servicewarstwie, ale jeśli będziemy bardziej pedantyczni, zauważymy, że lepiej do niej należy Repository. Pamiętajmy, czym jest repozytorium. Eric Evans podał dokładny opis w swojej książce [DDD]:

Repozytorium reprezentuje wszystkie obiekty określonego typu jako zbiór pojęciowy. Działa jak kolekcja, z wyjątkiem bardziej skomplikowanych funkcji zapytań.

A zatem Repositoryjest zasadniczo fasadą, która wykorzystuje semantykę stylu Kolekcji (Dodaj, Aktualizuj, Usuń) w celu zapewnienia dostępu do danych / obiektów. Dlatego, gdy masz coś takiego: getFriendsList, getUserGroups, removeFriendmożna umieścić go w Repository, ponieważ kolekcja podobny semantyki jest całkiem jasne tutaj. I kod jak:

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

jest zdecydowanie logiką biznesową, ponieważ wykracza poza podstawowe CRUDoperacje i łączy dwa obiekty domeny ( Friendi Request), dlatego należy ją umieścić w Servicewarstwie. Chcę też zauważyć: nie twórz niepotrzebnych abstrakcji . Mądrze stosuj wszystkie te podejścia. Ponieważ jeśli przytłoczysz swoją aplikację abstrakcjami, zwiększy to jej przypadkową złożoność, a złożoność powoduje więcej problemów w systemach oprogramowania niż cokolwiek innego

Opisuję wam „stary” przykład Celu C, ale to podejście można bardzo łatwo dostosować do języka Swift z dużo większą liczbą ulepszeń, ponieważ ma ono więcej przydatnych funkcji i funkcjonalny cukier. Bardzo polecam skorzystać z tej biblioteki: Moya . Pozwala stworzyć bardziej elegancką APIClientwarstwę (nasz koń roboczy, jak pamiętasz). Teraz nasz APIClientdostawca będzie typem wartości (wyliczeniem) z rozszerzeniami zgodnymi z protokołami i wykorzystującymi dopasowanie wzorca destrukcji. Szybkie wyliczanie + dopasowanie wzorców pozwala nam tworzyć algebraiczne typy danych, jak w klasycznym programowaniu funkcjonalnym. Nasze mikrousługi wykorzystają tego ulepszonego APIClientdostawcę, jak w zwykłym podejściu Celu C. Do warstwy modelu zamiast Mantlemożesz użyć biblioteki ObjectMapperlub lubię używać bardziej eleganckiej i funkcjonalnej biblioteki Argo .

Opisałem więc moje ogólne podejście architektoniczne, które, jak sądzę, można dostosować do każdego zastosowania. Oczywiście można wprowadzić znacznie więcej ulepszeń. Radzę nauczyć się programowania funkcjonalnego, ponieważ możesz z niego wiele skorzystać, ale nie posuwaj się za daleko. Wyeliminowanie nadmiernego, wspólnego, globalnego stanu mutable, stworzenie niezmiennego modelu domeny lub stworzenie czystych funkcji bez zewnętrznych skutków ubocznych jest ogólnie dobrą praktyką, a nowy Swiftjęzyk zachęca do tego. Ale zawsze pamiętaj, że przeciążanie kodu ciężkimi czystymi wzorcami funkcjonalnymi, podejście teoretyczne do kategorii jest złym pomysłem, ponieważ inne programiści będą czytać i wspierać Twój kod, i mogą być sfrustrowani lub przerażeniprismatic profunctors i tego rodzaju rzeczy w niezmiennym modelu. To samo dotyczy ReactiveCocoa: nie RACifykod za bardzo , ponieważ może stać się bardzo nieczytelny, szczególnie dla początkujących. Użyj go, gdy naprawdę może uprościć twoje cele i logikę.

Więc read a lot, mix, experiment, and try to pick up the best from different architectural approaches. To najlepsza rada, jaką mogę ci dać.

Ołeksandr Karaberow
źródło
Również ciekawe i solidne podejście. Dzięki.
MainstreamDeveloper00
1
@darksider Jak już napisałem w odpowiedzi: „Nigdy nie używam singletonów, Boże APIManager, bez względu na klasę lub inne złe rzeczy, ponieważ singleton jest anty-wzorcem, aw większości przypadków (z wyjątkiem rzadkich) jest złym rozwiązaniem. ". 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 raz `) w każdym kontrolerze
Oleksandr Karaberov
14
Cześć @alexander. Czy masz jakieś przykładowe projekty na GitHub? Opisujesz bardzo interesujące podejście. Dzięki. Ale jestem początkującym w rozwoju Celu C. A dla mnie trudno jest zrozumieć niektóre aspekty. Może możesz przesłać jakiś projekt testowy na GitHub i podać link?
Denis
1
Witaj @AlexanderKaraberov, jestem trochę zdezorientowany, jeśli chodzi o wyjaśnienie dotyczące sklepu. Załóżmy, że mam 5 modeli, dla każdej mam 2 klasy, z których jedna utrzymuje sieć i inne buforowanie obiektów. Teraz powinienem mieć osobną klasę Store dla każdego modelu, która wywołuje funkcję sieci i klasy pamięci podręcznej, lub pojedynczą klasę Store, która ma wszystkie funkcje dla każdego modelu, aby sterownik zawsze miał dostęp do jednego pliku danych.
meteory
1
@icodebuster Ten projekt demonstracyjny pomógł mi zrozumieć wiele pojęć przedstawionych tutaj: github.com/darthpelo/NetworkLayerExample
31

Zgodnie z celem tego pytania chciałbym opisać nasze podejście do architektury.

Podejście architektury

Nasza ogólna architektura aplikacji na iOS opiera się na następujących wzorcach: warstwy usług , MVVM , wiązanie danych interfejsu użytkownika , wstrzykiwanie zależności ; oraz paradygmat programowania funkcjonalnego reaktywnego .

Możemy podzielić typową aplikację skierowaną do konsumentów na następujące logiczne warstwy:

  • montaż
  • Model
  • Usługi
  • Przechowywanie
  • Menedżerowie
  • Koordynatorzy
  • Interfejs użytkownika
  • Infrastruktura

Warstwa montażowa jest punktem początkowym naszej aplikacji. Zawiera kontener wstrzykiwania zależności oraz deklaracje obiektów aplikacji i ich zależności. Ta warstwa może również zawierać konfigurację aplikacji (adresy URL, klucze usług stron trzecich i tak dalej). W tym celu korzystamy z biblioteki Typhoon .

Warstwa modelu zawiera klasy modeli domen, walidacje, mapowania. Używamy biblioteki Mantle do mapowania naszych modeli: obsługuje serializację / deserializację do JSONformatu i NSManagedObjectmodeli. Do sprawdzania poprawności i reprezentacji formularzy naszych modeli używamy bibliotek FXForm i FXModelValidation .

Warstwa usług deklaruje usługi, których używamy do interakcji z systemami zewnętrznymi w celu wysyłania lub odbierania danych reprezentowanych w naszym modelu domeny. Zwykle mamy więc usługi komunikacji z interfejsami API serwera (na jednostkę), usługi przesyłania wiadomości (takie jak PubNub ), usługi pamięci masowej (jak Amazon S3) itp. Zasadniczo usługi obejmują obiekty dostarczane przez SDK (na przykład SDK PubNub) lub implementują własną komunikację logika. Do ogólnych sieci korzystamy z biblioteki AFNetworking .

Celem warstwy pamięci jest zorganizowanie lokalnego przechowywania danych na urządzeniu. Używamy do tego Core Data lub Realm (oba mają zalety i wady, decyzja o tym, czego użyć, zależy od konkretnych specyfikacji). Do konfiguracji danych podstawowych używamy biblioteki MDMCoreData i szeregu klas - magazynów - (podobnych do usług), które zapewniają dostęp do lokalnej pamięci dla każdej jednostki. W Realm używamy podobnych magazynów, aby mieć dostęp do lokalnego magazynu.

Warstwa menedżerów to miejsce, w którym żyją nasze abstrakcje / opakowania.

W roli menedżera może być:

  • Credentials Manager z różnymi implementacjami (pęku kluczy, NSDefaults, ...)
  • Menedżer bieżącej sesji, który wie, jak zachować i zapewnić bieżącą sesję użytkownika
  • Capture Pipeline, który zapewnia dostęp do urządzeń multimedialnych (nagrywanie wideo, audio, robienie zdjęć)
  • BLE Manager, który zapewnia dostęp do usług i urządzeń peryferyjnych Bluetooth
  • Geo Location Manager
  • ...

Tak więc w roli menedżera może znajdować się dowolny obiekt, który implementuje logikę określonego aspektu lub troski potrzebnej do działania aplikacji.

Staramy się unikać Singletonów, ale ta warstwa jest miejscem, w którym żyją, jeśli są potrzebne.

Warstwa koordynatorów zapewnia obiekty zależne od obiektów z innych warstw (usługa, pamięć, model) w celu połączenia ich logiki w jedną sekwencję pracy wymaganą dla określonego modułu (funkcja, ekran, historia użytkownika lub doświadczenie użytkownika). Zwykle łączy operacje asynchroniczne i wie, jak reagować na ich przypadki powodzenia i niepowodzenia. Jako przykład możesz wyobrazić sobie funkcję przesyłania wiadomości i odpowiedni MessagingCoordinatorobiekt. Obsługa operacji wysyłania wiadomości może wyglądać następująco:

  1. Sprawdź poprawność komunikatu (warstwa modelu)
  2. Zapisz wiadomość lokalnie (miejsce na wiadomości)
  3. Prześlij załącznik wiadomości (usługa Amazon S3)
  4. Zaktualizuj stan wiadomości i adresy URL załączników i zapisz wiadomość lokalnie (miejsce na wiadomości)
  5. Serializuj komunikat do formatu JSON (warstwa modelu)
  6. Opublikuj wiadomość w PubNub (usługa PubNub)
  7. Zaktualizuj status i atrybuty wiadomości i zapisz ją lokalnie (miejsce na wiadomości)

Na każdym z powyższych kroków odpowiednio obsługiwany jest błąd.

Warstwa interfejsu użytkownika składa się z następujących podwarstw:

  1. ViewModels
  2. ViewControllers
  3. Wyświetlenia

Aby uniknąć kontrolerów Massive View używamy wzorca MVVM i implementujemy logikę potrzebną do prezentacji interfejsu użytkownika w ViewModels. ViewModel zwykle ma koordynatorów i menedżerów jako zależności. ViewModels używane przez ViewControllers i niektóre rodzaje widoków (np. Komórki widoku tabeli). Klej pomiędzy ViewControllers i ViewModels to Powiązanie danych i Wzorzec poleceń. Aby mieć ten klej, używamy biblioteki ReactiveCocoa .

Używamy również ReactiveCocoa i jego RACSignalkoncepcji jako interfejsu i typu wartości zwracanej przez wszystkich koordynatorów, usługi, metody przechowywania. To pozwala nam łączyć operacje, uruchamiać je równolegle lub szeregowo oraz wiele innych przydatnych rzeczy dostarczanych przez ReactiveCocoa.

Staramy się implementować nasze zachowanie interfejsu użytkownika w sposób deklaratywny. Wiązanie danych i automatyczny układ bardzo pomagają w osiągnięciu tego celu.

Warstwa infrastruktury zawiera wszystkie pomocniki, rozszerzenia, narzędzia potrzebne do pracy aplikacji.


To podejście działa dobrze dla nas i dla tych typów aplikacji, które zwykle tworzymy. Ale powinieneś zrozumieć, że jest to tylko subiektywne podejście, które należy dostosować / zmienić dla konkretnych celów zespołu.

Mam nadzieję, że to ci pomoże!

Więcej informacji na temat procesu tworzenia systemu iOS można znaleźć w tym poście na blogu Rozwój systemu iOS jako usługa

Alex Petropavlovsky
źródło
Zaczęłam lubić tę architekturę kilka miesięcy temu, dzięki Alex za udostępnienie! Chciałbym wypróbować to z RxSwift w najbliższej przyszłości!
ingaham
18

Ponieważ wszystkie aplikacje na iOS są różne, myślę, że należy tutaj rozważyć różne podejścia, ale zwykle idę w ten sposób:
Utwórz klasę centralnego menedżera (singleton) do obsługi wszystkich żądań API (zwykle o nazwie APICommunicator), a każda metoda instancji jest wywołaniem API . I jest jedna centralna (niepubliczna) metoda:

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

Dla przypomnienia używam 2 głównych bibliotek / frameworków, ReactiveCocoa i AFNetworking. ReactiveCocoa doskonale obsługuje asynchroniczne odpowiedzi sieciowe, możesz to zrobić (sendNext :, sendError :, itd.).
Ta metoda wywołuje interfejs API, pobiera wyniki i wysyła je przez RAC w formacie „surowym” (np. NSArray, co zwraca sieć AF).
Następnie metoda podobna do tej, getStuffList:która wywołała powyższą metodę, subskrybuje jego sygnał, analizuje surowe dane w obiekty (za pomocą czegoś takiego jak Motis) i wysyła obiekty jeden po drugim do osoby wywołującej ( getStuffList:i podobne metody również zwracają sygnał, który kontroler może subskrybować) ).
Subskrybowany kontroler odbiera obiekty przez subscribeNext:blok i obsługuje je.

Próbowałem wielu sposobów w różnych aplikacjach, ale ten działał najlepiej ze wszystkich, więc ostatnio używałem go w kilku aplikacjach, pasuje zarówno do małych, jak i dużych projektów i jest łatwy do rozszerzenia i utrzymania, jeśli coś wymaga modyfikacji.
Mam nadzieję, że to pomaga, chciałbym usłyszeć opinie innych na temat mojego podejścia i być może, jak inni myślą, że można to poprawić.

Rickye
źródło
2
Dzięki za odpowiedź +1. Dobre podejscie. Zostawiam pytanie. Być może będziemy mieli inne podejścia od innych programistów.
MainstreamDeveloper00
1
Podoba mi się wariant tego podejścia - używam centralnego menedżera API, który zajmuje się mechaniką komunikacji z API. Jednak staram się, aby cała funkcjonalność była widoczna na obiektach mojego modelu. Modele będą dostępne metody, jak + (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;i - (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;które zrobić niezbędne przygotowania, a następnie zadzwonić do kierownika poprzez API.
jsadler
1
Takie podejście jest proste, ale wraz ze wzrostem liczby interfejsów API trudniej jest zarządzać menedżerem singleton API. I każdy nowy dodany interfejs API będzie odnosił się do menedżera, bez względu na moduł, do którego należy ten interfejs API. Spróbuj użyć github.com/kevin0571/STNetTaskQueue do zarządzania żądaniami API.
Kevin
Poza tym, dlaczego reklamujesz swoją bibliotekę, która jest jak najdalej od mojego rozwiązania i znacznie bardziej skomplikowana, wypróbowałem to podejście w niezliczonych projektach, zarówno małych, jak i dużych, jak wspomniano, i korzystałem z niego dokładnie to samo odkąd napisałem tę odpowiedź. Dzięki sprytnym konwencjom nazewnictwa wcale nie jest to trudne do utrzymania.
Rickye,
8

W mojej sytuacji zwykle używam biblioteki ResKit do konfigurowania warstwy sieciowej. Zapewnia łatwą w użyciu analizę. Zmniejsza to mój wysiłek związany z konfigurowaniem mapowania dla różnych odpowiedzi i innych rzeczy.

Dodaję tylko trochę kodu, aby automatycznie ustawić mapowanie. Definiuję klasę podstawową dla moich modeli (nie protokołu z powodu dużej ilości kodu, aby sprawdzić, czy jakaś metoda jest zaimplementowana, czy nie, i mniej kodu w samych modelach):

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

Relacje to obiekty, które w odpowiedzi reprezentują obiekty zagnieżdżone:

Związek Obiekt.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

Związek Obiekt.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

Następnie konfiguruję mapowanie RestKit w następujący sposób:

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

Przykład implementacji MappableEntry:

Użytkownik. 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

Użytkownik.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

Teraz o pakowaniu żądań:

Mam plik nagłówkowy z definicją bloków, aby zmniejszyć długość linii we wszystkich klasach APIRequest:

APICallbacks.h

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

I przykład mojej klasy APIRequest, której używam:

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

A wszystko, co musisz zrobić w kodzie, wystarczy zainicjować obiekt API i wywołać go w dowolnym momencie:

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

Mój kod nie jest idealny, ale łatwo go ustawić raz i używać w różnych projektach. Jeśli jest to interesujące dla kogoś, mógłbym poświęcić trochę czasu i stworzyć uniwersalne rozwiązanie gdzieś na GitHub i CocoaPods.

Andrew Cherkashyn
źródło
7

Moim zdaniem cała architektura oprogramowania jest napędzana potrzebą. Jeśli jest to do celów edukacyjnych lub osobistych, wybierz główny cel i poprowadź architekturę. Jeśli jest to praca najemna, potrzeba biznesu jest najważniejsza. Sztuką jest, aby nie pozwolić błyszczącym rzeczom odciągnąć cię od rzeczywistych potrzeb. Trudno mi to zrobić. W tym biznesie zawsze pojawiają się nowe błyszczące rzeczy i wiele z nich jest nieprzydatnych, ale nie zawsze można to powiedzieć z góry. Skoncentruj się na potrzebie i bądź gotów porzucić złe wybory, jeśli możesz.

Na przykład ostatnio zrobiłem szybki prototyp aplikacji do udostępniania zdjęć dla lokalnej firmy. Ponieważ potrzebą firmy było zrobienie czegoś szybkiego i brudnego, architektura zakończyła się kodem iOS, który wyświetlał kamerę, oraz kodem sieciowym dołączonym do przycisku wysyłania, który przesłał obraz do sklepu S3 i napisał w domenie SimpleDB. Kod był trywialny, a jego koszt minimalny, a klient ma skalowalną kolekcję zdjęć dostępną w Internecie za pomocą wywołań REST. Tania i głupia, aplikacja miała wiele wad i czasami blokowała interfejs użytkownika, ale zrobienie więcej dla prototypu byłoby marnotrawstwem i pozwala im wdrożyć do swoich pracowników i łatwo wygenerować tysiące zdjęć testowych bez wydajności lub skalowalności obawy. Nędzna architektura, ale idealnie pasująca do potrzeb i kosztująca.

Inny projekt obejmował wdrożenie lokalnej bezpiecznej bazy danych, która synchronizuje się z systemem firmy w tle, gdy sieć jest dostępna. Stworzyłem synchronizator w tle, który używał RestKit, ponieważ wydawało się, że ma wszystko, czego potrzebowałem. Ale musiałem napisać tyle niestandardowego kodu dla RestKit, aby poradzić sobie z idiosynkratycznym JSON, że mogłem to zrobić szybciej, pisząc własny JSON do transformacji CoreData. Jednak klient chciał wprowadzić tę aplikację do domu i czułem, że RestKit będzie podobny do frameworków, których używali na innych platformach. Czekam, czy to dobra decyzja.

Ponownie, moim problemem jest skupienie się na potrzebie i pozwolenie na określenie architektury. Staram się jak cholera, aby uniknąć korzystania z pakietów stron trzecich, ponieważ powodują one koszty, które pojawiają się dopiero po pewnym czasie działania aplikacji. Staram się unikać tworzenia hierarchii klas, ponieważ rzadko się to opłaca. Jeśli mogę napisać coś w rozsądnym czasie zamiast przyjąć pakiet, który nie pasuje idealnie, to robię to. Mój kod jest dobrze skonstruowany do debugowania i odpowiednio skomentowany, ale rzadko są to pakiety stron trzecich. Powiedziawszy to, uważam, że AF Networking jest zbyt przydatny, aby ignorować i dobrze zorganizowany, dobrze skomentowany i utrzymywany, i często go używam! RestKit obejmuje wiele typowych przypadków, ale czuję się, jakbym walczył, gdy go używam, a większość źródeł danych, które napotykam, jest pełna dziwactw i problemów, które najlepiej rozwiązać przy pomocy niestandardowego kodu. W moich ostatnich aplikacjach używam tylko wbudowanych konwerterów JSON i piszę kilka metod narzędziowych.

Jednym ze wzorów, których zawsze używam, jest usunięcie połączeń sieciowych z głównego wątku. Ostatnie 4-5 aplikacji, które wykonałem, skonfigurowałem zadanie timera w tle przy użyciu dispatch_source_create, który budzi się tak często i wykonuje zadania sieciowe w razie potrzeby. Musisz wykonać pewne prace związane z bezpieczeństwem wątków i upewnić się, że kod modyfikujący interfejs użytkownika zostanie wysłany do głównego wątku. Pomaga także w wykonywaniu procesu instalacji / inicjalizacji w taki sposób, aby użytkownik nie czuł się obciążony ani opóźniony. Do tej pory działało to dość dobrze. Proponuję zajrzeć do tych rzeczy.

Wreszcie myślę, że w miarę jak pracujemy coraz więcej i ewoluuje system operacyjny, mamy tendencję do opracowywania lepszych rozwiązań. Wiele lat zajęło mi przełamanie mojego przekonania, że ​​muszę przestrzegać wzorów i wzorów, które inni ludzie uważają za obowiązkowe. Jeśli pracuję w kontekście, w którym jest to część lokalnej religii, mam na myśli najlepsze praktyki inżynieryjne w departamencie, to przestrzegam zwyczajów do listu, za to mi płacą. Ale rzadko stwierdzam, że podążanie za starszymi projektami i wzorami jest optymalnym rozwiązaniem. Zawsze staram się patrzeć na rozwiązanie przez pryzmat potrzeb biznesowych i budować architekturę dopasowaną do niego i utrzymywać rzeczy tak proste, jak tylko mogą być. Kiedy wydaje mi się, że jest za mało, ale wszystko działa poprawnie, to jestem na dobrej drodze.

Fran K.
źródło
4

Korzystam z podejścia, które dostałem stąd: https://github.com/Constantine-Fry/Foursquare-API-v2 . Ponownie napisałem tę bibliotekę w Swift i możesz zobaczyć podejście architektoniczne z tych części kodu:

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}}
}

Zasadniczo istnieje podklasa NSOperation, która tworzy NSURLRequest, analizuje odpowiedź JSON i dodaje blok zwrotny z wynikiem do kolejki. Główna klasa API konstruuje NSURLRequest, inicjuje podklasę NSOperation i dodaje ją do kolejki.

bzz
źródło
3

Stosujemy kilka podejść w zależności od sytuacji. W większości przypadków AFNetworking to najprostsze i najbardziej niezawodne podejście, w którym można ustawiać nagłówki, przesyłać dane wieloczęściowe, używać GET, POST, PUT & DELETE, a dla UIKit jest wiele dodatkowych kategorii, które pozwalają na przykład ustawić obraz z adres URL. W złożonej aplikacji z dużą liczbą połączeń czasami streszczamy to w oparciu o naszą własną wygodną metodę, która wyglądałaby mniej więcej tak:

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

Istnieje kilka sytuacji, w których AFNetworking nie jest odpowiedni, na przykład w przypadku tworzenia frameworka lub innego komponentu biblioteki, ponieważ AFNetworking może już znajdować się w innej bazie kodu. W tej sytuacji użyłbyś NSMutableURLRequest albo wbudowany, jeśli wykonujesz pojedyncze wywołanie, albo abstrakcji w klasę żądania / odpowiedzi.

Jaskółka oknówka
źródło
Dla mnie to najlepsza i najczystsza odpowiedź, na zdrowie. "To takie proste". @martin, osobiście przez cały czas korzystamy z NSMutableURLRequest; czy jest jakiś prawdziwy powód, aby korzystać z AFNetworking?
Fattie
AFNetworking jest naprawdę wygodny. Dla mnie sukcesy i niepowodzenia bloków sprawiają, że warto, ponieważ ułatwia zarządzanie kodem. Zgadzam się jednak, że czasami jest to całkowita przesada.
Martin
Świetny punkt na blokach, dzięki za to. Wydaje mi się, że specyfika tego wszystkiego zmieni się wraz z Swift.
Fattie,
2

Podczas projektowania aplikacji unikam singletonów. Są typowe dla wielu osób, ale myślę, że bardziej eleganckie rozwiązania można znaleźć gdzie indziej. Zazwyczaj to, co robię, to budowanie moich jednostek w CoreData, a następnie umieszczanie mojego kodu REST w kategorii NSManagedObject. Gdybym na przykład chciał utworzyć i wysłać nowego użytkownika POST, zrobiłbym to:

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

Używam RESTKit do mapowania obiektów i inicjalizuję go podczas uruchamiania. Przekierowanie wszystkich twoich połączeń przez singleton jest stratą czasu i dodaje dużo płyty kotłowej, która nie jest potrzebna.

W 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];
}

W 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];
}

Po co dodawać dodatkowe klasy pomocnicze, kiedy można rozszerzyć funkcjonalność wspólnej klasy podstawowej o kategorie?

Jeśli jesteś zainteresowany bardziej szczegółowymi informacjami na temat mojego rozwiązania, daj mi znać. Z przyjemnością się dzielę.

Sandy Chapman
źródło
3
Byłbym zdecydowanie zainteresowany bardziej szczegółowym przeczytaniem tego podejścia w poście na blogu.
Danyal Aytekin
0

Z czysto klasowego punktu widzenia zazwyczaj będziesz mieć coś takiego:

  • Twój widok kontrolerów kontroli jeden lub więcej widoków
  • Klasa modelu danych - To naprawdę zależy od tego, z iloma rzeczywistymi odrębnymi bytami masz do czynienia i jak są one powiązane.

    Na przykład, jeśli masz tablicę elementów do wyświetlenia w czterech różnych reprezentacjach (lista, wykres, wykres itp.), Będziesz mieć jedną klasę modelu danych dla listy elementów, i jeszcze jedną dla pozycji. TheLista klasie pozycja zostanie podzielona przez cztery widok kontrolerów - wszystkich dzieci kontrolera bar tab lub kontrolerem nawigacyjnym.

    Klasy modeli danych przydadzą się nie tylko do wyświetlania danych, ale także do szeregowania ich, przy czym każda z nich może ujawnić swój własny format serializacji za pomocą metod eksportu JSON / XML / CSV (lub cokolwiek innego).

  • Ważne jest, aby zrozumieć, że potrzebujesz także klas konstruktorów żądań API które są mapowane bezpośrednio na punkty końcowe interfejsu API REST. Załóżmy, że masz interfejs API, który loguje użytkownika - więc klasa konstruktora interfejsu API logowania utworzy ładunek POST JSON dla interfejsu API logowania. W innym przykładzie klasa narzędzia budującego żądania API dla listy elementów katalogu API utworzy ciąg zapytania GET dla odpowiedniego interfejsu API i uruchomi zapytanie REST GET.

    Te klasy konstruktorów żądań API zwykle otrzymują dane od kontrolerów widoku, a także przekazują te same dane z powrotem do kontrolerów widoku w celu aktualizacji interfejsu użytkownika / innych operacji. Wyświetl kontrolery następnie zdecydują, jak zaktualizować obiekty modelu danych o te dane.

  • Wreszcie, serce klienta REST - klasa modułu pobierania danych API, która jest nieświadoma wszystkich żądań API twoich aplikacji. Ta klasa będzie prawdopodobnie singletonem, ale jak zauważyli inni, nie musi to być singleton.

    Pamiętaj, że link jest tylko typową implementacją i nie bierze pod uwagę scenariuszy takich jak sesja, pliki cookie itp., Ale wystarcza, abyś mógł zacząć bez korzystania z zewnętrznych platform.

Nirav Bhatt
źródło
0

To pytanie ma już wiele doskonałych i obszernych odpowiedzi, ale czuję, że muszę o tym wspomnieć, ponieważ nikt inny nie ma.

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

Jest tworzony przez te same osoby co AFNetworking, ale jest zaprojektowany bardziej bezpośrednio z myślą o Swift.

matt.writes.code
źródło
0

Myślę, że na razie średni projekt korzysta z architektury MVVM, a duży projekt korzysta z architektury VIPER i stara się to osiągnąć

  • Programowanie zorientowane na protokół
  • Wzorce projektowania oprogramowania
  • SPRZEDANA zasada
  • Programowanie ogólne
  • Nie powtarzaj się (DRY)

I Podejścia architektoniczne do budowania aplikacji sieciowych iOS (klienci REST)

Problem separacji dla czystego i czytelnego kodu pozwala uniknąć powielania:

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)
}

inwersja zależności

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

Główny odpowiedzialny:

  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()
        }
    }

Znajdziesz tutaj architekturę GitHub MVVM z pozostałym API Swift Project

Nazmul Hasan
źródło
0

W inżynierii oprogramowania mobilnego najczęściej stosowane są wzorce Clean Architecture + MVVM i Redux.

Czysta architektura + MVVM składa się z 3 warstw: domeny, prezentacji, warstw danych. W przypadku gdy warstwa prezentacji i warstwa repozytoriów danych zależą od warstwy domeny:

Presentation Layer -> Domain Layer <- Data Repositories Layer

Warstwa prezentacji składa się z 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

W tym artykule znajduje się bardziej szczegółowy opis Clean Architecture + MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Oleh Kudinov
źródło