Share
Sign In

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