<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>감자주먹밥</title>
    <link>https://justhm.tistory.com/</link>
    <description>IOS 공부 블로그
</description>
    <language>ko</language>
    <pubDate>Sat, 25 Jul 2026 03:41:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>JustHm</managingEditor>
    <image>
      <title>감자주먹밥</title>
      <url>https://tistory1.daumcdn.net/tistory/4804104/attach/6344a6668564409c86168f2e7cced72d</url>
      <link>https://justhm.tistory.com</link>
    </image>
    <item>
      <title>HealthKit 수영 데이터 Fetch 해오기</title>
      <link>https://justhm.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취미로 하는 수영을 가지고 앱을 만들어보자 라는 다짐으로 혼자 프로젝트를 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 기능 중 피트니스 앱 처럼 수영 데이터를 보여주는 기능을 만들기 위해 HealthKit을 사용하게 됐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식문서나 다른 글을 봐도 HealthKit steps 정도만 사용하는 경우가 많아서 이번 기회에 삽질한 내용을 정리합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 프로젝트 생성 후&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-01 22.18.31.png&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsVVP6/btsQZP0Q6nE/kha5LVrcJzjoJDNILxF7Yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsVVP6/btsQZP0Q6nE/kha5LVrcJzjoJDNILxF7Yk/img.png&quot; data-alt=&quot;HealthKit Capability&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsVVP6/btsQZP0Q6nE/kha5LVrcJzjoJDNILxF7Yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsVVP6%2FbtsQZP0Q6nE%2Fkha5LVrcJzjoJDNILxF7Yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1426&quot; height=&quot;480&quot; data-filename=&quot;스크린샷 2025-10-01 22.18.31.png&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;HealthKit Capability&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 App Target으로 이동해서 HealthKit Capability를 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 개의 체크박스는 그냥 데이터를 fetch하는데는 필요가 없기 때문에 그냥 넘어가도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 간략하게 정리하자면.. 질병 정보 읽기 권한 체크, 레코드 갱신시 백그라운드에서도 동작하게 허용하는 체크입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-10-01 22.24.43.png&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s3iGb/btsQ05aLBeE/uoQErt0bsr74MxkX4xxgD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s3iGb/btsQ05aLBeE/uoQErt0bsr74MxkX4xxgD0/img.png&quot; data-alt=&quot;info.plist&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s3iGb/btsQ05aLBeE/uoQErt0bsr74MxkX4xxgD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs3iGb%2FbtsQ05aLBeE%2FuoQErt0bsr74MxkX4xxgD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1686&quot; height=&quot;174&quot; data-filename=&quot;스크린샷 2025-10-01 22.24.43.png&quot; data-origin-width=&quot;1686&quot; data-origin-height=&quot;174&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;info.plist&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;plist에도 권한 체크를 위한 메시지를 설정해야 합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;share, read 중 하나만 사용한다고 해도 둘 다 설정해야 제대로 뜨는걸 삽질하다보니 알아냈습니다 ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제부터는 구현으로 넘어갑니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HealthKit 구현&lt;/h2&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;import HealthKit

final class HealthKitRecordRepositoryImpl: HealthKitRecordRepository {
    private let store: HKHealthStore = HKHealthStore()
    private let read: Set&amp;lt;HKObjectType&amp;gt;

