[ios] iOS 네트워킹 애플리케이션 (REST 클라이언트)을 구축하기위한 최고의 아키텍처 접근 방식

나는 경험이있는 iOS 개발자 이며이 질문은 정말 흥미 롭습니다. 나는이 주제에 관해 많은 다른 자원과 자료를 보았지만 그럼에도 여전히 혼란 스럽다. iOS 네트워크 애플리케이션에 가장 적합한 아키텍처는 무엇입니까? 기본 추상 프레임 워크, 패턴을 의미합니다. 서버 요청이 적거나 복잡한 REST 클라이언트가있는 작은 앱이든 모든 네트워킹 응용 프로그램에 적합합니다. Apple MVC은 모든 iOS 응용 프로그램에 기본 아키텍처 접근 방식 을 사용 하는 것이 좋지만 , MVC현대적인 MVVM패턴 이나 네트워크 논리 코드의 배치 위치 및 일반적인 구성 방법에 대해서는 설명 하지 않습니다 .

MVCS( Sfor Service)와 같은 것을 개발해야합니까? 이 Service계층 에서 모든 API요청과 다른 네트워킹 논리를 넣으십시오 . 몇 가지 연구를 한 후 두 가지 기본 접근법을 발견했습니다. 여기 에서는 기본 요청 추상 클래스에서 모두 상속되는 웹 서비스에 대한 모든 네트워크 요청 API( LoginRequest클래스 또는 PostCommentRequest클래스 등)에 대해 AbstractBaseRequest별도의 클래스 를 작성하고 공통 네트워킹 코드를 캡슐화하는 글로벌 네트워크 관리자 를 작성하는 것이 좋습니다. 다른 환경 설정 ( AFNetworking맞춤 설정 또는RestKit복잡한 객체 매핑 및 지속성 또는 표준 API를 사용한 자체 네트워크 통신 구현이있는 경우 조정). 그러나이 접근법은 나에게 오버 헤드 인 것 같습니다. 또 다른 방법은 몇 가지 싱글 것입니다 API첫 번째 방법에서와 같이 디스패처 또는 관리자 클래스를, 하지만 같은이 관리자 클래스의 인스턴스 공개 방법으로 모든 요청을 캡슐화하는 대신 모든 요청에 대해 클래스를 생성하고 : fetchContacts, loginUser방법 등 그래서를, 무엇을 가장 좋고 올바른 방법입니까? 아직 모르는 다른 흥미로운 접근법이 있습니까?

그리고 Service, 또는 이와 같은 모든 네트워킹 물건에 대해 다른 계층을 만들 거나 NetworkProviderMVC아키텍처 위에 무엇이든 만들거 나이 계층을 기존 MVC계층에 통합 (주입)해야 Model합니까?

나는 아름다운 접근법이 있거나 Facebook 클라이언트 또는 LinkedIn 클라이언트와 같은 모바일 몬스터가 어떻게 점점 증가하는 네트워킹 논리의 복잡성을 처리하는지 알고 있습니까?

문제에 대한 정확하고 공식적인 답변이 없다는 것을 알고 있습니다. 이 질문의 목표는 숙련 된 iOS 개발자로부터 가장 흥미로운 접근 방식을 수집하는 것입니다 . 최선의 제안 방법은 승인 된 것으로 표시되고 평판 현상금이 수여되며 다른 방법은 찬성됩니다. 그것은 주로 이론적이고 연구적인 질문입니다. iOS의 네트워킹 응용 프로그램에 대한 기본적이고 추상적이며 올바른 아키텍처 접근 방식을 이해하고 싶습니다. 숙련 된 개발자의 자세한 설명을 바랍니다.



답변

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: 애플리케이션 아키텍처를 구축하기위한 “최고”또는 “가장 올바른”접근 방법 이 없습니다 . 그것은이다 매우 창조적 인 작업. 항상 가장 간단하고 확장 가능한 아키텍처를 선택해야합니다.이 아키텍처는 프로젝트에서 작업을 시작한 모든 개발자 나 팀의 다른 개발자에게 분명하지만 “좋음”과 “나쁨”이있을 수 있음에 동의합니다. ” 건축물.

