<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>iOS ♥︎</title>
        <link>https://velog.io/</link>
        <description>제가 나중에 다시 보려고 기록합니다 ✏️</description>
        <lastBuildDate>Sat, 08 Oct 2022 09:09:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>iOS ♥︎</title>
            <url>https://images.velog.io/images/dev_jane/profile/6fe28ec8-eef3-4e15-a21d-fb137b284744/IMG_5517.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. iOS ♥︎. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_jane" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[RxSwift] flatMap, flatMapFirst, flatMapLatest]]></title>
            <link>https://velog.io/@dev_jane/RxSwift-flatMap-flatMapFirst-flatMapLatest</link>
            <guid>https://velog.io/@dev_jane/RxSwift-flatMap-flatMapFirst-flatMapLatest</guid>
            <pubDate>Sat, 08 Oct 2022 09:09:22 GMT</pubDate>
            <description><![CDATA[<p>진행하고 있는 프로젝트에서 검색 결과에 대한 pagination기능을 구현할 때 스크롤이 일정 범위에 도달하면 다음 페이지 결과를 가져오기 위해서 네트워크 요청을 보낼때</p>
<p>연속적인 스크롤 오프셋 이벤트들 중 하나의 이벤트에 대해서만 반응하여 네트워크 요청을 보내기 위해서 flatMapLatest를 사용했다.</p>
<p>아래 코드 예시를 보면,</p>
<p>스크롤 오프셋 이벤트를 방출하는 loadMoreContent Observable에 </p>
<pre><code class="language-swift">private func collectionViewContentOffsetChanged() -&gt; Observable&lt;Void&gt; {
    return collectionView.rx.contentOffset
        .withUnretained(self)
        .filter { (self, offset) in
            guard self.collectionView.contentSize.height != 0 else {
                return false
            }
            return self.collectionView.frame.height + offset.y + 100 &gt;= self.collectionView.contentSize.height
        }
        .map { _ in }
}

let input = SearchViewModel.Input(
    //..
    loadMoreContent: collectionViewContentOffsetChanged()
)</code></pre>
<p>flatMapLatest를 이용해서 가장 마지막 이벤트에 대해서만 네트워크 요청을 보내도록 처리하였다.</p>
<pre><code class="language-swift">let moreResults = input.loadMoreContent
    .map { _ in
        print(&quot;event 발생!&quot;)
    }
    .flatMapLatest { _ -&gt; Observable&lt;[SearchCellViewModel]&gt; in
        return self.useCase.getSearchResults(with: self.searchText, page: self.page)
            .map { (movieList) -&gt; [SearchCellViewModel] in
                print(&quot;네트워크 요청&quot;)
                self.page = movieList.page + 1
                return movieList.items.filter { $0.posterPath != &quot;&quot; }
                    .map { SearchCellViewModel(movie: $0) }
            }
    }</code></pre>
<img src="https://velog.velcdn.com/images/dev_jane/post/5889c3df-fabe-40d5-9177-5d9ba21e462c/image.png" width="100" height="100"/>
이렇게 하면 수많은 이벤트가 발생하지만, 네트워크 요청은 딱 한번만 이루어지는 모습을 볼 수 있다.


<hr>
<p>이 과정에서 flatMap, flatMapFirst, flatMapLatest의 차이에 대해 알아보게 되었다,,</p>
<p>똑같은 예시에 대해서 Operator만 flatMap, flatMapFirst, flatMapLatest로 바꿔가며 결과가 어떻게 다르게 나오는지 살펴보자.</p>
<h3 id="flatmap">flatMap</h3>
<p>flatMap은 한 Observable에서 발생한 이벤트를 다른 Observable로 변환하는 Operator이다.</p>
<p>만약 특정 이벤트로 만들어진 Observable 시퀀스가 진행되고 있는 도중에 다른 이벤트가 발생하면 그 이벤트로 만들어진 Observable 시퀀스도 동시에 진행할 수 있다.</p>
<p>이렇게만 말하면 이해가 안갈테니 예시를 보자.</p>
<p>아래 예시는 1, 2, 3 이벤트를 방출하는 Observable에 대해서 각각의 이벤트를 sequenceString Observable로 변환한다.</p>
<pre><code class="language-swift">let sequenceInt = Observable.of(1, 2, 3)
let sequenceString = Observable.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;)

sequenceInt
    .flatMap { (x: Int) -&gt; Observable&lt;String&gt; in
        print(&quot;Emit Int Item : \(x)&quot;)
        return sequenceString
    }
    .subscribe(onNext: {
        print(&quot;Emit String Item : \($0)&quot;)
    })
    .disposed(by: disposeBag)</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/65e18ad0-8fb3-46ed-9692-d69b2c3a47fa/image.png" alt="">
출력 결과를 보면, 이벤트 1에 대한 Observable 시퀀스가 진행되고 있는 와중에 새로운 이벤트 2가 발생하면 2에 대한 Observable 시퀀스도 동시에 발생하고, 마찬가지로 새로운 이벤트 3이 발생하면 3에 대한 Observable 시퀀스도 시작되어 각각의 Observable 이벤트들의 순서가 섞여서 나오는 모습을 볼 수 있다.</p>
<p>1에 대한 Observable 시퀀스 A-&gt;B-&gt;C-&gt;D 중간에 
2에 대한 Observable 시퀀스 A-&gt;B-&gt;C-&gt;D가 시작하고 나서, 
3에 대한 Observable 시퀀스 A-&gt;B-&gt;C-&gt;D도 시작한다. </p>
<h3 id="flatmapfirst">flatMapFirst</h3>
<p>그렇다면 flapMapFirst는 뭘까</p>
<p>Observable의 이벤트에 대한 Observable 시퀀스가 진행되고 있는 도중에 다음 이벤트가 발생하더라도, 원래 진행되고 있던 시퀀스가 종료될때까지 다음 이벤트에 대한 Observable 시퀀스를 생성하지 않는다.</p>
<p>그리고 원래 진행되던 이벤트의 Observable 시퀀스가 종료되고 나서 발생한 이벤트에 대해서는 다시 Observable 시퀀스를 생성한다.</p>
<pre><code class="language-swift">let sequenceInt = Observable.of(1, 2, 3)
let sequenceString = Observable.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;)

sequenceInt
    .flatMapFirst { (x: Int) -&gt; Observable&lt;String&gt; in
        print(&quot;Emit Int Item : \(x)&quot;)
        return sequenceString
    }
    .subscribe(onNext: {
        print(&quot;Emit String Item : \($0)&quot;)
    })
    .disposed(by: disposeBag)</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/18bf29b1-eb34-4f9a-9fda-0bb52ed209b6/image.png" alt=""></p>
<p>따라서 이 예시에서는 이벤트 1에 대한 Observable 시퀀스 A-&gt;B-&gt;C-&gt;D가 진행되고 있는 와중에 이벤트 2, 3이 발생했기 때문에 무시된다.</p>
<h3 id="flatmaplatest">flatMapLatest</h3>
<p>마지막으로 flatMapLatest는 특정 이벤트에 대한 Observable 시퀀스가 진행되고 있는 동안에 다음 이벤트가 발생하면 Observable을 dispose 시키고, 다음 이벤트에 대한 Observable을 생성하게 된다.</p>
<pre><code class="language-swift">let sequenceInt = Observable.of(1, 2, 3)
let sequenceString = Observable.of(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;)

sequenceInt
    .flatMapLatest { (x: Int) -&gt; Observable&lt;String&gt; in
        print(&quot;Emit Int Item : \(x)&quot;)
        return sequenceString
    }
    .subscribe(onNext: {
        print(&quot;Emit String Item : \($0)&quot;)
    })
    .disposed(by: disposeBag)</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/02a701e4-9b05-4504-bbee-f8f5ccfcda0c/image.png" alt=""></p>
