ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Alamofire를 이용한 api service 설계
    iOS 2018. 8. 12. 23:33

    개인 프로젝트를 하는 동안, 기본적인 UI 설계가 끝나고 인터넷에서 json 값을 받아들이는 부분을 구현하기 시작할 때가 왔습니다.

    이 때 고려해야 했던 사항들이 있었습니다.


    1. 기획이 추후에 바뀔 수 있음을 고려해서, 클라단에서 할 수 있는 것만 설계합니다. 이것은 json의 형식과 모델도 바뀔 수 있다는 소리입니다.

    파싱 방식은 미뤄두어야 했습니다.

    2. 상당히 많은 api가 다양한 형태의 모델로 올 수 있고, 그에 따른 url 스트링도 다종다양할 것입니다. 해당 부분에서 일어나는 중복 부분을 가능한 최소화하며 코드의 가독성을 줄입니다.

    3. 백엔드 부분을 어떻게 할 지 정해져 있지 않으므로, 일단 백엔드에서 json 값을 받아오는 것을 가정한 코드를 세울 것입니다.


    위 조건을 가능한 만족하는 api 서비스를 생각해보았습니다.


    import UIKit

    import Alamofire

    import SwiftyJSON


    /**

     인터넷 익스플로링

     

     **/

    enum Method : String {

        case recentPhotos = "photos"

        case recentVideo = "video"

    }

    enum ContentResult<T> {

        case success(T)

        case failure(Error)

    }


    class ApiService : NSObject {

        

        static let shared = ApiService()

        let baseUrl = "http://naver.com" //base URL

        func baseFetch(method :  Method,  completion : @escaping (Result<Any>)->() ){

            Alamofire.request("\(baseUrl)/\(method.rawValue)").responseJSON { (responseData) in

                completion(responseData.result)

            }

        }

        

        func fetchPhotoContents( completion : @escaping (ContentResult<[PhotoContent]>)-> ()) {

            self.baseFetch(method: Method.recentPhotos) { (result) in

                switch (result){

                case let .failure(error):

                    completion(ContentResult<[PhotoContent]>.failure(error))

                    break

                case let .success(value):

                    //TODO

                    break

                }

            }

        }

        

        func fetchVideoContents( completion : @escaping (ContentResult<[VideoContent]>)-> ()) {

            var videoContent = [VideoContent]()

            let v = VideoContent()

            v.titleText = "젤다의 전설 브레스 오브 더 와일드 [1화] 한글로 즐기는 젤다의 첫 발걸음!"

            v.thumbnailImageName = "dora"

            v.username = "도라"

            videoContent.append(v)

            let vd = VideoContent()

            vd.titleText = "젤다의 전설 브레스 오브 더 와일드 [1화] 한글로 즐기는 젤다의 첫 발걸음!"

            vd.thumbnailImageName = "dora"

            vd.username = "도라"

            videoContent.append(vd)

            

            completion(ContentResult<[VideoContent]>.success(videoContent))

        }

    }



    class ApiService 는 싱글톤으로 구현했습니다.
    회사에서 구했던 alamofire service 는 유저디폴트와 일반 클래스를 사용해서, 필요할 때마다 뷰컨트롤러의 멤버로 할당해서 사용했습니다만,
    .. 은근히 멤버를 필요할 때마다 정의하는 게 귀찮더라고요. 싱글턴은 멤버를 할당할 걱정은 없으니까요.


    url 값은 

    baseURl + 나머지 형식으로 보통 나타나는데, let으로 모든 엔드포인트를 정의하면 별로 안 이뻐 보입니다. 클래스로 빼면 static으로 빼야 하는 문제도 있고,

    static let recentPhotos = "http://naver.com/photos" 이런식으로 하면 static let 이라는 게 너무 길었어요.

     하이라이팅도 헷갈리고.

    이번엔 String enum 으로 정의하되, rawvalue 로 baseURL 과 합쳐서 해보기로 했습니다.


    에러 핸들링은

    enum ContentResult<T> {

        case success(T)

        case failure(Error)

    }

    로 했습니다. 소스코드를 뜯어보면 알겠지만 alamofire 에서 result를 정의한 enum 과 동일한 방식입니다.

    성공하면 제너릭 T 를 받아와 UI를 그대로 받아오면 되지만, 실패 또한 처리해주어야 합니다. 

    네트워크가 안 좋다 등등의 표시를 해 주고, Pull to Refresh 등의 UI action 이 오면 재시도를 하거나,

    CoreData 로 저장된 기존 데이터라도 가져 오거나 해서 구현할 방침입니다.


    처음 앱은 에러 핸들링이 미숙해서 비행기 모드에서는 아예 켜지지도 않았는데, 적어도 그렇게 하면 안 되겠죠.


     func baseFetch(method :  Method,  completion : @escaping (Result<Any>)->() ){

            Alamofire.request("\(baseUrl)/\(method.rawValue)").responseJSON { (responseData) in

                completion(responseData.result)

            }

        }


    Alamofire 리퀘스트를 보낼 때 공통적으로 써먹을 만한 부분을 묶어서 썼습니다. json 파싱은 이것을 사용한 각각의 함수로 넘기고요.

    추후 개발에 따라서 헤더에 인증을 쏴주는 부분도 추가해 공통화할 수 있겠습니다.


    func fetchPhotoContents( completion : @escaping (ContentResult<[PhotoContent]>)-> ()) {

            self.baseFetch(method: Method.recentPhotos) { (result) in

                switch (result){

                case let .failure(error):

                    completion(ContentResult<[PhotoContent]>.failure(error))

                    break

                case let .success(value):

                    //TODO

                    break

                }

            }

        }


    샘플로 만들어 본 코드입니다. 공통되는 baseFetch를 불러 네트워킹을 처리하고, 받아 온 value 를 파싱해서 이스케이핑 클로저로 넘겨 주는 부분만 담당하게 해 주었습니다.

    문제는 success 일 때 json 값의 백엔드 구현이 아직 정해진 것이 없다는 것입니다. 이 부분은 추후에 처리할 생각.



        func fetchVideoContents( completion : @escaping (ContentResult<[VideoContent]>)-> ()) {

            var videoContent = [VideoContent]()

            let v = VideoContent()

            v.titleText = "젤다의 전설 브레스 오브 더 와일드 [1화] 한글로 즐기는 젤다의 첫 발걸음!"

            v.thumbnailImageName = "dora"

            v.username = "도라"

            videoContent.append(v)

            let vd = VideoContent()

            vd.titleText = "젤다의 전설 브레스 오브 더 와일드 [1화] 한글로 즐기는 젤다의 첫 발걸음!"

            vd.thumbnailImageName = "dora"

            vd.username = "도라"

            videoContent.append(vd)

            

            completion(ContentResult<[VideoContent]>.success(videoContent))

        }


    백엔드 구현 전에 처리하는 코드입니다. json 파싱도 아직 없고, 네트워크 통신도 없습니다. 아직 있는 건 미완성인 모델 뿐이죠.

    전에 것 전부 건너뛰고 모델을 그냥 새로 생성해서 할당하고, 클로저로 쏴 줍니다.

    나중에 변경해야 하지만, 이런 가상 코드를 api service  안에ㅅ 싹 모아두면 수정할 코드량이 줄 것입니다.


    import UIKit

    import SnapKit



    class VideoCollectionViewCell : BaseCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {

        

        weak var delegate : MovieCellDelegate?

        var contents : [VideoContent]?


        let todaysTrendLabel : UILabel = {

            let label = UILabel()

            label.text = "인기 동영상"

            label.font = UIFont.systemFont(ofSize: 14)

            label.numberOfLines = 1

            

            return label

        }()

        lazy var collectionView : UICollectionView = {

            let layout = UICollectionViewFlowLayout()

            let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)

            cv.backgroundColor = UIColor.white

            cv.dataSource = self

            cv.delegate = self

            return cv

        }()

        let cellId = "cellid"

        /* func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

         print(targetContentOffset.pointee.x )

         if (targetContentOffset.pointee.x == 0 && velocity.x < 0){

         print("hey")

         scrollView.isScrollEnabled = false

         }else {

         scrollView.isScrollEnabled = true

         }

         

         }*/

        /*

         func scrollViewDidScroll(_ scrollView: UIScrollView) {

         

         if scrollView == self.collectionView.scrollView || scrollView == offersCollectionView {

         let offersCollectionViewPosition: CGFloat = offersCollectionView.contentOffset.y

         let scrollViewBottomEdge: CGFloat = scrollView.contentOffset.y + scrollView.frame.height

         if scrollViewBottomEdge >= self.scrollView.contentSize.height {

         self.scrollView.isScrollEnabled = false

         offersCollectionView.isScrollEnabled = true

         } else if offersCollectionViewPosition <= 0.0 && offersCollectionView.isScrollEnabled() {

         self.scrollView.scrollRectToVisible(self.scrollView.frame(), animated: true)

         self.scrollView.isScrollEnabled = true

         offersCollectionView.isScrollEnabled = false

         }

         }

         }*/

        override func setupViews() {

            fetchVideo()

            collectionView.register(VideoCell.self, forCellWithReuseIdentifier: cellId)

            if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {

                flowLayout.scrollDirection = .horizontal

                flowLayout.minimumLineSpacing = 0

            }

            collectionView.bounces = false

            collectionView.showsHorizontalScrollIndicator = false

            addSubview(todaysTrendLabel)

            addSubview(collectionView)

            

            todaysTrendLabel.snp.makeConstraints { (make) in

                make.top.equalTo(self).offset(10)

                make.leading.equalTo(self).offset(10)

                make.height.equalTo(20)

            }

            

            collectionView.snp.makeConstraints { (make) in

                make.top.equalTo(todaysTrendLabel.snp.bottom).offset(10)

                make.left.equalTo(self)

                make.right.equalTo(self)

                make.bottom.equalTo(self)

            }

            

        }

        func fetchVideo(){

            ApiService.shared.fetchVideoContents { (result) in

                switch(result){

                case let .failure(error):

                    break

                case let .success(videoContents):

                    self.contents = videoContents

                    DispatchQueue.main.async {

                        self.collectionView.reloadData()

                    }

                    break

                }

            }

        }

        

        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {

            return contents?.count ?? 0

        }

        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

            self.delegate?.videoContentDidClicked(nil)

        }

        

        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! VideoCell

            cell.content = contents?[indexPath.item]

            return cell

        }

        

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

            return CGSize(width: 150 , height: frame.height - 40)

        }

        

        

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {

            return 0

        }

        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {

            return 0

        }

        

        

    }


    이제 실사용하는 콜렉션뷰에서 api 를 적용합니다.

    apiservice 의 싱글톤을 부르고, 해당 함수를 콜링하면 끝.

    성공일 때에는 메인 큐로 옮겨서 콜렉션뷰를 리로딩해 줍니다.

    실패일 때는 추후에 생각해 보아야 하겠습니다.



Designed by Tistory.