당신은 말했다 : collect the most interesting approaches from experienced iOS developers, 나는 나의 접근 방식이 가장 흥미 롭거나 정확하다고 생각하지 않지만, 나는 여러 프로젝트에서 그것을 사용하고 만족했다. 그것은 위에서 언급 한 것과 하이브리드 연구 방식으로 개선 된 접근법입니다. 여러 잘 알려진 패턴과 관용구를 결합한 접근 방식을 구축하는 문제에 흥미가 있습니다. Fowler의 많은 엔터프라이즈 패턴 이 모바일 애플리케이션에 성공적으로 적용될 수 있다고 생각합니다 . 여기에서 우리는 아이폰 OS 응용 프로그램 아키텍처를 만들기 위해 적용 할 수있는 가장 흥미로운 것들의 목록입니다 ( 제 생각에 ) : 서비스 계층 , 작업 단위 , 원격 외관 , 데이터 전송 개체 ,게이트웨이 , 계층 슈퍼 타입 , 특수 사례 , 도메인 모델 . 항상 모델 레이어를 올바르게 설계하고 지속성을 잊지 마십시오 (앱 성능을 크게 향상시킬 수 있음). Core Data이것을 위해 사용할 수 있습니다 . 하지만 당신은 그 잊지 Core DataORM 또는 데이터베이스하지만, 좋은 옵션으로 지속성을 가진 객체 그래프 매니저가 없습니다. 따라서 Core Data요구 사항에 비해 너무 무거울 수 있으며 RealmCouchbase Lite 와 같은 새로운 솔루션을 보거나 원시 SQLite 또는 LevelDB를 기반으로 자체 경량 객체 매핑 / 지속성 계층을 구축 할 수 있습니다. 또한 도메인 기반 설계CQRS 에 익숙해 지도록 조언합니다 .

처음에는 생각, 우리가 해야 우리가 지방 컨트롤러 또는 무거운 압도 모델을 원하지 않기 때문에, 네트워킹을위한 또 다른 레이어를 만듭니다. 나는 그런 fat model, skinny controller것들을 믿지 않습니다 . 그러나 나는 믿는다skinny everything더 클래스는 지금까지, 지방 없어야하기 때문에, 접근. 모든 네트워킹은 일반적으로 비즈니스 로직으로 추상화 될 수 있으므로 다른 계층이 있어야합니다. 서비스 계층 은 우리에게 필요한 것입니다.

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

우리 MVC영역 Service Layer에는 도메인 모델과 컨트롤러 사이의 중재자와 같은 것이 있습니다. a 가 실제로 우리의 계층 인 MVCS 라고하는이 접근 방식에는 다소 유사한 변형이 있습니다. 모델 인스턴스를 공급하고 네트워킹, 캐싱 등을 처리합니다 . 서비스 계층에 모든 네트워킹 및 비즈니스 로직을 작성 해서는 안된다는 것을 언급하고 싶습니다 . 이것은 또한 나쁜 디자인으로 간주 될 수 있습니다. 자세한 내용은 AnemicRich 도메인 모델을 참조하십시오. 일부 서비스 방법 및 비즈니스 로직은 모델에서 처리 될 수 있으므로 “풍부한”(행동이있는) 모델이됩니다.
StoreServiceStore

나는 항상 AFNetworking 2.0ReactiveCocoa의 두 라이브러리를 광범위하게 사용 합니다. 네트워크 및 웹 서비스와 상호 작용하거나 복잡한 UI 논리를 포함하는 최신 응용 프로그램 의 필수 요소 라고 생각합니다 .

건축물

처음에는 AFHTTPSessionManagerAPIClient 의 하위 클래스 인 일반 클래스를 만듭니다 . 이것은 애플리케이션에서 모든 네트워킹의 핵심 요소입니다. 모든 서비스 클래스는 실제 REST 요청을 위임합니다. 여기에는 특정 응용 프로그램에서 필요한 HTTP 클라이언트의 모든 사용자 정의가 포함되어 있습니다 : SSL 고정, 오류 처리 및 NSError자세한 실패 이유 및 모든 설명 API및 연결 오류 설명이있는 간단한 객체 만들기 (이러한 경우 컨트롤러는 올바른 메시지를 표시 할 수 있음) 사용자), 설정 요청 및 응답 시리얼 라이저, http 헤더 및 기타 네트워크 관련 항목. 그럼 난 논리적으로 하위 서비스 또는, 더 정확하게에 모든 API 요청, 분할 microservices을 : UserSerivces, CommonServices, SecurityServices,FriendsServices그리고 그들이 구현하는 비즈니스 로직에 따라. 이러한 마이크로 서비스는 각각 별도의 클래스입니다. 그들은 함께 Service Layer. 이 클래스에는 각 API 요청, 프로세스 도메인 모델에 대한 메소드가 포함 RACSignal되며 구문 분석 된 응답 모델 또는 NSError호출자에게 항상를 리턴 합니다.

