<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>syong_e.log</title>
        <link>https://velog.io/</link>
        <description>iOS앱 개발자가 될테야</description>
        <lastBuildDate>Fri, 01 Aug 2025 05:20:36 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. syong_e.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/syong_e" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[SwiftUI] StackView]]></title>
            <link>https://velog.io/@syong_e/SwiftUI-StackView</link>
            <guid>https://velog.io/@syong_e/SwiftUI-StackView</guid>
            <pubDate>Fri, 01 Aug 2025 05:20:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/dldmswo1209/post/6b5d4e2f-4440-433e-93fe-18559539889e/image.png" alt="">
SwiftUI 에서 제공하는 가장 기본적인 뷰 컨테이너로 HStack, VStack, ZStack 이 있다.</p>
<p>이들은 단순히 하위 뷰들을 정렬하는 방식에 차이가 있다.
HStack은 하위 뷰를 가로로 정렬하고, VStack은 세로로, ZStack은 뷰들을 층으로 쌓는다.</p>
<h1 id="hstack">HStack</h1>
<p><img src="https://velog.velcdn.com/images/dldmswo1209/post/73d0eb92-d95c-4be9-968a-99518e7e9384/image.png" alt=""></p>
<p>뷰를 가로로 정렬하고 싶다면 HStack을 사용한다</p>
<pre><code class="language-swift">struct HStackEx: View {
    var body: some View {
        HStack(alignment: .top, spacing: 12) {
            Text(&quot;Text1&quot;)
            Text(&quot;Text2&quot;)
            Text(&quot;Text3&quot;)
            Text(&quot;Text4&quot;)
            Text(&quot;Text5&quot;)
        }
    }
}</code></pre>
<p>생성자 파라미터인 alignment와 spacing을 사용하여 하위 뷰의 세부 정렬을 어떻게 할 것인지 설정할 수 있다</p>
<ul>
<li>alignment: 세로 방향 정렬</li>
<li>spacing: 하위 뷰들의 간격</li>
</ul>
<h1 id="vstack">VStack</h1>
<p><img src="https://velog.velcdn.com/images/dldmswo1209/post/4ff0836d-7a8e-41ed-a4dc-995d362bea94/image.png" alt="">
뷰를 세로로 정렬하고 싶다면 VStack을 사용한다</p>
<pre><code class="language-swift">struct VStackEx: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(&quot;Text1&quot;)
            Text(&quot;Text2&quot;)
            Text(&quot;Text3&quot;)
            Text(&quot;Text4&quot;)
            Text(&quot;Text5&quot;)
            Text(&quot;Text6&quot;)
            Text(&quot;Text7&quot;)
            Text(&quot;Text8&quot;)
            Text(&quot;Text9&quot;)
            Text(&quot;Text10&quot;)
        }
    }
}</code></pre>
<ul>
<li>alignment: 가로 방향 정렬<h1 id="zstack">ZStack</h1>
<img src="https://velog.velcdn.com/images/dldmswo1209/post/daf4a36c-c3fb-421e-acd5-280a868c3d53/image.png" alt="">
뷰를 겹쳐서 쌓고 싶다면 ZStack을 사용한다<pre><code class="language-swift">struct ZStackEx: View {
  var body: some View {
      ZStack {
          Text(&quot;1&quot;)
              .font(.largeTitle)
              .frame(width: 300, height: 300, alignment: .top)
              .background(Color.red)
          Text(&quot;2&quot;)
              .font(.largeTitle)
              .frame(width: 200, height: 200, alignment: .top)
              .background(Color.blue)
          Text(&quot;3&quot;)
              .font(.largeTitle)
              .frame(width: 100, height: 100, alignment: .top)
              .background(Color.green)
      }
  }
}</code></pre>
</li>
<li>alignment: 가로/세로 방향 정렬</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UIKit] UIScrollView LayoutGuide]]></title>
            <link>https://velog.io/@syong_e/UIKit-UIScrollView-contentSize</link>
            <guid>https://velog.io/@syong_e/UIKit-UIScrollView-contentSize</guid>
            <pubDate>Tue, 20 Feb 2024 14:40:09 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트를 하면서 UIScrollView를 오랜만에 사용해봤는데, AutoLayout을 설정하는데 있어서 어려움을 겪었다.</p>
<table>
<thead>
<tr>
<th align="center">구현 해야하는 UI</th>
<th align="center">AutoLayout에 문제가 발생한 UI</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><img src="https://velog.velcdn.com/images/syong_e/post/8aff4fe8-52bc-457c-ad2a-b4f8b03f3773/image.png" alt=""></td>
<td align="center"><img src="https://velog.velcdn.com/images/syong_e/post/a56e0323-fc1d-4a5a-9f1b-8e85ee457e48/image.png" alt=""></td>
</tr>
</tbody></table>
<p>왼쪽 UI를 ScrollView와 함께 구현하고자 AutoLayout을 설정했더니 오른쪽과 같은 UI가 나왔다.
스크롤뷰를 쉽게 확인하기 위해서 backgroundColor 를 보라색으로 설정했다.</p>
<p>문제의 AutoLayout 설정 코드는 아래와 같다. (SnapKit 사용)</p>
<pre><code class="language-swift">view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
    make.edges.equalTo(view.safeAreaLayoutGuide)
}

scrollView.addSubview(containerStackView)
containerStackView.snp.makeConstraints { make in
    make.top.horizontalEdges.equalToSuperview().inset(20)
}</code></pre>
<p>scrollView가 SafeArea를 제외하고, 화면을 꽉 채우도록 설정한 후 Label과 TextField를 포함하는 ContainerStackView의 위치를 스크롤뷰를 기준으로 AutoLayout을 설정했다.
확인하기 전까지는 문제 없는 코드라고 생각했으나, 실제로는 내가 예상한 것과는 다른 결과가 나왔다.</p>
<h2 id="uiscrollview-layoutguide">UIScrollView LayoutGuide</h2>
<p>알아보니 스크롤 뷰를 사용하는 것은 꽤나 까다로운 것 같다.</p>
<p>스크롤 뷰는 말 그대로 스크롤이 가능한 뷰이고, 스크롤을 하기 위해서는 스크롤이 가능한 영역의 크기를 알아야 한다. 즉, 스크롤할 수 있는 content의 높이와 너비를 지정해 줘야 한다.</p>
<p>스크롤이 가능한 영역의 크기를 지정하기 위해서 contentLayoutGuide와 frameLayoutGuide 각각에 대해 설정 해야 한다.</p>
<p>2개의 LayoutGuide 에 대해 간단하게 설명하면 다음과 같다</p>
<ul>
<li><a href="https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide">contentLayoutGuide</a>: UIScrollView의 <code>스크롤 가능한 영역</code>을 나타내는 가이드</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiscrollview/2865772-framelayoutguide">frameLayoutGuide</a>: UIScrollView의 <code>화면에 보여지는 영역</code>을 나타내는 가이드</li>
</ul>
<p>LayoutGuide를 설정한 코드는 다음과 같다. </p>
<pre><code class="language-swift">private let scrollView: UIScrollView
private let contentView: UIView
private let titleInputContainerStackView: UIStackView

...

view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
    make.egdes.equalTo(view.safeAreaLayoutGuide)
}

scrollView.addSubview(contentView)
contentView.snp.makeConstraints { make in
    make.edges.equalTo(scrollView.contentLayoutGuide)
    make.width.equalTo(scrollView.frameLayoutGuide.snp.width)
}

contentView.addSubview(containerStackView)
containerStackView.snp.makeConstraints { make in
    make.top.horizontalEdges.equalToSuperview().inset(20)
    make.bottom.equalTo(contentView)
}</code></pre>
<ul>
<li>LayoutGuide를 설정하기 위해서 contentView라는 UIView를 추가한다.</li>
<li>스크롤 가능한 영역, 즉 contentLayoutGuide를 설정하기 위해서 contentView에 제약 조건을 추가한다.</li>
<li>화면에 보여지는 영역, 즉 frameLayoutGuide를 설정하기 위해서 width를 frameLayoutGuide와 일치시킨다. 만약, 가로 스크롤을 하고 싶다면, height를 설정하면 된다.</li>
<li>현재까지는 contentView의 height는 정의되지 않은 상태. 이 예제에서는 contentView에 subView를 추가하여 contentView의 height를 지정했다.</li>
<li>containerStackView의 bottom을 contentView로 지정하여 StackView의 높이만큼 contentView의 높이를 설정했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Clean Architecture]]></title>
            <link>https://velog.io/@syong_e/Clean-Architecture</link>
            <guid>https://velog.io/@syong_e/Clean-Architecture</guid>
            <pubDate>Thu, 18 Jan 2024 09:59:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/syong_e/post/2cff4a5d-f05a-4c5d-a4e7-021191173acf/image.png" alt=""></p>
<p>클린 아키텍쳐의 주요 원칙은 내부 Layer 에서 외부 Layer 로 종속성을 갖지 않는 것
반대로 외부 Layer 에서는 내부로만 종속성이 있을 수 있습니다.</p>
<p>모든 계층을 그룹화하면 아래와 같이 3가지의 계층으로 나뉩니다.
<img src="https://velog.velcdn.com/images/syong_e/post/e1dcac3d-0092-4ec4-aa74-9ccec7b1cf96/image.png" alt=""></p>
<h2 id="domain-layer-비즈니스-로직">Domain Layer (비즈니스 로직)</h2>
<ul>
<li>가장 안쪽의 레이어(다른 레이어에 대한 종속성이 없이 완전히 격리)</li>
<li><code>Entity</code>, <code>UseCase</code>, <code>Repository Interface</code> 가 포함되어 있음.</li>
<li>다른 레이어의 종속성을 갖지 않기 때문에 <code>재사용성</code>과 <code>테스트</code>가 용이</li>
</ul>
<p>MVVM 패턴에서 ViewModel이 가지고 있던 네트워크 통신, 비즈니스 로직을 Domain Layer 로 분리합니다.
이를 통해 ViewModel은 Presentation Layer 로직에만 집중할 수 있습니다. 즉, UI와 관련된 상태 관리 및 이벤트 처리에 집중 할 수 있습니다.</p>
<h3 id="entity">Entity</h3>
<p>Entity는 &quot;Enterprise wide business rules&quot; 를 캡슐화 한다.
이는 메서드가 포함된 개체일 수도 있고, 데이터 구조 및 함수의 집합일 수도 있다.
가장 일반적이고 높은 수준의 규칙을 캡슐화하고, 외부 변화가 있을 때, 변화할 가능성이 가장 적다.</p>
<p>솔직히 이것만 보면, Entity가 대체 뭔지 감도 안잡힙니다.</p>
<p>이해를 돕기 위해 iOS 클린 아키텍처 예제와 함께 보도록 하겠습니다.</p>
<p>Github: <a href="https://github.com/kudoleh/iOS-Clean-Architecture-MVVM">https://github.com/kudoleh/iOS-Clean-Architecture-MVVM</a></p>
<table>
<thead>
<tr>
<th align="left"><img src="https://velog.velcdn.com/images/syong_e/post/5ce3654f-19f9-4346-b847-7c059df61065/image.png" alt=""></th>
<th align="left"><img src="https://velog.velcdn.com/images/syong_e/post/9c5cff42-0a1e-4ec7-839d-92f350ec2548/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>간단한 영화 검색 앱 입니다.
이 앱에서 Entity 는 무엇일까요?
프로젝트의 Domain/Entity 폴더를 보면 Movie.swift 파일이 있습니다.</p>
<pre><code class="language-swift">struct Movie: Equatable, Identifiable {
    typealias Identifier = String
    enum Genre {
        case adventure
        case scienceFiction
    }
    let id: Identifier
    let title: String?
    let genre: Genre?
    let posterPath: String?
    let overview: String?
    let releaseDate: Date?
}</code></pre>
<p>영화의 정보를 담는 단순한 <code>데이터 구조</code>를 가지고 있습니다. 추가로 함수들도 가질 수 있겠죠.
그냥 단순하게 데이터를 담는 데이터 구조, 함수 집합이라고 생각하면 될 것 같습니다.</p>
<p>영화 앱에서는 영화에 대한 정보를 보여줘야 하니까 Movie 라는 데이터 구조체를 선언해서 데이터를 담아서 사용하는 것 입니다.</p>
<h3 id="usecase">UseCase</h3>
<p>UseCase는 애플리케이션에서 수행되는 <code>특정한 작업</code>이나<code>행동</code>을 의미합니다.</p>
<p>영화 검색 앱에서 특정한 작업이나 행동은 무엇일까요?</p>
<pre><code class="language-swift">protocol SearchMoviesUseCase {
    func execute(
        requestValue: SearchMoviesUseCaseRequestValue,
        cached: @escaping (MoviesPage) -&gt; Void,
        completion: @escaping (Result&lt;MoviesPage, Error&gt;) -&gt; Void
    ) -&gt; Cancellable?
}</code></pre>
<p>영화를 검색하는 행위가 있을 것입니다.</p>
<p>이러한 특정한 작업이나 행동에 해당하는 애플리케이션의 모든 비즈니스 로직을 UseCase라고 합니다.</p>
<p>예를 들어, &#39;사용자 정보를 조회하는&#39; 작업 또한 UseCase 입니다.</p>
<p>MVVM 패턴에서는 이러한 비즈니스 로직이 ViewModel 에 있었지만, 클린 아키텍처에서는 비즈니스 로직을 UseCase로 분리하여 역할 분할을 더욱 확실하게 한 모습입니다.</p>
<h3 id="repository-interface">Repository Interface</h3>
<p>Repository Interface는 UseCase(Domain) 와 Repository(Data) 사이의 Interface 를 제공합니다.
클린 아키텍처의 주요 원칙으로서 &#39;내부 계층은 외부 계층으로의 종속성을 갖지 않는다&#39; 라고 했었죠?
따라서 원칙상 UseCase에서 Repository에 의존성을 가져서는 안됩니다.</p>
<p>그래서 UseCase 에서는 Repository 구현체를 직접적으로 가지지 않고, Interface 를 통해서 접근합니다.</p>
<pre><code class="language-swift">// MoviesRepository.swift

protocol MoviesRepository {
    @discardableResult
    func fetchMoviesList(
        query: MovieQuery,
        page: Int,
        cached: @escaping (MoviesPage) -&gt; Void,
        completion: @escaping (Result&lt;MoviesPage, Error&gt;) -&gt; Void
    ) -&gt; Cancellable?
}

// DefaultMoviesRepository.swift
final class DefaultMoviesRepository: MoviesRepository { ... }

//SearchMoviesUseCase.swift

final class DefaultSearchMoviesUseCase: SearchMoviesUseCase {

    private let moviesRepository: MoviesRepository

    ...

}</code></pre>
<p>위와 같이 protocol을 통해 추상화를 하면, Domain Layer 는 Data Layer의 변경에 영향을 받지 않게 됩니다. 즉, 데이터 소스가 변경되더라도 이에 대한 변경 사항은 Data Layer 내에서만 처리되며, Domain Layer 는 이에 대해 알 필요가 없습니다.</p>
<h2 id="presentation-layer">Presentation Layer</h2>
<p>Presentation Layer 는 사용자와 직접적으로 상호작용하는 계층으로, UI와 관련된 작업을 담당합니다. </p>
<p>ViewController 와 ViewModel 이 Presentation Layer 에 속합니다.
ViewModel에서 UseCase로부터 데이터를 요청하고, 받은 데이터를 통해 ViewController에서 UI를 업데이트 합니다.</p>
<h2 id="data-layer">Data Layer</h2>
<p>Data Layer 는 애플리케이션의 데이터에 관련된 모든 작업을 처리하는 계층입니다.
네트워크 통신, 로컬 DB 가 이 계층에 포함됩니다.</p>
<pre><code class="language-swift">final class DefaultMoviesRepository {

    private let dataTransferService: DataTransferService
    private let cache: MoviesResponseStorage
    private let backgroundQueue: DataTransferDispatchQueue

    init(
        dataTransferService: DataTransferService,
        cache: MoviesResponseStorage,
        backgroundQueue: DataTransferDispatchQueue = DispatchQueue.global(qos: .userInitiated)
    ) {
        self.dataTransferService = dataTransferService
        self.cache = cache
        self.backgroundQueue = backgroundQueue
    }
}

extension DefaultMoviesRepository: MoviesRepository {

