실제 앱에서 적용 하는 RXSwift Code

cheshire0105·2024년 1월 15일

iOS

목록 보기
5/46
post-thumbnail

실제 앱에서 적용 하는 RXSwift Code

RxSwift에서의 바인딩

  • RxSwift에서 "바인딩"은 주로 Observable 혹은 Observable 시퀀스에서 방출되는 데이터를 UI 컴포넌트에 연결하는 것을 의미합니다. 이 연결을 통해 데이터가 변경될 때 UI도 자동으로 업데이트됩니다.
let observableText = Observable.just("Hello, RxSwift!")
observableText.bind(to: label.rx.text)
  • 이 예제에서 observableText는 "Hello, RxSwift!"라는 문자열을 방출하는 Observable입니다. 이 데이터를 labeltext 프로퍼티에 바인딩하면, label은 자동으로 "Hello, RxSwift!"라는 문자열을 표시하게 됩니다.

데이터 바인딩의 장점

  1. 자동 동기화: 데이터와 UI 컴포넌트 사이의 동기화를 수동으로 관리할 필요가 없습니다. 데이터가 변경될 때마다 UI는 자동으로 업데이트됩니다.
  2. 코드 간결성: 전통적인 이벤트 핸들러나 리스너를 사용할 때보다 코드가 더 간결하고 명확해집니다.
  3. 데이터 흐름의 명확성: 데이터의 흐름과 변화를 쉽게 추적할 수 있어 디버깅이나 코드 이해가 더욱 쉽습니다.

바인딩의 종류

  1. One-Way 바인딩: 하나의 데이터 소스에서 UI 컴포넌트로의 단방향 연결입니다. 데이터 소스의 변화가 UI에 반영되지만, UI의 변화는 데이터 소스에 영향을 주지 않습니다.
  2. Two-Way 바인딩: 데이터 소스와 UI 컴포넌트 간의 양방향 연결입니다. 데이터 소스의 변화가 UI에 반영되고, UI의 변화도 데이터 소스에 반영됩니다.

결론

  • RxSwift의 바인딩은 데이터와 UI 사이의 연결을 간편하게 만들어줍니다. 이를 통해 개발자는 데이터와 UI의 동기화에 대한 걱정 없이 로직에 집중할 수 있게 됩니다. 바인딩은 RxSwift의 핵심적인 특징 중 하나로, 이를 잘 이해하고 활용하면 효율적인 앱 개발이 가능해집니다.

RXSwift 이용하여 TableView를 사용 하는 앱 빌드

  • RXSwift는 여러 프로그래밍 도구와 플랫폼에서 전역으로 사용 가능한 ReactiveX 도구의 일부입니다.
  • RXSwift는 Swift와 반응형으로 상호작용 하기 위한 프레임워크 이며, RXCocoa는 UI 측면에서 Reactive 개념을 활용하여 기존의 Cocoa API를 좀 더 쉽게 사용 할 수 있게 도움을 주는 프레임워크 라고 할 수 있습니다.

구현에 사용할 개념들

Observable

  1. 변경 알림을 보내고

Observer

  1. 그 알림을 구독하고 변경 되면 알림을 받으며

Disposable Bag

  1. 사용한 메모리를 관리 합니다.

RX의 사용 여부

RX를 사용 하지 않는 일반적인 구현법

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    let tableViewItems = ["Item 1", "Item 2", "Item 3", "Item 4"]
    
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
	
        tableView.delegate = self
        tableView.dataSource = self
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tableViewItems.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell")!
        cell.textLabel?.text = tableViewItems[indexPath.row]
        return cell
    }
}

