Swift | 빠르게 빠르게 작성하고 기억하기

iOS 개발자로 성장하면서 빠르게 기록해야 하는 내용을 담아냅니다.
한결
Swift에서 캐시(Cache)
일반적으로 캐시는 앱에서 자주 사용되는 데이터를 어딘가에 미리 저장해두어서 빠르게 접근 가능하도록 하는 저장소 역할을 한다.
아무래도 캐시를 이용하는 이유를 생각해보면 이미지를 다루는 경우가 많을 것 같은데 (내 생각 정리)
1.
앱은 웹보다 화면 크기가 작기 때문에 이미지를 쓰는 경우가 많다. (가독성의 문제)
2.
이미지를 서버 통신으로 매번 조회하면 앱에 대한 부하가 너무 걸린다.
3.
disk, memory 단에 이미지 데이터를 저장해두었다가 활용할 수 있다면 불러오는 속도도 빠르고, 불필요한 네트워크 리소스도 들지 않는다.
Swift는 Memory Storage의 NSCache로 기본적인 캐싱을 제공한다. 앱이 살아있는 동안 유지되고, 접근이 빠르다.
키-벨류 값으로 저장된다.
Filemanager를 통해 Cache Disk를 만들 수도 있다. 앱이 inactive로 들어가더라도 샌드박스단에 저장되기 때문에 삭제되지 않는다.
Memory Cache보다 접근은 늦을 수 있지만 네트워크로 불러오는 것보다는 빠르다.
Reaction
Comment
Share
한결
IAP - 인앱 결제에 대한 기본
PG와 IAP를 비교한다면,
실물이 있는 제품을 앱에서 구매한다고 한다면 PG를 통해서 결제를 진행한다. (애플 수수료 정책으로 실제 매출이 잡히기 어렵기 때문)
앱 내부에서 활용되고 (이모티콘 같은) 앱 외부에서 실물이 없다면 인앱결제를 통해 진행한다. (StoreKit 활용)
인앱 결제를 붙인다고 한다면,
개발자 계정을 생성하고 유료 응용 프로그램 계약을 진행한다.
개발한 App의 Appstore Connect에서 앱 내 구입 항목을 만들 수 있다.
소모품/비소모품 여부를 결정한다. 소모품은 포인트 같이 계속 결제할 수 있는 상품이다. 비소모품은 테마/광고제거와 같은 '기능'을 결제하는 상품이다.
인앱 상품에 대한 고유 식별 정보와 제품 ID를 구성한다. (제품 ID는 고유해야 한다.)
어떤 지역에서 인앱 결제를 활성화 할 것인지 설정해주고, 해당 앱 스토어에서 어떤 문구로 어떤 설명으로 보여질 지 결정한다.
Xcode에서 앱 내 구입을 활성화하고 개발을 진행한다.
구매 내역 복구가 있는 경우? → 유저가 Device 자체를 변경하여 앱을 다시 설치한 경우 '비소모품' 기능을 계속 이관시킬 수 있도록 필요하다.
단, 복구의 기준은 AppleID 가 동일해야 한다.
권한/기능을 구독하는 경우? → 구독하는 유저의 device가 애플 기기가 맞는지 (웹, 안드로이드에도 서비스를 하는 경우 기기가 변경되면 어떻게 처리하는지 서버 로직 결정 필요), 정상적인 구독이 맞는지에 대한 영수증 확인 로직이 필요하다.
TestFlight, SandBox를 통해서 실제 결제 없이 인앱 결제 테스트를 한다. (Sandbox 계정)
See more...
Reaction
Comment
Share
한결
swift의 inout 키워드
swift에서 함수의 파라미터는 기본적으로 값을 복사하여 집어넣는 call by value이다.
즉, 함수의 인자로 어떤 값을 넣을 때, 그 값은 불변성을 지닌다. (함수 내부에서 인자로 들어온 값에 어떤 값을 새로 할당하거나 변경할 수 없다는 말!)
실제로 변경을 하거나 새로 할당하려고 하면, 인자가 let 키워드로 된 변수이기 때문에 수정이 불가하다 라는 에러를 볼 수 있다.
함수 내부에서 인자로 받은 값을 변형하겠다는 신호로 inout 키워드를 붙여줄 수 있다.
함수 정의부에서 특정 파라미터의 타입 선언 전에 inout을 붙이면, 그 인자는 변경이 가능한 인자가 된다.
함수를 호출하는 부분에서는 &키워드로 인자가 inout한 타입이라는 것을 명시해준다.
func someFunc(_ num: inout Int) { // num은 내부에서 값의 할당, 변경이 가능 } var a = 0 someFunc(&a)
inout으로 들어가는 인자는
우선 함수 파라미터로 들어가는 것이기 때문에 값에 대한 복사가 진행된다.
함수 내부에서 복사된 값을 업데이트 하거나 새롭게 할당하는 과정이 진행된다.
함수가 종료되면 내부에서 수정/재할당 된 값을 외부의 참조하는 값에 다시 할당한다. (마치 여기서 값의 참조가 일어난 것처럼 보인다. call by reference)
in 할 때 copy가 발생하고, out 할 때 새로운 값의 copy가 발생해서 in-copy out-copy를 줄여 inout이 된다.
Reaction
Comment
Share
한결
동시성의 기본
동시성이라는건 왜 필요할까?
하나의 프로덕트에서 여러 프로그램이 여러 프로세스를 동시에 처리(멀티 프로세스)할 수 있는 환경이 필요할 수 있기 때문에
ex. 나의 아이폰에서 음악앱을 켜 음악을 들으면서 + 카톡도 보내고 + 네이버에 검색도 하는 환경이 독립적이면 너무 귀찮다.
운영체제는 메모리를 이용해서 여러 프로세스를 동시적으로 처리하는 환경을 갖춘다.
메모리에서는 하나의 프로세스안에서 코드/데이터/힙 영역 + 스레드라고 하는 스택을 가진 태스크 처리 영역이 구성된다.
스레드에 등록된 태스크들을 처리해가면서 앱이라고 하는 프로그램의 메인 프로세스를 돌린다.
동시적으로 스레드가 일을 처리하면 어떤 문제가 날 수 있을까?
Data Race
여러 스레드에서 동시에 동일한 값에 접근할 떄 의도하지 않게 값이 훼손될 수 있는 상황
swift에서는 actor 객체를 통해서 의도적인 데이터 접근의 임계치를 만들어 비동기 환경에서 하나의 작업 스레드만 데이터에 접근할 수 있는 환경을 만든다.
actor를 쓰지 않는다면, 의도적으로 하나의 스레드만 한 번에 접근할 수 있도록 신경을 써줘야 한다.
Mutex 방식으로 Lock이라고 하는 데이터 접근 권한을 관리하여 각기 다른 스레드가 독립적으로 접근하도록 제어할 수 있다.
Mutex 방식에서는 서로 다른 스레드가 Lock을 취득하려고 하는 SpinLock 상태가 발생할 수 있고,
특정 스레드는 무한히 작업을 대기하는 Starvation 기아 상태가 발생할 수 도 있다.
See more...
Reaction
Comment
Share
한결
Swift에서의 Unit Test의 기본2 - 비동기로 동작하는 코드의 테스트
기본적으로 XCTest는 동기적은 환경의 테스트를 지원한다.
그 이유는 모든 코드는 기본적으로 비동기적인 상황을 만들지 않은다면, 라인바이라인으로 동기적으로 동작하기 때문이다.
그래서, 비동기로 동작하는 코드를 테스트하기 위해서는 테스트 환경에서 비동기 테스크가 진행될 수 있는 환경을 먼저 만들어줘야 한다.
공식문서에 확인해보면 2가지 방법을 제시한다고 한다. (swift concurrency base, closure base)
swift concurrency 환경에서는 테스트 코드 자체를 async throws로 만들어서 내부 동작이 async하게 동작하다는 것을 인식시킬 수 있다.
// cc. apple developer swift documentation example code func testDownloadWebDataWithConcurrency() async throws { // Create a URL for a webpage to download. let url = URL(string: "https://apple.com")! // Use an asynchronous function to download the webpage. let dataAndResponse: (data: Data, response: URLResponse) = try await URLSession.shared.data(from: url, delegate: nil) // Assert that the actual response matches the expected response. let httpResponse = try XCTUnwrap(dataAndResponse.response as? HTTPURLResponse, "Expected an HTTPURLResponse.") XCTAssertEqual(httpResponse.statusCode, 200, "Expected a 200 OK response.") }
비동기 로직이 completionHandler와 같이 탈출 클로저 내부에서 동작한다면, XCTest가 제공하는 expectation 인스턴스, wait, fulfill 메서드를 통해 비동기 환경을 만들 수 있다.
func test_NetworkManager_FetchSuccess() { // given correctDrwNum = 1125 expectation = .init(description: "정확한 회차 정보에 대한 비동기 테스트") // when sut.fetchLotto(correctDrwNum) { [weak self] lotto, error in guard let self else { return } guard let lotto else { XCTAssertNil(lotto, "로또 정보가 없으면 nil 입니다.") expectation.fulfill() return } // then XCTAssertTrue(lotto.bnusNo >= 1, "정확한 회차를 반영하면 보너스 정보가 1~45 사의 숫자로 반환된다.") XCTAssertGreaterThanOrEqual(lotto.bnusNo, 1) // then XCTAssertTrue(lotto.bnusNo <= 45, "정확한 회차를 반영하면 보너스 정보가 1~45 사의 숫자로 반환된다.") XCTAssertLessThanOrEqual(lotto.bnusNo, 45) // end asynchronous expectation.fulfill() } wait(for: [expectation], timeout: 2.0) }
expectaion은 XCTestExpecation의 인스턴스이고, 비동기 작업의 시작과 끝남을 관장하는 역할을 한다.
wait 메서드는 expectaion 인스턴스가 비동기 환경을 얼마나 유지할 지를 결정해준다. timeout 설정으로 얼마나 기다릴지 정해줄 수 있다.
클로저 내부 구문에서는 expectation.fulfill() 호출 시점이 중요하다.
클로저의 탈출 조건을 충족하는 부분에서는 해당 비동기 작업 충족을 해주어야 하기 때문에 그 시점에 fulfill 메서드를 정확하게 호출시켜줘야 한다.
Reaction
Comment
Share
한결
Swift에서의 Unit Test 의 기본
XCode 프로젝트에서 UnitTesting 타겟을 생성하면 XCTestCase를 상속받는 테스팅 클래스가 만들어진다.
Unit Test를 하기 위한 ViewController, ViewModel을 서로 다른 위치의 Test Target에 불러오기 위해서는 @testable이라고 하는 어트리뷰트를 통해 프로젝트를 import 해줄 수 있다.
@testable import TargetProject
물론, targetMembership을 활용해서 프로젝트의 클래스나 어떤 파일을 Test Target에 넣어줄 수 있다.
또한, public 접근자를 통해서 타겟간의 참조가 가능한 제어를 만들 수는 있겠다.
테스트 케이스 클래스에는 4개의 메서드가 기본적으로 셋업된다.
final class SomeTestCase: XCTestCase { // system under test: 테스트 타겟 var sut: ViewController! override func setUpWithError() throws { sut = .init() } override func tearDownWithError() throws { sut = nil } func testExample() throws { } func testPerformanceExample() throws { self.measure { } } }
override되는 두 개의 메서드는 각각의 Test Unit이 테스트를 진행할 때마다 앞, 뒤로 동작하면서 필요한 초기화, 정리를 담당한다.
testExample 메서드가 테스트 로직을 진행한다고 하면,
1.
setUpWithError 구문이 먼저 돌아, 테스트에 필요한 값들을 초기화 해줄 수 있고
2.
testExample의 테스트 로직이 돌고
3.
tearDownWithError 의 정리 구문이 돌아간다.
Node신에서 활용하는 Jest 라이브러리의 beforeEach, afterEach 메서드와 비슷하다.
test 로직을 작성하는 메서드는 기본적으로 test 라는 prefix가 붙는다.
See more...
Reaction
Comment
Share
한결
프로젝트 협업을 위한 이해 높이기
.xcodeproj 파일의 의미
프로젝트를 위한 디렉토리
프로젝트 진행을 위한 연관된 파일들이 모여있는 공간 ⇒ 단일 파일 형태로 보여지는 패키지화
즉, xcode에서 swift 개발을 위한 통합 패키지 파일을 의미한다.
.xcodeproj 파일을 패키지로 확장해서 본다면
xcuserdata 폴더
xcode ide 환경에 대한 개인 설정 정보가 포함됨
.xcscheme 파일을 통해 xcode 개인 설정을 관리할 수 있다.
이 폴더를 gitignore 처리하지 않으면 → 개인 설정이기 때문에 → xcode 설정에 따라서 conflict가 발생할 수 있음
xcworkspace 파일
라이브러리를 관리하는 파일
사실 더 확장해서 말하자면, 다른 프로젝트들을 모아서 우리 프로젝트에 도움이 될 수 있는 어떤 작업 공간을 구축해주는 파일
왜냐하면, 결국 라이브러리도 xcode 프로젝트이니까.
See more...
Reaction
Comment
Share
한결
UDID와 UUID의 차이
UDID: 기기가 가진 유일한 고유값
UUID: 앱에서 생성 가능한 프로젝트별 범용 고유 식별자
Reaction
Comment
Share
한결
[출시 후 Push Notification 적용]
Push Notification
앱이 InActive 상태가 아니더라도 확인할 수 있는 알림의 일종
개발자가 보내는 타이밍, 메시지 등을 미리 지정해서 코드로 빌드해둔 로컬 알림과 다르게
서버를 통해서 매번 다른 시간, 다른 내용으로 알림을 보내도록 APNs에 요청을 보낸다.
로컬/푸시 알림 모두 사용자의 권한 확보가 가장 중요하다.
Firebase Cloud Messaging 서비스로 Push 보내기
1.
결국에는 APNs를 거쳐야 하기 때문에, XCode 프로젝트에서 Push Notification을 지정해줘야 한다. (유료 계정!)
+ background 상태에서도 앱이 노티를 받아야 하기 때문에 background 설정까지 넣어준다.
2.
Push 사용을 위한 인증키를 발급 받는다.
developer.apple.com > 식별자 > key 발급 (.p8 형식의 보안 파일)
.p8 파일의 경우 만료 기한이 정해져 있지 않고, .p12 형태의 인증서 파일 인증서의 유효기간이 정해져 있다.
.p12 인증서는 개발용/배포용 구분이 되어 있고, 개별 앱마다 인증서 발급이 필요하다.
.p8의 경우는 하나의 인증키로 여러 앱에 적용이 가능하다.
See more...
Reaction
Comment
Share
한결
[SwiftUI] NavigationView의 뷰 전환을 우회(?)하는 방법
SwiftUI에서 push, pop 형태로 뷰를 전환하기 위해서 NavigationView 라고 하는 View 구조체를 활용한다.
var body: some View { NavigationView { ScrollView { NavigationLink { PushedView() } label: { Text("이동하기") } } } }
뷰 이동이 발생할 뷰를 NavigationView로 감싸주고, 내부에서 NavigationLink 뷰로 이동 액션을 담당할 뷰 객체를 지정해준다. (web에서 <anchor /> 태그의 역할과 비슷하다.)
UIKit에서 뷰 전환을 위해 UINavigationController로 ViewController를 먼저 감싸줘야 한다는 개념이 SwiftUI에서도 비슷하게 적용된다.
다만, NavigationView로 이렇게 뷰 이동을 진행하는 경우, 이동되는 뷰 (자식뷰)가 이동 액션이 발생하기도 전에 미리 Init 되는 것을 확인할 수 있다.
유저가 이동할 지 안할지 모르기 때문에 NavigationLink에 연결된 뷰를 미리 init시켜 준비시켜두는 느낌이다.
자식뷰 Init 구문에 무거운 API 통신이 있거나, 스크롤뷰 안의 여러 뷰 객체가 모두 자식뷰를 가지고 있는 경우 뷰 이동 전에 과한 init이 발생할 수 있다.
이런 문제를 우회(?) 하기 위해서 뷰의 이동만을 담당하는 WrapperView 구조체를 사이에 둘 수 있다.
struct NavigatingViewWrapper<V: View>: View { var navigatingContentHandler: () -> V var body: some View { navigatingContentHandler() } init(_ navigatingContentHandler: @autoclosure @escaping () -> V) { self.navigatingContentHandler = navigatingContentHandler } }
NavigatingViewWrapper 객체는 init 구문에서 이동시킬 View를 반환하기만 하는 역할을 한다.
자신의 init을 부모 뷰에 맡기는 대신, 실제로 이동되어야 하는 뷰의 init은 실제 이동이 발생한 뒷 시점으로 미뤄줄 수 있다. (진짜 원하던 것)
Reaction
Comment
Share
한결
UIKit 프로젝트에서 SwiftUI 코드로 작성한 화면을 띄우는 방법
iOS13+ 버전에서 활용할 수 있는 UIHostingController를 통해서 some View를 반환하는 구조체 인스턴스를 넣어준다.
UIHostingController는 기본적으로 UIViewController를 상속받고, View 프로토콜을 따르는 객체를 반환하는 인스턴스를 제네릭 타입으로 가진다.
let vc = UIHostingController(rootView: ContentView1()) present(vc, animated: true)
Reaction
Comment
Share
한결
SwiftUI 기본
Source Of Truth
SwiftUI에서는 body라고하는 연산 프로퍼티에서 특정 코드 / 유저의 액션 / 바인딩 된 데이터 에 따라서 연산을 돌려 어떤 의도된 View를 반환한다.
단순하게 내부 뷰 객체에 바인딩 된 액션이 발생한다고 무작정 뷰를 그리는 body의 연산이 일어나는건 아니다. (눌리는 액션이 있는 객체라도 바인딩 된 액션이 없을 수 있으니까)
핵심은,
뷰를 그리는
구조체가 바라보고 있는, 구조체에 묶여있는 어떤 데이터(@State와 같은)에 변경이 있는 경우
body는 정해진 연산을 통해 새로운 어떤 some View를 그려낸다.
그렇기 때문에, 외부의 다른 뷰를 관리하는 객체가 내 상태에 함부로 접근하거나 쉽게 변경하지 못하도록 접근 제어자를 잘 설정하는게 중요하다.
struct SomeView { @State private var number = 0 }
Binding
외부에서 Source Of Truth (State와 같은) 데이터를 주입 받아 참조하고 관리한다.
외부의 값을 읽고, 그 값을 직접 수정해서 참조를 변경시키는 역할을 한다.
PropertyWrapper로 구현되어 있는 값이기 때문에 $ 표시로 값을 바인딩한다.
Reaction
Comment
Share
한결
RXSwift 기본
RX의 기본 컨셉은 우리가 작성한 코드가 앱 안에서 기본적으로 라인바이라인으로 동작하는 것이 아니다에 있다.
💬
ReactiveX, many instructions may execute in parallel and their results are later captured, in arbitrary order, by “observers.”
RX의 컨셉에 따라, 앱 전반적인 동작 상태에서 아래의 과정이 병렬적으로( ~= 비동기적으로) 진행되고, Observer에 의해 연산된 결과가 뷰에 전달된다.
Observable에 맡겨둔 이벤트가 감지되면
Observer에 등록해둔 연산(어떤 로직)이 Stream에 따라 동작하여
결과를 전달한다.
Observable과 Observer의 관계는 기본적으로 subscribe라고 하는 구독 여부에 의해 형성된다.
Observable에 연결된 Observer가 없다면, 이벤트 감지에 따른 어떤 스트림도 발생하지 않기에 Observer 구독이 시작된 시점부터 이벤트에 대한 적절한 처리가 진행될 것이다.
물론 구독 관계가 형성되어 있다가 관계를 끊는 unsubscribe 과정도 진행할 수 있다.
구독 관계는 Observable의 메서드를 통해 형성된다.
onNext: 이벤트를 단계적으로 인식시킨다. emit(배출)된 이벤트마다 해당 메서드가 동작된다.
onCompleted: 이벤트에 대한 처리를 성공적으로 처리한 상태에 호출된다. 모든 onNext 처리가 된 다음 마지막으로 실행된다.
onError: onNext로 인지된 이벤트 처리 도중에 에러가 발생할 경우 호출된다. 에러가 탐지된 경우, onCompleted도 동작하지 않는다.
onCompleted, onError는 당연하겠지만 동시에 호출될 수 없다. 이벤트 구독에 따른 스트림 처리 결과는 성공 / 실패 뿐인 것이다.
See more...
Reaction
Comment
Share
한결
Automatic Reference Count (ARC)
iOS 앱에서 메모리의 힙 영역을 관리하는 프로세스 (참조하고 있는 것의 갯수를 관리하는 방식)
힙 영역에는 참조되어 관리하는 요소들이 코드 영역에서 올라온다. (대표적인 참조형은 클래스의 인스턴스나 클로저가 될 수 있겠다.)
힙 영역에 참조할 것이 생성되면 RC를 증가시키고, 해당 인스턴스의 변수나 참조형이 더 이상 역할을 하지 않는다면 RC를 감소시키고 자체적으로 메모리에서 해제시킨다.
컴파일 타임에 RC에 대한 관리가 된다는 점이 주요점이다. (런타임단에서 관리되는 가비지 컬렉터와 차이가 있다.)
강한 참조
class Person { var name: String init(name: String) { self.name = name print("Person init") } deinit { print("Person deinit") } } var a: Person? = Person(name: "a") // RC + 1 var b = a // RC + 1 a = nil // RC - 1 => RC가 0이 되지 않았음.
위 코드에서 변수 a에 nil을 재할당해도 Person의 인스턴스는 deinit을 하지 않는다. 이건, 변수 b가 a에 담겼던 Person의 인스턴스를 강하게 참조하고 있기 때문이다.
Person 인스턴스를 deinit하려면 b = nil 로 RC를 다 감소시키고, 참조하고 있는 요소를 다 없애줘야 한다. (강함 참조의 경우)
약한 참조
var a: Person? = Person(name: "a") weak var b = a // RC 증가 없음 a = nil // "Person deinit"
weak 키워드로 다른 인스턴스를 참조시키는 경우, 참조 시점에 RC를 증가시키지 않는다.
참조하는 인스턴스가 해제될 경우, 주소값 자체를 지운다.
weak로 선언한 변수의 경우 컴파일 타임에서 nil이 할당될 수 있기 때문에,
- 반드시
변수로(var 키워드) 선언해야 하고(재할당이 될 수 있도록)
- 타입을
옵셔널로 정의해줘야 한다.
weak로 선언한 참조 관계는 참조가 해제되는 순서가 중요할 수 있다.
특정 하나의 클래스 멤버만 다른 참조 타입을 weak로 바라볼 경우 deinit 되는 시점을 잘 체크해야 한다.
See more...
Reaction
Comment
Share
한결
Result Enum Type
실패와 성공의 케이스만 가지는 @frozen된 열거형 타입이다.
@frozen public enum Result<Success, Failure> where Failure : Error { /// A success, storing a `Success` value. case success(Success) /// A failure, storing a `Failure` value. case failure(Failure) .. }
제네릭으로 성공, 실패의 케이스에 사용될 '타입'을 선언해서, associated value로 저장해준다.
네트워크 통신을 하는 API 코드에서 completionHanlder의 인자로 Result Enum Type을 지정해주면, 내부 응답 핸들링 코드에서 편하게 사용해볼 수 있다.
func fetch<T: Decodable> ( of decodable: T.Type, completionHandler: @escaping (Result<T, Error>) -> () ) { URLSession.shared.dataTask(with: someRequest) { data, _, error in if let error { // .failure 케이스에 associated value의 값으로 에러를 담아준다. completionHandler(.failure(error)) return } guard let data else { completionHanlder(.failure(NSError(domain: "No Data", code: 0, userInfo: nil))) return } do { let result = try JSONDecoder().decode(T.self, from: data) completionHanlder(.success(result)) } catch { completionHanlder(.failure(error)) } } }
of: T.Type 을 인자로 둔 이유는, fetch<T> 메서드 자체를 호출하는 환경에서 Result<T, Error> 열거형에 T라고 하는 Decodable한 타입을 맵핑해주기 어려워서이다.
Alamofire의 API를 활용할 때에도 decodeOf 등으로 decoding 할 데이터 타입을 먼저 맵핑해준다.
Reaction
Comment
Share
한결
Observable 패턴
View와 Model을 연결해주는 ViewModel에서 데이터 '값'의 변화를 포착해서 원하는 동작을 만들어내는 클로저를 맵핑하기 위해서 Observable이라고 하는 클래스를 자주 사용한다.
View에서 다루고자 하는 값을 ViewModel에서 관리하고 → Observable한 input 값 설정
View에서는 이 값을 변화시키려고 하는 액션(ex. 텍스트필드 입력, 버튼 터치, 테이블뷰 업데이트 등)을 ViewModel에게 인지시킨다. → Observable한 input 값 변경 + 클로저 맵핑
Model에서는 View에 보여줄 데이터를 가지고 있고(가지고 있다기 보다는 데이터와 관련된 로직을 관리하는) 이 데이터를 View에 전달하기 위해서 ViewModel을 활용한다.
Observable한 output 값 설정
ViewModel에서는 View에서 들어온 액션 + 변경된 값을 기반으로 내부 로직(Model 로직에 기반한)을 돌려 View가 원하는 형태의 값으로 반환해준다. → Observable한 output 값 변경 후 반환
final class Observable<T> { private var handler: ((T) -> ())? var value: T { didSet { // 2. 초기화를 통해 값이 세팅되었지만, // handler가 현재 할당되지 않은 상태라 nil로 평가됨 self.handler?(self.value) } } init(_ value: T) { // 1. Observable 인스턴스 멤버 변수 value에 값이 들어오면서 타입이 결정된다. self.value = value } // 3. Observable 인스턴스로 bind 메서드를 호출 // => 첫 인자로 들어온 값을 클로저 구현부의 로직에 맞게 핸들링 // => 그 핸들링한 로직을 앞으로의 handler 클로저에 맵핑함 func bind(_ value: T, handler: @escaping (T) -> ()) { handler(value) self.handler = handler // 4. vlaue의 값이 바뀌면(->didSet 활성화), // 여기서 맵핑된 클로저 로직이 자연스럽게 돌아감 } }
Reaction
Comment
Share
한결
최적화 수준
Debug
최적화 수준은 낮지만 개발자 친화적인 모드로 관리됨
Incremental 방식 - Swift에서는 각각의 파일 개별적으로 컴파일 진행 (파일 단위로 컴파일을 한 다음 파일간의 관계성을 도출)
Release
최적화 수준이 높음
Whole Module Optimization (전체 모듈 최적화) - 전체 파일을 하나의 파일로 관리하여 파일간의 관계성을 만들어 실행하는 컴파일링 방식
💡
컴파일 최적화의 핵심은?
:
서로의 관계가 연결될 필요가 없다면, 최대한 파일간의 연결성을 차단시키는 것
→ final 키워드 같이, 클래스의 추후 상속을 단절시킬 수 있는 키워드를 잘 활용하자!
→ private 키워드 같이, 외부에서 바로 멤버나 메서드를 건드리지 못하도록 제어하자!
→ *Dynamic Dispatch 상태인 메서드를 Static Dispatch 형태로 설정해보자!
*Method Dispatch: 함수를 어떻게 실행시킬지에 대한 명세
컴파일 단계에서 함수가 확정되는지 vs 런타임 단계에서 함수가 실행되는지에 대한 여부
Dynamic Dispatch - 런타임 단계에서 함수 확정 - class (오버라이딩을 하거나 상속을 시킬 수 있어서)
Static Dispatch - 컴파일 단계에서 함수 확정 - struct, enum 내부에서 정의한 메서드
컴파일 단계에서 어떤 시점에 어떤 함수가 어떻게 실행될 지 정해져서 직접 호출하는 메서드
Reaction
Comment
Share
한결
Swift - queryString에 한글 퍼센트 인코딩하기
특정 외부 API를 통해 데이터를 한글 키워드로 검색할 경우 쿼리스트링에 키워드 자체가 %가 들어간 이상한 형태로 깨지는 경우를 본 적 있을 것이다. 이런 인코딩 방식을 Percent-Encoding 이라고 한다.
URL을 기반으로 네트워크 통신을 하기 위한 또 하나의 규약이라고 볼 수 있다. 미리 정해진 인코딩 방식에 따라 한글도 %가 들어간 특이한 16진수 값으로 맵핑된다.
iOS 앱을 만들어 가면서 분명 한글로 검색하여 외부 데이터를 패칭해와야 하는 경우가 많았다. 그럴 때 queryString에 퍼센트 인코딩을 적용하는 방법을 간단히 기록으로 남긴다.
guard let text = self.mainView.header.searchBar.text else { return } guard let formattingQuery = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return } // 커스텀 API Network Service APIService.manager.fetch(.searching(query: formattingQuery)) { (data: MovieResult) in successLogic(data) } errorHandler: { error in errorLogic(error) }
텍스트 필드로 받아온 String 값이 nil인지 확인한다.
String 값이 있다면 .addingPercentEncoding 메서드로 한 번 인코딩 해주고 API 통신을 하는 코드에 넣어준다.
끗! 아주 간단하다.
Reaction
Comment
Share
한결
URLSession
Request
Configuration
shared ===> delegation 형태로 응답을 받을 수 없음
default ===> delegation 형태로 응답 받을 수 있음
ephemeral
background
Task (어떤 요청 처리인지?)
data get (json, media ..)
upload (formType, content-type ..)
download
Response - 응답을 어떻게 받을거?
completion handler - closure
success/failure 형태로 응답 상태를 한 번에 확인
한 번에 확인할 수 있기 때문에 큰 용량의 데이터를 얼마나 어떻게 받아오고 있는지 캡쳐하기 쉽지 않다. (⇒ delegation 방식을 사용함)
See more...
Reaction
Comment
Share
한결
DispatchQueue.global().async, DispatchGroup 으로 비동기 컨트롤하기
DispatchQueue.global() 로 백그라운드 스레드에서 task들이 동작할 수 있도록 넘겨줄 수 있다.
여기서 동기/비동기(sync/async) 방식을 정해줄 수 있다.
동기 방식으로 넘겨줄 경우, 결국 백그라운드 스레드에서 앞선 작업들이 다 진행되는 것을 main 스레드는 기다려야 하기 때문에, main 스레드가 동기적으로 task를 처리하는 것과 크게 다르지 않다.
비동기 방식으로 넘겨줄 경우, 넘겨준 작업의 시작과 종료를 기다리지 않고 main 스레드가 일을 진행할 수 있다. 다만 다른 스레드의 일이 언제 끝날지 알 겨를이 없다.
그래서, DispatchGroup을 통해서 각각의 DispatchQueue.gloabl().async 작업의 task 시작 시점(enter)과 종료 시점(leave)을 그룹 단위로 ± 할 수 있게 정해줄 수 있다.
DispatchQueue.gloabl().async(group: groupName) { 클로저 } 형태로, 비동기 코드를 그룹으로 쉽게 묶을 수 있다.
func someAsync() { let dispatchGroup = DispatchGroup() dispatchGroup.enter() DispatchQueue.global().async(group: dispatchGroup) { // api 통신과 같은 비동기 코드 실행 dispatchGroup.leave() } dispatchGroup.notify(queue: .main) { // 백그라운드 스레드에서 돌던 dispatchGroup의 비동기 코드가 모두 종료되었음을 // 메인 스레드에 전달해준다. // main 스레드가 처리할 작업 여기서 진행 } }
dispatchGroup.enter() dispatchGroup.leave() 의 호출로 그룹내 특정 비동기 코드가 종료되었음을 감지할 수 있는 장치를 만들어준다.
dispatchGroup.notify(queue: someQueue) 로 특정 스레드에 그룹의 비동기 task가 모두 종료되었음을 알려준다. 보통 main 스레드에서 비동기적 통신에 따른 UI를 그리기 때문에 큐에 .main을 반영해준다.
Reaction
Comment
Share
한결
Project Setting - 외부 Font 사용 설정하기
1.
프로젝트에서 사용하고 싶은 Font 파일을 준비한다.
2.
프로젝트 파일에 Font 파일을 추가한다.
3.
프로젝트 설정 > Build Phase > Copy Bundle Resources 항목에 Font 파일이 잘 등록되었는지 확인한다.
4.
info.plist에 Fonts provided by application 속성을 등록하고, Font 파일 이름을 정확하게 기입한다.
5.
사용하고자 하는 코드에서 정확한 폰트 패밀리 네이밍을 반영해준다.
label.font = UIFont(name: "GmarketSansMedium", size: 22)
*폰트 패밀리에 등록된 폰트의 정확한 이름은 쉬운 반복문으로 사용해볼 수 있다.
UIFont.familyNames.forEach { print("\($0): ", UIFont.fontNames(forFamilyName: $0)) }
Reaction
Comment
Share
한결
UIKit - 쌓여있는 VC를 앱 사용성에 맞게 dismiss 시키고 특정 VC 보여주기
SceneDelegate의 scene 메서드 내부 구현처럼, 앱을 마치 처음 켠 것처럼 만들어버린다.
@objc func logout() { // SceneDelegate 자체를 가져오는 느낌 let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene let sceneDelegate = scene?.delegate as? SceneDelegate let window = sceneDelegate?.window window?.rootViewController = UINavigationController(rootViewController: SignInViewController()) window?.makeKeyAndVisible() }
SceneDelegate에 정의한 window 객체를 불러와서, 새롭게 띄우려고 하는 VC를 갈아끼우는 느낌으로 코드가 동작한다.
viewDidLoad가 다시 돌기 때문에, 앱 전체의 테마를 바꾼다던지 로그인 여부 자체를 변경하는 것 등을 관리할 때 이전에 쌓은 페이지들을 다 dismiss 해주는게 좋을 수 있다.
Reaction
Comment
Share
한결
UIKit - CollectionView 가로 스크롤 UI
커머스 앱이나 배달 앱 등 비슷한 카테고리로 동일한 데이터를 좁은 View 안에서 보여주기 위해서 가로의 끝이 언제인지 알 수 없는 가로 스크롤 UI를 사용하곤 한다. iOS에서는 UICollectionView로 이런 가로 UI를 쉽게 생성할 수 있다.
// SomeViewController.swift class SomeViewController: UIViewController { lazy var horizontalCollection = UICollectionView( frame: .zero, collectionViewLayout: configureCollectionLayout() ) .. override func viewDidLoad() { super.viewDidLoad() view.addSubView(horizontalCollection) horizontalCollection.snp.makeConstraints { make in make.horizontalEdges.top.equalTo(view.safeAreaLayoutGuide) make.height.equalTo(300) } } func configureCollectionLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() let width = UIScreen.main.bounds.width * 0.6 let height = UIScreen.main.bounds.height / 3 layout.itemSize = CGSize(width: width, height: height) layout.scrollDirection = .horizontal layout.minimumLineSpacing = 20 layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) return layout } }
컬랙션뷰를 구현하기 위해서는 delegate, datasource에 대한 위임, 커스텀 Item에 대한 등록 역시 필요하다. (이 부분은 테이블뷰 구현과 동일하다.)
Swift 자체에서 시스템 테이블로 제공하는 형식이 없다보니, 테이블뷰 구현과 다르게 컬랙션 아이템(테이블에서는 셀), 컬랙션 틀 자체에 대한 레이아웃을 랜더링 하기 전에 반드시 설정해줘야 한다.
*설정하지 않고 빌드해보면 아래와 같은 에러를 마주할 수 있다.
"UICollectionView must be initialized with a non-nil layout parameter"
UICollectionViewFlowLayout 클래스로 생성한 인스턴스에서 컬랙션 아이템, 틀에 대한 레이아웃을 지정할 수 있다. (내장 멤버 변수를 조정하여 설정 가능하다.)
itemSize - 말 그대로 각 아이템의 사이즈를 결정해준다.
scrollDirection - 당연히 스크롤 방향이다.
minimumLineSpacing - 컬랙션 뷰 내부에서 아이템들 간의 간격을 결정한다.
sectionInset - 컬랙션 뷰 내부의 아이템 그룹의 Edge에 대한 간격을 결정한다.
위와 같이 설정을 한다면, 아래 첨부된 영상처럼 가로 스크롤이 가능한 UI 구현이 가능하다.
00:07
Reaction
Comment
Share
한결
UIKit - Pagination (a.k.a. 무한 스크롤) - 계속 수정 예정
유저의 스크롤 시점에 따른 데이터 조회를 위해서 iOS에서는 페이지네이션을 기본으로 한다.
웹에서의 페이지네이션과는 그 방법 자체가 조금 다르다. 웹에서는 조회 가능한 데이터셋을 기준에 따라 정말 '페이지'로 구분해서 버튼 형식으로 구분했었던 것 같다. iOS에서의 페이지네이션은 웹에서 '무한 스크롤'이라고 부르는 형태를 페이지네이션이라고 지칭한다.
스크롤을 기반으로 데이터 조회 함수를 호출하는 시점을 조절하는 것이 핵심이다. iOS 화면이 스크롤 되어서 아래의 컨텐츠 뷰 영역이 뜨기 전에 미리 데이터를 fetching 하는 방식이 필요하다.
Off-Set 기반
주의사항: 데이터 fetching 중에, 데이터 전체 갯수에 변동이 (새로운 데이터가 fetching 중간에 갱신되는 경우) 있는 경우 원하는 결과가 되지 않을 수 있다.
Cursor 기반
스크롤 전, 후에 데이터가 어떻게 되는지의 차이를 이용하여 구현하게 된다.
특정 데이터 인스턴스에 Previous, Next의 키로 이전, 이후의 데이터셋을 미리 정해두고 데이터를 fetching 하는 방식이다.
채팅, SNS에서
Reaction
Comment
Share
한결
UIKit - 스토리보드 기반에서 코드 기반으로 시뮬레이터 작업 세팅하기
💡
스토리보드 + 코드 기반에서 코드 기반으로만 iOS 개발 작업을 할 수 있는 세팅 방법을 순서대로 기록합니다.
1. MainStoryboard 파일을 삭제한다.
사실 파일을 삭제할 필요까지는 없지만, 앞으로 사용할 일이 없기 때문에 굳이 남겨둘 이유도 없다.
2.
info.plist 파일에서 Storyboard Name 을 지워준다.
3.
프로젝트 설정 > Build Settings > Info.plist Values 항목에서 스토리보드 항목을 지워준다.
4.
SceneDelegate.swift 파일에서 scene 함수 내부에서 Window 객체에 엔트리로 연결하려고 하는 ViewController 인스턴스를 맵핑해준다.
func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let scene = (scene as? UIWindowScene) else { return } window = UIWindow(windowScene: scene) let entryVC = ViewController() window?.rootViewController = entryVC window?.makeKeyAndVisible() }
Reaction
Comment
Share
한결
Swift - tableView.delegate = self 에서 프로토콜 타입을 할당해야 하는 자리에 self를 할당하는 이유는?
Swift 프로토콜에는 Protocol as Type, Delegation이라는 개념이 적용되기 때문이다.
Protocol as Type
class A: C {} struct B: C {} protocol C { /** - 타입으로의 프로토콜 - Protocol Delegation - self에 특정 프로토콜을 연결시켜줬기 때문에, self 자체는 해당 프로토콜의 타입을 이어받음 */ } class D { // 인스턴스에 프로토콜 타입을 접목해줌 var a:C = A() func _i() { // 다른 클래스의 인스턴스에도 // 동일 프로토콜을 적용받은 다른 클래스를 할당할 수 있음 a = D() } }
Protocol's Delegation
클래스에 Protocol을 연결하여 위임자를 자기 자신의 인스턴스로 할당할 수 있다.
물론 동일 프로토콜을 적용한 다른 클래스의 인스턴스로 위임하는 것도 가능!
class SomeView: UIViewController, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! @override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self } }
여기서는 tableView 라고 하는 프로퍼티의 일을 SomeView의 인스턴스가 직접 위임받아 처리할게! 라는 뜻으로 이해하면 된다.
Reaction
Comment
Share
한결
UIKit - View Class에 프로토콜로 클래스 이름이 할당된 타입 프로퍼티 지정하기
TableView에 커스텀셀을 등록하거나, 다른 Storyboard의 뷰를 연결하기 위해서 identifier 값을 지정하게 된다. 이럴경우 (나의 경우에는) 뷰 클래스 내부에 identifier라는 타입 프로퍼티를 지정해주는 편인데, 매번 클래스를 선언할 때마다 나의 타이핑 오류를 감안하기는 번거롭다. protocol, extension 을 이용하면, 프로젝트 단위로 이런 번거로움을 쉽게 해소할 수 있다.
1.
protocol은 특정 클래스나 구조체 등에 형태의 제약을 걸어주는 역할을 한다.
프로토콜을 적용하는 클래스가 반드시 identifier라는 타입 프로퍼티를 가지게 강제해본다.
protocol ViewClassIdentifier { static var identifier: String { get } }
2.
extension은 특정 클래스나 구조체 등의 기존 역할을 확장해주는 역할을 한다.
강제의 개념이라기 보다는
이런이런 역할도 할 수 있어! 로 기능을 확장하는 느낌이다.
extension에 미리 만들어둔 ViewClassIdentifier 프로토콜을 연결한다.
extension UIViewController: ViewClassIdentifier { static var identifier: String { return String(describing: self); } }
identifier라고 하는 타입 프로퍼티 작성을 강제했기 때문에 extension 선언 시에 반드시 작성해줘야 한다.
연산형 프로퍼티에 따로 getter, setter 를 부여하지 않으면 디폴트로 getter로 역할을 한다.
반환하는 부분에 describing: self 를 통해서 확장하는 View 클래스 자기 자신의 이름으로 값이 맵핑된다. (아주 나이스하다!)
3.
해당 extension이 적용된 View 객체들은 코드 생성시에 해당 프로퍼티를 초기화 하는 과정을 개발자가 직접 거치지 않아도 바로 타입 프로퍼티 형태로 접근이 가능하다.
... tableView.dequeueReusableCell( withIdentifier: ViewCell.identifier, for: indexPath ) as! ViewCell
Reaction
Comment
Share
한결
UIKit - 원하는 방향으로 layer.cornerRadius 부여하기
UIImageView, UIView 등등에 원하는 방향(좌상단, 우상단, 좌하단, 우하단)에만 radius 처리를 하는 방법은 간단하다. 전체에 cornerRadius 값을 주고, 부여하고자 하는 포인트를 masked하면 된다. 뭔가 피그마에서 이미지 틀을 만들고 그 안에 이미지를 마스킹 하는 것과 비슷하다.
아래에 깎으려고 하는 코너의 값 자체를 넣어뒀는데, 사실 외울 필요가 전혀 없다.
생각해보면, 뷰에서 좌측은 X좌표가 뷰의 가장 minimum한 값이고 뷰의 하단은 Y좌표가 뷰의 가장 maximum한 값이기 때문이다. 그래서 머리속으로 깎으려고 하는 코너의 위치가 어떻게 되는지 생각만 하면 된다.
좌상단: .layerMinXMinYCorner
우상단: .layerMaxXMinYCorner
좌하단: .layerMinXMaxYCorner
우하단: .layerMaxXMaxYCorner
UIView.layer.maskedCorners = CACornerMask( arrayLiteral: 위의 깎으려고 하는 layer 코너 값을 ,로 구분해서 넣자 )
Reaction
Comment
Share
한결
XCode StoryBoad 상 세그웨이를 이용한 화면전환이 아닌 코드로 화면 전환하기
왜 코드로?
스토리보드에서 세그웨이를 연결해서 하면 화면간 연결이 편하지 않을까?
⇒ 세그웨이를 끌어다가 연결하는건 편하지만, 연결을 위한 버튼이나 테이블 뷰의 셀 등을 직접 만들어서 화면으로 하나하나 연결해줘야 하는 불편함이 생긴다. 물론 연결되는 화면을 일일이 그려야 한다는 번거로움도 커진다.
화면 전환?
전환되는 방식을 먼저 고려하자 (정답은 없다.)
특정 콘텐츠(메뉴)의 상세 정보 ⇒ 보통 우측에서 Show 방식으로
특정 콘텐츠(메뉴)의 기존과 다른 정보 ⇒ 보통 아래에서 위로 Modal 방식으로
그러면 어떻게?
1.
스토리보드 가져오기
2.
스토리보드 내 전환하려는 화면 가져오기
3.
화면 띄우기
let sb = UIStoryboard(name: "StoryBoardFileName", bundle: nil) let vc = sb.instantiateViewController( withIdentifier: SomeViewController.id ) as! SomeViewController present(vc, animated: true);
See more...
Reaction
Comment
Share
한결
[Swift - 문법]
구조체의 연산 프로퍼티
다른 (저장형) 인스턴스 프로퍼티를 이용해서 말 그대로 '연산'을 담당하는 구조체의 속성.
초기화를 하지 않고, 구조체 자체에서 연산을 하기 때문에 메모리의 힙/스택 영역을 차지하지 않음.
함수(메서드)는 아닌데 마치 함수처럼 작성함.
생성한 인스턴스에서 멤버 프로퍼티를 조회하는 것처럼 사용하면 됨.
let 키워드로 선언은 안되고 무조건 var 키워드로 선언해야 함.
struct BMI { var weight: Double var height: Double private func calc() -> Double { return weight / pow(height / 100, 2) } private func getBMI() -> Bool { return self.calc() > 18.5 } // 연산 프로퍼티 var BMIResult: String { return self.getBMI() ? "과체중" : "정상" } } BMI(weight: 40, height: 140).BMIResult
Reaction
Comment
Share
한결
Swift Extension으로 반복적인 코드 관리하기
Extension은 기존에 존재하는 타입을 확장하여 기능성을 높여준다. (Add functionality to an existing type.)
Class, Struct는 물론이고 Protocol, View에 대한 인스턴스 메서드나 계산이 들어간 인스턴스 프로터피도 새롭게 추가할 수 있다.
extension SomeType { // new functionality to add to SomeType goes here } extension SomeType: SomeProtocol, AnotherProtocol { // implementation of protocol requirements goes here } extension UIViewController { // View ~ }
Reaction
Comment
Share
한결
ATS Policy
HTTP와 HTTPS와 같은 Apple(App) Transport Security 정책을 말한다.
iOS 10+ 에서 적용된 보안 정책으로, 쉽게 생각하면 데이터를 받아오는 URL 구조에서 보안정책을 무시한 내용이 담겨있으면 (ex. http로 통신하는 URL) 앱에서 로드를 거부한다.
물론 프로젝트 plist에서 ATS > Allow Arbitrary Loads 설정을 YES로 강제하면 HTTP 프로토콜 내에서도 통신이 가능하게 허용할 수는 있다.
Reaction
Comment
Share
한결
XIB과 NIB 파일
XIB: XML Interface Builder
XML은 내가, 그리고 모두가 알고 있는 것과 동일하게 그 마크업 언어가 맞다.
XML로 되어 있는 인터페이스를 만드는 도구를 XIB라고 이해하면 쉽겠다.
스토리보드 형태로 되어 있어서 쉽게 인터페이스를 디자인하고 개발할 수 있다. 물론 코드 파일과 연결시켜서 코드베이스로도 통제할 수 있다.
NIB
XIB에서 정의된 인터페이스의 뷰나 액션이 정의된 XML 기반의 컴파일된 파일이다.
쉽게 생각해서 XIB가 컴파일된 파일이라고 보면 된다.
요 아티클에서 알아본 파일 관리 시스템에서 앱 번들 디렉토리에 저장되어, 런타임이 실행되는 시점에 불러와져서 화면을 그린다. 그 화면이 켜질때 불러와진다.
앱 개발에 사용하는 방식은 학습 내용 정리 블로그에 아티클로 정리할 예정이다.
Reaction
Comment
Share
한결
UIViewController + UITableView vs. TableViewController
TableViewController에서 테이블 형태의 뷰를 만들어가면,
이후 앱 디자인이 변경될 경우 더 많은 작업을 해야 할 수 있겠다. 혹은 ViewController자체를 바꿔야 할 수도 있을 것 같다.
또한, 자유롭게 UI 요소를 컨트롤하기 어렵다는 한계를 마주해야 한다.
그래서 보통,
UIViewController 위에다가 UITableView 요소를 올리는 방식으로 개발 작업의 자유도를 높인다.
물론, 이렇게 작업할 경우 상속 관계의 차이가 있어서 테이블 작업을 할 때의 방식이 차이가 날 수 있다. (그냥 난다라고 보면 된다.)
그러면, UIViewController + UITableView 조합으로 코드는 어떻게 작성할까?
1.
UIViewController의 특정 위치에 UITableView를 얹는다. 필요에 따라서 TableView 객체를 Outlet 속성으로 잡는다.
2.
UIViewController 클래스를 상속받은 클래스에 TableView를 다루기 위한 두 개의 프로토콜을 연결한다.
UITableViewDelegate
UITableViewDataSource
3.
viewDidLoad 메서드에서 TableView의 두 프로토콜을 클래스 인스턴스 자체로 설정해준다.
override func viewDidLoad() { super.viewDidLoad tableView.delegate = self tableView.dataSource = self }
See more...
Reaction
Comment
Share
Share
한결
Observable 패턴
View와 Model을 연결해주는 ViewModel에서 데이터 '값'의 변화를 포착해서 원하는 동작을 만들어내는 클로저를 맵핑하기 위해서 Observable이라고 하는 클래스를 자주 사용한다.
View에서 다루고자 하는 값을 ViewModel에서 관리하고 → Observable한 input 값 설정
View에서는 이 값을 변화시키려고 하는 액션(ex. 텍스트필드 입력, 버튼 터치, 테이블뷰 업데이트 등)을 ViewModel에게 인지시킨다. → Observable한 input 값 변경 + 클로저 맵핑
Model에서는 View에 보여줄 데이터를 가지고 있고(가지고 있다기 보다는 데이터와 관련된 로직을 관리하는) 이 데이터를 View에 전달하기 위해서 ViewModel을 활용한다.
Observable한 output 값 설정
ViewModel에서는 View에서 들어온 액션 + 변경된 값을 기반으로 내부 로직(Model 로직에 기반한)을 돌려 View가 원하는 형태의 값으로 반환해준다. → Observable한 output 값 변경 후 반환
final class Observable<T> { private var handler: ((T) -> ())? var value: T { didSet { // 2. 초기화를 통해 값이 세팅되었지만, // handler가 현재 할당되지 않은 상태라 nil로 평가됨 self.handler?(self.value) } } init(_ value: T) { // 1. Observable 인스턴스 멤버 변수 value에 값이 들어오면서 타입이 결정된다. self.value = value } // 3. Observable 인스턴스로 bind 메서드를 호출 // => 첫 인자로 들어온 값을 클로저 구현부의 로직에 맞게 핸들링 // => 그 핸들링한 로직을 앞으로의 handler 클로저에 맵핑함 func bind(_ value: T, handler: @escaping (T) -> ()) { handler(value) self.handler = handler // 4. vlaue의 값이 바뀌면(->didSet 활성화), // 여기서 맵핑된 클로저 로직이 자연스럽게 돌아감 } }
👍