    func fetchMoviesList(
        query: MovieQuery,
        page: Int,
        cached: @escaping (MoviesPage) -&gt; Void,
        completion: @escaping (Result&lt;MoviesPage, Error&gt;) -&gt; Void
    ) -&gt; Cancellable? {

        let requestDTO = MoviesRequestDTO(query: query.query, page: page)
        let task = RepositoryTask()

        cache.getResponse(for: requestDTO) { [weak self, backgroundQueue] result in

            if case let .success(responseDTO?) = result {
                cached(responseDTO.toDomain())
            }
            guard !task.isCancelled else { return }

            let endpoint = APIEndpoints.getMovies(with: requestDTO)
            task.networkTask = self?.dataTransferService.request(
                with: endpoint,
                on: backgroundQueue
            ) { result in
                switch result {
                case .success(let responseDTO):
                    self?.cache.save(response: responseDTO, for: requestDTO)
                    completion(.success(responseDTO.toDomain()))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }
        return task
    }
}</code></pre>
<p>네트워크 통신을 통해 영화 목록을 가져오는 Repository 구현체는 Data Layer 에 해당합니다. UseCase 에서는 이 Repository 의 Interface를 통해 데이터를 요청합니다.</p>
<pre><code class="language-swift">struct MoviesResponseDTO: Decodable {
    private enum CodingKeys: String, CodingKey {
        case page
        case totalPages = &quot;total_pages&quot;
        case movies = &quot;results&quot;
    }
    let page: Int
    let totalPages: Int
    let movies: [MovieDTO]
}</code></pre>
<p>JSON 형태의 데이터를 Swift 데이터 타입으로 변환하기 위한 DTO(Data Transfer Object) 입니다.
DTO 또한 Data Layer 에 속합니다.</p>
<pre><code class="language-swift">extension MoviesResponseDTO {
    func toDomain() -&gt; MoviesPage {
        return .init(page: page,
                     totalPages: totalPages,
                     movies: movies.map { $0.toDomain() })
    }
}

extension MoviesResponseDTO.MovieDTO {
    func toDomain() -&gt; Movie {
        return .init(id: Movie.Identifier(id),
                     title: title,
                     genre: genre?.toDomain(),
                     posterPath: posterPath,
                     overview: overview,
                     releaseDate: dateFormatter.date(from: releaseDate ?? &quot;&quot;))
    }
}

extension MoviesResponseDTO.MovieDTO.GenreDTO {
    func toDomain() -&gt; Movie.Genre {
        switch self {
        case .adventure: return .adventure
        case .scienceFiction: return .scienceFiction
        }
    }
}</code></pre>
<p>Domain Layer 또는 Presentation Layer 에서는 Data Layer 에 종속성을 가져서는 안됩니다.
따라서 Domain Layer에 해당하는 Entity로 변환해주는 메서드를 extension 으로 구현한 모습입니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] @ObservedObject 와 @StateObject 차이점 ]]></title>
            <link>https://velog.io/@syong_e/SwiftUI-ObservedObject-%EC%99%80-StateObject-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@syong_e/SwiftUI-ObservedObject-%EC%99%80-StateObject-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Tue, 02 Jan 2024 08:32:53 GMT</pubDate>
            <description><![CDATA[<h1 id="문제-상황">문제 상황</h1>
<img src="https://velog.velcdn.com/images/syong_e/post/9bc9be41-ef69-41b2-84c2-2a4dd702234f/image.gif" width="50%"/>

<p>프로젝트의 채팅 기능을 구현하던 중 사용자가 채팅 화면을 보고 있을 때, 다른 사용자가 채팅방에 입장하거나 퇴장할 때, 갑자기 메세지가 보이지 않는 버그가 발생했다.
이 문제를 해결하기 위해 ViewModel 생성자에서 디버깅을 해보았다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/8b70bf2e-ab0b-42ed-88df-055fa6503df1/image.png" alt=""></p>
<p>그 결과 현재 사용자가 참여하고 있는 CarPool 데이터 모델이 업데이트 될 때마다 ViewModel이 초기화되고 있음을 알게되었다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/6b8a26a3-be97-4eb4-9cc0-9c11d03beaa7/image.png" alt=""></p>
<p>View 에서는 앱 전체에서 사용되는 ObservableObject인 AppData 객체를 가지고 있고, @ObservedObject 속성으로 선언된 ViewModel을 가지고 있다.</p>
<p>그리고 AppData에서 현재 사용자가 참여하고 있는 CarPool 데이터 모델을 @Published로 가지고 있다.
따라서 CarPool이 변경될 때마다 뷰를 다시 그린다.</p>
<h1 id="문제-해결">문제 해결</h1>
<p>결론부터 말하면, ViewModel을 @ObservedObject가 아닌 @StateObject로 선언해서 해결했다.
@ObservedObject는 상위뷰의 상태값이 변경되었을 때, 초기화 되지만, @StateObject는 초기화 되지 않고, ObservableObject 객체를 가지고 있는 뷰의 생명주기 동안 상태를 유지한다.</p>
<p>따라서 AppData에서 가지고 있는 CarPool 데이터 모델이 변경되면(사용자 입장 or 퇴장) @ObservedObject로 선언한 객체는 상위뷰의 상태가 변경되었으므로 초기화된다. 그래서 이 문제를 해결하기 위해 @StateObject로 변경했다.</p>
<h1 id="간단한-예시">간단한 예시</h1>
<p>위 문제 상황을 좀 더 간단한 예시로 바꿔 ObservedObject와 StateObject의 차이점에 대해서 명확히 이해할 수 있었다.</p>
<p>아주 간단한 카운터를 만들기 위한 ViewModel을 아래와 같이 만들어봤다.</p>
<pre><code class="language-swift">extension CountView {
    final class ViewModel: ObservableObject {
        @Published var count: Int = 0

        init() {
            print(&quot;DEBUG: CountView&#39;s ViewModel is initialize&quot;)
        }

        func increase() { count += 1 }
    }
}</code></pre>
<p>ViewModel은 <code>ObservableObject</code> 를 채택한다. 이 프로토콜을 채택하면, 말 그대로 관찰가능한 객체가 되고, <code>@Published</code> 프로퍼티 래퍼 속성으로 선언된 변수, <code>count</code> 가 변경될 때, 외부에 변경사항을 알릴 수 있게된다.
만약 View에서 이 ViewModel을 가지고 있다면, count 가 변경되었을 때, count 를 사용하고 있는 뷰 계층 구조를 <code>다시 그린다.</code></p>
<pre><code class="language-swift">struct CountView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack(spacing: 10) {
            Text(&quot;CountView&#39;s Count is \(viewModel.count)&quot;)

            Button(action: {
                viewModel.increase()
            }, label: {

                Text(&quot;Add Count&quot;)
            })
        }
    }
}</code></pre>
<p>위에서 만든 ViewModel을 사용하는 CountView 이다.
ViewModel 선언을 보면, <code>@ObservedObject</code> 프로퍼티 래퍼로 선언되었다.</p>
<p>애플 공식 문서를 보면 @ObservedObject를 다음과 같이 설명한다.</p>
<blockquote>
<p>ObservableObject를 구독하고 해당 객체가 변경될 때마다 View를 무효화하는 프로퍼티 래퍼 타입입니다.</p>
</blockquote>
<p>따라서 ViewModel을 구독하고, ViewModel의 count가 변경될 때마다 View를 업데이트 하기 위해서 @ObservedObject 프로퍼티 래퍼로 선언했다.</p>
<p>이 코드의 결과를 화면으로 보면, 아래와 같다.
<img src="https://velog.velcdn.com/images/syong_e/post/3ec9b8d0-a426-488b-88ea-41ca244386c6/image.gif" alt=""></p>
<p>Button을 누를 때마다 count 가 변경되고, count를 사용하고 있는 View가 다시 그려지면서 위와 같은 간단한 카운터를 만들 수 있다.</p>
<h1 id="observedobject의-문제점">@ObservedObject의 문제점</h1>
<p>위와 같은 상황에서는 정상적으로 동작하고, 문제가 없어보인다. 하지만, @ObservedObject를 사용하다보면 상태가 유지되어야 할 것 같은데, 초기화되는 문제를 종종 맞닥뜨린다.</p>
<pre><code class="language-swift">struct CountView: View {
    @Binding var parentCount: Int
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack(spacing: 10) {
            Text(&quot;CountView&#39;s Count is \(viewModel.count)&quot;)

            Button(action: {
                viewModel.increase()
            }, label: {

                Text(&quot;Add Count&quot;)
            })

            Text(&quot;ParentView&#39;s Count is \(parentCount)&quot;)

            Button(action: {
                parentCount += 1
            }, label: {

                Text(&quot;Add Count&quot;)
            })
        }
    }
}

struct ParentView: View {
    @State private var count: Int = 0

    var body: some View {
        NavigationView {
            VStack {
                Text(&quot;ParentView&#39;s count is \(count)&quot;)
                NavigationLink {
                    CountView(parentCount: $count)
                } label: {
                    Text(&quot;Navigate to CountView&quot;)
                }
            }
        }
    }
}</code></pre>
<p>문제 상황과 비슷한 예시를 만들기 위해서 코드를 위와 같이 작성했다. 문제없이 동작할 것 같지만, 막상 돌려보면 문제가 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/3e84a441-5751-468c-9f0d-6e87d2536e05/image.gif" alt=""></p>
<p>ParentView의 count가 변경되면, CountView의 Count가 초기화된다.
무엇이 문제인지 확인하기 위해서 로그를 보면
<img src="https://velog.velcdn.com/images/syong_e/post/c3f98cbd-fd5a-4475-a2aa-a9dce34b1af2/image.png" alt=""></p>
<p>ParentView의 count를 증가시킬 때마다 CountView의 ViewModel이 초기화 되고있다. 그래서 count가 0으로 돌아가는 것이다.</p>
<p>사실 생각해보면 이 결과는 당연하다. ParentView의 State 변수인 count가 변경되면, 해당 변수를 사용하고 있는 모든 하위 View들은 초기화된다. 그러면 CountView의 ViewModel도 초기화될 것이다.</p>
<h1 id="stateobject">StateObject</h1>
<p>이 문제점을 해결하기 위해서 iOS 14부터 StateObject가 등장했다.</p>
<p>애플 공식 문서에서는 다음과 같이 소개한다.</p>
<blockquote>
<p>ObservableObject를 인스턴스화 하는 프로퍼티 래퍼 타입입니다.</p>
</blockquote>
<p>이것만 봐서는 ObservedObject와 어떤 차이가 있는지 극명하게 드러나지 않는다. 
다른 공식 문서를 찾아보면 추가적인 설명을 찾아볼 수 있다.</p>
<blockquote>
<p>StateObject는 ObservedObject와 거의 똑같으나, StateObject는 하나의 객체로 만들어지고, View가 얼마나 초기화되든지 상관없이 별개의 객체로 관리된다.</p>
</blockquote>
<p>즉, StateObject로 생성된 객체는 View의 생명 주기와 상관없이 SwiftUI가 별도의 공간에 저장해서 상태값을 유지할 것이다.</p>
<p>위 코드에서 ObservedObject로 선언한 것을 StateObject로 바꿔보자.</p>
<pre><code class="language-swift">struct CountView: View {
    @Binding var parentCount: Int
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        VStack(spacing: 10) {
            Text(&quot;CountView&#39;s Count is \(viewModel.count)&quot;)

            Button(action: {
                viewModel.increase()
            }, label: {

                Text(&quot;Add Count&quot;)
            })

            Text(&quot;ParentView&#39;s Count is \(parentCount)&quot;)

            Button(action: {
                parentCount += 1
            }, label: {

                Text(&quot;Add Count&quot;)
            })
        }
    }
}</code></pre>
<p>ViewModel을 StateObject로 생성했다. 따라서 이 ViewModel은 ContentView가 초기화되는 것과 상관없이 SwiftUI가 별도의 공간에 저장하여 상태값을 유지할 것이다.</p>
<p>실제로 실행시켜보면, ParentView의 count 값이 증가하여 ContentView가 다시 초기화 된다고 하더라도 ViewModel의 count는 초기화되지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/1493c29a-a386-4ae1-b665-0562de8d5f2e/image.png" alt=""></p>
<p>실제로 ViewModel이 초기화되지 않는지 로그를 확인해보니 최초에 1번만 초기화되고, 이후에 ParentView의 count 를 증가시켜도 초기화되지 않는다.</p>
<h1 id="stateobject와-observedobject-사용법">StateObject와 ObservedObject 사용법</h1>
<pre><code class="language-swift">class DataModel: ObservableObject {
    @Published var name = &quot;Some Name&quot;
    @Published var isEnabled = false
}


struct MyView: View {
    @StateObject private var model = DataModel()


    var body: some View {
        Text(model.name)
        MySubView(model: model)
    }
}


struct MySubView: View {
    @ObservedObject var model: DataModel


    var body: some View {
        Toggle(&quot;Enabled&quot;, isOn: $model.isEnabled)
    }
}</code></pre>
<p>애플 공식 문서에는 위와 같은 코드 예시를 보여주고 있다.</p>
<p>ObservableObject 객체를 생성할 때, StateObject로 생성하고, 해당 객체를 하위뷰에서 사용할 때에는 ObservedObject로 전달받아 사용하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Firebase] Firestore 데이터 페이징 (Swift)]]></title>
            <link>https://velog.io/@syong_e/Firebase-Firestore-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%9D%B4%EC%A7%95</link>
            <guid>https://velog.io/@syong_e/Firebase-Firestore-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8E%98%EC%9D%B4%EC%A7%95</guid>
            <pubDate>Mon, 01 Jan 2024 09:41:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/syong_e/post/14ed5165-3181-48a1-bc05-3eef4c4d5c8a/image.jpeg" alt=""></p>
<p>해피뉴이어~ ^_^
새해부터 공부하고 있는 나... 대견하다
올해는 꼭 취업하자!!!!</p>
<h1 id="문제-상황">문제 상황</h1>
<p>이전에 했던 프로젝트 리팩토링을 하던 중 채팅 기능을 구현해야했고, 별도의 서버를 두지 않고 Firestore를 사용해서 구현했습니다.</p>
<p>Firestore는 문서에 대한 스냅샷 리스너를 등록해두면, 해당 문서가 변경되었을 때 실시간으로 변경사항을 가져올 수 있습니다. 이 리스너를 통해 간단하게 채팅앱을 구성했습니다.</p>
<p>하지만, 단순히 리스너를 등록했을 때 비용적인 측면에서 문제점이 존재합니다.
예를들어, 유저가 참여중인 채팅방에 수백 ~ 수천개의 메세지 데이터가 저장되어있다고 가정했을 때 유저가 해당 채팅방에 들어가면, 모든 메세지를 한번에 읽어오기 때문에 비용 측면에서 낭비가 발생합니다.</p>
<p>이러한 문제를 해결하기 위해서 페이징(Paging) 기법을 통해 한 번에 데이터를 읽어오는 갯수를 제한하여 비용을 절약할 수 있습니다.</p>
<p>그래서 저는 Firestore 데이터를 페이징하여 효율적으로 데이터를 읽어오는 것에 대한 고민을 했습니다.</p>
<h1 id="메세지-페이징-아이디어">메세지 페이징 아이디어</h1>
<p>정적인 데이터 리스트를 페이징 처리하는 것은 쉽게 할 수 있지만, 이전 채팅 기록을 페이징하는 것과 동시에 새로운 메세지에 대한 처리도 해줘야 했기 때문에 까다롭다고 생각을 했습니다. 그래서 수많은 고민을 했고, 최선의 방법을 도출해냈습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/db9a48ad-3547-4cfd-a8a7-36bb5adeff22/image.png" alt=""></p>
<blockquote>
<ol>
<li>유저가 채팅방에 들어갔을 때, 가장 최근 메세지부터 20개를 읽어온다.</li>
<li>읽어온 데이터의 첫번째 Document(startDoc)와 마지막 Document(lastDoc)를 캐싱한다.</li>
<li>다음 페이지 요청시 startDoc(이전 페이지의 첫번째 Document) 이전까지의 메세지를 20개 읽어오고 다시 2번으로</li>
<li>새 메세지에 대한 리스너 등록은 lastDoc(최초에 읽어온 메세지의 마지막 Doucument) 이후의 메세지에 대해 등록한다.</li>
</ol>
</blockquote>
<p>이렇게 구현하면, 채팅방에 메세지 데이터가 수백 ~ 수천개 쌓여 있다고 하더라도 사용자가 요청한 페이지만큼의 데이터만을 읽기 때문에 효율적이고, 또한 새 메세지를 구독하기 때문에 실시간 채팅 기능도 구현할 수 있습니다.</p>
<h1 id="코드-구현">코드 구현</h1>
<pre><code class="language-swift">    private var startDoc: DocumentSnapshot?
    private var lastDoc: DocumentSnapshot?
    private var endPaging: Bool = false
    private var limit: Int = 5</code></pre>
<p>우선 필요한 프로퍼티를 선언해줍니다.
endPaging은 모든 페이지를 읽어왔는지 확인하기 위한 Flag 변수이고, limit는 한 페이지 당 읽어올 데이터 수 입니다. </p>
<pre><code class="language-swift">    func fetchMessages(
        roomId: String
    ) async -&gt; [WrappedMessage] {
        if endPaging { return [] }
        guard let uid = auth.currentUser?.uid else { return [] }

        let commonQuery = db.collection(&quot;Rooms&quot;)
            .document(roomId)
            .collection(&quot;ChatLogs&quot;)
            .order(by: &quot;timestamp&quot;) // timestamp 필드를 기준으로 오름차순 정렬
            .limit(toLast: limit) // 마지막에서 20개

        let requestQuery: Query

        /// 이전 페이지의 첫번재 Document가 있는지 확인
        if let startDoc = startDoc {
            /// 다음 페이지 = 이전 페이지의 첫번째 Document 이전까지의 20개
            requestQuery = commonQuery
                .end(beforeDocument: startDoc)
        } else {
            requestQuery = commonQuery
        }

        do {
            let snapshot = try await requestQuery.getDocuments()

            /// document가 비어있다면, 더 이상 다음 페이지는 존재하지 않음.
            /// 이후 쿼리 요청을 막기 위해서 Bool 타입 flag 변수 사용
            if snapshot.documents.isEmpty {
                endPaging = true
                return []
            }

            /// 현재 페이지의 첫번째 Document와 마지막 Document를 기록
            /// startDoc은 다음 페이지의 마지막 Document를 지정하기 위해서 사용
            /// lastDoc은 새로운 메세지를 구독하는 시작 Document를 지정하기 위해서 사용
            startDoc = snapshot.documents.first
            lastDoc = snapshot.documents.last

            /// 스냅샷 데이터 처리...


        } catch {
            print(&quot;DEBUG: Fail to subscribeNewMessages with error: \(error.localizedDescription)&quot;)
            return []
        }
    }</code></pre>
<p>limit(toLast:)를 사용해서 읽어올 데이터 수를 제한하고, end(beforeDocument:)를 사용해서 startDoc 이전까지의 데이터를 읽어옵니다.</p>
<p>결과적으로 해당 쿼리를 요청하면, 이전 페이지의 첫번째 Document 이전까지 데이터를 읽을건데, 그 중에서 마지막에서 20개를 읽어오는 것입니다.</p>
<pre><code class="language-swift">    func subscribeNewMessages(
        roomId: String,
        completion: @escaping([WrappedMessage]) -&gt; Void
    ) {
        guard let uid = auth.currentUser?.uid else { return }

        let commonQuery = db.collection(&quot;Rooms&quot;)
            .document(roomId)
            .collection(&quot;ChatLogs&quot;)
            .order(by: &quot;timestamp&quot;) // timestamp를 기준으로 오름차순 정렬

        let requestQuery: Query

        /// 첫번째 페이지의 마지막 Document(구독의 시작 지점)를 가져옴
        if let lastDoc = lastDoc {
            /// lastDoc 이후의  Query
            requestQuery = commonQuery
                .start(afterDocument: lastDoc)
        } else {
            requestQuery = commonQuery
        }

        requestQuery.addSnapshotListener { snapshot, error in
            guard let document = snapshot?.documents else {
                print(&quot;DEBUG: Fail to subscribeNewMessages with error document is nil&quot;)
                return
            }

            // 스냅샷 데이터 처리...
        }
    }</code></pre>
<p>새로운 메세지에 대한 리스너를 등록하는 메서드입니다.
lastDoc 이후의 메세지를 읽어오도록 start(afterDocument:)를 사용했습니다.</p>
<pre><code class="language-swift">final class ChatLogViewModel: ObservableObject {
    @Published var prevMessages: [WrappedMessage] = []
    @Published var newMessages: [WrappedMessage] = []

    var messages: [WrappedMessage] {
        return prevMessages + newMessages
    }

    var messageListenerExist: Bool = false

    private let carPoolManager: CarPoolManagerType

    init(carPoolManager: CarPoolManagerType) {
        self.carPoolManager = carPoolManager
    }

    func fetchMessages() {
        Task {
            let prev = await carPoolManager.fetchMessages(roomId: &quot;채팅방 식별값&quot;)

            if !messageListenerExist {
                subscribeNewMessage()
            }
            await MainActor.run {
                prevMessages.insert(contentsOf: prev, at: 0)
            }
        }
    }

    private func subscribeNewMessage() {
        messageListenerExist = true

        carPoolManager.subscribeNewMessages(roomId: &quot;채팅방 식별값&quot;) { [weak self] newMessages in
            self?.newMessages = newMessages
        }
    }
}</code></pre>
<p>ViewModel은 위와 같이 정의했습니다.
prevMessages는 페이징을 통해 가져올 메세지를 저장할 배열, newMessages는 새 메세지를 저장할 배열입니다.</p>
<p>그리고 결과적으로 View에 표시할 데이터는 computed property인 messages 입니다.</p>
<p>새 메세지에 대한 구독 요청은 최초에 1번만 해야하기 때문에 messageListenerExist 변수를 사용했습니다.</p>
<p>fetchMessages() 메서드는 View의 onAppear() 에서 호출할 것이고, 이 후 다음 페이지 요청시 다시 호출합니다.</p>
<h1 id="결과-화면">결과 화면</h1>
<img src="https://velog.velcdn.com/images/syong_e/post/70c9bdd8-3961-4337-a31b-17009a82c08f/image.gif" width = "50%"/>

<p>추가적으로 채팅방 입장 시점을 기준으로 입장 이후의 채팅 기록만을 가져오는 것을 구현해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] @State와 @Binding]]></title>
            <link>https://velog.io/@syong_e/SwiftUI-State%EC%99%80-Binding</link>
            <guid>https://velog.io/@syong_e/SwiftUI-State%EC%99%80-Binding</guid>
            <pubDate>Sat, 30 Dec 2023 09:59:42 GMT</pubDate>
            <description><![CDATA[<h1 id="state">State</h1>
<p>@State는 SwiftUI에서 제공하는 읽기/쓰기가 가능한 Property Wrapper 타입입니다.
@State 속성으로 선언된 프로퍼티의 값이 변경되면, 해당 프로퍼티 값에 의존하는 뷰 계층 구조의 일부를 업데이트합니다.</p>
<pre><code class="language-swift">struct PlayButton: View {
    @State private var isPlaying: Bool = false

    var body: some View {
        Button(action: {
            isPlaying.toggle()
        }, label: {
            Image(systemName: isPlaying ? &quot;pause.fill&quot; : &quot;play.fill&quot;)
                .resizable()
                .frame(width: 30, height: 30)
        })
    }
}</code></pre>
<p>@State 속성으로 선언된 isPlaying 의 값이 변경될 때마다 버튼의 이미지가 변경됩니다.</p>
<p>@State 속성으로 선언된 프로퍼티는 초깃값을 가져야하고, 생성자를 통해 값이 초기화되지 않도록 private로 선언하는 것을 권장합니다.</p>
<h1 id="binding">Binding</h1>
<p>하위뷰에서 상위뷰의 상태값을 변경시키고 싶을 때, 상위뷰에서 하위뷰로 상태값에 대한 바인딩을 전달하여 목적을 달성할 수 있습니다.</p>
<pre><code class="language-swift">// subView
struct PlayButton: View {
    @Binding var isPlaying: Bool

    var body: some View {
        Button(action: {
            isPlaying.toggle()
        }, label: {
            Image(systemName: isPlaying ? &quot;pause.fill&quot; : &quot;play.fill&quot;)
                .resizable()
                .frame(width: 30, height: 30)
        })
    }
}</code></pre>
<p>하위뷰에서는 상위뷰로부터 바인딩을 전달받기 위해 프로퍼티를 @Binding 속성으로 선언합니다.</p>
<pre><code class="language-swift">// parent view
struct ContentView: View {
    @State private var isPlaying: Bool = false

    var body: some View {
        PlayButton(isPlaying: $isPlaying)
    }
}</code></pre>
<p>상태값에 대한 바인딩을 얻기 위해서 프로퍼티의 이름 앞에 달러 기호($)를 붙이면 됩니다.
이 바인딩을 하위뷰 생성자를 통해 전달합니다.</p>
<h1 id="정리">정리</h1>
<ul>
<li>@State는 SwiftUI에서 제공하는 읽기/쓰기가 가능한 Property Wrapper 타입이고, 해당 프로퍼티 값이 변경되면, 프로퍼티 값에 의존하는 뷰 계층 구조의 일부가 업데이트됩니다.</li>
<li>하위뷰에서 상위뷰의 상태값을 변경하고 싶다면, 상위뷰에서 하위뷰로 상태값에 대한 바인딩을 전달하면 됩니다. 하위뷰에서 @Binding 속성으로 프로퍼티를 선언하고, 상위뷰에서 하위뷰로 상태값의 바인딩을 전달합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ] 백준  9019 DSLR (Swift 풀이)]]></title>
            <link>https://velog.io/@syong_e/BOJ-%EB%B0%B1%EC%A4%80-9019-DSLR-Swift-%ED%92%80%EC%9D%B4</link>
            <guid>https://velog.io/@syong_e/BOJ-%EB%B0%B1%EC%A4%80-9019-DSLR-Swift-%ED%92%80%EC%9D%B4</guid>
            <pubDate>Wed, 27 Dec 2023 06:56:48 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/9019">백준 9019번: DSLR</a></p>
<h1 id="문제-분석">문제 분석</h1>
<p>0 이상 10000 미만의 십진수 A와 B가 입력으로 주어졌을 때, A를 B로 변환하기 위해 필요한 <code>최소한</code>의 명령어 나열을 출력합니다.</p>
<p>명령어는 다음과 같이 4개입니다.</p>
<ul>
<li><p>D: D 는 n을 두 배로 바꾼다. 결과 값이 9999 보다 큰 경우에는 10000 으로 나눈 나머지를 취한다. 그 결과 값(2n mod 10000)을 레지스터에 저장한다.</p>
</li>
<li><p>S: S 는 n에서 1 을 뺀 결과 n-1을 레지스터에 저장한다. n이 0 이라면 9999 가 대신 레지스터에 저장된다.</p>
</li>
<li><p>L: L 은 n의 각 자릿수를 왼편으로 회전시켜 그 결과를 레지스터에 저장한다. 이 연산이 끝나면 레지스터에 저장된 네 자릿수는 왼편부터 d2, d3, d4, d1이 된다.</p>
</li>
<li><p>R: R 은 n의 각 자릿수를 오른편으로 회전시켜 그 결과를 레지스터에 저장한다. 이 연산이 끝나면 레지스터에 저장된 네 자릿수는 왼편부터 d4, d1, d2, d3이 된다.</p>
</li>
</ul>
<h1 id="접근-방법">접근 방법</h1>
<p>명령어가 4개 존재하기 때문에 A에서 특정 수로 변환될 수 있는 경우의 수는 총 4개입니다. 
<img src="https://velog.velcdn.com/images/syong_e/post/f3423586-4655-454f-af9c-1f255fc8232e/image.png" alt=""></p>
<p>A가 1234라고 가정했을 때, 4개의 명령어에 의해서 각각 2468, 1233, 2341, 4123 으로 변환될 수 있습니다.
또한, 각 수들은 4개의 명령어에 의해서 또 다른 값으로 변환될 수 있습니다.
이러한 형태로 쭉 뻗어 나가다가 B가 나왔을 때, 현재까지 사용한 명령어를 출력하면 됩니다.</p>
<p>이 때, 최소한의 명령어 나열을 구하기 위해서 BFS(너비우선탐색) 알고리즘을 사용하면 될 것입니다.</p>
<h1 id="첫번째-코드-시간-초과">첫번째 코드 (시간 초과)</h1>
<pre><code class="language-swift">struct Queue&lt;T: Comparable&gt; {
    typealias ValueType = (T, String)

    private var inbox: [ValueType] = []
    private var outbox: [ValueType] = []

    var isEmpty: Bool { return inbox.isEmpty &amp;&amp; outbox.isEmpty }
    var size: Int { return inbox.count + outbox.count }

    mutating func enqueue(_ value: ValueType) { inbox.append(value) }

    @discardableResult
    mutating func dequeue() -&gt; ValueType? {
        if outbox.isEmpty {
            outbox.append(contentsOf: inbox.reversed())
            inbox = []
        }

        return outbox.isEmpty ? nil : outbox.removeLast()
    }
}
let T = Int(readLine()!)!
for _ in 0..&lt;T {
    let ab = readLine()!.split(separator: &quot; &quot;).map { Int($0)! }
    let (a, b) = (ab[0], ab[1])

    var queue = Queue&lt;Int&gt;()
    queue.enqueue((a, &quot;&quot;))
    var visited: Set&lt;Int&gt; = []
    visited.insert(a)

    while !queue.isEmpty {
        let v = queue.dequeue()!
        if v.0 == b {
            print(v.1)
            break
        }

        for op in OperationCode.allCases {
            let newValue = operate(n: v.0, op: op)
            if !visited.contains(newValue) {
                queue.enqueue((newValue, v.1 + op.rawValue))
                visited.insert(newValue)
            }
        }
    }
}