RX를 사용한 코드 간소화 및 Reactive 프로그래밍 채택

  • obervable에서 생성된 배열을 따르는 간단한 TableView UI 구성입니다.
    import UIKit
    import RxSwift
    import RxCocoa
    
    class ViewController: UIViewController {
    
        // 1. 선언된 데이터를 Observable 형태로 감싸서 RxSwift를 사용할 준비를 합니다.
        let tableViewItems = Observable.just(["Item 1", "Item 2", "Item 3", "Item 4"])
        
        // 2. disposeBag은 Observable에 의해 생성된 리소스가 제대로 해제되도록 관리해주는 객체입니다.
        let disposeBag = DisposeBag()
    
        @IBOutlet weak var tableView: UITableView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 3. Observable 데이터를 테이블 뷰에 바인딩합니다.
            // 이것은 Observable 데이터가 변경될 때마다 자동으로 테이블 뷰를 업데이트합니다.
            tableViewItems
                .bind(to: tableView.rx.items(cellIdentifier: "myCell")) { (row, tableViewItem, cell) in
                    // 4. 각 셀의 내용을 설정합니다.
                    cell.textLabel?.text = tableViewItem
                }
                .disposed(by: disposeBag) // 5. 바인딩이 해제될 때 자원을 정리하기 위해 disposeBag에 추가합니다.
        }
    }
  • RxSwift와 RxCocoa의 기능을 활용하여 tableView와 데이터를 바인딩하고, 사용자의 상호작용에 반응하는 방식으로 작성되었습니다.
    import UIKit
    import RxSwift
    import RxCocoa
    
    class ViewController: UIViewController {
    
        // Observable 시퀀스로 음식 목록을 생성
        let tableViewItems = Observable.just([
            Food(name: "Hamburger", image: "hamburger"),
            Food(name: "Pizza", image: "pizza"),
            Food(name: "Salmon", image: "salmon"),
            Food(name: "Spaghetti", image: "spaghetti")
        ])
    
        // RxSwift 작업에서 메모리 누수를 방지하기 위한 DisposeBag 객체 생성
        let disposeBag = DisposeBag()
    
        // Interface Builder에서 연결된 UITableView
        @IBOutlet weak var tableView: UITableView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 뷰 컨트롤러의 타이틀 설정
            self.title = "Menu"
    
            // tableViewItems의 데이터를 tableView에 바인딩
            tableViewItems
                .bind(to: tableView.rx.items(cellIdentifier: "myCell", cellType: FoodTableViewCell.self)) { (_, tableViewItem, cell) in
                    cell.foodLabel.text = tableViewItem.name
                    cell.foodImageView.image = UIImage(named: tableViewItem.image)
                }
                .disposed(by: disposeBag)
    
            // tableView의 아이템 선택 시 동작 정의
    				// modelSelected: tableView의 아이템이 선택될 때 동작을 정의합니다. 
    				// 선택된 아이템에 해당하는 상세 페이지(FoodDetailViewController)로 이동합니다.
            tableView.rx.modelSelected(Food.self)
                .subscribe(onNext: { foodObject in
                    let foodVC = self.storyboard?.instantiateViewController(identifier: "FoodVC") as! FoodDetailViewController
                    foodVC.imageName = foodObject.image
                    self.navigationController?.pushViewController(foodVC, animated: true)
                })
                .disposed(by: disposeBag)
    		    }
    		}
    tableView
    	.rx
    	.itemSeleted
    	.subscribe(onNext: {
    		indexPath in 
    	})
    	.disposed(by: disposeBag)
    }
    • modelSelected와 또 다른 방법으로 itemSeleted를 사용 하여 행 선택을 처리 하는 방법입니다.

상위의 2번에서 제작한 TableViewCell을 눌렀을 때 보이는 DetailViewControllerRX로 구현 하는 방법.

필요한 개념

  • 전체적 간단 요약
    1. bservable (옵저버블):
      • 번역: 관찰 가능한
      • 설명: 이벤트를 발생시키는 객체. 옵저버블은 여러 이벤트를 방출하며, 이러한 이벤트들을 구독자들에게 전달합니다.
    2. Observer (옵저버):
      • 번역: 관찰자
      • 설명: 옵저버블의 이벤트를 수신하는 객체. 옵저버블이 방출하는 이벤트를 "관찰"하고, 해당 이벤트에 반응합니다.
    3. Subject (서브젝트):
      • 번역: 주제 또는 대상
      • 설명: 옵저버와 옵저버블의 역할을 동시에 수행하는 객체. 이벤트를 방출하면서 동시에 이벤트를 수신할 수 있습니다.
    4. Subscription (서브스크립션):
      • 번역: 구독
      • 설명: 옵저버블에 대한 구독 과정. 옵저버블이 방출하는 이벤트를 수신하기 위해 옵저버가 옵저버블에 "구독"하는 것을 의미합니다.
    5. DisposeBag (디스포즈백):
      • 번역: 처리 가방
      • 설명: 구독 해제를 관리하기 위한 객체. 옵저버블의 구독이 더 이상 필요하지 않을 때 메모리에서 해제하는 역할을 합니다.
    6. Relay (릴레이):
      • 번역: 중계, 전달
      • 설명: 오류나 완료 이벤트를 방출하지 않는 특별한 종류의 서브젝트. 주로 UI 작업에 사용됩니다.
  • 반응형으로 만들기 위해 subject 의 4가지 유형 중 하나인 BehaviorRelay , 연산자 중의 하나인 map 연산자를 사용합니다.

