<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dodo_ios.log</title>
        <link>https://velog.io/</link>
        <description>신비로운 iOS 세계로 당신을 초대합니다.</description>
        <lastBuildDate>Fri, 08 Sep 2023 02:44:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dodo_ios.log</title>
            <url>https://velog.velcdn.com/images/dodo_dev/profile/57313068-36fa-4261-bb7e-4f775528962a/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dodo_ios.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dodo_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[How to reload / update cells in TableView and CollectionView using Diffable DataSource]]></title>
            <link>https://velog.io/@dodo_dev/How-to-reload-or-update-value-type-items-in-TableView-and-CollectionView-using-Diffable-DataSource</link>
            <guid>https://velog.io/@dodo_dev/How-to-reload-or-update-value-type-items-in-TableView-and-CollectionView-using-Diffable-DataSource</guid>
            <pubDate>Fri, 08 Sep 2023 02:44:37 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요! 오늘은 Diffable DataSource를 활용하여 value type 아이템들의 데이터 변경을 어떻게 하는지에 대해 포스팅을 써보려 합니다.</p>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/33667f34-ca83-4411-b87a-1a902bc838e2/image.gif" alt=""></p>
<p>위의 동영상처럼 상세화면에서 하트 인터렉션이 발생했을 때, 리스트 화면에서도 하트 정보가 업데이트 되어야하는 상황으로 예시를 들어볼게요!</p>
<h2 id="reference-타입과-value-타입에-따라-다른-reload-방법">Reference 타입과 Value 타입에 따라 다른 reload 방법</h2>
<p>무슨 말이냐 하면, 참조 타입의 경우엔 스냅샷의 <code>reloadItems(_:)</code> 를 활용하여 지정된 항목을 다시 로드할 수 있지만, 값 유형의 경우엔 <code>reloadItems(_:)</code> 는 작동하지 않으므로 스냅샷 내에서 업데이트 된 항목을 수동으로 교체해야 한다는 것입니다.</p>
<p>바로 코드로 볼게요!</p>
<h2 id="reference-type">Reference Type</h2>
<ol>
<li>Diffable 데이터 소스 생성: <code>ItemIdentifier</code> 타입에 객체를 넣어줍니다.</li>
</ol>
<pre><code class="language-swift">var dataSource: UICollectionViewDiffableDataSource&lt;CommunityCategoryType, StoryNew&gt;!</code></pre>
<ol start="2">
<li>셀 리로딩</li>
</ol>
<pre><code class="language-swift">// 1. IndexPath로 해당 item 찾기
guard let communityStories = dataSource.itemIdentifier(for: indexPath) else { return }

// 2. 기존 객체를 새로운 객체로 업데이트
communityStories = newStory

// 3. 수정을 위한 새로운 snapshot 생성
var newSnapshot = dataSource.snapshot()

// 4. reload
newSnapshot.reloadItems([communityStories])

// 5. apply changes
dataSource.apply(newSnapshot)</code></pre>
<br>

<h2 id="value-type">Value Type</h2>
<p>처음에 언급했듯이, 값 타입의 아이템은 <code>reloadItems(_:)</code> 에서 작동하지 않습니다.</p>
<blockquote>
<p>저도 프로젝트 진행하면서 위의 방법으로는 업데이트가 안되는 거에요.. 왜일까 생각해보니 대부분의 모델이 값 타입인 Struct로 구현이 되어있는데 참조 타입 방식으로 업데이트 하려하니 안되는 거였어요!</p>
<p>간단히 말하면, StoryNew가 value type이라면, communityStories는 StoryNew의 새 인스턴스가 될거고, 스냅샷 내에서 communityStories로 객체를 가리키지 않을거죠? 따라서 newSnapshot에서 communityStories를 다시 로드하려고 하면 <code>“Invalid item identifier specified for reload“</code>라는 이유로 <code>NSInternalInconsistencyException</code> 예외가 발생할거에요.</p>
</blockquote>
<p>그래서 찾은 해경방법은! 스냅샷 내에서 기존의 객체를 새 객체로 교체하면 가능합니다!
<br></p>
<ol>
<li>Diffable 데이터 소스 생성: <code>ItemIdentifier</code> 타입에 <code>String</code> 을 넣어줍니다.</li>
</ol>
<pre><code class="language-swift">var dataSource: UICollectionViewDiffableDataSource&lt;CommunityCategoryType, StoryNew&gt;!</code></pre>
<ol start="2">
<li>셀 리로딩</li>
</ol>
<pre><code class="language-swift">// 1. IndexPath로 해당 item 찾기
guard let communityStories = dataSource.itemIdentifier(for: indexPath) else { return }

// 2. 새로운 스토리 카피를 생성 &amp; 업데이트 
var updatedStories = communityStories
updateStories = newStory 
// updateStories.heartButton.isSelected = true 이런식으로 바꿔도 상관없음.

// 3. 수정을 위한 새로운 snapshot 생성
var newSnapshot = dataSource.snapshot()

// 4. 업데이트 된 객체로 replace
newSnapshot.insertItems([updateStories], beforeItem: communityStories)
newSnapshot.deleteItems([communityStories])

newSnapshot.reloadItems([communityStories])

// 5. apply changes
dataSource.apply(newSnapshot)</code></pre>
<br>

<h4 id="결과물">결과물:</h4>
<br>

<p><img src="https://velog.velcdn.com/images/dodo_dev/post/e340a4e5-bb7f-4851-b9fe-619bbed0196a/image.gif" alt=""></p>
<blockquote>
<p>근데 보면 음.. insert / delete로 업데이트를 해주다 보니 뒤로 가면 스크롤 위치가 계속 바뀌더라구요! </p>
</blockquote>
<p>이러한 불편함을 없애기 위해 찾아낸 방법도 공유해볼게요!</p>
<br>

<h4 id="해결-방법">&lt;해결 방법&gt;</h4>
<h2 id="모델의-identifier를-diffable-data-source의-item-identifier로-활용하기">모델의 identifier를 Diffable Data Source의 Item Identifier로 활용하기!</h2>
<ol>
<li>첫 번째로 해야 할 일은 우리의 StoryNew 구조체에 <strong><code>고유한 식별자를 추가</code></strong>하는 것입니다. 이 식별자를 저는 <strong><code>identifier</code></strong>로 지정하고 <strong><code>UUID()</code></strong>를 이 식별자로 사용할게요!</li>
</ol>
<pre><code class="language-swift">struct StoryNew: Codable, Hashable {
    var identifier = UUID()

    func hash(into hasher: inout Hasher) {
        hasher.combine(identifier)
    }

    static func == (lhs: StoryNew, rhs: StoryNew) -&gt; Bool {
        return lhs.identifier == rhs.identifier
    }
}</code></pre>
<br>

<ol start="2">
<li>그리고 datasource의 item identifier 타입에 StoryNew가 아닌 <code>UUID</code> 으로 바꿔줍니다.</li>
</ol>
<pre><code class="language-swift">var dataSource: UICollectionViewDiffableDataSource&lt;Section, UUID&gt;!</code></pre>
<br>

<ol start="3">
<li>UUID와 해당 StoryNew 객체를 갖고있는 dictionary도 하나 생성해줄게요. 이렇게 함으로써 stories 배열을 루프로 반복하지 않고도 identifier를 사용해 StoryNew 객체를 쉽게 가져올 수 있습니다.</li>
</ol>
<pre><code class="language-swift">var storyDic = [UUID: StoryNew]()</code></pre>
<br>

<ol start="4">
<li>그럼 이제 dictionary에 값을 넣어줘야겠죠. <code>[StoryNew]</code> 를 <code>[(UUID, StoryNew)]</code>로 변환한 후, <code>Dictionary(uniqueKeysWithValues:)</code> 를 사용해서 <code>[UUID : StoryNew]</code>로 변환해볼게요.</li>
</ol>
<pre><code class="language-swift">// convert `[StoryNew]` -&gt; `[(UUID, StoryNew)]`
let tupleArray = stories.map { ($0.identifier, $0) }

// convert `[(UUID, StoryNew)]` -&gt; `[UUID : StoryNew]`
self.storyDic = Dictionary(uniqueKeysWithValues: tupleArray)</code></pre>
<br>

<ol start="5">
<li>storyDictionary가 준비되면 이제 셀 register로 넘어가볼게요. CellRegistration Item 타입에 UUID로 변경해야합니다. 기존과는 달리 <code>모델 객체</code>가 아닌 <code>UUID</code>를 제공하기 때문에 cell registration handler도 살짝 변경이 필요하겠죠!</li>
</ol>
<pre><code class="language-swift">let cellRegistration = UICollectionView.CellRegistration&lt;CommunityCollectionViewCell, UUID&gt; { (cell, indexPath, uuid) in
    // `uuid`를 사용하여 Story 가져오기
    if let story = self.viewModel.storyDic[uuid] {
        cell.setData(story: story)
    }
}</code></pre>
<blockquote>
<p>Cell Registration 핸들러 내에서 주목해야 할 점은 storyDic을 사용하여 해당 스토리 객체를 가져오는 방법입니다.</p>
</blockquote>
<br>

<ol start="6">
<li>마지막은  data source snapshot을 어떻게 채워 업데이트할지 입니다. 이제 스냅샷에 모델 객체를 추가하는 대신 모델 객체 identifier를 스냅샷에 추가해야 합니다.</li>
</ol>
<pre><code class="language-swift">// &lt;Section, UUID&gt;
var dataSource: UICollectionViewDiffableDataSource&lt;CommunityCategoryType, UUID&gt;!

// 스냅샷 items로 모든 `identifier` 추가
snapshot.appendItems(stories.map { $0.identifier }, toSection: categoryType)

dataSource.apply(snapshot, animatingDifferences: true)</code></pre>
<p>이렇게 하면 모델 객체의 식별자를 data source item 식별자로 사용하도록 성공적으로 변환 성공!
<br></p>
<h2 id="자-그러면-이제-제일-중요한-셀-업데이트는-어떻게-할까">자 그러면 이제 제일 중요한 셀 업데이트는 어떻게 할까?</h2>
<p>기존과의 차이점은 data source가 모델 객체 대신 identifier를 제공한다는 것입니다. 따라서 우리는 storyDic을 사용하여 story를 가져와 그에 따라 업데이트를 해야합니다.</p>
<pre><code class="language-swift">func updateItem(selectedId: UUID) {
    var updated = storyDic[selectedId]!

    // story 하트 업데이트하고 그에 따라 `storyDic` 업데이트
    if let isLiked {
        updated.isMyEmpathized = isLiked
        updated.nEmpathies = likeCount ?? (updated.nEmpathies ?? 0) + (isLiked ? 1 : -1)
        storyDic[selectedId] = updated
    }

    // 수정을 위해 데이터 소스 스냅샷의 새 복사본 생성
    var newSnapshot = dataSource.snapshot()

    if #available(iOS 15, *) {
        // iOS 15
        // `newSnapshot`에서 업데이트해야 하는 항목의 데이터 지정하기 (`StoryNew.identifier`를 사용)
        newSnapshot.reconfigureItems([selectedId])
    } else {
        // iOS 14
        newSnapshot.reloadItems([selectedId])
    }

    // data source에 `newSnapshot` 적용하여 변경 사항이 컬렉션 뷰에 반영되도록 함
    dataSource.apply(newSnapshot)
}</code></pre>
<br>

