본문 바로가기

공부/swift

[RxSwift] JSON파싱해서 테이블뷰에 뿌리기

이전에 예제에 사용한 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