<p>이벤트 1에 대한 Observable 시퀀스가 A -&gt; 까지 진행된 순간에 다음 이벤트 2가 발생하면 이벤트 1에 대한 Observable은 dispose되고, 이벤트 2에 대한 Observable 시퀀스가 시작된다.</p>
<p>마찬가지로 이벤트 2에 대한 Observable 시퀀스가 A -&gt; 까지 진행된 순간에 다음 이벤트 3이 발생하면 이벤트 2에 대한 Observable은 dispose되고 이벤트 3에 대한 Observable 시퀀스가 시작된다.</p>
<p>이벤트 3이 가장 마지막 이벤트이기 때문에 이벤트 3에 대한 Observable 시퀀스는 A-&gt;B-&gt;C-&gt;D 끝까지 진행이 된다.</p>
<h1 id="reference">Reference</h1>
<p><a href="https://reactivex.io/documentation/operators/flatmap.html">https://reactivex.io/documentation/operators/flatmap.html</a></p>
<p><a href="http://minsone.github.io/programming/reactive-swift-flatmap-flatmapfirst-flatmaplatest">http://minsone.github.io/programming/reactive-swift-flatmap-flatmapfirst-flatmaplatest</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] 순환참조는 왜 일어날까?]]></title>
            <link>https://velog.io/@dev_jane/RxSwift%EC%97%90%EC%84%9C-%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0%EB%8A%94-%EC%99%9C-%EC%9D%BC%EC%96%B4%EB%82%A0%EA%B9%8C</link>
            <guid>https://velog.io/@dev_jane/RxSwift%EC%97%90%EC%84%9C-%EC%88%9C%ED%99%98%EC%B0%B8%EC%A1%B0%EB%8A%94-%EC%99%9C-%EC%9D%BC%EC%96%B4%EB%82%A0%EA%B9%8C</guid>
            <pubDate>Sat, 24 Sep 2022 15:27:24 GMT</pubDate>
            <description><![CDATA[<p>RxSwift의 Operator을 사용할때 순환 참조가 일어나지 않도록 클로저의 캡쳐리스트에 [weak self]로 약한 참조를 해야한다.</p>
<p>어떻게 순환 참조가 일어난다는건지 항상 궁금해서 찾아봤지만 이해가 안되다가 이제 드디어 이해가 되어서 이 유레카 모먼트를 기록해놓는다. 😇</p>
<h3 id="사전-개념">사전 개념</h3>
<p>클로저가 Context를 Capture하는 속성때문에 
escaping closure내에서 클래스의 인스턴스를 참조하면, 강한 순환 참조가 발생할 수 있다.</p>
<ul>
<li>non-escaping closure인 경우에는 함수가 종료될때 해당 함수의 scope를 벗어나지 않아 클로저도 메모리에서 해제되어 강한 참조를 해도 상관없는데,<pre><code class="language-swift">func someFunction(completion: () -&gt; Void = {}) {
completion()
print(&quot;someFunc!&quot;)
return
}</code></pre>
</li>
<li>escaping closure는 함수가 종료되어도 해당 함수의 scope를 벗어나 함수 종료 후에 실행되기 때문에 강한 순환 참조가 발생할 수 있는 것이다.<pre><code class="language-swift">func someFunction(completion: @escpaing () -&gt; Void = {}) {
self.someDelayProcess {
  completion()
}
print(&quot;someFunc!&quot;)
return
}</code></pre>
</li>
</ul>
<p>이렇게 escaping closure가 강한 순환 참조의 가능성을 가지고있는데... RxSwift 의 operator들과 subscription 메서드들은 모두 escaping closure로 이루어져 있다. 😱
<img src="https://velog.velcdn.com/images/dev_jane/post/d631067c-6d17-4fe2-ab96-981b8e6bdf7e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/df8bf4a4-6055-459e-816f-22a0aaa0f4e9/image.png" alt=""></p>
<p>따라서 저런 escaping closure를 파라미터로 받는 메서드들을 사용할때 꼭! 잊지말고 해줘야하는게 약한참조다</p>
<h3 id="rxswift에서-순환참조를-해결하기-위해-weak-self를-사용한다">RxSwift에서 순환참조를 해결하기 위해 weak self를 사용한다</h3>
<p>RxSwift로 비동기 처리를 할때 이런 코드를 많이 봤을 것이다.</p>
<pre><code class="language-swift">final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()

    override func viewDidLoad() {
        super.viewDidLoad()

        let parsedObject = theObservable
            .map { [weak self] json in
                return self.parser.parse(json)
            }

        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}
</code></pre>
<p>보통 ViewController에서 Observable의 subscription이 일어나기 때문에 이런식으로 코드를 작성한다.
DisposeBag를 이용해서 subscribe의 리턴값인 Disposable을 저장하고, 클로저의 캡쳐리스트에 [weak self]로 self를 약하게 참조한다.</p>
<p>순환참조를 해결하기 위해 약하게 참조하는건 알겠는데... 어떤 부분에서 순환참조가 생긴다는건지 궁금했다. </p>
<p>아무리 봐도 ViewController는 Observable을 참조하고 있지 않은데, 어디서 순환참조가 생긴다는거지?</p>
<h3 id="범인은-disposebag이었다">범인은 DisposeBag이었다!</h3>
<h4 id="disposebag을-사용하는-이유">DisposeBag을 사용하는 이유</h4>
<blockquote>
<p>간단히 말하자면 Observable과 Disposable의 강한 순환참조를 해결하기 위해서 Disposable마다 dispose()를 해줘야하는데, 일일히 해주기 귀찮아서 DisposeBag을 사용한다.</p>
</blockquote>
<p>Observable을 subscribe하면, Rx는 Observable과 Disposable간에 강한 순환 참조를 만든다.
<code>Observable &lt;-&gt; Disposable</code>
따라서 ViewController가 화면에서 사라져서 deinit이 되더라도, 둘 간의 강한 순환 참조 때문에 subscription이 취소되지 않는다.</p>
<p>따라서, 이를 해결하기 위해서 Disposable에는 구독을 취소하는 dispose() 메서드가 있다.</p>
<p>ViewController의 deinit시점에 아래처럼 dispose()한다면 Disposable이 deinit되면서 Observable도 deinit된다~!</p>
<pre><code class="language-swift">    final class MyViewController: UIViewController {
    var subscription: Disposable?

    override func viewDidLoad() {
        super.viewDidLoad()
        subscription = theObservable().subscribe(onNext: {
            // handle your subscription
        })
    }

    deinit {
        subscription?.dispose()
    }
}
</code></pre>
<p>하지만... ViewController에서 subscription이 한번만 일어나지 않고 여러번 일어난다면, 매 구독마다 저렇게 일일히 dispose를 시켜주는 것은 너무나도 귀찮은 일이기때문에 DisposeBag의 개념이 나온다.</p>
<p>DisposeBag를 ViewController의 프로퍼티로 두고, 구독이 일어날때마다 DisposeBag에 Disposable을 차곡차곡 넣어놓으면, ViewController의 deinit 시점에 DisposBag의 deinit이 불리면서 자기가 가지고 있던 모든 Disposable을 dispose() 시킨다.</p>
<p>아래 DisposeBag의 구현 부분을 보면,, deinit 시점에 정말 자기가 가진 모든 Disposable을 차례로 dispose() 시키는 모습을 볼 수 있다.</p>
<pre><code class="language-swift">public final class DisposeBag: DisposeBase {

    private var lock = SpinLock()

    // state
    private var disposables = [Disposable]()
    private var isDisposed = false

    /// Constructs new empty dispose bag.
    public override init() {
        super.init()
    }

    /// Adds `disposable` to be disposed when dispose bag is being deinited.
    ///
    /// - parameter disposable: Disposable to add.
    public func insert(_ disposable: Disposable) {
        self._insert(disposable)?.dispose()
    }

    private func _insert(_ disposable: Disposable) -&gt; Disposable? {
        self.lock.performLocked {
            if self.isDisposed {
                return disposable
            }

            self.disposables.append(disposable)

            return nil
        }
    }

    /// This is internal on purpose, take a look at `CompositeDisposable` instead.
    private func dispose() {
        let oldDisposables = self._dispose()

        for disposable in oldDisposables {
            disposable.dispose()
        }
    }

    private func _dispose() -&gt; [Disposable] {
        self.lock.performLocked {
            let disposables = self.disposables

            self.disposables.removeAll(keepingCapacity: false)
            self.isDisposed = true

            return disposables
        }
    }

    deinit {
        self.dispose()
    }
}</code></pre>
<p>그런데, DisposeBag을 사용했을 때 생기는 문제가 있다.
ViewController가 DisposeBag을 참조하게 되면서...
<code>ViewController -&gt; DisposeBag</code></p>
<p>DisposeBag 안에는 Disposable이 들어있으니...
<code>DisposeBag -&gt; Disposable</code></p>
<p>이런 끔찍한 사각관계가 형성이 되어버렸다 ...
<img src="https://velog.velcdn.com/images/dev_jane/post/7900c6a0-8d67-42b3-98a9-55402f351694/image.png" alt=""></p>
<p>이렇게 되면 아래 코드에서 map의 escaping closure에서 
<code>self</code>(aka. ViewController)를 강하게 참조하고 있어서 참조 카운트가 1이 되어 ViewController가 화면에서 내려가도 deinit이 안불린다.</p>
<pre><code class="language-swift">final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()

    override func viewDidLoad() {
        super.viewDidLoad()

        let parsedObject = theObservable
            .map { json in
                return self.parser.parse(json)
            }

        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}</code></pre>
<p>그렇게 되면 아까 말했던 ViewController deinit -&gt; DisposeBag deinit -&gt; Disposable deinit -&gt; Observable deinit의 아름다운 deinit이 불가능해진다. </p>
<p>근데 사실 해결책은 간단하다
<strong>[weak self]</strong> 로 Observable이 ViewController를 약하게 참조하면 되는 것,,, ㅋ</p>
<pre><code class="language-swift">final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()

    override func viewDidLoad() {
        super.viewDidLoad()

        let parsedObject = theObservable
            .map { [weak self] json in
                return self.parser.parse(json)
            }

        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/2e6fdf3c-cd74-465e-9ce7-0a9dccf854b5/image.png" alt=""></p>
<p>이렇게되면 ViewController가 화면에서 내려갈때 참조 카운트가 0이되어서 정상적으로 deinit이 불리게 된다 😚</p>
<p>이렇게 어떤식으로 순환참조가 일어나는지, 왜 클로저의 캡쳐리스트에 weak self를 사용해서 순환참조를 해결할 수 있는지 알아보았다 ~! </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/53dec06d-58ef-4e79-8845-984983bb81d6/image.png" alt=""></p>
<h1 id="reference">Reference</h1>
<p><a href="http://adamborek.com/memory-managment-rxswift/">Memory management in RxSwift – DisposeBag</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionViewDiffableDataSource의 identifier가 Hashable 해야하는 이유가 뭘까?]]></title>
            <link>https://velog.io/@dev_jane/UICollectionViewDiffableDataSource%EC%9D%98-identifier%EA%B0%80-Hashable-%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EA%B0%80-%EB%AD%98%EA%B9%8C</link>
            <guid>https://velog.io/@dev_jane/UICollectionViewDiffableDataSource%EC%9D%98-identifier%EA%B0%80-Hashable-%ED%95%B4%EC%95%BC%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EA%B0%80-%EB%AD%98%EA%B9%8C</guid>
            <pubDate>Tue, 06 Sep 2022 05:58:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>diffable datasource... 너 대체 뭔데?</p>
</blockquote>
<h3 id="uicollectionviewdiffabledatasource가-uicollectionviewdatasource와-다른점">UICollectionViewDiffableDataSource가 UICollectionViewDataSource와 다른점?</h3>
<p>우선 UICollectionViewDiffableDataSource는 iOS 13부터 적용이 가능한 CollectionView의 datasource이다. 기본적으로 기존 DataSource와 역할은 같다. CollectionView를 구성하는 데이터를 관리하고, UI를 업데이트하는 역할을 한다.</p>
<p>하지만 업데이트하는 방식에서 차이가 있다.
DiffableDataSource는 이름에서도 알 수 있듯이, 데이터의 <strong>Diff</strong> 한 부분을 파악해서 달라진 부분만 업데이트하는 것이 가능하다.</p>
<p>이게 가능한 이유는 데이터를 식별하는 방법의 차이 때문인데, </p>
<p>기존의 DataSource는 Section과 Items의 &quot;<strong>위치를 나타내는 indexPath</strong>&quot;를 들고 있어서 만약 데이터가 수정, 삭제된다면 indexPath도 변경될 수 있기 때문에 불안정한 정보를 가진다. 특정 indexPath에 반드시 해당 값이 존재한다고 확신할 수가 없기 때문에 reloadData()를 할 때, 어떤 데이터가 변했는지 datasource에서 파악할 수가 없어서 모든 데이터를 다시 로딩하여 컬렉션뷰를 업데이트한다.</p>
<p>이러한 한계를 극복하기 위해, DiffableDataSource는 Section과 Items의 &quot;바뀌지 않는 identifier&quot;를 가지도록 구성되었다. item을 identifier로 식별할 수 있게 되면서, 특정 item이 변경된다면 datasource는 변경된 item이 무엇인지 파악할 수 있게 된다.</p>
<p>구체적으로 변경된 item이 무엇인지 파악하는 방법은, 
현재 상태를 나타내는 snapshot과 이전 상태를 나타내는 snapshot을 비교해서 달라진 item을 파악한다.
이때 비교는 바로 identifier로 할 수 있다.</p>
<p>이렇게 어떤 item이 달라졌는지 파악이 된다면, 그 부분만 다시 로딩함으로써 애니메이션 효과도 자동으로 적용이 가능해진다. </p>
<p>DataSource와 비교해서 DiffableDataSource가 안정적인 이유는 변하지 않는 identifier 덕분인데, 그렇다면 어떻게 변하지 않는 identifier를 만들 수 있을까?</p>
<h2 id="hashable">Hashable</h2>
<p>diffable datasource를 사용해 봤다면 값을 identifier로 사용하기 위해서는 Hashable해야한다는 말을 많이 들어봤을 것이다.</p>
<p>그래서 Hashable이 뭔데. 왜 변하지 않는데?
<img src="https://velog.velcdn.com/images/dev_jane/post/9f447ab1-013c-417f-989c-952a7771b957/image.png" alt="">
타입이 Hashable하다는 것은...값이 해시함수에 들어가서 정수 해시값으로 변경될 수 있다는 것을 의미한다. 변환된 해시값은 해시테이블의 key로 사용될 수 있다.</p>
<p>해시 테이블은 마치 dictionary와 같은 방법으로 key값을 index로 사용해서 value를 저장한다.
그럼 key값만 가지고 있으면 아주 쉽게 해시 테이블에 저장된 value를 꺼내올 수 있어서 시간 복잡도가 O(1)이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/50884ea5-5959-4d36-ba59-97a48b6483a2/image.png" alt=""></p>
<p>해시 테이블은 Dictionary나 Set처럼 순서가 없지만, 내부적으로는 배열로 구현되어있어서 index가 있다.</p>
<p>아니 순서가 없다면서 무슨 index가 갑자기 나와? 할 수 있겠지만
해시함수가 있어서 가능한 일이다.</p>
<p>그림의 예시를 보자면 James라는 키가 해시함수에 들어가면 04라는 해시값(index)로 변환하는 모습을 볼 수 있다.
해시함수가 키를 해시 주소값(=index)로 변환을 해주어서 해시 테이블 내부적으로 인덱스가 존재할 수 있었던 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/3bb80b43-99f1-4dd4-bfc1-99faf7c318cc/image.png" alt=""></p>
<h4 id="그렇다면-여기서-드는-의문은-hashable은-왜-equatable을-상속받을까">그렇다면 여기서 드는 의문은 Hashable은 왜 Equatable을 상속받을까?</h4>
<h2 id="equatable">Equatable</h2>
<p>커스텀 타입중에 </p>
<ul>
<li>모든 저장 프로퍼티가 Hashable한 Struct나</li>
<li>모든 연관값이 Hashable한 enum</li>
</ul>
<p>을 제외하고는 Hashable 프로토콜을 채택하려면 
<img src="https://velog.velcdn.com/images/dev_jane/post/652d67bc-d706-4d81-bd13-8a8250f9c682/image.png" alt=""></p>
<p>Hashable이 상속받고 있는 Equatable의 요구사항인 ==와
Hashable의 요구사항인 hash(into:)를 구현해야한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/57612016-b700-4bcf-818d-0d2fb7e312b4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/066c388f-183d-4cc0-bb92-4976a2802bbf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/71ad4aa4-622c-4b39-9606-a480e92a9de8/image.png" alt=""></p>
<p>Equatable 프로토콜을 채택한다면 두 객체가 서로 같은 값을 가졌는지 비교할 수 있다.</p>
<p>같은 값은 해시함수에 들어가면 무조건 같은 해시값으로 계산되지만,
두 값이 같은 해시값을 가졌다고 해서 무조건 두 값이 같은 것은 아니다.
그러니깐 <strong>서로 완전 다른 두 값이 우연히 같은 해시값으로 변환될 수 있다</strong>는 것이다.</p>
<p>이것을 <strong>해시 충돌</strong>이라고 하는데, Hash함수는 단순히 어떤 객체를 정수의 키값으로 변환하는 역할만 하기때문에 두 객체가 서로 같은지는 Equatable을 이용하여 비교해야하는 것이다. </p>
<p>마지막으로 Diffable DatatSource에서 identifiable을 사용하는 경우에 대해 살펴보자.</p>
<h2 id="identifiable">Identifiable</h2>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/86e650f5-e069-4630-95b8-3be1fa81d7c7/image.png" alt="">
Identifiable 프로토콜을 채택하면 id프로퍼티를 필수로 구현해야한다.
(클래스의 경우에는 자동으로 구현해준다고 한다)
값 타입의 경우에는 id프로퍼티를 따로 구현해야하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/5adf364e-7889-4b72-9af3-9709688a6cdc/image.png" alt=""></p>
<p>요 id프로퍼티는 Hashable해서 안정적인 identity를 가진다.</p>
<p><a href="https://velog.io/@dev_jane/CollectionView-Diffable-DataSource%EC%99%80-Hashable%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C">지난번 포스팅</a>에서 Diffable DatatSource에 identifier를 struct로 채택하지 않고 Identifiable을 채택한 struct의 AssociatedType인 ID로 채택하였다. </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/52bd4fa4-f63b-49db-a994-b682a2355e55/image.png" alt=""></p>
<p>만약 저 구조체의 title이나 numberOfLikes가 변경되면서 collectionview가 수정되어 업데이트되어야할때 만약 DestinationPost 구조체 자체가 identifier가 된다면...</p>
<p>안정적인 identity라고 볼 수 없다.</p>
<p><a href="https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/updating_collection_views_using_diffable_data_sources">공식문서</a>에 따르면 identifier 타입이 간단하기 때문에 diffable data source의 성능을 최적화한다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/e28a19b6-b122-4dc3-a9d9-e37fb1042337/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CollectionView의 성능 향상에 대한 고민: reconfigureItems, cell prefetching, image preparation(iOS 15)(feat. WWDC Make blazing fast lists and collection views)]]></title>
            <link>https://velog.io/@dev_jane/CollectionView-Diffable-DataSource%EC%99%80-Hashable%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@dev_jane/CollectionView-Diffable-DataSource%EC%99%80-Hashable%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Mon, 05 Sep 2022 11:39:50 GMT</pubDate>
            <description><![CDATA[<p>사용자들은 앱을 사용할 때 컬렉션뷰를 스크롤하면서 자연스럽고 부드러운 스크롤을 기대하게 된다. 만약 버벅거리거나 부자연스럽다면 사용자 경험을 해치게 될 것이다.</p>
<p>iOS 15부터 빠른 컬렉션뷰를 만들기 위해서 이 세가지 방법을 적용해볼 수 있게 되었다. </p>
<ol>
<li>structuring data to use reconfigureItems</li>
<li>cell prefetching</li>
<li>image preparation</li>
</ol>
<h3 id="1-structuring-data">1. structuring data</h3>
<p>셀의 데이터를 수정해야하는 경우에는 reconfigureItems를 쓰면 좋다.
reconfigureItems는 기존의 셀을 다시 재사용하기때문에,, 기존의 reloadItems처럼 새로운 셀을 dequeue해서 configure하는게 아니라서 성능상 좋다.
<img src="https://velog.velcdn.com/images/dev_jane/post/c6f5b859-31c8-4a1c-be09-7a4e8d2aff41/image.png" alt=""></p>
<p>iOS 15부터 가능해진 reconfigureItems를 적용하기 우ㅣ해서는...</p>
<blockquote>
<p>Diffable data source is built to store identifiers of items in your model, and not the model objects themselves.</p>
</blockquote>
<p>Diffable data source의 identifiers를 모델 자체가 아니라 identifiable을 채택한 모델의 id값으로 설정해야한다. </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/cb06c300-c3fc-401f-bd95-e1dfbe2769c5/image.png" alt=""></p>
<p>따라서 DestinationPost.ID를 Diffable DataSource의 item identifier로 설정해보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/9656f733-3f76-4d7a-bf18-d1e4323a8ce8/image.png" alt="">
이렇게 ID를 identifier로 등록하면, DestinationPost의 프로퍼티중 하나가 바뀌더라도 identifier는 바뀌지 않기때문에 안정성이 있게 된다. </p>
<p>데이터를 불러와서 ID만 뽑은 다음에, snapshot에 appendItems를 통해 Snapshot에 ID들을 append해준다.</p>
<p>여기서 들었던 의문은, diffable datasource에 모델이 아니라 ID타입만 지정해주면, 어떻게 차이를 찾아내지? 였다</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/669b088f-f870-4447-ba43-4e0dc79f2d90/image.png" alt="">  </p>
<p>걱정마 그거 cell 등록할때 ID로 다시 찾아오면 돼 ㅎ</p>
<p>만약에 cellRegistration을 사용하지 않는다고 해도 아래 코드처럼
UICollectionViewDiffableDataSource 메서드의 클로저로 id가 전달되기 때문에 그 id를 이용해서 모델을 찾아오면 됨 
<img src="https://velog.velcdn.com/images/dev_jane/post/43e42f12-3fe4-4d3a-985c-d046c579b4e3/image.png" alt=""></p>
<p>iOS 15 이전에는 애니메이션 없이 Snapshot을 apply하면 내부적으로 reloadData()를 불러서 성능상 그렇게 좋지 않았다고 한다. (아니 ㅋ 이걸 이제야 말하네.. 처음 diffable 소개할때는 성능이 완전 좋아졌다고 말하더니)
왜냐면 컬렉션뷰는 자신이 가진 셀들을 다 버리고 다시 모든 셀들을 만들어야했기 때문에!</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/c6f5b859-31c8-4a1c-be09-7a4e8d2aff41/image.png" alt=""></p>
<p>그러나, iOS 15 이후부터는 애니메이션 없이 Snapshot을 apply하는 같은 기능을 이 새로운 메서드인 reconfigureItems()로 한다면 변경된 사항만 변경하여 성능이 훨씬 좋아졌다고 한다. </p>
<h3 id="2-cell-prefetching">2. cell prefetching</h3>
<p>iOS 15부터 Lists, Compsitional Layout using estimated size, UITableView에 무려 cell prefetching이 &quot;자동적용&quot; 된다고 한다.!</p>
<p>cell prefetching을 왜 하는데?
collectionview를 스크롤 하다보면 잠깐잠깐씩 어딘가에서 걸리는 느낌이 들 때가 있다. 이 현상을 &quot;hitch&quot;라고 한다.</p>
<p>이 현상이 발생하는 원인은, 셀이 화면에 보여질때 준비시간이 허락된 시간을 넘을 때 발생한다고 한다.</p>
<p>그러니까 대체 언제?
자세히 알아보자.</p>
<p>셀 라이프사이클은 아래와 같이 dequeue한 후 prepareForReuse에서 초기화 된 후 다시 register해서 configure한 후에 화면에 보여진다.
<img src="https://velog.velcdn.com/images/dev_jane/post/430a0f8b-7759-4d35-ae56-6baee72e7b7e/image.png" alt=""></p>
<p>스크롤뷰를 스크롤하면 스크롤과 동시에 cell들이 보여지게 되는데 이때 화면에 보여질 준비를 하는 것을 <strong>&quot;commit&quot;</strong> 이라고 한다. 기기에 따라 이 준비시간이 다른데, display의 refresh rate가 높을수록 시간이 짧다.</p>
<p>예를 들어 iPad Pro의 경우 120Hz이고, iPhone의 경우 60Hz라면 iPad에게 주어진 시간이 두배 짧은 것이다. </p>
<p>refresh rate가 높을수록 짧은 시간 내에 보여줘서 디스플레이 퀄리티가 더 좋은것이었군</p>
<p>어쨌든... 다시 셀로 돌아와보면
새 셀이 필요한 시점에서는 기존 셀을 재사용했을때보다 훨~씬 많은 시간이 걸리게 된다. 아래 그림처럼 저 초록색 바가 긴 것이 바로 새로운 셀을 사용했을 때. 
<img src="https://velog.velcdn.com/images/dev_jane/post/614fbd5a-bcaa-45eb-a77d-bf3ab0679e38/image.png" alt=""></p>
<p>만약 저 초록색 준비시간인 <strong>&quot;commit&quot;</strong> 이 칸막이처럼 되어있는 deadline을 넘기게 된다면,, 끝날때까지 이전의 frame을 가지고 있어서 아까 말했던 <strong>&quot;hitch&quot;</strong> 현상이 발생하는 것이다.</p>
<p>따라서 이런 상황에 대한 해결방안으로 cell prefeching이 등장한 것이다... 
시간이 오래걸리는 cell의 commit을 남는 시간에 미리 땡겨와서 해버려서 나중에 안해도 되는 것!
전체 작업시간은 똑같지만, 중간중간 쉴 때 미리 오래걸리는 다음 작업을 끝내놓았다고 생각하면 쉽겠다 😁</p>
<h3 id="3-updating-cell-content">3. Updating cell content</h3>
<h4 id="reconfigureitems로-이미지-로딩이-끝난-후-준비되어있는-셀에-이미지만-추가하기">reconfigureItems로 이미지 로딩이 끝난 후 준비되어있는 셀에 이미지만 추가하기</h4>
<p>셀의 이미지를 비동기적으로 불러올 때 캐싱된 이미지가 없다면 일단 placeholder 이미지를 가져와 놓고 진짜 이미지를 다운로드 시작하는데,</p>
<p>진짜 이미지를 다운로드한 후에 바로 이미지뷰에 할당하지 말고 reconfigureItems를 불러서 할당하도록 하면, 이미지 빼고 모든 것이 다 준비되어있는 셀을 사용하여 이미지만 추가로 로딩하게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/e60b1c76-2f15-4326-af7c-092bb0012cda/image.png" alt=""></p>
<p>fetchByID는 내부적으로 id를 이용해 캐시에서 먼저 이미지를 찾고, 없으면 placeholder를 리턴하는 메서드다.</p>
<pre><code class="language-swift">    func fetchByID(_ id: Asset.ID) -&gt; Asset {
        if let image = preparedImages.fetchByID(id) {
            return image
        }

        return placeholderStore.fetchByID(id) ?? Asset(id: id, isPlaceholder: true, image: Self.placeholderFallbackImage)
    }</code></pre>
<p>만약 isPlaceholder == true라면, 저 downloadAsset 클로저에 네트워크 통신 결과 이미지가 비동기적으로 가져와질텐데.. 이때 바로 cell의 imageView.image에 할당하지 말아야 한다.</p>
<p>왜?
이미지가 다운로드 되는 그 시점은 비동기이기 때문에 그때 우리가 저 클로저에서 캡쳐했던 cell은 이미 다른곳에 사용되고 있을지도 모르기 때문이다.
따라서 직접적으로 cell에 이미지를 업데이트하기보다는, collectionView DataSource에 업데이트가 필요하다고 알리는 것이 더 안전하다.</p>
<p>그때 사용하는 것이 reconfigureItems ㅎ
reloadItems 사용시 새 cell을 dequeue하기 때문에 별로다.</p>
<blockquote>
<p>&quot;calling reconfigureItems on the prepared cell will rerun its registration&#39;s configuration handeler&quot;</p>
</blockquote>
<p>아래의 reconfigureItems를 부르면 다시 저 위의 cellRegistration config handler을 실행하게 된다. 
그럼 이제 fetchByID가 placeholder가 아니라 진짜 이미지를 리턴하게 된다.<br>(downloadAsset에서 이미지 다운로드해서 캐시해놓아서 캐시에서 찾아오게됨)
<img src="https://velog.velcdn.com/images/dev_jane/post/96ea12e0-4c0c-4050-abdb-2379b8abdfc6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/79eef3f6-b47a-420c-94e9-deff2a35ee6c/image.png" alt=""></p>
<h4 id="image-preparation">image preparation</h4>
<p>이미지 다운로드 작업을 비동기적으로 하더라도, 이미지를 display하기 위해서는 한가지 단계가 더 필요하다. 이미지가 비트맵 형태일때만 오직 display할 수 있는데... 다운로드 된 이미지의 형태는 jpeg, png 같은 데이터 형태이다.</p>
<p>*비트맵: 서로 다른 점(픽셀)들의 조합으로 그려지는 이미지 표현 방식 
(cf. 벡터 이미지: 점과 점을 연결해 수학적 원리로 그림을 그려 표현하는 방식)</p>
<p><a href="https://velog.io/@dev_jane/UICollectionView-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-downsampling">downsampling 포스팅</a>에서 이미지를 화면에 display하려면
아래와 같은 단계가 필요하다고 알아본 적이 있다.</p>
<ol>
<li>UIImage가 data buffer(jpeg, png...) -&gt; image buffer로 decoding하면 얻는 것이 pixel</li>
<li>그 후에 UIImageView가 pixel을 render함
<img src="https://velog.velcdn.com/images/dev_jane/post/fd463bbc-f4ce-4c4a-b8d3-a150f46f4467/image.png" alt=""></li>
</ol>
<p>이 <code>1. UIImage가 pixel을 준비하는 부분</code> 작업이 오래걸리면 또 hitch현상이 발생한다.</p>
<p>따라서, 아래 새로운 API는 <code>1. UIImage가 pixel을 준비하는 부분</code>을 우리가 컨트롤할 수 있도록 만들어줬다!
아래 그림에서 초록색으로 된 image preparation 단계가 바로 그것인데, 
<img src="https://velog.velcdn.com/images/dev_jane/post/08318e26-841d-4a98-86e0-8515849e4223/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/31c503c1-922d-4973-981e-4557d1a55af4/image.png" alt=""></p>
<p>이런식으로 원본 이미지를 prepareForDisplay 를 불러서 화면에 바로 디스플레이 될 수 있도록 준비시켜놓는 것이다. 
<img src="https://velog.velcdn.com/images/dev_jane/post/6357cdd1-bd85-4c76-a5fd-2ec84444d7f2/image.png" alt=""></p>
<p>그렇게 하면 메인 스레드를 막지 않고 백그라운드 스레드에서 작업이 일어나기 때문에 hitch현상이 일어나지 않는다.
<img src="https://velog.velcdn.com/images/dev_jane/post/1fac631a-f405-44cc-8771-1bed0f4b3226/image.png" alt=""></p>
<p>여기서 주의할점은 pixel 데이터의 특성상 디스크 캐시는 하지 말아야한다.
pixel 데이터는 메모리 캐시에,
원본 데이터를 디스크 캐시에 저장하라고 한다. </p>
<h1 id="reference">Reference</h1>
<p><a href="https://developer.apple.com/videos/play/wwdc2021/10252/?time=147">WWDC 20 Make blazing fast lists and collection views</a>
<a href="https://swiftsenpai.com/development/cells-reload-improvements-ios-15/">Table and Collection View Cells Reload Improvements in iOS 15</a></p>
<p>참고한 코드
<a href="https://developer.apple.com/documentation/uikit/uiimage/building_high-performance_lists_and_collection_views">Building High-Performance Lists and Collection Views</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kingfisher 사용해서 image downSampling 해보기(.cacheOriginalImage의 의미는 무엇일까?)]]></title>
            <link>https://velog.io/@dev_jane/Kingfisher-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-image-downSampling-%ED%95%B4%EB%B3%B4%EA%B8%B0.cacheOriginalImage%EC%9D%98-%EC%9D%98%EB%AF%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@dev_jane/Kingfisher-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-image-downSampling-%ED%95%B4%EB%B3%B4%EA%B8%B0.cacheOriginalImage%EC%9D%98-%EC%9D%98%EB%AF%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sat, 03 Sep 2022 05:21:40 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_jane/UICollectionView-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-downsampling">지난 포스팅</a>에서 아래 코드와 같이 downSampling 메서드를 직접 구현하여 다운샘플링된 이미지를 imageView의 image에 할당해주었다.</p>