subject : "통신 채널"이나 "통로” - Programing context

  • PublishSubject: 새로운 요소만 subscribers에게 방출합니다.
let pSub = PublishSubject<String>()
// 제네릭 문법 < > 
// RX는 여러 가지 타입이 사용 가능한 문법이기 때문에 유연 하게 대응 하기 위해 제네릭을 사용 합니다.

pSub.onNext("PS El 1") // pSub에 첫 번째 요소를 추가

let ob1 = pSub.subscribe(onNext: { 
	elem in print(elem) 
}) 
// pSub를 구독하여 요소가 추가될 때마다 출력

pSub.onNext("PS El 2") // pSub에 두 번째 요소를 추가

// PS E1 2
  • BehaviorSubject: 새로운 subscribers에게 마지막 요소를 방출합니다.
let bSub = BehaviorSubject<String>(value: "BS El 1") 
// 초기 값으로 BehaviorSubject 생성
// 항상 최신 값을 내보내기 때문에 초기 값이 필요합니다. 없다면 생성 할 수 없습니다.

let ob2 = bSub.subscribe(onNext: { 
	elem in print(elem) 
}) 
// bSub를 구독하여 요소가 추가될 때마다 출력

// BS E1 1
  • ReplaySubject: 새로운 subscribers에게 버퍼 크기의 요소를 방출합니다.
let rSub = ReplaySubject<Int>.create(bufferSize: 3)
// Int 요소를 생성 합니다.

rSub.onNext(1)
rSub.onNext(2)
rSub.onNext(3)
// 그리고 그 새로 생성한 Subject 안에 새로운 요소들을 넣습니다.

let ob3 = rSub.subscribe(onNext: { 
	elem in print(elem) 
}) 
// rSub를 구독하여 요소가 추가될 때마다 출력하게 합니다.

// 1
// 2
// 3
  • AsyncSubject: 시퀀스의 마지막 이벤트만 방출하며, Subject가 완료 이벤트를 받을 때만 방출됩니다.
let aSub = AsyncSubject<String>()

aSub.onNext("aSub El 1")
aSub.onNext("aSub El 2")
aSub.onCompleted() 
// 만약 onCompleted 이벤트를 받지 않는다면 print 되지 않습니다. 

// AsyncSubject는 완료 이벤트가 발생했을 때만 마지막 요소를 방출

let ob4 = aSub.subscribe(onNext: { 
	elem in print(elem) 
})

// sSub E1 2 

Relay : 중계, 전달

  • Relay는 오직 next 이벤트만을 방출합니다.
  • errorcompleted 이벤트는 방출되지 않기 때문에, 주로 UI 작업과 같은 곳에서 사용됩니다.
  • RelaySubject를 래핑하고 있습니다.

PublishRelay: PublishSubject를 래핑하며, 오직 새로운 요소만 방출합니다.

let pRel = PublishRelay<String>()

pRel.accept("pRel El 1") 
// PublishRelay에 첫 번째 요소를 추가

let ob5 = pRel.subscribe(onNext: { 
	elem in print(elem) 
}) 
// pRel를 구독하여 요소가 추가될 때마다 출력

pRel.accept("pRel El 2") 
// PublishRelay에 두 번째 요소를 추가

// pRel El 2

BehaviorRelay:BehaviorRelay는 내부적으로 BehaviorSubject를 래핑합니다. 그러나 BehaviorRelay는 오직 next 이벤트만 방출하도록 제한되어 있습니다.

  • errorcompleted 이벤트를 방출할 수 없기 때문에 UI와 관련된 작업에서 유용합니다.
let bRel = BehaviorRelay<String>(value: "bRel El 1") 
// 초기 값으로 BehaviorRelay 생성

let ob6 = bRel.subscribe(onNext: { 
		elem in print(elem) 
}) 
// bRel를 구독하여 요소가 추가될 때마다 출력

