이전에 예제에 사용한 JSON을 가지고 파싱해서 테이블뷰에 뿌려보자.
1. JSON 분석
my.api.mockaroo.com/members_with_avatar.json?key=44ce18f0
위 JSON구조를 보면 다음과 같다.
{"id":1,"name":"Nalani Ayers","avatar":"https://robohash.org/illumasperioreset.png?size=50x50\u0026set=set1","job":"Staff Accountant IV","age":36}
Int타입의 id, String타입의 name, String타입의 avatar, String타입의 job, Int타입의 age다.
2. 데이터 객체 만들기
struct Member: Decodable {
let id: Int
let name: String
let avatar: String
let job: String
let age: Int
}
Int타입의 id, String타입의 name, String타입의 avatar, String타입의 job, Int타입의 age에 맞게 데이터를 받을 객체를 생성한다.
3. 데이터 받아오기
그럼 이전 포스팅에서 신나게 사용했던 JSON사용하던 코드를 가져와 보자.
이전포스팅 : 2020/09/14 - [공부/swift] - [swift] RxSwift 기본적인 사용법
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { emitter in
let url = URL(string:url)!
let task = URLSession.shared.dataTask(with: url) { (data, _, err) in
guard err == nil else {
emitter.onError(err!) // 에러 처리
return
}
if let dat = data, let json = String(data: dat, encoding: .utf8) {
emitter.onNext(json) // data전달
}
emitter.onCompleted() // complete
}
task.resume()
return Disposables.create() { // dispose 실행되었을 때
task.cancel()
}
}
}
이 때는 Json string을 그냥 가지고 오기 위해서 String으로 받았는데 이를 파싱할 것이다.
그리고 리턴되는값은 String이 아니라 Member들의 배열일 것이다.
func loadMembers() -> Observable<[Member]> { // 반환 값은 Member들의 배열
return Observable.create { emitter in
let task = URLSession.shared.dataTask(with: URL(string: MEMBER_LIST_URL)!) { data, _, error in
if let error = error { // 에러 처리
emitter.onError(error)
return
}
guard let data = data, // 데이터가 있으면
let members = try? JSONDecoder().decode([Member].self, from: data) else { // JSon을 멤버배열로 파싱
emitter.onCompleted() // 못 가져오면 완료처리
return
}
emitter.onNext(members) // 데이터는 member배열을 전달
emitter.onCompleted() // 완료
}
task.resume() // URLSession 시작
return Disposables.create { // 만약 dispose가 호출된다면
task.cancel() // 동작 취소
}
}
}
그리고 이걸 구독해주자.
loadMembers()
.observeOn(MainScheduler.instance) // 메인스레드 동작
.subscribe(onNext: { [weak self] members in // 순환참조 방지
self?.data = members // members를 받으면 data변수에 저장
self?.tableView.reloadData() // 테이블 리로드
})
.disposed(by: disposeBag)
이렇게 하면 테이블뷰에 뿌려줄 준비가 완료되었다.
4. 테이블뷰 구성
extension으로 table view data source와 delegate를 처리해주자.
// MARK: - Table view data source
extension MembersTableViewController {
// row수 설정
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count // row수는 데이터의 수만큼
}
// 셀 설정
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MemberItemCell") as! MemberItemCell // 커스텀 셀
let item = data[indexPath.row] // 해당 인덱스의 데이터를 가져와서
cell.setData(item) // 셀의 setData함수 호출
return cell // 셀 반환
}
// 셀을 클릭했을때
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = data[indexPath.row] // 해당 인덱스의 데이터를 가져와서
performSegue(withIdentifier: "DetailViewController", sender: item) // 상세뷰에 넘겨줌
}
}
5. 셀 구성
아까 테이블뷰쪽에서 Member를 받아 커스텀 셀에 셋팅해주면 된다.
// MARK: - TableView Cell
class MemberItemCell: UITableViewCell {
@IBOutlet var avatar: UIImageView! // 셀의 이미지 뷰
@IBOutlet var name: UILabel! // 이름
@IBOutlet var job: UILabel! // 직업
@IBOutlet var age: UILabel! // 나이
var disposeBag = DisposeBag() // disposebag
func setData(_ data: Member) { // 데이터를 전달 받았을 때
loadImage(from: data.avatar) // 이미지 로딩
.observeOn(MainScheduler.instance) // 메인스레드 동작
.bind(to: avatar.rx.image) // image뷰에 바인드
.disposed(by: disposeBag)
avatar.image = nil // 기본값은 nil
name.text = data.name // 이름
job.text = data.job // 직업
age.text = "(\(data.age))" // 나이
}
private func loadImage(from url: String) -> Observable<UIImage?> {
return Observable.create { emitter in
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, error in
if let error = error {
emitter.onError(error)
return
}
guard let data = data,
let image = UIImage(data: data) else {
emitter.onNext(nil)
emitter.onCompleted()
return
}
emitter.onNext(image)
emitter.onCompleted()
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
override func prepareForReuse() { // 재 사용될때
super.prepareForReuse()
disposeBag = DisposeBag() // dispose
}
}
6. 상세 뷰 작성
셀 클릭했을때 전달되는 DetailView도 데이터를 넘겨받아서 셋팅해보자.
class MembersDetailViewController: UIViewController {
var data: Member!
@IBOutlet var avatar: UIImageView!
@IBOutlet var id: UILabel!
@IBOutlet var name: UILabel!
@IBOutlet var job: UILabel!
@IBOutlet var age: UILabel!
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setData(data)
}
func setData(_ data: Member) {
loadImage(from: data.avatar)
.observeOn(MainScheduler.instance)
.bind(to: avatar.rx.image)
.disposed(by: disposeBag)
id.text = "#\(data.id)"
avatar.image = nil
name.text = data.name
job.text = data.job
age.text = "(\(data.age))"
}
private func makeBig(_ url: String) -> Observable<String> {
return Observable.just(url)
.map { $0.replacingOccurrences(of: "size=50x50&", with: "") }
}
private func loadImage(from url: String) -> Observable<UIImage?> {
return Observable.create { emitter in
let task = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, error in
if let error = error {
emitter.onError(error)
return
}
guard let data = data,
let image = UIImage(data: data) else {
emitter.onNext(nil)
emitter.onCompleted()
return
}
emitter.onNext(image)
emitter.onCompleted()
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
}
셀과 거의 비슷한 작업이다.
7. 이미지 사이즈 빼기
그런데 돌려보면 이미지가 깨진다.
json을 다시 살펴보자.
{"id":1,"name":"Nalani Ayers","avatar":"https://robohash.org/illumasperioreset.png?size=50x50\u0026set=set1","job":"Staff Accountant IV","age":36}
위에 보면 size=50X50이라고 써져있다.
이미지를 50X50으로 받아왔는데 더 큰데 받아오기 때문에 깨지는것이다.
그래서 이 부분을 제거하는 함수가 아래와 같다.
private func makeBig(_ url: String) -> Observable<String> {
return Observable.just(url)
.map { $0.replacingOccurrences(of: "size=50x50&", with: "") }
}
그럼 이 makeBig을 먼저 구독한 뒤 loadImage를 호출해야 한다.
makeBig(data.avatar).bind { (str) in // makeBig결과 바인딩
self.loadImage(from: str) // 로드이미지 호출
.observeOn(MainScheduler.instance) // 메인스레드
.bind(to: self.avatar.rx.image) // 이미지뷰에 바인딩
.disposed(by: self.disposeBag)
}.disposed(by: disposeBag)
// 딴곳에서 처리하고 싶으면 아래와 같이 해도 된다.
let url : BehaviorSubject<String> = BehaviorSubject(value: "") // Subject 선언
makeBig(data.avatar).bind(to: url).disposed(by: disposeBag) // url에 바인딩
url.bind { (str) in // bind onNext
self.loadImage(from: str) // 로드 이미지 호출
.observeOn(MainScheduler.instance) // 메인스레드
.bind(to: self.avatar.rx.image) // 이미지뷰에 바인딩
.disposed(by: self.disposeBag)
}.disposed(by: disposeBag)
위와 같이 코드를 바꾸면 큰 이미지가 로드된다.
근데 대괄호가 있어서 보기 좋진 않다.
저렇게 한 이유가 Observable<Observable<결과타입>>이런식으로 결과값이 나와서 그런데 이런경우엔 flatMap을 사용해주자.
makeBig(data.avatar)
.flatMap(loadImage)
.observeOn(MainScheduler.instance)
.bind(to: avatar.rx.image)
.disposed(by: disposeBag)
그럼 코드가 훨씬 간결해진다.
'공부 > swift' 카테고리의 다른 글
[swift] RxSwift Operator(SugarAPI) (0) | 2020.09.15 |
---|---|
[swift] RxSwift 기본적인 사용법 (0) | 2020.09.14 |
[swift] RxCocoa (0) | 2020.09.09 |
[swift] RxSwift (0) | 2020.09.06 |