<h4 id="끝이에요">끝이에요!</h4>
<p>이렇게 data source item identifier에 모델 객체 대신 고유 식별자 (identifier)를 저장하니 reference / value 타입에서 전부 다 작동되는 것을 확인했습니다!</p>
<h4 id="결과물-1">결과물:</h4>
<p>스크롤 위치가 변경되지 않고 그대로 유지되고 셀 업데이트도 정상적으로 작동 되는게 보이죠~?</p>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/a799486a-42d9-4c2d-986d-86f470921085/image.gif" alt=""></p>
<br>

<blockquote>
<p>여담으로 iOS 15 이상부터 사용 가능한 <code>reconfigureItems(_:)</code> 가 왜 <code>reloadItems(_:)</code> 보다 훨씬 성능이 좋을까요??</p>
</blockquote>
<p>그 이유는 <code>reconfigureItems&quot;</code> 는 변경된 항목만 업데이트하고 새로 그리는 대신, 필요한 변경 사항에만 중점을 둡니다. 반면 <code>reloadItems</code>는 셀을 완전히 다시 그리기 때문에 더 많은 작업을 수행해야 합니다.</p>
<p>결국 <code>reconfigureItems</code> 는 더 작은 범위의 변경 사항을 처리하고 셀 렌더링을 최적화하는 데 중점을 두어 더 빠른 결과를 제공하는 거네요! (iOS 15 이상부터는 reconfigreItems를 사용하자~~😃)</p>
<br>