    init(read: Set&amp;lt;HKQuantityType&amp;gt;? = nil) {
        if let read = read {
            self.read = read
        }
        else {
            self.read = Set(
                HealthKitType.allCases.compactMap{HKObjectType.quantityType(forIdentifier: $0.quantityTypeIdentifier)} +
                [HKObjectType.workoutType(), HKSeriesType.workoutRoute()]
            )
        }
    }
...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HealthKit에 접근할 수 있는 &lt;code&gt;HKHealthStore&lt;/code&gt;, fetch 해올 타입을 저장할 &lt;code&gt;HKObjectType&lt;/code&gt;의 집합을 하나 만들어 놓습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;HKObjectType&lt;/code&gt;은 특별한 상황이 아니면 사용하는것만 사용할테니 enum으로 정의 해둔것 사용하게 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 &lt;code&gt;HKObjectType.workoutType()&lt;/code&gt;, &lt;code&gt;HKObjectType.workoutRoute()&lt;/code&gt;는 각각 운동 세션 오버뷰, 운동 경로를 접근할 수 있는 타입입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HKObjectType.workoutRoute()&lt;/code&gt;는 꼭 하지 않아도 됩니다. (추후 OpenWater 운동 데이터 경로를 뽑기위해 넣어둠)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 요청&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;func requestAuthorization() async throws -&amp;gt; Bool {
    guard HKHealthStore.isHealthDataAvailable() else { return false }
    try await store.requestAuthorization(toShare: [], read: read)
    // 요청 후 한 번 더 물어볼 필요가 없는지 확인
    return try await hasReadAuthorization()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;isHealthDataAvailable()&lt;/code&gt; 메서드를 사용해 기기가 HealthKit을 사용할 수 있는지 먼저 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;store.requestAuthorization&lt;/code&gt; 메서드에 읽어올 타입 집합을 넣고 실행시키면, 실제 앱에서 권한체크를 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 return문 에서는 따로 권한체크를 위한 함수를 구현해뒀습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 Fetch 하기위한 작업이었고 진짜 Fetch를 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HealthKit Fetch Quries&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 읽어올 때 사용하는 &lt;b&gt;쿼리&lt;/b&gt;는 여러 종류가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 Swift Concurrency를 지원하지 않은것으로 공부했는데, 이번 프로젝트를 진행하며 보니 Swift Concurrency를 지원하는 쿼리들로 업데이트 되었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여기서는 업데이트 된 쿼리 몇 가지를 비교해보고 사용법을 정리하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 54px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.635659%; height: 18px;&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://developer.apple.com/documentation/healthkit/hksamplequerydescriptor&quot;&gt;HKSampleQueryDescriptor&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.031007%; height: 18px;&quot;&gt;단순히 데이터만 가져옴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.635659%; height: 18px;&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://developer.apple.com/documentation/healthkit/hkstatisticsquerydescriptor&quot;&gt;HKStatisticsQueryDescriptor&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.031007%; height: 18px;&quot;&gt;합계, 평균, 최댓값 등의 통계를 가져옴 (요약용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 22.635659%; height: 18px;&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://developer.apple.com/documentation/healthkit/hkstatisticscollectionquerydescriptor&quot;&gt;HKStatisticsCollectionQueryDescriptor&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 44.031007%; height: 18px;&quot;&gt;일정 단위로 나눠 통계를 시계열 데이터로 가져옴 (차트에 유용)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 증분쿼리, 변경 감지용 이벤트 쿼리 등 여러가지가 있지만, 통계용 화면을 만들기 위한 3가지만 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HKSampleQuery&lt;/h4&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;func fetchDataByDateRange(type: HealthKitType, start: Date, end: Date) async throws -&amp;gt; [HKQuantitySample] {
    try await hasReadAuthorization()

    let swimType = HKQuantityType(type.quantityTypeIdentifier)
    let rangeOfDate = HKQuery.predicateForSamples(withStart: start, end: end, options:
.strictStartDate)

    let descriptor = HKSampleQueryDescriptor(predicates:[.quantitySample(type: swimType,
predicate: rangeOfDate)], sortDescriptors: [.init(\.startDate, order: .forward)])

    return try await descriptor.result(for: store)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수영과 관련된 타입 &lt;code&gt;.distanceSwimming&lt;/code&gt;, &lt;code&gt;.swimmingStrokeCount&lt;/code&gt;을 fetch 하는 코드입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;.distanceSwimming&lt;/code&gt;은 측정된 기록 시간 범위에 수행한 거리가 반환됩니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.swimmingStrokeCount&lt;/code&gt;는 측정된 기록 시간 범위에 스트로크 카운트와 영법 유형이 반환됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입 결정 후 fetch 할 범위를 predicate로 지정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 &lt;code&gt;HKSampleQueryDescriptor&lt;/code&gt; 에 지정한 타입, 조건을 넣어 fetch를 해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 가져오는 방식은 간단하지만 사용할 데이터를 추출하는게 어려웠습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.distanceSwimming 데이터 추출하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 간단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 사용할 데이터는 시간 범위와 측정된 수행 거리만 추출하면 됐습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let distanceResult = try await repository.fetchDataByDateRange(type: .distanceSwimming, start: startDate, end: endDate)

let startDate = distanceResult.first?.startDate
let endDate = distanceResult.first?.endDate
let distance = distanceResult.first?.quantity.doubleValue(for: .meter())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게 정리용으로 작성한 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch를 하면 구간 데이터가 배열로 넘어오게 되고 여기서 구간별 거리 및 시간범위를 추출할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;.swimmingStrokeCount 데이터 추출하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 영법을 추출하는 방법에 대해 엄청나게 검색을 했습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도저히 내부 메서드로는 추출할 수 없는것 같아 공식문서 포함 이곳저곳 검색해보니 &lt;code&gt;metadata&lt;/code&gt;에 Key 값으로 접근해야 데이터를 받을 수 있다는 것을 알게 되었습니다.&lt;/p&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;let strokeResult = try await repository.fetchDataByDateRange(type: .swimmingStrokeCount, start: startDate, end: endDate)

let sourceRevision = strokeResult.first?.sourceRevision.source.name ?? &quot;Unknown device&quot;
let startDate = strokeResult.first?.startDate
let endDate = strokeResult.first?.endDate
let strokeStyle = strokeResult.first?.metadata?[HKMetadataKeySwimmingStrokeStyle] as? Int&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 혹시 &lt;code&gt;metadata[HKMetadataKeySwimmingStrokeStyle]&lt;/code&gt;를 &lt;code&gt;distanceResult&lt;/code&gt; 에서도 뽑을 수 있지 않을까? 했지만, 테스트 해 본 결과 &lt;code&gt;.swimmingStrokeCount&lt;/code&gt;에서만 기록되는 데이터 였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HKMetadataKeySwimmingStrokeStyle 는 어떤 값이 들어올까? 라는게 가장 큰 관건이었는데, 값을 하나하나 피트니스앱과 비교하기 귀찮았기에..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색을 해보니 아래와 같은 데이터를 얻을 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;/**
 @enum          HKSwimmingStrokeStyle
 @abstract      Represents a style of stroke used during a swimming workout.
 */

@available(iOS 10.0, *)
public enum HKSwimmingStrokeStyle : Int, @unchecked Sendable {
    case unknown = 0
    case mixed = 1
    case freestyle = 2
    case backstroke = 3
    case breaststroke = 4
    case butterfly = 5

    @available(iOS 16.0, *)
    case kickboard = 6

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Enum이 이미 존재하기 때문에 하나하나 구분할 필요는 없습니다!&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AppleWatch SE 기준&lt;br /&gt;kickboard 케이스가 매칭되지 않음! (최신 애플워치가 없어서 다른 테스트는 못해봤습니다 ㅠㅠ)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;distanceSwimming 타입에서는 측정 범위와 거리가 존재하지만,&lt;br /&gt;swimStrokeCount 타입에서는 kickboard 사용했을때의 측정데이터가 존재하지 않음&lt;br /&gt;&lt;b&gt;stroke 가 없어서 인 것 같지만, 그래도 이부분 참고&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 데이터 추출까지 확인해봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HKStatisticsQuery&lt;/h4&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;func fetchStatisticsQuery(
    type: HealthKitType,
    unit: HKUnit,
    start: Date,
    end: Date,
    options: HKStatisticsOptions
) async throws -&amp;gt; Double {
    let quantityType = HKQuantityType(type.quantityTypeIdentifier)
    let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
    let descriptor = HKStatisticsQueryDescriptor(
        predicate: .quantitySample(type: quantityType, predicate: predicate),
        options: options
    )
    let stats = try await descriptor.result(for: store)
    // 데이터 추출 후 옵션에 따른 데이터 뽑는 방식
    ... // 아래에서 따로 설명
}
// usage
result.totalActivityBurn = try await repository.fetchStatisticsQuery(type: .activeEnergyBurned, unit: .kilocalorie(), start: start, end: end, options: .cumulativeSum)

result.heartRateMax = try await repository.fetchStatisticsQuery(type: .heartRate, unit: bpmUnit, start: start, end: end, options: .discreteMax)

result.heartRateAvg = try await repository.fetchStatisticsQuery(type: .heartRate, unit: bpmUnit, start: start, end: end, options: .discreteAverage)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fetch 하는법은 비슷하기때문에 설명은 스킵하고,&lt;br /&gt;&lt;code&gt;HKStatisticsQueryDescriptor&lt;/code&gt;에서 반환되는 값을 원하는 방식으로 추출하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보면 &lt;code&gt;options&lt;/code&gt;에 따라 데이터 뽑는 법이 다른것을 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;switch options {
    case .cumulativeSum: // 누적합계
        return stats?.sumQuantity()?.doubleValue(for: unit) ?? 0
    case .discreteAverage: // 샘플들의 평균값
        return stats?.averageQuantity()?.doubleValue(for: unit) ?? 0
    case .discreteMin: // 최솟값
        return stats?.minimumQuantity()?.doubleValue(for: unit) ?? 0
    case .discreteMax: // 최댓값
        return stats?.maximumQuantity()?.doubleValue(for: unit) ?? 0
    case .mostRecent: // 최근 기록된 값
        return stats?.mostRecentQuantity()?.doubleValue(for: unit) ?? 0
    default:
        return 0
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 옵션이 사용되진 않지만, 알아보기 위해서 전부 올려두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합계, 평균값 등에 따라 Fetch 후 사용할 수 있는 내장 메서드가 정의되어 있기때문에 쉽게 사용이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;HKUnit&lt;/code&gt; 은 어떤 포멧으로 값을 추출할지를 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;HKStatisticsCollectionQuery&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;func fetchStatisticsCollectionQuery(
    type: HealthKitType,
    start: Date,
    end: Date,
    options: HKStatisticsOptions,
    interval: DateComponents = DateComponents(minute: 1)
) async throws -&amp;gt; HKStatisticsCollection {

    let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: .strictStartDate)
    let quantityType = HKQuantityType(type.quantityTypeIdentifier)
    let sameplePredicate = HKSamplePredicate.quantitySample(type: quantityType, predicate: predicate)

    let descriptor = HKStatisticsCollectionQueryDescriptor(predicate: sameplePredicate, options: options, anchorDate: start, intervalComponents: interval)
    return try await descriptor.result(for: store)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 차트에서 사용하기 유용한 CollectionQuery 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 Query와 마찬가지로 데이터타입, 가져올 날짜 범위를 설정해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다른점은 &lt;code&gt;interval&lt;/code&gt; 옵션이 있는것입니다.&lt;br /&gt;&lt;code&gt;interval&lt;/code&gt;을 설정하면 특정 범위의 데이터를 interval로 분할하여 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 들면&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let interval = DateComponents(second: 60)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 옵션을 설정하면 데이터가 60초 단위로 통계 데이터가 쌓여 반환됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방법들로 데이터를 활용하면 수영기록에 대한 Overview를 쉽게 만들 수 있습니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;2621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4E9dq/dJMb84Dut6U/oQnrd13LwnM7OuTzMo61AK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4E9dq/dJMb84Dut6U/oQnrd13LwnM7OuTzMo61AK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4E9dq/dJMb84Dut6U/oQnrd13LwnM7OuTzMo61AK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4E9dq%2FdJMb84Dut6U%2FoQnrd13LwnM7OuTzMo61AK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;387&quot; height=&quot;842&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;2621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>iOS</category>
      <category>healthkit</category>
      <category>Swim</category>
      <category>수영</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/99</guid>
      <comments>https://justhm.tistory.com/99#entry99comment</comments>
      <pubDate>Thu, 23 Oct 2025 14:00:38 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] Lv2 귤 고르기</title>
      <link>https://justhm.tistory.com/98</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/138476&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/138476&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761101722318&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/138476&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gq8zR/hyZLiNf6PI/DdZw4o8WMgB235SaK6dyYK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/QbFy5/hyZL1ZdV44/Cm8WZJZ9l2TmjuHncwWmw1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/138476&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/138476&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gq8zR/hyZLiNf6PI/DdZw4o8WMgB235SaK6dyYK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/QbFy5/hyZL1ZdV44/Cm8WZJZ9l2TmjuHncwWmw1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 특정 크기의 귤의 갯수를 구하는게 먼저라 생각해 이것을 먼저 사전 처리하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 종류의 수를 최소화 해야하니 가장 많은 갯수의 귤부터 먼저 다 넣어가면서 차례대로 처리한다면 쉽게 풀 수 있겠다 생각이 들었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법&lt;/h2&gt;
&lt;pre id=&quot;code_1761101876010&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func solution(_ k:Int, _ tangerine:[Int]) -&amp;gt; Int {
    var k = k
    var answer = 0
    var dict = [Int:Int]()
    for item in tangerine { // 크기별 귤 갯수 카운트
        dict[item, default: 0] += 1 
    }

	// 귤 갯수를 내림차순으로 정렬 후 박스에 넣기
    for item in dict.sorted{$0.value &amp;gt; $1.value} {
        k -= item.value
        answer += 1       
        if k &amp;lt;= 0 { break }
    }
    
    return answer
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귤 갯수가 많은 것 부터 넣어야지만 종류를 최소화 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 쉽게 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코퉤엣</category>
      <category>lv2</category>
      <category>귤 고르기</category>
      <category>프로그래머스</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/98</guid>
      <comments>https://justhm.tistory.com/98#entry98comment</comments>
      <pubDate>Wed, 22 Oct 2025 12:00:05 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] Lv2 점프와 순간이동</title>
      <link>https://justhm.tistory.com/97</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12980#&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/12980#&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1761101607138&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12980#&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b83mvi/hyZLiGuCDH/k4P54q7Eecmmds0T0Y1pB0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bfIMs4/hyZLfiHcPI/xxmzk1kFFXiKkdZXBrzkI0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12980#&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12980#&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b83mvi/hyZLiGuCDH/k4P54q7Eecmmds0T0Y1pB0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bfIMs4/hyZLfiHcPI/xxmzk1kFFXiKkdZXBrzkI0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육의 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 부터 12 까지 직접 계산한 결과 규칙이 있는것을 확인했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2로 나눠지면 나눈값에 해당하는 인덱스의 값을 사용&lt;/li&gt;
&lt;li&gt;나머지가 있으면 바로 이전 인덱스의 값에 +1&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 규칙을 찾고서 DP 문제라고 생각해 코드를 작성했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실패한 DP 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면 DP 방식은 효율성 점수 0점.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방식을 사용해야한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;func solution(_ n:Int) -&amp;gt; Int {
    var arr = [0, 1, 1]
    guard n &amp;gt; 2 else { return arr[n] }

    for i in 3...n {
        if i % 2 == 0 {
            arr.append(arr[i / 2])
        }
        else {
            arr.append(arr[i-1] + 1)
        }
    }

    return arr[n]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 10억 이하의 자연수를 범위로 잡고 있기에, 높은 숫자를 다 체크하면서 제한시간을 초과하는것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 다른 방식을 고민해봤다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;통과한 방식 (그리디? 비트 카운팅?)&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;func solution(_ n:Int) -&amp;gt; Int {
    var n = n
    var ans = 1
    while n &amp;gt; 1 {
        if n % 2 == 0 {
            n /= 2
        }
        else {
            n -= 1
            ans += 1
        }
    }
    return ans
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 계산하는게 아닌 N부터 0으로 가는 스텝을 계산한 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지가 있으면 한 걸음, 없으면 텔레포트 하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방식으로 효율성 점수까지 통과했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나누기를 최대한 활용하니까 스킵하는 스텝수가 많아져 효율성을 통과할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 풀이를 보고나니 살짝 이상했다. 이거 비트 카운팅 할때랑 비슷한데?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 몇 개 테스트케이스의 값을 이진수로 바꿔 비트가 1인 갯수만 세어보니 결과와 똑같았다..&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;간단한 다른 해결 방법&lt;/h2&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;func solution(_ n:Int) -&amp;gt; Int {
    return n.nonzeroBitCount
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 스위프트 내장 메서드인 &lt;code&gt;nonzeroBitCount&lt;/code&gt; 를 써서 해결 할 수도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코퉤엣</category>
      <category>lv2</category>
      <category>점프와순간이동</category>
      <category>프로그래머스</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/97</guid>
      <comments>https://justhm.tistory.com/97#entry97comment</comments>
      <pubDate>Wed, 22 Oct 2025 11:54:38 +0900</pubDate>
    </item>
    <item>
      <title>  SwiftData 도입기 비동기로 사용해보기</title>
      <link>https://justhm.tistory.com/96</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1754051136865&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - EST-iOS-TEAM2/Dietto: ESTSoft Final 팀프로젝트&quot; data-og-description=&quot;ESTSoft Final 팀프로젝트. Contribute to EST-iOS-TEAM2/Dietto development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/EST-iOS-TEAM2/Dietto&quot; data-og-url=&quot;https://github.com/EST-iOS-TEAM2/Dietto&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cCYNWN/hyZuxCM4WN/bzZA6mqjRT1tFLR3ulIpR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cS4DYB/hyZuyhoMCN/JI4bkFq18wCEzPeTmoWO1k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/EST-iOS-TEAM2/Dietto&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/EST-iOS-TEAM2/Dietto&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cCYNWN/hyZuxCM4WN/bzZA6mqjRT1tFLR3ulIpR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/cS4DYB/hyZuyhoMCN/JI4bkFq18wCEzPeTmoWO1k/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - EST-iOS-TEAM2/Dietto: ESTSoft Final 팀프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ESTSoft Final 팀프로젝트. Contribute to EST-iOS-TEAM2/Dietto development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 프로젝트에서 내부 DB가 필요할 때 CoreData를 주로 사용했는데&lt;br /&gt;&lt;br /&gt;SwiftData도 알아두면 추후에 마이그레이션 하기 좋을 거 같아 iOS 17 부터 사용할 수 있는 SwiftData를 이번 기회에 도입해보기로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoreData보다 쉽게 모델을 정의하고, 비슷하게 CRUD를 할 수 있는점 덕분에 SwiftData 도입에 어려운것은 없었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;난관에 봉착한 때는 CoreData처럼 BackgroundContext를 이용해 비동기로 CRUD 작업을 해보기 위한 도전부터 시작됐다..&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SwiftData Repository 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftData를 처음 도입할때 CoreData를 도입했을때와 비슷하게 작성하였다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;final class StorageRepositoryImpl&amp;lt;T: PersistentModel&amp;gt;: StorageRepository {

    private let context: ModelContext

    init(modelContainer: ModelContainer) {
        self.context = ModelContext(modelContainer)
    }

...

    func updateData(predicate: Predicate&amp;lt;T&amp;gt;, updateBlock: @escaping (T) -&amp;gt; Void) throws {
        let descriptor = FetchDescriptor&amp;lt;T&amp;gt;(predicate: predicate)
        if let result = try context.fetch(descriptor).first {
            updateBlock(result)
            try context.save()
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 &lt;code&gt;modelContext&lt;/code&gt;를 선언해두고 함수에서 CRUD를 구현하는 방식을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 타입을 Generic으로 한 이유는 여러 모델에 대응하는 하나의 Repository만 두고싶어 사용하게 됐다.&lt;br /&gt;&lt;br /&gt;Generic으로 Repository를 만들면 사용할때는 &lt;code&gt;StorageRepositoryImpl&amp;lt;InterestsDTO&amp;gt;()&lt;/code&gt;와 같이 주입해주면 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이렇게 했을땐 CRUD가 동기적으로 실행된다.&lt;br /&gt;&lt;br /&gt;데이터가 적을때는 Main Thread에서 블락이 일어날 일이 없겠지만, 데이터가 많으면 블락이 생길 수 밖에 없다&lt;br /&gt;&lt;br /&gt;그래서 이번에는 어떻게 하면 비동기로 Thread-safe하게 CRUD 할 수 있을지를 알아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 CoreData는 &lt;code&gt;NSPersistentContainer&lt;/code&gt;에서 &lt;code&gt;newBackgroundContext()&lt;/code&gt;메서드를 통해 비동기 작업을 할 수 있었다.&lt;br /&gt;&lt;br /&gt;하지만 SwiftData에는 ModelContainer에 위와 같은 메서드를 제공하지 않는다.&lt;br /&gt;&lt;br /&gt;그렇다면 이런 방법은 어떨까?&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;Task.detached {
    let object = ...
    await modelContext.insert(object)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 없을거 같았지만 아래와 같은 경고 문구가 떴다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-sendable type &amp;lsquo;ModelContext&amp;rsquo; in implicitly asynchronous access to main actor-isolated property &amp;lsquo;modelContext&amp;rsquo; cannot cross actor boundary&lt;br /&gt;ModelContext는 non-sendable타입인데 암묵적으로 비동기로 접근되었다. 이 속성은 Main Actor에 속해있어 actor 경계를 넘을 수 없다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 사용된 &lt;code&gt;modelContext&lt;/code&gt;는 MainActor의 리소스기 때문에 반드시 MainActor 내에서만 호출해야 안전하다는 경고다.&lt;br /&gt;그래서 위 코드로는 비동기 CRUD를 구현해도 충돌은 안나지만 문제가 생길 수 있기에 더 좋은 방법을 찾아봤다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ModelActor 매크로 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 놓치고 있던 부분! 비동기 작업에 안전성을 보장해야한다.&lt;br /&gt;&lt;br /&gt;일반 class로 생성했을때는 동시성 제어를 자동으로 해주지 않는다.&lt;br /&gt;&lt;br /&gt;ModelContext는 그냥 사용하면 Thread-safe 하지 않기 때문에 동시 접근시 Race Condition이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ModelActor는 이름에도 있듯이 SwiftData를 위한 Actor라고 생각하면 된다.&lt;br /&gt;&lt;br /&gt;기본적으로 actor 기반으로 동작하며 ModelContext의 접근을 직렬화해준다.&lt;br /&gt;&lt;br /&gt;이런 특징 덕분에 Thread-safe한 비동기 작업이 가능해진다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;@ModelActor
actor AnotherStorageRepositoryImpl&amp;lt;T: PersistentModel&amp;gt;: AnotherStorageRepository {

    init() {
        let configure = ModelConfiguration(&quot;\(T.self)&quot;) // 이름 지정
        do {
            let modelContainer = try ModelContainer(for: T.self, configurations: configure)
            self.init(modelContainer: modelContainer)
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    func insertData(data: T) async throws {
        modelContext.insert(data)
        try modelContext.save()
    }

    func updateData(predicate: Predicate&amp;lt;T&amp;gt;, updateBlock: @escaping (T) -&amp;gt; Void) async throws {
        let descriptor = FetchDescriptor&amp;lt;T&amp;gt;(predicate: predicate)
        guard let result = try modelContext.fetch(descriptor).first else { throw StorageError.updateError }
        updateBlock(result)
        try modelContext.save()
    }

    func fetchData(where predicate: Predicate&amp;lt;T&amp;gt;? = nil, sort: [SortDescriptor&amp;lt;T&amp;gt;] = []) async throws -&amp;gt; [T] {
        let descriptor = FetchDescriptor&amp;lt;T&amp;gt;(predicate: predicate, sortBy: sort)
        return try modelContext.fetch(descriptor)
    }

    func deleteData(where predicate: Predicate&amp;lt;T&amp;gt;) async throws {
        try modelContext.delete(model: T.self, where: predicate)
        try modelContext.save()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ModelActor 매크로에는 ModelContext와 Executor가 존재해 따로 Context를 선언을 하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 Container만 잘 생성해서 넘겨주면 된다! (아래는 ModelActor 매크로의 본문 중 일부)&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@available(swift 5.9)
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
extension ModelActor {

    /// The optimized, unonwned reference to the model actor's executor.
    nonisolated public var unownedExecutor: UnownedSerialExecutor { get }

    /// The context that serializes any code running on the model actor.
    public var modelContext: ModelContext { get }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 비동기 작업을 구현할 수 있는 초석이다.&lt;br /&gt;그럼 이제 Container를 주입해주고 초기화하여 사용하면 의도한대로 MainThread가 아닌 다른 Thread에서 동작을 할까?&lt;br /&gt;&lt;br /&gt;&lt;b&gt;NOPE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미자막으로 하나만 해주면 완성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 ModelActor를 초기화 할 때, Background Thread에서 초기화 해준다면 이 후 CRUD 함수를 호출해도 Background Thread에서 동작하는 걸 확인 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Task.detached(priority: .background) { [weak self] in
    self?.userStorageUsecase = UserStorageUsecaseImpl(storage: AnotherStorageRepositoryImpl&amp;lt;UserDTO&amp;gt;())
//Container는 Generic을 정의할 때 자동으로 초기화 후 주입되게 했습니다. (ModelActor 매크로 사용하기 부분 코드 참조)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-01 22.24.14.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zcNz2/btsPFvCk1ir/pLSDufZKaeAHMRPErvT36k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zcNz2/btsPFvCk1ir/pLSDufZKaeAHMRPErvT36k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zcNz2/btsPFvCk1ir/pLSDufZKaeAHMRPErvT36k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzcNz2%2FbtsPFvCk1ir%2FpLSDufZKaeAHMRPErvT36k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1964&quot; data-filename=&quot;스크린샷 2025-06-01 22.24.14.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;그렇다면 마지막으로 Background Thread 동작이 보장되는 이유도 알아보자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;ModelActor를 초기화 할 때는 ModelContext라는 변수를 먼저 초기화 해 준다. 여기서 이 Context가 초기화 시점에 부모 Context를 상속받아 사용하기 때문에, 비동기 컨텍스트 내에서 생성하면 비동기로 동작하는게 보장되는 ModelActor가 생성된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ref&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swift/unownedserialexecutor&quot;&gt;UnownedSerialExecutor Apple dev docs&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swiftdata/modelactor&quot;&gt;ModelActor Apple dev docs&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=VG4oCnQ0bfw&quot;&gt;Concurrency of SwiftData, by Donny Wals&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://syed4asad4.medium.com/power-of-modelactor-in-swiftdata-0053651261bb&quot;&gt;Power of ModelActor in SwiftData&lt;/a&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 자세히 공부하게 된 이유는..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트에서는 비동기로 초기화 후 주입하지 않아도 Background Thread에 할당되기에 너무 이상해서 다른 팀원과 함께 열심히 알아보았다.. (도움을 받은 ㅎㅎ)&lt;br /&gt;결국 샘플 프로젝트를 만들어 실행해보니, Background Thread에서 초기화를 해줘야만 되는것이 맞았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여전히 미스테리다.. 왜 이 프로젝트는 시뮬, 실 기기 모두 백그라운드에서 잘 돌았는가..&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 친구의 기기로 실행해보니 Background Thread에서 초기화 하지 않으면 Main Thread에 할당되는 정상적인 모습 확인. (내 기기가 이상한 거 같다)&lt;/li&gt;
&lt;li&gt;참고로 SwiftData는 auto save가 된다고 했지만, Background Thread에서는 안된다. 그래서 꼭 &lt;b&gt;context.save() 명시 해줘야함&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>iOS/SwiftUI</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/96</guid>
      <comments>https://justhm.tistory.com/96#entry96comment</comments>
      <pubDate>Fri, 1 Aug 2025 21:29:04 +0900</pubDate>
    </item>
    <item>
      <title>iOS - Multi-Threading</title>
      <link>https://justhm.tistory.com/95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi-Threading?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 시간에 복잡하고 많은 작업을 실행하여 사용자에게 좋은 경험을 제공하기 위해,&lt;br /&gt;작업을 여러 프로세스에서 동시에 실행시키는 병렬 처리 방법!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서도 역시 Multi-Threading을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;iOS에서 사용하는 Multi-Threading&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 3 가지!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GCD (Grand Central Dispatch)&lt;/li&gt;
&lt;li&gt;Operation, OperationQueue (NSOperation)&lt;/li&gt;
&lt;li&gt;Swift Concurrency&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 하나씩 알아보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GCD (Grand Central Dispatch)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/dispatch&quot;&gt;https://developer.apple.com/documentation/dispatch&lt;/a&gt;&lt;br /&gt;GCD는...&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS 8.0+&lt;/li&gt;
&lt;li&gt;저수준 큐 기반&lt;/li&gt;
&lt;li&gt;멀티코어 환경과 멀티스레드 환경에서 최적화된 프로그래밍을 할 수 있도록 애플이 개발한 기술&lt;/li&gt;
&lt;li&gt;어떻게 사용할지 (일의 순서, 동기/비동기 처리) 정해주면 스레드를 관리해준다.&lt;/li&gt;
&lt;li&gt;GCD 사용은 Dispatch라는 프레임 워크를 사용하고 &lt;b&gt;DispatchQueue&lt;/b&gt;라는 클래스를 주로 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용법&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 동기, sync 
DispatchQueue.main.sync { ... } 
DispatchQueue.global().sync { ... } 
// 비동기, async 
DispatchQueue.main.async { ... } 
DispatchQueue.global().async { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 이전에 정리했던 노션 글 참조! (야곰 강의 정리본)&lt;br /&gt;&lt;a href=&quot;https://galvanized-jeep-eb7.notion.site/Concurrency-Programming-cc51f4320fd149acab2e44e20d3ba647&quot;&gt;https://galvanized-jeep-eb7.notion.site/Concurrency-Programming-cc51f4320fd149acab2e44e20d3ba647&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Operation, OperationQueue (NSOperation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/foundation/operation&quot;&gt;https://developer.apple.com/documentation/foundation/operation&lt;/a&gt;&lt;br /&gt;Operation은...&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS 2.0+&lt;/li&gt;
&lt;li&gt;GCD를 객체지향적으로 재탄생 시킨것&lt;/li&gt;
&lt;li&gt;동시성 프로그래밍에 관련한 작업을 Operation이라는 객체로 만들어 놓고 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;Operation을 상속받아 커스텀 클래스로 직접 만들거나 &lt;b&gt;BlockOperation&lt;/b&gt;이라는 하위 클래스를 사용해 사용가능&lt;/li&gt;
&lt;li&gt;GCD에도 작업을 객체로 만드는 DispatchWorkItem이 있다... 다른점은?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일단 Operation은 DispatchWorkItem 보다 먼저 나옴&lt;/li&gt;
&lt;li&gt;Operation은 객체지향적으로 설계되어 더 세부적인 스케쥴링 가능&lt;/li&gt;
&lt;li&gt;세부적인것 중 대표적인 부분은 Operation 상태추적을 위한 다양한 프로퍼티가 존재함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용법&lt;/h4&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;let operation = BlockOperation { ... } // 생성
operation.addExecutionBlock { ... } // 첫번째로 초기화 한 위 작업이 끝나면 실행됨.
operation.completionBlock = { ... } // ExecutionBlock이 모두 실행된 뒤 호출되는 Property

operation.start() // 직접 실행하는 방식

let operationQueue = OperationQueue() //OperationQueue에 넣어 실행하는 방식
operationQueue.addOperation(operation)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 내용은 이전에 정리했던 노션 글 참조! (야곰 강의 정리본)&lt;br /&gt;&lt;a href=&quot;https://galvanized-jeep-eb7.notion.site/Operation-3df4b450f95a4db985b5c193838c6b30&quot;&gt;https://galvanized-jeep-eb7.notion.site/Operation-3df4b450f95a4db985b5c193838c6b30&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swift Concurrency&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swift/concurrency&quot;&gt;https://developer.apple.com/documentation/swift/concurrency&lt;/a&gt;&lt;br /&gt;Concurrency는...&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;iOS 13.0+&lt;/li&gt;
&lt;li&gt;구조적 동시성(Structured Concurrency)을 지향&lt;/li&gt;
&lt;li&gt;GCD의 복잡함을(콜백) 줄이고 가독성, 안전성을 보장한 비동기 코드를 작성할 수 있도록 만듦.&lt;/li&gt;
&lt;li&gt;스레드, 큐를 직접 다루지 않아도 비동기 작업을 간단하고 명확하게 구현가능.&lt;/li&gt;
&lt;li&gt;직관적인 문법제공 - &lt;code&gt;async&lt;/code&gt;, &lt;code&gt;await&lt;/code&gt;, &lt;code&gt;Task&lt;/code&gt;, &lt;code&gt;actor&lt;/code&gt; 등의 키워드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 이전 스터디 노션 정리내용으로!&lt;br /&gt;강의영상 저작권 때문에 링크는 X&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 몰랐지만 직접 쓰레드를 생성하는 방법도 있었다!&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;let current = Thread.current
print(&quot;current thread&quot;, current, current.stackSize)

let newTread1 = Thread()
newTread1.name = &quot;secondThread&quot;
print(&quot;secondary thread with default size&quot;, newThread1, newThread1.stackSize)

let newTread2 = Thread()
newThread2.name = &quot;thirdThread&quot;
newThread2.stackSize = 4096 * 512
print(&quot;secondary thread with default size&quot;, newThread2, newThread2.stackSize)


/* Thread class also has...*/
Thread.isMainThread //Bool&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DispatchQueue, OperationQueue의 차이&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&amp;nbsp;&lt;/th&gt;
&lt;th&gt;DispatchQueue&lt;/th&gt;
&lt;th&gt;OperationQueue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;작업 간 의존성 설정&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;td&gt;addDependency 메서드 사용해 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;작업 취소 기능&lt;/td&gt;
&lt;td&gt;수동구현&lt;/td&gt;
&lt;td&gt;cancel로 명시적 취소 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;우선순위 설정&lt;/td&gt;
&lt;td&gt;qos로 제한적 가능&lt;/td&gt;
&lt;td&gt;queuePriority qualityOfService 설정으로 상세하게 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;재사용/재시작/중단&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;td&gt;Operation으로 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결론&lt;/td&gt;
&lt;td&gt;간단한 작업에 적합&lt;/td&gt;
&lt;td&gt;복잡한 작업에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Race Condition&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Race Condition은 두 개이상 스레드가 &lt;b&gt;하나의 자원에 동시에 접근 및 수정하려 할 때 발생하는 문제&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 각 Multi-Threading 방식에서는 어떻게 방지할 수 있는지 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GCD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Serial Queue 사용&lt;/b&gt;&lt;br /&gt;Serial Queue를 사용하면 같은 큐에 실행되는 작업은 순차적으로 실행되기 때문에 동시접근이 발생 안함&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let serialQueue = DispatchQueue(label: &quot;com.example.myQueue&quot;)

serialQueue.async {
    // 공유 자원에 안전하게 접근
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Dispatch Semaphore 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제 과목을 공부하다보면 나오는 Semaphore와 같은 개념&lt;br /&gt;특정 자원에 접근 가능한 스레드 수를 제한하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 스레드가 접근하여 작업을 하는동안 다른 스레드는 대기하다가 들어갈 수 있게 되면 작업을 시작함.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;let semaphore = DispatchSemaphore(value: 1) //접근 가능한 수 설정으로 이해하면 됌
func updateSharedResource() { 
    semaphore.wait() // 접근 시작 
    sharedResource += 1 
    semaphore.signal() // 접근 종료 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Operation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Operation 의존성 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 동시에 실행되지 않게 실행순서를 보장하는 방법&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let op1 = BlockOperation { /* 공유 자원 수정 */ }
let op2 = BlockOperation { /* 이후 작업 */ }

op2.addDependency(op1)
queue.addOperations([op1, op2], waitUntilFinished: false)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NSLock 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lock을 사용해 명시적으로 접근을 제어가능&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;let lock = NSLock()
let operation = BlockOperation {
    lock.lock()
    // 공유 자원 접근
    lock.unlock()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swift Concurrency&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;Actor&lt;/code&gt; 사용&lt;/b&gt;&lt;br /&gt;actor는 내부 상태를 단일 태스크만 접근 가능하도록함.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;actor Counter {
    private var count = 0

    func increment() {
        count += 1
    }

    func value() -&amp;gt; Int {
        return count
    }
}

// 사용
let counter = Counter()
await counter.increment()
let current = await counter.value()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메인 스레드에서 UI 업데이트를 해야 하는 이유는 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 iOS에서 메인 스레드는 &lt;b&gt;UI, event 처리&lt;/b&gt;를 모두 담당하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 사용자 경험 및 일관성을 위해,&lt;br /&gt;프레임워크 자체가 메인스레드에서 안전하게 작동되도록 설계되었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI업데이트를 병렬로 처리한다면 Race Condition이 발생 할 수 있다.&lt;br /&gt;물론 방지하는 방법을 쓰면 괜찮겠지만, 어쩔 수 없이 지연이 생기게 될 것이다.&lt;br /&gt;그렇게되면 일관성 및 사용자 경험이 저해되는 상황이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번외로 메인스레드에서 오래걸리고 복잡한 로직을 (UI아님) 실행하게 된다면&lt;br /&gt;메인 스레드 블락 현상이 발생한다. 그럼 화면 터치도 안되고 앱 자체가 작업이 끝날때 까지 멈추게 된다.&lt;br /&gt;이를 방지하기 위해 메인스레드에서 UI 또는 이벤트 작업 외에는 동시성을 사용하는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/95</guid>
      <comments>https://justhm.tistory.com/95#entry95comment</comments>
      <pubDate>Tue, 29 Jul 2025 18:50:15 +0900</pubDate>
    </item>
    <item>
      <title>의존성 관리 도구 알아보기 (SPM, CocoaPod, Carthage)</title>
      <link>https://justhm.tistory.com/94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;의존성 관리도구가 하는 일&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에 &lt;b&gt;외부 라이브러리&lt;/b&gt;를 쉽게 관리할 수 있게 도와주는 도구!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리 설치, 버전관리, 업데이트 관리, 빌드 통합 등을 도와주는 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서는 대표적으로 3가지 의존성 관리 도구가 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SPM(Swift Package Manager) - First party, git기반, 요즘에는 대부분 라이브러리가 SPM도 지원함&lt;/li&gt;
&lt;li&gt;CocoaPod - third party, obj-c 시절부터 인기있었음, Ruby 기반&lt;/li&gt;
&lt;li&gt;Carthage - third party&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 장단점을 알아보자&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPM (Swift Package Manager)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode 내장(first party도구)이 되어있어 프로젝트에 쉽게 라이브러리를 추가하고 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 나왔을때는 지원하는 라이브러리가 적었지만 지금은 신경안써도 될 정도로 많이 지원하고 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 기반으로 외부 스크립트에 의존이 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obj-c 시절 레거시 라이브러리는 지원이 미비함..&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;CocoaPod&amp;nbsp;&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;obj-c 시절부터 유명한 도구기 때문에 거의 모든 라이브러리가 지원하고있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트에 라이브러리를 가져오고 빌드 설정을 자동으로 해주기때문에 편리하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;업데이트나 프로젝트에 install시 속도가 느리다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다른 pod file간 충돌 또는 workspace가 꼬일 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;xcworkspace로 프로젝트 파일이 자동으로 생겨 바로 쓰면 되긴하지만, 추가적인 부분이 필요하면 나머지 설정도 CocoaPod에 맞게 설정해둬야한다.&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Carthage&lt;/h2&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CocoaPod과 다르게 Xcode 프로젝트를 건드리지 않고 프레임워크만 만들어준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;빌드 링킹 방식을 내가 직접 관리할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가볍고 빠르다!&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;자동으로 빌드는 해주지만 통합은 내가해야한다.. (수동) 프레임워크 링킹을 추가해줘야하는것.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지원하는 라이브러리가 적음&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 SPM에 많이 힘을 쏟고있으니 레거시가 아니라면 SPM을 사용하는편이 관리하기 편하다 생각이든다.&lt;/p&gt;</description>
      <category>iOS</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/94</guid>
      <comments>https://justhm.tistory.com/94#entry94comment</comments>
      <pubDate>Tue, 15 Jul 2025 16:26:53 +0900</pubDate>
    </item>
    <item>
      <title>Swift 고차함수 정리 - map, flatMap, compactMap / filter, reduce, forEach</title>
      <link>https://justhm.tistory.com/81</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;고차함수 (High order function)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 함수를 매개변수로 받거나 반환하는 함수&lt;/b&gt;를 의미한다.&lt;br /&gt;클로저와는 다르다 (클로저는 이름이 없는 익명함수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고차함수가 매개변수로 받는 함수가 클로저일때가 많아서 헷갈릴수는 있지만, 고차함수 안에서 클로저를 자주 사용하는 것 뿐 같은 개념은 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가독성&lt;/li&gt;
&lt;li&gt;함수형 프로그래밍 스타일&lt;/li&gt;
&lt;li&gt;재사용성 높음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;map&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let numbers = [1, 2, 3, 4, 5]
let twoTimesNumbers = numbers.map{ $0*2 }
//[2, 4, 6, 8, 10]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소에 동일한 연산을 적용해 새로운 배열을 반환한다.&lt;br /&gt;&lt;code&gt;for-in&lt;/code&gt;문과 비교했을 때 차이는 없지만, 속도면에서 차이가 있다. (요소가 많을수록 차이 남)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;flatMap&lt;/h2&gt;
&lt;pre class=&quot;lua&quot;&gt;&lt;code&gt;let numbers = [[1, nil], [nil, nil, 2], [3]]
let flatArray = numbers.flatMap{ $0 }
//[Optional(1), nil, nil, nil, Optional(2), Optional(3)]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다차원 배열을 1차원 배열로 flat하게 만들어 반환한다.&lt;br /&gt;Optional을 따로 처리하진 않기 때문에 만약 Optional을 없애고 싶다면 CompactMap을 같이 쓰면 된다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용하다보면 1차원 배열에 flatMap을 써도 옵셔널이 제거되는걸 볼 수 있는데, deprecated 된 것이니까 compactMap을 쓰자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한건 compactMap에 설명&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;compactMap&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;let numbers = [1, nil, 3, nil, 5]
let numbersWithOutNil = numbers.compactMap{ $0 }
//[1, 3, 5]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소 중 nil을 제거한 나머지 요소들을 반환한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nil이 있는 1차원 배열에 FlatMap을 사용해도 compactMap과 동일한 기능을 하는데,&lt;br /&gt;이는 Swift4.1 이전 flatMap이 두 기능(평탄화, 옵셔널제거)을 모두 같이 했었다가&lt;br /&gt;이후에 평탄화만 되는 flatMap, 옵셔널 제거를 위한 compactMap 두 가지로 나눴다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;filter&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let numbers = [1, 2, 3, 4, 5]
let temp = numbers.filter{ $0 % 2 == 0 }
//[2, 4]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소 중 주어진 조건을 만족하는 요소들만 포함하는 새로운 배열을 반환한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;reduce&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
//15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요소를 하나로 합쳐 단일값을 반환한다.&lt;br /&gt;초기값과 결합하는 방법을 파라미터로 넘기면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결합하는 방법은 간단하게 사칙연산자를 사용해도 되고, &lt;code&gt;numbers.reduce(0) {$0 + $1}&lt;/code&gt; 처럼 클로저를 사용하는것도 가능하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ForEach&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let numbers = [1, 2, 3, 4, 5]

numbers.forEach { number in
    print(number)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소에 대해 주어진 작업을 수행한다. 반환값은 없고, 반복문 대용으로 사용하는 편.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;for-in&lt;/code&gt;문과 비슷해보이지만 &lt;code&gt;break&lt;/code&gt;, &lt;code&gt;continue&lt;/code&gt;와 같은 반복문에 들어가는 키워드를 사용할 수 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>iOS/Swift</category>
      <category>high-order-function</category>
      <category>SWIFT</category>
      <category>고차함수</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/81</guid>
      <comments>https://justhm.tistory.com/81#entry81comment</comments>
      <pubDate>Tue, 8 Jul 2025 00:43:45 +0900</pubDate>
    </item>
    <item>
      <title>ARC (자동 참조 카운트) 자세히 알아보기 + 참조 키워드(weak, strong, unowned)</title>
      <link>https://justhm.tistory.com/78</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;mod-frontmatter mod-ui&quot;&gt;
&lt;h2 dir=&quot;auto&quot; data-heading=&quot;정의&quot; data-ke-size=&quot;size26&quot;&gt;정의&lt;/h2&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;ARC(Automatic Reference Counting)는 &lt;span class=&quot;internal-link&quot;&gt;메모리 구조&lt;/span&gt;에서 힙 영역을 관리한다.&lt;br /&gt;Swift에서 힙에 메모리를 할때는 참조타입을 생성할 때 자동으로 할당하게 된다.&lt;br /&gt;ARC는 할당된 메모리가 더 이상 필요하지 않을 때 자동으로 해제한다.&lt;/p&gt;
&lt;blockquote dir=&quot;auto&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서는 new, delete 키워드를 통해 클래스 인스턴스를 생성할 때 메모리를 할당, 해제 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 dir=&quot;auto&quot; data-heading=&quot;특징&quot; data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;컴파일 시점에 언제 참조되고 해제되는지 결정되어 런타임때 그대로 실행된다.&lt;/p&gt;
&lt;h2 dir=&quot;auto&quot; data-heading=&quot;장단점&quot; data-ke-size=&quot;size26&quot;&gt;장단점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;개발자가 참조 해제 시점을 파악할 수 있음&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;RunTime 시점에 추가 리소스가 발생하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;단점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;순환 참조 발생 시 영구적으로 메모리가 해제되지 않을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;MRC? ARC?&quot; data-ke-size=&quot;size23&quot;&gt;MRC? ARC?&lt;/h3&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;둘 다 Reference Counting을 하지만, Manual, Automatic의 차이가 있다.&lt;br /&gt;MRC는 2011년 이전 Objective-C에서 사용했던 방법이다.&lt;br /&gt;C++와 비슷하게 &lt;code&gt;retain&lt;/code&gt;, &lt;code&gt;release&lt;/code&gt; 를 사용해 할당, 해제를 한다.&lt;br /&gt;Obj-C 코드를 건드릴 일이 있다면... 참고&lt;/p&gt;
&lt;h2 dir=&quot;auto&quot; data-heading=&quot;동작 방식&quot; data-ke-size=&quot;size26&quot;&gt;동작 방식&lt;/h2&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;메모리의 참조 횟수를 계산해 참조 횟수가 0이 되면 더 이상 사용하지 않는 메모리라 생각해 해제한다.&lt;br /&gt;참조 횟수를 계산하는 방법을 알아보자&lt;/p&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;참조 횟수가 증가할 때&quot; data-ke-size=&quot;size23&quot;&gt;참조 횟수가 증가할 때&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;인스턴스를 새로 생성할 때&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;기존 인스턴스를 다른 변수에 대입할 때&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;참조 횟수가 감소할 때&quot; data-ke-size=&quot;size23&quot;&gt;참조 횟수가 감소할 때&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;인스턴스가 가리키던 변수가 메모리에서 해제되었을 때&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;nil이 지정되었을 때&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;변수에 다른 값을 대입한 경우&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;프로퍼티의 경우 속해있는 클래스 인스턴스가 메모리에서 해제될 때
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;A클래스 안에 B 클래스 인스턴스가 프로퍼티로 존재할때 각각 인스턴스의 RC가 증가함&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;A 인스턴스를 생성 후 제거 했을 때 A의 프로퍼티인 B 인스턴스도 A 해제 후 해제.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 dir=&quot;auto&quot; data-heading=&quot;순환 참조(retain cycle) &amp;amp; keywords&quot; data-ke-size=&quot;size26&quot;&gt;순환 참조(retain cycle) &amp;amp; keywords&lt;/h2&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;ARC 단점 중 &lt;code&gt;순환참조 발생 시 영구적으로 메모리가 해제되지 않을 수 있다.&lt;/code&gt; 라는게 있었다.&lt;br /&gt;순환 참조는&lt;/p&gt;
&lt;mark&gt;두 개의 객체가 서로를 참조하고 있는 형태다.&lt;/mark&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;먼저 코드로 이해해보자&lt;/p&gt;
&lt;pre class=&quot;language-swift&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;class A { var b: B?}
class B { var a: A?}
// 여기서 A, B의 RC 각각 + 1
var a: A = A()
var b: B = B()
// 여기서 A, B의 RC 각각 + 1
a.b = b
b.a = a
// 최종 A,B의 RC는 각각 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 되면 &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;변수를 nil로 만들어 해제하든 뭘하든 RC는 1 남게된다.&lt;br /&gt;그렇게 memory leak이 발생하게 된다.&lt;/p&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;이런 문제를 해결하기 위해서는 키워드 중 weak, unowned를 사용하면 된다.&lt;/p&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;strong (강한 참조)&quot; data-ke-size=&quot;size23&quot;&gt;strong (강한 참조)&lt;/h3&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;Swift를 사용하면서 인스턴스의 주소값이 변수에 할당될 때 강한참조를 사용한것이다.&lt;br /&gt;인스턴스 생성시 default로 &lt;b&gt;strong&lt;/b&gt; 키워드가 있어 RC를 +1 하게 된다.&lt;/p&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;하지만 강한 참조를 사용하다보면&lt;/p&gt;
&lt;mark&gt;순환참조&lt;/mark&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;가 발생해 문제를 일으킬 수 있다.&lt;/p&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;weak (약한 참조)&quot; data-ke-size=&quot;size23&quot;&gt;weak (약한 참조)&lt;/h3&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;단어 그대로 strong의 반대인 약한 참조다&lt;br /&gt;strong은 default라 사용안했지만 weak의 경우 명시해줘야 한다.&lt;br /&gt;특징을 먼저 알아보고 간단하게 코드를 확인해보자&lt;/p&gt;
&lt;h4 dir=&quot;auto&quot; data-heading=&quot;특징&quot; data-ke-size=&quot;size20&quot;&gt;특징&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;인스턴스 참조시 RC를 증가시키지 않음&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;참조하던 인스턴스가 메모리에서 해제된 경우, 자동으로 nil이 할당돼 메모리가 해제된다.&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;위와 같은 이유로 무조건 Optional 타입의 변수다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 dir=&quot;auto&quot; data-heading=&quot;해제되는 과정&quot; data-ke-size=&quot;size20&quot;&gt;해제되는 과정&lt;/h4&gt;
&lt;pre class=&quot;language-swift&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;class A { weak var b: B?}
class B { var a: A?}
// 여기서 A, B의 RC 각각 + 1
var a: A = A()
var b: B = B()
// A의 RC = 2, B의 RC = 1
a.b = b
b.a = a
// 순환이 끊어져 
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li dir=&quot;auto&quot;&gt;생성시 A, B 각각 +1&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;각 인스턴스에 프로퍼티를 할당할 때도 +1 이지만 weak이 붙은 프로퍼티인 B는 증가하지 않음&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;메모리 해제시 각 RC를 -1하게 됨&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;B의 RC는 해제 직전 1이 였기에 0이되고 메모리에서 제거됨&lt;/li&gt;
&lt;li dir=&quot;auto&quot;&gt;A는 B가 메모리에서 제거됐기 때문에 B가 가지고 있던 A인스턴스가 해제되고 A의 최종 RC도 0이 된다.&lt;br /&gt;생각보다 간단하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;weak을 어디다 선언해도 문제 없을까?&lt;/b&gt;&lt;br /&gt;둘 중 수명이 더 짧은 인스턴스를 가리키는 애를 약한 참조로 선언하면 된다.&lt;br /&gt;아니면 그냥 둘 다 weak으로 하고 예외처리 하거나..&lt;/p&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;unowned (미소유 참조)&quot; data-ke-size=&quot;size23&quot;&gt;unowned (미소유 참조)&lt;/h3&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;weak&lt;/code&gt;과 동일한 특징을 갖는다. (마지막 특징 빼고..) &lt;b&gt;특별한 점&lt;/b&gt;은&lt;/p&gt;
&lt;mark&gt;옵셔널이 아니다&lt;/mark&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하는것이다.&lt;/p&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;만약에&lt;/b&gt; 인스턴스가 메모리에서 해제된 경우에도 unowned는 nil을 할당받지 못하고 계속 해제된 메모리의 주소를 갖고있게 된다.&lt;/p&gt;
&lt;p dir=&quot;auto&quot; data-ke-size=&quot;size16&quot;&gt;그래서 위와 같은 경우에서는 런타임 에러가 생긴다.&lt;/p&gt;
&lt;mark&gt;웬만해서 &lt;b&gt;weak&lt;/b&gt;을 사용하는걸 권장한다&lt;/mark&gt;
&lt;blockquote dir=&quot;auto&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift 5.0 부터 Optional Type으로도 선언 가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 dir=&quot;auto&quot; data-heading=&quot;추가&quot; data-ke-size=&quot;size26&quot;&gt;추가&lt;/h2&gt;
&lt;h3 dir=&quot;auto&quot; data-heading=&quot;Grabage Collection과의 차이&quot; data-ke-size=&quot;size23&quot;&gt;Grabage Collection과의 차이&lt;/h3&gt;
&lt;blockquote style=&quot;color: #0e0e0e; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;&amp;ldquo;ARC는 객체의 참조 카운트를 기반으로 메모리를 관리하는 방식이고, GC는 주기적으로 불필요한 객체를 탐색해 제거하는 방식&lt;/blockquote&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0e0e0e; text-align: start;&quot;&gt;GC는 런타임에서 ARC는 컴파일 타임에서!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #0e0e0e; text-align: start;&quot;&gt;GC는 런타임에서 동작하다보니 추가 리소스가 필요해 성능 저하가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;</description>
      <category>iOS/Swift</category>
      <category>ARC</category>
      <category>Strong</category>
      <category>SWIFT</category>
      <category>unowned</category>
      <category>Weak</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/78</guid>
      <comments>https://justhm.tistory.com/78#entry78comment</comments>
      <pubDate>Tue, 1 Jul 2025 14:39:59 +0900</pubDate>
    </item>
    <item>
      <title>Pinit 셀 재사용 이슈</title>
      <link>https://justhm.tistory.com/93</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;발생했던 문제&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;R1280x0.jpeg&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;2436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diGSKR/btsOSmS7Myl/KMpxZ59xJ6ZoUhy3AxQYu0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diGSKR/btsOSmS7Myl/KMpxZ59xJ6ZoUhy3AxQYu0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diGSKR/btsOSmS7Myl/KMpxZ59xJ6ZoUhy3AxQYu0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiGSKR%2FbtsOSmS7Myl%2FKMpxZ59xJ6ZoUhy3AxQYu0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;259&quot; height=&quot;561&quot; data-filename=&quot;R1280x0.jpeg&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;2436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pinit 에서는 이미지가 있을때는 그대로 보여주고, 이미지가 없다면 기록한 위치의 지도를 스냅샷으로 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지는 저장된걸 바로 보여주지만, 스냅샷은 주소만 보내주고 셀에서 스냅샷 이미지를 로드하기에 몇 초 더 지연시간이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 셀이 엄청 많을때 스크롤을 하면, 재사용 때문에 이전에 사용된 이미지 또는 스냅샷이 먼저 보이고 나중에 제대로 된 스냅샷으로 갈아끼워지는 문제가 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제점 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점은 역시 셀 재사용 매커니즘을 제대로 이해못하고 지연시간을 고려하지 않았기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀이 재사용 될 때 이전 데이터를 그대로 들고 있다는 것 역시 인지하지 못했기 때문에 일어난 문제였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셀이 화면에서 사라지면 셀 자체가 지워지지 않고 이전 데이터 그대로 Reusable Queue에 할당된다. (해제되지 않음)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 해결&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;셀 코드&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;func configure(model: PinEntity) {
    pinDateLabel.text = model.date.formatted()
    pinTitleLabel.text = model.title

    if let image = model.mediaPath {
        thumbnailImageView.image = image
    }
    else {
        captureMapSnapshotWithPin(
            center: CLLocationCoordinate2D(
                latitude: model.latitude,
                longitude: model.longitude
            ),
            imageSize: CGSize(
                width: contentView.frame.height,
                height: contentView.frame.width
            )) { image in
                self.thumbnailImageView.image = image ?? UIImage(systemName: &quot;house&quot;)
            }
    }

    setupLayout()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 else로 들어가면 딜레이가 생기는 것, 썸네일이 제대로 뜨기 전 까진 기본 이미지가 뜨게 만들어뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 봤던것처럼 셀 자체의 데이터가 지워진채로 재사용 되는것이 아니기 때문에, 설정시 데이터를 다시 초기화해주면 쉽게 해결이 가능했다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&amp;gt; UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: &quot;cell&quot;, for: indexPath) as? PinRecordCell
    else { return UICollectionViewCell() }

    cell.thumbnailImageView.image = nil
    cell.configure(model: data[indexPath.row])
    cell.layoutIfNeeded()

    return cell
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;cell.configure&lt;/code&gt; 내에서 초기화해주는게 좋겠지만, 급하게 제출하느라 &lt;code&gt;cellForItemAt&lt;/code&gt; 에서 데이터를 지워줬다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>iOS/UIKit</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/93</guid>
      <comments>https://justhm.tistory.com/93#entry93comment</comments>
      <pubDate>Wed, 25 Jun 2025 16:17:50 +0900</pubDate>
    </item>
    <item>
      <title>☝️커스텀 바텀시트 구현 + PanGesture 로 바텀시트 같은 인터렉션 만들기</title>
      <link>https://justhm.tistory.com/92</link>
      <description>&lt;figure id=&quot;og_1749714992035&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - EST-iOS-TEAM2/Pinit: ESTSoft 2차 팀프로젝트&quot; data-og-description=&quot;ESTSoft 2차 팀프로젝트. Contribute to EST-iOS-TEAM2/Pinit development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/EST-iOS-TEAM2/Pinit&quot; data-og-url=&quot;https://github.com/EST-iOS-TEAM2/Pinit&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/3oGu9/hyY5cU7kCc/NnZ9peY61vgThvrqPhj7k1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/tjjdC/hyY5gwtA4M/PhVSXUkBPvta2NwVXoCUak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/EST-iOS-TEAM2/Pinit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/EST-iOS-TEAM2/Pinit&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/3oGu9/hyY5cU7kCc/NnZ9peY61vgThvrqPhj7k1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/tjjdC/hyY5gwtA4M/PhVSXUkBPvta2NwVXoCUak/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - EST-iOS-TEAM2/Pinit: ESTSoft 2차 팀프로젝트&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;ESTSoft 2차 팀프로젝트. Contribute to EST-iOS-TEAM2/Pinit development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;바텀시트 만들어보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탭바를 가리지 않으면서 바텀시트를 홈화면에서 상시운용을 하려는데, 간단한 방법들로는 불가능한거 같아 직접 바텀시트를 만들게 됐다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_A52AF5C4135C-1.jpeg&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;2436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dodLLW/btsOx65HUFX/nFySPLHHuApVv5u3Vu75C1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dodLLW/btsOx65HUFX/nFySPLHHuApVv5u3Vu75C1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dodLLW/btsOx65HUFX/nFySPLHHuApVv5u3Vu75C1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdodLLW%2FbtsOx65HUFX%2FnFySPLHHuApVv5u3Vu75C1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;214&quot; height=&quot;463&quot; data-filename=&quot;IMG_A52AF5C4135C-1.jpeg&quot; data-origin-width=&quot;1125&quot; data-origin-height=&quot;2436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 검색해보면 엄청난 방법으로 만드는분도 있었지만, 이 프로젝트엔 간단하게만 사용되기에 만드는 방법만 좀 알아보고 적용해봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;바텀시트로 사용될 View&lt;/h2&gt;
&lt;pre class=&quot;roboconf&quot;&gt;&lt;code&gt;final class CustomBottomSheet: UIView {
    var collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init())

    let grabber: UIView = {
        let grabber = UIView()
        grabber.layer.cornerRadius = 4
        grabber.backgroundColor = DesignSystemColor.Lavender.value
        grabber.layer.borderColor = UIColor.white.cgColor
        grabber.layer.borderWidth = 1
        grabber.clipsToBounds = true
        grabber.isUserInteractionEnabled = false //Grabber는 Guide용으로만 사용
        return grabber
    }()

    init() {
        super.init(frame: .zero)
        setupLayout()
    }

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

    func setupLayout() {
        let cornerRadius = 20.0
        self.backgroundColor = .secondarySystemBackground
        self.layer.cornerRadius = cornerRadius
        // cornerRadius 상단 좌,우측만 적용하기
        self.layer.maskedCorners = .init(arrayLiteral: [.layerMaxXMinYCorner, .layerMinXMinYCorner])

        addSubviews(collectionView, grabber)

        grabber.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.top.equalToSuperview().offset(8)
            $0.width.equalTo(100)
            $0.height.equalTo(grabber.layer.cornerRadius * 2)
        }

        collectionView.snp.makeConstraints {
            $0.top.equalToSuperview().offset(cornerRadius*1.8)
            $0.leading.trailing.bottom.equalToSuperview()
        }

        collectionView.isUserInteractionEnabled = false
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 바텀시트로 사용될 뷰를 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grabber를 만들어두긴 했지만, 가이드용으로만 사용하고 실제 인터렉션은 바텀시트 자체에서 처리할것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;isUserInteractionEnabled&lt;/code&gt;를 false로 두어 인터렉션 이벤트에서 아예 제외해버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 CollectionView는 완전 제외한것이 아니다! 가장 높이가 낮을때는 CollectionView는 보이지 않은 상태이니 false인 것,&lt;br /&gt;나중에 높이가 높아지면 인터렉션이 가능하게 변경한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ViewController에 바텀시트 등록하기&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;class HomeViewController: UIViewController {
    // MARK: Variables
    // BottomSheet 동적 높이를 저장하기위한 변수
    private lazy var bottomSheetHeight: CGFloat = view.frame.height / 14
    ...

    // MARK: UI Components
    private var adapter: PinCollectionViewAdapter?
    private let mapView = MKMapView(frame: .zero)
    private let bottomSheet = CustomBottomSheet()

    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        setupProperties()
        ...
    }

    private func setupProperties() {
        view.backgroundColor = .gray
        // 바텀시트 패닝 제스처 추가
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureHandler(_:)))
        bottomSheet.addGestureRecognizer(panGesture)
        // 버튼 액션 추가
        addPinButton.addTarget(self, action: #selector(moveToAddPin), for: .touchUpInside)
        currentLocationButton.addTarget(self, action: #selector(moveToUserLocation), for: .touchUpInside)
    }

    private func setupLayout() {
        view.addSubviews(mapView, addPinButton, currentLocationButton, bottomSheet)
        bottomSheet.snp.makeConstraints {
            $0.leading.trailing.equalToSuperview()
            $0.bottom.equalTo(view.safeAreaLayoutGuide)
            $0.height.equalTo(bottomSheetHeight)
        }
        ...
    }
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;code&gt;bottomSheet&lt;/code&gt;를 선언하고, 높이를 동적으로 관리해주기 위해 &lt;code&gt;bottomSheetHeight&lt;/code&gt;를 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bottomSheet에 PanGesture를 등록해 드래그 했을때 위-아래로 자유롭게 움직일 수 있게 한다.&lt;br /&gt;&lt;code&gt;setupLayout()&lt;/code&gt; 함수를 보면 &lt;code&gt;bottomSheetHeight&lt;/code&gt;변수를 사용해 동적으로 constraint를 잡아주는 걸 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 간단하지만, 조금 어려웠던 PanGesture로 등록한 함수 내용을 보자&lt;br /&gt;여기에서 동적으로 높이를 변경하고 업데이트 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BottomSheet에 사용될 PanGesture 등록하기&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@objc private func panGestureHandler(_ gesture: UIPanGestureRecognizer) {
    // Pretend 크기 설정
    let small = view.frame.height / 14
    let medium = view.frame.height * 0.4
    let large = view.frame.height * 0.8
    // 제스처 시작
    let translation = gesture.translation(in: view)
    let newHeight = bottomSheetHeight - translation.y
    // 제스처 (드래그) 위치에 따라 업데이트
    if newHeight &amp;gt;= small &amp;amp;&amp;amp; newHeight &amp;lt;= large {
        bottomSheetHeight = newHeight
        gesture.setTranslation(.zero, in: view)
    }
    // 제스터 (드래그 끝났을때) 위치에 따라 최종 높이 업데이트
    if gesture.state == .ended {
        // 높이 조정 pretend?
        if newHeight &amp;gt; (view.frame.height * 0.6) {
            bottomSheetHeight = large
            bottomSheet.collectionView.isUserInteractionEnabled = true
        }
        else if newHeight &amp;gt; (view.frame.height * 0.25) {
            bottomSheetHeight = medium
            bottomSheet.collectionView.isUserInteractionEnabled = true
        }
        else {
            bottomSheetHeight = small
            bottomSheet.collectionView.isUserInteractionEnabled = false
        }
    }
    bottomSheet.snp.updateConstraints {
        $0.height.equalTo(self.bottomSheetHeight)
    }
    // Constraint 업데이트 + Animation
    UIView.animate(withDuration: 0.07) {
        self.view.layoutIfNeeded()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 3개의 위치를 만들어두고 그 범위에 들어온다면 자동으로 높이를 조정되게 하고싶어 small, medium, large로 고정 높이를 만들어뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드래그를 시작할때 호출되는 함수기때문에 함수 시작부 부터 &lt;code&gt;gesture.translation.y&lt;/code&gt;로 현재 제스처의 y위치를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 값을 가지고 bottomSheet의 높이가 실시간으로 정해지지만, 초반에 정해둔 small, large를 벗어나면 y값을 갱신하지 않게 만들어뒀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제스처가 끝났을때(손가락을 땠을때)는 현재 bottomSheet Height가 어디에 가까운지에 따라서 small, medium, large가 정해지게 로직을 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 중요한 Constraint 업데이트!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SnapKit을 사용하고 있어서 간편하게 업데이트를 해줬지만, 이것만 하면 제대로 반영되지 않는다.&lt;br /&gt;&lt;br /&gt;꼭 &lt;code&gt;layoutIfNeeded()&lt;/code&gt;를 호출해 다시 그리게 해야하고, 부드러운 효과를 위해 애니메이션에 duration을 달아 처리를 해준다.&lt;br /&gt;이렇게 하지 않으면 제스처에 따라서 확확 따라오기 때문에 일반적으로 사용하던 바텀시트와 사용감이 달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS/UIKit</category>
      <author>JustHm</author>
      <guid isPermaLink="true">https://justhm.tistory.com/92</guid>
      <comments>https://justhm.tistory.com/92#entry92comment</comments>
      <pubDate>Thu, 12 Jun 2025 16:57:17 +0900</pubDate>
    </item>
  </channel>
</rss>