enum OperationCode: String, CaseIterable {
    case D, S, L, R
}

func operate(n: Int, op: OperationCode) -&gt; Int {
    switch op {
    case .D: // n을 2배
        return n * 2 % 10000
    case .S: // n-1
        return n == 0 ? 9999 : n - 1
    case .L: // n을 왼편으로 회전
        return n % 1000 * 10 + n / 1000
    case .R: // n을 오른편으로 회전
        return n % 10 * 1000 + n / 10
    }
}</code></pre>
<p>처음에는 A에서 B로 변환하는데 필요한 명령어 나열을 큐의 튜플에 문자열 형태로 저장했습니다.
하지만, 시간 초과가 났고 원인을 알아내지 못해 구글링한 결과 Swift에서는 문자열을 더하는 연산은 상수 시간에 가능하지만, 일반적으로 정수 덧셈 연산 보다는 느리기 때문이라고 합니다.</p>
<p>따라서 문자열을 직접 저장하지 않고, 명령어에 따른 정수값을 누적하여 시간 초과 문제를 해결해야합니다.</p>
<p>D = 1, S = 2, L = 3, R = 4 로 가정했습니다.
예를 들어, 누적된 정수값이 1123인 경우 DDSL을 의미합니다.</p>
<p>또한, 방문 노드 체크를 위한 자료구조로 Set을 사용했는데 Set은 insert, remove, contains 연산이 모두 O(1) 으로 매우 빠르게 접근이 가능합니다. 그러나 실제로는 해시 함수를 계산하는 데 드는 비용과 해시 충돌 해결에 필요한 추가 연산이 필요하기 때문에 연산 시간이 길어질 수 있습니다. 따라서 방문 노드 체크를 위한 자료구조를 배열로 변경했습니다.</p>
<h1 id="정답-코드">정답 코드</h1>
<pre><code class="language-swift">
struct Queue&lt;T: Comparable&gt; {
    typealias ValueType = (value: T, record: Int)

    private var queue: [ValueType] = []
    private var front = 0

    var isEmpty: Bool { front &gt;= queue.count }

    mutating func enqueue(_ value: ValueType) { queue.append(value) }

    mutating func dequeue() -&gt; ValueType? {
        defer {
            front += 1
        }
        return queue[front]
    }
}
let T = Int(readLine()!)!
for _ in 0..&lt;T {
    let ab = readLine()!.split(separator: &quot; &quot;).map { Int($0)! }
    let (a, b) = (ab[0], ab[1])

    var queue = Queue&lt;Int&gt;()
    queue.enqueue((a, 0))
    var visited: [Bool] = Array(repeating: false, count: 10001)
    visited[a] = true

    while !queue.isEmpty {
        let n = queue.dequeue()!
        if n.value == b {
            printResult(n.record)
            break
        }

        for op in OperationCode.allCases {
            let next = operate(n: n.value, op: op)

            if !visited[next] {
                queue.enqueue((next, n.record * 10 + op.rawValue))
                visited[next] = true
            }
        }
    }
}

enum OperationCode: Int, CaseIterable {
    case D = 1, S, L, R

    var toString: String {
        switch self {
        case .D:
            return &quot;D&quot;
        case .S:
            return &quot;S&quot;
        case .L:
            return &quot;L&quot;
        case .R:
            return &quot;R&quot;
        }
    }
}

func printResult(_ record: Int) {
    var record = record
    var result = &quot;&quot;

    while record &gt; 0 {
        result += OperationCode(rawValue: record % 10)!.toString
        record /= 10
    }
    print(String(result.reversed()))
}

func operate(n: Int, op: OperationCode) -&gt; Int {
    switch op {
    case .D: // n을 2배
        return n * 2 % 10000
    case .S: // n-1
        return n == 0 ? 9999 : n - 1
    case .L: // n을 왼편으로 회전
        return n % 1000 * 10 + n / 1000
    case .R: // n을 오른편으로 회전
        return n % 10 * 1000 + n / 10
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SwiftUI] SwiftUI에서 UIKit 사용하기]]></title>
            <link>https://velog.io/@syong_e/SwiftUI-SwiftUI%EC%97%90%EC%84%9C-UIKit-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@syong_e/SwiftUI-SwiftUI%EC%97%90%EC%84%9C-UIKit-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 23 Dec 2023 10:40:05 GMT</pubDate>
            <description><![CDATA[<p>SwiftUI에서 지원되지 않는 기능을 구현할 때, UIKit의 기능이 필요할 수 있습니다.
이 때, UIViewRepresentable 프로토콜을 채택하여 구현하면, SwiftUI에서 UIKit의 View를 사용할 수 있습니다.</p>
<blockquote>
<p>참고:  <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable">https://developer.apple.com/documentation/swiftui/uiviewrepresentable</a></p>
</blockquote>
<h2 id="uiviewrepresentable-필수-메서드">UIViewRepresentable 필수 메서드</h2>
<ul>
<li>makeUIView(context:) - 사용할 UIView를 생성하고 초기화하는 메서드</li>
<li>updateUIView(_ uiView:, context:) - UIView의 뷰 업데이트가 필요할 때 호출되는 메서드</li>
</ul>
<h2 id="사용-방법">사용 방법</h2>
<pre><code class="language-swift">import SwiftUI
import MapKit

struct MapViewRepresentable: UIViewRepresentable {
    let mapView = MKMapView()

    /// 사용할 UIView를 생성하고, 초기화하는 메서드
    func makeUIView(context: Context) -&gt; MKMapView {
        return mapView
    }

    /// UIView의 뷰 업데이트가 필요할 때 호출되는 메서드
    func updateUIView(_ uiView: MKMapView, context: Context) {

    }
}</code></pre>
<p>MapKit을 사용해서 지도를 보여주고 싶을 때, UIKit에서는 MKMapView 인스턴스를 생성해서 구현할 수 있었습니다. SwiftUI에서도 MKMapView를 사용하기 위해서는 UIViewRepresentable 프로토콜을 채택하고 구현할 필요가 있습니다.</p>
<pre><code class="language-swift">struct MapView: View {
    var body: some View {
        MapViewRepresentable()
            .ignoresSafeArea()
    }
}</code></pre>
<p>그리고 SwiftUI에서는 UIViewRepresentable을 구현한 구조체를 위와 같이 사용하면 됩니다.</p>
<h2 id="coordinator">Coordinator</h2>
<ul>
<li>UIViewRepresentable은 Coordinator 타입이 존재합니다.</li>
<li>Coordinator는 UIView의 Delegate 역할을 합니다.</li>
<li>UIView를 초기화 하는 makeUIView(context:) 메서드에서 coordinator 클래스 인스턴스를 delegate로 넘겨줍니다.</li>
<li>coordinator는 context를 통해서 접근이 가능합니다.</li>
<li>UIKit -&gt; SwiftUI로 데이터 전달 기능을 수행합니다.</li>
</ul>
<h2 id="coordinator-사용">Coordinator 사용</h2>
<p>Coordinator를 통해 delegate 메서드를 구현하고, 사용자의 현재 위치로 지도를 이동시키는 예제입니다.</p>
<pre><code class="language-swift">extension MapViewRepresentable {
    final class MapViewCoordinator: NSObject, MKMapViewDelegate {
        private let parent: MapViewRepresentable
        private let locationManager = CLLocationManager()

        init(parent: MapViewRepresentable) {
            self.parent = parent
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.requestWhenInUseAuthorization()
            super.init()
        }

        //MARK: - MKMapViewDelegate
        func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
            let coordinate = userLocation.coordinate
            let region = MKCoordinateRegion(
                center: coordinate,
                span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
            )
            parent.mapView.setRegion(region, animated: true)
        }
    }
}</code></pre>
<p>Coordinator를 생성할 때, MapViewRepresentable을 파라미터로 받아 프로퍼티로 저장합니다. 이를 통해 mapView에 접근할 수 있습니다.</p>
<p>locationManager는 위치 권한을 요청하기 위해 사용합니다. 위치 권한이 없으면, delegate 메서드를 구현해도 사용자의 위치 정보를 가져올 수 없습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/2110a358-eaf9-4a43-93bd-b14d9a738cc3/image.png" alt=""></p>
<p>또한, info.plist 에서 위 요소를 추가해줘야합니다.</p>
<p>MKMapViewDelegate를 채택하고, mapView(didUpdate:) 메서드를 구현했습니다.
이 메서드를 통해 사용자의 현재 위치를 알아낼 수 있습니다.</p>
<pre><code class="language-swift">struct MapViewRepresentable: UIViewRepresentable {
    /// 사용할 UIView를 생성하고, 초기화하는 메서드
    func makeUIView(context: Context) -&gt; MKMapView {
        mapView.showsUserLocation = true
        mapView.delegate = context.coordinator
        return mapView
    }

    /// UIView의 뷰 업데이트가 필요할 때 호출되는 메서드
    func updateUIView(_ uiView: MKMapView, context: Context) { }

    func makeCoordinator() -&gt; MapViewCoordinator {
        return MapViewCoordinator(parent: self)
    }
}</code></pre>
<p>UIViewRepresentable에서는 makeCoordinator() 메서드를 통해 Coordinator 인스턴스를 생성해 리턴하고, makeUIView(context:) 메서드에서 delegate를 설정합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] TableView Cell 높이 동적 변경]]></title>
            <link>https://velog.io/@syong_e/iOS-TableView-Cell-%EB%86%92%EC%9D%B4-%EB%8F%99%EC%A0%81-%EB%B3%80%EA%B2%BD</link>
            <guid>https://velog.io/@syong_e/iOS-TableView-Cell-%EB%86%92%EC%9D%B4-%EB%8F%99%EC%A0%81-%EB%B3%80%EA%B2%BD</guid>
            <pubDate>Thu, 14 Dec 2023 13:45:08 GMT</pubDate>
            <description><![CDATA[<img src="https://velog.velcdn.com/images/syong_e/post/ed50e8b6-6a2b-413e-b014-550ba744ba83/image.gif" width = "40%"/>

<p>위와 같이 TableView Cell의 Button을 눌렀을 때, 확장하거나 축소할 수 있도록 해야할 필요가 있을 수 있습니다. </p>
<p>구현 방법이 복잡하지 않고, <code>intrinsicContentSize</code> 개념에 대해 이해하고 있다면, 충분합니다.</p>
<blockquote>
<p>참고: <a href="https://velog.io/@syong_e/AutoLayout-IntrinsicContentSize-HuggingCompression-Priority">[AutoLayout] IntrinsicContentSize, Hugging/Compression Priority</a></p>
</blockquote>
<h2 id="tableview-세팅">TableView 세팅</h2>
<pre><code class="language-swift">/// ViewController.swift

    private lazy var tableView: UITableView = {
        let tv = UITableView()
        tv.rowHeight = UITableView.automaticDimension
        tv.separatorStyle = .none
        tv.dataSource = self
        tv.delegate = self
        tv.register(MyCell.self, forCellReuseIdentifier: MyCell.id)
        return tv
    }()

    extension ViewController: UITableViewDataSouce { 
        /// 필수 메서드 구현... 
    }</code></pre>
<p>TableView Cell의 높이가 고정이 아니고, 동적으로 변경되어야하기 때문에 rowHeight 속성 값을 <code>UITableView.automaticDimension</code> 으로 설정합니다.</p>
<blockquote>
<p>rowHeight 속성을 UITableView.automaticDimension으로 설정하면, TableView가 만들어지고, 레이아웃을 계산한 후, 셀의 높이를 자동으로 재설정합니다.</p>
</blockquote>
<h2 id="cell">Cell</h2>
<pre><code class="language-swift">/// MyCell.swift

    private var isExtended: Bool = false

    @objc private func handleExtensionButtonTapped() {
        isExtended.toggle()
        extensionButton.setImage(isExtended ? UIImage(systemName: &quot;chevron.up&quot;) : UIImage(systemName: &quot;chevron.down&quot;), for: .normal)
        descriptionLabel.numberOfLines = isExtended ? 0 : 3

        invalidateIntrinsicContentSize()
    }</code></pre>
<p>셀 버튼의 이벤트 핸들러입니다.</p>
<p>Label의 numberOfLines 속성을 isExtended 값에 따라 0 또는 3으로 설정합니다.
그리고, invalidateIntrinsicContentSize() 메서드를 호출하면, Cell의 intrinsicContentSize를 다시 계산하여 Cell의 높이를 재설정 할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 양궁대회(DFS) - 파이썬]]></title>
            <link>https://velog.io/@syong_e/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%96%91%EA%B6%81%EB%8C%80%ED%9A%8CDFS-%ED%8C%8C%EC%9D%B4%EC%8D%AC</link>
            <guid>https://velog.io/@syong_e/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%96%91%EA%B6%81%EB%8C%80%ED%9A%8CDFS-%ED%8C%8C%EC%9D%B4%EC%8D%AC</guid>
            <pubDate>Mon, 06 Nov 2023 10:26:33 GMT</pubDate>
            <description><![CDATA[<p><a href="https://campus.programmers.co.kr/tryouts/99094/challenges">https://campus.programmers.co.kr/tryouts/99094/challenges</a></p>
<h1 id="문제-설명">문제 설명</h1>
<p>카카오배 양궁대회가 열렸습니다.
라이언은 저번 카카오배 양궁대회 우승자이고 이번 대회에도 결승전까지 올라왔습니다. 결승전 상대는 어피치입니다.
카카오배 양궁대회 운영위원회는 한 선수의 연속 우승보다는 다양한 선수들이 양궁대회에서 우승하기를 원합니다. 따라서, 양궁대회 운영위원회는 결승전 규칙을 전 대회 우승자인 라이언에게 불리하게 다음과 같이 정했습니다.</p>
<ol>
<li>어피치가 화살 n발을 다 쏜 후에 라이언이 화살 n발을 쏩니다.</li>
<li>점수를 계산합니다.
 2-1. 과녁판은 아래 사진처럼 생겼으며 가장 작은 원의 과녁 점수는 10점이고 가장 큰 원의 바깥쪽은 과녁 점수가 0점입니다.<img src="https://velog.velcdn.com/images/syong_e/post/4af4a63d-64f8-4ecb-be83-b6133efbcbaa/image.png" width = 50%/>
 2-2. 만약, k(k는 1~10사이의 자연수)점을 어피치가 a발을 맞혔고 라이언이 b발을 맞혔을 경우 더 많은 화살을 k점에 맞힌 선수가 k 점을 가져갑니다. 단, a = b일 경우는 어피치가 k점을 가져갑니다. k점을 여러 발 맞혀도 k점 보다 많은 점수를 가져가는 게 아니고 k점만 가져가는 것을 유의하세요. 또한 a = b = 0 인 경우, 즉, 라이언과 어피치 모두 k점에 단 하나의 화살도 맞히지 못한 경우는 어느 누구도 k점을 가져가지 않습니다.</li>
</ol>
<ul>
<li>예를 들어, 어피치가 10점을 2발 맞혔고 라이언도 10점을 2발 맞혔을 경우 어피치가 10점을 가져갑니다.</li>
<li>다른 예로, 어피치가 10점을 0발 맞혔고 라이언이 10점을 2발 맞혔을 경우 라이언이 10점을 가져갑니다.
  2-3. 모든 과녁 점수에 대하여 각 선수의 최종 점수를 계산합니다.</li>
</ul>
<p>3.최종 점수가 더 높은 선수를 우승자로 결정합니다. 단, 최종 점수가 같을 경우 어피치를 우승자로 결정합니다.</p>
<p>현재 상황은 어피치가 화살 n발을 다 쏜 후이고 라이언이 화살을 쏠 차례입니다.
라이언은 어피치를 가장 큰 점수 차이로 이기기 위해서 n발의 화살을 어떤 과녁 점수에 맞혀야 하는지를 구하려고 합니다.</p>
<p>화살의 개수를 담은 자연수 n, 어피치가 맞힌 과녁 점수의 개수를 10점부터 0점까지 순서대로 담은 정수 배열 info가 매개변수로 주어집니다. 이때, 라이언이 가장 큰 점수 차이로 우승하기 위해 n발의 화살을 어떤 과녁 점수에 맞혀야 하는지를 10점부터 0점까지 순서대로 정수 배열에 담아 return 하도록 solution 함수를 완성해 주세요. 만약, 라이언이 우승할 수 없는 경우(무조건 지거나 비기는 경우)는 [-1]을 return 해주세요.</p>
<h1 id="제한-사항">제한 사항</h1>
<p>1 ≤ n ≤ 10
info의 길이 = 11
0 ≤ info의 원소 ≤ n
info의 원소 총합 = n
info의 i번째 원소는 과녁의 10 - i 점을 맞힌 화살 개수입니다. ( i는 0<del>10 사이의 정수입니다.)
라이언이 우승할 방법이 있는 경우, return 할 정수 배열의 길이는 11입니다.
0 ≤ return할 정수 배열의 원소 ≤ n
return할 정수 배열의 원소 총합 = n (꼭 n발을 다 쏴야 합니다.)
return할 정수 배열의 i번째 원소는 과녁의 10 - i 점을 맞힌 화살 개수입니다. ( i는 0</del>10 사이의 정수입니다.)
라이언이 가장 큰 점수 차이로 우승할 수 있는 방법이 여러 가지 일 경우, 가장 낮은 점수를 더 많이 맞힌 경우를 return 해주세요.
<strong>가장 낮은 점수를 맞힌 개수가 같을 경우 계속해서 그다음으로 낮은 점수를 더 많이 맞힌 경우를 return 해주세요.</strong>
예를 들어, [2,3,1,0,0,0,0,1,3,0,0]과 [2,1,0,2,0,0,0,2,3,0,0]를 비교하면 [2,1,0,2,0,0,0,2,3,0,0]를 return 해야 합니다.
다른 예로, [0,0,2,3,4,1,0,0,0,0,0]과 [9,0,0,0,0,0,0,0,1,0,0]를 비교하면[9,0,0,0,0,0,0,0,1,0,0]를 return 해야 합니다.
라이언이 우승할 방법이 없는 경우, return 할 정수 배열의 길이는 1입니다.
라이언이 어떻게 화살을 쏘든 라이언의 점수가 어피치의 점수보다 낮거나 같으면 [-1]을 return 해야 합니다.</p>
<h1 id="문제-풀이-접근-방식">문제 풀이 접근 방식</h1>
<p>문제를 처음 봤을 때, 일단 라이언이 n개의 화살을 모두 쏘는 모든 경우의 수를 구해서 그중 라이언이 우승하는 경우만 고려하면 될 것이라고 생각했다.
모든 경우의 수를 구하기 위해서 DFS 알고리즘을 사용하면 될 것이고, 상태 트리를 뻗어나가기 위해서 라이언은 화살을 쏠 때, 어피치가 k점을 맞춘 횟수보다 1만큼 더 쏘거나 아니면 아예 쏘지 않거나 이 두가지로 뻗어나가면 될 것이다.</p>
<h1 id="dfs">DFS</h1>
<pre><code class="language-python">    def dfs(L, cnt):
        global max_gap, answer
        if L == 11 or cnt == 0:    
            is_winner, gap = is_winner_with_gap(score)
            if is_winner:
                if cnt &gt;= 0: # 화살이 남은 경우
                    score[10] = cnt # 0점에 쏴도 이김

                if gap &gt; max_gap: # 갭이 더 큰 경우로 업데이트
                    max_gap = gap
                    answer = score.copy()

                elif gap == max_gap: # 가장 낮은 점수를 많이 맞힌 경우로 업데이트
                    for i in range(len(score)):
                        if answer[i] &gt; 0:
                            max_i_1 = i
                        if score[i] &gt; 0:
                            max_i_2 = i
                    if max_i_2 &gt; max_i_1:
                        answer = score.copy()

            return

        # k점을 어피치보다 많이 맞추거나 아예 안맞추거나
       if cnt&gt;info[L]:
           score[L]=info[L]+1
           dfs(L+1, cnt-(info[L]+1))
           score[L]=0

       dfs(L+1, cnt)</code></pre>
<p>dfs 함수 파라미터로 받는 L은 info리스트의 인덱스이면서 상태 트리의 Depth입니다.
cnt는 쏜 화살의 개수입니다.</p>
<p>재귀 탈출 조건으로 상태 트리의 최대 깊이까지 갔거나, 화살을 모두 쏜 경우에 탈출할 수 있도록 합니다. </p>
<p>is_winner_with_gap(score)는 라이언과 어피치의 점수를 비교해서 (라이언의 승/패(True/False), 라이언과 어피치의 점수 차)의 튜플 형태로 리턴하는 함수입니다.</p>
<p>라이언이 이긴 경우에 바로 answer에 업데이트하면 안되고, 다음 조건들을 고려해야합니다.</p>
<blockquote>
<ol>
<li>쏠 수 있는 화살이 남은 경우</li>
<li>현재 경우의 점수 차(gap)가 이전의 gap보다 더 큰가? -&gt; answer 업데이트</li>
<li>현재 경우의 점수 차와 이전의 점수 차가 같은 경우 -&gt; 가장 낮은 점수를 많이 맞힌 경우로 업데이트</li>
</ol>
</blockquote>
<p>위의 조건들을 모두 고려해서 answer를 업데이트한 후 재귀를 탈출합니다.</p>
<p>상태 트리를 뻗어나가기 위해서 k점을 어피치보다 많이 맞추거나 아예 맞추지 않아야합니다.</p>
<h1 id="어피치와-라이언의-점수-비교">어피치와 라이언의 점수 비교</h1>
<pre><code class="language-python">    def is_winner_with_gap(score):
        a=0 # 어피치 점수
        b=0 # 라이언 점수

        for i in range(len(info)):
            if info[i] &gt; 0 or score[i] &gt; 0:
                if info[i]&gt;=score[i]:
                    a += (10-i)
                else:
                    b += (10-i)
        return (b &gt; a, abs(a-b))</code></pre>
<p>어피치와 라이언의 점수를 비교하여 튜플 형태로 라이언의 승/패 여부와 두 점수 차를 리턴합니다.</p>
<h1 id="전체-코드">전체 코드</h1>
<pre><code class="language-python">def solution(n, info):
    global max_gap, answer

    answer = [-1]
    score = [0]*11
    max_gap=0

    def is_winner_with_gap(score):
        a=0 # 어피치 점수
        b=0 # 라이언 점수

        for i in range(len(info)):
            if info[i] &gt; 0 or score[i] &gt; 0:
                if info[i]&gt;=score[i]:
                    a += (10-i)
                else:
                    b += (10-i)
        return (b &gt; a, abs(a-b))

    def dfs(L, cnt):
        global max_gap, answer
        if L == 11 or cnt == 0:    
            is_winner, gap = is_winner_with_gap(score)
            if is_winner:
                if cnt &gt;= 0: # 화살이 남은 경우
                    score[10] = cnt # 0점에 쏴도 이김

                if gap &gt; max_gap: # 갭이 더 큰 경우로 업데이트
                    max_gap = gap
                    answer = score.copy()

                elif gap == max_gap: # 가장 낮은 점수를 많이 맞힌 경우로 업데이트
                    for i in range(len(score)):
                        if answer[i] &gt; 0:
                            max_i_1 = i
                        if score[i] &gt; 0:
                            max_i_2 = i
                    if max_i_2 &gt; max_i_1:
                        answer = score.copy()

            return

        # k점을 어피치보다 많이 맞추거나 아예 안맞추거나
        if cnt&gt;info[L]:
            score[L]=info[L]+1
            dfs(L+1, cnt-(info[L]+1))
            score[L]=0

        dfs(L+1, cnt)

    dfs(0,n)

    return answer</code></pre>
<p>문제를 제대로 이해하는 것이 중요한 문제인 것 같습니다.
저는 라이언이 가장 큰 점수 차이로 우승할 수 있는 방법이 여러가지 일 경우, 가장 낮은 점수를 더 많이 맞힌 경우를 return하라는 것을 잘못 이해하여 문제를 해결하는데 어려움을 겪었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[iOS] 커스텀 슬라이더 만들기]]></title>
            <link>https://velog.io/@syong_e/iOS-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%8D%94-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@syong_e/iOS-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%8A%AC%EB%9D%BC%EC%9D%B4%EB%8D%94-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Fri, 20 Oct 2023 06:56:15 GMT</pubDate>
            <description><![CDATA[<p>iOS 개발 오픈 채팅방에서 1단위 씩 뚝뚝 끊기는 슬라이더를 어떻게 커스텀해서 구현해야하는지 모르겠다고 하셔서 직접 만들어봤습니다.</p>
<p>사실 단순히 1단위씩 뚝뚝 끊겨서 값이 설정되는 슬라이더는 기본적으로 UIKit에서 제공하는 UISlider로 구현할 수 있겠지만, 그것 뿐만 아니라 1단위마다 구분선(?) 이 필요했기 때문에 아예 커스텀 뷰를 만드는 것이 좋을 것 같다고 판단했습니다.</p>
<h1 id="결과물">결과물</h1>
<img src="https://velog.velcdn.com/images/syong_e/post/dece73ef-3a08-454f-8a25-96097ac8597f/image.gif" width = "30%"/>

<h1 id="기능적-요구-사항">기능적 요구 사항</h1>
<ol>
<li>좌/우 슬라이드 기능</li>
<li>값 변화시 한 단위씩 뚝뚝 끊기는 효과</li>
<li>값 변화시 외부에서 관찰 가능</li>
</ol>
<h1 id="ui-요구-사항">UI 요구 사항</h1>
<p>위 커스텀 슬라이더를 만들기 위해서 아래와 같이 4개의 View들이 필요합니다</p>
<ol>
<li>trackView</li>
<li>divider</li>
<li>fillTrackView</li>
<li>thumbView</li>
</ol>
<h1 id="코드-구현">코드 구현</h1>
<pre><code class="language-swift">final class SliderView: UIView { ... }</code></pre>
<p>먼저 커스텀뷰를 만들기 위해서 UIView를 상속하는 SliderView 클래스를 생성합니다.</p>
<pre><code class="language-swift">//MARK: - SliderView.swift

    private let trackView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemGray5
        return view
    }()

    private lazy var thumbView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBackground
        view.isUserInteractionEnabled = true
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
        view.addGestureRecognizer(gesture)
        view.layer.shadowColor = UIColor.gray.cgColor
        view.layer.shadowOffset = .init(width: 3, height: 3)
        view.layer.shadowRadius = 8
        view.layer.shadowOpacity = 0.8
        return view
    }()

    private let fillTrackView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBlue
        return view
    }()

    private var dividers: [UIView] = []

    private var maxValue: Int
    private var touchBeganPosX: CGFloat?
    private var didLayoutSubViews: Bool = false

    private let thumbSize: CGFloat = 30
    private let dividerWidth: CGFloat = 8</code></pre>