<p>아래 WWDC 영상에서 Diffable DataSource 관련한 더 많은 개선점들이 나와있으니 보시는 걸 추천드릴게요!</p>
<p><a href="https://developer.apple.com/videos/play/wwdc2021/10252/">https://developer.apple.com/videos/play/wwdc2021/10252/</a>
<a href="https://developer.apple.com/documentation/uikit/uiimage/building_high-performance_lists_and_collection_views">https://developer.apple.com/documentation/uikit/uiimage/building_high-performance_lists_and_collection_views</a> (Sample Code)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[The Composable Architecture 1.0 with SwiftUI (1)]]></title>
            <link>https://velog.io/@dodo_dev/The-Composable-Architecture-1.0-with-SwiftUI-1</link>
            <guid>https://velog.io/@dodo_dev/The-Composable-Architecture-1.0-with-SwiftUI-1</guid>
            <pubDate>Wed, 16 Aug 2023 09:28:47 GMT</pubDate>
            <description><![CDATA[<p>PointFree에서 TCA 1.0이 공식적으로 배포된 후에 최신 버전으로 다시 처음부터 차근차근 설명해주는 무료 세션이 열렸는데 복습할 겸 기록해보고자 합니다.</p>
<h2 id="1-package-dependencies에서-composable-architecture-추가해주기">1. Package Dependencies에서 Composable Architecture 추가해주기.</h2>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/b5fc2920-6dc8-4697-b737-446906c95b5a/image.png" alt=""></p>
<h2 id="2-contentview-레이아웃-구성">2. ContentView 레이아웃 구성</h2>
<pre><code class="language-swift">struct SectionContentView: View {
    var body: some View {
        // 숫자 텍스트가 가운데에 오게 하고싶어 ZStack 사용
        ZStack {
            Form {
                Section {
                    Button(&quot;Increment&quot;) {
                        // To Do
                    }

                    Button(&quot;Decrement&quot;) {
                        // To Do
                    }
                }

                Section {
                    Button(&quot;Get Fact&quot;) {
                        // To Do
                    }
                }

                Section {
                    Button(&quot;Timer On&quot;) {
                        // To Do
                    }
                }
            }

            // 이 숫자는 &quot;Increment/Decrement&quot; 버튼을 눌렀을 때 값 변경
            Text(&quot;0&quot;)
                .bold()
                .font(.system(size: 50))
        }
    }
}</code></pre>
<img src="https://velog.velcdn.com/images/dodo_dev/post/d96262e7-8432-43dc-b9ae-477f0a2f048a/image.png" width="300">

<br>

<h2 id="3-composable-architecture-적용">3. Composable Architecture 적용</h2>
<p><code>Feature</code> 는 스유의 <code>ViewModel</code> 이라고 생각하면 편할 듯. <code>Reducer</code> 프로토콜을 준수해야 하는데 기능의 <code>State</code>(상태변화), <code>Action</code>, <code>Logic</code>, <code>동작</code>을 캡슐화한다.</p>
<p>일반적인 흐름:</p>
<ul>
<li>사용자가 UI와 상호작용하여 액션이 생성.</li>
<li>액션이 Reducer로 전송됨.</li>
<li>Reducer는 현재 상태와 Action을 받아 새로운 상태를 생성.</li>
<li>새로운 상태는 body를 업데이트 해 UI를 다시 렌더링.</li>
</ul>
<br>

<pre><code class="language-swift">import ComposableArchitecture

struct SectionContentFeature: Reducer {
    struct State: Equatable {
        // 뷰에서 필요한 상태값들을 정의
        var count: Int = 0
        var fact: String? = nil
        var isTimerOn: Bool = false
    }

    // 유저가 UI와 상호작용하는 동작들 정의
    // Side Effect를 반환하는 case도 넣을 수 있음 (밑에서 설명 예정)
    // TCA는 명확함과 간결성을 위해 액션의 이름을 logic-based가 아닌 사용자 인터렉션에 따라 지정하는 것을 선호한다고 강조. 
    // (incrementCounts X -&gt; incrementButtonTapped O)
    enum Action {
        case incrementButtonTapped
        case decrementButtonTapped
        case getFactButtonTapped
        case timerButtonTapped
    }

    var body: some ReducerOf&lt;Self&gt; {
        // 여러개의 reducer를 가질 수 있음.
        // inout 파라미터로 현재 state를 캡처
        // 리턴값은 Effect
        Reduce { state, action in
            switch action {
            case .incrementButtonTapped:
                return .none
            case .decrementButtonTapped:
                return .none
            case .getFactButtonTapped:
                return .none
            case .timerButtonTapped:
                return .none
            }
        }
    }
}</code></pre>
<br>

<h2 id="4-store--withviewstore-생성-및-바인딩">4. Store / WithViewStore 생성 및 바인딩</h2>
<p>뷰는 <code>Store</code> 에 바인딩해서 현재 상태에 접근한다. 이 상태가 변경되면 UI는 자동으로 새로운 상태를 반영하여 업데이트 됨.</p>
<p><code>Store</code> 가 생성되면 액션을 디스패치할 수 있고, <code>Reducer</code> 와의 통신을 처리하면서 상태를 업데이트함.</p>
<p><code>WithViewStore</code> 를 사용하면 코드 가독성을 더 높힐 수 있음. </p>
<ul>
<li>스토어에서 <code>현재 상태 추출</code> -&gt; 뷰에서 상태 데이터를 직접 접근하고 사용할 수 있음.</li>
<li><code>액션 바인딩</code> -&gt; 특정 이벤트 (버튼 클릭 등)가 발생했을 때, store에서 직접 액션을 호출할 수 있음.</li>
</ul>
<p><code>observe</code> 도 중요함. <code>$0</code> 은 전체를 reload 한다는 의미인데 뷰가 커지면 커질수록 양이 많아지면서 버거워짐. $0 대신 <code>observe: \.{state에 있는 프로퍼티 명}</code> 을 넣어주면 전체가 아닌 해당 값만 변경 됨. (나중에 정리 예정)
<br></p>
<pre><code class="language-swift">struct SectionContentView: View {
    let store: StoreOf&lt;SectionContentFeature&gt;

    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            ZStack {
                Form {
                    Section {
                        Button(&quot;Increment&quot;) {
                            viewStore.send(.incrementButtonTapped)
                        }

                        Button(&quot;Decrement&quot;) {
                            viewStore.send(.decrementButtonTapped)
                        }
                    }

                    ...(생략)
                }

                Text(&quot;\(viewStore.count)&quot;)
                    .bold()
                    .font(.system(size: 50))
            }            
        }
    }
}</code></pre>
<pre><code class="language-swift">var body: some ReducerOf&lt;Self&gt; {
    Reduce { state, action in
        switch action {
        case .incrementButtonTapped:
            state.count += 1
            return .none
        case .decrementButtonTapped:
            state.count -= 1
            return .none

        ...(생략)
    }
}</code></pre>
<br>

<h2 id="5-effect-반환">5. Effect 반환</h2>
<p><code>Effect</code> 는 네트워크 요청 등과 같은 비동기적 (async) 혹은 부작용을 가진 작업을 나타내는데 사용된다. 가장 많이 쓰이는 이펙트는 <code>run</code> 이다.</p>
<p>먼저 뷰 UI 구성을 조금 바꿔보자. HStack Button에 타이틀과 api에서 받아온 String을 넣어주도록 바꿈.</p>
<pre><code class="language-swift">struct SectionContentView: View {
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in
            Section {
                Button {
                    viewStore.send(.getFactButtonTapped)
                } label: {
                    HStack {
                        Text(&quot;GetFact&quot;)
                        Spacer()
                        if let fact = viewStore.fact {
                            Text(fact)
                        }
                    }
                }
            }
            ... (생략)</code></pre>
<br>

<ol>
<li>inout 파라미터 state는 비동기 코드에서 바꿀 수 없으므로 <code>캡처리스트</code> 를 생성해줘야 함. </li>
<li>구조체 비동기에서는 값 변경이 안되기 때문에 값을 전달해줄 factResponse 액션을 추가해줌.</li>
</ol>
<pre><code class="language-swift">struct SectionContentFeature: Reducer {    
    enum Action {
        case incrementButtonTapped
        case decrementButtonTapped
        case getFactButtonTapped
        case factResponse(String) // response를 받기위한 액션 추가
        case timerButtonTapped
    }

    var body: some ReducerOf&lt;Self&gt; {
        Reduce { state, action in
            switch action {

            ... (생략)

            case .getFactButtonTapped:
                return .run { [count = state.count] send in  // 캡처
                    let (data, _) = try await URLSession.shared.data(from: URL(string: &quot;http://www.numbersapi.com/\(count)&quot;)!)

                    let fact = String(data: data, encoding: .utf8) ?? &quot;&quot;
                    await send(.factResponse(fact))
                }

            case .factResponse(let fact):
                state.fact = fact
                return .none
            }
        }
    }
}</code></pre>
<img src="https://velog.velcdn.com/images/dodo_dev/post/b913d4a9-c723-4327-97c7-01806b13eca1/image.png" width="300">

<br>

<h2 id="6-cancellable">6. Cancellable</h2>
<p>state의 <code>isTimerOn</code> 값에 따라 타이머를 재생시킬지 종료시킬지 구분.</p>
<pre><code class="language-swift">struct SectionContentView: View {
    var body: some View {
        WithViewStore(store, observe: { $0 }) { viewStore in

            ... (생략)

                    Section {
                        Button(&quot;Timer \(viewStore.isTimerOn ? &quot;Off&quot; : &quot;On&quot;)&quot;) {
                            viewStore.send(.timerButtonTapped)
                        }
                    }
                }
            }            
        }
    }
}</code></pre>
<p>Effect를 <code>캔슬</code> 시키기 위해선 hashable한 identifier가 필요함. Typo나 실수를 방지하기 위해 private enum 타입으로 만들 수 있다.</p>
<pre><code class="language-swift">struct SectionContentFeature: Reducer {    
    enum Action {
        case incrementButtonTapped
        case decrementButtonTapped
        case getFactButtonTapped
        case factResponse(String)
        case timerButtonTapped
        case timerCounts // 타이머 카운트 올라가는 값 전달
    }

    // 캔슬 identifiers
    private enum CancelIdentifiers: CaseIterable {
        case cancelTimer
        ...
    }

    var body: some ReducerOf&lt;Self&gt; {
        Reduce { state, action in
            switch action {

            ... (생략)

            case .timerButtonTapped:
                state.isTimerOn.toggle()

                if state.isTimerOn {
                    return .run { send in
                        while true {
                            try await Task.sleep(for: .seconds(1))
                            await send(.timerCounts) // 비동기에서 값 변경 X
                        }
                    }
                    .cancellable(id: CancelIdentifiers.cancelTimer)
                } else {
                    // cancel effect로 캔슬 가능
                    return .cancel(id: CancelIdentifiers.cancelTimer)
                }

            case .timerCounts:
                state.count += 1
                return .none
            }
        }
    }
}</code></pre>
<br>

<h2 id="7-완성">7. 완성</h2>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/78d1e02a-3c43-40ad-8c5b-bfabbb2e1cbf/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@propertyWrapper와 UserDefaults를 활용한 데이터 저장 방법]]></title>
            <link>https://velog.io/@dodo_dev/propertyWrapper%EC%99%80-UserDefaults%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%80%EC%9E%A5-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@dodo_dev/propertyWrapper%EC%99%80-UserDefaults%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%80%EC%9E%A5-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Mon, 14 Aug 2023 07:59:26 GMT</pubDate>
            <description><![CDATA[<p>기존의 UserDefaults 사용법을 보면 key와 type을 제외하고 get{}, set{} 부분이 아래와 같이 중복되어서 사용되고 있었습니다. </p>
<pre><code class="language-swift">UserDefaults.standard.object(forKey: key.rawValue)
UserDefaults.standard.set(object, forKey: key.rawValue)</code></pre>
<br>

<p>Swift 5.1에서 property wrapper가 새로 도입되면서 이렇게 반복되는 로직들을 프로퍼티 자체에 연결할 수 있게 되었습니다. 이번 포스팅에서는 <strong>@propertyWrapper</strong> 속성을 사용하여 UserDefaults 작업을 간소화하는 방법을 살펴보겠습니다. 이를 통해 코드를 더 깔끔하고 효율적으로 유지 관리하기 쉽게 만들 수 있습니다.</p>
<h1 id="userdefaults-이해하기">UserDefaults 이해하기:</h1>
<p>유저 디폴트는 키-값 저장소로 가벼운 정보와 같은 작은 양의 데이터를 저장하는 데 사용됩니다. 사용이 간편하고 빠른 설정 때문에 종종 간단한 데이터 저장에 사용됩니다. </p>
<pre><code class="language-swift">// UserDefaults에 값 저장
UserDefaults.standard.set(true, forKey: &quot;isDarkModeEnabled&quot;)

// UserDefaults에서 값 꺼내기
let isDarkModeEnabled = UserDefaults.standard.bool(forKey: &quot;isDarkModeEnabled&quot;)</code></pre>
<br>

<h1 id="propertywrapper-소개">@propertyWrapper 소개:</h1>
<p>@propertyWrapper는 저장에 대한 <strong>사용자 정의 동작을 정의</strong>할 수 있게 해줍니다. 이 속성은 코드의 가독성과 유지 관리성을 크게 향상시킬 수 있습니다.</p>
<p>@propertyWrapper를 사용하면 데이터를 UserDefaults에 저장하고 검색하는 과정을 간소화(캡슐화)하여 보일러플레이트 코드와 오류 발생 가능성을 줄일 수 있습니다.</p>
<br>

<h2 id="😃-정의">😃 정의</h2>
<ol>
<li><p>먼저 &#39;UserDefault&#39;라는 구조체에 <strong>@propertyWrapper를 정의</strong>. 
저는 Codable 유형과 함께 사용해야했기 때문에 타입 인자는 제네릭인 &lt;T: Codable&gt;를 사용했습니다.</p>
</li>
<li><p>초기화를 위한 init 값 지정 (초기값은 각자 케이스에 맞게 설정하면 됨.)</p>
<blockquote>
<ul>
<li><strong>&#39;key&#39;</strong>: 값을 유저디폴트에 저장할때 사용할 키</li>
<li><strong>&#39;defaultValue&#39;</strong>: 키에 해당하는 값이 유저디폴트에 없을 경우 사용할 기본 값</li>
<li><strong>&#39;needEncrypt&#39;</strong>: 암호화가 필요한지 flag 값</li>
<li><strong>&#39;isCustomObject&#39;</strong>: 코더블 커스텀 object를 사용할 것인지 flag 값</li>
</ul>
</blockquote>
</li>
<li><p>wrappedValue 정의</p>
</li>
</ol>
<ul>
<li><p><strong>게터 (getter)</strong>
유저디폴트에서 값을 검색하는 역할을 담당합니다. UserDefaults.standard.object(forKey:)를 사용하여 주어진 키에 저장된 오브젝트를 검색합니다. 검색된 오브젝트를 T 유형으로 변환하는데 실패하면, defaultValue를 반환합니다.</p>
</li>
<li><p><strong>세터 (setter)</strong>
유저디폴트에 값을 저장하는 역할을 담당합니다.
UserDefaults.standard.set(newValue, forKey:)를 사용하여 지정된 키 아래에 제공된 newValue를 저장합니다.</p>
</li>
</ul>
<br>

<pre><code class="language-swift">@propertyWrapper
struct UserDefault&lt;T: Codable&gt; {
    let key: UserDefaultKey
    let defaultValue: T?
    let needEncrypt: Bool
    let isCustomObject: Bool
    let storage: UserDefaults = UserDefaults.standard

    init(key: UserDefaultKey, 
         defaultValue: T? = nil, 
         needEncrypt: Bool = false, 
         isCustomObject: Bool = false) {
        self.key = key
        self.defaultValue = defaultValue
        self.needEncrypt = needEncrypt
        self.isCustomObject = isCustomObject
    }

    var wrappedValue: T? {
        // Read value from UserDefaults
        get {
            if needEncrypt {  
                // 암호화 필요할때
                return self.storage.secretObject(forKey: self.key.rawValue) as? T ?? self.defaultValue
            } else if isCustomObject {  
                // 커스텀 오브젝트일때
                guard let data = self.storage.object(forKey: key.rawValue) as? Data else { return defaultValue }
                let session = try? JSONDecoder().decode(T.self, from: data)
                return session ?? defaultValue
            } else {  
                // 기본 hashable 오브젝트일때 (String, Bool, etc)
                return self.storage.object(forKey: self.key.rawValue) as? T ?? self.defaultValue
            }
        }

        // Set value to UserDefaults
        set {
            if needEncrypt {
                self.storage.setSecretObject(newValue, forKey: self.key.rawValue)
            } else if isCustomObject {
                let data = try? JSONEncoder().encode(newValue)
                self.storage.set(data, forKey: self.key.rawValue)
            } else {
                self.storage.set(newValue, forKey: self.key.rawValue)
            }

            self.storage.synchronize()
        }
    }
}</code></pre>
<br>

<h2 id="😃-적용">😃 적용</h2>
<p>위처럼 선언을 해준 후에는 다른 클래스나 구조체를 만든 후 속성을 정의하고 @UserDefault property wrapper로 주석을 달아줍니다. </p>
<pre><code class="language-swift">class UserDefaultManager {
    @UserDefault(key: .loginId, needEncrypt: true)
    static var loginId: String?

    @UserDefault(key: .showMainOnboarding, defaultValue: true)
    static var showMainOnboarding: Bool!

    @UserDefault(key: .counselWriteTemporary, isCustomObject: true)
    static var counselWriteTemporary: WriteHolder?
}</code></pre>
<br>
실제로 사용하는 코드에서는:

<pre><code class="language-swift">if UserDefaultManager.showMainOnboarding {
    UserDefaultManager.showMainOnboarding = false
    let onboardingVC = OnboardingViewController(type: .main)
    onboardingVC.modalPresentationStyle = .fullScreen
    present(onboardingVC, animated: false)
}</code></pre>
<p>기존과는 다르게 프로퍼티에 값을 대입하기만 하면 값을 저장할 수 있게 되었습니다. 값을 가져오는 것도 훨씬 간편해졌죠!</p>
<br>

<h1 id="결론">결론:</h1>
<p>로직을 캡슐화해서 보일러 플레이트 코드를 줄이고 가독성을 향상시키며 앱 전체에서 일관성을 가질 수 있습니다. 기본값 뿐만이 아니라 Codable 유형을 저장해야 할 때도 훨씬 더 편리하게 사용할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Enum String Representations: CustomStringConvertible와 String Raw 값 타입을 활용한 Swift 열거형(Enum) 비교]]></title>
            <link>https://velog.io/@dodo_dev/Enum-String-Representations-CustomStringConvertible%EC%99%80-String-Raw-%EA%B0%92-%ED%83%80%EC%9E%85%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-Swift-%EC%97%B4%EA%B1%B0%ED%98%95Enum-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@dodo_dev/Enum-String-Representations-CustomStringConvertible%EC%99%80-String-Raw-%EA%B0%92-%ED%83%80%EC%9E%85%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-Swift-%EC%97%B4%EA%B1%B0%ED%98%95Enum-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Fri, 11 Aug 2023 06:40:54 GMT</pubDate>
            <description><![CDATA[<p>enum을 사용하면서 CaseIterable, CustomStringConvertible 등의 프로토콜을 채택하는 경우가 종종 있었는데 CustomStringConvertible을 채택해서 사용하는거랑 그냥 String을 채택해서 rawValue로 사용하는거랑 비슷하지 않나? 크게 뭐가 다른가 궁금해서 개념을 확인하게 되었다.</p>
<h1 id="1-customstringconvertible-프로토콜">1. CustomStringConvertible 프로토콜</h1>
<p>&#39;CustomStringConvertible&#39; 프로토콜을 채택함으로써 enum의 각 case에 대해 사용자 정의 문자열 표현을 정의할 수 있다. 이 프로토콜을 채택함으로써, 요구에 맞게 더 자세하고 읽기 쉬운 문자열 표현을 제공할 수 있다!</p>
<ul>
<li>애플 공식 예시:</li>
</ul>
<pre><code class="language-swift">enum Direction: CustomStringConvertible {
    case north, south, east, west

    var description: String {
        switch self {
        case .north: return &quot;북쪽&quot;
        case .south: return &quot;남쪽&quot;
        case .east: return &quot;동쪽&quot;
        case .west: return &quot;서쪽&quot;
        }
    }
}

let east = Direction.east
print(&quot;east: &quot;, east) // 동쪽</code></pre>
<p>위와 같은 구현을 통해 <em>&#39;print(direction)&#39;</em> 또는 <em>&#39;String(describing: direction)&#39;</em> 를 호출하면 각 case에 대해 정의한 사용자 정의 문자열 표현이 반환된다.</p>
<h3 id="추가-예제">추가 예제:</h3>
<p>CustomStringConvertible 은 enum 문자열 표현에 추가 정보를 포함할때 유용하다. 그래서 특정 사용 사례에 맞는 더 구체적인 출력을 만들 수 있다.</p>
<pre><code class="language-swift">enum VehicleType: CustomStringConvertible {
    case car(name: String, year: Int)
    case bike(name: String, year: Int)
    case truck(name: String, year: Int)

    var description: String {
        switch self {
        case .car(let name, let year):
            return &quot;자동차 - 이름: \(name), 제조 연도: \(year)&quot;
        case .bike(let name, let year):
            return &quot;오토바이 - 이름: \(name), 제조 연도: \(year)&quot;
        case .truck(let name, let year):
            return &quot;트럭 - 이름: \(name), 제조 연도: \(year)&quot;
        }
    }
}

let vehicles: [VehicleType] = [
    .car(name: &quot;Toyota Camry&quot;, year: 2022),
    .bike(name: &quot;Harley Davidson&quot;, year: 2020),
    .truck(name: &quot;Ford F-150&quot;, year: 2021)
]

for vehicle in vehicles {
    print(vehicle)
}

// Outputs:
// 자동차 - 이름: Toyota Camry, 제조 연도: 2022
// 오토바이 - 이름: Harley Davidson, 제조 연도: 2020
// 트럭 - 이름: Ford F-150, 제조 연도: 2021
</code></pre>
<br>

<h1 id="2-string-raw-값-타입">2. String Raw 값 타입</h1>
<p>enum의 case에는 raw value를 할당할 수 있고, 이때 String, Int 등의 hashable 타입을 raw value 타입으로 사용할 수 있다. &#39;String&#39; 형태의 raw value를 사용하면 각 enum case는 해당 case의 raw value를 기반으로 하는 기본 문자열 표현과 연관된다. </p>
<ul>
<li>예시:</li>
</ul>
<pre><code class="language-swift">enum Direction: String {
    case north = &quot;N&quot;
    case south = &quot;S&quot;
    case east = &quot;E&quot;
    case west = &quot;W&quot;
}

let east = Direction.east.rawValue
print(&quot;east: &quot;, east)
// east:  E</code></pre>
<p>이 방식을 사용하면 enum case의 raw string 값을 가져오는 것은 &#39;direction.rawValue&#39; 를 사용하는 것만으로 간단하다.</p>
<br>

<h2 id="-적절한-방식-선택">* 적절한 방식 선택</h2>
<p>둘 다 enum 값이 문자열로 어떻게 표현되는지를 사용자 정의하는 데 유용한 도구이다. 요구 사항에 따라 앱의 필요에 가장 적합한 접근 방식을 선택하면 될 것 같다. 
<br></p>
<ul>
<li>enum case의 문자열 표현에 미세한 제어가 필요한 경우 <strong>&#39;CustomStringConvertible&#39;</strong> 를 사용.</li>
</ul>
<blockquote>
<p>🤔 추가 정보를 활용해 더 구체적인 출력이 필요할때 가독성있게 만들어 줄 수 있을 것 같다.</p>
</blockquote>
<ul>
<li>enum case의 raw 값이 이미 원하는 문자열 표현과 일치하는 경우, <strong>&#39;String&#39;</strong> rawValue 타입을 선택. </li>
</ul>
<blockquote>
<p>🤔 문자열로 쉽게 변환 가능하고 enum과 해당 string 간 간단한 매핑이 필요한 경우엔 이 접근 방식이 편리한 선택인 것 같다.</p>
</blockquote>
<br>

<p>References:
<a href="https://developer.apple.com/documentation/swift/customstringconvertible">https://developer.apple.com/documentation/swift/customstringconvertible</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[키체인에서 CertificateSigningRequest (CSR) 생성 & 인증서 등록]]></title>
            <link>https://velog.io/@dodo_dev/%ED%82%A4%EC%B2%B4%EC%9D%B8%EC%97%90%EC%84%9C-CertificateSigningRequest-CSR-%EC%83%9D%EC%84%B1-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%93%B1%EB%A1%9D</link>
            <guid>https://velog.io/@dodo_dev/%ED%82%A4%EC%B2%B4%EC%9D%B8%EC%97%90%EC%84%9C-CertificateSigningRequest-CSR-%EC%83%9D%EC%84%B1-%EC%9D%B8%EC%A6%9D%EC%84%9C-%EB%93%B1%EB%A1%9D</guid>
            <pubDate>Fri, 11 Aug 2023 04:14:59 GMT</pubDate>
            <description><![CDATA[<h1 id="1-certificatesigningrequest-csr-생성">1. CertificateSigningRequest (CSR) 생성</h1>
<ul>
<li><p><strong>&quot;키체인 접근&quot;</strong> 앱 실행
<img src="https://velog.velcdn.com/images/dodo_dev/post/d47e99dd-f719-442c-b88a-5973ea593caa/image.png" alt=""></p>
</li>
<li><p>상단 메뉴 &gt; 키체인접근 &gt; 인증서 지원 &gt; <strong>&quot;인증 기관에서 인증서 요청...&quot;</strong> 선택 
<img src="https://velog.velcdn.com/images/dodo_dev/post/f0f43b80-421b-47ea-8e42-3c51316ea544/image.png" alt=""></p>
</li>
<li><p>이메일, 이름 입력 &gt; <strong>&quot;디스크에 저장됨&quot;</strong> 으로 선택 후 &quot;계속&quot; 누른 후 저장
<img src="https://velog.velcdn.com/images/dodo_dev/post/0a5cf6d4-57ce-43eb-9f60-5647c1b10acb/image.png" alt=""></p>
</li>
<li><p>저장된 CSR 파일
<img src="https://velog.velcdn.com/images/dodo_dev/post/bef3dc23-8690-4c19-91c7-fc03718cc61c/image.png" alt=""></p>
</li>
</ul>
<br>


<h1 id="2-애플-개발자-페이지에서-certificates-생성-및-추가">2. 애플 개발자 페이지에서 Certificates 생성 및 추가</h1>
<ul>
<li><p>&quot;+&quot; 버튼 눌러서 새로 추가
<img src="https://velog.velcdn.com/images/dodo_dev/post/2d89ab55-1c29-48f1-a70e-9f6da583b0c5/image.png" alt=""></p>
</li>
<li><p>원하는 certificate 선택 후 &quot;continue&quot;
<img src="https://velog.velcdn.com/images/dodo_dev/post/225c80cc-3d26-4379-8cb7-f73569179e17/image.png" alt=""></p>
</li>
<li><p>App ID 선택 후 &quot;continue&quot;
<img src="https://velog.velcdn.com/images/dodo_dev/post/bcb8be1c-2be2-448b-af3c-082943acde2f/image.png" alt=""></p>
</li>
<li><p>위에서 발급받은 CSR 파일 업로드 후 &quot;continue&quot;
<img src="https://velog.velcdn.com/images/dodo_dev/post/9e2b4968-27fe-4df3-bdf6-ee0002c2158b/image.png" alt=""></p>
</li>
<li><p>&quot;Download&quot; 눌러서 저장
<img src="https://velog.velcdn.com/images/dodo_dev/post/199cc50d-2dd1-47ec-9e93-71031c23ecd2/image.png" alt=""></p>
</li>
</ul>
<br>

<h1 id="3-키체인에서-인증서-등록">3. 키체인에서 인증서 등록</h1>
<ul>
<li><p>키체인 접근 &gt; 인증서 오른쪽 클릭 &gt; <strong>&quot;정보 가져오기&quot;</strong>
<img src="https://velog.velcdn.com/images/dodo_dev/post/f247bda5-8b02-4b54-87d9-64df4d495000/image.png" alt=""></p>
</li>
<li><p>신뢰 &gt; 항상 신뢰로 변경
<img src="https://velog.velcdn.com/images/dodo_dev/post/3a99c614-e3ed-4514-b456-fdb3d57190e7/image.png" alt=""></p>
</li>
<li><p>다시 인증서 다운받을라면 <strong>인증서, 개인키</strong> 두개 항목 전부 선택후 내보내기 해야함
<img src="https://velog.velcdn.com/images/dodo_dev/post/a322360a-71bd-44bf-aed4-34d3086d2042/image.png" alt=""></p>
</li>
</ul>
<br>

<h2 id="이제-필요한-경우에-다운받은-p12-사용하면-된다">이제 필요한 경우에 다운받은 .p12 사용하면 된다.</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[NSMutableAttributedString - 이미지가 들어간 attachment를 가진 문자열 속성 지정]]></title>
            <link>https://velog.io/@dodo_dev/NSMutableAttributedString-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EB%93%A4%EC%96%B4%EA%B0%84-attachment%EB%A5%BC-%EA%B0%80%EC%A7%84-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%86%8D%EC%84%B1-%EC%A7%80%EC%A0%95</link>
            <guid>https://velog.io/@dodo_dev/NSMutableAttributedString-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EB%93%A4%EC%96%B4%EA%B0%84-attachment%EB%A5%BC-%EA%B0%80%EC%A7%84-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%86%8D%EC%84%B1-%EC%A7%80%EC%A0%95</guid>
            <pubDate>Thu, 10 Aug 2023 09:09:57 GMT</pubDate>
            <description><![CDATA[<p>UILabel의 trailing이 아닌 <strong>문자열의 끝 부분에 이미지를 배치</strong>하려면 어떻게 해야할 지 알아봅시다. </p>
<blockquote>
<p>NSAttributedString을 사용하여 문자열과 이미지를 조합한 후 UILabel에 설정해야 합니다.</p>
</blockquote>
<img src="https://velog.velcdn.com/images/dodo_dev/post/63e26a11-19d6-4c93-97d6-472ecef8eec0/image.jpg" width="300">

<br>

<ol>
<li><p>image가 들어간 attachment를 가진 NSAttributedString 생성</p>
<pre><code class="language-swift">extension NSAttributedString {
 static func attachAttributed(_ bounds: CGRect, attach image: UIImage?) -&gt; NSAttributedString {
     let attachment = NSTextAttachment()
     attachment.image = image
     attachment.bounds = bounds
     return NSAttributedString(attachment: attachment)
 }
}</code></pre>
<br>
</li>
<li><p>NSMutableAttributedString로 원하는 문자열 속성 지정</p>
<pre><code class="language-swift">private func imageAttributedString(string: String, isFree: Bool, isSelected: Bool) -&gt; NSAttributedString {
 let attributedString = NSMutableAttributedString(string: string, attributes: [.font: Fonts.AppleMedium.of(size: 14)])

 // ... (생략)

 attributedString.append(
     .attachAttributed(
         CGRect(x: 0,
                   y: (Fonts.AppleMedium.of(size: 14).capHeight - 20) / 2,
                width: 50,
                height: 20),
         attach: UIImage(named: &quot;icFreeVod&quot;)
     )
 )

 return attributedString
}</code></pre>
<p>y값에 UILabel의 height에서 이미지의 height를 뺀 값에 2를 나눈 값을 넣어줍니다.</p>
</li>
</ol>
<br>

<ol start="3">
<li>UILabel에 적용<pre><code class="language-swift">var title = video.vodTitle ?? &quot;&quot;
title.append(&quot;  &quot;) // 타이틀이랑 뱃지 사이에 간격 주려고
titleLabel.attributedText = imageAttributedString(
 string: title, 
 isFree: video.isPreview ?? false, 
 isSelected: isSelected
)</code></pre>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compositional Layout으로 가로 세로 둘 다 스크롤 되게 만드는 방법]]></title>
            <link>https://velog.io/@dodo_dev/Compositional-Layout%EC%9C%BC%EB%A1%9C-%EA%B0%80%EB%A1%9C-%EC%84%B8%EB%A1%9C-%EB%91%98-%EB%8B%A4-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EB%90%98%EA%B2%8C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@dodo_dev/Compositional-Layout%EC%9C%BC%EB%A1%9C-%EA%B0%80%EB%A1%9C-%EC%84%B8%EB%A1%9C-%EB%91%98-%EB%8B%A4-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EB%90%98%EA%B2%8C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Thu, 10 Aug 2023 02:58:32 GMT</pubDate>
            <description><![CDATA[<h2 id="q-가로와-세로-둘-다-스크롤-할-수-있는-컬렉션-뷰는-어떻게-만들까">Q. 가로와 세로 둘 다 스크롤 할 수 있는 컬렉션 뷰는 어떻게 만들까?</h2>
<h4 id="ans-compositional-layout으로-만들-수-있을-것-같은데-한번-만들어-보자">Ans) Compositional layout으로 만들 수 있을 것 같은데.. 한번 만들어 보자.</h4>
<blockquote>
<p>문제점: </p>
</blockquote>
<p>Compositional Layout에서 Section을 가로로 스크롤링 되게 만들면 될 줄 알았는데 section이 계속 vertical로 쌓였음 ㅠㅠ </p>
<p>구글링 서치를 계속 해보다 아래 링크에서 section을 가로로 스크롤링 하는 방법을 찾아똬..!!</p>
<p><a href="https://stackoverflow.com/questions/56963187/horizontally-scrolling-multiple-sections-with-uicollectionviewcompositionallayou">https://stackoverflow.com/questions/56963187/horizontally-scrolling-multiple-sections-with-uicollectionviewcompositionallayou</a></p>
<br>

<p>코드로 봐보자~</p>
<pre><code class="language-swift">// 1. Configure layout section
private func testLayout() -&gt; NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)

    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(145))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

    let section: NSCollectionLayoutSection = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .continuous

    return section
}

private func createLayout() -&gt; UICollectionViewLayout {
    // 2. Set scroll direction for layout section
    let config = UICollectionViewCompositionalLayoutConfiguration()
    config.scrollDirection = .horizontal

    // 3. My section provider
    let sectionProvider = { [weak self] (sectionIndex: Int, environment: NSCollectionLayoutEnvironment) -&gt; NSCollectionLayoutSection? in
        return self?.testLayout()
    }

    let layout = UICollectionViewCompositionalLayout(
        sectionProvider: sectionProvider, 
        configuration: config
    )

    return layout
}</code></pre>
<br>

<p><strong>UICollectionViewCompositionalLayoutConfiguration</strong> 에서 section layout 가로 스크롤링을 설정할 수 있다!</p>
<p>그러고 빌드 해보니 섹션이 horizontal로 잘 들어감!!</p>
<h3 id="-한가지-더">*** 한가지 더!</h3>
<p>여기까지만 하면 섹션 스크롤링이 continuous하게 됨. paging 애니메이션은 어디서 넣는걸까!?
.
.
.
그냥 컬렉션 뷰 내에서 <em><strong>isPagingEnabled = true</strong></em> 로만 설정해 주면 끝~~ 😀</p>
<pre><code class="language-swift">private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()).then {
    ...(생략)
    $0.isPagingEnabled = true
}</code></pre>
<br>

