-
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 의 싱글톤을 부르고, 해당 함수를 콜링하면 끝.
성공일 때에는 메인 큐로 옮겨서 콜렉션뷰를 리로딩해 줍니다.
실패일 때는 추후에 생각해 보아야 하겠습니다.
반응형'iOS' 카테고리의 다른 글
주니어 / 미들 / 시니어 레벨 iOS 개발자를 구분하는 기술 일람 (0) 2018.08.19 iOS 앱 간의 통신을 구현하기 (0) 2018.08.18 UICollectionView서 헤더로 다이나믹하게 높이 계산하는 로직 (UITextView) 넣기 (0) 2018.07.25 [swift]WKWebview 스크롤 맨 아래로 정확하게 계산해서 내리기 (0) 2018.07.25 iOS swift 네이버 프로필 api 샘플 코드 (0) 2018.07.10