<pre><code class="language-swift">func downSample(at url: URL, to pointSize: CGSize, scale: CGFloat) -&gt; UIImage {
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, imageSourceOptions) else {
        return
    }

    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
    let downsampleOptions = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
    ] as CFDictionary

    guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
        return UIImage()
    }
    return UIImage(cgImage: downsampledImage)
}</code></pre>
<p>커스텀 셀 configure 부분</p>
<pre><code class="language-swift">let serialQueue = DispatchQueue(label: &quot;Decode queue&quot;)
serialQueue.async { [weak self] in

    let downsampled = downSample(
        at: url,
        to: CGSize(width: 60, height: 90),
        scale: UIScreen.main.scale
    )
    DispatchQueue.main.async {
        self?.posterImageView.image = downsampled
    }
}</code></pre>
<p>WWDC 보고 적용해본건데, Kingfisher가 똑같이 구현해놓았기도 하고 기존에 다른 이미지 처리 기능을 위해 Kingfisher을 사용하고 있었기 때문에 Kingfisher의 DownsamplingImageProcessor을 사용해보기로 했다.</p>
<p><a href="https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet">Kingfisher cheat sheet</a>를 참고해서 사용해보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/c99674dd-99c2-47f9-9ed9-0fefef8a8ff2/image.png" alt=""></p>
<p>DownsamplingImageProcessor는 scaleFactor과 cacheOrigialImage와 함께 사용된다고 한다.</p>
<p>scaleFactor에 대해서는 저번에 알아봤고,
cacheOrigialImage에 대해 보도록 하자ㅏㅏㅏ</p>
<p>*이건 번외인데 지난 포스팅에서 말했던 것처럼 아래 사진의 설명에서도 resize를 쓰지말라고 되어있는 모습을 볼수 있음</p>
<ul>
<li>downsampling은 data -&gt; image -&gt; rendering 과정에서 data에서 image로 가기 전에 데이터 자체를 줄이는것이라면 </li>
<li>resizing은 rendering 이미 다 된 이미지를 다시 rendering해서 메모리를 많이 잡아먹게 됨
<img src="https://velog.velcdn.com/images/dev_jane/post/ebcd50a9-0852-40df-8980-c23ff24246b8/image.png" alt=""></li>
</ul>
<p>자 다시 돌아와서
cacheOrigialImage에 대해서...
한가지 의문점이 들었다.</p>
<h3 id="원본-이미지를-왜-캐시해">원본 이미지를 왜 캐시해?</h3>
<p>지난번 포스팅에서 알아봤던 것처럼 UIImage가 디코딩된 이미지를 메모리에 영구적으로 들고있는다고 했는데, 원본 이미지를 캐시하면 메모리를 똑같이 사용하는거 아니냐</p>
<p>대체 어떤 이미지를 어디에 캐시해놓는건지...
Araboza</p>
<pre><code class="language-swift">let processor = DownsamplingImageProcessor(size: CGSize(width: 368, height: 500))
self.posterImageView.kf.setImage(
    with: url,
    placeholder: UIImage(),
    options: [
        .processor(processor),
        .scaleFactor(UIScreen.main.scale),
        .cacheOriginalImage],
    completionHandler: nil
)
</code></pre>
<p>그래서 cacheOriginalImage가 어떤 이미지를 어디에 캐시하는지 들어가서 봤다.
<img src="https://velog.velcdn.com/images/dev_jane/post/8b8022d1-6b97-45c3-a576-b8109df400f9/image.png" alt=""></p>
<p>헉
ImageProcessor가 사용됐을 경우 kingfisher는 원본 이미지랑 다운샘플링 된 이미지 두개 다 저장하는데
나중에 이미지에 다른 ImageProcessor가 사용될 때 디스크에 캐시된 이미지를 가져와서 적용하려는 이유에서 원본 이미지는 디스크에만 저장이 된대</p>
<p>그럼 다운샘플링된 이미지는 메모리에만 저장된다는 거군</p>
<p><strong>원본 이미지는 메모리에 저장되지 않아서 메모리 사용량이 줄었던 것이었구나</strong> 🙌</p>
<h3 id="캐시된-이미지를-어떻게-사용하는지도-한번-보자면">캐시된 이미지를 어떻게 사용하는지도 한번 보자면</h3>
<p>kingfisher의 retrieveImageFromCache에서 캐시된 이미지를 찾을 때 먼저 processed된 이미지가 있는지 확인한 후 없는 경우에 원본 이미지가 있는지 확인한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/af0fc531-61b4-47a3-a945-c20b391414d6/image.png" alt=""></p>
<p>processed된 이미지가 있는지는 메모리 캐시에서 찾음
<img src="https://velog.velcdn.com/images/dev_jane/post/86ef9a87-e346-4a29-a16d-7f10bab16c15/image.png" alt=""></p>
<p>그리고 그 다음으로 원본 이미지가 있는지는 디스크 캐시에서 찾고있는 모습을 볼 수 있음
<img src="https://velog.velcdn.com/images/dev_jane/post/7a99afaa-a17d-4ca5-abe8-ba402a6d48be/image.png" alt=""></p>
<p>이렇게 된다면
ListView에서 DetailView로 갈때 이미 디스크 캐시해놓은 원본 이미지가 있으니 DetailView에서는 디스크 캐시에서 가져온 이미지로 downsampling만 진행하면 되겠군!</p>
<p>ListView에서 디스크 캐시해놓으면</p>
<pre><code class="language-swift">func configure(with viewModel: MovieListCellViewModel) {
    self.viewModel = viewModel
    guard let url = viewModel.imageUrl else {
        return
    }
    self.posterImageView.kf.setImage(
        with: url,
        options: [
            .processor(DownsamplingImageProcessor(size: CGSize(width: 200, height: 300))),
            .scaleFactor(UIScreen.main.scale),
            .cacheOriginalImage
    ])
    self.titleLabel.text = viewModel.title
    self.originalLanguageLabel.text = viewModel.originalLanguage
    self.genresLabel.text = viewModel.genres
}
    }</code></pre>
<p>DetailView에서는 네트워크 통신할 필요없이 디스크 캐시에서 원본 이미지 가져와서 바로 다운샘플링 가능 ㅎ
그리고 아래와 같이 cacheOriginalImage 옵션이 있어도 어짜피 이전에 캐시된 원본 이미지가 있기때문에 내부적으로 검사해서 중복이라면 디스크 캐시 또 안함 </p>
<pre><code class="language-swift">private func configureImageView(with url: URL) {
    let processor = DownsamplingImageProcessor(size: CGSize(width: 368, height: 500))
    self.posterImageView.kf.setImage(
        with: url,
        placeholder: UIImage(),
        options: [
            .transition(.fade(1)),
            .forceTransition,
            .processor(processor),
            .scaleFactor(UIScreen.main.scale),
            .cacheOriginalImage],
        completionHandler: nil
    )
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/32e2fafb-e97f-492d-9205-8d79f52051d7/image.gif" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionView 이미지 처리: downsampling(feat. WWDC Image and Graphics Best Practices)]]></title>
            <link>https://velog.io/@dev_jane/UICollectionView-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-downsampling</link>
            <guid>https://velog.io/@dev_jane/UICollectionView-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B2%98%EB%A6%AC-downsampling</guid>
            <pubDate>Thu, 25 Aug 2022 05:43:59 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-상황">문제 상황</h3>
<p>검색 결과를 UICollectionView에 표시할때 크기가 큰 이미지가 들어오는 상황에서</p>
<ol>
<li>이미지 로딩 속도가 느리고, 메모리 사용량이 급격하게 늘어남</li>
<li>스크롤시 이미지가 깜빡거리면서 바뀌기도 하는 현상이 발생함</li>
</ol>
<p>&lt;원본 이미지 크기들&gt;
<img src="https://velog.velcdn.com/images/dev_jane/post/149aa9ea-6b89-489f-97ad-4b7e66f0a84d/image.png" alt="원본 이미지 크기"></p>
<p><a href="https://medium.com/geekculture/find-image-dimensions-from-url-in-ios-swift-a186297e9922">참고: URL로 이미지의 사이즈 얻는 방법</a></p>
<pre><code class="language-swift">let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, imageSourceOptions) else {
    return UIImage()
}

if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
    let pixelWidth = imageProperties[kCGImagePropertyPixelWidth] as! Int
    let pixelHeight = imageProperties[kCGImagePropertyPixelHeight] as! Int
    print(url.path, &quot;Width: \(pixelWidth), Height: \(pixelHeight)&quot;)
}</code></pre>
<p>&lt;메모리 사용량&gt;
5번 검색하면 메모리 사용량이 1기가가 넘었다...
이걸 어째 🤯
<img src="https://velog.velcdn.com/images/dev_jane/post/10a1c27b-383b-4c67-8123-588222c37f89/image.png" alt="메모리 사용량"></p>
<p>큰 이미지를 사용해서 이런 결과가 나왔다면... 원본 이미지를 셀의 크기에 맞게 width 60 height 80으로 줄이면 해결되지 않을까?</p>
<h3 id="해결-방안">해결 방안</h3>
<p>단순히 이미지 사이즈만 줄이면 처리 속도도 빨라지지 않을까? 라는 이유로 처음엔 resizing을 생각했다.</p>
<p>하지만, WWDC 영상 &#39;Image and Graphics Best Practices&#39; 를 보고 생각이 바뀌었다.</p>
<p>결론부터 말하자면 메모리 사용량을 줄이려면, resizing이 아니라 downsampling을 해야한다.</p>
<ul>
<li>이미지 사이즈를 줄이면 될까? NO</li>
<li>답은 <strong>downsampling</strong></li>
</ul>
<p>지금부터 그 이유에 대해 알아보자 😆</p>
<h2 id="image-and-graphics-best-practices">Image and Graphics Best Practices</h2>
<p>UIImageView는 view를 display하는 역할을 하고,
UIImage는 view를 load하는 역할을 한다.</p>
<p>크게 보면 UIImage가 view(=이미지)를 로딩하면 
UIImageView가 그 결과물을 렌더링해서 Frame Buffer라는 곳에서 보여준다.
<img src="https://velog.velcdn.com/images/dev_jane/post/7c7d5a7c-9c46-4ee8-9754-a80818a844c5/image.png" alt=""></p>
<ul>
<li><p>UIImage가 하는일
Data Buffer(JPEG, PNG...)에서 Image Buffer로 <strong>Decoding</strong></p>
</li>
<li><p>UIImageView가 하는일
Image Buffer를 UIImageView의 contentMode에 맞게 <strong>rendering</strong> 하여 Frame Buffer에 표시</p>
</li>
</ul>
<p>이때 UIImage가 하는 <strong>decoding</strong> 작업은 굉장히 CPU 집약적이라서 비용이 크다고 한다. 따라서 UIImage는 한번 디코딩된 이미지(Image Buffer)를 거의 영구적으로 가지고 있어버림...
이때 저 디코딩된 이미지는 원본 이미지라서 사이즈가 큰 이미지인 경우에 굉장한 비용이 들어가는 것,,
(렌더링된 작은 UIImageView의 사이즈를 가지고 있으면 참 좋을텐데...)</p>
<p>이렇게 디코딩된 이미지를 저장하려고 메모리를 많이 할당하게 되면 다른 컨텐츠들을 파편화시키게됨... cpu가 개입하고.. 심하면 앱종료로 이어진다.</p>
<p>하여튼 결론은 원본 이미지의 사이즈가 큰 경우에 UIImage가 디코딩된 이미지를 가지고 있으므로 메모리 사용량이 늘어난다는 소리다.</p>
<h3 id="downsampling">Downsampling</h3>
<p>그렇다면, UIImage가 data buffer를 디코딩하기 전에 원본 사이즈를 줄이고 나서 디코딩하고, 이때 디코딩된 이미지(image buffer)를 저장하면 되겠지! 
그렇게 하면 앱의 메모리 사용량을 줄일 수 있다.</p>
<p>&lt;원래 방식&gt;
<img src="https://velog.velcdn.com/images/dev_jane/post/409d3a79-274f-4da7-96f9-376cad260760/image.png" alt=""></p>
<p>&lt;downsampling 하는 경우&gt;
<img src="https://velog.velcdn.com/images/dev_jane/post/22211ac6-0d6b-48df-b7fb-69e7a46659d3/image.png" alt=""></p>
<p>디코딩 하기전에 data buffer 자체의 사이즈를 줄여버려서 메모리 절약 가능~!</p>
<h3 id="코드">코드</h3>
<p>url을 통해 이미지를 불러오고,
downsampling 하고싶은 사이즈를 입력하면 된다.</p>
<p>예를 들어 이미지 크기를 width 60, height 90으로 줄이고 싶다면 <code>CGSize(width: 60, height: 90)</code> 입력하면 된다.</p>
<pre><code class="language-swift">func downSample(at url: URL, to pointSize: CGSize, scale: CGFloat) -&gt; UIImage {
    let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, imageSourceOptions) else {
        return
    }

    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
    let downsampleOptions = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
    ] as CFDictionary

    guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
        return UIImage()
    }
    return UIImage(cgImage: downsampledImage)
}</code></pre>
<p>커스텀 셀 configure에서 url을 받아서 셀의 이미지뷰에 이미지를 할당하는 부분에서 적용해주었다.</p>
<pre><code class="language-swift">let serialQueue = DispatchQueue(label: &quot;Decode queue&quot;)
serialQueue.async { [weak self] in

    let downsampled = downSample(
        at: url,
        to: CGSize(width: 60, height: 90),
        scale: UIScreen.main.scale
    )
    DispatchQueue.main.async {
        self?.posterImageView.image = downsampled
    }
}</code></pre>
<h4 id="그런데-downsampling을-했더니">그런데 Downsampling을 했더니</h4>
<p>이미지들이 블러처리한듯이 퀄리티가 낮아졌다.</p>
<p>그 이유는 앞에서 설명하지 않은 마지막 파라미터
scale 때문... scale을 화면에 맞게 설정하지 않으면 이미지 퀄리티가 낮아보인다.</p>
<p>따라서 scale에는 현재 화면의 scale 정보인<code>UIScreen.main.scale</code>을 넣어줬는데
<a href="https://developer.apple.com/forums/thread/109445">DownSampling makes image blurry</a> 
<img src="https://velog.velcdn.com/images/dev_jane/post/9e71c46c-d878-4bf3-b38d-d72506099c96/image.png" alt="">
위 링크에 따르면 디스플레이의 종류에 따라서 1point가 몇개의 pixel로 이루어지는지가 달라진다고 한다. </p>
<p><code>UIScreen.main.scale</code>이 </p>
<ul>
<li>iPhone 13 Pro의 경우 3이었고</li>
<li>iPhone SE (3th gen)의 경우 2였다. 
와 신기하다.</li>
</ul>
<p>그러면 똑같은 CGSize(width: 60, height: 90)을 표현하기 위해서 iPhone 13 Pro는 180, 270 pixel을 사용하고 iPhone SE는 120, 180 pixel을 사용한다는 것이다.</p>
<p>이렇게 downsampling을 적용한 결과 같은 횟수로 검색한 결과 메모리 사용량이 현저하게 줄었다.. 거의 1/5 😆</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/a09c63d6-fc8d-4f93-8796-1664b9257c00/image.png" alt="결과"></p>
<h1 id="reference">Reference</h1>
<p><a href="https://developer.apple.com/videos/play/wwdc2018/219/">WWDC19 Image and Graphics Best Practices</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Collectionview에 큰 이미지를 표시해야 할 때 UICollectionViewCompositionalLayout list layout 사용 금지]]></title>
            <link>https://velog.io/@dev_jane/Image-and-Graphics-Best-Practices</link>
            <guid>https://velog.io/@dev_jane/Image-and-Graphics-Best-Practices</guid>
            <pubDate>Thu, 25 Aug 2022 04:10:59 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-상황">문제 상황</h3>
<p>Layout이 UICollectionViewCompositionalLayout list인 Collectionview에 큰 이미지를 표시하려고 하니 셀 크기가 제 멋대로 커졌다가 줄어드는 현상이 발생함</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/237963de-c3a5-4831-949c-0b2808faf875/image.gif" alt=""></p>
<h3 id="문제-상황시-collectionview-세팅은">문제 상황시 collectionview 세팅은...</h3>
<ul>
<li>UICollectionViewCompositionalLayout list<pre><code class="language-swift">var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)</code></pre>
</li>
<li>xib 파일로 커스텀 셀 생성하여 등록해줌
<img src="https://velog.velcdn.com/images/dev_jane/post/c2865a70-293d-4163-af99-23a56e450230/image.png" alt=""></li>
</ul>
<h3 id="문제-해결을-위한-노력">문제 해결을 위한 노력</h3>
<ol>
<li>이미지뷰의 content hugging priority를 높히고 content resistance priority도 높혀서 이미지뷰가 커지거나 작아지지 못하도록 막았음. </li>
</ol>
<p>-&gt; 그러나 여전히 이미지뷰가 커지거나 작아져 셀 전체 높이도 바뀌는 현상 발생</p>
<ol start="2">
<li>셀의 높이, 너비와 이미지뷰의 높이, 너비를 아예 지정해줌</li>
</ol>
<p>-&gt; 그랬더니 레이아웃 깨져버림... 아니 셀의 높이를 110으로 정했는데 갑자기 44라는 높이는 어디서 나온거냐? 😨
<img src="https://velog.velcdn.com/images/dev_jane/post/d9f7297f-a71c-45da-9a48-5fad211a4e67/image.png" alt=""></p>
<ol start="3">
<li>결국 UICollectionViewCompositionalLayout list 대신 UICollectionViewFlowLayout 사용하였음. 
그랬더니 문제 바로 해결 ㅋㅋㅋ 🥲</li>
</ol>
<p>작은 이미지를 표현할때는 문제가 없었는데...
이미지 크기가 커지면 레이아웃이 꼬이는 문제가 생기는 것 같다. </p>
<p>UICollectionViewCompositionalLayout list는 아래와 같이 이렇게 간단한 테이블뷰같은 리스트를 생성하는 용도로 사용해야 할 것 같다. </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/ba8a4b12-50c3-442f-8a8b-721ad20ae98c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[multipart/form-data 구성 방식 살펴보기, Alamofire 없이 구현해보기]]></title>
            <link>https://velog.io/@dev_jane/multipartform-data-post-Alamofire-%EC%97%86%EC%9D%B4-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0Alamofire-%EC%99%80-%EB%B9%84%EA%B5%90</link>
            <guid>https://velog.io/@dev_jane/multipartform-data-post-Alamofire-%EC%97%86%EC%9D%B4-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0Alamofire-%EC%99%80-%EB%B9%84%EA%B5%90</guid>
            <pubDate>Wed, 17 Aug 2022 05:39:39 GMT</pubDate>
            <description><![CDATA[<h2 id="multipartform-data-란">multipart/form-data 란?</h2>
<p>POST Request를 보낼 때 body의 data를 인코딩하는 방식이다.
multi-part의 의미는 data가 다수의 부분으로 나뉘어져 서버로 업로드된다는 것이다. 
그래서 보통 용량이 큰 파일을 업로드할때 사용한다.</p>
<p>일반적으로 json 데이터를 보낼때는 Content-Type이 application/json인데, multipart/form-data도 네트워크 요청 방식은 똑같다. </p>
<p>URLRequest에서 url과 method를 명시해준 후,
header, parameter, 그리고 body를 넣어준다.</p>
<p>그리고 나서 urlSession의 dataTask 메서드에 앞서 만든 request를 넣어주면 response가 클로저에 담겨서 돌아온다.</p>
<p>다른 점은 body를 구성하기가 정말로 까다롭다는 것이다.</p>
<h3 id="구성-요소">구성 요소</h3>
<p>자 그럼 이제 어떤식으로 구성되어있는지 살펴보자</p>
<ul>
<li>Content-Type header</li>
</ul>
<p>Content-Type과 Boundary에 대한 정보가 있음</p>
<pre><code class="language-swift">Content-Type: multipart/form-data; boundary=3A42CBDB-01A2-4DDE-A9EE-425A344ABA13</code></pre>
<p>Content-Type header에 Boundary가 필요한 이유는 서버로 데이터를 보낼 때 데이터가 한번에 가는게 아니라 여러 부분으로 나뉘어져서 보내진다고 했다. 따라서 서버는 조각난 데이터의 처음과 끝을 boundary를 통해 알 수 있음 </p>
<p>Boundary는 따라서 header에 존재해야한다. 처음에 딱 명시줘야 그 다음부터 소통이 될 것 아냐</p>
<p>그리고 또 Boundary는 유니크해야하는데, 그래서 보통 UUID로 만든다.</p>
<ul>
<li>HTTP body</li>
</ul>
<p>아래는 body 전체코드이다.</p>
<pre><code class="language-swift">--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name=&quot;family_name&quot;

Gupta
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name=&quot;name&quot;

Shubhransh
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name=&quot;file&quot;; filename=&quot;profilepic.jpg&quot;
Content-Type: image/png

-a long string of image data-
--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13—</code></pre>
<p>하나씩 나눠서 설명해보자면</p>
<pre><code class="language-swift">--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13
Content-Disposition: form-data; name=&quot;family_name&quot;

Gupta</code></pre>
<p>Boundary가 나왔으니 body의 시작이겠네
하이픈 두개<code>— -</code>는 새로운 부분이 시작된다는 의미이다. </p>
<p>그리고 한줄 띄우고, 내용 <code>Gupta</code>이 나온다 </p>
<p>그 다음부분도 똑같은 형식임
그 다음다음에는 <code>filename=&quot;profilepic.jpg&quot;</code>이 추가되었는데 이것의 의미는 파일 업로드가 끝나면 그 파일의 이름을 저걸로 부를 수 있다고 알려주는 것이다</p>
<p><code>Content-Type: image/png</code> 는 말 그대로 파일 타입을 말한다.</p>
<p>마지막에 하이픈 두개— - 가 나오는데 이 의미는 body가 끝났다는 것이다.
<code>--Boundary-3A42CBDB-01A2-4DDE-A9EE-425A344ABA13—</code></p>
<h3 id="코드">코드</h3>
<p>이러한 body의 구성 방식을 염두해두고 이제 직접 코드로 body를 만들어보자.</p>
<p>아래 코드가 전체 코드이다.
createMultiPartFormData 메서드에서 보면 알 수 있듯이... lineBreak나 하이픈 같이 사소한것 하나라도 놓치면 코드가 작동이 안된다.</p>
<pre><code class="language-swift">let boundary = &quot;Boundary-\(UUID().uuidString)&quot;

func addProduct(information: NewProductInformation, images: [NewProductImage], completion: @escaping (Result&lt;ProductDetail, Error&gt;) -&gt; Void) {
    guard let url = URLManager.addNewProduct.url else { return }
    var request = URLRequest(url: url, method: .post)
    request.setValue(&quot;multipart/form-data; boundary=\(boundary)&quot;, forHTTPHeaderField: &quot;Content-Type&quot;)
    request.addValue(&quot;819efbc3-71fc-11ec-abfa-dd40b1881f4c&quot;, forHTTPHeaderField: &quot;identifier&quot;)
    request.httpBody = createRequestBody(product: information, images: images)
    createDataTaskWithDecoding(with: request, completion: completion)
}

func createRequestBody(product: NewProductInformation, images: [NewProductImage]) -&gt; Data {
    let parameters = createParams(with: product)
    let dataBody = createMultiPartFormData(with: parameters, images: images)
    return dataBody
}

func createParams(with modelData: NewProductInformation) -&gt; Parameters? {
    guard let parameterBody = JSONParser.encodeToDataString(with: modelData) else { return nil }
    let params: Parameters = [&quot;params&quot;: parameterBody]
    return params
}

func createMultiPartFormData(with params: Parameters?, images: [NewProductImage]?) -&gt; Data {
    let lineBreak = &quot;\r\n&quot;
    var body = Data()

    if let parameters = params {
        for (key, value) in parameters {
            body.append(&quot;--\(boundary + lineBreak)&quot;)
            body.append(&quot;Content-Disposition: form-data; name=\&quot;\(key)\&quot;\(lineBreak + lineBreak)&quot;)
            body.append(&quot;\(value)\(lineBreak)&quot;)
        }
    }

    if let images = images {
        for image in images {
            body.append(&quot;--\(boundary + lineBreak)&quot;)
            body.append(&quot;Content-Disposition: form-data; name=\&quot;\(image.key)\&quot;; filename=\&quot;\(image.fileName)\&quot;\(lineBreak)&quot;)
            body.append(&quot;Content-Type: image/jpeg, image/jpg, image/png\(lineBreak + lineBreak)&quot;)
            body.append(image.data)
            body.append(lineBreak)
        }
    }

    body.append(&quot;--\(boundary)--\(lineBreak)&quot;)

    return body
}

func createDataTaskWithDecoding&lt;T: Decodable&gt;(with request: URLRequest, completion: @escaping (Result&lt;T, Error&gt;) -&gt; Void) {
    let task = urlSession.dataTask(with: request) { data, response, error in
        if let httpResponse = response as? HTTPURLResponse,
           httpResponse.statusCode &gt;= 300 {
            completion(.failure(URLSessionError.responseFailed(code: httpResponse.statusCode)))
            return
        }

        if let error = error {
            completion(.failure(URLSessionError.requestFailed(description: error.localizedDescription)))
            return
        }

        guard let data = data else {
            completion(.failure(URLSessionError.invaildData))
            return
        }
        guard let decodedData = JSONParser.decodeData(of: data, type: T.self) else {
            completion(.failure(JSONError.dataDecodeFailed))
            return
        }
        completion(.success(decodedData))
    }
    task.resume()
}
</code></pre>
<h3 id="alamofire">Alamofire</h3>
<p>이 코드를 Alamofire을 사용해서 똑같이 작성해보면,
앞서 골치아팠던 하이픈이나 띄어쓰기 등을 신경쓰지 않아도 되어 훨씬 간편하다는 것을 알 수 있다.</p>
<p>그냥 multipartFormData.append 메서드를 사용하면 key-value 형태로 데이터를 업로드할 수 있다.</p>
<p>이렇게 간편하고 쓸데없는 곳에 신경을 덜써도 되니 안 사용할 이유가 없다고 생각한다. </p>
<pre><code class="language-swift">func uploadDataWithImage(information: NewProductInformation, images: [NewProductImage], completion: @escaping (Result&lt;Int, Error&gt;) -&gt; Void) {
    guard let url = URLManager.addNewProduct.url else { return }
    let headers: Alamofire.HTTPHeaders = [
        &quot;Content-Type&quot;: &quot;multipart/form-data&quot;,
        &quot;identifier&quot;: &quot;819efbc3-71fc-11ec-abfa-dd40b1881f4c&quot;
    ]

    guard let informationData = JSONParser.encodeToDataString(with: information) else {
        return
    }
    let parameters = [&quot;params&quot;: informationData]

    AF.upload(multipartFormData: { multipartFormData in
        for (key, value) in parameters {
            multipartFormData.append(value.data(using: .utf8)!, withName: key)
        }
        for image in images {
            multipartFormData.append(image.data, withName: image.key, fileName: image.fileName, mimeType: &quot;image/jpeg, image/jpg, image/png&quot;)
        }

    }, to: url, method: .post, headers: headers)
    .response { response in
        guard let statusCode = response.response?.statusCode else {
            return
        }
        completion(.success(statusCode))
    }
}</code></pre>
<h1 id="reference">Reference</h1>
<p><a href="https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#configuring-the-encoding-of-coding-keys">Alamofire 공식문서</a>
<a href="https://medium.com/@Shubhransh-Gupta/swift-mutltiformdata-upload-videos-to-server-as-a-multipart-form-data-using-alamofire-75ae2746cd4">Upload Videos to Server as a Multipart form data Using Alamofire</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionViewDiffableDataSource : SupplementaryView로 Section마다 다른 제목 구현하기]]></title>
            <link>https://velog.io/@dev_jane/UICollectionViewDiffableDataSource-SupplementaryView%EB%A1%9C-Section%EB%A7%88%EB%8B%A4-%EB%8B%A4%EB%A5%B8-%EC%A0%9C%EB%AA%A9-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_jane/UICollectionViewDiffableDataSource-SupplementaryView%EB%A1%9C-Section%EB%A7%88%EB%8B%A4-%EB%8B%A4%EB%A5%B8-%EC%A0%9C%EB%AA%A9-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 17 Jun 2022 09:10:06 GMT</pubDate>
            <description><![CDATA[<p>앞서 기본적인 UICollectionViewDiffableDataSource에 대해 설명한 포스팅과 이어집니다~!</p>
<h3 id="headerview-만들기-코드로-바로-고">headerView 만들기 코드로 바로 고</h3>
<h4 id="1-uicollectionreusableview-를-상속한-커스텀-뷰-생성">1. UICollectionReusableView 를 상속한 커스텀 뷰 생성</h4>
<pre><code class="language-swift">class MovieListHeaderView: UICollectionReusableView {
    lazy var label: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.font = .preferredFont(forTextStyle: .title1)
        label.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(label)
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setConstraints()
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;Not Implemented&quot;)
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        initialize()
    }

    func initialize() {
        self.label.text = nil
    }

    func setConstraints() {
        NSLayoutConstraint.activate([
            self.label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15),
            self.label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 10),
            self.label.topAnchor.constraint(equalTo: self.topAnchor, constant: 15),
            self.label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 10)
        ])
    }
}</code></pre>
<h4 id="2-collectionview에-앞서-생성한-커스텀-뷰-register">2. collectionview에 앞서 생성한 커스텀 뷰 register</h4>
<p>생성한 애를 사용하려면 register 해줘야겠지?</p>
<pre><code class="language-swift">self.collectionView.register(
    MovieListHeaderView.self,
    forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
    withReuseIdentifier: &quot;MovieListHeaderView&quot;)</code></pre>