복잡한 모델 직렬화 논리가있는 경우 Data Mapper 와 같은 다른 레이어를 만들지 만 JSON / XML-> Model mapper 와 같은 일반적인 레이어를 언급하고 싶습니다 . 캐시가있는 경우 : 별도의 계층 / 서비스로 캐시를 작성하십시오 (비즈니스 로직을 캐싱과 혼합해서는 안 됨). 왜? 올바른 캐싱 레이어는 자체 문제로 인해 매우 복잡 할 수 있습니다. 사람들은 복잡한 논리를 구현하여 예를 들어 profunctors를 기반으로 프로젝션을 사용하는 단일체 캐싱과 같이 유효하고 예측 가능한 캐싱을 얻습니다. 자세한 내용을 이해하기 위해 Carlos 라는이 아름다운 라이브러리에 대해 읽을 수 있습니다 . 또한 핵심 데이터가 모든 캐싱 문제를 해결하는 데 도움이되고 논리를 적게 작성할 수 있다는 것을 잊지 마십시오. 또한 NSManagedObjectContext서버 요청 모델과 서버 요청 모델 간에 논리가 있는 경우리포지토리 패턴-데이터를 검색하는 로직을 분리하여 모델에서 작동하는 비즈니스 로직에서 엔티티 모델로 맵핑합니다. 따라서 핵심 데이터 기반 아키텍처가있는 경우에도 리포지토리 패턴을 사용하는 것이 좋습니다. 저장소 캔 추상적 인 것 같은 NSFetchRequest, NSEntityDescription, NSPredicate등 같은 일반 메소드에 getput.

서비스 계층에서 이러한 모든 작업을 수행 한 후 호출자 (뷰 컨트롤러)는 ReactiveCocoa프리미티브를 사용하여 신호 조작, 체인, 매핑 등의 응답으로 복잡한 비동기 작업을 수행 하거나 구독하여 뷰에 결과를 표시 할 수 있습니다. . 나는 함께 주입 의존성 삽입 (Dependency Injection) 모든 서비스 클래스 내에서 APIClient해당에 특정 서비스 호출 변환합니다, GET, POST, PUT, DELETE, 등 REST 엔드 포인트에 요청합니다. 이 경우 APIClient모든 컨트롤러에 암시 적으로 전달되며, APIClient서비스 클래스를 매개 변수화하여이를 명시 적으로 만들 수 있습니다. 다른 사용자 정의를 사용하려는 경우 이는 의미가 있습니다.APIClient특정 서비스 클래스의 경우, 어떤 이유로 든 추가 사본을 원하지 않거나 항상 하나의 특정 인스턴스를 사용하여 (사용자 정의없이) 사용할 것입니다 APIClient-단일 톤으로 만드십시오. 그러나 DON하지 마십시오. ‘서비스 클래스를 싱글 톤으로 만들지 마십시오.

그런 다음 DI가있는 각 뷰 컨트롤러는 필요한 서비스 클래스를 주입하고 적절한 서비스 메소드를 호출하고 결과를 UI 로직으로 구성합니다. 의존성 주입을 위해 BloodMagic 또는 더 강력한 프레임 워크 Typhoon 을 사용하고 싶습니다 . 나는 싱글 톤, 신 APIManagerWhatever수업 또는 다른 잘못된 것을 절대 사용하지 않습니다 . 당신 WhateverManager이 당신의 클래스를 호출하는 경우 , 이것은 당신이 그것의 목적을 알지 못하고 나쁜 디자인 선택 이라는 것을 나타냅니다 . 싱글 톤은 또한 안티 패턴이며, 대부분의 경우 (드문 경우는 제외) 잘못된 솔루션입니다. 다음 기준 중 세 가지가 모두 충족되는 경우에만 싱글 톤을 고려해야합니다.

  1. 단일 인스턴스의 소유권은 합리적으로 할당 될 수 없습니다.
  2. 게으른 초기화가 바람직합니다.
  3. 달리 글로벌 액세스는 제공되지 않습니다.

우리의 경우 단일 인스턴스의 소유권은 문제가 아니며 신 관리자를 서비스로 나눈 후 글로벌 액세스가 필요하지 않습니다. 이제 하나 또는 여러 개의 전용 컨트롤러 만 특정 서비스 (예 : UserProfile컨트롤러 요구 UserServices등)를 필요 로하기 때문에 .

우리는 항상 SOLID의S 원칙을 존중 하고 우려를 분리 해야하므로 모든 서비스 방법과 네트워크 호출을 한 클래스에 두지 마십시오. 특히 대기업 응용 프로그램을 개발하는 경우에는 미치므로 말입니다. 따라서 의존성 주입 및 서비스 접근 방식을 고려해야합니다. 나는이 접근법을 현대적이고 사후 OO 로 간주합니다 . 이 경우 애플리케이션을 제어 로직 (컨트롤러 및 이벤트)과 파라미터의 두 부분으로 나눕니다.