<h2 id="완성본">완성본:</h2>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/488aff50-0a44-4b05-9eeb-04f974409d4b/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS Compositional Layout & Diffable Datasource로 홈 화면 리팩토링 및 성능 개선]]></title>
            <link>https://velog.io/@dodo_dev/iOS-Compositional-Layout-Diffable-Datasource%EB%A1%9C-%ED%99%88-%ED%99%94%EB%A9%B4-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@dodo_dev/iOS-Compositional-Layout-Diffable-Datasource%EB%A1%9C-%ED%99%88-%ED%99%94%EB%A9%B4-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-%EB%B0%8F-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Mon, 07 Aug 2023 05:29:15 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요. 마인드카페 JP 앱을 새로 개발 중에 기존의 홈 화면을 iOS 13부터 도입한 Compositional Layout과 Diffable DataSource를 활용하여 리팩토링 하는 작업을 진행해보았습니다. 그 결과, 메모리 사용량이 약 68.71% 개선되었고 이에 대한 경험과 결과를 공유해보겠습니다.</p>
<h1 id="문제인식">문제인식</h1>
<p>기존의 홈 화면에서는 데이터 및 뷰 업데이트 시 많은 메모리 사용과 성능 문제가 발생했습니다. 특히 뷰가 점점 늘어나면서 큰 데이터셋을 처리하고 동적인 레이아웃을 관리하는 데 어려움을 겪었습니다. 원인은 <strong><code>UITableView</code></strong> 및 <strong><code>UICollectionView</code></strong>의 nested 구조로 구성된 홈 화면의 구조 때문이라고 생각했습니다. 이유는 아래와 같습니다.</p>
<ol>
<li>Nested 뷰는 매우 복잡하고 렌더링 및 이벤트 처리에 추가적인 layer를 추가하면서 성능에 영향을 줄 수 있다.</li>
<li>각 뷰는 메모리 할당이 필요한데 nested 뷰는 더 많은 메모리를 소비한다. 따라서 뷰가 적었을 때는 괜찮았지만 현재 시점에서는 불필요한 메모리 낭비가 발생할 우려가 있다.</li>
<li>레이아웃 크기 조정하는 부분에서도 많이 까다롭고 업데이트 하는 시점이 불확실해 디버그와 수정에 시간을 많이 투자해야한다.</li>
</ol>
<h1 id="솔루션-도입">솔루션 도입</h1>
<h3 id="compositonal-layout">Compositonal Layout</h3>
<p>iOS 13 이후 도입된 시스템으로, 복잡한 레이아웃을 구성하고 관리하기 쉽게 펼칩니다. 하나의 컬렉션 뷰로 다양한 레이아웃을 구성할 수 있고 빠른 속도의 장점이 있습니다.</p>
<h3 id="diffable-datasource">Diffable DataSource</h3>
<p>Hashable 기반으로 동작하며 기존의 DataSource와 reloadData()를 호출하는 것보다 데이터 관리와 업데이트 프로세스를 간소화하여 더 효율적이고 유지 관리 가능하며 시각적으로 매력적인 사용자 인터페이스를 구축하는 데 도움을 줍니다.</p>
<h4 id="장점">장점:</h4>
<ul>
<li><p><strong>스냅샷 기반 접근</strong>:</p>
<ul>
<li>IndexPath 대신 Unique identifier (Hashable을 준수) 를 사용하여 데이터 상태의 스냅샷을 생성. 이러한 스냅샷 기반 특성은 실행 취소/다시 실행 기능, 다른 데이터 상태 저장 및 복원을 단순화 함.<br></li>
</ul>
</li>
<li><p><strong>자동 애니메이션</strong>:</p>
<ul>
<li><p>apply()를 사용하여 snapshot에 데이터 상태를 반영하여 UI 업데이트.</p>
<p>  기존에는 reloadData()를 호출해야 했는데 그럼 모든 셀을 다시 그리며 애니메이션이 끊겨 부자연스럽게 변경이 됨. 
  snapshot은 데이터 변경을 스스로 파악해 자동으로 처리 → 부드러운 전환 효과로 사용자 경험 향상.   </p>