<h4 id="3-uicollectionviewdiffabledatasource의-supplementaryviewprovider-설정하여-데이터-뿌려주기">3. UICollectionViewDiffableDataSource의 supplementaryViewProvider 설정하여 데이터 뿌려주기</h4>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/ad63dd2e-ae07-49a8-89e2-903617cad44f/image.png" alt="">
dataSource의 supplementaryViewProvider 프로퍼티에 해당 headerView가 속한 collectionView와 indexPath가 넘어오는데,</p>
<p>collectionView로는 dequeueReusableSupplementaryView 를 통해 등록한 supplementaryView를 꺼내오고</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/72b624a5-e394-4edf-83eb-7336a68f5d25/image.png" alt="">
indexPath로는... 해당 headerview의 section이 뭔지 알 수 있다 ㅎ
indexPath로 dataSource의 snapshot에서 해당하는 section 데이터를 가져와 앞서 꺼낸 headerview의 title을 설정해준다.</p>
<pre><code class="language-swift">self.movieListDataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) in
    guard let header = collectionView.dequeueReusableSupplementaryView(
        ofKind: UICollectionView.elementKindSectionHeader,
        withReuseIdentifier: &quot;MovieListHeaderView&quot;,
        for: indexPath) as? MovieListHeaderView else {
        return MovieListHeaderView()
    }

    let section = self.movieListDataSource.snapshot().sectionIdentifiers[indexPath.section]
    header.label.text = section.description

    return header
}</code></pre>
<h4 id="4-layout-설정">4. Layout 설정</h4>
<p>레이아웃은 앞 포스팅에서 기본적으로 설정해준 것을 바탕으로
section 부분에만 headerView를 추가해주면 된다. </p>
<p>section.boundarySupplementaryItems를 설정해주면 완료~!</p>
<pre><code class="language-swift">    private func createLayout() -&gt; UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(
            top: 20,
            leading: 20,
            bottom: 20,
            trailing: 20)

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

        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging
        section.interGroupSpacing = 0
        //요기
        let headerSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .estimated(44))
        let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerSize,
            elementKind: UICollectionView.elementKindSectionHeader,
            alignment: .top)
        section.boundarySupplementaryItems = [sectionHeader]

        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }</code></pre>
<p><a href="https://swiftsenpai.com/development/list-interactive-custom-header/">Custom Header</a>
<a href="https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/supplementaryviewprovider">Developer Documentation: SupplementaryViewProvider</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기본적인 UICollectionViewDiffableDataSource 설정하기]]></title>
            <link>https://velog.io/@dev_jane/%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-UICollectionViewDiffableDataSource-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_jane/%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-UICollectionViewDiffableDataSource-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 17 Jun 2022 08:40:14 GMT</pubDate>
            <description><![CDATA[<p>UICollectionViewDiffableDataSource와 UICollectionViewCompositionalLayout을 사용하여 아래와 같은 뷰를 그려보았다.</p>
<p>한 CollectionView가 여러 Section으로 나눠져 있고, 각 Section마다 Item들이 가로로 스크롤이 되는 형식으로 구현하였다. </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/5dc4d8ba-1fbd-422d-a21a-44521da7fa59/image.png" alt=""></p>
<p>이번 포스팅에서는 UICollectionViewDiffableDataSource에 대해 소개해보겠다.
처음 적용해보는 section이라는 개념 때문에 꽤 애를 먹어서... 정리해보려고 한다.</p>
<p>그럼 시작~!
iOS 13부터 UICollectionViewDataSource를 대체할 수 있는 UICollectionViewDiffableDataSource를 사용할 수 있다.</p>
<h2 id="uicollectionviewdatasource와의-차이점">UICollectionViewDataSource와의 차이점</h2>
<p>기존 UICollectionViewDataSource에서는 컬렉션뷰에 몇개의 아이템을 보여줘야하는지.. 등에 대해서도 알려줘야 했다면 </p>
<p>이 새로나온 diffable datasource는 말 그대로 diff 차이를 스스로 계산해서 업데이트 된 뷰의 데이터를 전달한다면 기존과 달라진 부분을 스스로 계산하여 달라진 부분만 업데이트가 가능하다.</p>
<p>이러한 성질때문에 각 요소들이 같은지 다른지 확인하기 위해서 UICollectionViewDiffableDataSource의 데이터는 모두 Hashable해야한다.</p>
<p>기존 UICollectionViewDataSource에서는 컬렉션뷰가 이미 업데이트 된 상황에서, 새로운 데이터로 업데이트해야할 때 reloadData()를 사용했다. 
하지만...뷰의 요소를 수정, 삭제, 추가할때... animation이 적용이 안된다는 점이 한계였다.</p>
<p>UICollectionViewDiffableDataSource는 이러한 한계를 극복해버렸다.</p>
<ul>
<li>뷰 업데이트시 자동으로 애니메이션이 적용되고</li>
<li>CollectionView와 DataSource 간에 데이터 자동으로 동기화되어
사용하기 아주 편리하다!</li>
</ul>
<p>그럼 이제 UICollectionViewDiffableDataSource의 기본적인 사용 방법에 대해 알아보자  </p>
<h3 id="uicollectionviewdiffabledatasource-구현-순서">UICollectionViewDiffableDataSource 구현 순서</h3>
<p>대략적으로 큰 틀은,</p>
<ul>
<li>CollectionView에 사용될 셀을 등록하고, </li>
<li>dataSource 역할을 하는 UICollectionViewDiffableDataSource에서 cell에 데이터를 어떻게 채워줄지 설정해준 후, </li>
<li>실제 데이터가 넘어오면 Snapshot에 추가하여 앞서 설정해준 dataSource에 적용하는 흐름이다.</li>
</ul>
<h2 id="코드와-함께-araboza">코드와 함께 Araboza</h2>
<h4 id="1-뷰컨에-스토리보드로-추가한-collectionview-준비">1. 뷰컨에 스토리보드로 추가한 collectionView 준비</h4>
<pre><code class="language-swift">final class MovieListViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
}</code></pre>
<h4 id="2-uicollectionviewcell-상속한-커스텀-셀-생성">2. UICollectionViewCell 상속한 커스텀 셀 생성</h4>
<pre><code class="language-swift">class MovieListCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var posterImageView: DownloadableUIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var originalLanguageLabel: UILabel!
    @IBOutlet weak var genresLabel: UILabel!

    private var viewModel: MovieListItemViewModel!

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func prepareForReuse() {
        posterImageView.cancleLoadingImage()
    }

    func configure(with viewModel: MovieListItemViewModel) {
        self.viewModel = viewModel

        guard let posterPath = viewModel.posterPath,
              let url = MovieURL.image(posterPath: posterPath).url,
              let title = viewModel.title else {
            return
        }
        self.posterImageView.kf.setImage(with: url)
        self.titleLabel.text = title
        self.originalLanguageLabel.text = viewModel.originalLanguage
        self.genresLabel.text = viewModel.genres
    }

}</code></pre>
<h4 id="3-collectionview에-앞서-생성한-커스텀-셀-register">3. collectionview에 앞서 생성한 커스텀 셀 register</h4>
<pre><code class="language-swift">     self.collectionView.register(
        UINib(nibName: &quot;MovieListCollectionViewCell&quot;, bundle: nil),
        forCellWithReuseIdentifier: &quot;MovieListCollectionViewCell&quot;)</code></pre>
<h4 id="4-uicollectionviewdiffabledatasource-설정">4. UICollectionViewDiffableDataSource 설정</h4>
<ul>
<li>SectionIdentifierType, ItemIdentifierType을 정해줌</li>
</ul>
<p>여기서 SectionIdentifierType로 등록해줄 Section 타입은, 앞서 말한 것처럼 Hashable 해야한다.</p>
<p>한 section이 movies 배열을 가지고 있는 구조이고,
unique하게 identify되게 하기 위해서 id 프로퍼티를 생성해주었다.</p>
<pre><code class="language-swift">final class Section: Hashable {
    var id = UUID()
    var title: String
    var movies: [MovieListItemViewModel]

    init(title: String, movies: [MovieListItemViewModel]) {
        self.title = title
        self.movies = movies
    }

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

    static func == (lhs: Section, rhs: Section) -&gt; Bool {
        lhs.id == rhs.id
    }
}</code></pre>
<p>ItemIdentifierType은 MovieListItemViewModel 타입으로 생성하였는데, 아래 아래 코드에서 collectionView.dequeueReusableCell을 통해 셀을 configure할 때 사용하는 데이터이다.</p>
<pre><code class="language-swift">struct MovieListItemViewModel: Hashable {
    let id: Int
    let posterPath: String?
    let title: String?
    let originalLanguage: String
    let genres: String
    let section: MovieListURL

    init(movie: MovieListItem, section: MovieListURL) {
        self.id = movie.id
        self.posterPath = movie.posterPath
        self.title = movie.title
        self.originalLanguage = movie.originalLanguage.formatted
        self.genres = movie.genres.map {$0.name.uppercased()}
                                    .joined(separator: &quot;/&quot;)
        self.section = section
    }
}</code></pre>
<ul>
<li>UICollectionViewDiffableDataSource의 
인스턴스를 생성하여 뷰에 데이터를 어떻게 뿌려줄지 설정</li>
</ul>
<p>이니셜라이저의 후행 클로저로 collectionView, indexPath, itemIdentifier 가 넘어오면, 
dequeueReuseableCell로 앞서 등록해준 셀에 대한 configuration을 해준다
<img src="https://velog.velcdn.com/images/dev_jane/post/d646b757-3daf-43e4-82d0-cdbfc45f19c8/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_jane/post/33cea454-5116-4a3c-88d7-d51cd182f993/image.png" alt=""></p>
<pre><code class="language-swift">    private var movieListDataSource: DataSource!
    private typealias DataSource = UICollectionViewDiffableDataSource&lt;Section, MovieListItemViewModel&gt;

    self.movieListDataSource = DataSource(collectionView: self.collectionView) { collectionView, indexPath, itemIdentifier in
       guard let cell = collectionView.dequeueReusableCell(
          withReuseIdentifier: &quot;MovieListCollectionViewCell&quot;,
          for: indexPath) as? MovieListCollectionViewCell else {
            return UICollectionViewCell()
    }
       cell.configure(with: itemIdentifier)
            return cell
    }</code></pre>
<h4 id="5-nsdiffabledatasourcesnapshot-설정">5. NSDiffableDataSourceSnapshot 설정</h4>
<ul>
<li><p>(DiffableDataSource와 동일한) SectionIdentifierType, ItemIdentifierType을 정해줌 </p>
</li>
<li><p>넘어온 데이터(Section, Item)를 snapshot에 append하고, snapshot을 dataSource에 apply해줌</p>
<pre><code class="language-swift">  private typealias Snapshot = NSDiffableDataSourceSnapshot&lt;Section, MovieListItemViewModel&gt;

  var snapshot = Snapshot()
  snapshot.appendSections(sections)
  sections.forEach { section in
      snapshot.appendItems(section.movies, toSection: section)
  }
  self.movieListDataSource?.apply(snapshot)</code></pre>
</li>
</ul>
<p>수정사항이 있을때마다 snapshot에만 apply해주면 간편하게 데이터를 업데이트 가능하다.</p>
<h4 id="6-layout-설정-uicollectionviewcompositionallayout">6. Layout 설정: UICollectionViewCompositionalLayout</h4>
<pre><code class="language-swift">private func createLayout() -&gt; UICollectionViewLayout {
    let itemSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .fractionalHeight(1.0))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    item.contentInsets = NSDirectionalEdgeInsets(
        top: 20,
        leading: 20,
        bottom: 20,
        trailing: 20)

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

    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .paging
    section.interGroupSpacing = 0

    let layout = UICollectionViewCompositionalLayout(section: section)
    return layout
}</code></pre>
<p>레이아웃 설정이 완료되었다면, 레이아웃을 리턴하여 viewDidLoad에서 collectionViewLayout을 세팅해준다.</p>
<pre><code class="language-swift">collectionView.setCollectionViewLayout(createLayout(), animated: true)</code></pre>
<p>전체코드</p>
<pre><code class="language-swift">final class MovieListViewController: UIViewController, UICollectionViewDelegate {
    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var collectionView: UICollectionView!

    let viewModel: MovieListViewModel
    private let disposeBag = DisposeBag()
    private var movieListDataSource: DataSource!

    private typealias DataSource = UICollectionViewDiffableDataSource&lt;Section, MovieListItemViewModel&gt;
    private typealias Snapshot = NSDiffableDataSourceSnapshot&lt;Section, MovieListItemViewModel&gt;

    override func viewDidLoad() {
        super.viewDidLoad()
        registerCollectionViewItems()
        configureDataSource()
        configureBind()
        collectionView.setCollectionViewLayout(createLayout(), animated: true)
        self.collectionView.backgroundColor = .black
    }

    init?(viewModel: MovieListViewModel, coder: NSCoder) {
        self.viewModel = viewModel
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }

    private func registerCollectionViewItems() {
        self.collectionView.register(
            UINib(nibName: &quot;MovieListCollectionViewCell&quot;, bundle: nil),
            forCellWithReuseIdentifier: &quot;MovieListCollectionViewCell&quot;) 
    }

    private func configureDataSource() {
        self.movieListDataSource = DataSource(collectionView: self.collectionView) { collectionView, indexPath, itemIdentifier in
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: &quot;MovieListCollectionViewCell&quot;,
                for: indexPath) as? MovieListCollectionViewCell else {
                return UICollectionViewCell()
            }
            cell.configure(with: itemIdentifier)
            return cell
        }

    private func populate(with sections: [Section]) {
        var snapshot = Snapshot()
        snapshot.appendSections(sections)
        sections.forEach { section in
            snapshot.appendItems(section.movies, toSection: section)
        }
        self.movieListDataSource?.apply(snapshot)
    }

    private func configureBind() {
        let input = MovieListViewModel.Input(viewWillAppear: self.rx.viewWillAppear.asObservable())
        let output = viewModel.transform(input)

        output.sectionObservable
            .withUnretained(self)
            .subscribe(onNext: { (self, sections) in
                self.populate(with: sections)
                print(&quot;성공&quot;)
            }).disposed(by: disposeBag)
    }

    private func createLayout() -&gt; UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1.0),
            heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(
            top: 20,
            leading: 20,
            bottom: 20,
            trailing: 20)

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

        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging
        section.interGroupSpacing = 0

        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }

}</code></pre>
<p>Section마다 타이틀을 다르게 구현한 부분은 다음 포스팅에서 이어서 설명하겠다.</p>
<h1 id="reference">Reference</h1>
<p><a href="https://www.raywenderlich.com/8241072-ios-tutorial-collection-view-and-diffable-data-source">Collection View and Diffable Data Source</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionView 셀 재사용 문제: 빠르게 스크롤시 잘못된 이미지가 나타나는 현상]]></title>
            <link>https://velog.io/@dev_jane/UICollectionView-%EC%85%80-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EB%AC%B8%EC%A0%9C-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EC%8B%9C-%EC%9E%98%EB%AA%BB%EB%90%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EB%82%98%ED%83%80%EB%82%98%EB%8A%94-%ED%98%84%EC%83%81</link>
            <guid>https://velog.io/@dev_jane/UICollectionView-%EC%85%80-%EC%9E%AC%EC%82%AC%EC%9A%A9-%EB%AC%B8%EC%A0%9C-%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EC%8B%9C-%EC%9E%98%EB%AA%BB%EB%90%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EB%82%98%ED%83%80%EB%82%98%EB%8A%94-%ED%98%84%EC%83%81</guid>
            <pubDate>Fri, 10 Jun 2022 05:57:37 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@dev_jane/UICollectionView-%EC%85%80%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0-NSCache%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%BA%90%EC%8B%B1">이전 글</a>에서는 이미지 로딩 속도를 개선하는 방법에 대해 알아보았다.