한 종류의 매개 변수는 일반적인 “데이터”매개 변수입니다. 그것이 우리가 함수를 전달하고, 조작하고, 수정하고, 지속하는 것입니다. 이것들은 엔티티, 집합체, 컬렉션, 케이스 클래스입니다. 다른 종류는 “서비스”매개 변수입니다. 비즈니스 로직을 캡슐화하고 외부 시스템과 통신하고 데이터 액세스를 제공하는 클래스입니다.

다음은 내 아키텍처의 일반적인 워크 플로 예입니다. FriendsViewController사용자의 친구 목록을 표시하고 친구를 제거 할 수있는 옵션 이 있다고 가정 해 보겠습니다 . FriendsServices클래스 에서 다음과 같은 메소드를 작성합니다 .

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

Friend모델 / 도메인 객체는 어디에 있습니까 (또는 User비슷한 속성을 가진 객체 일 수도 있습니다). 이 방법 파싱 언더 후드 FriendNSDictionaryJSON 매개 변수 friend_id, name, surname, friend_request_id등. 필자는 항상 이러한 종류의 상용구 및 모델 레이어 (맨 앞뒤로 구문 분석, JSON에서 중첩 된 객체 계층 관리 등)에 Mantle 라이브러리를 사용 합니다. 구문 분석 후에 호출 APIClient DELETE실제 REST 요청에 반환하게하는 방법 Response에서 RACSignal(호출자에게 FriendsViewController사용자 또는 어떤 적절한 메시지를 표시하는 우리의 경우)를.

응용 프로그램이 매우 큰 경우 논리를 더 명확하게 분리해야합니다. 예를 들어 로직과 로직 을 혼합 하거나 모델링 하는 것이 항상 좋은 것은 아닙니다 . 내 접근 방식을 설명했을 때 나는 그 방법이 계층에 있어야한다고 말 했지만, 만약 우리가 더 pedantic이라면 우리는 그 방법이 더 낫다는 것을 알 수 있습니다 . 리포지토리가 무엇인지 기억합시다. 에릭 에반스 (Eric Evans)는 그의 책 [DDD]에서 그 내용을 정확하게 설명했다.RepositoryServiceremoveFriendServiceRepository

리포지토리는 특정 유형의 모든 개체를 개념 집합으로 나타냅니다. 보다 정교한 쿼리 기능을 제외하고는 컬렉션처럼 작동합니다.

따라서 a Repository는 기본적으로 컬렉션 스타일 의미론 (추가, 업데이트, 제거)을 사용하여 데이터 / 객체에 대한 액세스를 제공하는 정면입니다. 그렇기 때문에 컬렉션과 같은 의미가 매우 명확하기 때문에 다음과 같은 것을 가질 때 getFriendsList, getUserGroupsremoveFriend배치 할 수 Repository있습니다. 그리고 같은 코드 :

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

기본 CRUD오퍼레이션을 넘어서 두 개의 도메인 오브젝트 ( FriendRequest)를 연결 하기 때문에 비즈니스 로직 Service입니다. 이것이 레이어에 배치되어야하는 이유 입니다. 또한 불필요한 추상화를 만들지 마십시오 . 이 모든 접근법을 현명하게 사용하십시오. 응용 프로그램을 추상화로 압도하면 실수로 복잡성 이 증가 하고 소프트웨어 시스템에서 복잡성으로 인해 다른 것보다 많은 문제
발생하기 때문에

“오래된”Objective-C 예제를 설명했지만이 방법은 Swift 언어에 훨씬 더 쉽게 개선 될 수 있습니다. 더 유용한 기능과 기능성 설탕이 포함되어 있기 때문입니다. 이 라이브러리를 사용하는 것이 좋습니다 : Moya . 그것은 당신이 더 우아한 APIClient층 (당신이 기억하는 것처럼 우리의 주력) 을 만들 수있게합니다 . 이제 우리의 APIClient공급자는 프로토콜을 준수하고 파괴적인 패턴 일치를 활용하는 확장 기능을 갖춘 값 유형 (enum)이 될 것입니다. Swift enums + pattern matching은 고전적인 함수형 프로그래밍에서와 같이 대수적 데이터 유형 을 만들 수있게합니다 . 우리의 마이크로 서비스는 APIClient일반적인 Objective-C 접근 방식 에서처럼 이 개선 된 공급자를 사용할 것 입니다. 대신 모델 레이어의 Mantle경우 ObjectMapper 라이브러리를 사용할 수 있습니다또는 더 우아하고 기능적인 Argo 라이브러리 를 사용하고 싶습니다 .