bRel.accept("bRel El 2") 
// BehaviorRelay에 두 번째 요소를 추가

// bRel El 2

제네릭 (1)

개념을 적용한 실제 코드

  • 앞서 만든 TableViewCell을 누르면 나오는 디테일 페이지의 코드 입니다.
import Foundation
import UIKit
import RxSwift
import RxCocoa

class FoodDetailViewController: UIViewController {

    // UI 요소를 참조하기 위한 아웃렛
    @IBOutlet weak var foodImageView: UIImageView!
    
    // BehaviorRelay를 사용하여 이미지 이름을 관리. 초기값은 빈 문자열로 설정
    let imageName: BehaviorRelay<String> = BehaviorRelay<String>(value: "")
    
    // 메모리 관리를 위한 DisposeBag 객체 생성
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // BehaviorRelay에서 문자열 값을 가져와 UIImage 객체로 변환
        imageName
            .map { name in
                return UIImage(named: name)
            }
            // 변환된 이미지를 foodImageView의 image 속성에 바인딩
            .bind(to: foodImageView.rx.image)
            // 메모리 관리를 위해 disposeBag에 추가
            .disposed(by: disposeBag)
    }
}
  • 하지만 앞의 TableViewCell에서의 modelSelected도 같이 변경이 되어야 합니다.
  • 이 코드는 테이블 뷰에서 특정 셀을 선택했을 때 그 셀의 모델(Food)을 가져오고, 해당 모델의 이미지 이름을 FoodDetailViewControllerimageName BehaviorRelay에 전달하는 역할을 합니다. 그리고 해당 뷰 컨트롤러는 네비게이션 컨트롤러에 푸시됩니다.
// 선택된 테이블 뷰 셀의 모델을 가져옴
tableView.rx.modelSelected(Food.self)
    .subscribe(onNext: { foodObject in
        // 스토리보드에서 FoodDetailViewController를 인스턴스화
        guard let foodVC = self.storyboard?.instantiateViewController(identifier: "FoodVC") as? FoodDetailViewController else {
            return
        }

        // BehaviorRelay를 사용하여 선택된 음식의 이미지 이름을 FoodDetailViewController에 전달
        foodVC.imageName.accept(foodObject.image)
        
        // FoodDetailViewController를 네비게이션 컨트롤러에 푸시
        self.navigationController?.pushViewController(foodVC, animated: true)
    })
    .disposed(by: disposeBag) // 메모리 관리를 위해 disposeBag에 추가

Search TableView에서 Operator 중에 Map과 Filter를 활용한 반응형 검색 기능 구현.

마블을 통한 이해

  • 우선 새로운 operator를 사용 하기 전에 마블을 통해서 operator가 어떻게 작동 되는지 확인 해보면 좋습니다.

searchBar의 반응형 설계

기존 코드의 설계 한계

  • 앞에서 설계한 코드의 Food 배열은 Observable.just 로 설계 되었습니다.
    // Observable 시퀀스로 음식 목록을 생성
    let tableViewItems = Observable.just([
        Food(name: "Hamburger", image: "hamburger"),
        Food(name: "Pizza", image: "pizza"),
        Food(name: "Salmon", image: "salmon"),
        Food(name: "Spaghetti", image: "spaghetti")
    ])
  • 그렇기 때문에 이벤트를 방출 하는 역할만 할 수 있습니다. 다시 말해, Observable은 데이터에 직접적으로 접근 하거나 수정 할 수 없습니다. 오직 방출만 가능 합니다.
  • 그렇기 때문에 데이터를 업데이트 하거나 검색과 같은 연산을 수행 할 수 없습니다.

Solution - BehaviorRelay

  • BehaviorRelay는 RxCocoa에서 제공하는 특별한 종류의 Observable입니다.
  • 이것은 현재의 값을 저장하고, 새로운 구독자에게 가장 최근의 값을 방출하는 특성이 있습니다.
  • BehaviorRelay는 값을 직접 수정하거나 검색할 수 있습니다. 따라서 필터링 같은 연산을 수행하기에 적합합니다.
  • BehaviorRelay는 오류를 방출하지 않으며, 종료되지 않는 특성도 가지고 있습니다.
  • 따라서 위에서 Observable로 만든 코드는 BehaviorRelay로 변경 하여 데이터에 접근 하고, 수정 해야 합니다.