<br>
</li>
</ul>
</li>
<li><p>⭐️ <strong>아래와 같은 기존의 synchronization crash 발생을 예방.</strong></p>
<ul>
<li>Centrailized Truth를 사용해 데이터 업데이트 시 크래시가 발생할 경우가 없음.</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">// UI와 DataSource간의 truth가 맞지않을때 크래시 오류

*** Terminating app due to uncaught exception 
‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number 
of sections. The number of sections contained in the collection view after 
the update (10) must be equal to the number of sections contained in the 
collection view before the update (10), plus or minus the number of sections 
inserted or deleted (0 inserted, 1 deleted).’
***</code></pre>
<br>

<h1 id="😃-마인드카페-jp-홈-레이아웃-구현">😃 마인드카페 JP 홈 레이아웃 구현</h1>
<br>

<h3 id="grid-section-category-구현">Grid Section (Category) 구현</h3>
<img src="https://velog.velcdn.com/images/dodo_dev/post/d2f82092-4cf7-4e9b-8e98-63d779f856d3/image.jpg" width="30%" height="30%">

<p>카테고리 섹션은 group에 5개의 item을 가지고 있고 화면 가로 넓이에 맞게 구성되어있습니다.</p>
<pre><code class="language-swift">// item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 0, leading: 4, bottom: 0, trailing: 4)

// group 
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(70))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

// section
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(top: 16, leading: 8, bottom: 16, trailing: 8)
section.interGroupSpacing = 4</code></pre>
<p>Group에 4개의 item이 들어가도록 itemSize를 .fractionalWidth(0.25)로 설정하고 group에는 .horizontal layout으로 적용했습니다.
<br></p>
<h3 id="orthogonal-section-구현-좌우-스크롤-되는-레이아웃">Orthogonal Section 구현 (좌우 스크롤 되는 레이아웃)</h3>
<p>기존에는 홈에서 좌우 스크롤되는 아이템에서 페이징 애니메이션을 적용하려면 scrollViewWillEndDragging() 에서 계산을 따로 해줘야하고, 애니메이션이 뚝뚝 끊겨 부자연스러웠는데 compositional layout에서는 orthogonalScrollingBehavior로 자유롭게 스크롤이 가능합니다.</p>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/cb6e66e8-3e18-40f9-ac36-f5db6466dad9/image.gif" alt=""></p>
<pre><code class="language-swift">// item
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = .init(top: 0, leading: 5, bottom: 0, trailing: 5)

// group 
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(254), heightDimension: .estimated(234))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

// section
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 11, bottom: 16, trailing: 11)
section.supplementariesFollowContentInsets = false

// 좌우 스크롤 설정 값 + paging 애니메이션 선택
section.orthogonalScrollingBehavior = .groupPaging</code></pre>
<br>
group에 원하는 사이즈에 각각 section contentInset 값을 더한 값을 주고 item은 그걸 따르는 fractional 1.0으로 설정했습니다. 

<h4 id="orthogonalscrollingbehavior-종류">OrthogonalScrollingBehavior 종류</h4>
<pre><code class="language-swift">// default behavior. Section will layout along main layout axis (i.e. configuration.scrollDirection)
case none = 0

// NOTE: For each of the remaining cases, the section content will layout orthogonal to the main layout axis (e.g. main layout axis == .vertical, section will scroll in .horizontal axis)

// Standard scroll view behavior: UIScrollViewDecelerationRateNormal
case continuous = 1

// Scrolling will come to rest on the leading edge of a group boundary
case continuousGroupLeadingBoundary = 2

// Standard scroll view paging behavior (UIScrollViewDecelerationRateFast) with page size == extent of the collection view&#39;s bounds
case paging = 3

// Fractional size paging behavior determined by the sections layout group&#39;s dimension
case groupPaging = 4

// Same of group paging with additional leading and trailing content insets to center each group&#39;s contents along the orthogonal axis
case groupPagingCentered = 5</code></pre>
<br>

<h3 id="supplementary-header--footer--decoration-view-구현">Supplementary Header &amp; Footer &amp; Decoration View 구현</h3>
<p>Compositional Layout에서는 section 배경색은 decoration view로 구현 가능합니다. 
<img src="https://velog.velcdn.com/images/dodo_dev/post/489c848c-e8ee-4838-92b1-de7412920293/image.png"></p>
<pre><code class="language-swift">// item, group 관련 코드 생략..