그래서 저는 모든 응용 분야에 적용 할 수있는 일반적인 아키텍처 접근 방식을 설명했습니다. 물론 더 많은 개선이있을 수 있습니다. 함수형 프로그래밍을 배우는 것이 좋습니다. 많은 이점을 얻을 수는 있지만 너무 멀리 가지 마십시오. 과도하게 공유되는 전역 변경 가능 상태를 제거하고, 변경 불가능한 도메인 모델을 작성하거나 외부 부작용없이 순수한 기능을 작성하는 것이 일반적으로 좋은 습관이며 새로운 Swift언어로이를 권장합니다. 그러나 다른 개발자가 코드를 읽고 지원할 수 있기 때문에 코드가 무겁고 순수한 기능 패턴으로 코드에 과부하가 걸리면 카테고리 이론적 접근 방식은 나쁜 생각 입니다.prismatic profunctors불변 모델에 이런 것들이 있습니다. 와 같은 것 ReactiveCocoa: RACify코드를 너무 많이 사용 하지 마십시오 . 특히 초보자에게는 정말 빨리 읽을 수 없기 때문입니다. 목표와 논리를 단순화 할 수있을 때 사용하십시오.

그래서 read a lot, mix, experiment, and try to pick up the best from different architectural approaches. 내가 줄 수있는 최선의 조언입니다.


답변

이 질문의 목표에 따라 아키텍처 접근 방식을 설명하고 싶습니다.

아키텍처 접근

우리의 일반적인 iOS 애플리케이션 아키텍처는 서비스 레이어 , MVVM , UI 데이터 바인딩 , 의존성 주입 ; 및 기능 반응성 프로그래밍 패러다임.

일반적인 소비자 용 애플리케이션을 다음과 같은 논리적 계층으로 분할 할 수 있습니다.

  • 어셈블리
  • 모델
  • 서비스
  • 저장
  • 관리자
  • 코디네이터
  • UI
  • 하부 구조

어셈블리 계층 은 응용 프로그램의 부트 스트랩 지점입니다. 여기에는 Dependency Injection 컨테이너와 응용 프로그램 객체 및 해당 종속성의 선언이 포함됩니다. 이 계층에는 응용 프로그램 구성 (URL, 타사 서비스 키 등)도 포함될 수 있습니다. 이를 위해 우리는 태풍 라이브러리를 사용 합니다.

모델 계층 에는 도메인 모델 클래스, 유효성 검사, 매핑이 포함됩니다. 모델을 매핑 하기 위해 Mantle 라이브러리를 사용 합니다. 직렬화 / 직렬화를 JSON형식 및 NSManagedObject모델 로 지원합니다 . 모델의 검증 및 양식 표현을 위해 FXFormsFXModelValidation 라이브러리를 사용합니다.

서비스 계층 은 도메인 모델에 표시된 데이터를 보내거나 받기 위해 외부 시스템과 상호 작용하는 데 사용하는 서비스를 선언합니다. 따라서 일반적으로 서버 API (엔터티 당), 메시징 서비스 ( PubNub 등 ), 스토리지 서비스 (Amazon S3 등) 와 통신하는 서비스가 있습니다 . 기본적으로 서비스는 SDK에서 제공하는 객체 (예 : PubNub SDK)를 랩핑하거나 자체 통신을 구현합니다. 논리. 일반적인 네트워킹에는 AFNetworking 라이브러리를 사용 합니다.

저장소 계층 의 목적은 장치에서 로컬 데이터 저장소를 구성하는 것입니다. 이를 위해 Core Data 또는 Realm 을 사용합니다 (장점과 단점이 있으며 사용 방법 결정은 구체적인 사양을 기반으로 함). 핵심 데이터 설정을 위해 MDMCoreData 라이브러리 및 모든 클래스의 로컬 스토리지에 대한 액세스를 제공하는 스토리지 (서비스와 유사) 클래스를 사용합니다. Realm의 경우 유사한 스토리지를 사용하여 로컬 스토리지에 액세스 할 수 있습니다.

관리자 레이어 는 추상화 / 래퍼가있는 곳입니다.

관리자 역할은 다음과 같습니다.

  • 다른 구현 (keychain, NSDefaults 등)을 가진 자격 증명 관리자
  • 현재 사용자 세션을 유지하고 제공하는 방법을 알고있는 현재 세션 관리자
  • 미디어 장치 (비디오 녹화, 오디오, 사진 촬영)에 대한 액세스를 제공하는 캡처 파이프 라인
  • 블루투스 서비스 및 주변 장치에 대한 액세스를 제공하는 BLE 관리자
  • 지리적 위치 관리자