let tableViewItems = BehaviorRelay.init(value:[
	Food.init(name: "Pizza", image: "pizza"),
	...
	])
  • searchBar의 추가와 RxSwift와 RxCocoa를 활용하여 서치바를 통해 배열에 담긴 항목들을 필터링하고, 그 결과를 테이블 뷰에 바인딩하는 과정입니다.
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    // BehaviorRelay로 음식 목록을 초기화합니다. BehaviorRelay는 값의 변경을 감지하고 구독자에게 알릴 수 있습니다.
    let tableViewItems = BehaviorRelay(value: [
        Food(name: "Hamburger", image: "hamburger"),
        Food(name: "Pizza", image: "pizza"),
        // ... 나머지 항목들 ...
    ])

    // 메모리 관리를 위한 DisposeBag 객체를 생성합니다.
    let disposeBag = DisposeBag()

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.title = "Menu"

        // 서치바의 텍스트가 바뀔 때마다 필터링을 수행합니다.
        searchBar.rx.text.orEmpty
						// searchBar.rx.text는 서치바의 텍스트가 변경될 때마다 이벤트를 발생시키는 Observable입니다.	
						// .orEmpty는 서치바의 텍스트 값이 nil일 경우, 빈 문자열("")로 대체하는 역할을 합니다. 이렇게 함으로써 항상 문자열 값을 받아 처리할 수 있습니다.
						.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
            .distinctUntilChanged() // 연속적으로 동일한 검색 텍스트가 입력될 경우 필터링을 재수행하지 않습니다.
            .map { query in // map 연산자를 사용하여 검색 쿼리를 기반으로 필터링된 결과를 반환합니다.
                self.tableViewItems.value.filter { food in
                    query.isEmpty || food.name.lowercased().contains(query.lowercased())
                }
            }
            .bind(to: tableView.rx.items(cellIdentifier: "myCell", cellType: FoodTableViewCell.self)) { // 필터링된 결과를 테이블 뷰에 바인딩합니다.
                (tv, tableViewItem, cell) in
                cell.foodLabel.text = tableViewItem.name
                cell.foodImageView.image = UIImage(named: tableViewItem.image)
            }
            .disposed(by: disposeBag) // 메모리 관리를 위해 disposeBag에 바인딩을 추가합니다.
						// ... 나머지 항목들 ...

throttle 함수

.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
  • throttle 연산자는 지정된 시간 간격 내에 발생하는 연속적인 이벤트 중에서 첫 번째 이벤트만 통과시키는 역할을 합니다. 여기서는 300 밀리초 동안의 이벤트 중 첫 번째 이벤트만을 받아들입니다.
  • 예를 들어, 사용자가 "apple"이라고 빠르게 입력했을 때, 각 글자를 입력할 때마다 이벤트가 발생하지만, 300 밀리초 내에는 "a", "ap", "app", "appl", "apple" 중에서 첫 번째인 "a"만 통과시키고 나머지는 무시합니다.
  • MainScheduler.instance는 이러한 동작이 메인 스레드에서 수행되도록 합니다. 이는 UI 업데이트와 관련된 작업이므로 메인 스레드에서 수행되어야 합니다.
  • 성능 향상:
    • 사용자가 빠르게 연속해서 텍스트를 입력하더라도, 모든 입력에 대해 반응할 필요가 없으므로 리소스를 절약할 수 있습니다.
  • 사용자 경험 개선:
    • 사용자가 검색어를 입력하는 중간에 불필요한 검색 결과 업데이트로 인한 깜박임이 줄어듭니다.
  • 서버 요청 절약:
    • 원격 검색 API를 사용하는 경우, throttle을 사용하면 너무 많은 요청을 보내는 것을 방지하고, 서버에 부담을 줄일 수 있습니다.

RxDataSources Framework

  • RxDataSources와 RxSwift를 사용하여 테이블 뷰를 섹션으로 나누고 메뉴처럼 표시하는 과정
  • 이전에는 TableViewCellsearchBar를 추가 하고, filter operator를 사용하여 음식을 검색 하는 코드를 작성 하였습니다.
  • 이 목차에선 음식 목록을 실제 메뉴 처럼 보이게 만들기 위한 코드를 작성 하겠습니다.