읽고 오시면 이번 글 이해에 도움이 됩니다!😆</p>
<h2 id="빠르게-스크롤시-잘못된-이미지가-나타나는-현상">빠르게 스크롤시 잘못된 이미지가 나타나는 현상</h2>
<p>UICollectionView나 UITableView를 만들때 셀에 이미지가 포함된 경우 빠르게 스크롤시 잘못된 이미지가 들어가는 상황이 발생한다. </p>
<h2 id="문제의-원인">문제의 원인</h2>
<h3 id="이런-상황이-발생하는-이유는">이런 상황이 발생하는 이유는</h3>
<h4 id="--하나의-셀이-계속-재사용되면서-여러-정보를-표현함">- 하나의 셀이 계속 재사용되면서 여러 정보를 표현함</h4>
<p>iOS에서는 화면에 최대로 보여지는 셀 개수로만 컬렉션뷰를 구성하고, 스크롤하는 순간 화면에서 사라진 셀을 재사용하여 화면에 새로 보여지는 셀을 만든다.</p>
<p>이렇게 셀을 재사용하면 매번 새로운 셀을 생성할 필요가 없기때문에 성능상의 이점을 가질 수 있지만, 반대로 한 셀에 여러 정보들이 지워졌다가 담겼다가 하는 과정에서 잘못된 정보가 나타날 수 있는 문제가 생긴다.</p>
<h4 id="--비동기적으로-작동하는-이미지-로딩-작업">- 비동기적으로 작동하는 이미지 로딩 작업</h4>
<p>이미지 로딩 작업은 무거운 작업이기 때문에 메인큐가 아닌 글로벌 큐에서 작업한다. 또한 비동기적으로 작동하기 때문에 끝나는 순서를 보장하지 못한다. </p>
<h3 id="셀-재사용--비동기-작업으로-나타난-헬파티임">셀 재사용 + 비동기 작업으로 나타난 헬파티임</h3>
<p>그러니깐 셀이 재사용되면서 새로운 이미지를 받아와야하는데 </p>
<ul>
<li>새로운 이미지가 만약 늦게 로딩되어서 그 전까지는 기존 이미지가 보여지는 문제가 있을 수도 있고</li>
<li>더 최악의 경우에는 새로운 이미지가 로딩이 되었는데 그 뒤에 기존 이미지가 로딩이 되어서 아예 잘못된 이미지가 보여질 수도 있는 것이다.</li>
</ul>
<p>이미지 로딩 작업은 비동기라서 아무도 그 순서를 예측할 수 없기 때문에,,, ㅎ</p>
<h3 id="셀이-재사용되는-과정을-살펴보면">셀이 재사용되는 과정을 살펴보면</h3>
<ol>
<li>화면에서 셀이 벗어나자마자</li>
<li>prepareForReuse에서 초기화된 후</li>
<li>재사용 큐로 들어가고</li>
<li>재사용 큐에서 dequeue되어 다시 사용된다.
<img src="https://velog.velcdn.com/images/dev_jane/post/ac32bef0-f155-4341-b522-835aca41140b/image.png" alt=""></li>
</ol>
<p>이 순서가 맞는지 궁금해서 직접 실험해봤는데 
각 셀에 UUID를 주고 UICollectionViewCell의 prepareForReuse 메서드와 UICollectionView DataSource의 cellForItemAt 메서드를 각각 프린트해보았다.</p>
<p>아래 그림에서 6개의 셀이 처음에 dequeue되고 나서 재사용되는 모습을 볼 수 있다.
흥미로운 점은 재사용 큐에 들어간 순서대로 dequeue될줄 알았는데 아니라는 점,,, 
<img src="https://velog.velcdn.com/images/dev_jane/post/58f098fe-368b-443e-8532-02f7c8e659ca/image.png" alt=""></p>
<h3 id="prepareforreuse">prepareForReuse</h3>
<p>그렇다면 지금까지 말한 prepareForReuse는 대체 뭐냐</p>
<p>컬렉션뷰의 prepareForReuse 문서에 가보면 
prepareForReuse는 컬렉션뷰가 셀을 디큐하기 전에 불리는 메서드로 
프로퍼티를 기본값으로 설정하거나 뷰를 재사용할 수 있는 상태로 만들때 override해서 사용하라고 한다.
근데 새로운 데이터를 할당하지는 말라고 한다(그건 dataSource의 역할이라)</p>
<p>그렇다는 것은.. 여기서 이미지뷰를 초기화해도 된다는것...?</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/f8f31af4-6f8e-4399-b12d-a5a701df29d8/image.png" alt=""></p>
<p>근데 헷갈리는건 테이블뷰의 prepareForReuse 문서에 가보면 
prepareForReuse에서는 성능상의 이슈를 피하기 위해서 셀의 컨텐츠와 무관한 속성만 리셋하라고 한다.</p>
<p>그렇다는 것은.. 여기서 이미지뷰를 초기화하면 안된다는것...?
컬렉션뷰랑 말이 다른데..? 검색을 해보아도 이건 의견이 분분하다...
이 부분은 아직 잘 모르겠네,, </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/a9633d52-ae5d-4a1b-89c1-c8eb68a4c360/image.png" alt=""></p>
<h2 id="해결방법">해결방법</h2>
<p>두가지 방법으로 해결할 수 있다.</p>
<ol>
<li>네트워크 통신 작업 전에 이미지를 초기화하기</li>
<li>셀이 다시 사용되기 전에 불리는 prepareForReuse에서 진행중인 네트워크 요청 취소하기</li>
</ol>
<p>이전 포스팅에서 만들었던 UIImageView의 서브클래스인 DownloadableImageView이다.</p>
<p>여기서 두가지 부분을 추가해주었는데</p>
<ol>
<li><p>네트워크 통신을 통해 이미지를 로딩하기 전에 
self.image = UIImage() 를 넣어주어 초기화하기
이미 캐싱된 이미지를 가져오는 경우에는 비동기 작업이 없기때문에 잘못된 이미지가 보여질 가능성이 없다.
따라서 캐싱된 이미지가 없을때 네트워크 통신을 하게 되는데 그 전에 셀의 이미지를 초기화하여 셀이 재사용되기전의 기존 이미지가 남아있다면 초기화해준다.</p>
</li>
<li><p>dataTask를 취소하는 cancelLoadingImage 메서드 생성
셀이 재사용되려고 큐에 들어갔다가 나온 시점에서도 만약 셀이 재사용되기 전의 이미지를 받아오는 작업이 끝나지 않았다면 그 작업을 취소한다. </p>
</li>
</ol>
<p>아래 사진에서 breakpoint로 새로 추가한 두 부분을 표시해뒀다.
<img src="https://velog.velcdn.com/images/dev_jane/post/151e898a-b016-479b-b0e6-41b95e6e0444/image.png" alt=""></p>
<p>그리고 커스텀 컬렉션뷰 셀에서 prepareForReuse를 override하여
아까 만든 cancleLoadingImage를 실행한다.
<img src="https://velog.velcdn.com/images/dev_jane/post/db49dd94-3c1d-48ce-9d54-b6e1906060bc/image.png" alt=""></p>
<p>이렇게 하면 셀이 재사용되기 전에 아직도 진행중인 네트워크 통신이 있다면 취소되고, 기존 이미지가 남아있는 경우 새로 받아오는 네트워크 통신 작업 전에 초기화해줌으로써 문제점들을 해결할 수 있다~!</p>
<h3 id="더-알아보고-싶은-부분">더 알아보고 싶은 부분</h3>
<p>해결 방안을 검색해보다가 찾은... 
셀의 indexPath나 이미지의 URL를 비교해서 셀에 알맞은 데이터가 들어가는지 확인하는 방법도 있었는데 
일단 위에서 소개한 두 방법으로 해결이 되어서 이건 다음에 알아보겠다. ㅎㅎ
<a href="https://stackoverflow.com/questions/59107182/swift-remove-cell-from-tableview-after-button-tap/59108366#59108366">indexPath 구하는 방법</a></p>
<h1 id="reference">Reference</h1>
<p><a href="https://stackoverflow.com/questions/59207307/uitableviewcell-shows-the-wrong-image-while-images-load">UITableViewCell shows the wrong image while images load</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UICollectionView 셀의 이미지 로딩 속도 개선: NSCache로 이미지 캐싱]]></title>
            <link>https://velog.io/@dev_jane/UICollectionView-%EC%85%80%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0-NSCache%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%BA%90%EC%8B%B1</link>
            <guid>https://velog.io/@dev_jane/UICollectionView-%EC%85%80%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A1%9C%EB%94%A9-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0-NSCache%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%BA%90%EC%8B%B1</guid>
            <pubDate>Thu, 09 Jun 2022 15:53:19 GMT</pubDate>
            <description><![CDATA[<p>UICollectionView나 UITableView를 만들때 셀에 이미지가 포함된 경우 다음과 같은 두가지 상황을 고려해야한다.
<strong>1. 이미지가 로딩되는 속도가 느린 문제
2. 빠르게 스크롤시 맞지 않는 이미지가 나타나는 문제</strong></p>
<p>이번 포스팅에서는 첫번째 이슈인 이미지 로딩 속도에 대해 알아보자</p>
<h2 id="이미지가-로딩되는-속도가-느린-문제">이미지가 로딩되는 속도가 느린 문제</h2>
<p>두가지 방법으로 해결 가능하다.
<strong>1. 이미지 네트워크 통신 global Queue에서 하기
2. 이미지 캐싱</strong></p>
<h3 id="이미지-네트워크-통신-global-queue에서-하기">이미지 네트워크 통신 global Queue에서 하기</h3>
<p>다들 테이블뷰의 cellForRowAt이나 컬렉션뷰의 cellForItemAt에서는 무거운 작업을 하지 않는 것이 좋다는 것을 알고 있을 것이다
이 메서드는 최대한 가볍게 유지해야하기 때문에,,</p>
<pre><code class="language-swift">    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&gt; UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: &quot;MovieListCollectionViewCell&quot;, for: indexPath) as! MovieListCollectionViewCell
        cell.configure(with: viewModel.itemViewModels[indexPath.row])
        return cell
    }</code></pre>
<p>따라서 URL을 통해 이미지를 로딩하는 작업같이 무거운 작업은  비동기적으로 일어나기도 해서 메인 스레드에서 작업하기보다는 concurrent한 Global Queue에서 하도록 하자.</p>
<p>이미지 로딩을 하기 위해서 URLSession.shared.dataTask를 활용해보자.
이 경우에는 URLSession 자체가 Global Queue에서 동작하기 때문에 직접 Dispatch Queue를 만들어줄 필요 없이 사용 가능하다.</p>
<pre><code class="language-swift"> if let imageUrl = URL(string: urlString) {
    let urlRequest = URLRequest(url: imageUrl, cachePolicy: .returnCacheDataElseLoad)
    self.dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
        if let _ = error {
            DispatchQueue.main.async {
                self.image = UIImage()
            }
            return
        }
    }
    self.dataTask?.resume()
}</code></pre>
<h3 id="이미지-캐싱">이미지 캐싱</h3>
<p>다음으로는 캐시다.
캐시에는 디스크 캐시와 메모리 캐시 두 종류가 있다.</p>
<ul>
<li><p>디스크 캐시의 경우 데이터를 파일 형태로 디스크 영역에 저장하여 앱을 종료하고 다시 실행해도 디스크에 캐시가 남아있지만, </p>
</li>
<li><p>메모리 캐시의 경우에는 앱이 종료되면 사용중이던 메모리 영역을 반환하여 앱 종료와 함께 캐싱된 정보도 같이 사라지게 된다...ㅎ</p>
</li>
</ul>
<p>따라서 디스크 캐시로는 앱이 처음 실행되었을 때의 이미지 로딩속도를 개선할 수 있고, 메모리 캐시로는 이미 한번 보여진 이미지의 로딩속도를 개선할 수 있다.</p>
<h4 id="nscache로-이미지-메모리-캐싱">NSCache로 이미지 메모리 캐싱</h4>
<p>앱을 실행하는 도중에 컬렉션뷰를 스크롤하여 셀이 화면 밖으로 나간 경우 셀이 reuseable queue에 들어가게 되는데,
이때 셀의 모든 데이터가 초기화되어서 다시 뒤로 스크롤하면 이미지를 다시 로딩해야하는 상황이 발생한다.
이때 한번 네트워크 통신을 통해 받아온 이미지는 캐싱을 하여 더이상 무거운 네트워크 통신 작업을 하지 않고 캐시된 이미지를 사용하여 이미지 로딩 속도를 개선할 수 있다.</p>
<pre><code class="language-swift">guard let urlString = MovieURL.image(posterPath: posterPath).url?.absoluteString else {
    return
}
let cacheKey = NSString(string: urlString)
if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) {
    self.image = cachedImage
    return
}</code></pre>
<h4 id="디스크-캐싱에-대한-고민">디스크 캐싱에 대한 고민</h4>
<p>메모리 캐싱을 통해 한번 화면에 보여진 이미지를 다시 볼 때는 속도가 개선된다고 해도,
앱을 실행할때는 초기화되기 때문에 초기 화면에서 이미지를 로딩하려면 3초정도의 딜레이가 발생한다.</p>
<p>따라서 디스크 캐시를 추가로 구현하여 
한번 받아온 리스트의 이미지들을 앱을 종료하고 다시 실행하더라도 캐싱된 이미지들을 사용하여 이미지 로딩속도를 개선할 수 있지 않을까는 생각도 해보았다. </p>
<p>디스크 캐싱은 UserDefault나 FileManager에 저장하는 형태로 구현하는데,
UserDefault의 경우 사용자의 간단한 정보를 저장하는 용도이기 때문에 배제하였다.</p>
<p>서치해보니... <a href="https://github.com/stephencelis/SQLite.swift/issues/783#:~:text=file%20in%20swift-,It%27ll%20be%20something%20like%3A,-import%20Foundation%0Aimport">이 글</a>에서처럼
FileManager에 이미지 data를 저장해놓고 저장된 path를 SQLite에 저장하는 방식을 많이 사용하는 것 같다.</p>
<p>처음에는 Core Data에 저장하는 방식을 생각했는데,
서치해보니 <a href="https://stackoverflow.com/questions/4158286/storing-images-in-core-data-or-as-file?rq=1">스택오버플로우나</a> <a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/Performance.html#//apple_ref/doc/uid/TP40001075-CH25-SW11">CoreData 공식문서</a>에서도 이미지나 소리 데이터를 Core Data에 저장하는것은 비추천이라고 한다.</p>
<p>Core Data 공식문서에서 SQLite를 추천하는 모습..ㅋㅋㅋㅋ
<img src="https://velog.velcdn.com/images/dev_jane/post/4a38ec8f-7626-485f-83c0-e0d9ac12a826/image.png" alt=""></p>
<p>디스크 캐싱을 적용한 예를 찾아보았는데...
카카오톡에서 이미지를 다운받지 않고 그냥 보기만 하더라도 처음 봤을때만 이미지를 받아오고, 디스크 캐시에 저장한다고 한다.
따라서 그 다음에는 앱을 껐다가 켰음에도 불구하고 캐싱된 이미지를 불러올 수 있는 것이다.</p>
<p>카카오톡 사용하다보면 이렇게 캐시 용량이 늘어난다. 심한 사람은 몇기가까지 찬다는데 나는 적군..ㅎ
<img src="https://velog.velcdn.com/images/dev_jane/post/f16403c4-ca98-4d5e-9390-079d75f9a3a8/image.jpg" alt=""></p>
<p>암튼 어떻게 구현할지 찾아보기는 했는데, 아무래도 디스크에 저장하는 방식은 좀 아닌것 같다.</p>
<p>내 앱의 경우에는 모든 영화 포스터를 디스크 캐싱해놓기는 사용자 입장에서 부담스러울 것 같다.</p>
<p>그냥 메모리 캐싱만 사용하는 걸로~ㅎ</p>
<p>-&gt; 처음에는 이렇게 생각했는데 결국 디스크 캐싱도 사용하였고, 그 대신 캐시의 유효기간과 최대 저장가능 용량을 제한하였다.</p>
<h3 id="결론-nscache만-사용함">결론, NSCache만 사용함</h3>
<p>NSCache 객체를 가진 ImageCacheManager와,
UIImageView를 상속받은 DownloadableUIImageView 클래스를 만들어서 이미지 로딩을 관리하였다.</p>
<p>getImage()는 url을 이용하여 이미지를 가져오는 함수이다.</p>
<p>먼저 String으로 변환한 url을 key로 하여 저장된 이미지가 캐시에 존재하는지 확인하고 나서 캐시에 없다면 그제서야 네트워크 통신을 통해 이미지를 가져오고 캐싱한다.</p>
<pre><code class="language-swift">class ImageCacheManager {
    static let shared = NSCache&lt;NSString, UIImage&gt;()
    private init() {}
}

class DownloadableUIImageView: UIImageView {
    var dataTask: URLSessionDataTask?