<p>필요한 프로퍼티를 정의합니다.
UIPanGestureRecognizer는 슬라이드 기능을 구현하기 위한 GestureRecognizer입니다.
handlePan 메서드는 아래에서 구현해보겠습니다.</p>
<pre><code class="language-swift">//MARK: - LifeCycle
    init(maxValue: Int) {
        if maxValue &lt; 1 {
            self.maxValue = 1
        }
        else if maxValue &gt; 20 {
            self.maxValue = 20
        }
        else{
            self.maxValue = maxValue
        }
        super.init(frame: .zero)

        layout()
    }</code></pre>
<p>생성자는 인자로 정수형 타입의 maxValue를 전달받고, layout() 메서드를 호출합니다.
maxValue는 슬라이더의 최댓값이고, layout() 메서드는 AutoLayout을 설정하는 메서드입니다.</p>
<p>AutoLayout 부분은 생략하겠습니다.</p>
<pre><code class="language-swift">    private func makeDividerAndLayout() {
        let unitWidth = trackView.frame.width / CGFloat(maxValue - 1)

        for i in 0..&lt;maxValue {
            let dividerPosX = unitWidth * CGFloat(i)
            let divider = makeDivider()

            trackView.addSubview(divider)
            divider.snp.makeConstraints { make in
                make.centerY.equalTo(trackView)
                make.left.equalTo(trackView).offset(dividerPosX - 4)
                make.width.equalTo(dividerWidth)
                make.height.equalTo(trackView).offset(7)
            }
        }

        didLayoutSubViews.toggle()
    }

    private func makeDivider() -&gt; UIView {
        let divider = UIView()
        divider.backgroundColor = .systemGray5
        divider.clipsToBounds = true
        divider.layer.cornerRadius = 3
        dividers.append(divider)
        return divider
    }</code></pre>
<p>makeDividerAndLayout() 메서드는 트랙의 구분선을 만들고, 배치하는 메서드입니다.
unitWidth는 한 단위의 너비입니다. 이 값으로 트랙 위에서의 구분선의 위치를 지정할 수 있습니다.
생성자로 전달받은 maxValue개의 구분선을 만들어서 트랙 위에 배치해줍니다.</p>
<p>makeDividerAndLayout() 메서드에서는 trackView의 frame을 사용하기 때문에 trackView의 크기가 정해진 후에 호출되어야 합니다. </p>
<pre><code class="language-swift">    override func layoutSubviews() {
        super.layoutSubviews()

        if !didLayoutSubViews {
            makeDividerAndLayout()
            thumbView.layer.cornerRadius = thumbView.frame.width / 2
            thumbView.layer.shadowPath = UIBezierPath(
                roundedRect: thumbView.bounds,
                cornerRadius: thumbView.layer.cornerRadius
            ).cgPath
        }
    }</code></pre>
<p>그래서 위와 같이 layoutSubviews() 를 오버라이딩하고, 그 안에서 makeDividerAndLayout() 를 호출합니다.</p>
<blockquote>
<p>layoutSubviews() 메서드는 뷰의 크기나 위치가 변경될 때마다 호출되는 메서드입니다. trackView의 크기가 정해지면, 이 메서드가 호출될 것이기 때문에 이 메서드 안에서는 뷰의 최신 크기와 위치 정보를 사용할 수 있습니다.</p>
</blockquote>
<p>뷰의 크기나 위치가 변경될 때마다 호출되기 때문에 makeDividerAndLayout() 메서드를 중복 호출하는 문제가 발생할 수 있습니다. 따라서 조건문을 통해 최초에 딱 1번만 호출하도록 코드를 작성했습니다.</p>
<pre><code class="language-swift">    //MARK: - Actions

    @objc func handlePan(_ recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translation(in: thumbView)

        if recognizer.state == .began {
            // 팬 제스쳐가 시작된 x좌표 저장
            touchBeganPosX = thumbView.frame.minX
        }
        if recognizer.state == .changed {
            guard let startX = self.touchBeganPosX else { return }

            var offSet = startX + translation.x // 시작지점 + 제스쳐 거리 = 현재 제스쳐 좌표
            if offSet &lt; 0 || offSet &gt; trackView.frame.width { return } // 제스쳐가 trackView의 범위를 벗어나는 경우 무시
            let unitWidth = trackView.frame.width / CGFloat(maxValue - 1) // 1단위 너비

            // value = 반올림(현재 제스쳐 좌표 / 1단위의 크기) -&gt; 슬라이더의 값이 변할 때마다 똑똑 끊기는 효과를 주기 위해
            let newValue = round(offSet / unitWidth)
            offSet = unitWidth * newValue - (thumbSize / 2)

            thumbView.snp.updateConstraints { make in
                make.left.equalTo(trackView).offset(offSet)
            }
            fillTrackView.snp.updateConstraints { make in
                make.width.equalTo(offSet)
            }

            if value != Int(newValue + 1) {
                value = Int(newValue + 1)
                for i in 0..&lt;value {
                    dividers[i].backgroundColor = .systemBlue
                }
                for i in value..&lt;maxValue {
                    dividers[i].backgroundColor = .systemGray5
                }
            }
        }
    }</code></pre>
<p>슬라이드 기능을 구현하는 메서드입니다.
제스쳐에 따라 thumbView와 fillTrackView를 재배치합니다.
자세한 설명은 주석으로 달아놨습니다.</p>
<pre><code class="language-swift">protocol SliderViewDelegate: AnyObject {
    func sliderView(_ sender: SliderView, changedValue value: Int)
}

weak var delegate: SliderViewDelegate?
var value: Int = 1 {
    didSet {
        delegate?.sliderView(self, changedValue: value)
    }
}
</code></pre>
<p>마지막으로 값의 변화를 외부에서 관찰할 수 있도록 만들기 위해서 Delegate 패턴을 사용했습니다.</p>
<h1 id="전체-코드">전체 코드</h1>
<p><a href="https://github.com/EJLee1209/UIKit_Storage/blob/Develop/iOS_Study/View/CustomView/CustomView/SliderView.swift">https://github.com/EJLee1209/UIKit_Storage/blob/Develop/iOS_Study/View/CustomView/CustomView/SliderView.swift</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] UIKit으로 Sticky Header 만들기]]></title>
            <link>https://velog.io/@syong_e/TIL-UIKit%EC%9C%BC%EB%A1%9C-Sticky-Header-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@syong_e/TIL-UIKit%EC%9C%BC%EB%A1%9C-Sticky-Header-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 03 Oct 2023 09:38:33 GMT</pubDate>
            <description><![CDATA[<h1 id="예제-결과물">예제 결과물</h1>
<img src="https://velog.velcdn.com/images/syong_e/post/ddcfd565-6adf-40a0-800f-01746b0855c4/image.gif" width="40%"/>

<p>배민이나 당근마켓 앱을 사용해보면, 위와 같이 스크롤에 반응하는 이미지 UI를 볼 수 있습니다.
검색해보니 이러한 UI를 Sticky Header라고 하는 것 같습니다. (확실하진 않아요!!)</p>
<p>스크롤시에 그저 상단으로 없어지는 단순한 UI를 만들수도 있지만, 위와 같이 좀 더 유려한 반응을 추가해서 완성도 높은 앱으로 만들면 좋을 것 같습니다.</p>
<h1 id="sticky-header-예제">Sticky Header 예제</h1>
<p>Sticky Header 구현을 목적으로 하기 때문에 최대한 간소화해서 진행하도록 할게요</p>
<p>예제를 진행하기 위해 필요한 컴포넌트는 CollectionView와 ImageView 입니다.</p>
<h2 id="properties">Properties</h2>
<pre><code class="language-swift">//MARK: - StickyHeaderViewController

    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.showsVerticalScrollIndicator = false
        cv.clipsToBounds = true
        cv.backgroundColor = .clear
        cv.contentInsetAdjustmentBehavior = .never
        cv.contentInset = .init(top: headerHeight, left: 0, bottom: 0, right: 0)
        cv.register(ImageCell.self, forCellWithReuseIdentifier: ImageCell.identifier)
        cv.register(MyHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: MyHeaderView.identifier)
        cv.dataSource = self
        cv.delegate = self
        return cv
    }()

    private let headerImageView: UIImageView = {
      let view = UIImageView()
      view.image = UIImage(named: &quot;cat&quot;)
      view.clipsToBounds = true
      view.contentMode = .scaleAspectFill
      return view
    }()

    private let headerHeight = 250.0
</code></pre>
<p>예제를 진행하는데 필요한 프로퍼티를 선언해줍니다.
top contentInset을 이미지의 높이 만큼 설정해서 이미지를 마치 CollectoinView의 셀처럼 보이게 할 수 있습니다.</p>
<h2 id="autolayout">AutoLayout</h2>
<pre><code class="language-swift">    //MARK: - Helpers
    private func layout() {
        view.backgroundColor = .white
        [collectionView, headerImageView].forEach(view.addSubview)

        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }

        headerImageView.snp.makeConstraints { make in
            make.top.left.right.equalToSuperview()
            make.height.equalTo(headerHeight)
        }

    }</code></pre>
<p>AutoLayout 설정을 해줍니다.</p>
<h2 id="flowlayout">FlowLayout</h2>
<pre><code class="language-swift">//MARK: - UICollectionViewDelegateFlowLayout
extension StickyHeaderViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(
        _ collectionView: UICollectionView,
        layout collectionViewLayout: UICollectionViewLayout,
        sizeForItemAt indexPath: IndexPath
    ) -&gt; CGSize {
        return .init(width: UIScreen.main.bounds.width, height: 150)
    }

    func collectionView(
        _ collectionView: UICollectionView,
        layout collectionViewLayout: UICollectionViewLayout,
        minimumLineSpacingForSectionAt section: Int
    ) -&gt; CGFloat {
        return 4
    }
}</code></pre>
<p>FlowLayout 설정입니다.</p>
<h2 id="uicollectionviewdelegate">UICollectionViewDelegate</h2>
<pre><code class="language-swift">extension StickyHeaderViewController: UICollectionViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let distanceFromOrigin = abs(scrollView.contentOffset.y) // 스크롤뷰의 원점에서 현재 스크롤 offset.y 의 거리
        let scrollUp = scrollView.contentOffset.y &lt;= -headerHeight // 이미 스크롤이 원점인 상태에서 위로 스크롤 중인가?
        let stopExpandHeader = scrollView.contentOffset.y &lt;= -(headerHeight*2) // 이미지 확장을 멈춰야 하는가?

        print(scrollView.contentOffset.y)
        if !stopExpandHeader, scrollUp {
            // 이미지 확장 가능하고, 이미 스크롤이 원점인 상태에서 위로 스크롤 중
            headerImageView.snp.updateConstraints { make in
                make.height.equalTo(distanceFromOrigin)
            }
            headerImageView.alpha = 1
        }
        else if !scrollUp {
            // 아래로 스크롤 중
            let height = scrollView.contentOffset.y &lt;= 0 ? distanceFromOrigin : 0
            headerImageView.snp.updateConstraints { make in
                make.height.equalTo(height)
            }

            headerImageView.alpha = distanceFromOrigin / headerHeight
        }
    }
}</code></pre>
<p>scrollViewDidScroll 메서드를 구현하기 위해서 delegate를 채택합니다.</p>
<p>스크롤시 파라미터로 전달되는 scrollView의 offset을 활용해서 이미지의 높이와 투명도를 변경합니다.
따로 설명이 필요하진 않을 것 같습니다. 저도 직접 offset 값을 콘솔에 출력하면서 하나씩 구현했습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SwiftUI 튜토리얼(1)]]></title>
            <link>https://velog.io/@syong_e/SwiftUI-%EA%B8%B0%EC%B4%88-%EB%BD%80%EA%B0%9C%EA%B8%B01</link>
            <guid>https://velog.io/@syong_e/SwiftUI-%EA%B8%B0%EC%B4%88-%EB%BD%80%EA%B0%9C%EA%B8%B01</guid>
            <pubDate>Sat, 30 Sep 2023 08:21:42 GMT</pubDate>
            <description><![CDATA[<p>오늘은 SwiftUI 프로젝트를 생성했을 때 생성되는 기본 템플릿 코드에 대해서 분석해보도록 하겠습니다.</p>
<pre><code class="language-swift">import SwiftUI

@main
struct ExSwiftUIApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
</code></pre>
<pre><code class="language-swift">import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: &quot;globe&quot;)
            // modifier를 사용해서 View의 속성값 조절
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text(&quot;Hello, world!&quot;)
        }
        .padding()

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
</code></pre>
<p>SwiftUI 프로젝트를 생성하면 기본적으로 위와 같은 struct로 구현되어 있는 기본 템플릿 코드가 생성됩니다.</p>
<p>위에서부터 차근차근 살펴보겠습니다</p>
<h2 id="exswiftapp">ExSwiftApp</h2>
<pre><code class="language-swift">@main</code></pre>
<p>SwiftUI 앱의 Entry Point(진입점)을 나타내기 위해 App에 @main 속성을 적용합니다.
Entry Point는 1개만 존재해야합니다. 2개 이상 있다면 컴파일 에러가 발생합니다.</p>
<pre><code class="language-swift">@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -&gt; Bool {
        // Override point for customization after application launch.
        return true
    }
}</code></pre>
<p>UIKit은 위와 같이 앱의 Entry Point가 AppDelegate에 @main 속성으로 정의되어 있었죠?</p>
<p>SwiftUI에서는 AppDelegate, SceneDelegate가 따로 없으며, App 프로토콜을 채택하는 구조체에 @main 속성을 적용하여 앱의 Entry Point를 지정합니다.</p>
<pre><code class="language-swift">struct ExSwiftUIApp: App { ... }</code></pre>
<p>앱을 생성하기 위해 App 프로토콜을 준수하는 구조체를 선언합니다.
App은 앱의 구조와 동작을 나타내는 유형입니다.</p>
<pre><code class="language-swift">var body: some Scene { ... }</code></pre>
<p>앱의 콘텐츠를 정의하는데 필요한 Computed property인 body를 구현합니다.
body는 Scene 프로토콜을 준수합니다. 이때, some 키워드를 사용해 body의 구체적인 타입을 숨기고, 단순하게 Scene 타입을 준수하는 어떠한 타입으로 만듦으로써 뷰의 복잡성을 감추고, 생상성을 높일 수 있습니다.
각 Scene은 뷰 계층 구조의 루트 뷰를 포함하며 시스템에서 관리하는 수명주기를 갖습니다.
SwiftUI에서 Scene은 WindowGroup, Window, DocumentGroup, Settings 등 다양한 유형의 Scene을 제공합니다.</p>
<pre><code class="language-swift">WindowGroup {
            ContentView()
    }</code></pre>
<p>멀티 윈도우를 지원하는 앱의 주요 인터페이스를 선언하기 위해 WindowGroup을 사용하고, 앱의 콘텐츠를 포함하고 있는 뷰 계층 구조를 만드는 사용자 지정 뷰인 ContentView를 포함합니다.</p>
<h2 id="contentview">ContentView</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/8c6235e5-9940-49d7-9343-8f941c4dfa8c/image.png" alt=""></p>
<p>SwiftUI에서 Scene에는 UI로 표시하는 뷰 계층 구조가 포함됩니다.
뷰 계층 구조는 다른 뷰를 기준으로 뷰의 레이아웃을 정의합니다.</p>
<pre><code class="language-swift">struct ContentView: View { ... }</code></pre>
<p>ContentView는 View 프로토콜을 준수하는 구조체입니다.
View는 화면의 시각적인 요소를 정의하고 일반적으로 다른 뷰들로 구성되어 뷰 계층 구조를 만듭니다.</p>
<pre><code class="language-swift">var body: some View { ... }</code></pre>
<p>콘텐츠를 정의하기 위해 View 프로토콜을 따르는 computed property인 body를 구현합니다.
마찬가지로 some 키워드를 사용해서 body의 구체적인 타입을 감춰 복잡성을 낮춥니다.</p>
<p>코드의 패턴을 보면, ExSwiftUIApp에 있는 App, Scene 프로토콜을 준수하는 코드의 패턴과 동일합니다. 이러한 패턴은 SwiftUI 코드에서 볼 수 있는 일반적인 패턴입니다.</p>
<pre><code class="language-swift">VStack {
            Image(systemName: &quot;globe&quot;)
                // view modifier를 사용해 view의 속성값 조절 
                .imageScale(.large) 
                .foregroundColor(.accentColor)
            Text(&quot;Hello, world!&quot;)
        }
        .padding()</code></pre>
<p>VStack을 사용해서 블럭 내부의 모든 하위 뷰를 세로로 정렬할 수 있습니다.
하위 뷰가 많지 않고 세로로 정렬하고 싶을 때 VStack을 사용하는 것이 이상적이지만,
많은 하위 뷰를 포함하는 경우 LazyVStack을 사용하는 것이 좋습니다.</p>
<pre><code class="language-swift">        ScrollView {
            LazyVStack {
                ForEach(1...300, id: \.self) {
                    Text(&quot;Row \($0)&quot;)
                }
            }
        }</code></pre>