RXDataSourses 의 개요

  • RxDataSources는 RxSwift와 함께 사용하는 데 도움이 되는 라이브러리로, 특히 테이블 뷰와 컬렉션 뷰의 데이터를 관리하기 위해 유용합니다.
  • RxDataSources는 테이블 뷰 및 컬렉션 뷰에 섹션화된 데이터를 표시하고 업데이트하는 데 도움을 주며, RxSwift와 함께 사용하여 데이터를 쉽게 바인딩할 수 있도록 돕습니다.

특징

  1. 섹션화된 데이터
    1. RxDataSources는 데이터를 섹션 모델로 구성하여 테이블 뷰 또는 컬렉션 뷰를 섹션으로 나누고 각 섹션에 해당하는 항목을 관리합니다.
  2. 데이터 소스
    1. RxDataSources는 데이터와 뷰를 연결하는 데이터 소스를 제공합니다. 이 데이터 소스를 사용하여 데이터를 뷰에 표시하고 업데이트할 수 있습니다.
  3. 간편한 바인딩
    1. RxDataSources는 RxSwift와 함께 사용하여 데이터와 뷰를 쉽게 바인딩할 수 있도록 돕습니다. 이를 통해 데이터가 변경될 때 자동으로 뷰가 업데이트됩니다.
  4. 사용자 정의 셀 및 헤더
    1. RxDataSources를 사용하여 사용자 정의 셀과 섹션 헤더를 구성할 수 있으며, 각 항목을 개별적으로 커스터마이즈할 수 있습니다.
  • 간단히 말해, RxDataSources는 RxSwift와 함께 사용하여 테이블 뷰 및 컬렉션 뷰를 효과적으로 다루고 데이터를 표시하고 업데이트하는 데 도움을 주는 라이브러리입니다.

RXDataSourses 사용법

  • pod 파일에 추가가 필요합니다
pod 'RxDataSource' 

RXDataSourses 를 기존의 코드에 적용

  • 기존에 있던 TabelView에 section을 추가 해야 함으로 sectionModel file을 추가합니다.
import Foundation
import RxDataSources

struct SectionModel {
    var header: String
    var items: [Food]
}
// SectionModel 구조체는 섹션의 정보를 저장합니다. 
// 각 섹션은 헤더(타이틀)와 해당 섹션의 음식 항목을 포함합니다.

extension SectionModel: SectionModelType {
    typealias Item = Food

    init(original: SectionModel, items: [Food]) {
		// SectionModelType 프로토콜에서 필요한 메서드 입니다. 
		// 이 메서드를 구현하여 원본 SectionModel과 업데이트된 항목 배열을 사용하여 새로운 SectionModel을 생성합니다.
        self = original
        self.items = items
    }
}
  • 기존의 TableView가 있는 class도 변경이 필요 합니다. 방금 만든 섹션을 적용 해야 하기 때문입니다.
  • 앞서 만든 TableViewCellSectonModel을 사용 하여 SectionModel을 초기화 합니다.
  • MainMenu , Desert 두가지로 나누었습니다.
// BehaviorRelay를 사용하여 섹션화된 테이블 뷰 데이터를 초기화합니다.
let tableViewItemsSectioned = BehaviorRelay(value: [
    SectionModel(header: "Main Courses", items: [
        Food(name: "Hamburger", image: "hamburger"),
        Food(name: "Pizza", image: "pizza"),
        Food(name: "Salmon", image: "salmon"),
        Food(name: "Spaghetti", image: "spaghetti"),
        Food(name: "Club-sandwich", image: "club-sandwich"),
        Food(name: "Curry", image: "curry"),
        Food(name: "Salad cheese", image: "salad-cheese"),
        Food(name: "Salad veggie", image: "salad-veg"),
        Food(name: "Ribs", image: "ribs"),
        Food(name: "Chana masala", image: "chana-masala")
    ]),
    SectionModel(header: "Desserts", items: [
        Food(name: "Pancakes", image: "pancakes"),
        Food(name: "Tiramisu", image: "tiramisu"),
        Food(name: "Cake", image: "cake")
    ])
])

// DisposeBag를 초기화하여 메모리 누수를 방지합니다.
let disposeBag = DisposeBag()
  • RxDataSources 를 사용하여 앞의 SectionModel을 바인딩 하는 과정입니다.