let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)
section.supplementariesFollowContentInsets = false
section.orthogonalScrollingBehavior = .groupPaging

// header
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(43))
let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                                 elementKind: HomeTitleHeaderView.homeHeaderElementKind,
                                                                 alignment: .topLeading)
section.boundarySupplementaryItems = [headerElement]

// Background
let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(elementKind: GrayBackgroundView.elementKind)
section.decorationItems = [sectionBackgroundDecoration]

// Register decoration view
let layout = UICollectionViewCompositionalLayout(section: section)
layout.register(
        GrayBackgroundView.self, 
        forDecorationViewOfKind: GrayBackgroundView.elementKind)</code></pre>
<br>
❗️**주의점:**

<p>header / footer 의 supplementary view는 collectionView 내에서 register()를 해야하고
decoration view는 UICollectionViewCompositionalLayout 내에서 register()를 해야 됩니다.</p>
<p>더 자세한 내용은 <a href="https://velog.io/@dodo_dev/UICollectionViewCompositionalLayout-Background-Decoration-View-%EB%A1%9C-section%EC%97%90-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%9E%85%ED%9E%88%EB%8A%94-%EB%B0%A9%EB%B2%95">여기에</a> 정리해놨습니다.</p>
<br>

<h3 id="header--footer-register">Header &amp; Footer register()</h3>
<p>최근 후기 section 같은 경우엔 header(제목)와 footer(회색뷰)가 같이 있는 경우입니다.
<img src="https://velog.velcdn.com/images/dodo_dev/post/f37628f6-3ca1-4d10-be92-25505c462de1/image.png"></p>
<pre><code class="language-swift">// .. 생략

let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(43))
let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                                 elementKind: HomeTitleHeaderView.homeHeaderElementKind,
                                                                 alignment: .topLeading)

let footerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(19))
let footerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: footerSize,
                                                                 elementKind: UICollectionView.elementKindSectionFooter,
                                                                 alignment: .bottom)

section.boundarySupplementaryItems = [headerElement, footerElement]

// registration

let headerRegistration = UICollectionView.SupplementaryRegistration
            &lt;HomeTitleHeaderView&gt;(elementKind: HomeTitleHeaderView.homeHeaderElementKind) 
    { supplementaryView, kind, index in
            // ... 생략
    }

let footerRegistration = UICollectionView.SupplementaryRegistration
            &lt;GrayBackgroundView&gt;(elementKind: UICollectionView.elementKindSectionFooter) 
    { supplementaryView, kind, index in
            // ... 생략
    }

// 헤더랑 풋터 register 함수를 각각 따로 같이 호출하면 exception 에러남. 
// 한 function 안에서 kind로 비교해서 dequeue 해줘야 함.
viewModel.datasource.supplementaryViewProvider = { collectionView, kind, index in
    if kind == HomeTitleHeaderView.homeHeaderElementKind {
        return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: index)
    } else {
        return collectionView.dequeueConfiguredReusableSupplementary(using: footerRegistration, for: index)
    }
}</code></pre>
<br>
section의 boundarySupplementayItems에 각각 headerElement와 footerElement를 넣어주고, dataSource supplementaryViewProvider에 register() 해줍니다.

<p>(* elementKind는 헤더와 풋터를 구분짓는 string 값인데 구분 잘 해줘야 함!)</p>
<blockquote>
<p><strong>CellRegistration</strong>, <strong>SupplementaryRegistration</strong>, <strong>DequeueConfiguredReusableSupplementary(using:)</strong> 는 iOS 14 이상에서만 가능합니다. </p>
<p>그 이하 버전은 기존처럼 컬렉션뷰 내에 register()를 해주고 <em>dequeueReusableCell(withReuseIdentifier)</em> 를 사용하셔야 합니다.</p>
</blockquote>
<br>

<h1 id="😃-diffable-datasource-구현">😃 Diffable DataSource 구현</h1>
<p>구현부분을 간단한 버전으로 정리해보면,</p>
<pre><code class="language-swift">enum HomeSection: Int, Hashable, CaseIterable, CustomStringConvertible {
    case topBanner
    case category
    case macapick

    var description: String {
        switch self {
        case .topBanner: return &quot;TopBanner&quot;
        case .category: return &quot;Category&quot;
        case .macapick: return &quot;마카&#39;s PICK&quot;
        }
    }
}

enum HomeDataItem: Hashable {
    case topBanner([Banner])
    case category(HomeServiceMenu)
    case macapick(Story)
}</code></pre>
<br>
마인드카페 홈은 약 17개의 section이 있기 때문에 관리를 쉽게 하기 위해 enum으로 생성했습니다. Diffable datasource는 hashable을 준수하기 때문에 section과 item은 hashable을 채택해야합니다. item이 갖고 있는 모델 또한 hashable을 채택하고 있어야 합니다.

<pre><code class="language-swift">var datasource: UICollectionViewDiffableDataSource&lt;HomeSection, HomeDataItem&gt;!
var snapshot = NSDiffableDataSourceSnapshot&lt;HomeSection, HomeDataItem&gt;()

viewModel.datasource = UICollectionViewDiffableDataSource&lt;HomeSection, HomeDataItem&gt;(collectionView: collectionView, cellProvider: { collectionView, indexPath, listItem in
    switch listItem {
    case .topBanner(let banners):
        let cell = collectionView.dequeueConfiguredReusableCell(using: topBannerCellRegistration, for: indexPath, item: banners)
        return cell
    case .category(let homeMenu):
        let cell = collectionView.dequeueConfiguredReusableCell(using: categoryCellRegistration, for: indexPath, item: homeMenu)
        return cell
    case .macapick(let story):
        let cell = collectionView.dequeueConfiguredReusableCell(using: macapickCellRegistration, for: indexPath, item: story)
        return cell
    }
})

// section 순서 세팅
let sections = HomeSection.allCases

// snapshot에 section data 추가
snapshot.appendSections(sections)

// snapshot에 item data 추가
snapshot.appendItems([.topBanner(banners), .category(category)])

// snapshot 반영
datasource.apply(snapshot, animatingDifferences: false)</code></pre>
<br>
Datasource 생성 시, item enum 값에 따라 cell을 return 하도록 구현했습니다. snapshot을 생성하고 section 과 item을 추가한 후 apply()를 호출해 데이터를 반영하여 UI를 업데이트 합니다.

<br>
<br>

<h1 id="👍🏻-이번-리팩토링으로-얻은-output">👍🏻 이번 리팩토링으로 얻은 Output</h1>
<blockquote>
<ol>
<li><strong>복잡성</strong></li>
</ol>
</blockquote>
<p>기존 코드로는 테이블 뷰 셀 안에 컬렉션 뷰를 중첩해야 했지만 UICollectionViewCompositionalLayout으로 하나의 컬렉션 뷰 안에서 여러 layout으로 구현할 수 있었습니다.</p>
<p>Debug View Hierarchy 로 비교해 봤을때도 리팩토링 이후에 상당히 view depth가 줄어든 것을 확인할 수 있습니다.
<br>
리팩토링 전:</p>
<img src="https://velog.velcdn.com/images/dodo_dev/post/f17a7374-afbc-4e80-84cc-e657a0948762/image.png" width="400">
리팩토링 후:

<img src="https://velog.velcdn.com/images/dodo_dev/post/d734f25c-3af6-4fe7-b8a6-69e5879be964/image.png" width="400">

<br>

<blockquote>
<ol start="2">
<li><strong>동적 콘텐츠 크기 조정:</strong></li>
</ol>
</blockquote>
<p>기존에는 cell 높이가 비동기적으로 변경될 경우에 cell이 제대로 보이지 않는 경우가 있었는데 Compositional Layout은 항목, 섹션 및 그룹의 적응적 크기 조정을 지원하면서 더 유동적이고 동적인 UI를 제공합니다. 
또한 스크롤 지점을 유지하기 위해 높이나 contentOffset을 캐싱하여 관리할 필요도 없어 편리했습니다.</p>
<blockquote>
<ol start="3">
<li><strong>효율적인 셀 구성:</strong></li>
</ol>
</blockquote>
<p>cellProvider 클로저를 사용하여 셀을 구성할 수 있으며, 이를 통해 위치, 내용 또는 데이터에 기반하여 셀을 사용자 지정할 수 있습니다. 이는 셀 구성을 간소화하고 데이터와 UI 로직 사이의 깔끔한 분리를 유지하는 데 도움을 줬습니다.</p>
<br>

<h3 id="4-⭐️⭐️⭐️-메모리-사용량-개선-⭐️⭐️⭐️"><strong>4. ⭐️⭐️⭐️ 메모리 사용량 개선 ⭐️⭐️⭐️</strong></h3>
<p>리팩토링 이후 메모리 사용량이 얼마나 개선 되었는지 확인해보기 위해 리팩토링 바로 직전의 운영버전과 리팩토링 된 버전을 동일한 환경과 조건에서 메모리 사용량 측정을 해보았습니다.</p>
<blockquote>
<h4 id="테스트-환경-및-조건">[테스트 환경 및 조건]</h4>
<p><em>Device - iPhone 11 Pro
0~30sec - 홈 화면 진입 후 아무것도 하지 않음
30sec - 하단으로 스크롤 시작
30sec ~ 1min - 최하단 도착 시 아무것도 하지 않음</em></p>
</blockquote>
<br>

<p>리팩토링 전 메모리 점유율 테스트:</p>
<div style="float:left; margin-right:20px;">
<img src="https://velog.velcdn.com/images/dodo_dev/post/b9d7d5d5-517e-42c2-8152-1263adf6c8f1/image.png" width="300">
</div>
<div style="float:left;">
<img src="https://velog.velcdn.com/images/dodo_dev/post/25379782-d022-4c80-8b61-b6fdd575cd43/image.png" width="300">
</div>

<br>

<p>리팩토링 후 메모리 점유율 테스트:</p>
<div style="float:left; margin-right:20px;">
<img src="https://velog.velcdn.com/images/dodo_dev/post/6d7a118b-a119-41d5-9765-574cdc87adcd/image.png" width="300">
</div>
<div style="float:left;">
<img src="https://velog.velcdn.com/images/dodo_dev/post/14b89830-d875-4da4-a4cd-b934644cf643/image.png" width="300">
</div>

<br>

<ul>
<li><p>홈 화면 진입: <em>기존 대비 약 <strong>23.49%</strong> 감소</em></p>
</li>
<li><p>홈 화면 맨 아래까지 스크롤: <em>기존 대비 약 <strong>68.71%</strong> 감소</em></p>
</li>
</ul>
<br>

<p>아직 일본앱 홈 화면에만 적용해 보았지만 성능도 수치상 굉장히 개선된 것을 확인 할 수 있고 앱 크래시도 예방할 수 있어 비정상 종료도 줄일 수 있다는 면에서 추후 한국 앱에서도 고려해보면 좋겠다는 생각이 들었습니다.</p>
<br>