따라서 관리자의 역할에는 응용 프로그램 작업에 필요한 특정 측면의 논리 또는 관심사를 구현하는 모든 개체가 될 수 있습니다.

우리는 싱글 톤을 피하려고 노력하지만이 레이어는 필요한 경우 그들이 사는 곳입니다.

코디네이터 계층 은 로직을 특정 모듈 (기능, 화면, 사용자 스토리 또는 사용자 경험)에 필요한 하나의 작업 시퀀스로 결합하기 위해 다른 계층 (서비스, 스토리지, 모델)의 오브젝트에 의존하는 오브젝트를 제공합니다. 일반적으로 비동기 작업을 연결하고 성공 및 실패 사례에 대응하는 방법을 알고 있습니다. 예를 들어 메시징 기능과 해당 MessagingCoordinator객체를 상상할 수 있습니다 . 메시지 전송 작업 처리는 다음과 같습니다.

  1. 메시지 검증 (모델 계층)
  2. 로컬로 메시지 저장 (메시지 저장)
  3. 메시지 첨부 파일 업로드 (amazon s3 서비스)
  4. 메시지 상태 및 첨부 파일 URL 업데이트 및 로컬로 메시지 저장 (메시지 저장)
  5. 메시지를 JSON 형식 (모델 계층)으로 직렬화
  6. PubNub에 메시지 게시 (PubNub 서비스)
  7. 메시지 상태 및 속성을 업데이트하고 로컬로 저장 (메시지 저장)

위의 각 단계에서 해당 오류가 처리됩니다.

UI 계층 은 다음과 같은 하위 계층으로 구성됩니다.

  1. 뷰 모델
  2. ViewControllers
  3. 견해

Massive View Controller를 피하기 위해 MVVM 패턴을 사용하고 ViewModel에서 UI 프리젠 테이션에 필요한 로직을 구현합니다. ViewModel에는 일반적으로 코디네이터와 관리자가 종속성으로 있습니다. ViewController 및 일부 뷰 (예 : 테이블 뷰 셀)에서 사용하는 ViewModel. ViewController와 ViewModel의 결합은 데이터 바인딩 및 명령 패턴입니다. 접착제를 사용하려면 ReactiveCocoa 라이브러리를 사용하십시오 .

또한 ReactiveCocoa와 그 RACSignal개념을 모든 코디네이터, 서비스, 스토리지 방법의 인터페이스 및 반환 값 유형으로 사용합니다. 이를 통해 작업을 체인화하고 병렬 또는 직렬로 실행할 수 있으며 ReactiveCocoa에서 제공하는 기타 여러 유용한 기능을 사용할 수 있습니다.

우리는 선언적인 방식으로 UI 동작을 구현하려고합니다. 데이터 바인딩 및 자동 레이아웃은이 목표를 달성하는 데 많은 도움이됩니다.

인프라 계층 에는 응용 프로그램 작업에 필요한 모든 도우미, 확장, 유틸리티가 포함됩니다.


이 방법은 우리와 우리가 일반적으로 구축하는 앱 유형에 적합합니다. 그러나 이것은 구체적인 팀의 목적에 맞게 조정 / 변경 되어야 하는 주관적인 접근 방법이라는 것을 이해해야 합니다.

희망이 당신을 도울 것입니다!

또한이 블로그 게시물 iOS Development as a Service 에서 iOS 개발 프로세스에 대한 자세한 정보를 찾을 수 있습니다.


답변

모든 iOS 앱이 다르기 때문에 여기에 고려해야 할 다른 접근 방식이 있다고 생각하지만 일반적으로 다음과 같이 진행합니다.

모든 API 요청 (일반적으로 APICommunicator)을 처리 할 중앙 관리자 (싱글 톤) 클래스를 작성하고 모든 인스턴스 메소드는 API 호출입니다. . 그리고 하나의 중앙 (비공개) 방법이 있습니다.

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

기록을 위해 2 개의 주요 라이브러리 / 프레임 워크 인 ReactiveCocoa 및 AFNetworking을 사용합니다. ReactiveCocoa는 비동기 네트워킹 응답을 완벽하게 처리합니다 (sendNext :, sendError : 등).

이 메서드는 API를 호출하고 결과를 가져 와서 RAC를 통해 ‘원시’형식 (AFNetworking이 반환하는 NSArray 등)으로 전송합니다.

그런 다음 getStuffList:위의 메소드를 호출 한 메소드는 신호를 구독하고 원시 데이터를 Motis와 같은 객체로 구문 분석하고 객체를 하나씩 발신자에게 보냅니다 ( getStuffList:유사한 메소드는 컨트롤러가 가입 할 수있는 신호를 반환합니다) ).