// 테이블 뷰와 검색 바 아울렛을 정의합니다.
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: UISearchBar!

// RxTableViewSectionedReloadDataSource를 사용하여 데이터와 테이블 뷰를 연결하는 데이터 소스를 설정합니다.
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel>(
    // 각 셀을 구성합니다.
    configureCell: { dataSource, tableView, indexPath, item in
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! FoodTableViewCell
        cell.foodLabel.text = item.name
        cell.foodImageView.image = UIImage(named: item.image)
        return cell
    },
    // 섹션 헤더를 설정합니다.
    titleForHeaderInSection: { dataSource, sectionIndex in
        return dataSource[sectionIndex].model
    }
)

override func viewDidLoad() {
    super.viewDidLoad()

    self.title = "Menu"
    
    // 검색 바에서 입력된 텍스트를 가져오고, 입력 간격과 중복 입력을 제어합니다.
    let foodQuery = searchBar.rx.text.orEmpty
        .throttle(.milliseconds(300), scheduler: MainScheduler.instance)
        .distinctUntilChanged()
        .map { query in
            // 검색어를 사용하여 섹션 모델을 업데이트하고, 필터링합니다.
            self.tableViewItemsSectioned.value.map { sectionModel in
                SectionModel(model: sectionModel.model, items: sectionModel.items.filter { food in
                    return query.isEmpty || food.name.lowercased().contains(query.lowercased())
                })
            }
        }
        // 데이터를 테이블 뷰에 바인딩합니다.
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)
    
    // 테이블 뷰에서 항목을 선택할 때 해당 음식 항목의 이미지를 전달하고 다음 뷰 컨트롤러로 이동합니다.
    tableView.rx.modelSelected(Food.self)
        .subscribe(onNext: { foodObject in
            let foodVC = self.storyboard?.instantiateViewController(identifier: "FoodVC") as! FoodDetailViewController
            foodVC.imageName.accept(foodObject.image)
            self.navigationController?.pushViewController(foodVC, animated: true)
        })
        .disposed(by: disposeBag)
}
  • 검색 바에서 입력된 텍스트를 사용하여 섹션화된 테이블 뷰의 데이터가 필터링되고 검색어와 일치하는 항목만 표시됩니다.

Login Page의 반응형 설계

  • RX를 통한 Login Page를 구현 하며 유효성 검사를 구현 해보겠습니다.

CombineLatest operator

  • combineLatest는 두 개 이상의 Observable 시퀀스에서 가장 최근에 발생한 이벤트들을 결합하여 새로운 이벤트를 생성합니다.
  • 이 연산자는 모든 입력 Observable 시퀀스에서 최소 하나의 이벤트를 발생시킬 때마다 작동하며, 입력 Observable 중 하나라도 이벤트를 발생시키면 combineLatest는 그 순간에 대한 결과를 생성합니다.
  • 결과로 생성된 이벤트에는 입력 Observable에서 발생한 가장 최근의 값들이 포함됩니다.
  • combineLatest는 주로 두 개 이상의 Observable 시퀀스의 값을 결합하여 복합적인 작업을 수행할 때 사용됩니다.
  • 이 연산자를 사용하면 각 Observable에서 나오는 최신 값을 결합하여 다른 Observable의 값과 조합할 수 있으며, 이를 통해 다양한 작업을 수행할 수 있습니다.

실제 LoginPage의 적용 사례

import Foundation
import UIKit
import RxSwift
import RxCocoa

class LoginViewController: UIViewController {

    @IBOutlet weak var userNameTf: UITextField!
    @IBOutlet weak var passwordTf: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Login"