<p>참고자료</p>
<ul>
<li><a href="https://developer.apple.com/videos/play/wwdc2019/220">https://developer.apple.com/videos/play/wwdc2019/220</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2021/10252">https://developer.apple.com/videos/play/wwdc2021/10252</a></li>
<li><a href="https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources">https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources</a></li>
<li><a href="https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views">https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CollectionView Diffable Datasource apply()에서 supplementary header register() exception 오류 해결]]></title>
            <link>https://velog.io/@dodo_dev/CollectionView-Diffable-Datasource-apply%EC%97%90%EC%84%9C-supplementary-header-register-exception-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dodo_dev/CollectionView-Diffable-Datasource-apply%EC%97%90%EC%84%9C-supplementary-header-register-exception-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 04 Aug 2023 02:29:49 GMT</pubDate>
            <description><![CDATA[<h3 id="각-section-별로-header가-있을-수도-없을-수도-있는-collectionview-compositional-layout-을-구현하고-diffable-datasource에서-apply를-해줄-때-계속-아래와-같은-exception-오류가-뜸">각 section 별로 header가 있을 수도 없을 수도 있는 CollectionView Compositional Layout 을 구현하고 diffable datasource에서 apply()를 해줄 때 계속 아래와 같은 exception 오류가 뜸..</h3>
<h3 id="1-첫번째-오류">1. 첫번째 오류:</h3>
<blockquote>
<p>libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception &#39;NSInternalInconsistencyException&#39;, reason: &#39;Invalid parameter not satisfying: self.supplementaryViewProvider || (self.supplementaryReuseIdentifierProvider &amp;&amp; self.supplementaryViewConfigurationHandler)&#39;
terminating with uncaught exception of type NSException</p>
</blockquote>
<p>로그를 대충 읽어보니 supplementaryViewProvider와 supplementaryReuseIdentifierProvider가 만족되지 않는다네. 
아래 코드처럼 collectionView에 헤더뷰 register()를 해줬는데 dequeueReusableSupplementaryView 도 같이 해줘야하나보다.</p>
<p>내 코드 &gt;</p>
<pre><code class="language-swift">    // 1. Configure collectionView
    private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()).then {
        $0.register(TopBannerCollectionViewCell.self, forCellWithReuseIdentifier: TopBannerCollectionViewCell.cellIdentifier)
        $0.register(HomeCategoryCollectionViewCell.self, forCellWithReuseIdentifier: HomeCategoryCollectionViewCell.identifier)
        $0.register(CommunityMainCollectionViewCell.self, forCellWithReuseIdentifier: CommunityMainCollectionViewCell.identifier)

        // Header register
        $0.register(HomeTitleHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HomeTitleHeaderView.identifier)
        $0.backgroundColor = .clear
    }

    // 2. configure header
    viewModel.datasource.supplementaryViewProvider = { [weak self] view, kind, index in
        return self?.collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HomeTitleHeaderView.identifier, for: index)
    }</code></pre>
<p>기존에는 UICollectionViewDelegate의 **&quot;<em>collectionView(:viewForSupplementaryElementOfKind:at:)&quot;</em> **함수 내에서 dequeueReusableSupplementaryView를 작성했다면</p>
<p>Diffable Datasource를 사용할 땐,** <em>&quot;supplementaryViewProvider&quot;</em>** 로 구현할 수 있음.</p>
<h4 id="저러고-다시-앱을-돌려보니-이번엔-또-다른-오류">저러고 다시 앱을 돌려보니 이번엔 또 다른 오류...</h4>
<blockquote>
<p>libc++abi: terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception &#39;NSInternalInconsistencyException&#39;, reason: &#39;the view returned from -collectionView:viewForSupplementaryElementOfKind:atIndexPath: does not match the element kind it is being used for. When asked for a view of element kind &#39;HomeTitleHeaderView&#39; the data source dequeued a view registered for the element kind &#39;UICollectionElementKindSectionHeader&#39;.&#39;
terminating with uncaught exception of type NSException</p>
</blockquote>
<p>이번엔 또 읽어보니 element kind가 매칭이 안된다..? 뭘까..
내가 알고있던 kind는 header인지 footer인지 구분하는 거고 헤더면 <strong><em>&quot;UICollectionView.elementKindSectionHeader&quot;</em></strong> 이거 쓰면 되는거 아니였냐구..!</p>
<p>내 코드 &gt;</p>
<pre><code class="language-swift">    // collectionView register header
    collectionView.register(CommunityMainCollectionViewCell.self, forCellWithReuseIdentifier: CommunityMainCollectionViewCell.identifier)

    // header reusable view dequeue
    collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: HomeTitleHeaderView.identifier, for: index)</code></pre>
<p>흠,, 잘 넣어준 것 같은데 다시 잘 찾아보니...</p>
<p>이유를 찾았다.. 맞아 코드는 거짓말을 안하지 ㅠㅠㅠ 항상 사람이 문제야....</p>
<h4 id="문제된-코드">문제된 코드 &gt;</h4>
<pre><code class="language-swift">    // item, group 관련 코드 생략

    let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(56))
    let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                                    elementKind: HomeTitleHeaderView.identifier, // 띠용..
                                                                    alignment: .topLeading)
       section.boundarySupplementaryItems = [headerElement]</code></pre>
<p>휴.. layout 잡아주는 부분에서 headerItem elementKind에 reuseIdentifier를 넣어주고 있... ㅠㅠ 
서로 값이 다르니 당연히 오류가 날 수 밖에 ㅎㅎ</p>
<p>저 부분을 <em><strong>&quot;UICollectionView.elementKindSectionHeader&quot;</strong></em> 로 넣어주니 에러 없이 잘 나온다! </p>
<p>어차피 kind는 string이기 때문에 직접 string 만들어서 넣어줘도 상관은 없고 값만 서로 동일하면 에러 안남!
.
.
.</p>
<h2 id="-uicollectionviewcellregistration">* UICollectionView.CellRegistration</h2>
<p>이거 때문에 열심히 구글링 해보다가 cell 과 header/footer register()를 해주는 방식이 기존과는 다른게 있길래 그거도 적용해 봤는데 잘 됨! 하지만 그건 iOS 14 이상에서만 가능..ㅎ</p>
<p>우리 앱은 iOS 13이 최소 지원 버전이라.. 버전을 올리자 할수도 없고 ㅠㅠ
이틀 내내 고생하다 원인을 찾고나니 허무.. 앞으론 더 꼼꼼히 봐보자!</p>
<p>코드 ex)</p>
<pre><code class="language-swift">    // cell registrations

    let topBannerCellRegistration = UICollectionView.CellRegistration&lt;TopBannerCollectionViewCell, [Banner]&gt; { (cell, indexPath, banner) in
        cell.setData(banners: banner)
    }

    let categoryCellRegistration = UICollectionView.CellRegistration&lt;HomeCategoryCollectionViewCell, HomeServiceMenu&gt; { (cell, indexPath, homeMenu) in
        // ... (생략)
    }

    let macapickCellRegistration = UICollectionView.CellRegistration&lt;CommunityMainCollectionViewCell, Story&gt; { (cell, indexPath, story) in
        // ... (생략)
    }

    let headerRegistration = UICollectionView.SupplementaryRegistration&lt;HomeTitleHeaderView&gt;(elementKind: HomeTitleHeaderView.identifier) { supplementaryView, elementKind, indexPath in
        // ... (생략)
    }

    // datasource에 적용

    viewModel.datasource = UICollectionViewDiffableDataSource&lt;HomeSection, HomeDataItem&gt;(collectionView: collectionView, cellProvider: { collectionView, indexPath, listItem in
        switch listItem {
        case .topBanner(let banners):
            let cell = collectionView.dequeueConfiguredReusableCell(
                using: topBannerCellRegistration, for: indexPath, item: banners
            )
            return cell
        .
        .
        .
        (생략)
        }
    })
</code></pre>
<h1 id="완성된-뷰-ui">완성된 뷰 UI:</h1>
<p align="center"><img src="https://velog.velcdn.com/images/dodo_dev/post/4a0c7800-578f-4c61-bc70-70e1dd7b44f0/image.PNG" height="100px" width="300px"></p>

<p>3번째 섹션에 header view 잘 적용된 거 확인~~!!</p>
<p>출처:
<a href="https://jamesrochabrun.medium.com/uicollectionviewdiffabledatasource-and-decodable-step-by-step-6b727dd2485">https://jamesrochabrun.medium.com/uicollectionviewdiffabledatasource-and-decodable-step-by-step-6b727dd2485</a>
<a href="https://swiftsenpai.com/development/reload-diffable-section-header/">https://swiftsenpai.com/development/reload-diffable-section-header/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionViewCompositionalLayout : Background Decoration View 로 section에 백그라운드 입히는 방법]]></title>
            <link>https://velog.io/@dodo_dev/UICollectionViewCompositionalLayout-Background-Decoration-View-%EB%A1%9C-section%EC%97%90-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%9E%85%ED%9E%88%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@dodo_dev/UICollectionViewCompositionalLayout-Background-Decoration-View-%EB%A1%9C-section%EC%97%90-%EB%B0%B1%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EC%9E%85%ED%9E%88%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 02 Aug 2023 03:07:29 GMT</pubDate>
            <description><![CDATA[<p>CollectionView Compositional Layout 으로 셀 구성 중에 section에 배경 색을 어떻게 넣는지 찾아보니 Decoration View 를 사용하면 되더라구요!</p>
<p align="center">
<img src="https://velog.velcdn.com/images/dodo_dev/post/2537cff3-93dc-4666-80a3-3f351fd55582/image.PNG" width="30%" height="30">
  저는 3번째 섹션에 그레이 색상의 백그라운드 뷰를 넣었어요!
</p>


<ol>
<li>먼저 백그라운드 뷰로 사용할 UICollectionReusableView 를 생성해줍니다.</li>
</ol>
<pre><code class="language-swift">class GrayBackgroundView: UICollectionReusableView {
    private let grayBackgroundView = UIView().then {
        $0.backgroundColor = .lightGray //UIColor(red: 246, green: 247, blue: 251)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .clear
        addSubview(grayBackgroundView)

        grayBackgroundView.snp.makeConstraints {
            $0.top.leading.trailing.bottom.equalToSuperview()
        }
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
}</code></pre>
<ol start="2">
<li>header는 boundarySupplementaryItems에, background는 decorationItems에 각각 지정해줍니다.</li>
</ol>
<pre><code class="language-swift">    private func pickLayout() -&gt; NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(300), heightDimension: .estimated(313))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 16
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16)
        section.supplementariesFollowContentInsets = false

        let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(56))
        let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                                        elementKind: UICollectionView.elementKindSectionHeader,
                                                                        alignment: .topLeading)
        section.boundarySupplementaryItems = [headerElement]
        section.orthogonalScrollingBehavior = .groupPaging

        // Background
        let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(elementKind: &quot;macapickBackground&quot;)
        section.decorationItems = [sectionBackgroundDecoration]

        return section
    }</code></pre>
<ol start="3">
<li>여기서 주의해야할 점은!<blockquote>
<p>Supplementary Header View는 UICollectionView 내에서 register()
Decoration View는 UICollectionViewCompositionalLayout 내에서 register() 해야합니다.</p>
</blockquote>
</li>
</ol>
<p>저는 예제 코드를 보면서 하는데 계속 register가 안됐다면서 fatal error가 뜨는 거에요.. 어떻게 어떻게 하다가 찾은 방법은 아래입니다! </p>
<pre><code class="language-swift">    private func createLayout() -&gt; UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex, environment) -&gt; NSCollectionLayoutSection? in
            switch sectionIndex {
            case 0: return self?.bannerLayout()
            case 1: return self?.categoryLayout()
            case 2: return self?.pickLayout()
            default: return nil
            }
        }

        // Register decoration view
        layout.register(GrayBackgroundView.self,
                        forDecorationViewOfKind: &quot;macapickBackground&quot;)

        return layout
    }</code></pre>
