Share
Sign In
Swift | 학습 내용 정리
[Swift - UIKit] Swift 프로젝트에서 VC간에 데이터를 전달하는 4가지 방법
한결
👍
모바일 앱의 특성상, 작은 화면에서 한 번에 많은 데이터를 보여주는 것이 쉽지 않다. 그래서 하나의 앱에서도 다양한 형태의 데이터를 다양한 페이지에서 보여주고 있다. 그런 이유로 페이지간 데이터 전달이 중요한데, 지금까지 내가 학습한 4가지의 데이터 전달 방식에 대해 기록해두고 기억해보고자 한다.
Swift는 기본적으로 클래스, 구조체로 화면에 대한 구조(=인스턴스)를 만든다. 구조의 멤버들은 뷰의 객체로서(아닌 경우도 있지만) 짜여진 구조에 맞게 데이터를 그려낸다. 인스턴스를 만드는 것이기 때문에 그 인스턴스가 생성되는 시점 + 생성되어 활용되는 그 구조의 공간이 무엇보다 중요하다. 전환하고자 하는 페이지(거의 VC)의 인스턴스를 생성하면, 내부의 멤버에 직접/간접적으로 접근해서 값을 할당해주는 것이 값 전달의 기본이다.
인스턴스의 멤버에 값 할당하기
💬
해당 방법으로는,
1.
넘어가고자 하는 VC의 인스턴스를 생성하고
2.
해당 VC의 생명주기가 활성화되기 전에 인스턴스 멤버나 인스턴스 메서드를 실행시키는
형태로 다른 VC에 값을 전달한다.
// Previous final class PreviousVC: UIViewController { .. @objc func goSomeVC(_ sender: UIButton) { let vc = NextVC() // 1. 넘어가고자 하는 VC 인스턴스 생성 vc.configureViewWithData(SomeData) // 2. 해당 VC의 인스턴스 멤버로 내부 멤버 configure navigationController?.pushViewcontroller(vc, animated: true) // 3. VC 이동 } } // Next final class NextVC: UIViewController { private let label = UILabel func configureViewWithData(_ data: SomeDataType) { self.label.text = data.someText } }
넘어가고자 하는 VC에 대한 명확한 공간(=인스턴스)를 하나의 스코프 내에서 컨트롤 할 수 있기 때문에, 어떻게보면 가장 쉬운 데이터 전달 방식이 될 수 있다.
'이전 VC에 대한 인스턴스를 생성하여 값을 넘겨주는 방식으로 넘어오기 전 VC로 접근할 수 있겠구나!' 라고 생각할 수 있겠지만, (어떻게 보면 당연하지만) 그렇지 않다.
넘어온 VC 환경에서 이전 VC에 대한 인스턴스를 생성한다는 것은 (아래 그림처럼) 완전히 새로운 VC의 인스턴스 환경을 구축하는 것과 같다. (⇒ 평행 세계에 이름만 같은 공간을 찾아가는 느낌이랄까)
그렇기 때문에, 넘어오기 전의 환경으로 컨택스트를 넘겨줄 수 없다는 의미가 된다.
클로저 활용하기
💬
해당 방법으로는,
1.
넘어가거나, 넘어온 VC에 값을 전달하는 간단한 익명함수(클로저)를 선언하고 (멤버 변수)
2.
VC 인스턴스에 접근하는 곳에서 해당 익명함수(클로저)의 내부 로직을 구현해주고
3.
필요한 시점에 클로저를 호출시켜 컨택스트를 해당 시점으로 가져오는
형태로 다른 VC에 값을 전달한다.
// Previous final class PreviousVC: UIViewController { private var someText: String = "" .. @objc func goSomeVC(_ sender: UIButton) { let vc = NextVC() // 1. 넘어가고자 하는 VC 인스턴스 생성 vc.sender = { // 2. NextVC가 사라질 때 실행될 클로저 내부 로직 구현 self.someText = vc.getLabelText() } navigationController?.pushViewcontroller(vc, animated: true) // 3. VC 이동 } } // Next final class NextVC: UIViewController { var sender: (() -> ())? private let label = UILabel override viewWillDisappear() { super.viewWillDisappear() // 해당 VC가 사라지는 시점에 클로저를 호출 // -> 해당 시점에 넘어오기 전 VC에서 선언한 클로저 내부 로직이 동작 // -> 컨택스트를 이전 VC의 환경으로 맞춰줌 sender?() } func getLabelText() -> String { return self.label.text } }
넘어온 VC, View에서 이전 VC나 View로의 값 전달은 클로저를 이용할 수 있다.
클로저의 구현부를 이전 VC, View에서 진행하여서, 클로저가 호출되는 시점에 해당 내부 로직이 동작할 수 있게 한다면 쉽게 데이터를 전달할 수 있다.
클로저의 매개변수를 지정하여서 지정된 타입의 데이터만 전달하는 것도 가능하다.
클로저가 호출된 그 시점 자체에서 구현된 부분이 동작하기 때문에 꼭 viewWillDisappear와 같이 VC 역할이 종료되는 시점이 아니어도 괜찮다.
개인적으로 클로저를 통한 데이터 전달이 가장 편하다.
프로토콜 - 델리게이트 활용하기
💬
해당 방법은,
1.
프로토콜이 가진 Protocol as Type 이라는 개념을 활용하여서
2.
프로토콜 내부에 데이터를 전달하는 함수를 정의해두고
3.
이 프로토콜을 채용하는 UIViewController가 내부에서 해당 함수(메서드)의 내부 로직을 구현하게 한 다음
4.
데이터를 전달받을 다른 VC에 delegate(꼭 delegate이라고 할 필요는 없음)라고 하는 멤버 변수에 해당 프로토콜을 타입으로 지정하여
5.
데이터를 넘겨받을 VC에서 해당 delegate 변수에 자기 자신을(self) 위임해주는
방식으로 동작한다.
// 1. 프로토콜을 정의하고 protocol DataSendDelegate: AnyObject { func sendData(_ t: String) } // 2. 위 프로토콜을 VC단에서 채용해준다. final class PreviousVC: UIViewController, DataSendDelegate { private let someLabel = UILabel() .. @objc func goSomeVC(_ sender: UIButton) { let vc = NextVC() // 4. 넘어가고자 하는 VC의 인스턴스를 만든 다음 vc.delegate = self // 5. 해당 VC의 delegate 변수에 해당 VC를 채택한다. navigationController?.pushViewcontroller(vc, animated: true) // 7. VC 이동 } // 3. 프로토콜에서 제약으로 한 함수를 내부에서 정의해주고 func sendData(_ t: String) { self.someLabel.text = t } } final class NextVC: UIViewController { weak var delegate: DataSendDelegate? // 6. 5에서 해당 VC의 delegate를 PreviousVC로 할당함 override viewWillDisappear() { // 8. 이전 페이지로 이동하면서 프로토콜의 함수로 데이터 전달 delegate?.sendData("전달할 텍스트") } }
Swift protocol이 가진 Protocol as Type을 활용하여서 Protocol을 채택한 클래스를 Protocol을 채택한 다른 변수에 값으로 할당할 수 있다.
프로토콜에서 구현하라고 제약한 함수를 프로토콜을 채용한 VC에서 구현하고, 그 함수를 데이터를 전달받을 VC에서 활용하는 방식이다.
애플이 이미 구현해 둔 UITableViewDelegate, UICollectionViewDelegate와 같은 프로토콜을 채용해서 사용하는 것과 동일한 방식이다.
프로토콜에서 강제하고자 하는 함수들을 잘 정의해두면, 해당 프로토콜을 채용하는 곳에서 어떤 역할을 해야하는지가 명확해지기 때문에 코드 안정성이 높아질 수 있다.
NotificationCenter 활용하기
💬
해당 방법은,
1.
NotificationCenter의 observer와 post를 활용하여
2.
NotificationCenter.default.post 내부에서 userInfo 값에 원하는 값을 딕셔너리로 담아 쏘고,
3.
NotificationCenter.default.addObserver 에서 탐지한 값을 활용하는
방식으로 동작한다.
// Previous final class PreviousVC: UIViewController { private let someLabel = UILabel() override viewDidLoad() { super.viewDidLoad() // view가 뜨는 시점부터 옵저버로 Post된 Notification을 포착한다. NotificationCenter.default.addObserver( self, selector: #selector(changeLabelText), // Post된 Notification 이름을 정확하게 포착해야 한다. name: NSNotification.Name("postName"), object: nil ) } @objc func goSomeVC(_ sender: UIButton) { let vc = NextVC() navigationController?.pushViewcontroller(vc, animated: true) } @objc func changeLabelText(_ noti: NSNotification) { // notification의 userInfo 딕셔너리에서 가진 값을 키로 조회한다. if let text = noti.userInfo?["someText"] as? String { self.someLabel.text = text // NextVC의 textfield의 텍스트 값이 전달 } } } // Next final class NextVC: UIViewController { private let textfield = UITextField() override viewWillDisappear() { super.viewWillDisappear() guard let text = textfield.text else { return } NotificationCenter.default.post( name: NSNotification.Name("postName"), object: nil, userInfo: ["someText": text] ) } }
Notification이 특정 이름으로 Post한 데이터 객체를 다른 뷰 객체에서 Observer로 포착하여 활용한다.
하나의 VC에서 발송한 데이터를 다른 여러 VC에서 동시에 전달 받을 수 있다는 점에서 활용도가 높다.
다만, 옵저버가 값을 기다리고 있지도 않은데 무분별하게 값을 쏘면 앱이 원하는 대로 동작하지 않는다. 따라서 반드시 옵저버가 먼저 등록이 되고, 동일한 이름으로 Post 되어야 한다.
개인적으로 이 방식은 작성해야 하는 코드도 많고, 뭔가 직관적이지 않은 느낌이 들어서 잘 손이가지 않는다.
Ha
Subscribe to 'hankyeol'
Welcome to 'hankyeol'!
By subscribing to my site, you'll be the first to receive notifications and emails about the latest updates, including new posts.
Join SlashPage and subscribe to 'hankyeol'!
Subscribe
👍
한결
[UIKit + RxSwift] MVVM 구조에서 Input-Output 패턴으로 UI 컨트롤하기
RxSwift의 기본 컨셉은 아래 콘텐츠를 통해 간단하게 정리해보았습니다. 커스텀한 Observable 객체를 이용해서 MVVM 패턴으로 간단한 iOS 앱을 만들어왔다. View 객체에서 발생하는 이벤트를 인식하고 이벤트에 따른 비즈니스 로직을 ViewModel에서 처리하고, 그걸 View가 원하는 Output으로 돌려주는 형태로 코드를 작성했다. 자연스럽게 앱 전반에서 '이벤트 방출 - 구독 - 연산 - 결과 도출'의 매커니즘으로 모든 이벤트를 Asynchronous하게 처리하는 RxSwift의 여러 API를 학습하게 되었고, 이를 통해 간단하게 UI를 컨트롤 하는 방식을 기록한다. MVVM(Input-Output 패턴) + RxSwift 로 코드를 구성한 화면은 아래의 이미지와 같다. 로그인 화면에서는 email, password를 입력받는 TextField의 텍스트 입력 이벤트를 구독하여 조건에 따라 UI를 컨트롤한다. shopping list 라는 타이틀을 가진 화면에서는 역시 TextField의 텍스트 입력 이벤트를 구독해서 테이블뷰에 셀을 보여주고, 컬렉션 아이템 터치 이벤트를 통해 테이블뷰를 컨트롤한다. Input - Output 패턴 MVVM 아키텍쳐의 주요 골자는 View를 컨트롤 하는 View 객체, 앱에서 활용될 데이터에 대한 명세~메서드를 구축하는 Model 객체, 그리고 그 두 객체 사이를 연결하여 이벤트에 대한 로직을 처리하는 ViewModel 객체의 조합이다. Input - Output 패턴도 이런 골자를 지키면서, 이벤트가 들어오는 Input 객체와 로직의 처리 결과로 View에게 반환되는 Output 객체를 각각 구분하는 컨셉을 가진다. ViewModel에서 활용되는 패턴으로, View에서는 Input 객체에 이벤트를 실어 ViewModel이 이벤트를 구독할 수 있게 한다. Input과 Output 객체를 연결하는 통로는 transform(Input) → Output 이라고 하는 메서드를 보통 활용한다. 프로토콜로 Input - Output 패턴으로 ViewModel 구현의 제약을 정해줄 수도 있겠다. Input - Output 패턴이 적용된 ViewModel의 전형적인 모습은 아래와 같다. 내가 파악한 Input - Output 패턴의 핵심은 요정도이다. View단에서 전달하려고 하는 Event Input과 View단에서 필요로 하는 Data(Disposable한) Output을 각각의 구조체에 묶어 보다 명확하게 인지할 수 있다. 결국 Output 객체의 프로퍼티로 전달하는 값도 Disposable을 반환하는 Observable 객체이다. 이벤트에 따라서 변화하는 Output 값을 View 단에서도 Observer에 구독시켜서 UI를 업데이트 한다. RxSwift, RxCocoa 라이브러리가 가지는 API를 잘 활용하면 보다 편하게 Event Emit을 컨트롤 할 수 있다.
한결
Swift - 의존성 주입과 MVVM
의존성과 주입 의존성 : 다른 것에 의지하여 생활하거나 존재하는 성질. 이라고 국어 사전에 정의되어 있다. 코드에 의존성이 생긴다는 것은 어떤 것일까? 쉽게 생각해보면, 어떤 클래스 내부에서 다른 클래스로 인스턴스를 생성하고, 그 인스턴스를 활용하는 것을 코드적인 의존성이 만들어졌다고 볼 수 있을 것 같다. 위의 SomeViewController 코드에서 보면, SomeViewController라고 하는 클래스는 APIService라고 하는 특정한 클래스에 의존성을 가지고 있다. APIService 내부에 구현된 fetch 라고 하는 메서드를 내부에서 활용하는데, APISerivce 클래스 구현부에서 해당 메서드의 이름을 fetches 라고 바꾼다고 하면 어떻게 될까? 당연하게도 SomeViewController 코드에서도 그 이름에 맞게 코드를 바꿔줘야 한다. 이것만이 아니라, 해당 인스턴스를 다른 클래스의 인스턴스로 바꾼다고 가정하면 코드로 바꿔줘야 하는 부분은 더 늘어날 수 있다. 코드에서는 의존성이 발생하지 않게 하는 것은 쉽지 않다. *싱글톤으로 작성된 객체를 타입 멤버로 접근한다면, 클래스 내부에서 의존성이 생기지 않을 수는 있겠다. 주입 : 흘러 들어가도록 부어 넣음. 이라고 국어 사전에 정의되어 있다. 코드에서 '주입'을 한다는 것은 쉽게 생각해보면 인스턴스를 생성할 때, 생성자에 값을 넣어(=주입하여) 내부의 저장 속성의 값을 초기화 시켜주는 것이라고 생각해볼 수 있다. 그러면 의존성을 주입한다는 것은, 외부에서 특정 클래스의 인스턴스를 만들어서 생성자로 전달을 하는 행위를 말하는 걸까? 아래의 코드처럼 APIService의 인스턴스를 단순히 생성자로 넣어준다고 하더라도 SomeViewController는 여전히 APIService에 의존성을 가지고 있다. 의존성을 넣어주는 것이기 때문에 의존성 주입이라고 볼 수 있겠지만, 정확하게 APIService 라고 하는 타입의 인스턴스를 넣어줘야 해서 확장성이 떨어진다. Swift에서의 의존성 주입 그러면 확장성을 갖추면서도 의존성을 외부에서 분리시키는 방법은 Swift에서 어떻게 할 수 있을까? 혼자 질문을 던지고 바로 답을 내려버리는 것 같아서 좀 그렇지만, Protocol as a Type의 개념을 활용할 수 있을 것 같다. Swift에서 프로토콜은 정말 다양한 방식으로 활용되는데, 단순히 어떤 객체에 구현부를 강제하는 것을 넘어서 비슷한 역할을 하는 객체들을 하나의 타입으로 묶어줄 수 있는 모듈러 역할로 활용할 수 있다. 즉, 특정 프로토콜을 동일하게 채용하는 클래스는 어느정도 '아 그 프로토콜의 일을 하겠구나'를 쉽게 유추할 수 있다. 이렇게 동일한 프로토콜을 채용한 객체(=클래스의 인스턴스)를 생성자로 주입시키면 어떨까? 이럴경우, SomeViewController는 이제부터 SomeDataRelatedProtocol이라는 프로토콜 타입에 의존성을 가지게 된다. 그러면 외부에서 해당 프로토콜을 채용한 어떤 객체 타입도 주입할 수 있게 된다.(Protocol as a Type) 주입하는 객체를 쉽게 교체할 수 있고, 쉽게 확장할 수 있다. 역으로, 주입되는 객체 역시 SomeDataRelatedProtocol이라는 프로토콜에 의존성을 가지기 때문에 오히려 역의 의존성 체계가 설계된다. 즉 구체적인 객체가 추상적인(=프로토콜 타입) 요소에 의존하게 되는 의존 관계 역전 원칙을 따르게 된다. MVVM 아키텍처는 왜 쓰는걸까? 아니 아키텍처는 왜 나누는 건가? 작은 앱을 혼자서 만들어간다면 ViewController 안에 데이터 모델링 코드, Network API 코드, 뷰를 그리는 코드 등을 모두 구현하는 것도 나쁘지 않을 수 있다. 어차피 ViewController가 어떻게든 접근해야 하는 객체나 뷰를 만들기 때문이다.
한결
[Swift - UIKit] BaseView, APIService 싱글턴 패턴, DispatchGroup 등을 활용한 간단한 영화 평점 매기기 서비스를 구현해보았다.
영화 앱에서는 TMDB API를 활용하여 다음과 같은 페이지를 구현했습니다. 자세한 코드는 아래 깃헙에서 확인 가능합니다. 메인페이지 - 현재 상영작 / 요즘 주목받는 작품 / 역대 최고의 평가를 받은 작품 리스트를 테이블뷰 + 가로 컬랙션뷰 형태로 노출 영화 상세 페이지 - 컬랙션 아이템 터치시 해당 영화 id값을 기반으로 내가 매긴 별점, 줄거리, 출연진, 유사한 영화 리스트가 한 페이지에서 노출 영화 검색 페이지 - 텍스트 필드에서 키워드로 영화를 검색한 결과를 테이블뷰로 노출 (검색 결과가 없을 경우 핸들링) BaseView를 만들어 뷰의 상속 관계 이용하기 UIView를 상속받는 기본적인 BaseView를 만들어서 해당 뷰를 상속받는 뷰 객체들이 미리 정의된 메서드를 오버라이딩하여 UI와 기능을 보다 빠르고 편하게 구축할 수 있게 코드를 작성했다. 프로젝트 코드에서 UI의 레이아웃을 잡거나 서브뷰를 구성한다던지, UI와 관련된 코드를 작성하는 경우에서 반복이 많다. 위의 코드처럼 베이스가 되는 뷰 클래스를 하나 만들어두면, 어떤 메서드에서 레이아웃을 잡고, 어떤 메서드에서 UI 코드를 잡을지 따로 신경쓰지 않아도 된다. (제약을 미리 하나 만들어두는 느낌이라 Protocol을 활용하는 것과 비슷하다.) 자주 사용되는 UI 객체 역시 BaseView를 만들어서 활용했다. 특히, 라벨이라던지 박스 형태의 컴포넌트는 프로젝트 페이지별로 많이 활용되기 때문에(=인스턴스를 생성할 때가 많음) 베이스가 되는 클래스를 만들어 두는 것이 정말 유용했다. 인스턴스로 생성된 뷰 객체들은 외부에서 되도록이면 속성을 직접 건드리지 않고, 내부에 구현된 메서드를 이용할 수 있도록 제어자를 만드는 것에 집중했다. BaseLabel.swift BaseBoxItem.swift ViewController에서 컨트롤할 뷰 객체 전체를 감싸는 mainView를 분리하였다. VC에서는 해당 전체 뷰 객체의 인스턴스를 만들어서 loadView 메서드 내부에서 기존의 root view를 mainView로 대치하여 사용했다. 즉, 커스텀한 뷰로 nil 상태의 root view를 대체해주는 것이 loadView의 역할인 것이다. loadView 메서드를 오버라이드 할 때 주의해야 할 점은 (어떻게 보면 당연한 이야기 일 수 있는데) super의 loadView 구현부를 상속받지 않는 것이다. ViewController에서 View의 레이아웃이나 UI 코드에 대한 관심사를 분리시킬 수 있어 아주 유용해 보인다. 다만, 뷰 컴포넌트들의 구조가 복잡해지거나, 한 페이지에 정말 다양한 뷰 요소들이 들어가는 앱이라면 클래스의 뎁스를 많이 두는 것이 코드를 유지보수 하는 관점에서 좋을지는 계속 고민해볼 필요가 있을 것 같다.