<p>LazyVStack은 필요할 때까지 뷰의 인스턴스를 생성하지 않는 Lazy(게으른)VStack입니다.
많은 양의 데이터를 처리해야 할 때 모든 데이터 항목에 대한 뷰를 한번에 로드하면, 앱의 성능 문제를 야기할 수 있습니다.
LazyVStack을 사용하면, 현재 화면에 보이는 항목들만 로드되므로 메모리 사용량을 크게 줄일 수 있습니다.</p>
<p>출처 : <a href="https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app">https://developer.apple.com/tutorials/swiftui-concepts/exploring-the-structure-of-a-swiftui-app</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL]CompositionalLayout + DiffableDataSource]]></title>
            <link>https://velog.io/@syong_e/CompositionalLayout-DiffableDataSource</link>
            <guid>https://velog.io/@syong_e/CompositionalLayout-DiffableDataSource</guid>
            <pubDate>Thu, 14 Sep 2023 08:33:23 GMT</pubDate>
            <description><![CDATA[<p>CompositionalLayout을 사용해서 CollectionView의 섹션마다 다른 레이아웃을 구성하고, DiffableDataSource를 통해 필요한 데이터 소스를 제공해서 CollectionView를 구현해보겠습니다.</p>
<p>코드에 대한 설명은 하지 않겠습니다. </p>
<p>아래 링크를 참고해주세요
<a href="https://velog.io/@syong_e/TILCompositionalLayout%EC%9C%BC%EB%A1%9C-%EC%84%B9%EC%85%98%EB%A7%88%EB%8B%A4-%EB%8B%A4%EB%A5%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0">CompositionalLayout</a>
<a href="https://velog.io/@syong_e/TIL-DiffableDataSource">DiffableDataSource</a></p>
<h1 id="결과물">결과물</h1>
<p><img src="https://velog.velcdn.com/images/syong_e/post/b6a9c946-a18b-4481-9980-96462ec996ee/image.gif" alt=""></p>
<p>3개의 섹션으로 나누고, 하단의 버튼을 클릭할 때마다 데이터를 업데이트하는 예제입니다.
DiffableDataSource를 통해 데이터 소스를 제공하기 때문에 현재 상태 Snapshot과 변경된 Snapshot을 비교해서 필요한 부분만 UI 업데이트(삽입/삭제/재정렬 등)를 수행합니다.
따라서 위와 같이 부드러운 애니메이션을 적용할 수 있습니다.</p>
<h1 id="section-item-타입-정의">Section, Item 타입 정의</h1>
<pre><code class="language-swift">
enum DiffableSection: Hashable, CaseIterable {
    case circle
    case slide
    case main
    // case etc... 섹션 추가
}

enum DiffableSectionItem: Hashable {
    case circleItem(CircleItemModel)
    case slideItem(SlideItemModel)
    case mainItem(MainItemModel)
    // case ectItem... 섹션 아이템 추가

    struct CircleItemModel: Hashable {
        let title: String
    }

    struct SlideItemModel: Hashable {
        let title: String
    }

    struct MainItemModel: Hashable {
        let title: String
    }
}
typealias MyDataSource = UICollectionViewDiffableDataSource&lt;DiffableSection, DiffableSectionItem&gt;
typealias MySnapshot = NSDiffableDataSourceSnapshot&lt;DiffableSection, DiffableSectionItem&gt;
</code></pre>
<h1 id="compositionallayout">CompositionalLayout</h1>
<pre><code class="language-swift">private lazy var collectionView: UICollectionView = {
        let cv = UICollectionView(frame: .zero, collectionViewLayout: self.makeFlowLayout())
        cv.register(RectangleCell.self, forCellWithReuseIdentifier: RectangleCell.identifier)
        cv.register(CircleCell.self, forCellWithReuseIdentifier: CircleCell.identifier)
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: circleSectionHeaderKind, withReuseIdentifier: circleSectionHeaderKind)
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: mainSectionHeaderKind, withReuseIdentifier: mainSectionHeaderKind)
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: slideSectionHeaderKind, withReuseIdentifier: slideSectionHeaderKind)
        return cv
    }()


    //MARK: - Make CollectionView Compositional Layout
extension Diffable_CompositionalViewController {

    private func makeFlowLayout() -&gt; UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { section, ev -&gt; NSCollectionLayoutSection? in
            // section에 따라 서로 다른 layout 구성
            switch DiffableSection.allCases[section] {
            case .circle:
                return self.makeCircleSectionLayout()
            case .slide:
                return self.makeSlideSectionLayout()
            case .main:
                return self.makeMainSectionLayout()
            }
        }
    }

    private func makeCircleSectionLayout() -&gt; NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .absolute(80),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

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

        // section
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)

        // header
        let header = makeHeaderView(elementKind: circleSectionHeaderKind)
        section.boundarySupplementaryItems = [header]

        return section
    }

    private func makeSlideSectionLayout() -&gt; NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // group
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(0.8),
            heightDimension: .fractionalHeight(0.2))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        group.contentInsets = NSDirectionalEdgeInsets(
            top: 0,
            leading: 0,
            bottom: 0,
            trailing: 10)

        // section
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)

        section.orthogonalScrollingBehavior = .groupPagingCentered

        // header
        let header = makeHeaderView(elementKind: slideSectionHeaderKind)
        section.boundarySupplementaryItems = [header]

        return section
    }

    private func makeMainSectionLayout() -&gt; NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(
            top: 0,
            leading: 0,
            bottom: 10,
            trailing: 0)

        // group
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(0.5))

        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        // section
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)
        // header
        let header = makeHeaderView(elementKind: mainSectionHeaderKind)
        section.boundarySupplementaryItems = [header]

        return section
    }

    private func makeHeaderView(elementKind: String) -&gt; NSCollectionLayoutBoundarySupplementaryItem {
        let headerSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .estimated(50))
        let header = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerSize,
            elementKind: elementKind,
            alignment: .top)

        return header
    }
}</code></pre>
<h1 id="diffabledatasource">DiffableDataSource</h1>
<pre><code class="language-swift">
//MARK: - DataSource
extension Diffable_CompositionalViewController {

    // Diffable DataSource 정의
    // 파라미터로 collectionView와 cellProvider 전달
    // cellProvider를 통해 UI에 표시할 셀을 리턴함
    private func setupDataSource() {
        dataSource = .init(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, item) -&gt; UICollectionViewCell? in
            switch item {
            case .mainItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            case .slideItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            case .circleItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CircleCell.identifier, for: indexPath) as! CircleCell
                cell.bind(text: model.title)
                return cell
            }
        })

        dataSource?.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -&gt; UICollectionReusableView? in
            // header, footer...
            switch elementKind {
            case self.circleSectionHeaderKind:
                let header = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: self.circleSectionHeaderKind, for: indexPath) as! SectionHeader
                header.bind(sectionTitle: &quot;Circle Section&quot;)
                return header
            case self.mainSectionHeaderKind:
                let header = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: self.mainSectionHeaderKind, for: indexPath) as! SectionHeader
                header.bind(sectionTitle: &quot;Main Section&quot;)
                return header
            case self.slideSectionHeaderKind:
                let header = collectionView.dequeueReusableSupplementaryView(ofKind: elementKind, withReuseIdentifier: self.slideSectionHeaderKind, for: indexPath) as! SectionHeader
                header.bind(sectionTitle: &quot;Slide Section&quot;)
                return header
            default:
                return nil
            }
        }
    }

    // DiffableDataSource는 Snapshot을 사용해서 CollectionView 또는 TableView에 데이터를 제공
    // 스냅샷을 사용해서 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영함
    // 스냅샷의 데이터는 표시하려는 Section과 Item으로 구성됨.
    private func updateSnapshot(
        circleSectionItems: [DiffableSectionItem],
        slideSectionItems: [DiffableSectionItem],
        mainSectionItems: [DiffableSectionItem]
    ) {
        var snapshot = MySnapshot() // 스냅샷 생성
        snapshot.appendSections(DiffableSection.allCases) // Section 추가
        // toSection 파라미터에 Section을 전달해서 아이템을 전달할 섹션을 명시함.
        // toSection 파라미터를 사용하지 않으면, 자동으로 마지막 섹션으로 전달됨.
        snapshot.appendItems(circleSectionItems, toSection: .circle)
        snapshot.appendItems(mainSectionItems, toSection: .main)
        snapshot.appendItems(slideSectionItems, toSection: .slide)
        dataSource?.apply(snapshot, animatingDifferences: true) // 데이터 새 상태 반영
    }

}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] DiffableDataSource]]></title>
            <link>https://velog.io/@syong_e/TIL-DiffableDataSource</link>
            <guid>https://velog.io/@syong_e/TIL-DiffableDataSource</guid>
            <pubDate>Thu, 14 Sep 2023 06:41:54 GMT</pubDate>
            <description><![CDATA[<p>오늘은 DiffableDataSource를 사용해서 CollectionView를 구현해보도록 하겠습니다.</p>
<h1 id="diffabledatasource">DiffableDataSource</h1>
<p><img src="https://velog.velcdn.com/images/syong_e/post/a3718de1-ab3d-4a0e-8e66-37bf7a29cdb4/image.png" alt=""></p>
<p><a href="https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource">공식 문서</a>에서는 다음과 같이 설명합니다.</p>
<blockquote>
<p>데이터를 관리하고, CollectionView에 셀을 제공하기 위해 사용하는 객체</p>
</blockquote>
<p>DiffableDataSource는 CollectionView와 함께 작동하는 특수한 유형의 DataSource입니다. CollectionView의 데이터 및 UI 업데이트를 간단하고(?), 효율적인 방식으로 관리하는데 필요한 동작을 제공합니다. 또한, UICollectionViewDataSource 프로토콜을 준수하며 프로토콜의 모든 메서드에 대한 구현을 제공합니다.</p>
<p>CollectionView를 데이터로 채우려면 다음과 같이 하세요</p>
<ol>
<li><code>DiffableDataSource</code>를 CollectionView에 연결합니다.</li>
<li><code>Cell Provider</code>를 구현하여 CollectionView의 Cell을 구성합니다.</li>
<li>데이터의 <code>현재 상태</code>를 생성합니다.</li>
<li>UI에 데이터를 <code>표시</code>합니다.</li>
</ol>
<p>DiffableDataSource를 CollectionView에 연결하려면, 해당 DataSource와 연결하려는 CollectionView를 전달하여 <a href="https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasource/3255138-init">init(collectionView:CellProvider:)</a> 이니셜라이저를 사용해 DiffableDataSource를 만듭니다.
또한, Cell Provider를 전달하여 UI에 데이터를 표시하는 방법을 결정하도록 각 셀을 구성합니다.</p>
<pre><code class="language-swift">dataSource = UICollectionViewDiffableDataSource&lt;Int, UUID&gt;(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: UUID) -&gt; UICollectionViewCell? in
    // Configure and return cell.
}</code></pre>
<p>그런 다음 Snapshot을 구성하고 적용하여 데이터의 현재 상태를 생성하고, UI에 데이터를 표시합니다.
자세한 내용은 <a href="https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot">NSDiffableDataSourceSnapshot</a>을 참고하세요.</p>
<p>.
.
.</p>
<p>네...! 정리하자면, CollectionView에 DataSource를 제공하기 위해서 DiffableDataSource를 만들어서 CollectionView에 연결해줘야하고, Cell Provider로 화면에 표시할 Cell을 구성한다. 이 과정을 생성자로 제공한다!</p>
<p>그리고, UI를 업데이트해주기 위해서 Snapshot을 생성하고, UI에 데이터를 표시한다!</p>
<p>또 하나 눈 여겨볼 것은 DiffableDataSource는 <code>섹션(SectionIdentifierType)</code>과 <code>아이템(ItemIdentifierType)</code>에 대한 <code>제네릭 타입</code>을 가집니다. 그리고, 두 타입 모두 <code>Hashable</code> 프로토콜을 채택해야합니다!</p>
<p>일단 여기까지 대충 이해했다고 치고, DiffableDataSource 생성자와 Snapshot에 대해서 좀 더 자세하게 알아볼게요.</p>
<h1 id="생성자">생성자</h1>
<p><img src="https://velog.velcdn.com/images/syong_e/post/c3997ab1-79f8-47b4-a7e1-c4703c9e4eda/image.png" alt=""></p>
<blockquote>
<p>지정된 Cell Provider를 사용하여 DiffableDataSource를 만들고, 지정된 CollectionView에 연결합니다.</p>
</blockquote>
<h2 id="parameters">Parameters</h2>
<p>collectionView : 데이터 소스를 연결할 CollectionView
cellProvider : 데이터 소스가 제공하는 데이터를 사용해서 CollectionView의 각 Cell을 만들고 반환하는 클로저</p>
<h1 id="nsdiffabledatasourcesnapshot">NSDiffableDataSourceSnapshot</h1>
<p><img src="https://velog.velcdn.com/images/syong_e/post/ee564310-3b52-456f-a917-a3c77072a629/image.png" alt=""></p>
<blockquote>
<p>특정 시점의 View에 있는 데이터 상태의 표현</p>
</blockquote>
<p>DiffableDataSource는 Snapshot을 사용하여 CollectionView 및 TableView에 데이터를 제공합니다. Snapshot을 사용하여 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영합니다.</p>
<p>그리고 DiffableDataSource와 마찬가지로 섹션과 아이템에 대한 제네릭 타입을 가지고, 역시 각각 Hashable 프로토콜을 준수해야만 합니다.</p>
<p>다음과 같이 Snapshot을 사용하여 뷰에 데이터를 표시합니다</p>
<ol>
<li>Snapshot을 만들고 표시하려는 데이터의 상태로 스냅샷을 채웁니다.</li>
<li>Snapshot을 적용하여 UI에 변경 사항을 반영합니다.</li>
</ol>
<pre><code class="language-swift">// Create a snapshot.
var snapshot = NSDiffableDataSourceSnapshot&lt;Int, UUID&gt;()        


// Populate the snapshot.
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()])


// Apply the snapshot.
dataSource.apply(snapshot, animatingDifferences: true)</code></pre>
<p>이론은 여기까지 하고, 이제 예제 진행할게요!</p>
<h1 id="예제-코드">예제 코드</h1>
<p>전통적인 방식인 UICollectionViewDataSource와 DiffableDataSource의 가장 큰 차이점은 데이터가 변경될 때 reloadData 메서드를 호출 여부입니다. Snapshot의 개념이 추가되면서 현재 상태와 새로운 상태의 차이점을 비교하고, 이에 따라 필요한 UI 업데이트(셀 추가/삭제/재정렬 등)을 자동으로 수행합니다.
이러한 방식은 코드가 조금 더 복잡해질 수 있지만, 효율적인 UI 업데이트를 보장하며, 큰 데이터 세트에 대해서도 높은 성능을 유지할 수 있습니다.</p>
<pre><code class="language-swift">enum DiffableSection: CaseIterable {
    case main
    // case etc... 섹션 추가
}

enum DiffableSectionItem: Hashable {
    case mainItem(MainItemModel)
    // case ectItem... 섹션 아이템 추가

    struct MainItemModel: Hashable {
        let title: String
    }
}
typealias MyDataSource = UICollectionViewDiffableDataSource&lt;DiffableSection, DiffableSectionItem&gt;
typealias MySnapshot = NSDiffableDataSourceSnapshot&lt;DiffableSection, DiffableSectionItem&gt;</code></pre>
<p>Section과 Item을 열거형으로 정의하고, typealias로 DiffableDataSource와 Snapshot에 대한 타입을 사용자 정의 타입으로 지정했습니다.</p>
<p>굳이 typealias를 사용해서 타입을 따로 만들 필요는 없지만, 가독성을 위해서 좋은 방법입니다.</p>
<pre><code class="language-swift">// Diffable DataSource 정의
    // 파라미터로 collectionView와 cellProvider 전달
    // cellProvider를 통해 UI에 표시할 셀을 리턴함
    private func setupDataSource() {
        dataSource = .init(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, item) -&gt; UICollectionViewCell? in
            switch item {
            case .mainItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            }
        })

        dataSource?.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -&gt; UICollectionReusableView? in
            // header, footer...
            return nil
        }
    }

    // DiffableDataSource는 Snapshot을 사용해서 CollectionView 또는 TableView에 데이터를 제공
    // 스냅샷을 사용해서 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영함
    // 스냅샷의 데이터는 표시하려는 Section과 Item으로 구성됨.
    private func updateSnapshot(items: [DiffableSectionItem], toSection: DiffableSection) {
        var snapshot = MySnapshot() // 스냅샷 생성
        snapshot.appendSections(DiffableSection.allCases) // Section 추가
        snapshot.appendItems(items, toSection: toSection) // Item 추가
        dataSource?.apply(snapshot, animatingDifferences: true) // 데이터 새 상태 반영
    }</code></pre>
<p>DiffableDataSource를 생성해서 collectionView에 연결합니다.
생성자 파라미터로 연결할 CollectoinView와 화면에 표시할 Cell을 구성하는 cellProvider를 클로저로 전달합니다.</p>
<p>또한, 데이터가 변경됐을 때, 변경사항을 업데이트하기 위해 snapshot을 생성해 섹션과 항목을 추가하고, 데이터 소스에 반영합니다.</p>
<pre><code class="language-swift">    private func makeMockDatas() {
        var mockDatas = (1...100).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
        DispatchQueue.main.asyncAfter(deadline: .now()+1) {
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }

        DispatchQueue.main.asyncAfter(deadline: .now()+3) {
            mockDatas = (20...120).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+5) {
            mockDatas = (40...120).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+7) {
            mockDatas = (30...45).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
    }</code></pre>
<p>DiffableDataSource가 잘 동작하는지 테스트하기 위해 데이터를 생성하고, updateSnapshot 메서드를 호출합니다.</p>
<h1 id="전체-소스코드">전체 소스코드</h1>
<pre><code class="language-swift">
import UIKit

enum DiffableSection: CaseIterable {
    case main
    // case etc... 섹션 추가
}

enum DiffableSectionItem: Hashable {
    case mainItem(MainItemModel)
    // case ectItem... 섹션 아이템 추가

    struct MainItemModel: Hashable {
        let title: String
    }
}
typealias MyDataSource = UICollectionViewDiffableDataSource&lt;DiffableSection, DiffableSectionItem&gt;
typealias MySnapshot = NSDiffableDataSourceSnapshot&lt;DiffableSection, DiffableSectionItem&gt;


class DiffableDataSourceViewController: UIViewController {

    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = .init(width: view.frame.width - 20, height: 100)
        layout.scrollDirection = .vertical
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.register(RectangleCell.self, forCellWithReuseIdentifier: RectangleCell.identifier)
        return cv
    }()

    var dataSource: MyDataSource?

    override func viewDidLoad() {
        super.viewDidLoad()

        layout()
        setupDataSource()
        makeMockDatas()
    }

    private func layout() {
        view.backgroundColor = .white
        view.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    private func makeMockDatas() {
        var mockDatas = (1...100).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
        DispatchQueue.main.asyncAfter(deadline: .now()+1) {
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }

        DispatchQueue.main.asyncAfter(deadline: .now()+3) {
            mockDatas = (20...120).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+5) {
            mockDatas = (40...120).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
        DispatchQueue.main.asyncAfter(deadline: .now()+7) {
            mockDatas = (30...45).map { DiffableSectionItem.mainItem(.init(title: &quot;main\($0)&quot;)) }
            self.updateSnapshot(items: mockDatas, toSection: .main)
        }
    }
}

//MARK: - DataSource
extension DiffableDataSourceViewController {

    // Diffable DataSource 정의
    // 파라미터로 collectionView와 cellProvider 전달
    // cellProvider를 통해 UI에 표시할 셀을 리턴함
    private func setupDataSource() {
        dataSource = .init(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, item) -&gt; UICollectionViewCell? in
            switch item {
            case .mainItem(let model):
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
                cell.bind(text: model.title)
                return cell
            }
        })

        dataSource?.supplementaryViewProvider = { (collectionView, elementKind, indexPath) -&gt; UICollectionReusableView? in
            // header, footer...
            return nil
        }
    }

    // DiffableDataSource는 Snapshot을 사용해서 CollectionView 또는 TableView에 데이터를 제공
    // 스냅샷을 사용해서 뷰에 표시되는 데이터의 초기 상태를 설정하고, 데이터의 변경 사항을 반영함
    // 스냅샷의 데이터는 표시하려는 Section과 Item으로 구성됨.
    private func updateSnapshot(items: [DiffableSectionItem], toSection: DiffableSection) {
        var snapshot = MySnapshot() // 스냅샷 생성
        snapshot.appendSections(DiffableSection.allCases) // Section 추가
        snapshot.appendItems(items, toSection: toSection) // Item 추가
        dataSource?.apply(snapshot, animatingDifferences: true) // 데이터 새 상태 반영
    }

}
</code></pre>
<p>CollectionView나 TableView를 구성할 때, 데이터 소스 업데이트가 자주 일어나고, 대량의 데이터를 다뤄야한다면, DiffableDataSource를 사용하는 것이 좋을 것 같습니다.
반면에 비교적 적은 양의 데이터와 단순한 목록 화면 같은 경우에는 전통적인 DataSource 방식으로 충분할 것 같습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL]CompositionalLayout으로 섹션마다 다른 레이아웃 구성하기]]></title>
            <link>https://velog.io/@syong_e/TILCompositionalLayout%EC%9C%BC%EB%A1%9C-%EC%84%B9%EC%85%98%EB%A7%88%EB%8B%A4-%EB%8B%A4%EB%A5%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@syong_e/TILCompositionalLayout%EC%9C%BC%EB%A1%9C-%EC%84%B9%EC%85%98%EB%A7%88%EB%8B%A4-%EB%8B%A4%EB%A5%B8-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83-%EA%B5%AC%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 13 Sep 2023 14:35:08 GMT</pubDate>
            <description><![CDATA[<p>오늘은 UICollectionViewCompositionalLayout을 사용해서 CollectionView의 Section마다 서로 다른 레이아웃을 구성해보도록 하겠습니다.</p>
<h1 id="uicollectionviewcompositionallayout">UICollectionViewCompositionalLayout</h1>
<p><img src="https://velog.velcdn.com/images/syong_e/post/9ed2b717-1782-4616-a445-0284718f039e/image.png" alt=""></p>
<p>애플 공식문서에서는 다음과 같이 설명하고 있습니다.</p>
<blockquote>
<p>높은 적응력과 유연한 시각적 배치로 항목을 결합할 수 있는 레이아웃 객체</p>
</blockquote>
<p>CompositionalLayout은 CollectionView 레이아웃의 한 유형입니다. 이것은 유연하고 빠르게 구성할 수 있도록 설계되어 각각의 작은 구성 요소를 전체 레이아웃으로 결합하거나 합성하여 콘텐츠에 대한 모든 종류의 시각적 배열을 구축할 수 있습니다.</p>
<p>CompositionalLayout은 레이아웃을 별개의 시각적 <code>Group</code> 으로 분할하는 하나 이상의 <code>Section</code>으로 구성됩니다. 각 Section은 표시하려는 데이터의 최소 단위인 개별 항목 Group으로 구성됩니다. Group은 항목을 가로 행, 세로 열 또는 커스텀 배치도 가능합니다.</p>
<blockquote>
<p>쉽게 말해서 CollectionView가 여러 개의 Section으로 구성되어 있고, Section마다 서로 다른 레이아웃을 표시하기 위해서 UICollectionViewCompositionalLayout을 사용할 수 있습니다.
이것은 iOS 13부터 사용할 수 있습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/syong_e/post/ab45ab9e-dd82-4202-a65c-3310b3783387/image.png" alt=""></p>
<p>위 그림을 보면, CollectionView는 여러 개의 <code>Section</code>을 가질 수 있고, Section 안에는 분할 된 여러 개의 <code>Group</code>과 Group 안에는 <code>Item</code>이 있습니다. </p>
<p>이론은 여기까지하고 바로 예제 진행하겠습니다.</p>
<h1 id="예제">예제</h1>
<p><img src="https://velog.velcdn.com/images/syong_e/post/fbf47e2a-d709-4dd1-b5a6-00a8084263b0/image.png" alt=""></p>
<p>위와 같은 UI를 CollectionView 하나만 사용해서 구현하고 싶을 때, CompositionalLayout을 사용하면 됩니다.</p>
<p>총 3개의 Section이 있고, Section마다 서로 다른 레이아웃을 구성하고 있습니다.
그리고 각각의 Section은 Header를 가지고 있습니다.</p>
<p>우선 Header는 제쳐두고, 3개의 섹션으로 나누고 각각 레이아웃을 배치하는 코드를 작성해볼게요.</p>
<pre><code class="language-swift">enum HomeSection {
    case circle([String])
    case slideRectangle([String])
    case rectangle([String])
}</code></pre>
<p>섹션을 구분 위한 열거형을 정의합니다.
각각의 섹션에 데이터를 뿌려주기 위해서 연관값으로 문자열 배열 데이터를 가집니다.</p>
<pre><code class="language-swift">
final class RectangleCell: UICollectionViewCell {

    private let label: UILabel = {
        let label = UILabel()
        label.textColor = .white
        label.textAlignment = .center
        return label
    }()

    static let identifier = &quot;RectangleCell&quot;

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

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

    private func layout() {
        self.contentView.backgroundColor = UIColor(
          red: CGFloat(drand48()),
          green: CGFloat(drand48()),
          blue: CGFloat(drand48()),
          alpha: 1.0
        )

        contentView.addSubview(label)
        label.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    func bind(text: String) {
        label.text = text
    }

}</code></pre>
<p>Slide와 Rectangle 섹션에서 사용할 Cell입니다.</p>
<p>CircleCell은 cornerRadius를 설정하는 것 제외하고는 위와 동일하기 때문에 따로 첨부하지 않겠습니다. </p>
<pre><code class="language-swift">class CompositionalLayoutViewController: UIViewController {

    private lazy var collectionView: UICollectionView = {
        let cv = UICollectionView(frame: .zero, collectionViewLayout: self.makeFlowLayout())
        cv.dataSource = self
        cv.register(SectionHeader.self, forSupplementaryViewOfKind: SectionHeader.elementKind, withReuseIdentifier: SectionHeader.identifier)
        cv.register(RectangleCell.self, forCellWithReuseIdentifier: RectangleCell.identifier)
        cv.register(CircleCell.self, forCellWithReuseIdentifier: CircleCell.identifier)
        return cv
    }()

    private var dataSource: [HomeSection] = [
        HomeSection.circle((0...10).map { &quot;Circle\($0)&quot; }),
        HomeSection.slideRectangle((0...10).map { &quot;Slide\($0)&quot; }),
        HomeSection.rectangle((0...10).map { &quot;Rectangle\($0)&quot; }),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        layout()
    }

    private func layout() {
        view.backgroundColor = .white

        view.addSubview(collectionView)
        collectionView.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide)
            make.left.right.bottom.equalToSuperview()
        }

    }
}</code></pre>
<p>collectionView를 배치하고, 각 섹션에 뿌려주기 위한 데이터를 임의로 생성해서 dataSource라는 변수에 저장했습니다.</p>
<p>여기까지 하면, makeFlowLayout()에서 에러가 발생하죠.
이 함수로 CompositionalLayout을 만들겁니다.</p>
<p>dataSource도 구현해주지 않았으니 먼저 구현해줍시다.</p>
<pre><code class="language-swift">//MARK: - DataSource
extension CompositionalLayoutViewController: UICollectionViewDataSource {
    func numberOfSections(in collectionView: UICollectionView) -&gt; Int {
        return dataSource.count
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -&gt; Int {
        switch dataSource[section] {
        case .circle(let data):
            return data.count
        case .slideRectangle(let data):
            return data.count
        case .rectangle(let data):
            return data.count
        }
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&gt; UICollectionViewCell {
        switch dataSource[indexPath.section] {
        case .circle(let data):
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CircleCell.identifier, for: indexPath) as! CircleCell
            cell.bind(text: data[indexPath.row])
            return cell
        case .slideRectangle(let data):
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
            cell.bind(text: data[indexPath.row])
            return cell
        case .rectangle(let data):
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RectangleCell.identifier, for: indexPath) as! RectangleCell
            cell.bind(text: data[indexPath.row])
            return cell
        }
    }
</code></pre>
<p>섹션에 따라 분기처리해주는 것 말고, 단일 섹션으로 CollectionView를 구현하는 것과 다르지 않습니다.</p>
<pre><code class="language-swift">//MARK: - Make CollectionView Compositional Layout
extension CompositionalLayoutViewController {

    private func makeFlowLayout() -&gt; UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { section, ev -&gt; NSCollectionLayoutSection? in
            // section에 따라 서로 다른 layout 구성
            switch self.dataSource[section] {
            case .circle:
                return self.makeCircleSectionLayout()
            case .slideRectangle:
                return self.makeSlideRectangleLayout()
            case .rectangle:
                return self.makeRectangleSectionLayout()
            }

        }
    }

    private func makeCircleSectionLayout() -&gt; NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .absolute(80),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

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

        // section
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous // 수평 스크롤
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)


        return section
    }

    private func makeSlideRectangleLayout() -&gt; NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = .init(
            top: 0,
            leading: 10,
            bottom: 0,
            trailing: 10)

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

        // section
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .groupPagingCentered
        section.contentInsets = NSDirectionalEdgeInsets(
            top: 12,
            leading: 10,
            bottom: 12,
            trailing: 10)

    }

    private func makeRectangleSectionLayout() -&gt; NSCollectionLayoutSection? {
        // item
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = NSDirectionalEdgeInsets(
            top: 0,
            leading: 0,
            bottom: 10,
            trailing: 0)

        // group
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(0.5))

        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

        // section
        let section = NSCollectionLayoutSection(group: group)
        item.contentInsets = NSDirectionalEdgeInsets(
            top: 0,
            leading: 10,
            bottom: 12,
            trailing: 10)


        return section
    }