<ol start="4">
<li>마지막으로 컬렉션뷰의 collectionViewLayout에 위에서 작성한 createLayout() 함수를 넣어주면 끝~<pre><code class="language-swift"> private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()).then {
     $0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: &quot;bannerCell&quot;)
     $0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: &quot;categoryCell&quot;)
     $0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: &quot;macapickCell&quot;)
     $0.register(UICollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: &quot;macapickHeader&quot;)
     $0.backgroundColor = .clear
     $0.dataSource = self
 }</code></pre>
</li>
</ol>
<p>아 아까 decoration view가 계속 register가 안된다고 오류가 나던 코드는 이 문제였어요..</p>
<pre><code class="language-swift">    private func pickLayout() -&gt; NSCollectionLayoutSection {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(300), heightDimension: .estimated(313))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])

        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = 16
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16)
        section.supplementariesFollowContentInsets = false
        section.orthogonalScrollingBehavior = .groupPaging

        let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(56))
        let headerElement = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
                                                                        elementKind: UICollectionView.elementKindSectionHeader,
                                                                        alignment: .topLeading)
        section.boundarySupplementaryItems = [headerElement]

        // Background
        let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(elementKind: &quot;macapickBackground&quot;)
        section.decorationItems = [sectionBackgroundDecoration]

        // section을 리턴하는 함수 안에서 layout 인스턴스를 새로 생성해 register()를 해줬더니 안되는 거였음..ㅠㅠ
        let layout = UICollectionViewCompositionalLayout(section: section)
        layout.register(GrayBackgroundView.self, forDecorationViewOfKind: &quot;macapickBackground&quot;)

        return section
    }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[<iOS> Codable - custom init decode(_:forKey:) & decodeIfPresent(_:forKey:)]]></title>
            <link>https://velog.io/@dodo_dev/iOS-Codable-custom-init-decodeforKey-decodeIfPresentforKey</link>
            <guid>https://velog.io/@dodo_dev/iOS-Codable-custom-init-decodeforKey-decodeIfPresentforKey</guid>
            <pubDate>Tue, 11 Jul 2023 01:59:41 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>서버에서 값이 nil로 들어오는 경우를 대비해 프로퍼티를 옵셔널 타입으로 정의해놓음.</p>
</li>
<li><p>그런데 만약 서버에서 내가 정의해 놓은 enum이 아닌 다른 타입으로 넘어오거나 nil 값으로 내려지는 경우는? 디코딩 에러남.</p>
</li>
<li><p>파싱 오류를 막기 위해선 default 값을 넣어줘야겠다고 생각.</p>
</li>
</ol>
<blockquote>
<p>decode()는 return 값이 non-optional
decodeIfPresent()는 return 값이 optional</p>
</blockquote>
<p>decodeIfPresent는 받는 타입도 optional 이여야함.</p>
<blockquote>
<p>프로퍼티가 옵셔널 값이면 나중에 쓸때 optional binding 해주기 귀찮을땐
decode() 를 사용하는게 나아보임.</p>
</blockquote>
<p>키가 없을 때의 경우는 decodeIfPresent 나 try? 를 써야 함. (안그럼 key not found 에러남)</p>
<pre><code class="language-swift">struct CounselorReview: Codable {
    1. let productType: SaleProductType  // 옵셔널 값으로 안쓰고 싶음
    2. let productType: SaleProductType?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        1. productType = (try? container.decode(SaleProductType.self, forKey: .productType)) ?? .none
        2. productType = try container.decodeIfPresent(SaleProductType.self, forKey: .productType) ?? SaleProductType.none
    }
}

enum SaleProductType: Int, Codable {
    case text = 1
    case onlineGroup = 3
    case offlineGroup = 4
    case voice = 11
    case video = 13
    case interpret = 12
    case psyTest = 15
    case none  // default 값 정의
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[layoutSubviews(), draw() 는 언제 사용할까? 뷰를 업데이트 하고싶을 때 원리.]]></title>
            <link>https://velog.io/@dodo_dev/layoutSubviews-draw-%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C-%EB%B7%B0%EA%B0%80-%ED%99%94%EB%A9%B4%EC%97%90-%EA%B7%B8%EB%A0%A4%EC%A7%80%EB%8A%94-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@dodo_dev/layoutSubviews-draw-%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C-%EB%B7%B0%EA%B0%80-%ED%99%94%EB%A9%B4%EC%97%90-%EA%B7%B8%EB%A0%A4%EC%A7%80%EB%8A%94-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Fri, 07 Jul 2023 04:08:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>*<em>문제: *</em></p>
</blockquote>
<ul>
<li>컬렉션뷰 셀 안에 있는 gradient 뷰가 첫 시점에 안나오고 스크롤을 한번 다녀와야지만 적용이 되는 문제.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dodo_dev/post/b1d03826-4cc9-4d71-8f24-2e233b3a1f4a/image.png" alt=""></p>
<p>위 문제의 원인이 뭘까..🤔</p>
<p>테이블뷰 셀 안에 컬렉션뷰 셀에서 화면 reload 하는 시점이 애매해서 gradient 뷰의 프레임을 제대로 못받아오는 것 같다. </p>
<blockquote>
<p><strong>해결방법!</strong>
레이아웃 업데이트를 시켜주자! 어떻게??</p>
</blockquote>
<ul>
<li>layoutSubviews()</li>
<li>draw()</li>
<li>layoutIfNeeded()</li>
<li>setNeedsLayout()</li>
<li>setNeedsDisplay()</li>
<li>displayIfNeeded()</li>
</ul>
<p>다 비슷한거 아녀..? 너무 헷갈려...</p>
<p>일단 첫번째로 override 함수인 layoutSubviews()를 호출해서 그 안에 gradientLayer의 프레임을 gradientView의 크기로 줘봤지만.. 무용지물 ㅠㅠ</p>
<pre><code class="language-swift">override func layoutSubviews() {
    super.layoutSubviews()
        gradientLayer.frame = gradientView.bounds
    }
}</code></pre>
<p>그래서 어 혹시 layoutIfNeeded()를 해주면 되려나? 싶어서 gradientView 오토레이아웃 잡은 후에 넣었더니 바로 적용이 된다..! 띠용</p>
<p>그럼 draw override 함수를 써봐도 되려나 싶어서 해봤더니 이것도 바로 잡힘!</p>
<pre><code class="language-swift">override func draw(_ rect: CGRect) {
    gradientLayer.frame = gradientView.bounds
}</code></pre>
<p>draw 함수에서는 super를 호출할 필요가 없다고 한다. 다른 커스텀 뷰를 상속받을때만 호출해주면 됨. 난 그냥 collectionViewCell 안에서 호출하는거니 super 호출 X. </p>
<h4 id="그래도-헷갈려-더-알아보자">그래도 헷갈려.. 더 알아보자</h4>
<h3 id="1-setneedsdisplay">1. setNeedsDisplay</h3>
<p>이 메서드는 drawRect 메서드랑 같이 사용됨. 이 메서드는 내부적으로 drawRect를 호출하면서 뷰를 다시 그림. </p>
<p>뷰의 컨텐츠가 바꼈을 때 이 메서드로 뷰가 변경됐다라는 걸 알린후에 drawRect 호출해서 뷰를 다시 그리는 것.</p>
<p>뷰가 계속 움직이거나 업데이트 되어야할 때 등등 속성의 변경 사항을 반영하려면 setNeedsDisplay를 호출하면 됨.</p>
<p>주의사항으로는,
drawRect 함수를 절대 직접 호출하면 안된다!! setNeedsDisplay로 다음 업데이트 요청하면 됨.</p>
<h3 id="2-setneedslayout">2. setNeedsLayout</h3>
<p>이 메서드는 아무런 작업 수행 X. 시스템에 뷰 업데이트가 필요하다고 trigger 하는 역할. 그 전 뷰 레이아웃을 무효화 시키고 다음 업데이트 주기때 한번에 업데이트 시키기 위함.
퍼포먼스에 도움이 된다.</p>
<pre><code class="language-swift">import UIKit

class CircleView: UIView {
    private var center: CGPoint = .zero
    private var radius: CGFloat = 0.0

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        // center와 radius에 기반하여 원을 그리기 위해 사용자 정의 그리기 수행
        let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        path.stroke()
    }

    func updateCircle(center: CGPoint, radius: CGFloat) {
        self.center = center
        self.radius = radius

        setNeedsLayout() // 레이아웃 업데이트 트리거
        setNeedsDisplay() // 다시 그리기 트리거
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // 필요한 경우 추가적인 레이아웃 조정 수행
    }
}</code></pre>
<h3 id="3-layoutifneeded">3. layoutIfNeeded</h3>
<p>이 메서드는 뷰 업데이트를 바로 적용하고 싶을때 사용하면 됨. 위에 setNeedsDisplay나 setNeedsLayout 같은 경우는 다음 업데이트 주기를 기다리는 거라면 이 메서드는 그냥 지금 바로! 느낌.</p>
<blockquote>
<p><strong>정리해보자면,</strong></p>
</blockquote>
<ul>
<li>drawRect 함수는 직접 호출 X. 커스텀 뷰 만들어서 setNeedsDisplay 메서드 호출해서 사용.</li>
<li>layoutSubviews 함수도 직접 호출 X. layoutIfNeeded나 setNeedsLayout 호출.</li>
</ul>
<h2 id="결론은">결론은,</h2>
<p>내 앱에서는 뷰가 계속 바껴야 하는 건 아니고, 그냥 처음에 셀 세팅될 때 gradient만 바로 적용이 되면 되기 때문에 drawRect 메서드를 호출할 필요도 없고 layoutSubviews 메서드도 셀이 재사용될때마다 계속 호출이 되기때문에 그럴 필요가 없다고 판단했다. </p>
<p>그래서 init 시점에서 뷰를 다 그려준 후에 gradientLayer의 프레임을 잡아주기 전에 layoutIfNeeded() 메서드만 호출 해 바로 레이아웃 업데이트 완료를 해주었다.</p>
<h2 id=""></h2>
<pre><code class="language-swift">override init(frame: CGRect) {
    super.init(frame: frame)
    configure()
    makeConstraints()
    setGradientLayer()
}

private func setGradientLayer() {
    gradientLayer = CAGradientLayer()
    gradientView.layoutIfNeeded()
    gradientLayer.frame = gradientView.bounds
    gradientLayer.colors = [UIColor(red: 78, green: 175, blue: 168).cgColor, MacaColors.turquoise600.cgColor]
    gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.2)
    gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.8)
    gradientView.layer.addSublayer(gradientLayer)
}</code></pre>
<h2 id="결과">결과!!</h2>
<p>이제 바로 나온다!
<img src="https://velog.velcdn.com/images/dodo_dev/post/ff4274ba-0281-4abf-af28-384e5b007455/image.png" alt=""></p>
<p>출처:
<a href="https://stackoverflow.com/questions/10818319/when-do-i-need-to-call-setneedsdisplay-in-ios">https://stackoverflow.com/questions/10818319/when-do-i-need-to-call-setneedsdisplay-in-ios</a>
<a href="https://zeddios.tistory.com/359">https://zeddios.tistory.com/359</a></p>
]]></description>
        </item>
    </channel>
</rss>