일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 코드리뷰
- 안드로이드 스튜디오
- 안드로이드
- 제약 레이아웃
- 자바
- 상대 레이아웃
- Swift
- edwith
- 코틀린
- 부스트코스
- 스낵바
- 드로어블
- 프로젝트
- 프로그래밍
- 테이블_레이아웃
- 수명주기
- 텍스트뷰 자동 스크롤
- 리니어 레이아웃
- IOS
- 데이터베이스
- 뷰
- 서비스
- textview auto scroll
- 안드로이드_프로그래밍
- SceneDelegate
- 텍스트뷰 스크롤
- 액션바
- 레이아웃
- 아이폰
- RecyclerView
- Today
- Total
듀다의 성장개발로그
iOS) Swift에서의 비동기 작업, 그리고 RxSwift(2) 본문
지난 시간에 이어 비동기 작업에 대해 설명하겠습니다.
결과를 기다렸다가 나중에 결과 데이터가 오면 처리하는 상황을 예로 들었죠?
식당에 대한 정보를 JSON으로 받아서 처리하는 예제를 직접 구현하면서 알아보겠습니다.
이 포스팅에 실린 예제는 하단 링크의 강의 내용을 바탕으로 작성되었습니다.
우선 RxSwift를 사용하기 위해선 해당 라이브러리를 설치해주셔야 합니다.
Podfile에 아래와 같은 코드를 추가하고 터미널에서 pod install을 입력하여 설치합니다.
pod 'RxSwift'
pod 'RxCocoa'
영상에서는 Coordinator를 결합한 MVVM-C 패턴을 사용했지만 여기서는 Coordinator 없이 구현하고
Coordinator에 대해서는 다른 포스팅에서 설명드리도록 하겠습니다.
스토리보드에서 위와 같이 내비게이션 컨트롤러와 테이블뷰, 테이블뷰 셀이 담긴 뷰 컨트롤러를 만들어 줍니다. 테이블뷰 셀의 identifier는 cell로 설정했습니다.
[
{
"name": "Blue Legume",
"cuisine": "european"
},
{
"name": "Fig & Olive",
"cuisine": "european"
},
{
"name": "Le Mercury",
"cuisine": "french"
},
{
"name": "Dishoom",
"cuisine": "indian"
},
{
"name": "The Grand Brasserie",
"cuisine": "french"
},
{
"name": "Wahaca",
"cuisine": "mexican"
},
{
"name": "The Islington",
"cuisine": "english"
}
]
그리고 데이터를 보내줄 JSON 파일인 restaurants.json을 생성합니다. 파일의 내용은 위와 같이 구성합니다.
import Foundation
struct Restaurant: Decodable {
let name: String
let cuisine: Cuisine
}
enum Cuisine: String, Decodable {
case european
case indian
case mexican
case french
case english
}
식당 정보를 담을 Restaurant 객체는 위와 같이 구성했습니다.
guard let path = Bundle.main.path(forResource: "restaurants", ofType: "json") else {
return Disposables.create { }
}
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let restaurant = try JSONDecoder().decode([Restaurant].self, from: data)
observer.onNext(restaurant)
} catch(let error) {
observer.onError(error)
}
이제 식당 정보를 JSON으로부터 Observable에 담아오는 코드를 구현할 차례입니다.
RestaurantService.swift라는 파일을 만들어서 진행하겠습니다.
path 메소드로 restaurants.json파일을 경로로 설정한 뒤 do - try - catch 안에서 JSONDecoder로 json의 정보를 Restaurant 객체의 배열로 받습니다. 그리고 이를 Observer에 추가합니다.
import Foundation
import RxSwift
protocol RestaurantServiceProtocol {
func fetchRestaurants() -> Observable<[Restaurant]>
}
class RestaurantService: RestaurantServiceProtocol {
func fetchRestaurants() -> Observable<[Restaurant]> {
return Observable.create { observer -> Disposable in
guard let path = Bundle.main.path(forResource: "restaurants", ofType: "json") else {
return Disposables.create { }
}
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let restaurant = try JSONDecoder().decode([Restaurant].self, from: data)
observer.onNext(restaurant)
} catch(let error) {
observer.onError(error)
}
return Disposables.create { }
}
}
}
RestaurantService.swift 전체 코드
그리고 Restaurant의 정보를 다룰 View Model을 생성합니다. RestaurantViewModel.swift 파일을 만들고 식당의 정보를 표시할 변수와 초기화 메소드를 넣습니다.
이 파일이 없어도 실행에 전혀 문제가 없어 보이지만, 굳이 Restaurant를 구조체로 받아서 내용을 재구성한 뒤 새로운 변수에 넣어주는데요,
식당의 정보를 표시할 변수를 굳이 넣어 주는 이유는, 만약 UI보다 API가 더 늦게 개발되었을 때에도 표시하는 데에 문제가 없도록 UI에 맞게 정보를 재구성하는 연습이라고 보시면 되겠습니다.
import Foundation
struct RestaurantViewModel {
private let restaurant: Restaurant
var displayText: String {
return restaurant.name + " - " + restaurant.cuisine.rawValue.capitalized
}
init(restaurant: Restaurant) {
self.restaurant = restaurant
}
}
ReataurantViewModel.swift 전체 코드
그리고 List를 위한 View Model도 만들어 줍니다.
주목해야 할 점은 Observable<[Reataurant]>의 내용을 복사하여 Observalbe<[RestaurantViewModel]>로 만들어 준다는 것입니다.
RestaurantViewModel의 사용으로 뷰 컨트롤러에서 접근하여 Restaurant의 내용을 출력할 수 있습니다.
import Foundation
import RxSwift
final class RestaurantsListViewModel {
let title = "Restaurants"
private let restaurantService: RestaurantServiceProtocol
init(restaurantService: RestaurantServiceProtocol = RestaurantService()) {
self.restaurantService = restaurantService
}
func fetchRestaurantViewModels() -> Observable<[RestaurantViewModel]> {
restaurantService.fetchRestaurants().map { $0.map { RestaurantViewModel(restaurant: $0)}}
}
}
RestaurantViewModel.swift 전체 코드
self.viewModel = RestaurantsListViewModel()
navigationItem.title = viewModel.title
navigationController?.navigationBar.prefersLargeTitles = true
그리고 뷰 컨트롤러에서는 내비게이션 컨트롤러의 제목을 설정하고,
viewModel.fetchRestaurantViewModels().observe(on: MainScheduler.instance).bind(to: tableView.rx.items(cellIdentifier: "cell")) {
index, viewModel, cell in
cell.textLabel?.text = viewModel.displayText
}.disposed(by: disposeBag)
뷰 모델로부터 정보를 불러와서 테이블뷰에 표시하는 코드를 작성합니다.
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
let disposeBag = DisposeBag()
private var viewModel: RestaurantsListViewModel!
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel = RestaurantsListViewModel()
tableView.tableFooterView = UIView()
navigationItem.title = viewModel.title
navigationController?.navigationBar.prefersLargeTitles = true
tableView.contentInsetAdjustmentBehavior = .never
viewModel.fetchRestaurantViewModels().observe(on: MainScheduler.instance).bind(to: tableView.rx.items(cellIdentifier: "cell")) {
index, viewModel, cell in
cell.textLabel?.text = viewModel.displayText
}.disposed(by: disposeBag)
}
}
ViewController.swift 전체 코드
이렇게 만든 앱을 실행하면 뷰 컨트롤러에서 작업을 따로 하지 않아도, 위와 같이 식당 정보와 제목이 출력됩니다.
뷰 컨트롤러에서 테이블뷰의 정보에 넣을 정보를 직접 부르고 수정하는 기존의 방식과는 조금 다른,
RxSwift를 활용한 간단한 MVVM 예제였습니다.
다음 포스팅에서는 위의 예제를 조금 수정하여, 버튼 클릭으로 테이블뷰 내용을 수정하는 예제를 함께 보도록 하겠습니다.
참고영상: www.youtube.com/watch?v=PAHqiqb5CxM&list=PLLvVbXNzMjktXQKfgVv3L_iyRWINJ8cKr&index=2