        // 사용자가 로그인 버튼을 탭할 때
				// login 함수를 실행 합니다.
        loginButton.rx.tap
            .withLatestFrom(Observable.combineLatest(self.userNameTf.rx.text.orEmpty, self.passwordTf.rx.text.orEmpty))
            .subscribe(onNext: { 
                self.login(user: $0, pass: $1)
            })
            .disposed(by: disposeBag)
    }

    // 사용자의 입력된 유저명과 패스워드를 검증하고, 유효한 경우 다음 화면으로 이동
    func login(user: String, pass: String) {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
				// 정규 표현식을 사용하여 이메일 형식을 검증하기 위한 패턴을 정의합니다. 
				// 이 패턴은 이메일 주소의 형식을 검사합니다.
        let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegEx)
				// NSPredicate를 사용하여 정규 표현식을 이용한 패턴 매칭을 위한 객체를 생성합니다. 
				// 이 emailTest 객체는 이메일 형식을 검증하는데 사용됩니다.
        let emailValid = emailTest.evaluate(with: user)
				// 유저명이 이메일 형식과 일치하는지 여부를 판단합니다. 
				// evaluate(with: 메서드를 사용하여 유저명을 정규 표현식과 비교합니다. 
				// emailValid는 이 유저명이 이메일 형식과 일치하면 true, 그렇지 않으면 false가 됩니다.
        let passValid = (pass != "" && pass.count >= 6)
				// 이 부분은 패스워드가 조건을 충족하는지 여부를 판단합니다. 
				// 패스워드는 비어있지 않고 최소 6자 이상이어야 합니다.

        if emailValid && passValid {
            // 유효한 경우, 다음 화면으로 이동
            if let foodListVC = self.storyboard?.instantiateViewController(identifier: "FoodListVC") as? ViewController {
                self.navigationController?.pushViewController(foodListVC, animated: true)
            }
        } else {
            // 유효하지 않은 경우, 경고 표시
            Utils.displaySimpleAlert(title: "Wrong credentials", message: "Please enter a valid username and password", viewController: self)
        }
    }
}

정규 표현식 (1)

RX의 Debug

  • XCode 자체 에서 제공 하는 breakPoint 기능이 있지만 RX의 특성상 디버그를 할 수 없는 상황이 발생 합니다.
    • 왜 비동기 및 Reactive 프로그래밍은 xcode의 디버그 기능으로는 한계가 있는가? Xcode의 기본 디버깅 도구로는 RXSwift 코드 내의 비동기 이벤트 및 스트림을 실시간으로 디버그하기 어려울 수 있습니다. 기존의 디버깅 도구는 코드의 실행을 중지하고 특정 지점에서 상태를 확인하거나 변수 값을 추적하는 데 사용됩니다. 하지만 RXSwift에서는 이벤트 및 스트림이 비동기적으로 발생하며, 여러 연산자와 핸들러 함수를 통해 데이터가 처리됩니다. 이로 인해 전통적인 중단점 디버깅 방식은 비동기 처리 및 리액티브 스트림의 흐름을 명확하게 파악하기 어려울 수 있습니다. 따라서 RXSwift는 디버깅을 보다 용이하게 하기 위해 .debug("custom label")와 같은 디버깅 연산자를 제공합니다. 이 연산자를 사용하면 RXSwift의 리액티브 스트림에서 발생하는 이벤트를 실시간으로 모니터링하고, 각 이벤트에 사용자 지정 레이블을 추가하여 디버깅 정보를 출력할 수 있습니다. 이를 통해 RXSwift 코드의 동작을 실시간으로 추적하고 디버깅할 수 있습니다. 또한 메모리 누수와 같은 문제는 RXSwift의 전역 변수인 RxSwift.Resources.total을 사용하여 디버그할 수 있습니다. 이 변수를 통해 할당된 리소스의 증가를 추적하여 메모리 누수를 확인할 수 있습니다. RXSwift의 디버깅 방법은 기존의 디버깅과는 다르지만 리액티브 프로그래밍의 복잡성을 다루기 위한 강력한 도구를 제공합니다. 이를 통해 비동기 및 리액티브 코드를 효과적으로 디버깅하고 문제를 해결할 수 있습니다.
  • RX는 자체의 Debug operator를 제공 하여 Debug 기능을 제공 합니다.

Debug operator

.debug("custom label")
  • 이벤트가 발생할 때 해당 이벤트에 대한 정보를 콘솔에 출력합니다.
  • 코드를 디버그하는 데 유용합니다.

Memory Leak Debug

RxSwift.Resources.total
  • 모든 할당된 리소스를 추적하는 전역 변수입니다.
  • DEBUG 모드를 활성화해야 합니다.
  • 무한 증가하는 리소스 카운트를 식별하여 메모리 누수를 찾아줍니다.

Memory Leak Debug 해결

  • Dispose 를 사용합니다.
  • DisposeBag를 사용하여 메모리가 과하게 사용 되는 문제를 해결 합니다.

0개의 댓글