</code></pre>
<p>처음 보면, 굉장히 난해한 코드로 보일 수 있습니다만... 알고보면 어려운 건 없고, 그냥 item, group의 size를 지정하고, item을 group에 넣어주고, 그 group을 통해 section을 생성하는 코드입니다.</p>
<p>여기서 layoutSize를 지정하는 방법으로는 3가지가 있습니다.</p>
<ul>
<li>fractional : 화면 비율로 지정</li>
<li>estimated : 추정 값, 실제 크기는 콘텐츠가 렌더링될 때 결정</li>
<li>absolute : 절대값</li>
</ul>
<p>저는 코드 가독성을 위해 섹션마다 섹션 생성 함수를 분리해서 작성했습니다</p>
<p>지금까지의 코드를 실행해보면, 3개의 섹션으로 나뉘고, 섹션마다 서로 다른 레이아웃으로 구성된 UI가 나옵니다!</p>
<p>이제 각 Section에 Header를 넣어볼게요!</p>
<pre><code class="language-swift">import UIKit

final class SectionHeader: UICollectionReusableView {

    private let sectionLabel: UILabel = {
        let label = UILabel()
        label.font = .boldSystemFont(ofSize: 22)
        label.textColor = .black
        return label
    }()

    static let elementKind = &quot;SectionHeader&quot;
    static let identifier = &quot;SectionHeader&quot;

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

        layout()
    }

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

    private func layout() {
        addSubview(sectionLabel)
        sectionLabel.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    func bind(sectionTitle: String) {
        sectionLabel.text = sectionTitle
    }

}
</code></pre>
<p>Header는 UICollectionReusableView를 상속받아서 구현합니다.
저는 UILabel 하나만 배치해줬어요</p>
<pre><code class="language-swift">cv.register(SectionHeader.self, forSupplementaryViewOfKind: SectionHeader.elementKind, withReuseIdentifier: SectionHeader.identifier)</code></pre>
<p>collectionView 선언하는 곳에 header를 register 해주세요.</p>
<pre><code class="language-swift">private func makeHeaderView() -&gt; NSCollectionLayoutBoundarySupplementaryItem {
        let headerSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .estimated(50))
        let header = NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: headerSize,
            elementKind: SectionHeader.elementKind,
            alignment: .top)

        return header
    }</code></pre>
<p>Group, Item과 마찬가지로 header도 LayoutSize를 지정해서 만들어줍니다!</p>
<pre><code class="language-swift">// header
let header = makeHeaderView()
section.boundarySupplementaryItems = [header]</code></pre>
<p>각 섹션을 정의하는 부분에서 위와 같이 header를 넣어주면 됩니다!</p>
<pre><code class="language-swift">    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -&gt; UICollectionReusableView {
        switch kind {
        case SectionHeader.elementKind:
            let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeader.identifier, for: indexPath) as! SectionHeader

            switch dataSource[indexPath.section] {
            case .circle:
                header.bind(sectionTitle: &quot;Circle Layout&quot;)
            case .rectangle:
                header.bind(sectionTitle: &quot;Rectangle Layout&quot;)
            case .slideRectangle:
                header.bind(sectionTitle: &quot;Slide Layout&quot;)
            }

            return header
        default:
            return UICollectionReusableView()
        }
    }</code></pre>