    func getImage(with posterPath: String) {
        self.image = UIImage()

        guard let urlString = MovieURL.image(posterPath: posterPath).url?.absoluteString else {
            return
        }
        let cacheKey = NSString(string: urlString)
        if let cachedImage = ImageCacheManager.shared.object(forKey: cacheKey) {
            self.image = cachedImage
            return
        }

        if let imageUrl = URL(string: urlString) {
            let urlRequest = URLRequest(url: imageUrl, cachePolicy: .returnCacheDataElseLoad)
            self.dataTask = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                if let _ = error {
                    DispatchQueue.main.async {
                        self.image = UIImage()
                    }
                    return
                }
                DispatchQueue.main.async {
                    if let data = data, let image = UIImage(data: data) {
                        ImageCacheManager.shared.setObject(image, forKey: cacheKey)
                        self.image = image
                    }
                }
            }
            self.dataTask?.resume()
        }
    }

    func cancelLoadingImage() {
        dataTask?.cancel()
        dataTask = nil
    }
}
</code></pre>
<table>
<thead>
<tr>
<th>개선 전</th>
<th>개선 후</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/dev_jane/post/6ab5b759-054f-442f-9feb-97f23479da32/image.gif" alt=""></td>
<td><img src="https://velog.velcdn.com/images/dev_jane/post/f763b695-b7d3-4ac5-be05-cb180187552c/image.gif" alt=""></td>
</tr>
<tr>
<td>오른쪽에서 왼쪽으로 스크롤시 버벅임</td>
<td>오른쪽에서 왼쪽으로 스크롤시 자연스러움</td>
</tr>
<tr>
<td>- 이미 네트워크 통신이 완료되어 캐시된 셀의 이미지를 보여줄때 시간 단축됨</td>
<td></td>
</tr>
<tr>
<td>- 하지만 스크롤시 처음으로 네트워크 통신을 통해 이미지를 받아오는 경우에는 아직 해결하지 못함</td>
<td></td>
</tr>
<tr>
<td>-&gt; 디스크 캐싱으로 해결 ^^*</td>
<td></td>
</tr>
</tbody></table>
<h1 id="reference">Reference</h1>
<p><a href="https://greate-future.tistory.com/103">https://greate-future.tistory.com/103</a>
<a href="https://yusufkamilak.com/the-correct-approach-for-image-loading-in-uicollectionview/">https://yusufkamilak.com/the-correct-approach-for-image-loading-in-uicollectionview/</a>
<a href="https://developer.apple.com/videos/play/wwdc2018/219/?time=988">WWDC19 Image and Graphics Best Practices</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UIViewController 서브클래스의 custom initializer 만들기('required' initializer 'init(coder:)' must be provided by subclass of 'UIViewController')]]></title>
            <link>https://velog.io/@dev_jane/UIViewController-%EC%84%9C%EB%B8%8C%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-custom-initializer-%EB%A7%8C%EB%93%A4%EA%B8%B0required-initializer-initcoder-must-be-provided-by-subclass-of-UIViewController</link>
            <guid>https://velog.io/@dev_jane/UIViewController-%EC%84%9C%EB%B8%8C%ED%81%B4%EB%9E%98%EC%8A%A4%EC%9D%98-custom-initializer-%EB%A7%8C%EB%93%A4%EA%B8%B0required-initializer-initcoder-must-be-provided-by-subclass-of-UIViewController</guid>
            <pubDate>Sun, 05 Jun 2022 06:29:10 GMT</pubDate>
            <description><![CDATA[<p><code>UIViewController</code>을 상속받아서 커스텀뷰컨을 만드는 작업은 정말.. 항상하는 작업이다. 
대부분은 따로 직접 이니셜라이저를 구현하지 않고 사용하기 때문에 특별한 문제가 없다.</p>
<p>하지만... 뷰컨에 뷰모델을 생성자 주입하려고 이니셜라이저를 만들었더니... 
이런 에러가 나왔다.
<code>UIView</code>와 <code>UIViewController</code>를 상속해서 이니셜라이저를 작성하려고 하면 <code>&#39;required&#39;</code> 이니셜라이저인 <code>init(coder:)</code>을 작성하라고 한다.
<img src="https://velog.velcdn.com/images/dev_jane/post/d2b6dcb0-c86f-4a1f-8c1e-2fd08c888c48/image.png" alt=""></p>
<h3 id="항상-궁금했던-부분들">항상 궁금했던 부분들</h3>
<ol>
<li>왜 대체 <code>required init</code>을 구현하라는 것일까</li>
<li>그럼 원래 이니셜라이저가 없었을때는 왜 구현하라고 안했나</li>
<li>뷰컨을 <code>nib</code>/<code>storyboard</code>/<code>코드</code>로 구현했을때 각각 어떻게 작성해야할까</li>
</ol>
<h2 id="required-init-구현해야하는-이유">required init 구현해야하는 이유</h2>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/28e23186-e091-4c75-9ed6-745709e99814/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/f21f4770-dd1e-4f5b-927c-85ad04ff1e63/image.png" alt="">
<code>UIViewController</code>는 <code>NSCoding</code>을 채택한다.
위의 <code>NSCoding</code>의 정의부분을 보면... 지정생성자가 <code>init?(coder: NSCoder)</code>이다.
<code>NSCoding</code> 프로토콜을 구현하는 클래스들은 모두 저 <code>init?(coder: NSCoder)</code>구현해야 하고, 그 클래스들을 상속받는 자식 클래스들도 마찬가지로 동일하게 구현해야한다. </p>
<h3 id="근데-왜-required-키워드가-붙는거지">근데 왜 required 키워드가 붙는거지?</h3>
<p>프로토콜에 정의되어있는 이니셜라이저를 구현하면 <code>required</code> 키워드가 붙는다고 한다. </p>
<h3 id="그럼-원래-이니셜라이저가-없었을때는-왜-구현하라고-안했나">그럼 원래 이니셜라이저가 없었을때는 왜 구현하라고 안했나</h3>
<p>자식 클래스에서 이니셜라이저를 따로 구현하지 않는 경우 부모의 이니셜라이저를 자동으로 상속받기 때문에 그렇다.
그래서 보통의 경우에는 저 <code>UIViewController</code>에 구현되어있는 <code>init?(coder: NSCoder)</code>가 자동으로 상속된 것...
하지만 자식 클래스에서 이니셜라이저를 구현하는 이 상황의 경우에는 자동 상속이 안되어서 구현하라는 에러가 뜬 것이다 ㅎㅎ</p>
<h1 id="custom-initializer-구현-방법-세가지">custom initializer 구현 방법 세가지</h1>
<h2 id="코드">코드</h2>
<pre><code class="language-swift">init(viewModel: ProjectListViewModel) {
    self.viewModel = viewModel
    super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
    fatalError(&quot;init(coder:) has not been implemented&quot;)
    //super.init(coder: coder) 이것도 됨
}</code></pre>
<p>코드로 구현했으니 <code>init(coder:)</code>은 실행되지 않고 위에 구현해준 이니셜라이저가 실행이 될 것이다. 
그래서 어짜피 실행이 되지 않을 테니 <code>fatalError</code> 을 둬도 괜찮다.
그리고 nib을 사용하지 않으니 <code>init(nibName:, bundle:)</code>에는 각각 nil을 전달해준다.</p>
<h2 id="nib">nib</h2>
<pre><code class="language-swift">init(viewModel: MovieListCollectionViewModel, nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    self.viewModel = viewModel
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

required init?(coder: NSCoder) {
    fatalError(&quot;init(coder:) has not been implemented&quot;)
}</code></pre>
<p>nib로 구현했으니 <code>UIViewController</code>
의 이니셜라이저인 <code>init(nibName:, bundle:)</code>을 이용한다.</p>
<h2 id="storyboard">storyboard</h2>
<p>지금까지 설명한 코드, nib으로 구현하는 경우는 init(coder:)가 안불리게 redirect가 가능하기 때문에 비교적 간단했다.</p>
<p>그러나 문제는 바로 스토리보드로 구현하는 경우인데,
스토리보드로 뷰를 생성할 때는 진짜 무적권 init(coder:)가 불리는데
이때 뭔가 추가적인 설정을 해줄수가 없음 .... 
<a href="https://stackoverflow.com/questions/26923003/how-do-i-make-a-custom-initializer-for-a-uiviewcontroller-subclass-in-swift#:~:text=You%20basically%20have,thing%20and%20call">스택오버플로우</a> 이 사람도 스토리보드에서는 하지 말라고 그런다... 방법이 없다고 ㅋㅋㅋ</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/1c699a10-d5b8-4789-b8cc-1f29a53b38b0/image.png" alt=""></p>
<pre><code>storyboard.instantiateViewController(withIdentifier: &quot;ViewController&quot;)</code></pre><p>이 메서드를 이용해서 뷰컨트롤러를 초기화했을 때 required initializer인 init(coder:)가 진짜 무조건 ㅋ 불림 </p>
<p>그런데 또 super.init?(coder aDecoder: NSCoder)을 다른 파라미터와 함께 초기화하는 일은 불가능하다.... ㅎ ㅏ </p>
<p>더 구글링해보니깐 여러 사람들이 아주 눈물나는 노력을 했음...
이런식으로 팩토리 메서드를 만들어서 instantiate하고 나서 바로 프로퍼티로 주입해주는 방식이나
<img src="https://velog.velcdn.com/images/dev_jane/post/1d1fc29c-5b3c-4b79-ac8c-3d1dc687593a/image.png" alt=""></p>
<p>더 이상한 UIApplication에 접근해서 ㅎ.. 어쩌고 저쩌고 
<img src="https://velog.velcdn.com/images/dev_jane/post/2b4be365-23ff-42c5-8b61-f3ba2d140a04/image.png" alt=""></p>
<p>헬파티라고 생각한 순간 <a href="https://stackoverflow.com/questions/30449137/custom-init-of-uiviewcontroller-from-storyboard/41926924#41926924:~:text=Starting%20from%20iOS%2013%2C%20you%20can%20use%20newly%20updated%20API%2C%20while%20instantiating%20UIViewController%20from%20Storyboard.">이 글</a>을 만났다.</p>
<p>바로 iOS 13부터 새로운 메서드가 나왔는 것이다.</p>
<pre><code>storyboard.instantiateViewController(identifier:, creator:)</code></pre><p>바로 이것 ㅎ
설명을 보면 커스텀 이니셜라이저 코드를 사용하여 스토리보드에서 뷰컨트롤러를 생성할 때 사용한다고 나와있다.
<img src="https://velog.velcdn.com/images/dev_jane/post/610fdb24-0d54-49a0-b6d7-d8493913b6d7/image.png" alt=""></p>
<p>이전까지 우리가 자주 사용하던 아래 메서드로는 커스텀 이니셜라이저를 사용해서는 뷰컨을 생성하지 못한다. 여기 설명에는 custom이 없는것좀 봐라
<img src="https://velog.velcdn.com/images/dev_jane/post/fe006e89-8ec8-4849-a067-580d9645edc2/image.png" alt=""></p>
<p>그니까 이제는 스토리보드로 뷰컨트롤러를 이니셜라이즈하는 방식이 두가지가 있는 것이다. 그냥과 커스텀
<img src="https://velog.velcdn.com/images/dev_jane/post/8b85b6cd-9df4-4fc6-8f32-0df31042b7d8/image.png" alt=""></p>
<pre><code>storyboard.instantiateViewController(identifier:, creator:)</code></pre><p>의 공식 문서에 나와있는대로 커스텀 이니셜라이져에도 coder: NSCoder을 파라미터로 전달해주고, 상속받은 init(coder:)도 불러줬다 
<img src="https://velog.velcdn.com/images/dev_jane/post/db91fa42-cff2-48b6-97a9-bbd975a1e65c/image.png" alt=""></p>
<pre><code class="language-swift">    init?(viewModel: MovieListViewModel, coder: NSCoder) {
        self.viewModel = viewModel
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }</code></pre>
<h4 id="기존-방식-에러">기존 방식: 에러</h4>
<p>이렇게 실행하면 required init?(coder:) 타는 기존의 방식 대신에</p>
<pre><code class="language-swift">    window?.rootViewController = storyboard.instantiateViewController(withIdentifier: &quot;MovieListViewController&quot;)</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/45f88654-f7c3-4e09-a5c6-f73587315dd1/image.png" alt=""></p>
<h4 id="새로운-방식-성공">새로운 방식: 성공</h4>
<p>creator 파라미터가 추가된 이 새로운 방식을 사용하면 정상적으로 뷰컨이 생성된다~!!!! </p>
<pre><code class="language-swift">    window?.rootViewController = storyboard.instantiateViewController(identifier: &quot;MovieListViewController&quot;, creator: { creater in
                let viewModel = MovieListViewModel(defaultMoviesUseCase: DefaultMoviesUseCase(moviesRepository: DefaultMoviesRepository(apiManager: APIManager())))
                let viewController = MovieListViewController(viewModel: viewModel, coder: creater)
                return viewController
            })</code></pre>
<h1 id="reference">Reference</h1>
<p><a href="https://www.swiftbysundell.com/tips/handling-view-controllers-that-have-custom-initializers/">https://www.swiftbysundell.com/tips/handling-view-controllers-that-have-custom-initializers/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Custom View 생성시 init(frame:), init(coder:), awakeFromNib() ]]></title>
            <link>https://velog.io/@dev_jane/Custom-View-%EC%83%9D%EC%84%B1%EC%8B%9C-initframe-initcoder-awakeFromNib</link>
            <guid>https://velog.io/@dev_jane/Custom-View-%EC%83%9D%EC%84%B1%EC%8B%9C-initframe-initcoder-awakeFromNib</guid>
            <pubDate>Thu, 02 Jun 2022 08:51:37 GMT</pubDate>
            <description><![CDATA[<h1 id="initframe-initcoder">init(frame:), init(coder:)</h1>
<p>UIView를 상속받은 커스텀뷰를 생성할 때 UIView의 필수 생성자 두개를 작성해야한다.</p>
<ul>
<li>init(frame: CGRect) → 코드로 뷰를 만들 때 호출됨</li>
<li>init(coder: NSCoder) → 스토리보드/nib로 뷰를 만들 때 호출됨</li>
</ul>
<p>요기 UIView의 정의부분을 보면 생성자 두개가 있음
<img src="https://velog.velcdn.com/images/dev_jane/post/7269255d-0da2-4183-aeee-e97d59049896/image.png" alt=""></p>
<pre><code class="language-swift">class MyCustomView: UIView {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var descriptionLabel: UILabel!

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        custominit()
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        titleLabel.text = &quot;수정함&quot;
        titleLabel.textColor = .blue
        descriptionLabel.text = &quot;엥&quot;
    }


    func custominit() {
        if let view = Bundle.main.loadNibNamed(&quot;MyCustomView&quot;, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            addSubview(view)
        }
    }
}</code></pre>
<h1 id="awakefromnib">awakeFromNib()</h1>
<p>그리고 awakeFromNib()는 XIB로 만든 커스텀 뷰에서 인터페이스 빌더에서 연결된 객체를 설정할때 사용한다.</p>
<p>우리가 흔히 UIViewController에서 초기 화면 설정할때 사용하는 viewDidLoad() 와 비슷한 개념이라고만 알고 있었다. 도대체 뭔지 한번 알아보자.</p>
<h3 id="호출-시점">호출 시점</h3>
<p>언제 호출되냐면 Nib/Xib 파일이 언아카이브되고 나서 init(coder: NSCoder)로 모든 객체가 초기화가 완료되었을 때 호출된다. </p>
<p>*빌드시 xib가 nib로 전환된다</p>
<p>모든 객체가 초기화완료된다는 것의 의미는… 예를 들어 뷰가 서브뷰에 대한 참조를 가지고 있을수도 있고, 뷰의 참조를 설정하는 것은 한번에 하나씩 이루어지므로 전체적인 로딩 과정이 끝나지 않은 경우 이니셜라이즈가 안된 객체가 있을수도 있다.</p>
<p>그런데 이게 불리면 확실히 뷰의 모든 객체가 이니셜라이즈 된 것이라고 보면 된다. </p>
<p>그리고, 아카이브의 모든 객체가 로드되고 초기화된 이후에, nib archive로부터 재성성된 각 객체에게 awakeFromNib 메세지를 보낸다. </p>
<p>따라서 참조된 객체가 awakeFromNib 메세지를 수신할 때, 모든 IBOutlet과 IBAction연결되어 접근이 가능한 상태이다.</p>
<p>따라서 뷰의 추가적인 설정이 필요한 경우 여기서 한다.</p>
<h3 id="추가적으로">+추가적으로…</h3>
<p>하지만, 뷰컨의 awakeFromNib() 시점에는 IBOutlet에 접근이 불가하다. 왜냐하면 뷰컨과 뷰컨이 가진 서브뷰의 Nib 파일이 분리되어있어서 그렇다.</p>
<p>뷰컨의 nib는 top-level object nib로서 이것만 로딩하고 서브뷰의 nib은 로딩하지 않은 채로 awakeFromNib()를 전달한다. </p>
<p>서브뷰의 nib파일은 이후 loadView() 메서드에서 비로소 로드된다고 한다. </p>
<h1 id="reference">Reference</h1>
<p><a href="https://velog.io/@yohanblessyou/%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B3%B4%EB%93%9C%EC%9D%98-%EC%9A%94%EC%86%8C%EA%B0%80-Nib%EC%9D%B4-%EB%90%98%EB%8A%94-%EA%B3%BC%EC%A0%95-feat.-awakeFromNib">https://velog.io/@yohanblessyou/스토리보드의-요소가-Nib이-되는-과정-feat.-awakeFromNib</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[REST API란?]]></title>
            <link>https://velog.io/@dev_jane/REST-API%EB%9E%80</link>
            <guid>https://velog.io/@dev_jane/REST-API%EB%9E%80</guid>
            <pubDate>Mon, 30 May 2022 08:04:37 GMT</pubDate>
            <description><![CDATA[<h1 id="api란">API란?</h1>
<h2 id="api의-시초">API의 시초</h2>
<p>API는하드웨어 독립성을 보장하기 위해서 생겨난 개념인데, 
서로 다른 하드웨어 부품에 맞는 프로그래밍을 각각 하기보다는, </p>
<p>추상화 계층을 통해 한번만 작성한 코드를 
여러 곳에서 공통적으로 사용할 수 있도록 하기 위해서 생겨났다.</p>
<h2 id="web-api">Web API?</h2>
<p>API는 개인 컴퓨터가 나오기 이전부터 존재한 개념으로 대체로 운영 체제의 라이브러리 개념으로 사용되어서 시스템에서 로컬로 작동했다.</p>
<p>그러나 시간이 지나며 API는 로컬에서 분리되어서 원격 API가 나타나게 되었다.
원격이란 API에 의해 조작되는 리소스가 요청을 보내는 컴퓨터의 외부에 있는 것을 의미하는데, 대부분 커뮤니케이션을 할 때 인터넷을 사용하기 때문에 대부분의 원격 API는 웹 API라고 할 수 있다.</p>
<p>Web API는 HTTP 요청 메세지 - XML/JSON 형식의 응답 메세지 형식을 가진다.</p>
<p>XML/JSON 형식은 쉽게 조작할 수 있는 데이터 표현 방식으로 
서버에서 제공하는 Web API를 통해 각기 다른 기기(iOS, Android)에서 동일한 서버의 데이터를 읽어오거나 업데이트할 수 있다.</p>
<h2 id="api의-정의와-기능">API의 정의와 기능</h2>
<p>API는 Application Programing Interface의 약자로,
다른 소프트웨어 시스템과 통신하기 위해 따라야하는 규칙을 정의한다. </p>
<p>이렇게 통신의 규칙을 정하므로써 좋은 점은 뭘까?
-&gt; 세세한 구현 방식을 알지 못하는 제품이나 서비스와 통신이 가능해지기 때문에 개발 시간과 비용을 간소화할 수 있다.</p>
<p>API는 또한 특정한 요청과 응답의 형식을 갖춘 &quot;상호간 계약&quot;으로 비유되기도 하는데
한쪽에서 특정한 방식으로 요청을 보내면, 다른 쪽에서 이에 응답을 다시 보내준다.</p>
<p>이러한 계약의 여러 형식들중 가장 보편적으로 쓰이는 방식이 REST인 것이다. (REST 이전에는 SOAP라는 프로토콜을 통해 XML을 주고 받았었다.)</p>
<h2 id="api의-종류">API의 종류</h2>
<ul>
<li>프라이빗
기업 내부에서만 사용 가능함</li>
<li>파트너
특정 비즈니스 파트너와만 공유 가능함</li>
<li>퍼블릭
모두에게 제공되며, 제3자가 해당 API를 활용하여 새로운 기술을 만들어낼 수 있음</li>
<li><blockquote>
<p>일반 사용자에게 API를 공개하면 브랜드 인지도를 높이고 새로운 수익 채널을 확보할 수 있다는 장점이 있다. </p>
</blockquote>
</li>
</ul>
<h1 id="rest란">REST란?</h1>
<p>Representational State Transfer의 약자로, API 작동 방식에 대한 조건을 부과하는 소프트웨어 아키텍처이다. 여러 아키텍처로 API를 설계할 수 있는데, REST는 그 방식들중 한가지이다. </p>
<h2 id="rest-아키텍처-스타일">REST 아키텍처 스타일</h2>
<h4 id="균일한-인터페이스">균일한 인터페이스</h4>
<ol>
<li>요청은 리소스를 식별할 수 있어야 해서 각각의 요청마다 고유한 리소스 식별자(URL)를 사용한다. </li>
<li>클라이언트는 리소스를 수정하거나 삭제하기에 충분한 정보를 리소스 표현에서 가지고 있는다.</li>
<li>클라이언트는 표현을 추가적으로 처리하는 방법에 대한 정보를 수신한다.</li>
<li>클라이언트는 작업을 완료하는데 필요한 다른 모든 리소스에 대한 정보를 수신한다. </li>
</ol>
<h4 id="무상태stateless">무상태(Stateless)</h4>
<p>모든 요청은 다른 요청들과 분리되어 서버는 이전의 모든 요청과 독립적으로 모든 클라이언트 요청을 완료한다. </p>
<p>이렇게 하면 과거의 클라이언트 요청 정보를 유지할 필요가 없기 때문에 성능을 향상시킬 수 있다. </p>
<h4 id="계층화-시스템">계층화 시스템</h4>
<p>클라이언트와 서버 사이의 다른 중개자에게 연결될 수 있고, 서버는 요청을 다른 서버로 전달할 수 있다. 이러한 계층은 클라이언트에겐 보이지 않는 상태로 유지된다. </p>
<h4 id="캐시-가능성">캐시 가능성</h4>
<p>서버 응답 시간을 개선하기 위해 첫번째 응답 이후에 해당 이미지를 캐싱하여 다음 응답시 사용한다.</p>
<p>최근 Open API는 REST API를 정의하는 공통 표준으로 부상하여서 사용자는 별다른 추측 없이도 이를 이해할 수 있다. </p>
<h2 id="장점">장점</h2>
<h4 id="확장성-유연성">확장성, 유연성</h4>
<p>완전한 클라이언트 - 서버 분리를 통해 각 부분이 독립적으로 발전할 수 있다. 예를 들어 서버의 기술 변경은 클라이언트에 영향을 주지 않는다. </p>
<h4 id="독립성">독립성</h4>
<p>REST API는 사용되는 기술과 독립적이므로 API 설계에 영향을 주지 않고 다양한 프로그래밍 언어로 클라이언트 및 서버 애플리케이션을 작성 가능하다. </p>
<h1 id="rest-api">REST API</h1>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/3055013a-b564-4c7e-807c-03093fe08bb4/image.png" alt=""></p>
<h4 id="1-클라이언트가-서버에-지정된-형식으로-요청-전송">1. 클라이언트가 서버에 지정된 형식으로 요청 전송</h4>
<ul>
<li>고유 리소스 식별자(=URL)</li>
<li>메서드(GET, POST, PUT, PATCH)</li>
<li>HTTP Header(데이터, 파라미터)<h4 id="2-서버가-클라이언트를-인증하고-클라이언트의-요청-권한확인">2. 서버가 클라이언트를 인증하고 클라이언트의 요청 권한확인</h4>
<h4 id="3-서버가-요청을-수신하고-처리">3. 서버가 요청을 수신하고 처리</h4>
</li>
</ul>
<h4 id="4-서버가-클라이언트에게-응답-반환">4. 서버가 클라이언트에게 응답 반환</h4>
<ul>
<li>상태 코드(200, 201, 400, 404)
200 일반적인 성공 응답
201 POST 성공 응답
400 서버가 처리하지 못하는 잘못된 요청
404 리소스 찾을 수 없음</li>
<li>메세지 본문</li>
<li>Header</li>
</ul>
<h1 id="reference">Reference</h1>
<p><a href="https://www.redhat.com/ko/topics/api/what-are-application-programming-interfaces">https://www.redhat.com/ko/topics/api/what-are-application-programming-interfaces</a></p>
<p><a href="https://aws.amazon.com/ko/what-is/restful-api/">https://aws.amazon.com/ko/what-is/restful-api/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[set의 연산을 이용한 Local - Remote DB 연동하기]]></title>
            <link>https://velog.io/@dev_jane/set%EC%9D%98-%EC%97%B0%EC%82%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-Local-Remote-DB-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@dev_jane/set%EC%9D%98-%EC%97%B0%EC%82%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-Local-Remote-DB-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 02 May 2022 10:48:04 GMT</pubDate>
            <description><![CDATA[<h3 id="네트워크-연결이-끊어진-오프라인-환경에서-데이터-변경사항이-생길-경우">네트워크 연결이 끊어진 오프라인 환경에서 데이터 변경사항이 생길 경우</h3>
<ul>
<li>먼저 Local에 저장한 후에,</li>
<li>네트워크가 다시 연결되면 Local의 변경사항을 Remote에 업데이트하는 방식으로 구현하였다.</li>
</ul>
<p>Remote에서 변경이 일어날 가능성은 없으므로 Remote변경사항을 Local에 업데이트하는 부분은 배제하였다. </p>
<h3 id="local의-변경사항을-remote에-업데이트할-때-set의-메서드들을-활용하여-두-데이터를-연동하였다">Local의 변경사항을 Remote에 업데이트할 때 Set의 메서드들을 활용하여 두 데이터를 연동하였다.</h3>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/fe651495-db8e-4126-b73c-6aed6003f999/image.png" alt="">
<img src="https://velog.velcdn.com/images/dev_jane/post/2a782a09-b412-4e7c-98af-a78166a97587/image.png" alt=""></p>
<p>오프라인에서 추가/삭제/수정된 Project를 찾아서 Remote에서도 똑같이 추가/삭제/수정해주는 방식을 사용했다.</p>
<p>각 Project를 identify할 수 있는 ID 프로퍼티를 활용하였다. </p>
<pre><code class="language-Swift">let localIDSet = Set(localProjects.map { $0.id })
let remoteIDSet = Set(remoteProjects.map { $0.id })</code></pre>
<ul>
<li>오프라인에서 Local에만 새로 추가된 ID를 구하려면 <code>Local - Remote</code> 한 결과를 
→ <strong>Remote에도 추가하기</strong><pre><code class="language-Swift">let locallyAppendedIDSet = Set(localIDSet).subtracting(Set(remoteIDSet))
let locallyAppendedProjects = localProjects.filter {
     locallyAppendedIDSet.contains($0.id)
}</code></pre>
</li>
</ul>
<ul>
<li>오프라인 환경에서 Local에만 삭제된 ID를 구하려면 <code>Remote - Local</code> 한 결과를 
→ <strong>Remote에서도 삭제</strong><pre><code class="language-Swift">let deletedIDSet = remoteIDSet.subtracting(localIDSet)
let locallyDeletedProjects = remoteProjects.filter {
   deletedIDSet.contains($0.id)
}</code></pre>
</li>
<li>오프라인에서 Local에만 수정된 ID를 구하려면 둘다 동일하게 가진 ID에서 수정이 일어나지 않은 부분을 제외하면 된다.<ul>
<li><code>Remote와 Local의 교집합</code> 부분은 둘다 동일하게 가진 ID들로써 수정이 일어나지 않았거나, 수정이 일어난 ID들이다.</li>
<li>수정이 일어나지 않은 부분은 가만히 놔둠 (어짜피 Local, Remote 똑같으니 굳이 업데이트 안해도됨)</li>
<li>요기서 수정이 일어났는지는 어케 아냐면, 수정시에 Project 객체의 updatedAt 프로퍼티에 Date()를 할당하여, 만약 Local의 updatedAt이 Remote보다 최신일 경우, 오프라인에서 수정이 일어난 것
→ <strong>Remote에서도 수정</strong><pre><code class="language-Swift">let intersectingIDSet = localIDSet.intersection(remoteIDSet)
let intersectingLocalProjects = localProjects.filter {
intersectingIDSet.contains($0.id)
}
let intersectingRemoteProjects = remoteProjects.filter {
intersectingIDSet.contains($0.id)
}
</code></pre>
</li>
</ul>
</li>
</ul>
<p>intersectingRemoteProjects.flatMap { remoteProject in
     intersectingLocalProjects.filter { localProject in
     localProject.updatedAt &gt; remoteProject.updatedAt
   }
}</p>
<pre><code></code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[AppDelegate와 SceneDelegate]]></title>
            <link>https://velog.io/@dev_jane/AppDelegate%EC%99%80-SceneDelegate</link>
            <guid>https://velog.io/@dev_jane/AppDelegate%EC%99%80-SceneDelegate</guid>
            <pubDate>Mon, 02 May 2022 08:50:23 GMT</pubDate>
            <description><![CDATA[<p>iOS는 사용자가 앱을 직관적인 방법으로 사용할 수 있도록 디자인되었다. 
따라서 iOS 개발을 하면서 우리는 사용자 경험이 물 흐르듯이 자연스럽게 이루어질 수 있도록 신경써야한다. 
그러한 관점에서 앱이 처음 시작되고 종료되어 메모리에서 내려갈때까지의 생명주기인 
<strong>iOS Application Life Cycle</strong>의 각 단계를 잘 이해하고 있어야 한다.</p>
<p>그렇다면 
처음에 앱이 시작되면 가장 먼저 불리는 메서드는 무엇일까?</p>
<h2 id="uiapplicationmain이다">UIApplicationMain(<em>:</em>:<em>:</em>:)이다.</h2>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/d3ac1f7f-b962-4701-a71d-8c0432f0ea7b/image.png" alt="">
이 메서드는 싱글턴으로 구현된 UIApplication 객체를 생성하고, UIApplicationDelegate 프로토콜을 따르는 delegate를 정의한다. 
그리고 app launch, app termination과 메모리 부족 경고같은 중요한 런타임 이벤트들을 delegate에게 알려서 대신 대응하도록 한다. </p>
<p>개발자가 직접 UIApplication을 상속받아서 사용할 필요는 없고 보통 미리 정의되어있는 AppDelegate 객체를 이용해서 시스템과 앱 간의 상호작용을 다룬다.</p>
<h3 id="ios-13-이후-바뀐점">iOS 13 이후 바뀐점</h3>
<p>iOS 12 이전에는 <code>AppDelegate</code> 만 사용하여 Process LifeCycle과 UILifeCycle 둘다 AppDelegate가 관리했으나,</p>
<p>iOS 13부터는</p>
<ul>
<li><strong>UI LifeCycle</strong>에 관해서는 <code>SceneDelegate</code> 가 관리하게 되었다.</li>
<li>그러나 앱이 실행되고 종료되는 <strong>Process LifeCycle</strong>에 관해서는 <code>AppDelegate</code> 가 여전히 관리한다.</li>
<li>따라서 역할이 분리되었지만, scene session이 생성되거나 삭제되는 <strong>Session LifeCycle</strong>을 <code>AppDelegate</code> 가 관리하게 되어 Scene에 관한 정보를 업데이트받는다.</li>
</ul>
<p>그러니깐 다시말해서 앱 전반의 관리는 AppDelegate가, 거기에서 특정 scene의 라이프사이클은 SceneDelegate가 따로 관리한다는 뜻이다.
<img src="https://velog.velcdn.com/images/dev_jane/post/e98ac985-42d4-4588-9bef-7d1376a76fa6/image.png" alt=""></p>
<p>따라서 iOS 12까지는 <strong>하나의 앱에 하나의 window(창)</strong> 즉, 한 앱을 동시에 키는 것이 불가능하였지만</p>
<p>iOS 13부터는 <strong>window의 개념이 scene으로 대체되고 하나의 앱에서 여러 개의 scene을 가질 수 있게 되었다. 즉, 하나의 앱을 동시에 여러개 키는 것이 가능해졌다.</strong></p>
<h3 id="그렇다면-scene이란-대체-무엇인가">그렇다면 Scene이란 대체 무엇인가?</h3>
<ul>
<li>UI의 각 인스턴스를 나타내는 <code>UIWindow</code>와 <code>View Controllers</code>가 포함되어 있음</li>
<li>각 scene에는 해당하는 <code>UIWindowSceneDelegate</code> 객체를 가지고 있음</li>
<li>하나의 앱은 여러 scene과 해당 <code>UIWindowSceneDelegate</code> 객체를 동시에 활성화 가능</li>
</ul>
<h3 id="appdelegate가-관리하게-된-scene-session은-뭘까">AppDelegate가 관리하게 된 Scene Session은 뭘까</h3>
<p>UISceneSession 객체는 각 scene의 독립적인 런타임 인스턴스를 관리한다. 유저가 한 앱에 새로운 scene을 추가하게 되면 시스템은 UISceneSession 객체를 생성하여 그 scene을 관리하게 된다. 따라서 각 scene을 만들고 없애는 일련의 과정을 관리하는 것이다. </p>
<h3 id="ios-13부터-appdelegate는-무슨-일을-할까">iOS 13부터 AppDelegate는 무슨 일을 할까</h3>
<p>Session LifeCycle 과 Process LifeCycle
이 두가지를 관리하는 일을 하는데 전자에 관해서는 이전 질문에서 설명을 했으니 후자인 process에 관해 설명하겠다.</p>
<ul>
<li>앱의 중심적인 데이터 구조를 초기화하기</li>
<li>앱 외부에서 발생하는 notification에 대응하기: 메모리 부족 워닝, 다운로드 완료 노티 등 </li>
<li>앱 전체를 타겟으로 하는 이벤트에 대응하기</li>
<li>앱의 launch time에 필요한 서비스를 등록하는 일 ex) Apple Push Notification Service 등</li>
</ul>
<h2 id="app-based--scene-based-life-cycle">App-Based / Scene-Based Life Cycle?</h2>
<p>라이프 사이클과 각 단계로 전환되면서 호출되는 메서드들을 보기 쉽게 정리해놓은 이미지가 있어서 <a href="https://qiita.com/KenNagami/items/cbbe98b736fbdb24fef8">여기</a>에서 가져왔다.</p>
<ul>
<li><p>App Life Cycle
<img src="https://velog.velcdn.com/images/dev_jane/post/56076ee9-98b7-4f40-903b-8ba32f0d6abc/image.png" alt=""></p>
</li>
<li><p>Scene Life Cycle
<img src="https://velog.velcdn.com/images/dev_jane/post/9ccf338d-ca4a-4470-86bf-9bebf024a8ea/image.png" alt=""></p>
</li>
</ul>
<p>iOS 13부터 <strong>UI LifeCycle</strong>에 관해서는 <code>SceneDelegate</code> 가 관리하게 되었다고 했으니 Scene-Based Life-Cycle Events에 대해 자세히 알아보자.</p>
<h2 id="scene-based-life-cycle-events">Scene-Based Life-Cycle Events</h2>
<p>Scene이란 디바이스에서 실행중인 앱의 UI의 인스턴스 중 하나이다. 단일 앱에 대해 여러 Scene을 생성할 수 있고 각각 독립적으로 화면에 띄우고, 숨길 수 있다. 각 Scene은 독립적인 Life Cycle을 가진다.</p>
<p>사용자나 시스템이 앱에 대한 새로운 scene을 요청하면, UIKit는 scene을 생성하여 <strong>unattatched</strong> 상태로 보낸다. 그리고 나서 사용자가 요청한 경우에는 <strong>foreground</strong> 상태로 변하지만, 시스템이 요청한 경우에는 <strong>background</strong> 상태로 보내진다.</p>
<p>사용자가 앱을 사용하다가 UI를 화면에서 없앴을때, UIKit는 scene을 <strong>background</strong> 상태로 보내고 결과적으로 <strong>suspended</strong> 상태로 보내게 된다.
그렇게 <strong>background</strong>나 <strong>suspended</strong> 상태에 있는 scene은 언제든지 연결이 끊겨서 <strong>unattatched</strong> 상태로 변할 수 있다.</p>
<p>각 state에 대해서 자세하게 알아보자면,
<code>UIScene</code> 클래스 안에 <code>ActivationState</code> 라는 Enum 타입을 가지고 있다.</p>
<pre><code class="language-swift">extension UIScene {
    @available(iOS 13.0, *)
    public enum ActivationState : Int {
        case unattached = -1
        case foregroundActive = 0
        case foregroundInactive = 1
        case background = 2
    }
}</code></pre>
<ul>
<li><strong>unattached</strong><ul>
<li><code>scene</code>은 처음에 unattached 상태로 시작되며 시스템이 connection notification을 주기 전까지는 계속 이 상태를 유지한다. </li>
</ul>
</li>
<li><strong>foregroundActive</strong><ul>
<li><code>scene</code>이 <strong>foreground에서 돌아가고 있으며, 현재 event들을 받고 있는 상태</strong>이다. active scene의 interface는 화면에 있으며 사용자에게 보여지게 된다.</li>
</ul>
</li>
<li><strong>forgroundInactive</strong><ul>
<li><code>scene</code>이 <strong>foreground에서 돌아가고는 있지만 event를 받지는 않는다.</strong></li>
<li><code>scene</code>이 다른 상태로 전환되는 동안에 바로 이 foregroundInactive 상태를 <strong>통과</strong>하게 된다.</li>
<li>예를 들면, 시스템 알람이 오거나 알람 창을 내리거나 app-switching을 하는 상황에서 이 상태가 된다.</li>
</ul>
</li>
<li><strong>background</strong><ul>
<li><code>scene</code>이 <strong>스크린이 아닌 background에서 실행이 되고있는 상태</strong>이다.</li>
<li>background scene은 보여지는 interface가 없다.</li>
</ul>
</li>
<li><strong>suspended</strong><ul>
<li>이 상태는 실제로 case 안에는 존재하지는 않는다.<code>scene</code>이 <strong>background 상태에 있으며, 아무것도 실행되지 않는 상태</strong>를 의미한다.</li>
</ul>
</li>
</ul>
<h3 id="앱이-background와-foreground-상태에-있는-경우-제약사항이-있는지">앱이 background와 foreground 상태에 있는 경우 제약사항이 있는지?</h3>
<ul>
<li>foreground에는 제약사항이 없고, 시스템 리소스에 우선권을 가진다.</li>
<li>background의 경우 메모리와 시스템 리소스에 대한 우선권을 박탈당한 상태이다.
(아래의 상황 등 몇가지 예외사항 외에는 실행시간을 할당받지 못함)<ul>
<li>Audio, AirPlay, PIP(Picture in Picture)</li>
<li>사용자 위치 업데이트</li>
<li>Voice over IP</li>
<li>외부 악세서리와의 통신</li>
<li>블루투스 LE(Low Energy)와 통신, 혹은 디바이스를 블루투스 LE 악세서리로 변환</li>
<li>서버에서의 정기적인 업데이트</li>
<li>RemoteNotification(Apple Push Notification 지원)</li>
<li>Background Processing</li>
</ul>
</li>
</ul>
<h3 id="앱이-background에-있을-때-메모리에-올라와-있을지">앱이 background에 있을 때 메모리에 올라와 있을지</h3>
<ul>
<li>메모리에 올라가있고, 화면 밖에서 실행되고 있는 상태이다.</li>
</ul>
<h3 id="앱이-메모리에서-언제-완전히-해제될까">앱이 메모리에서 언제 완전히 해제될까</h3>
<ul>
<li>메모리 확보, 사용자의 종료 등의 이유로 suspended 상태에서 not running상태로 이동할 때</li>
<li>applicationWillTerminate(_:) 메서드 이후</li>
</ul>
<h3 id="다음-상황에서의-app-lifecycle-state는-어떻게-변할까">다음 상황에서의 App LifeCycle State는 어떻게 변할까</h3>
<ul>
<li>실행 중인 앱을 멀티태스킹을 사용하여 Background로 보낸 뒤 다시 멀티태스킹을 사용하여 해당 앱으로 돌아왔을때<ul>
<li>active -&gt; inactive -&gt; background -&gt; inactive -&gt; active</li>
<li><img src="https://velog.velcdn.com/images/dev_jane/post/66cff6c5-963d-44a2-8f0a-0984e695efbd/image.png" alt=""></li>
</ul>
</li>
</ul>
<ul>
<li><p>실행 중인 앱을 멀티태스킹 창을 띄워 바로 종료하는 경우</p>
<ul>
<li><img src="https://velog.velcdn.com/images/dev_jane/post/99b2d1e0-2d5d-49c1-9ed9-998e67346276/image.png" alt=""></li>
</ul>
</li>
<li><p>백그라운드 상태에 있는 앱을 종료하는 경우
실험 결과 백그라운드에서 앱을 바로 종료하면 applicationWillTerminate 메서드가 불리지 않는다.
<img src="https://velog.velcdn.com/images/dev_jane/post/cd9547dd-065f-4caf-962f-bb6e499649f6/image.png" alt=""></p>
</li>
<li><p>실행 중인 앱을 바로 종료하는 경우 
그러나 실행 중인 앱을 바로 종료시 불린다.
<img src="https://velog.velcdn.com/images/dev_jane/post/4c84f843-fe1b-4c68-8b5f-4ad942490e5a/image.png" alt=""></p>
</li>
</ul>
<h3 id="그렇다면-중요한-데이터를-언제-저장하는-것이-좋을까">그렇다면 중요한 데이터를 언제 저장하는 것이 좋을까</h3>
<p>앱을 quit하는 상황(=&gt; background로 전환하는 상황)에서 불리는 sceneWillResignActive에 저장하는 것이 좋을 것 같다.</p>
<p>왜냐하면 applicationWillTerminate(_:)의 경우 background를 지원하는 앱인 경우 background 상태에서 앱을 종료하면 불리지 않기 때문이다.</p>
<p><a href="https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background">공식문서</a>에도 앱을 deactivate하면 불리는 메서드인sceneWillResignActive에서 유저데이터를 저장하라고 한다.
<img src="https://velog.velcdn.com/images/dev_jane/post/44015109-1dcb-4251-8342-5f300eedfef0/image.png" alt=""></p>
<h1 id="reference">Reference</h1>
<p><a href="https://developer.apple.com/documentation/uikit/uiapplication">https://developer.apple.com/documentation/uikit/uiapplication</a>
<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate">https://developer.apple.com/documentation/uikit/uiapplicationdelegate</a>
<a href="https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background">https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background</a>
<a href="https://velog.io/@dev-lena/iOS-AppDelegate%EC%99%80-SceneDelegate">https://velog.io/@dev-lena/iOS-AppDelegate%EC%99%80-SceneDelegate</a>
<a href="https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle">https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] Single과 Completable]]></title>
            <link>https://velog.io/@dev_jane/Traits-Single%EA%B3%BC-Completable</link>
            <guid>https://velog.io/@dev_jane/Traits-Single%EA%B3%BC-Completable</guid>
            <pubDate>Sat, 30 Apr 2022 13:26:29 GMT</pubDate>
            <description><![CDATA[<h1 id="traits">Traits</h1>
<p>Observable을 감싸고 있는 구조체임
<code>.asObservable()</code> 사용시 다시 기본 Observable로 돌아갈 수 있음 </p>
<ul>
<li>Single</li>
<li>Completable</li>
<li>Maybe</li>
</ul>
<h1 id="single">Single</h1>
<p><code>단 하나의 요소</code>나 <code>error</code>만 방출하는 Observable의 변형인 Traits중 하나이다.</p>
<ul>
<li><p>언제 사용하냐?
보통 HTTP 요청의 결과로 response와 error를 딱 리턴하므로 많이 쓰임</p>
</li>
<li><p>특징</p>
<ul>
<li>딱 하나의 요소나 error만 방출</li>
<li>side effects를 공유하지 않음(?)</li>
<li>Observable에 <code>.asSingle()</code> 사용시 Single로 변환 가능<h3 id="1-singlecreate">1. Single.create</h3>
</li>
</ul>
</li>
</ul>
<pre><code class="language-swift">func fetch() -&gt; Single&lt;[Project]&gt; {
    var projects = [Project]()
    return Single.create { single in
        self.realm.objects(ProjectRealm.self).forEach { projectRealm in
            let project = Project(projectRealm: projectRealm)
            projects.append(project)
        }
        single(.success(projects))
        return Disposables.create()
    }
}</code></pre>
<pre><code class="language-swift">func getRepo(_ repo: String) -&gt; Single&lt;[String: Any]&gt; {
    return Single&lt;[String: Any]&gt;.create { single in
        let task = URLSession.shared.dataTask(with: URL(string: &quot;https://api.github.com/repos/\(repo)&quot;)!) { data, _, error in
            if let error = error {
                single(.error(error))
                return
            }

            guard let data = data,
                  let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),
                  let result = json as? [String: Any] else {
                single(.error(DataError.cantParseJSON))
                return
            }

            single(.success(result))
        }

        task.resume()

        return Disposables.create { task.cancel() }
    }
}

getRepo(&quot;ReactiveX/RxSwift&quot;)
    .subscribe { event in
        switch event {
            case .success(let json):
                print(&quot;JSON: &quot;, json)
            case .error(let error):
                print(&quot;Error: &quot;, error)
        }
    }
    .disposed(by: disposeBag)</code></pre>
<h1 id="completable">Completable</h1>
<p><code>completed</code> 나 <code>error</code>만 방출하는 Observable의 변형인 Traits중 하나이다.</p>
<ul>
<li>언제 사용하냐?
특정 동작이 끝났는지 여부만 관심이 있고, 그 동작으로 인해 일어난 결과물에는 관심이 없을 때 사용한다.</li>
</ul>
<p>ex) Firebase에 데이터를 업데이트하고 업데이트 완료했다고 completable로 알려주면 
구독하고 있는 곳에서 데이터 업데이트가 되고 나서 수행해야할 동작을 수행할 수 있음</p>
<ul>
<li>특징<ul>
<li>요소를 방출하지 않음</li>
<li>side effect를 공유하지 않음(무슨말이지?)</li>
</ul>
</li>
</ul>
<h3 id="1-completablecreate">1. Completable.create</h3>
<p>Observable.create하는거랑 같은 방법이다.
똑같이 create의 리턴 타입이 Disposable이라서 <code>return</code> Disposables.create() 해야함 </p>
<h3 id="프로젝트-적용-예시">프로젝트 적용 예시</h3>
<pre><code class="language-swift">//Completable.create
func delete(_ project: Project) -&gt; Completable {
    return Completable.create { completable in
        self.dataBase
            .collection(&quot;users&quot;)
            .document(project.id.description)
            .delete()
        completable(.completed)
        return Disposables.create()
    }
}

//Completable 사용할 때는 subscribe해서 사용
Completable.zip(
    remoteDataSource.delete(project),
    localDataSource.delete(project)
).subscribe(onCompleted: { [self] in
    var currentProjects = projects.value
    if let row = currentProjects.firstIndex(where: { $0.id == project.id }) {
        currentProjects.remove(at: row)
    }
    projects.accept(currentProjects)
}).disposed(by: disposeBag)</code></pre>
<p>local, remote datasource의 delete 메서드를 실행하는 곳(repo)에서 Completable.zip으로 둘을 묶어서 구독하여, 두 작업 모두가 끝난 후에 바로 모델 업데이트할 수 있게 구현한 예시이다.</p>
<h3 id="2-completablezip">2. Completable.zip</h3>
<p>Completable들이 배열에 담긴: [Completable]을 하나의 Completable 형태로 바꿔주는 메서드 zip
<img src="https://velog.velcdn.com/images/dev_jane/post/febe0599-0119-417f-9b16-d875cc98b107/image.png" alt=""></p>
<h3 id="프로젝트-적용-예시-1">프로젝트 적용 예시</h3>
<p><strong>1st. [Project] → [Completable]</strong></p>
<ul>
<li>intersectingRemoteProjects: [Project]</li>
<li>intersectingLocalProjects: [Project]</li>
</ul>
<p>배열의 뎁스는 유지하면서 filter의 조건으로 localProject, remoteProject 둘다를 사용하고 싶어서</p>
<p>이렇게 flatMap과 filter을 겹쳐서 사용함 </p>
<pre><code class="language-swift">let sameIDCompletable: [Project] = intersectingRemoteProjects.flatMap { remoteProject in
        intersectingLocalProjects.filter { localProject in
            localProject.updatedAt &gt; remoteProject.updatedAt
        }
}</code></pre>
<p>여기서 .map 를 해서 배열 안의 Project 타입을 Completable로 바꾼 것임 
self.remoteDataSource.update($0)의 리턴값이 Completable</p>
<pre><code class="language-swift">let sameIDCompletable: [Completable] =
      intersectingRemoteProjects.flatMap { remoteProject in
          intersectingLocalProjects.filter { localProject in
              localProject.updatedAt &gt; remoteProject.updatedAt
          }.map {
              return self.remoteDataSource.update($0)
          }
      }</code></pre>
<ul>
<li><p>만약 flatMap이 아니라 map을 썼다면 [[Completable]] 타입이었을 것...</p>
<pre><code class="language-swift">  let sameIDCompletable: [[Completable]] =
      intersectingRemoteProjects.map { remoteProject in
          intersectingLocalProjects.filter { localProject in
              localProject.updatedAt &gt; remoteProject.updatedAt
          }.map {
              return self.remoteDataSource.update($0)
          }
      }</code></pre>
</li>
</ul>
<p><strong>2nd. [Completable] → Completable</strong>
Completable.zip을 사용하여 최종적으로 [Completable] 타입에서 Completable 타입으로 변경해줌</p>
<pre><code class="language-swift">let sameIDCompletable = Completable.zip(
    intersectingRemoteProjects.flatMap { remoteProject in
        intersectingLocalProjects.filter { localProject in
            localProject.updatedAt &gt; remoteProject.updatedAt
        }.map {
            return self.remoteDataSource.update($0)
        }
    }
)</code></pre>
<h1 id="reference">Reference</h1>
<p><a href="https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md">https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RxSwift] combineLatest와 zip]]></title>
            <link>https://velog.io/@dev_jane/combineLatest%EC%99%80-zip</link>
            <guid>https://velog.io/@dev_jane/combineLatest%EC%99%80-zip</guid>
            <pubDate>Sat, 30 Apr 2022 13:03:28 GMT</pubDate>
            <description><![CDATA[<h1 id="zip과-combinelatest의-차이와-사용법">zip과 combineLatest의 차이와 사용법</h1>
<p>디폴트로 combineLatest를 사용하고 특정한 경우에만 zip을 사용하기
둘다 두 Observable을 합칠때 사용하지만, 합치는 방식이 다르다.</p>
<p>*Single의 경우 zip만 사용가능</p>
<h2 id="zip">zip</h2>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/0ce8c901-167a-4326-bdbc-e9818d529263/image.png" alt=""></p>
<ul>
<li>두 Observable 중 방출하는 요소가 작은 Observable의 요소 수만큼 합쳐진 결과가 방출된다<ul>
<li>예를 들어 a Observable의 요소 개수가 3이고, b Observable의 요소 개수가 4일때 합쳐진 결과는 3개가 나온다는 뜻이다.</li>
</ul>
</li>
<li>방출된 순서가 같은 요소끼리만 합쳐진다.<ul>
<li>예를 들어  a Observable에서 <code>1, 2, 3</code>이 방출되었고, b Observable에서 <code>1, 2</code>가 방출되었다면 합친 결과는 <code>1-1, 2-2</code> 가 된다. a Observable의 <code>3</code>은 짝이 없어서 합쳐지지 않는다.</li>
</ul>
</li>
</ul>
<h3 id="프로젝트-적용-예시">프로젝트 적용 예시</h3>
<p>Observable의 변형인 Traits의 한 종류인 Completable을 zip으로 묶어서 나오는 이벤트를 구독하여, 두 Completable에서 onCompleted 이벤트가 둘다 내려오면 하나의 Completable 이벤트를 발생시킨다. </p>
<pre><code class="language-swift">Completable.zip(
    remoteDataSource.delete(project),
    localDataSource.delete(project)
).subscribe(onCompleted: { [self] in
    var currentProjects = projects.value
    if let row = currentProjects.firstIndex(where: { $0.id == project.id }) {
        currentProjects.remove(at: row)
    }
    projects.accept(currentProjects)
}).disposed(by: disposeBag)</code></pre>
<h2 id="combinelatest">combineLatest</h2>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/8a5e1e2e-8227-48cf-8862-a290fea68efe/image.png" alt=""></p>
<ul>
<li>두 Observable의 요소가 최소한 하나씩 방출되어야 합쳐진 시퀀스에서 이벤트가 발생한다.<ul>
<li>예를 들어  a Observable에서 <code>1</code> 만 나온 상태일때는 아무런 이벤트도 발생하지 않는다.</li>
</ul>
</li>
<li>한 Observable에서 요소가 방출되면 다른 Observable에서 제일 최근에 방출된 요소랑 합쳐진다.</li>
</ul>
<p>따라서 두 Observable의 요소가 하나씩 방출되고 나서는 한 Observable에서 요소가 방출될때마다 합쳐진 시퀀스에서 이벤트가 발생한다. </p>
<ul>
<li>예를 들어 a Observable과  b Observable에서 각각 <code>1, 2</code> 두개씩 나온 상태일때 b Observable에서 <code>3</code>이 나오면 a Observable의 가장 최신 요소인 <code>2</code>와 합쳐진다.</li>
</ul>
<h3 id="비교-실험">비교 실험</h3>
<pre><code class="language-swift">import RxSwift
import Foundation

func exampleZip(a: Observable&lt;Int&gt;) -&gt; Observable&lt;(Int, String)&gt; {
    let b = a.map { &quot;\($0)&quot; }
    return Observable.zip(a, b)
}

func exampleCombineLatest(a: Observable&lt;Int&gt;) -&gt; Observable&lt;(Int, String)&gt; {
    let b = a.map { &quot;\($0)&quot; }
    return Observable.combineLatest(a, b)
}

exampleZip(a: Observable.from([1, 2, 3]))
    .subscribe(onNext: { print(&quot;zip&quot;, $0) })

exampleCombineLatest(a: Observable.from([1, 2, 3]))
    .subscribe(onNext: { print(&quot;combineLatest&quot;, $0) })</code></pre>
<pre><code class="language-swift">zip (1, &quot;1&quot;)
zip (2, &quot;2&quot;)
zip (3, &quot;3&quot;)
combineLatest (1, &quot;1&quot;)
combineLatest (2, &quot;1&quot;)
combineLatest (2, &quot;2&quot;)
combineLatest (3, &quot;2&quot;)
combineLatest (3, &quot;3&quot;)</code></pre>
<h1 id="reference">Reference</h1>
<p><a href="https://reactivex.io/documentation/operators/combinelatest.html">https://reactivex.io/documentation/operators/combinelatest.html</a>
<a href="https://reactivex.io/documentation/operators/zip.html">https://reactivex.io/documentation/operators/zip.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Frame과 Bounds]]></title>
            <link>https://velog.io/@dev_jane/Frame%EA%B3%BC-Bounds</link>
            <guid>https://velog.io/@dev_jane/Frame%EA%B3%BC-Bounds</guid>
            <pubDate>Fri, 22 Apr 2022 06:32:08 GMT</pubDate>
            <description><![CDATA[<p>UIView의 속성으로 frame과 bounds 두가지가 있는데 </p>
<ul>
<li><p>UIView 이니셜라이저에는 왜 frame만 있을까?
<img src="https://velog.velcdn.com/images/dev_jane/post/5c532f1b-77b4-473c-b20f-16b0a860fcc6/image.png" alt="">
참고로,, 뷰 생성할때 init(frame:)은 코드 사용시 불리고, init(coder:)은 Storyboard 사용시 불림</p>
</li>
<li><p>frame과 bounds는 둘다 CGRect 타입으로 시작 좌표 x, y와 width, height을 가지는데 과연 뭐가 다른 걸까?</p>
</li>
</ul>
<p>라는 궁금증에 공부해보았다. 🙂</p>
<h1 id="frame-bounds">Frame, Bounds</h1>
<p>frame은 view 자신의 superview의 좌표계 안에서 자신의 위치를 결정한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/c9d0bfe5-fa09-4d2f-b17b-bfaa5b9aa6e0/image.png" alt=""></p>
<p>아래 discussion에 보니 view의 사이즈와 위치를 세팅할 때 이 frame을 사용하라고 한다.
frame을 세팅하면 view의 center 프로퍼티랑 bounds 가 자동으로 바뀐다.</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/1649308a-8e42-44ac-86d8-a93c4e128b25/image.png" alt=""></p>
<p>그럼 bounds는 뭐지?
슈퍼뷰하고 상관없이 자신만의 좌표시스템을 가진다고 한다</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/ccc39b77-5044-45a8-9840-cb9633f876ea/image.png" alt=""></p>
<p>UIView 문서를 보니 뷰를 생성할때는 슈퍼뷰가 될 뷰에 상대적으로 사이즈와 위치를 설정해야한다고 한다.
그래서 초기화시 frame을 사용하는구나 ! </p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/c2290aa1-5894-4071-bcae-7803372d338d/image.png" alt=""></p>
<h2 id="frame과-bounds를-공부할-때-헷갈리는-상황-두가지가-있다">frame과 bounds를 공부할 때 헷갈리는 상황 두가지가 있다</h2>
<ol>
<li>뷰의 bounds를 바꿨을 때 왜 뷰가 아니라 서브뷰가 반대방향으로 이동하지?<ul>
<li>frame을 바꿨을 땐 보통의 상식 대로 뷰가 이동함</li>
<li>bounds를 바꿨을 땐 서브뷰가 이동하는 것처럼 보이지만, 서브뷰가 직접 이동하는 것은 아니다.<ul>
<li>사실은 뷰가 이동을 해서 그 안에 들어간 서브뷰가 반대로 이동<strong>하는 것처럼 보이</strong>는 것임</li>
</ul>
</li>
</ul>
</li>
<li>뷰를 회전시켰을때 왜 뷰의 frame이 변하지?<ul>
<li>bounds는 그대로임</li>
<li>frame만 변하는데 그 이유는 frame의 정의가 뷰를 감싸고 있는 사각형이어서이다. 뷰를 회전시키면 비스듬히 되는데 그걸 감싸는 사각형을 그리면 보통 원래 뷰보다 너비와 높이가 길어짐</li>
</ul>
</li>
</ol>
<p>이 두개의 질문을 해결하기 위해서</p>
<p>view를 이동하고 회전하는 상황에서 frame과 bounds가 가진 두가지 속성, origin과 size가 어떻게 변하는지 비교해볼 것이다.</p>
<h2 id="1-origin-이동">1. origin 이동</h2>
<p>예제 그림으로 살펴보자</p>
<p>아래 그림에서 mainView의 frame의 origin은 superView의 origin로부터 50, 50 만큼 떨어져있음</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/c91be190-d59c-42bc-ab2f-c1f7fd271815/image.png" alt=""></p>
<pre><code class="language-swift">//superView - Cyan
let superViewRect = CGRect(origin: CGPoint(x: 50, y: 100),
                   size: CGSize(width: 300, height: 300))
let superView = UIView(frame: superViewRect)
superView.backgroundColor = .cyan
self.view.addSubview(superView)

//mainView - Yellow
let mainViewRect = CGRect(origin: CGPoint(x: 50, y: 50),
                           size: CGSize(width: 200, height: 200))
let mainView = UIView(frame: mainViewRect)
mainView.backgroundColor = .yellow
superView.addSubview(mainView)
print(&quot;mainView frame:&quot;, mainView.frame) //(50.0, 50.0, 200.0, 200.0)
print(&quot;mainView bounds:&quot;, mainView.bounds) //(0.0, 0.0, 200.0, 200.0)</code></pre>
<p>frame과 bounds를 찍어보니 이렇게 나왔음</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/e5fd9b9c-ff6e-4e47-86cd-0b467fd32c36/image.png" alt=""></p>
<p>frame은 슈퍼뷰에 비해 50, 50만큼 자신이 떨어져있다는 사실을 알고있고
bounds는 슈퍼뷰 상관안하고 그냥 자신이 기준이라 0,0임</p>
<h2 id="1-1-frame의-origin을-이동했을-때">1-1. frame의 origin을 이동했을 때</h2>
<p>그렇다면 이제 frame을 움직여보면 어떤 일이 일어날까?
궁금해서 세개의 뷰가 있는 예제를 만들어 직접 간단하게 찍어보았다.
대충 만든거라 못생겼어도 양해를 부탁합니다,,,</p>
<p><img src="https://velog.velcdn.com/images/dev_jane/post/349f4f2a-58b8-450a-a839-eac28c78e29a/image.png" alt=""></p>
<p>blueView, yellowView, greenView 세가지의 뷰들이 있는데
각각의 frame의 x좌표를 10만큼 움직이면 어떻게 될까?
<img src="https://velog.velcdn.com/images/dev_jane/post/b022d47c-9091-48b5-a6ad-278bc41f6da4/image.gif" alt=""></p>
<p>예상했던대로 frame은 자신의 슈퍼뷰와의 관계속에서 결정되니까
슈퍼뷰를 기준으로 10씩 오른쪽으로 움직이는군</p>
<h2 id="1-2-bounds의-origin을-이동했을-때">1-2. bounds의 origin을 이동했을 때</h2>
<p>그렇다면 bounds는?
<img src="https://velog.velcdn.com/images/dev_jane/post/003bc0cc-b03d-4d34-bb6f-fd4ccb6a6fd2/image.gif" alt=""></p>
<p>아니뭐야 blueView의 bounds의 x좌표를 10만큼 움직였더니 
blueView가 아니라 서브뷰인 YellowView의 frame이 움직이는 것 같이 보인다.
근데 yellowView의 frame은 변하지 않는 것 보이져?
blueView의 bounds만이 잘 바뀌고 있음 </p>
<p>yellowView의 bounds의 x좌표를 10만큼 움직였더니 
마찬가지로 또 yellowView가 아니라 서브뷰인 greenView가 움직이는 것 같이 보이네</p>
<p>마지막으로 greenView의 bounds의 x좌표를 10만큼 움직였는데 
이런,, 아무도 움직이지 않는다
greenView는 서브뷰가 없어서 greenView의 bounds가 변해도 티가 안나는 것이다
왜냐 저기 프린트되는 것을 보면 greenView의 bounds가 변하긴 하거든요</p>
<p>이 개념이 처음에 잘 이해가 안갔는데 아래 예시를 보고 바로 이해가 갔다.</p>
<h3 id="bounds의-개념은-스크롤뷰의-작동원리와도-관련이-있다">bounds의 개념은 스크롤뷰의 작동원리와도 관련이 있다.</h3>
<p>우선 아이폰으로 풍경사진을 찍는다고 가정해보자.</p>
<p>카메라를 켜면 넓은 실제 풍경을 다 담을 수 없고 어느 한 부분만 아이폰 화면에 보이게 된다. 
그리고 우리가 아이폰을 이리저리 움직이면 보여지는 풍경이 달라진다. </p>
<p>스크롤뷰도 이와 마찬가지인데, 화면보다 큰 내용을 가진 전체 페이지에서 사용자가 스크롤한 부분만 현재 보여주는 방식으로 작동을 한다. 
아이폰을 아래로 스크롤하는 상황에서, 
우리는 화면의 내용을 위로 올리고 있다고 생각하지만
사실은 화면이 내려가면서 페이지의 아래 부분을 보여주는 것이다. </p>
<p>내가 아이폰에서 스크롤 캡쳐를 한건데 이해가 쉽도록 나와있어서 가져와보았다.
<img src="https://velog.velcdn.com/images/dev_jane/post/0b0ad59f-5b26-4879-84cd-4a4c67b45b9d/image.gif" alt=""></p>
<p>그렇다면 다시 내가 만든 예제로 돌아와서,
<img src="https://velog.velcdn.com/images/dev_jane/post/b6b68aae-075f-4166-b47e-c3bb0043391e/image.png" alt=""></p>
<p>저 blueView의 bounds의 x좌표를 10만큼 움직이면
blueView가 오른쪽으로 이동하면서, 마치blueView의 서브뷰인 yellowView가 왼쪽으로 이동하는 것처럼 보이는 것이다. </p>
<p>이건 스크롤을 아래로 내릴때 실제로는 스마트폰 화면이 아래로 이동했는데 내용이 위로 올라가는 것처럼 보이는것과 같은 원리다. </p>
<h2 id="2-회전">2. 회전</h2>
<p>그렇다면 이제 뷰를 회전시켰을 때의 상황에 대해 알아보자.
검색하다가 발견한 좋은 예제이다.
<img src="https://velog.velcdn.com/images/dev_jane/post/c5d8bd25-c847-4c6d-88c9-2cbbaed07c73/image.gif" alt=""></p>
<p>뷰를 회전시키자 bounds는 그대로고 frame만 바뀌는 상황을 볼 수 있다.
bounds는 슈퍼뷰하고 상관없이 자신만의 좌표시스템을 가지기 때문에 회전해도 아무런 변화가 없다.</p>
<p>그렇담 frame은? 저 view를 감싸는 사각형이 frame인데
view가 회전되면 사각형의 크기가 달라진다.
따라서 변하는거</p>
<p><a href="https://serialcoder.dev/text-tutorials/ios-tutorials/frame-vs-bounds-in-ios-implementing-a-visual-demonstration/">GIF 출처</a></p>
]]></description>
        </item>
    </channel>
</rss>