
Observable 혹은 Observable 시퀀스에서 방출되는 데이터를 UI 컴포넌트에 연결하는 것을 의미합니다. 이 연결을 통해 데이터가 변경될 때 UI도 자동으로 업데이트됩니다.let observableText = Observable.just("Hello, RxSwift!")
observableText.bind(to: label.rx.text)
observableText는 "Hello, RxSwift!"라는 문자열을 방출하는 Observable입니다. 이 데이터를 label의 text 프로퍼티에 바인딩하면, label은 자동으로 "Hello, RxSwift!"라는 문자열을 표시하게 됩니다.ObservableObserverDisposable Bagimport 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
}
}
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에 추가합니다.
}
}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를 사용 하여 행 선택을 처리 하는 방법입니다.TableViewCell을 눌렀을 때 보이는 DetailViewController를 RX로 구현 하는 방법.subject 의 4가지 유형 중 하나인 BehaviorRelay , 연산자 중의 하나인 map 연산자를 사용합니다.subject : "통신 채널"이나 "통로” - Programing contextPublishSubject: 새로운 요소만 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 이벤트만을 방출합니다.error나 completed 이벤트는 방출되지 않기 때문에, 주로 UI 작업과 같은 곳에서 사용됩니다.Relay는 Subject를 래핑하고 있습니다.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 이벤트만 방출하도록 제한되어 있습니다.
error나 completed 이벤트를 방출할 수 없기 때문에 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
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)을 가져오고, 해당 모델의 이미지 이름을 FoodDetailViewController의 imageName 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에 추가
operator를 사용 하기 전에 마블을 통해서 operator가 어떻게 작동 되는지 확인 해보면 좋습니다.
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은 데이터에 직접적으로 접근 하거나 수정 할 수 없습니다. 오직 방출만 가능 합니다.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 밀리초 동안의 이벤트 중 첫 번째 이벤트만을 받아들입니다.MainScheduler.instance는 이러한 동작이 메인 스레드에서 수행되도록 합니다. 이는 UI 업데이트와 관련된 작업이므로 메인 스레드에서 수행되어야 합니다.throttle을 사용하면 너무 많은 요청을 보내는 것을 방지하고, 서버에 부담을 줄일 수 있습니다.RxDataSources FrameworkRxDataSources와 RxSwift를 사용하여 테이블 뷰를 섹션으로 나누고 메뉴처럼 표시하는 과정TableViewCell에 searchBar를 추가 하고, filter operator를 사용하여 음식을 검색 하는 코드를 작성 하였습니다.RXDataSourses 의 개요특징
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도 변경이 필요 합니다. 방금 만든 섹션을 적용 해야 하기 때문입니다.TableViewCell의 SectonModel을 사용 하여 SectionModel을 초기화 합니다.// 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 시퀀스에서 가장 최근에 발생한 이벤트들을 결합하여 새로운 이벤트를 생성합니다.combineLatest는 그 순간에 대한 결과를 생성합니다.combineLatest는 주로 두 개 이상의 Observable 시퀀스의 값을 결합하여 복합적인 작업을 수행할 때 사용됩니다.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)
}
}
}
DebugXCode 자체 에서 제공 하는 breakPoint 기능이 있지만 RX의 특성상 디버그를 할 수 없는 상황이 발생 합니다.xcode의 디버그 기능으로는 한계가 있는가? Xcode의 기본 디버깅 도구로는 RXSwift 코드 내의 비동기 이벤트 및 스트림을 실시간으로 디버그하기 어려울 수 있습니다. 기존의 디버깅 도구는 코드의 실행을 중지하고 특정 지점에서 상태를 확인하거나 변수 값을 추적하는 데 사용됩니다. 하지만 RXSwift에서는 이벤트 및 스트림이 비동기적으로 발생하며, 여러 연산자와 핸들러 함수를 통해 데이터가 처리됩니다. 이로 인해 전통적인 중단점 디버깅 방식은 비동기 처리 및 리액티브 스트림의 흐름을 명확하게 파악하기 어려울 수 있습니다. 따라서 RXSwift는 디버깅을 보다 용이하게 하기 위해 .debug("custom label")와 같은 디버깅 연산자를 제공합니다. 이 연산자를 사용하면 RXSwift의 리액티브 스트림에서 발생하는 이벤트를 실시간으로 모니터링하고, 각 이벤트에 사용자 지정 레이블을 추가하여 디버깅 정보를 출력할 수 있습니다. 이를 통해 RXSwift 코드의 동작을 실시간으로 추적하고 디버깅할 수 있습니다. 또한 메모리 누수와 같은 문제는 RXSwift의 전역 변수인 RxSwift.Resources.total을 사용하여 디버그할 수 있습니다. 이 변수를 통해 할당된 리소스의 증가를 추적하여 메모리 누수를 확인할 수 있습니다. RXSwift의 디버깅 방법은 기존의 디버깅과는 다르지만 리액티브 프로그래밍의 복잡성을 다루기 위한 강력한 도구를 제공합니다. 이를 통해 비동기 및 리액티브 코드를 효과적으로 디버깅하고 문제를 해결할 수 있습니다.Debug operator를 제공 하여 Debug 기능을 제공 합니다.Debug operator.debug("custom label")
DebugRxSwift.Resources.total
Debug 해결Dispose 를 사용합니다.DisposeBag를 사용하여 메모리가 과하게 사용 되는 문제를 해결 합니다.