가입 한 컨트롤러는 subscribeNext:의 블록으로 객체를 받아서 처리합니다.

나는 다른 응용 프로그램에서 여러 가지 방법을 시도했지만이 응용 프로그램이 가장 잘 작동 했으므로 최근에 몇 가지 응용 프로그램 에서이 응용 프로그램을 사용하고 있습니다. 작은 프로젝트와 큰 프로젝트에 모두 적합하며 무언가를 수정 해야하는 경우 쉽게 확장하고 유지할 수 있습니다.

이것이 도움이되기를 바랍니다. 나는 내 접근 방식에 대한 다른 사람들의 의견을 듣고 싶습니다.


답변

제 상황에서는 대개 ResKit 라이브러리를 사용 하여 네트워크 계층을 설정하고 있습니다. 사용하기 쉬운 구문 분석 기능을 제공합니다. 다른 응답과 물건에 대한 매핑을 설정하는 노력을 줄입니다.

매핑을 자동으로 설정하는 코드 만 추가합니다. 내 모델의 기본 클래스를 정의합니다 (일부 메소드가 구현되었는지 여부를 확인하는 코드가 많고 모델 자체의 코드가 적기 때문에 프로토콜이 아님).

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

관계는 응답으로 중첩 된 객체를 나타내는 객체입니다.

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

그런 다음 RestKit에 대한 매핑을 다음과 같이 설정합니다.

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

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

이제 요청 래핑에 대해 :

모든 APIRequest 클래스에서 줄 길이를 줄이기 위해 블록 정의가있는 헤더 파일이 있습니다.

APICallbacks.h

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

그리고 내가 사용하는 APIRequest 클래스의 예 :

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

그리고 코드에서해야 할 일은 API 객체를 초기화하고 필요할 때마다 호출하면됩니다.

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

내 코드는 완벽하지는 않지만 한 번 설정하고 다른 프로젝트에 사용하기 쉽습니다. 누구에게나 흥미 롭다면 mbHub와 CocoaPods에서 시간을 보내고 보편적 인 솔루션을 만들 수 있습니다.


답변

제 생각에는 모든 소프트웨어 아키텍처가 필요에 의해 주도됩니다. 이것이 학습 또는 개인적인 목적을위한 것이면, 주요 목표를 결정하고 아키텍처를 주도하게하십시오. 이것이 고용을위한 작업이라면 비즈니스 요구가 가장 중요합니다. 비결은 반짝이는 것들로 인해 실제 필요를 방해하지 않도록하는 것입니다. 나는 이것이 어렵다는 것을 안다. 이 비즈니스에는 항상 새롭고 반짝이는 것들이 있으며 그중 많은 것이 유용하지는 않지만 항상 미리 말할 수는 없습니다. 필요에 집중하고 가능한 경우 나쁜 선택을 기꺼이 포기하십시오.

예를 들어, 최근에 지역 비즈니스를위한 사진 공유 앱의 빠른 프로토 타입을 만들었습니다. 비즈니스에는 빠르고 더러운 일이 필요했기 때문에 아키텍처는 결국 카메라를 팝업하기위한 일부 iOS 코드와 이미지를 S3 저장소에 업로드하고 SimpleDB 도메인에 쓴 Send Button에 연결된 일부 네트워크 코드였습니다. 코드는 사소하고 비용이 최소화되었으며 클라이언트는 REST 호출로 웹을 통해 액세스 할 수있는 확장 가능한 사진 모음을 보유하고 있습니다. 싸고 멍청한 앱에는 결함이 많고 때때로 UI를 잠글 수 있지만 프로토 타입을 위해 더 많은 작업을 수행해야하는 낭비가되어 성능이나 확장 성없이 직원에게 배포하고 수천 개의 테스트 이미지를 쉽게 생성 할 수 있습니다. 우려. Crappy 아키텍처는 필요와 비용에 완벽하게 맞습니다.

다른 프로젝트는 네트워크가 사용 가능할 때 백그라운드에서 회사 시스템과 동기화되는 로컬 보안 데이터베이스를 구현하는 것과 관련이 있습니다. 필요한 모든 것이있는 것처럼 RestKit을 사용하는 백그라운드 동기화 프로그램을 만들었습니다. 그러나 RestKit이 특유의 JSON을 처리하기 위해 너무 많은 사용자 정의 코드를 작성해야했기 때문에 내 JSON을 CoreData 변환으로 작성하여 더 빨리 수행 할 수있었습니다. 그러나 고객은이 앱을 집에 가져오고 싶었고 RestKit은 다른 플랫폼에서 사용한 프레임 워크와 비슷하다고 생각했습니다. 나는 그것이 좋은 결정인지 기다리고 있습니다.