<p>Header도 Cell과 마찬가지로 재사용되기 때문에 DataSource의 메서드를 구현해줘야합니다.</p>
<p>이제 코드 돌려보면, 원하던 UI가 나올겁니다.</p>
<p>굉장히 복잡한 것 같지만, 이 코드에 익숙해지면, 추가적인 Section을 구현한다하더라도 거의 복붙이기 때문에 약간 복잡한 것 같으면서 유연한 구조인 것 같아요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AutoLayout] IntrinsicContentSize, Hugging/Compression Priority]]></title>
            <link>https://velog.io/@syong_e/AutoLayout-IntrinsicContentSize-HuggingCompression-Priority</link>
            <guid>https://velog.io/@syong_e/AutoLayout-IntrinsicContentSize-HuggingCompression-Priority</guid>
            <pubDate>Sat, 26 Aug 2023 07:58:03 GMT</pubDate>
            <description><![CDATA[<p>이번에는 그동안 미루고 있던 AutoLayout에 대해 공부를 하려고 합니다.
Constraints를 통해 AutoLayout을 사용하는 것에는 문제가 없지만, 그동안 IntrinsicContentSize나 Priority 등의 이해 없이 무식하게 AutoLayout을 사용한 것 같아서 오늘 제대로 이해해보는 시간을 갖겠습니다.</p>
<h2 id="intrinsiccontentsize">IntrinsicContentSize</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/f0617512-c174-4b69-b275-1f11452016d5/image.png" alt=""></p>
<blockquote>
<p>뷰에서 자기 자신이 표현해야 할 콘텐츠의 &#39;자연스러운&#39; 크기</p>
</blockquote>
<p>IntrinsicContentSize는 UIView의 서브클래스 중 일부가 가지는 속성으로, 뷰에서 자기 자신이 표현해야 할 콘텐츠의 자연스러운 크기를 나타냅니다.
이 크기는 Autolayout 시스템에서 사용되어 뷰의 크기를 결정하는데 도움이 됩니다.</p>
<p>예를 들어, UILabel이나 UIButton 같은 클래스는 내부에 표시할 텍스트에 따른 적절한 크기를 계산해서 IntrinsicContentSize로 제공합니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/b957e5ae-22d8-4775-9c5b-387d3efe026b/image.png" alt=""></p>
<p>스토리보드에 UILabel을 놓고, Top, Leading에 Constraint 설정했습니다.
width, height는 따로 설정하지 않았지만, UILabel은 IntrinsicContentSize를 제공하기 때문에 오류가 발생하지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/174cc0db-8358-4370-9799-7d80bf3f24a1/image.png" alt=""></p>
<p>이번엔 UIView를 놓고, 마찬가지로 Top, Leading에만 Constraint를 설정했습니다.
그런데, 이번에는 오류가 발생합니다.</p>
<p>UILabel과 똑같은 조건인데 Label은 오류가 발생하지 않고, UIView는 오류가 발생하네요?
왜 그럴까요?</p>
<p>UIView는 콘텐츠가 없기 때문에 기본적인 크기도 존재하지 않습니다.
오류를 해결하기 위해서는 UIView의 크기를 지정해주면 되겠죠?</p>
<p>이렇게 고유 콘텐츠가 포함된 UI들은 콘텐츠의 크기에 따라 자동으로 View의 Size가 계산되어 적용되고, 고유 콘텐츠가 없는 UI는 Size가 자동으로 적용되지 않기 때문에 크기에 대한 조건을 명시해야합니다.</p>
<h2 id="hugging--compression-resistance-priority">Hugging &amp; Compression Resistance Priority</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/da09efd7-8123-46e4-a9ba-cdacc673bff7/image.png" alt=""></p>
<p>고유 사이즈가 있는 View 들 사이에는 그 View들 중에서 어떤 녀석이 고유 사이즈를 지키고, 어떤 녀석이 줄어들고, 늘어나야하는지를 결정하는 우선순위가 존재합니다.
그 우선순위를 결정하는 것이 Hugging Priority, Compression Resistance Priority 입니다.</p>
<p>결론부터 먼저 말하면,</p>
<blockquote>
<p>Hugging 우선순위가 높으면, 고유 사이즈보다 커지지 않으려하고, 
Compression 우선순위가 높으면, 고유 사이즈보다 작아지지 않으려합니다.</p>
</blockquote>
<p>이 두가지만 외우면 어렵지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/d2b7132e-7ebf-4e62-b11f-aa50ad7719f9/image.png" alt=""></p>
<p>스토리보드에 label과 button을 두고, StackView로 묶어서 StackView를 Y축 정렬, 왼쪽, 오른쪽으로 10만큼 Constraints를 설정했습니다.</p>
<p>Label과 Button은 Intrinsic Content Size가 있고, StackView의 영역을 채우기에는 크기가 작기 때문에 두 View 중에서 하나는 남는 영역을 채워야 합니다.</p>
<p>UILabel은 고유 사이즈를 지켰고, Button은 고유 사이즈보다 더 크게 늘어났죠?
UILabel의 Hugging 우선순위가 더 높기 때문에. 즉, label이 고유 사이즈보다 커지지 않으려하기 때문에 Hugging 우선순위가 더 낮은 Button이 늘어났다고 예측할 수 있습니다.</p>
<p>실제로 그런지 인터페이스 빌더에서 확인해보겠습니다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/syong_e/post/bcca93c1-a880-4a55-8a4a-083d259e08f4/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/syong_e/post/cae2ca01-051d-42d1-86ee-2ae46679e588/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td>Label은 Horizontal Hugging Priority가 251이고, Button은 250이죠?</td>
<td></td>
</tr>
</tbody></table>
<p>위에서 예측한 것이 맞았네요.</p>
<p>그런데 보통 이런 UI를 구성할 때, 버튼을 늘리기 보다는 Label을 늘려서 Button의 사이즈를 유지하는게 더 보기 좋아보일 것 같습니다. </p>
<p>Button을 늘리지 않고, Label이 늘어나게 하려면 어떻게 해야할까요?
<img src="https://velog.velcdn.com/images/syong_e/post/779d8298-4926-4435-9273-bdb7e14e6cda/image.png" alt=""></p>
<p>Button의 Hugging Priority를 Label보다 높여주면 되겠죠?
아니면, Label의 Hugging Priority를 낮춰도 될거에요.</p>
<p>이렇게 고유 사이즈보다 커지지 않으려는 성질의 우선순위를 결정하는 것이 바로 Content Hugging Priority 입니다.</p>
<p>여기까지 이해 됐으리라 생각하고, 이제 Compression Priority에 대해서 알아볼게요!</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/e746aecd-5bc2-4045-8de9-be8b192e5dbc/image.png" alt=""></p>
<p>이렇게 Label의 Text에 아주 긴~ Text가 들어갔다고 해볼게요.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/7bbc73cd-0eac-49bf-bba2-629b9620cb1e/image.png" alt=""></p>
<p>그런데 이런 오류가 발생하네요?</p>
<p>horizontal compression resistance를 감소시키라고 알려주고 있습니다.
이 값이 감소하면, 어떤 일이 발생할까요?</p>
<p>위에서 compression resistance는 고유 사이즈보다 작아지지 않으려는 성질이 있다고 했죠?
그런데 이 값을 감소시키면, 우선순위가 낮아지게 되고, 결국 고유 사이즈보다 작아지겠죠? 즉, 콘텐츠가 잘려서 보이게됩니다.</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/syong_e/post/6b248385-2781-465c-98fa-d5c7b19ed06b/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/syong_e/post/ce435929-cc49-4eaa-9409-4685abe22fa7/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>현재 horisontal Compression Resistance Priority가 Label과 Button 모두 750으로 동일합니다. 이 경우에는 Label과 Button 중 어떤 View가 작아져야하는지 모르기 때문에 충돌이 발생한 것 입니다. </p>
<p>이를 해결하기 위해서 Button의 Compression Resistance Priority를 높여보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/14bd0359-7b24-4de6-b2a0-425bb3992600/image.png" alt=""></p>
<p>Button의 Compression Resistance 우선순위가 Label보다 높기 때문에. 즉, Button은 고유 사이즈보다 작아지지 않으려는 성질의 우선순위가 Label보다 높기 때문에 Button의 크기를 유지하고, Label의 크기가 고유 사이즈보다 작아져서 콘텐츠가 잘려보이게 됐습니다.</p>
<p>반대로 Label의 고유 사이즈를 유지하고, Button을 작아지게 하려면 어떻게 해야할까요?
<img src="https://velog.velcdn.com/images/syong_e/post/af92380f-0dca-4eba-b542-f2e077b83f35/image.png" alt=""></p>
<p>이렇게 Label의 Compression Priority를 Button보다 크게 설정해주면 되겠죠?</p>
<p>하지만, 일반적으로 UI를 구성할 때 Button의 Compression Priority를 높게 설정하는 것이 더 보기 좋을 것 같네요.</p>
<h2 id="최종-정리">최종 정리</h2>
<ul>
<li>IntrinsicContentSize는 고유 콘텐츠가 존재하는 View의 크기를 자동으로 계산한다.</li>
<li>허깅은 고유 사이즈보다 커지지 않으려고 하는 조건이다.</li>
<li>컴프레션은 고유 사이즈보다 줄어들지 않으려고 하는 조건이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] iOS Layout 이해하기]]></title>
            <link>https://velog.io/@syong_e/TIL-iOS-Layout-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@syong_e/TIL-iOS-Layout-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 15 Aug 2023 07:53:09 GMT</pubDate>
            <description><![CDATA[<h2 id="출처">출처</h2>
<blockquote>
<p><a href="https://tech.gc.com/demystifying-ios-layout/">https://tech.gc.com/demystifying-ios-layout/</a>
이 글은 위 블로그 게시물을 해석하고, 정리하는 글입니다.</p>
</blockquote>
<p>iOS 애플리케이션을 처음 빌드할 때 피하거나 디버깅하기 가장 어려운 문제 중 일부는 view 레이아웃 및 콘텐츠와 관련된 문제입니다. 이러한 문제는 view 업데이트 시점에 대한 오해로 인해 발생합니다. view가 업데이트되는 방법과 시점을 이해하려면 iOS 애플리케이션의 Main Run Loop와 UIView에서 제공하는 일부 메서드와의 관계에 대한 심층적인 이해가 필요합니다.</p>
<p>이 게시물에서는 이러한 상호 작용을 설명하여 UIView의 메서드를 사용하여 원하는 동작을 얻는 방법을 명확하게 설명합니다.</p>
<h2 id="main-run-loop">Main run loop</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/189639e9-bcbc-4d80-b55e-43f0422dc940/image.png" alt=""></p>
<p>iOS 앱의 Main run loop는 모든 사용자 입력 이벤트를 처리하고, 적절한 응답을 Trigger하는 역할을 합니다.
앱의 모든 사용자 상호 작용은 이벤트 대기열(Event Queue)에 추가됩니다. 위 다이어그램에 표시된 Application Object는 이벤트를 Event Queue로부터 가져오고, Core Object에 Dispatch 합니다.
Core Object에서는 해당 입력의 Handler(개발자가 작성한 코드)를 호출하여 run loop를 실행합니다.
Handler의 호출이 반환되면, 제어가 Main run loop로 돌아가고 Update Cycle이 시작됩니다.
Update Cycle은 View를 Layout하고 redrawing 작업을 담당합니다. </p>
<blockquote>
<ul>
<li>Main run loop는 사용자 이벤트를 처리하고, 적절한 응답을 보낸다.</li>
</ul>
</blockquote>
<ul>
<li>이벤트가 발생하면, Event Queue에 들어가고, Application 객체에 의해 해당 이벤트의 핸들러가 실행된다.</li>
<li>핸들러 실행이 완료되면, Update Cycle에 의해 View가 업데이트 된다.</li>
</ul>
<h2 id="update-cycle">Update Cycle</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/36a4ba77-eb88-4778-b299-bada37d32968/image.png" alt=""></p>
<p>Update Cycle은 앱이 모든 이벤트 처리 코드 실행을 완료한 후 제어가 Main run loop로 돌아가는 시점입니다. 이 시점에서 시스템이 Layout, Dispay 및 Constraints 를 업데이트하기 시작합니다.
이벤트 핸들러를 처리하는 동안 View의 변경을 요청하면, 시스템에서 View를 다시 그려야 한다고 인지합니다. 다음 Update Cycle에서 시스템은 이러한 View에 대한 모든 변경 사항을 실행합니다.
사용자 상호 작용과 Layout 업데이트 사이의 지연은 사용자가 인지할 수 없을 정도로 짧아야 합니다. iOS 앱은 일반적으로 60fps로 애니메이션 되므로, 한 번의 새로 고침 주기는 1/60초밖에 걸리지 않습니다. 
이 속도는 매우 빨라서 사용자는 UI가 업데이트가 지연되는 것을 느끼지 못합니다.
그러나 이벤트가 처리되는 시점과 해당 View가 다시 그려지는 시점 사이에 간격이 있기 때문에 run loop 중 특정 지점에서 뷰가 원하는 방식으로 업데이트되지 않을 수 있습니다.
뷰의 최신 콘텐츠 또는 레이아웃에 의존하는 계산이 있는 경우 뷰의 과거 정보로 계산될 위험이 있습니다.
run loop, 업데이트 주기 및 특정 UIView 메서드를 이해하면, 이러한 문제를 방지하거나 디버깅하는데 도움이 될 수 있습니다.</p>
<blockquote>
<ul>
<li>Update Cycle은 앱이 모든 이벤트를 처리한 후 제어가 Main run loop로 돌아가는 시점이다.</li>
</ul>
</blockquote>
<ul>
<li>Update Cycle 동안 Layout, Display, Constraint 를 업데이트한다.</li>
</ul>
<h2 id="layout">Layout</h2>
<p>View의 Layout은 화면에서 View의 크기와 위치를 나타냅니다. 모든 View에는 SuperView(상위 뷰)의 좌표계에서 View가 존재하는 위치와 크기를 정의하는 frame이 있습니다.
UIView는 뷰의 레이아웃이 변경되었음을 시스템에 알릴 수 있는 메서드를 제공할 뿐만 아니라 뷰의 레이아웃이 다시 계산된 후 수행할 작업을 정의하기 위해 재정의할 수 있는 메서드를 제공합니다.</p>
<h3 id="layoutsubviews">layoutSubviews()</h3>
<p><img src="https://velog.velcdn.com/images/syong_e/post/0d736dcb-c0b2-48af-a59d-2b7f868b288a/image.png" alt=""></p>
<p>layoutSubviews()는 UIView의 인스턴스 메서드로, 뷰와 모든 하위 뷰의 위치 변경 및 크기 조정을 처리합니다.
현재 뷰와 모든 하위 뷰에 위치와 크기를 지정합니다. 이 메서드는 뷰의 모든 하위 뷰에 대해 작동하고, 해당 레이아웃 하위 뷰 메서드를 호출하기 때문에 비용이 많이 듭니다.
시스템은 뷰의 프레임을 다시 계산해야 할 때마다 이 메서드를 호출하므로 프레임을 설정하고 위치 및 크기를 지정하려는 경우 이 메서드를 재정의해야 합니다.
그러나 뷰 계층 구조에 레이아웃 새로 고침이 필요한 경우 이 메서드를 명시적으로 호출해서는 안 됩니다. 대신, run loop 중 여러 지점에서 layoutSubviews() 호출을 trigger 하는데 사용할 수 있는 여러 메커니즘이 있으며, 이는 layoutSubviews 자체를 호출하는 것보다 훨씬 비용이 적게 듭니다.</p>
<p>layoutSubviews() 가 완료되면 뷰를 소유한 ViewController에서 viewDidLayoutSubviews()가 호출됩니다. layoutSubviews()는 뷰의 레이아웃이 업데이트된 후 안정적으로 호출되는 유일한 메서드이므로 레이아웃 및 크기 조정에 의존하는 모든 로직은 viewDidLoad 또는 viewDidAppear가 아닌 viewDidLayoutSubviews 에 작성해야 합니다.
이렇게 해야만 과거의 레이아웃 및 위치 값을 사용하는 오류를 방지할 수 있습니다.</p>
<blockquote>
<ul>
<li>layoutSubviews 는 현재 뷰와 모든 하위 뷰의 위치와 크기를 지정한다.</li>
</ul>
</blockquote>
<ul>
<li><p>뷰의 모든 하위 뷰에 대해서 작동하기 때문에 비용이 많이 든다</p>
</li>
<li><p>layoutSubviews 호출을 trigger하는 여러 메커니즘이 있고, 비용이 저렴하다.</p>
</li>
<li><p>뷰의 최신 상태값으로 어떤 다른 계산을 하려면, viewDidLayoutSubviews 에 로직을 작성해라. 그렇게 하면, 과거의 뷰 상태값을 사용하는 것을 방지할 수 있다.</p>
<h3 id="자동-layoutsubviews-trigger">자동 layoutSubviews Trigger</h3>
<p>뷰가 레이아웃을 변경한 것으로 자동으로 표시하는 여러 이벤트가 있으므로 개발자가 수동으로 이 작업을 수행하지 않고도 다음 update cycle에 layoutSubviews가 호출됩니다.</p>
<p>뷰의 레이아웃이 변경되었음을 시스템에 자동으로 알리는 몇 가지 방법이 있습니다.</p>
<ul>
<li>뷰 크기 조정</li>
<li>하위 뷰 추가</li>
<li>UIScrollView 스크롤</li>
<li>기기 회전</li>
<li>뷰의 Constraints 업데이트</li>
</ul>
</li>
</ul>
<p>이들은 모두 뷰의 위치를 다시 계산해야 한다는 것을 시스템에 전달하고, 자동으로 layoutSubviews 호출로 이어집니다. </p>
<p>그리고, 레이아웃 서브뷰를 직접 트리거하는 방법도 있습니다.</p>
<h3 id="setneedslayout">setNeedsLayout()</h3>
<p>layoutSubviews 호출을 트리거하는 가장 비용이 적게 드는 방법은 뷰에서 setNeedsLayout을 호출하는 것입니다. 이렇게 하면, 뷰의 레이아웃을 다시 계산해야 한다는 것을 시스템에 전달합니다.
setNeedsLayout은 즉시 실행되고 반환되며 반환하기 전에 실제로 뷰를 업데이트하지 않습니다. 대신 다음 Update cycle 에서 시스템이 해당 뷰에서 layoutSubviews를 호출하고, 모든 하위 뷰에서 layoutSubviews 호출을 트리거할 때 뷰가 업데이트 됩니다. 
setNeedsLayout이 반환되는 시점과 뷰가 다시 그려지고 레이아웃 되는 시점 사이에 임의의 시간 간격이 있더라도 앱에 지연을 일으킬 만큼 길지 않아야 하므로 지연으로 인한 사용자 영향이 없어야 합니다.</p>
<h3 id="layoutifneeded">layoutIfNeeded()</h3>
<p>layoutIfNeeded는 UIView의 또 다른 메서드로, layoutSubviews 호출을 트리거할 것입니다. 그러나 다음 Update cycle에서 실행되도록 layoutSubviews를 대기열에 넣는 대신, 시스템은 뷰에 레이아웃 업데이트가 필요한 경우 즉시 layoutSubviews를 호출합니다. setNeedsLayout을 호출한 후, 또는 위에서 설명한 자동 layoutSubviews trigger 중 하나를 호출한 후에 layoutIfNeeded를 호출하면 뷰에서 layoutSubviews가 호출됩니다.
그러나 뷰를 다시 그려야 한다는 동작이 시스템에 전달되지 않은 경우 layoutSubviews가 호출되지 않습니다. 동일한 run loop 중간에 레이아웃을 업데이트하지 않고 뷰에 대해 layoutIfNeeded를 두 번 호출하는 경우 두 번째 호출은 layoutSubviews 호출을 트리거하지 않습니다.</p>
<p>layoutIfNeeded를 사용하면 setNeedsLayout과 달리 서브뷰를 배치하고 다시 그리는 작업이 즉시 수행되며, 이 메서드가 반환되기전에 완료됩니다. 이 메서드는 새 레이아웃에 의존해야 하고, 다음 update cycle에서 뷰가 업데이트될 때까지 기다릴 수 없는 경우에 유용합니다. 그러나 이러한 경우가 아니라면 setNeedsLayout을 호출해서 다음 update cycle에 뷰를 업데이트하는 것을 권장합니다.</p>
<p>이 메서드는 Constraints에 대한 변경 사항을 애니메이션할 때 특히 유용합니다. 모든 레이아웃 업데이트가 애니메이션 시작 전에 전파되도록 하려면 애니메이션 블록을 시작하기 전에 layoutIfNeeded를 호출해야 합니다. 새 Constraints를 구성한 다음 애니메이션 블록 내에서 layoutIfNeeded를 다시 호출하여 새 상태로 애니메이션을 적용합니다.</p>
<blockquote>
<ul>
<li>layoutSubviews 호출을 자동으로 trigger하는 몇가지 방법이 있고, 직접 trigger하는 방법이 있다.</li>
</ul>
</blockquote>
<ul>
<li>setNeedsLayout은 다음 Update cycle에 뷰 업데이트를 하도록 요청하는 것이다. 즉, 즉시 업데이트 되지 않고, 다음 Update cycle에 해당 뷰에서 layoutSubviews가 호출되고, 하위뷰에서 연쇄적으로 layoutSubviews가 호출되면서 뷰가 업데이트 됨.</li>
<li>반면에, layoutIfNeeded는 layoutSubviews 호출을 즉시 trigger합니다.</li>
</ul>
<h2 id="display">Display</h2>
<p>View의 Display는 색상, 텍스트, 이미지 및 Core Graphics drawing 등 뷰 및 하위 뷰의 크기 및 위치와 관련이 없는 뷰의 속성이 포함됩니다. </p>
<h3 id="draw_">draw(_:)</h3>
<p>draw 메서드는 layoutSubviews 처럼 동작하지만, 하위 view 들의 draw 메서드를 호출하지는 않습니다.
layoutSubviews와 마찬가지로 draw를 직접 호출해서는 안되며, 대신 run loop 중 다른 지점에서 draw 호출을 트리거하는 메서드를 호출해야 합니다.</p>
<h3 id="setneedsdisplay">setNeedsDisplay()</h3>
<p>이 메서드는 setNeedsLayout에 해당하는 Display 메서드 입니다. 즉, view에 콘텐츠 업데이트가 있었다는 flag 를 설정하지만, 실제로 view를 곧바로 다시 그리지는 않습니다. 
다음 update cycle에서 이 flag가 설정된 모든 view를 검토하고, 해당 view에 draw를 호출합니다.</p>
<p>대부분의 경우 view의 UI 구성 요소를 업데이트하면, flag가 자동으로 설정되고, setNeedsDisplay를 호출할 필요 없이 다음 update cycle에서 view를 redraw 합니다.
그러나 UI 컴포넌트에 직접적으로 연결되지는 않았지만, 업데이트할 때마다 view를 다시 그려야하는 custom drawing은 setNeedsDisplay를 명시적으로 호출해야합니다.</p>
<p>다음 예제는 사용자의 숫자 입력값에 따라 보여지는 도형이 달라지는 로직입니다.</p>
<pre><code class="language-swift">class MyView: UIView {
    var numberOfPoints = 0 {
        didSet {
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        switch numberOfPoints {
        case 0:
            return
        case 1:
            drawPoint(rect)
        case 2:
            drawLine(rect)
        case 3:
            drawTriangle(rect)
        case 4:
            drawRectangle(rect)
        case 5:
            drawPentagon(rect)
        default:
            drawEllipse(rect)
        }
    }
}</code></pre>
<p>입력 값인 numberOfPoints의 값이 변할 때마다 다른 도형을 그려야 하므로 프로퍼티 관찰자에서 setNeedsDisplay를 호출해서 다음 update cycle에 뷰를 redraw 하도록 합니다.</p>
<p>layoutIfNeeded처럼 view에서 즉각적인 콘텐츠 업데이트를 트리거하는 Display 메서드는 없습니다.
일반적으로 view를 다시 그리려면 다음 update cycle에서 하는 것으로도 충분합니다.</p>
<blockquote>
<ul>
<li>Display는 view의 색상, 텍스트, 이미지 등 view의 크기, 위치와 관련 없는 속성값을 포함한다.</li>
</ul>
</blockquote>
<ul>
<li>view의 속성값을 업데이트하는 draw 메서드가 있고, 이는 직접 호출하는 것이 아니라 draw를 트리거하는 메서드를 사용해야 한다.</li>
<li>setNeedsDisplay 는 draw 호출을 트리거하기 위한 메서드이며, 즉시 view를 업데이트하는 것이 아니라 다음 update cycle에 flag를 통해 redraw할 view를 찾고, 해당 view의 draw를 호출한다.</li>
</ul>
<h2 id="constraints">Constraints</h2>
<p>Auto layout에서 view를 배치하고 다시 그리는 데는 3단계가 있습니다.</p>
<blockquote>
<ol>
<li>Constraints 업데이트
: 시스템에서 view에 필요한 모든 constraint를 계산하고 설정</li>
<li>Layout
: Layout 엔진이 view 및 subview의 frame을 계산하고 레이아웃을 배치</li>
<li>Display
: Update cycle의 마지막 단계로, 필요에 따라 draw 메소드를 호출해서 view의 콘텐츠를 다시 그림</li>
</ol>
</blockquote>
<h3 id="updateconstraints">updateConstraints()</h3>
<p>updateConstraints()는 Auto layout을 사용하는 view에서 constraints를 동적으로 변경하는데 사용할 수 있습니다. 직접 호출해서는 안되고, override해서 사용합니다.</p>
<p>정적인 constraints는 interface builder, view의 init 또는 viewDidLoad()에서 지정하고, 동적인 constraints는 updateConstarint 에서 구현해야합니다.</p>
<h3 id="setneedsupdateconstraints">setNeedsUpdateConstraints()</h3>
<p>setNeedsUpdateConstraints 메소드를 호출하면, 다음 update cycle에서 constraints 업데이트가 보장됩니다. 이 메소드는 view의 constraints 중 하나가 업데이트 되었음을 표시하여 updateContraints()를 trigger 합니다.</p>
<h3 id="updateconstraintsifneeded">updateConstraintsIfNeeded()</h3>
<p>이 메소드는 Auto Layout을 사용하는 View의 Constraints를 업데이트 해야한다고 판단되면, 다음 update cycle을 기다리지 않고, 즉시 updateContraints를 trigger 합니다.</p>
<p>View의 Constraints 업데이트는 &quot;constraints update&quot; flag를 통해 업데이트의 필요성을 판단합니다.
이 flag는 자동으로 설정되거나, setNeedsUpdateConstraints, invalidateIntrinsicContentSize 로 설정할 수 있습니다.</p>
<h3 id="invalidateintrinsiccontentsize">invalidateIntrinsicContentSize()</h3>
<p>Auto Layout을 사용하는 일부 View에는 콘텐츠가 주어졌을 때 View의 자연스러운 크기인 intrinsiceContentSize 속성이 있습니다.View의 intrinsicContentSize는 일반적으로 View에 포함된 요소의 constraints에 의해 결정되지만, custom 동작을 제공하기 위해 override 할 수 있습니다.</p>
<p>invalidateIntrinsicContentSize() 를 호출하면, view의 contents가 변경되면 intrinsicContentSize 프로퍼티를 통해 크기 계산을 다시 할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iOS 면접 질문 정리]]></title>
            <link>https://velog.io/@syong_e/iOS-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@syong_e/iOS-%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 01 Aug 2023 06:05:13 GMT</pubDate>
            <description><![CDATA[<p>원래 1개 질문당 1개 게시글로 정리하려고 했는데, 그렇게 하면 블로그에 면접 질문에 대한 게시물로 넘쳐날 것 같아서 그냥 한번에 정리하려고 합니다.</p>
<p>한 번에 모든 질문에 답하지 않고, 계속 업데이트할 예정입니다.</p>
<blockquote>
<p>마지막 수정 일 : 2023.08.26</p>
</blockquote>
<p>iOS 면접 예상 질문 리스트 출처
<a href="https://github.com/JeaSungLEE/iOSInterviewquestions">https://github.com/JeaSungLEE/iOSInterviewquestions</a></p>
<h1 id="ios">iOS</h1>
<h2 id="uikit-클래스들을-다룰-때-꼭-처리해야하는-애플리케이션-쓰레드-이름은-무엇인가">UIKit 클래스들을 다룰 때 꼭 처리해야하는 애플리케이션 쓰레드 이름은 무엇인가?</h2>
<blockquote>
<p>Main Thread 입니다.</p>
</blockquote>
<p>UIKit은 사용자 인터페이스(User Interface)를 구성하기 위해 필요한 기능을 제공해주는 프레임워크이고, UI를 다루기 위해서는 메인 스레드에서 처리해야합니다.</p>
<h2 id="모든-view-controller-객체의-상위-클래스는-무엇이고-그-역할은-무엇인가">모든 View Controller 객체의 상위 클래스는 무엇이고 그 역할은 무엇인가?</h2>
<blockquote>
<p>모든 ViewController의 상위 클래스는 UIViewController 입니다.
UIViewController는 뷰 업데이트, 이벤트 처리, 화면 전환등의 역할을 수행합니다.</p>
</blockquote>
<h2 id="자신만의-custom-view를-만들려면-어떻게-해야하는지-설명하시오">자신만의 Custom View를 만들려면 어떻게 해야하는지 설명하시오.</h2>
<blockquote>
<p>커스텀뷰를 만들기 위해서 2가지 방법이 존재합니다.
Xib 파일을 만들어 UIView를 상속받는 클래스와 연결해주거나 UIView를 상속 받는 클래스를 생성해서 코드로만 구현할 수 있습니다.</p>
</blockquote>
<h2 id="view-객체에-대해-설명하시오">View 객체에 대해 설명하시오.</h2>
<blockquote>
<p>View 객체는 콘텐츠를 화면에 표시하고, 해당 콘텐츠의 터치, 제스쳐등의 상호작용을 정의하는 객체 입니다.</p>
</blockquote>
<h2 id="uiview-에서-layer-객체는-무엇이고-어떤-역할을-담당하는지-설명하시오">UIView 에서 Layer 객체는 무엇이고 어떤 역할을 담당하는지 설명하시오.</h2>
<blockquote>
<p>View가 생성될 때 뷰에 대응하는 layer 객체가 생성됩니다. 
이 때, layer의 타입은 layerClass에 의해서 결정이 되고, default는 CALayer입니다.
CALayer는 CoreAnimation 프레임워크의 핵심 기능을 제공하는 클래스이며, 그래픽 콘텐츠를 관리하고 애니메이션을 처리하는데 사용됩니다. 
Core Animation 프레임워크를 사용하기 때문에 GPU를 사용해 렌더링을 가속화하고, 부드러운 프레임과 애니메이션 효과를 적용할 수 있습니다.</p>
</blockquote>
<p>References</p>
<ul>
<li><a href="https://developer.apple.com/documentation/uikit/uiview/1622436-layer">Apple 공식 문서 - layer</a></li>
<li><a href="https://developer.apple.com/documentation/uikit/uiview/1622626-layerclass">Apple 공식 문서 - layerclass</a></li>
<li><a href="https://developer.apple.com/documentation/quartzcore">Apple 공식 문서 - Core Animation</a></li>
</ul>
<h2 id="ios에서-뷰view와-레이어layer의-개념과-차이점에-대해-설명해보세요">iOS에서 뷰(View)와 레이어(Layer)의 개념과 차이점에 대해 설명해보세요.</h2>
<h4 id="처음-질문을-봤을-때-생각한-내-대답">처음 질문을 봤을 때 생각한 내 대답</h4>
<blockquote>
<p>View는 사용자에게 보여지는 UI요소 즉, 위치와 크기를 정의하며 상호작용을 관리하는데 사용됩니다. 반면에 Layer는 뷰의 모서리, 그림자, 애니메이션과 같은 View의 시각적인 요소를 변경하는데 사용됩니다.</p>
</blockquote>
<h4 id="좀-더-나은-대답">좀 더 나은 대답?</h4>
<blockquote>
<p>View는 화면의 콘텐츠를 관리하는 객체이고 (View의 개념), 
Layer는 렌더링에 사용되는 뷰의 핵심 애니메이션 객체입니다. (Layer의 개념)
View는 콘텐츠의 위치와 크기를 정의하며, 상호작용을 관리하는데 사용합니다.
반면에, Layer는 View의 모서리, 그림자, 애니메이션과 같은 View의 시각적인 요소를 변경하는데 사용됩니다.</p>
</blockquote>
<h2 id="uiwindow의-개념과-역할에-대해서-설명하세요">UIWindow의 개념과 역할에 대해서 설명하세요.</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/1d58c4e7-3899-4fde-95b4-437fe30d3542/image.png" alt=""></p>
<p>Apple 공식 문서에는 다음과 같이 UIWindow를 설명하고 있습니다.</p>
<blockquote>
<p>The backdrop for your app’s user interface and the object that dispatches events to your views.
앱의 사용자 인터페이스를 위한 배경이자, 뷰에 이벤트를 전송하기 위한 객체입니다.</p>
</blockquote>
<p>또한, UIView를 상속받는 class라는 것을 알 수 있습니다.</p>
<p>UIWindow에 대해서 정리하자면, <strong>UIWindow는 UIView의 하위 클래스로 앱의 UI를 담는 컨테이너이자, 뷰에 이벤트를 전달하는 객체</strong>입니다.</p>
<p>좀 더 자세하게 살펴보겠습니다.
<img src="https://velog.velcdn.com/images/syong_e/post/55ccb489-6df1-4c66-88c1-cdb6910aad65/image.png" alt=""></p>
<p>윈도우는 ViewController와 함께 작동하여 이벤트를 처리하고, 앱 작동의 기본이 되는 다른 많은 작업을 수행합니다. UIKit은 대부분의 window 관련 상호 작용을 처리하며, 필요에 따라 다른 객체와 함께 작업하여 많은 앱 동작을 구현합니다.</p>
<p>다음과 같은 작업을 수행해야 할 때만 창을 사용합니다.</p>
<ul>
<li>앱의 콘텐츠를 표시하는 기본 window를 제공할 때</li>
<li>추가 콘텐츠를 표시하기 위해 필요에 따라 추가 window를 생성할 때</li>
</ul>
<p>일반적으로 Xcode는 앱의 기본 창을 제공합니다. 새 iOS 프로젝트는 스토리보드를 사용하여 앱의 View를 정의 합니다. 스토리보드를 사용하려면 AppDelegate 객체에 window 프로퍼티가 있어야 하며, Xcode 템플릿이 이를 자동으로 제공합니다. 스토리보드를 사용하지 않는 경우에는 window 객체를 직접 만들어야 합니다.</p>
<blockquote>
<p>iOS 13 이전까지는 window의 관리를 AppDelegate에서 했지만, Scene 개념이 등장하면서 SceneDelegate에서 window 객체를 관리하게 됐습니다.</p>
</blockquote>
<p>대부분의 앱에는 기기의 메인 화면에 앱의 콘텐츠를 표시하는 window가 하나만 필요합니다. 장치의 기본 화면에 추가 window를 만들 수 있지만, 추가 window는 일반적으로 외부 화면(외부 디스플레이에 장치를 연결해서 사용하는 경우)에 콘텐츠를 표시하는데 사용됩니다.</p>
<p>네... 그렇다네요! 지금까지의 내용을 정리하자면!!</p>
<p>UIWindow는...</p>
<ul>
<li>UIView의 하위 클래스이고, UI를 담는 컨테이너이자 뷰에 이벤트를 전달하는 객체입니다.</li>
<li>ViewController와 함께 작동해서 이벤트를 처리하고, 앱 작동의 기본이 되는 작업을 수행합니다.</li>
<li>앱의 콘텐츠를 표시하는 기본 window를 제공하거나 필요에 의해 추가적인 window를 생성할 때 window를 사용할 수 있습니다.</li>
<li>스토리보드를 사용할 때 SceneDelegate에 window 프로퍼티가 있어야하며, 자동으로 초기화 됩니다.</li>
<li>스토리보드를 사용하지 않고 코드로만 개발하는 경우에는 window 객체를 직접 생성해야 합니다.</li>
<li>일반적으로는 하나의 window로 충분하지만, 외부 디스플레이에 연결하는 등 추가적인 window가 필요할 수 있습니다.</li>
</ul>
<h3 id="storyboard를-사용하지-않을-때-scenedelegate">Storyboard를 사용하지 않을 때 SceneDelegate</h3>
<pre><code class="language-swift">import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: scene)

        let navi = UINavigationController(rootViewController: UIViewController())
        window?.rootViewController = navi

        window?.makeKeyAndVisible()
    }

    ...
}</code></pre>
<p>Storyboard를 사용하지 않을 때에는 위와 같이 window 객체를 직접 생성해야합니다.</p>
<p>window?.makeKeyAndVisible() 메소드는 UIWindow 객체를 화면에 표시하고, 이를 앱의 주 윈도우로 설정하는 데 사용됩니다. </p>
<p>이 메소드에는 두 가지 주요 작업이 포함됩니다</p>
<ul>
<li><p>makeKey: UIWindow 객체를 앱의 &quot;key window&quot;로 만듭니다. 키 윈도우는 특정 이벤트를 구분하고 처리할 수 있는 윈도우로, 앱의 사용자 인터페이스에서 가장 우선적으로 이벤트 처리가 이루어집니다. 스크린에 여러 윈도우가 표시되면, 그 중 하나를 키 윈도우로 설정하여 이벤트 시스템이 정상적으로 작동할 수 있도록 합니다.</p>
</li>
<li><p>makeVisible: UIWindow 객체를 화면에 표시합니다. 이 메소드를 호출하면 윈도우를 화면에 표시하는 작업이 시작되고, 루트 뷰 컨트롤러와 그 하위 뷰들이 화면에 렌더링됩니다.</p>
</li>
</ul>
<h3 id="storyboard를-사용할-때-scenedelegate">Storyboard를 사용할 때 SceneDelegate</h3>
<pre><code class="language-swift">import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
    }

    ...
}</code></pre>
<p>Storyboard를 사용할 때에는 window객체가 자동으로 초기화되기 때문에 UIWindow 프로퍼티를 가지고만 있으면 됩니다.</p>
<h2 id="uinavigationcontroller의-역할이-무엇인지-설명하시오">UINavigationController의 역할이 무엇인지 설명하시오.</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/eb42244c-1f47-41b0-9fef-43541224eba9/image.png" alt=""></p>
<blockquote>
<p>계층적 콘텐츠를 탐색하기 위한 스택 기반 체계를 정의하는 컨테이너 뷰 컨트롤러 입니다.</p>
</blockquote>
<p>다시 말해서 UINavigationController는 UIViewController의 하위 클래스로 다른 뷰 컨트롤러들을 네비게이션 스택을 통해 계층적으로 관리하며 앱 내부의 화면 전환과 관련된 작업을 처리해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/d91e7da0-c6a4-4465-9c3b-f9e982d34633/image.png" alt=""></p>
<p>네비게이션 컨트롤러는 네비게이션 인터페이스에서 하나 이상의 자식 뷰 컨트롤러를 관리하는 <code>컨테이너 뷰 컨트롤러</code>입니다. 이 유형의 인터페이스에서는 한번에 하나의 하위 뷰 컨트롤러만 표시됩니다.</p>
<p>ViewController에서 항목을 선택하면, 새 ViewController가 화면에 Push 되고, 이전 ViewController는 숨겨집니다. 뒤로가기 버튼을 탭하면 현재 ViewController. 즉, 네비게이션 스택의 최상단 ViewController가 제거되고, 그 다음 ViewController가 표시됩니다.</p>
<h2 id="tableview의-동작-방식과-화면에-cell을-출력하기-위해-최소한-구현해야-하는-datasource-메서드를-설명하시오">TableView의 동작 방식과 화면에 Cell을 출력하기 위해 최소한 구현해야 하는 DataSource 메서드를 설명하시오.</h2>
<p>먼저 이 물음에 답하기 전에 질문의 키워드들에 대한 개념 정리를 해보도록 하겠습니다.</p>
<h3 id="tableview">TableView</h3>
<p><img src="https://velog.velcdn.com/images/syong_e/post/38762cb5-ac51-4224-b66a-dc664c176ce1/image.png" alt=""></p>
<blockquote>
<p>UITableView는 UIScrollView의 하위 클래스로 단일 열의 행을 사용하여 데이터를 표시하는 View입니다.</p>
</blockquote>
<p>TableView는 세로로 스크롤 되는 콘텐츠 행을 하나의 열에 표시합니다. 테이블의 각 행에는 앱의 콘텐츠가 하나씩 포함됩니다. 예를 들어 연락처 앱에는 각 연락처의 이름이 별도의 행에 표시되고 설정 앱의 기본 페이지에는 사용 가능한 설정 그룹이 표시됩니다. 하나의 긴 행 목록을 표시하도록 표를 구성하거나 관련 행을 <code>Section</code>으로 그룹화하여 콘텐츠를 더 쉽게 탐색할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/776b8b3d-ce88-4353-a54f-2a3ffbb4c0f7/image.png" alt=""></p>
<p>UITableView는 테이블의 각 행을 표시하는 기본 <code>셀(UITableView Cell 객체)</code>을 제공합니다.
또한, 사용자 지정 셀을 만들어서 원하는 방식으로 콘텐츠를 표시할 수 있고,     <code>Header</code>와 <code>Footer</code> View를 제공해서 셀 그룹에 대한 추가 정보를 제공할 수도 있습니다.</p>
<h3 id="cell">Cell</h3>
<p><img src="https://velog.velcdn.com/images/syong_e/post/0e5b46fe-f5be-42bc-a511-3ba1983eae84/image.png" alt=""></p>
<blockquote>
<p>테이블 뷰에서 하나의 행에 대한 시각적인 표현</p>
</blockquote>
<p>다시 말해서 테이블 뷰의 한 행을 표현하기 위한 View입니다.</p>
<h3 id="datasource">DataSource</h3>
<p><img src="https://velog.velcdn.com/images/syong_e/post/5ae13ee0-2a07-4e8e-b537-c44cd9ae5900/image.png" alt=""></p>
<blockquote>
<p>테이블 뷰의 데이터 소스 역할을하는 객체</p>
</blockquote>
<p>dataSource는 테이블 뷰에 셀을 표시하기 위해서 필요한 데이터 소스를 제공하는 객체이고, UITablewViewDataSource 프로토콜을 채택해야합니다.</p>
<h3 id="tableview-동작-방식">TableView 동작 방식</h3>
<ol>
<li><p>TableView는 UITableViewDataSource 프로토콜을 구현한 객체를 통해 데이터를 제공 받습니다.
주요 메소드로는 numberOfSections(in:), tableView(<em>:numberOfRowsInSection:), tableView(</em>:cellForRowAt:)이 있습니다. dataSource를 통해 섹션, 행 수와 각 행에 표시할 데이터를 알게됩니다.</p>
</li>
<li><p>UITableViewDelegate 프로토콜을 구현해서 사용자 상호작용 및 Cell의 높이, 헤더, 푸터 등의 처리를 할 수 있습니다.</p>
</li>
<li><p>TableView는 효율적인 메모리 관리를 위해 셀 재사용을 지원합니다. 화면에 보이지 않는 셀은 재사용 큐에 저장되며, 새로 보여줄 셀이 필요할 때, 재사용 큐에서 꺼내 재사용합니다. 
재사용은 dequeueReuseableCell(withIdentifier:for:) 메소드를 사용해 이루어집니다.</p>
</li>
</ol>
<h3 id="답변">답변</h3>
<blockquote>
<p>TableView의 동작 방식과 화면에 Cell을 출력하기 위해 최소한 구현해야 하는 DataSource 메서드를 설명하시오.</p>
</blockquote>
<p>TablewView의 동작 방식은...</p>
<ol>
<li>TableView의 <code>dataSource</code>를 구현하여 섹션과 행의 수, 테이블 뷰의 셀을 정의합니다.</li>
<li>TableView는 화면에 표시될 각 셀을 가져오거나, <code>재사용 큐로부터 셀을 재활용</code>합니다.</li>
<li><code>delegate</code>를 통해 사용자 상호작용에 대한 처리를 수행합니다.</li>
</ol>
<p>필수로 구현해야하는 DataSource 메서드는...</p>
<ol>
<li>테이블 뷰의 지정된 섹션에 있는 행의 수를 반환하는 <code>tableView(_:numberOfRowsInSection:)</code></li>
</ol>
<p><img src="https://velog.velcdn.com/images/syong_e/post/1300c820-2947-41e5-b0e8-5e116bb8e084/image.png" alt=""></p>
<ol start="2">
<li>테이블 뷰의 특정 위치에 삽입할 셀을 반환하는 <code>tableView(_:cellForRowAt:)</code></li>
</ol>
<p><img src="https://velog.velcdn.com/images/syong_e/post/7f7df847-fc51-4fa9-9d75-ddca366827df/image.png" alt=""></p>
<h2 id="하나의-view-controller-코드에서-여러-tableview-controller-역할을-해야-할-경우-어떻게-구분해서-구현해야-하는지-설명하시오">하나의 View Controller 코드에서 여러 TableView Controller 역할을 해야 할 경우 어떻게 구분해서 구현해야 하는지 설명하시오.</h2>
<p>TableView의 DataSource의 필수 메소드를 구현할 때, TableView의 이름으로 구분해서 TableView마다 서로 다른 Cell을 구현할 수 있습니다.</p>
<pre><code class="language-swift">func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -&gt; Int {
        return tableView == firstTableView ? 10 : 20
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {

        if tableView == firstTableView {
            // return firstTableView&#39;s Cell
        } else if tableView == secondTableView {
            // return secondTableView&#39;s Cell
        } else {
            fatalError()
        }

}</code></pre>
<h2 id="setneedslayout와-setneedsdisplay의-차이에-대해-설명하시오">setNeedsLayout와 setNeedsDisplay의 차이에 대해 설명하시오.</h2>
<blockquote>
<p>두 메서드 모두 다음 Update Cycle에 뷰를 업데이트하기 위한 메서드 호출을 트리거하는 메서드입니다.
setNeedsLayout은 layoutSubviews를 트리거하고, setNeedsDisplay는 draw를 트리거합니다.
layoutSubviews는 뷰의 위치나 크기 등 레이아웃에 대해 업데이트하고, draw는 뷰의 업데이트 flag를 확인한 후 업데이트가 필요한 뷰에 대해서 뷰를 다시 그립니다.</p>
</blockquote>
<h2 id="stackview의-장점과-단점에-대해서-설명하시오">stackView의 장점과 단점에 대해서 설명하시오</h2>
<blockquote>
<p>stackView를 사용하지 않고도 View마다 Constraints를 설정해서 UI를 배치할 수 있지만, stackView를 사용하면 보다 편리하게 AutoLayout을 지정할 수 있습니다.
다만, stackView로 구현하기 복잡한 레이아웃이나 단순 선형 레이아웃에서 벗어난 레이아웃은 stackView가 제한적일 수 있습니다.</p>
</blockquote>
<h2 id="urlsession에-대해서-설명하시오">URLSession에 대해서 설명하시오.</h2>
<blockquote>
<p>URLSession은 앱에서 서버와 통신을 통해 데이터를 주고 받을 수 있도록 API를 제공하는 클래스입니다.</p>
</blockquote>
<h2 id="prepareforreuse에-대해서-설명하시오">prepareForReuse()에 대해서 설명하시오.</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/8e884fc0-5b72-4c3f-ba64-75508dae409e/image.png" alt=""></p>
<blockquote>
<p>TableView 메서드인 dequeueReusableCell(withIdentifier:)이 반환되기 직전에 prepareForReuse() 메서드가 호출됩니다. 이 메서드의 목적은 셀을 재사용하기 전에 셀의 속성을 초기화하기 위한 것 입니다.</p>
</blockquote>
<h2 id="다크모드를-지원하는-방법에-대해-설명하시오">다크모드를 지원하는 방법에 대해 설명하시오.</h2>
<blockquote>
<p>다크모드를 지원하기 위해서는 라이트 모드와 다크 모드 각각에 대해 동작하는 Color Asset과 Image Asset을 등록해서 대응할 수 있습니다.</p>
</blockquote>
<h2 id="viewcontroller의-생명주기를-설명하시오">ViewController의 생명주기를 설명하시오.</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/5e3f720f-11f7-4aa2-ae46-fb1b38ac340e/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li>loadView: View를 메모리에 올리는 함수</li>
<li>viewDidLoad: View가 메모리에 올라온 후 호출되는 함수</li>
<li>viewWillAppear: View가 화면에 나타나기 전에 호출되는 함수</li>
<li>viewDidAppear: View가 화면에 나타난 후 호출되는 함수</li>
<li>viewWillDisappear: View가 사라지기 전에 호출되는 함수</li>
<li>viewDidDisappear: View가 사라진 후 호출되는 함수</li>
</ul>
<h2 id="tableview와-collectionview의-차이점을-설명하시오">TableView와 CollectionView의 차이점을 설명하시오.</h2>
<table>
<thead>
<tr>
<th>TableView</th>
<th>CollectionView</th>
</tr>
</thead>
<tbody><tr>
<td><img src="https://velog.velcdn.com/images/syong_e/post/49e3a3ab-0c83-4b10-8bfe-bd453757d765/image.png" alt=""></td>
<td><img src="https://velog.velcdn.com/images/syong_e/post/d8e5c7b6-ccaa-4741-994e-4c23e5a3c856/image.png" alt=""></td>
</tr>
</tbody></table>
<blockquote>
<p>둘 모두 재사용 가능한 Cell을 기반으로 여러 데이터를 표현할 수 있는 View입니다. 
TableView는 단일 열의 여러 행으로 Cell이 나열된 형태인 반면에, CollectionView는 다양한 행렬의 형태로 표현할 수 있습니다. 
또한, TableView는 기본적인 Cell의 스타일을 제공하지만, CollectionView는 기본 Cell 스타일을 제공하지 않습니다.</p>
</blockquote>
<h1 id="autolayout">Autolayout</h1>
<h2 id="autolayout을-코드로-작성하는-방법은-무엇인가3가지">Autolayout을 코드로 작성하는 방법은 무엇인가?(3가지)</h2>
<ol>
<li>NSLayoutConstraint</li>
</ol>
<pre><code class="language-swift">NSLayoutConstraint(item: myView, attribute: .leading, relatedBy: .Equal, toItem: view, attribute: .leadingMargin, multiplier: 1.0, constant: 0.0).isActive = true</code></pre>
<ol start="2">
<li>Visual Format Language<pre><code class="language-swift">let views = [&quot;myView&quot;: myView]
let formatString = &quot;|-[myView]-|&quot;
let constraints = NSLayoutConstraint.constraintsWithVisualFormat(formatString, 
 options: .AlignAllTop, 
 metrics: nil, 
 views: views)
</code></pre>
</li>
</ol>
<p>NSLayoutConstraint.activateConstraints(constraints)</p>
<pre><code>3. NSLayoutAnchor

```swift
let margins = view.layoutMarginsGuide
myView.leadingAnchor.constraint(equalTo: margins.leadingAnchor).active = true
myView.trailingAnchor.constraint(equalTo: margins.trailingAnchor).active = true
myView.heightAnchor.constraint(equalTo: myView.widthAnchor, multiplier: 2.0)</code></pre><h2 id="hugging-resistance에-대해서-설명하시오">hugging, resistance에 대해서 설명하시오.</h2>
<blockquote>
<p>Hugging Priority와 Compression Resistance Priority는 뷰의 IntrinsicContentSize에 대한 제약 조건을 설정하는 속성입니다.
Hugging Priority가 높으면 고유 사이즈보다 커지지 않으려하고, Compression Resistance Priority가 높으면, 고유 사이즈보다 작아지지 않으려합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 메인 스레드에서 main.sync는 왜 안될까?]]></title>
            <link>https://velog.io/@syong_e/TIL-%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%97%90%EC%84%9C-main.sync%EB%8A%94-%EC%99%9C-%EC%95%88%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@syong_e/TIL-%EB%A9%94%EC%9D%B8-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%97%90%EC%84%9C-main.sync%EB%8A%94-%EC%99%9C-%EC%95%88%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Sun, 30 Jul 2023 05:22:20 GMT</pubDate>
            <description><![CDATA[<p>이전에 GCD를 사용해서 동시성 프로그래밍을 하는 방법과 동기(Synchronous)와 비동기(Asynchronous) 그리고 직렬(Serial)과 동시(Concurrent)에 대해서 공부했었습니다.</p>
<blockquote>
<p>GCD가 뭔지 모른다면 <a href="https://velog.io/@syong_e/TIL-%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-Grand-Central-Dispatch-GCD">이 글</a>을 참고해주세요!</p>
</blockquote>
<p>그런데 한가지 주의해야할 것이 있습니다.</p>
<blockquote>
<p>메인 스레드에서 Main DispatchQueue에 동기적(Synchronous)으로 작업을 보내면 안된다.</p>
</blockquote>
<p>왜 안되는지 설명하실 수 있나요? 
굳이 알아야하냐고요?</p>
<p>메인 스레드에서 main.sync를 쓸 일이 없긴 하지만, 왜 안되는지 궁금하잖아요.. ㅎㅎ</p>
<p>그럼 먼저 메인 스레드에서 Main DispatchQueue에 비동기적으로 작업을 보내는 코드를 작성해볼까요?</p>
<h2 id="mainasync">main.async</h2>
<pre><code class="language-swift">DispatchQueue.main.async {
// task A
    print(&quot;start&quot;)
    sleep(3)
}
DispatchQueue.main.async {
// task B
    print(&quot;end&quot;)
}</code></pre>
<p>이 코드의 실행 결과는 다음과 같습니다.</p>
<pre><code>start
// 3초 후...
end</code></pre><p>예상대로 실행결과가 나왔나요?</p>
<p>비동기적으로 작업을 큐로 보내버렸죠? 근데 비동기는 작업의 완료를 대기하지 않고 다음 작업을 바로 실행한다고 했었습니다.
그런데 왜 task A의 작업이 끝날때까지 task B의 작업이 실행되지 않았을까요?</p>
<p>메인 스레드가 어떤 작업을 담당하고, 몇 개 있는지, 그리고 직렬과 동시에 대해 이해하고 있다면 답변할 수 있을 것 입니다.</p>
<p>메인 스레드는 UI를 담당하는 1개밖에 없는 스레드죠? 스레드가 1개밖에 없으니 당연히 직렬(Serial)일 수 밖에 없습니다.</p>
<p>이해를 돕기 위해서 그림으로 코드 동작을 확인해보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/e40a8c98-7394-4e85-b7e7-20c2b2f38eb1/image.png" alt=""></p>
<p>비동기적으로 실행했기 때문에 Task A를 Main DispatchQueue에 보내고, Task A의 완료를 대기하지 않은 상태로 바로 다음 Task B를 큐에 보냅니다.</p>
<p>큐는 들어온 순서대로 나가는 FIFO(First In First Out)형태의 자료구조입니다.
Task A가 먼저 큐에 들어왔기 때문에 먼저 스레드에 배치되겠죠?</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/239537d1-f963-4ec7-9146-291faafb90a8/image.png" alt=""></p>
<p>GCD가 적절한 스레드에 Task A를 배치해줄거에요.</p>
<p>우리는 메인 스레드에서 작업을 하기 위해서 Main DispatchQueue에 작업을 보냈죠?
근데 메인 스레드는 1개밖에 없습니다.</p>
<p>그렇기 때문에 메인 스레드에 직렬로 작업이 배치됩니다.
<img src="https://velog.velcdn.com/images/syong_e/post/ac652db5-5fb7-4b1b-812c-0287c550326e/image.png" alt=""></p>
<p>결과적으로 Task B도 메인 스레드에 배치될거에요.
직렬로 1개의 스레드에 배치됐기 때문에 Task A와 Task B는 순차적으로 실행됩니다.</p>
<pre><code class="language-swift">print(&quot;start&quot;)
sleep(3)
print(&quot;end&quot;)</code></pre>
<p>main.async로 메인 스레드에 비동기적으로 작업을 보냈지만, 결국에는 위 코드와 동일한 결과를 출력합니다.</p>
<p>당연한 얘기를 구구절절 한 것 같지만,,
Async와 Serial 각각의 의미에 대해 다시 상기시키는데에는 도움이 되는 것 같습니다.</p>
<p>그렇다면, main.sync를 했을 때에도 main.async와 동일한 결과가 나올까요?</p>
<h2 id="mainsync">main.sync</h2>
<p><img src="https://velog.velcdn.com/images/syong_e/post/4b9e9fa7-c603-4578-a889-f443596c5753/image.png" alt=""></p>
<p>이 코드는 에러를 뿜뿜!합니다</p>
<p>Main Thread에서 Main DispatchQueue에 동기(Sync)로 Task A와 Task B를 보내고 있습니다.
그렇다면, 왜 오류가 발생할까요?</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/62bb2165-8e6e-462c-a538-2ff5cd2a6a37/image.png" alt=""></p>
<p>Main Thread는 Main DispatchQueue에 동기적으로 작업을 보냈습니다. 
즉, Task A가 완료될 때까지 Task B를 수행하지 못하도록 대기합니다.</p>
<p><img src="https://velog.velcdn.com/images/syong_e/post/f8b36e32-bfe1-4e8b-9a56-11c3b9fbc1b1/image.png" alt=""></p>
<p>GCD는 Queue에 있는 Task A를 꺼내서 메인 스레드에 배치하려고 합니다.</p>
<p>그런데, Main Thread는 Task A의 완료를 대기하고 있죠?
GCD는 이러지도 저러지도 못하고, Task A를 메인스레드에 할당하지 못합니다.
Task A가 스레드를 할당받지 못해 작업이 지연되면, 메인 스레드는 작업이 계속 지연되겠죠?</p>
<p>이런 상황을 Dead Lock(교착 상태)라고 합니다.
Dead Lock이 발생하면, 결국 Task A는 실행조차 못하고, 영원히 서로를 기다리는 상황에 빠지게 됩니다.</p>
<pre><code class="language-swift">DispatchQueue.main.sync {
    // task A
    print(&quot;start&quot;)
    sleep(3)
}</code></pre>
<p>그렇기 때문에 이 코드는 교착 상태에 빠질 수 있는 코드이고, 에러가 발생합니다.</p>
<p>하지만, 이는 메인 스레드에서 이 코드를 실행하는 경우에 발생하는 문제이고, 백그라운드 스레드에서 실행한다면 괜찮습니다.</p>
<pre><code class="language-swift">DispatchQueue.global().async {
    DispatchQueue.main.sync {
        // task A
        print(&quot;start&quot;)
        sleep(3)
    }
    DispatchQueue.main.sync {
        // task B
        print(&quot;end&quot;)
    }
}</code></pre>
<p>이렇게요! :)
어렵지 않게 이해할 수 있죠?</p>
<p>우리는 그저</p>
<blockquote>
<p>메인 스레드에서 main.sync를 실행하면 Dead Lock(교착 상태)이 발생하기 때문에 에러가 발생한다. </p>
</blockquote>
<p>라고 알고 있으면 됩니다.</p>
<h3 id="끝">끝!</h3>
]]></description>
        </item>
    </channel>
</rss>