다시 한 번, 문제는 필요에 집중하고 아키텍처를 결정하도록하는 것입니다. 제 3 자 패키지는 앱이 한동안 현장에 있었던 후에 만 ​​나타나는 비용을 가져 오기 때문에 지옥처럼 사용하려고합니다. 나는 거의 돈을 지불하지 않기 때문에 클래스 계층 구조를 피하려고 노력합니다. 완벽하게 맞지 않는 패키지를 채택하는 대신 합리적인 기간 내에 무언가를 쓸 수 있다면 그렇게합니다. 내 코드는 디버깅을 위해 잘 구성되어 있으며 적절하게 주석 처리되어 있지만 타사 패키지는 거의 없습니다. 그렇기 때문에 AF 네트워킹은 무시하고 체계적이며 주석을 달고 유지 관리하기에는 너무 유용하다는 것을 알았습니다. RestKit은 많은 일반적인 경우를 다루지 만, 내가 그것을 사용할 때 싸운 것처럼 느낍니다. 내가 경험하는 대부분의 데이터 소스는 사용자 지정 코드로 가장 잘 처리되는 문제와 문제로 가득합니다. 마지막 몇 가지 앱에서는 내장 JSON 변환기를 사용하고 몇 가지 유틸리티 메소드를 작성합니다.

내가 항상 사용하는 한 가지 패턴은 메인 스레드에서 네트워크 호출을 얻는 것입니다. 마지막으로 수행 한 4-5 개의 앱은 dispatch_source_create를 사용하여 백그라운드 타이머 작업을 설정합니다. 스레드 안전 작업을 수행하고 UI 수정 코드가 기본 스레드로 전송되는지 확인해야합니다. 또한 사용자가 부담 스럽거나 지체되지 않는 방식으로 온 보딩 / 초기화를 수행하는 데 도움이됩니다. 지금까지 이것은 잘 작동했습니다. 나는 이것들을 살펴볼 것을 제안한다.

마지막으로, 우리가 더 많이 일하고 OS가 발전함에 따라 더 나은 솔루션을 개발하는 경향이 있다고 생각합니다. 다른 사람들이 필수라고 주장하는 패턴과 디자인을 따라야한다는 믿음을 극복하는 데 몇 년이 걸렸습니다. 그것이 지역 종교의 일부인 상황에서 일하고 있다면, 부서별 최고의 엔지니어링 관행을 의미합니다. 그런 다음 편지에 대한 관습을 따르십시오. 그것이 그들이 지불하는 것입니다. 그러나 나는 오래된 디자인과 패턴을 따르는 것이 최적의 해결책이라는 것을 거의 알지 못합니다. 저는 항상 비즈니스 요구 사항의 프리즘을 통해 솔루션을보고 그에 맞는 아키텍처를 구축하고 가능한 한 단순하게 유지하려고합니다. 충분하지 않다고 생각하지만 모든 것이 올바르게 작동하면 올바른 길을 가고 있습니다.


답변

https://github.com/Constantine-Fry/Foursquare-API-v2 에서 얻은 접근 방식을 사용합니다 . Swift에서 해당 라이브러리를 다시 작성했으며 다음 코드 부분에서 아키텍처 접근 방식을 볼 수 있습니다.

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

기본적으로 NSURLRequest를 만들고 JSON 응답을 구문 분석하고 결과와 함께 콜백 블록을 대기열에 추가하는 NSOperation 하위 클래스가 있습니다. 기본 API 클래스는 NSURLRequest를 구성하고 NSOperation 서브 클래스를 초기화하고이를 큐에 추가합니다.


답변

상황에 따라 몇 가지 접근 방식을 사용합니다. 대부분의 경우 AFNetworking은 헤더를 설정하고, 여러 부분으로 된 데이터를 업로드하고, GET, POST, PUT & DELETE를 사용할 수 있으며 UIKit에 대한 추가 범주가있어 이미지를 설정할 수있는 가장 단순하고 강력한 접근 방식입니다. URL. 많은 호출이있는 복잡한 앱에서는 때때로 다음과 같은 편리한 방법으로이를 추상화합니다.

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

AFNetworking이 이미 다른 코드 기반에있을 수 있으므로 프레임 워크 나 다른 라이브러리 구성 요소를 만드는 등 AFNetworking이 적합하지 않은 상황이 몇 가지 있습니다. 이 상황에서 단일 호출을하거나 요청 / 응답 클래스로 추상화 된 경우 NSMutableURLRequest를 인라인으로 사용합니다.