<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>maxcode</title>
    <link>https://wodyios.tistory.com/</link>
    <description>jaelee님의 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 10 Aug 2026 08:13:17 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jaelee</managingEditor>
    <image>
      <title>maxcode</title>
      <url>https://tistory1.daumcdn.net/tistory/4267350/attach/ea594e3c655f4545be4dbd37f3e515ca</url>
      <link>https://wodyios.tistory.com</link>
    </image>
    <item>
      <title>Link Binary with Libraries (Crash - Library not loaded)</title>
      <link>https://wodyios.tistory.com/97</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 LInk Binary with LIbraries와 관련된 트러블 슈팅에 대해서 작성해보려고 해요.&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;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;문제 상황&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;iOS 최소지원 버전이 16이지만 17부터 사용가능한 SwiftData를 이용하여 디버깅 도구를 개발했어요.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;근데, iOS 16 기기에서 앱이 실행되지 않았어요.&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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;빌드는 성공했으나 앱을 키려고 한 순간 아래와 같은 크래시가 났습니다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f6f6f6; color: #0c0d0e; text-align: left;&quot;&gt;&lt;code&gt;dyld[5910]: Library not loaded: /System/Library/Frameworks/SwiftData.framework/SwiftData&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;발생 원인&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Xcode 앱 Target의 Build Phases에 가보면 Target Dependencies와 Link Binary with Libraries를 확인할 수 있어요.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Target Dependencies&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 Target에 필요한 다른 Target들이 있다면 이곳에 추가해주어야 합니다. 현재 Target을 빌드되기 전에 반드시 빌드되어야 하는 다른 타깃을 정의하는 곳이예요. 예를 들어, 다른 프레임워크를 Target으로 추가해두고 현재 Target이 해당 프레임워크를 사용하고 있다면 Target Dependencies에 추가해주어야 해요.&amp;nbsp;&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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Target Depedencies에 추가된 Target들은 항상 앱 Target을 빌드하기 전에 먼저 빌드되기 때문에 항상 최신 상태를 유지할 수 있어요.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Link Binary with Libraries&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재 Target에서 사용되는 외부 라이브러리나 프레임워크들을 연결해주는 단계입니다. 이곳에 들어갈 수 있는 라이브러리는 아래와 같아요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 라이브러리: SnapKit, Alamofire 등&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Apple 기본 프레임워크: Collection, SwiftData, CoreLocation 등&lt;/li&gt;
&lt;li&gt;직접 만든 프레임워크: Custom SPM&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Xcode를 빌드할 때, 현재 Target에서 필요한 코드 심볼들은 Link Binary with Libraries에 정의된 라이브러리들을 사용해야한다고 정의해놓는 곳입니다.&amp;nbsp;&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: #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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Status&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;런타임에 해당 프레임워크가 없을 때 앱이 어떻게 동작해야할 지에 대한 Flag 값입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Required: 프레임워크가 없다면 앱을 실행하지 않는다. launch 단계에서 바로 크래시가 납니다.&lt;/li&gt;
&lt;li&gt;Optional: 프레임워크가 없더라도 앱을 실행한다. (weak linking) 대신 #available과 같은 전처리문을 통해서 코드 처리가 필요해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금 16.0 OS인 기기에서 앱을 실행하면 SwiftData 프레임워크가 없는데 디폴트 설정이 Required이기 때문에 launch단계에서 앱이 죽고 있었어요.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;해결방법&amp;nbsp;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SwiftData.framework를 추가하고 Status를 Optional로 변경합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-20 오후 4.23.56.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wOqON/dJMcaacCB5h/DpF4vJrCjhTJpalMlbN3l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wOqON/dJMcaacCB5h/DpF4vJrCjhTJpalMlbN3l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wOqON/dJMcaacCB5h/DpF4vJrCjhTJpalMlbN3l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwOqON%2FdJMcaacCB5h%2FDpF4vJrCjhTJpalMlbN3l1%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;2074&quot; height=&quot;192&quot; data-filename=&quot;스크린샷 2025-11-20 오후 4.23.56.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/78803238/app-with-min-deployment-ios16-crashes-because-of-swiftdata&quot;&gt;https://stackoverflow.com/questions/78803238/app-with-min-deployment-ios16-crashes-because-of-swiftdata&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763625908048&quot; style=&quot;color: #333333; text-align: start;&quot; contenteditable=&quot;false&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bgR2zj/hyZN3aPzFt/otrz5FiNiokqLQrWkYEzy1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/cELdad/hyZNWqPvac/ZPKMjHiKTekndkquQYCJiK/img.png?width=1858&amp;amp;height=574&amp;amp;face=0_0_1858_574&quot; data-og-url=&quot;https://stackoverflow.com/questions/78803238/app-with-min-deployment-ios16-crashes-because-of-swiftdata&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/78803238/app-with-min-deployment-ios16-crashes-because-of-swiftdata&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-description=&quot;I have an app on the AppStore that targets iOS 16 and later. I would like to introduce support for SwiftData for devices running iOS 17 and later, while maintaining support for iOS 16. Example code:&quot; data-og-title=&quot;App with min. deployment iOS16 crashes because of SwiftData&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://stackoverflow.com/questions/78803238/app-with-min-deployment-ios16-crashes-because-of-swiftdata&quot; data-source-url=&quot;https://stackoverflow.com/questions/78803238/app-with-min-deployment-ios16-crashes-because-of-swiftdata&quot;&gt;
&lt;div style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bgR2zj/hyZN3aPzFt/otrz5FiNiokqLQrWkYEzy1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316,https://scrap.kakaocdn.net/dn/cELdad/hyZNWqPvac/ZPKMjHiKTekndkquQYCJiK/img.png?width=1858&amp;amp;height=574&amp;amp;face=0_0_1858_574');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;App with min. deployment iOS16 crashes because of SwiftData&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;I have an app on the AppStore that targets iOS 16 and later. I would like to introduce support for SwiftData for devices running iOS 17 and later, while maintaining support for iOS 16. Example code:&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.mikegopsill.com/posts/target-dependencies-link-binary/&quot;&gt;https://www.mikegopsill.com/posts/target-dependencies-link-binary/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763625908048&quot; style=&quot;color: #333333; text-align: start;&quot; contenteditable=&quot;false&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://www.mikegopsill.com/posts/target-dependencies-link-binary/&quot; data-og-source-url=&quot;https://www.mikegopsill.com/posts/target-dependencies-link-binary/&quot; data-og-host=&quot;www.mikegopsill.com&quot; data-og-description=&quot;When developing iOS applications with Xcode, it&amp;rsquo;s essential to understand the difference between &amp;ldquo;Target Dependencies&amp;rdquo; and &amp;ldquo;Link Binary with Libraries.&amp;rdquo; These two options found in the Build Phases settings of a target play vital roles in defining&quot; data-og-title=&quot;Target Dependencies vs Link Binary with Libraries&quot; data-og-type=&quot;article&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://www.mikegopsill.com/posts/target-dependencies-link-binary/&quot; data-source-url=&quot;https://www.mikegopsill.com/posts/target-dependencies-link-binary/&quot;&gt;
&lt;div style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Target Dependencies vs Link Binary with Libraries&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;When developing iOS applications with Xcode, it&amp;rsquo;s essential to understand the difference between &amp;ldquo;Target Dependencies&amp;rdquo; and &amp;ldquo;Link Binary with Libraries.&amp;rdquo; These two options found in the Build Phases settings of a target play vital roles in defining&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;www.mikegopsill.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;</description>
      <category>iOS</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/97</guid>
      <comments>https://wodyios.tistory.com/97#entry97comment</comments>
      <pubDate>Thu, 20 Nov 2025 23:30:35 +0900</pubDate>
    </item>
    <item>
      <title>@dynamicMemberLookup</title>
      <link>https://wodyios.tistory.com/95</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;dynamicMemberLookup 말그대로 &lt;b&gt;런타임에 멤버를 바라볼 수 있다는 뜻&lt;/b&gt;이예요.&amp;nbsp;&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;@dynamicMemberLookup을 선언하면,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴파일 타임에 정의하지 않은 프로퍼티라도 subscript(dynamicMember:) 메서드를 통해 런타임에 처리할 수 있도록 도와줘요.&lt;/li&gt;
&lt;li&gt;&quot;dot&quot;을 통해 해당 타입의 프로퍼티에 접근할 수 있어요.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@dynamicMemberLookup을 선언하면 subscript(dynamicMember:) 메서드는 필수적으로 구현해주어야 합니다!&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1763516459213&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@dynamicMemberLookup
struct User {
    var name: String
    var like: String

    subscript(dynamicMember key: String) -&amp;gt; String {
        switch key {
        case &quot;hobby&quot;:
            return like
        default:
            return &quot;none&quot;
        }
    }
}

let member = User(name: &quot;Jaelee&quot;, like: &quot;보드&quot;)

// 사용 방법 1: dot (dynamicMemberLookup 덕분에 가능)
member.hobby

// 사용 방법 2: subscript
member[dynamicMember: &quot;hobby&quot;]&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;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Swift</category>
      <category>S</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/95</guid>
      <comments>https://wodyios.tistory.com/95#entry95comment</comments>
      <pubDate>Wed, 19 Nov 2025 11:30:03 +0900</pubDate>
    </item>
    <item>
      <title>SwiftUI에서 때 놓치기 쉬운 포인트 정리</title>
      <link>https://wodyios.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요새 SwiftUI로 화면을 빠르게 만드는데 재미가 들렸어요. 그치만 아무 생각없이 스유로 만들다보면 놓치기 쉬운 포인트들이 있어요. 이 포인트들을 놓치면 성능이 우려돼요. 체감상 느낄 수 없어도 스유에서 중요한 포인트들이라 한번 정리해봅니다.&amp;nbsp;&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;StateObject 와 ObservedObject 차이 이해하기&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;StateObject&lt;/h4&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;뷰가 생성될 때 객체를 힙 할당하고 id/token을 부여합니다. 뷰가 리렌더링될 때 해당 id/token을 기반으로 기존 인스턴스를 재사용합니다.&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: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SwiftUI는 StateObject 객체를 별도 힙에 객체를 저장하고 identity를 유지하여 단 한번만 생성됩니다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ObservedObject&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;ObservedObject로 선언하게 되면 뷰가 렌더링 될 때마다 힙할당을 하게 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Observable 매크로가 나온 이후 위 선언들은 모두 의미가 없어졌어요. &lt;br /&gt;@Observable로 선언된 인스턴스는 SwiftUI가 자동으로 변화를 감지하고 @State로 선언하면 생명주기를 관리해줘요.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;View body diffing 알고리즘 이해하기&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SwiftUI 렌더링 기본 매커니즘&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SwiftUI는 상태가 변경될 때마다 뷰트리를 새로 생성하여 이전 뷰트리와 비교하여 뷰를 리렌더링합니다.&amp;nbsp;&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. @State, @StateObject 등 상태 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 영향 받는 뷰의 body가 호출되며 뷰트리 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이전 뷰트리와 diffing 알고리즘 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 변경된 부분 UI 업데이트&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SwiftUI에서 성능이 좋다는 것은 위 과정이 막힘없이 이루어진다는 것을 의미해요.&lt;br /&gt;만약 하나의 과정에서 막힌다면 뷰 렌더링이 늦어지고 버벅이게 될 거예요.&amp;nbsp;&lt;br /&gt;&lt;b&gt;View를 최대한 가볍게 만드는 것이 좋습니다!&amp;nbsp;&lt;br /&gt;&lt;/b&gt;만약 무거운 View가 있다면, placeholderView를 만들어두는 것이 좋아요.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;diffing 알고리즘&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View가 들고 있는 property들을 비교합니다.&amp;nbsp;&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;property가 &lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Equatable 하다면,&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;/b&gt;  == 로직을 통해 비교&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Value Type이라면,&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;span&gt; Value 타입 내부를 전부 비교&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Reference Type이라면,&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt; 동일한 인스턴스인지 비교&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;Closure라면,&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&lt;span&gt; 무조건 다른 뷰임&lt;/span&gt;&lt;/span&gt;&lt;/span&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;뷰가 들고 있는 프로퍼티가 변하면 그 뷰는 diffing 알고리즘을 실행하고 이는 뷰 업데이트 로직을 탑니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763392928692&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Test1View: View {
    @State var count: Int = 0

    var body: some View {
        VStack(spacing: 10) {
            TextView(count: count)

            Button1View(count: $count)
            Button2View(count: $count)
        }
        .padding()
    }
}

struct TextView: View {
    let count: Int
    var body: some View {
        Text(&quot;색깔&quot;)
            .background(count % 2 == 0 ? Color.red : Color.green)
    }
}

struct Button1View: View {
    @Binding var count: Int
    var body: some View {
        Button {
            count += 2
        } label: {
            Text(&quot;짝수&quot;)
        }
    }
}&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;위 예제 코드에서 Button1을 계속 클릭하면, TextView는 계속 업데이트돼요.&amp;nbsp;&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;이를 개선하려면 아래처럼 isRed 프로퍼티로 변경하면 이 프로퍼티가 변경될 때만 뷰가 업데이트 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763393036227&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct TextView: View {
    let isRed: Bool
    var body: some View {
        Text(&quot;색깔&quot;)
            .background(isRed ? Color.red : Color.green)
    }
}&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;아래는 Button1을 6번 눌렀을 때 인스트루먼트예요. isRed는 계속 true이기 때문에 TextView는 생성될 때 1번 body가 호출되었습니다. Button2도 count 프로퍼티를 들고 있기 때문에 뷰가 업데이트된 것을 볼 수 있어요.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-11-18 오전 12.24.55.png&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biWYdX/dJMcahv1WdM/WMVj4fbaRkMPgJYcsSAME0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biWYdX/dJMcahv1WdM/WMVj4fbaRkMPgJYcsSAME0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biWYdX/dJMcahv1WdM/WMVj4fbaRkMPgJYcsSAME0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiWYdX%2FdJMcahv1WdM%2FWMVj4fbaRkMPgJYcsSAME0%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;637&quot; height=&quot;243&quot; data-filename=&quot;스크린샷 2025-11-18 오전 12.24.55.png&quot; data-origin-width=&quot;1142&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UIViewRepresentable &amp;nbsp;조심하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UIView를 SwiftUI에서 쓰기 위해선 아래와 같이 위 프로토콜을 채택해주어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1763392172318&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct AttributedText: UIViewRepresentable {
    private let string: String
    private let color: UIColor

    init(string: String, color: UIColor) {
        self.string = string
        self.color = color
    }

    func makeUIView(context: Context) -&amp;gt; UILabel {
        let label = UILabel()
        label.text = string
        label.textColor = color
        return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.textColor = color
    }
}&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;하지만, 위 프로토콜을 채택한 스유뷰는 업데이트가 어느 시점에 되는지 명확히 알기 어려워요.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763392474737&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;struct Test2View: View {
    @State var selectedName: [String] = []
    private let items: [Item] = [
        .init(name: &quot;A&quot;),
        .init(name: &quot;B&quot;),
        .init(name: &quot;C&quot;),
        .init(name: &quot;D&quot;),
        .init(name: &quot;E&quot;),
        .init(name: &quot;F&quot;)
    ]

    struct Item: Hashable {
        let id = UUID()
        let name: String
    }
    var body: some View {
        ScrollView {
            VStack {
                ForEach(items, id: \.id) { item in
                    Button {
                        // 선택한 아이템 추가/제거 로직 구현 
                    } label: {
                        TextView(
                            name: item.name,
                            isSelected: isSelected(item: item)
                        )
                    }
                }
            }
            .padding(.top, 16)
            .padding(.horizontal, 16)
        }
    }

    private func isSelected(item: Item) -&amp;gt; Bool {
        selectedName.contains(item.name)
    }
}

private struct TextView: View {
    let name: String
    let isSelected: Bool

    var body: some View {
        AttributedText(
            string: name,
            color: isSelected ? .blue : .red
        )
    }
}&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;위 Test2View를 Instrument로 돌리고 버튼을 2번 클릭해서 디버깅해보면 아래와 같이 AttributedText가 34번 업데이트 된 것을 볼 수 있어요. Time Profiler로 계속 디버깅해보면 UILabel 내부 attributedString을 변경하는 코드들이 계속 실행되는 것으로 보이는데 이 곳은 개발자들이 건들일 수가 없는 부분으로 보입니다.&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;그래서 최대한 UIViewRepresentable은 사용하지 않는 것이 좋아 보여요!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;1004&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brcr7a/dJMcaf53lIX/aZOL20DWQEnrLB1SdSXU41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brcr7a/dJMcaf53lIX/aZOL20DWQEnrLB1SdSXU41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brcr7a/dJMcaf53lIX/aZOL20DWQEnrLB1SdSXU41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrcr7a%2FdJMcaf53lIX%2FaZOL20DWQEnrLB1SdSXU41%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;2554&quot; height=&quot;1004&quot; data-origin-width=&quot;2554&quot; data-origin-height=&quot;1004&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/94</guid>
      <comments>https://wodyios.tistory.com/94#entry94comment</comments>
      <pubDate>Tue, 18 Nov 2025 00:27:47 +0900</pubDate>
    </item>
    <item>
      <title>StoreKit 2 기반 인앱결제 / 구독 정리하기</title>
      <link>https://wodyios.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;StoreKit 2&lt;/b&gt;를 이용해서 구독을 구현하기 위해 알아야하는 것들을 정리해보려고 합니다.&amp;nbsp;&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;StoreKit을 사용하면 앱에서 제공하는 구독 서비스와 상품들을 간편하고 안전하게 구입할 수 있다고 해요.&amp;nbsp;&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;a href=&quot;https://developer.apple.com/kr/storekit/&quot;&gt;https://developer.apple.com/kr/storekit/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763111541160&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;StoreKit - Apple Developer&quot; data-og-description=&quot;StoreKit을 사용하면 모든 Apple 플랫폼에서 사용자가 앱의 디지털 상품 또는 서비스를 구입할 수 있는 간편하고 안전한 방법을 제공할 수 있습니다.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/kr/storekit/&quot; data-og-url=&quot;https://developer.apple.com/kr/storekit/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bVB0CB/hyZNl43PVx/EdhUGwmMkRnOxajYckYGzK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7R5am/hyZNDiKAPU/GaEm4VOUsNx5Mg4NCk9ovK/img.png?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/kr/storekit/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/kr/storekit/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bVB0CB/hyZNl43PVx/EdhUGwmMkRnOxajYckYGzK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/7R5am/hyZNDiKAPU/GaEm4VOUsNx5Mg4NCk9ovK/img.png?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&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;StoreKit - Apple Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;StoreKit을 사용하면 모든 Apple 플랫폼에서 사용자가 앱의 디지털 상품 또는 서비스를 구입할 수 있는 간편하고 안전한 방법을 제공할 수 있습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인앱 결제로 구입할 수 있는 상품은 크게 2가지 타입으로 나눌 수 있어요.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;앱 내 구입&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성: AppStoreConnect의 수익화 섹션의 인앱결제 메뉴에서 생성할 수 있어요.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;종류: &lt;br /&gt;nonConsumable: &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;영구 소유하는 상품 (ex. 광고 제거, 영구 프리미엄 업그레이드)&lt;br /&gt;&lt;/span&gt;consumable: 구매 후 소비하면 다시 구매 가능한 상품 (ex. 게임 내 코인, 포인트)&lt;/li&gt;
&lt;li&gt;환불&amp;nbsp;&lt;br /&gt;유저가 AppStore 또는 설정에서 환불 요청하고 Apple이 내부 기준에 따라 결제 금액을 환불해줘요.&amp;nbsp;&lt;br /&gt;만약 consuable상품으로 이미 사용한 상품인데 환불 요청을 했다면, 그에 따라 앱 설계가 필요해요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구독&amp;nbsp;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성: AppStoreConnect의 수익화 섹션의 구독 메뉴에서 생성할 수 있어요.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;종류: autoRenewable(자동갱신) / nonRenewable(갱신 취소된 구독)&lt;/li&gt;
&lt;li&gt;취소: 설정/앱스토어에서 유저가 직접 구독 해지&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도메인 모델&amp;nbsp;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Transaction&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 스토어 구매 영수증 트랜잭션 ID에 대한 스냅샷입니다. 상품이 구독이라면 갱신될 때마다 발생해서 id는 새로 생성되지만, originalID는 처음 발생했던 트랜잭션의 ID를 가지고 있어서, 이미 처리했었던 영수증이라는 연속성을 알 수 있습니다.&amp;nbsp;&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;pre id=&quot;code_1763101252874&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;- id(PK): 트랜잭션 ID
- originalID: 첫 구매가 일어난 트랜잭션 ID
- productID: 구매한 상품 ID 
- originalPurchaseDate: 첫 구매 날짜 
- purchaseDate: 구매 날짜 (트랜잭션 발생 날짜) 
- expirationDate: 만료 날짜
- revocationDate: 환불 날짜
- isUpgraded: 업그레이드 여부 (true라면 다른 트랜잭션이 활성화됩니다.)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;VerficationResult&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StoreKit에서 내려주는 트랜잭션 처리 결과입니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763103677730&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum VerificationResult&amp;lt;SignedType&amp;gt; {
    case unverified(SignedType, VerificationResult&amp;lt;SignedType&amp;gt;.VerificationError)
    case verified(SignedType)
}&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;내부적으로 Apple 서명 검증을 이미 수행하기 때문에 개발자는 .verfied 여부만 확인해도 기본적인 보안 검증은 끝나요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 더 높은 수준이 필요하면, VerficationResult안에 들어있는 jwsRepresentation을 이용해 직접 서명 검증(서버 측 검증)을 수행할 수 있다고 해요. (jwsRepresentation을 서버에 보내 Apple 서명 데이터를 직접 검증할 수 있어요)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Transaction.currentEntitlements&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 구매한 각 상품에 대한 최신 트랜잭션의 스트림입니다. consumable 상품과 환불된 상품에 대해서는 포함되지 않아요. 만약 consumable 상품에 대한 트랜잭션을 불러오려면 Transaction.all 또는 Transaction.unfinished를 활용해야 합니다.&amp;nbsp;&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;div style=&quot;background-color: #000000; color: #f5f5f7; text-align: start;&quot; data-v-0e759aa9=&quot;&quot;&gt;
&lt;div data-v-0e759aa9=&quot;&quot; data-v-60225a94=&quot;&quot;&gt;
&lt;div data-v-60225a94=&quot;&quot; data-v-15d7e6ba=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #333336; color: #ffffff;&quot; data-syntax=&quot;swift&quot; data-v-4e049dee=&quot;&quot;&gt;
&lt;div data-v-4e049dee=&quot;&quot;&gt;
&lt;pre class=&quot;swift&quot; data-v-4e049dee=&quot;&quot; data-ke-language=&quot;swift&quot;&gt;&lt;code&gt;func refreshPurchasedProducts() async {
    for await verificationResult in Transaction.currentEntitlements {
        switch verificationResult {
        case .verified(let transaction):
            ...
        case .unverified(let unverifiedTransaction, let verificationError):
            ...
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transaction.updates&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;&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;구매를 구현해보면, updates 스트림을 구현해달라고 보라색 경고가 뜨니 구현이 필수인 듯 해요.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763105858831&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func observeTransactionUpdates() {
    transactionUpdatesTask = Task(priority: .background) { [weak self] in
        logger.info(&quot;트랜잭션 업데이트 관찰 시작&quot;)
        for await update in Transaction.updates {
            guard let self else { return }
            guard let transaction = unwrapVerificationResult(update) else { continue }

            // 영수증 처리
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Product.SubscriptionInfo.Status.updates&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 만료/취소/결제실패 등과 같이 영수증 변화 없이도 구독 상태가 변화할 수 있기 때문에 구독 상태 변화 감지용으로 구현해주어야 해요.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1763106518319&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func observeStatusUpdates() {
    statusUpdateTask = Task { [weak self] in
        logger.debug(&quot;상태 업데이트 관찰 시작&quot;)
        for await status in SubscriptionStatus.updates {
            guard let self,
                  let transaction = await unwrapVerificationResult(status.transaction),
                  let groupIDRawValue = transaction.subscriptionGroupID,
                  let subscriptionGroupID = SubscriptionGroupID(rawValue: groupIDRawValue)
            else {
                continue
            }
            // 구독 상태 변화 처리 
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유저 플로우&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 모든 인앱결제에 대한 처리하는 플로우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(서버에서 구독 상태를 관리해야 한다면 App Store Server API를 이용해야 해서 플로우가 달라져요.)&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구매하기&lt;/h4&gt;
&lt;pre id=&quot;code_1763106705093&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func purchaseProduct(_ pid: String) async {
    do {
        guard let storeProduct = try await Product.products(for: [pid]).first else { return }
        let purchaseResult = try await storeProduct.purchase()

        switch purchaseResult {
        case let .success(transaction):
            // 영수증 처리 
        case .userCancelled:
            logger.info(&quot;구매 요청이 취소되었습니다.&quot;)
        case .pending:
            logger.info(&quot;구매 요청이 대기중입니다.&quot;)
        @unknown default:
            break
        }
    } catch {
        logger.info(&quot;구매 오류&quot;)
    }
}&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;success 콜백으로 영수증 처리를 할 수 있지만, 부모의 허락을 받아야하는 경우 바로 콜백으로 오지 않고 pending으로 떨어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 updates도 꼭 구현이 필요합니다. (보라색 경고가 떠요)&lt;/p&gt;
&lt;pre id=&quot;code_1763106965074&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func observeTransactionUpdates() {
    transactionUpdatesTask = Task(priority: .background) { [weak self] in
        logger.info(&quot;트랜잭션 업데이트 관찰 시작&quot;)
        for await update in Transaction.updates {
            guard let self else { return }
            guard let transaction = unwrapVerificationResult(update) else { continue }

            // 영수증 처리
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;복원하기&lt;/h4&gt;
&lt;pre id=&quot;code_1763107551988&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func restorePurchases() async throws {
    do {
        try await AppStore.sync()
        for await entitlement in Transaction.currentEntitlements {
            guard let transaction = unwrapVerificationResult(entitlement) else { continue }
            // 복원 성공 처리
            await processedTransaction.finish()
        }
    } catch {
        // 복원 실패 처리
    }
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영수증을 AppStore에서 기기로 불러오면, 영수증에 변화가 있다면 Transaction.updates로 들어오는데 없다면 알 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Transaction.currentEntitlements에서 가져와야 합니다.&amp;nbsp;&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;구독 갱신 / 환불 / 취소&lt;/h4&gt;
&lt;pre id=&quot;code_1763108609575&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private func processSubscriptionTransaction(_ transaction: Transaction) async {
    let productType = transaction.productType

    guard productType == .autoRenewable || transaction.productType == .nonRenewable
    else {
        return nil
    }

    if let _ = transaction.revocationDate {
        // 상태 - 환불
    } else if let expirationDate = transaction.expirationDate, expirationDate &amp;lt; Date() {
        // 상태 - 만료
    } else if transaction.isUpgraded {
        // 상태 - Upgraded
        // 다른 활성화된 트랜잭션이 존재합니다.
    } else {
        // 상태 - 구독 / 갱신 / 변경
    }

    await transaction.finish()
}&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;구독 갱신인 경우, 영수증이 변하기 때문에 Transaction.updates 스트림으로 새로운 Transaction이 들어옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 환불 또는 취소인 경우, 실시간으로 반영이 필요하지 않다면 앱을 실행할 때마다 영수증을 검사해서 위와 같은 로직으로 처리해줍니다. 실시간으로 반영이 필요하다면 Transaction.all / Transaciton.unfinished 스트림을 구독해서 별도 처리가 필요합니다.&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;상품 환불&amp;nbsp;&lt;/h4&gt;
&lt;pre id=&quot;code_1763108821349&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private func processNonConsumableTransaction(_ transaction: Transaction) async {
    guard transaction.productType == .nonConsumable else {
        return
    }

    if transaction.revocationDate == nil, transaction.revocationReason == nil {
        ownedNonConsumables.insert(transaction.productID)
    } else {
        ownedNonConsumables.remove(transaction.productID)
    }

    await transaction.finish()
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인 앱 결제 플로우가 StoreKit 1 보다 훨씬 간단해졌어요. StoreKit 내부에서 영수증 검증도 모두 처리되고 앱 외부에서 처리한 구독/환불/취소에 대한 변경사항들을 더 쉽게 처리가 가능해졌어요. 또 콜백으로 async/await을 활용할 수 있어 코드도 더 swift스럽게 작성할 수 있을 것 같네요!&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/transaction/currententitlements&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/storekit/transaction/currententitlements&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763105299741&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;currentEntitlements | Apple Developer Documentation&quot; data-og-description=&quot;A sequence of the latest transactions that entitle a customer to In-App Purchases and subscriptions.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/storekit/transaction/currententitlements&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/storekit/transaction/currententitlements&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bSPoCH/hyZMB7Vtnf/mOgkP3E1UDJeEoujt3GRNK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bpxGsV/hyZNJpJT7r/XhKSztI1TyTqxyrlyrtbN1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/transaction/currententitlements&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/storekit/transaction/currententitlements&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bSPoCH/hyZMB7Vtnf/mOgkP3E1UDJeEoujt3GRNK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bpxGsV/hyZNJpJT7r/XhKSztI1TyTqxyrlyrtbN1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&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;currentEntitlements | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A sequence of the latest transactions that entitle a customer to In-App Purchases and subscriptions.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/verificationresult&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/storekit/verificationresult&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763105382734&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;VerificationResult | Apple Developer Documentation&quot; data-og-description=&quot;A type that describes the result of a StoreKit verification.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/storekit/verificationresult&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/storekit/verificationresult&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6QK9s/hyZNimZYxD/w4xqnGW0PwqaY09JtwTs7K/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dcQYLB/hyZNmXbZ4K/6s8agjxflR1a3Z7WFDHGo1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/verificationresult&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/storekit/verificationresult&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6QK9s/hyZNimZYxD/w4xqnGW0PwqaY09JtwTs7K/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/dcQYLB/hyZNmXbZ4K/6s8agjxflR1a3Z7WFDHGo1/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&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;VerificationResult | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A type that describes the result of a StoreKit verification.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/transaction/updates&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://developer.apple.com/documentation/storekit/transaction/updates&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1763105519743&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;updates | Apple Developer Documentation&quot; data-og-description=&quot;The asynchronous sequence that emits a transaction when the system creates or updates transactions that occur outside the app or on other devices.&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/storekit/transaction/updates&quot; data-og-url=&quot;https://docs.developer.apple.com/documentation/storekit/transaction/updates&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/7y2N6/hyZNCYsBhI/KHR6UsXHGlR0o5UuMvExLK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cfQQpd/hyZNT6XQQk/MXKHq0ddNrAefVvr8chSt0/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/storekit/transaction/updates&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/storekit/transaction/updates&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/7y2N6/hyZNCYsBhI/KHR6UsXHGlR0o5UuMvExLK/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cfQQpd/hyZNT6XQQk/MXKHq0ddNrAefVvr8chSt0/img.jpg?width=1024&amp;amp;height=512&amp;amp;face=0_0_1024_512');&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;updates | Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The asynchronous sequence that emits a transaction when the system creates or updates transactions that occur outside the app or on other devices.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.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;</description>
      <category>iOS</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/93</guid>
      <comments>https://wodyios.tistory.com/93#entry93comment</comments>
      <pubDate>Fri, 14 Nov 2025 17:44:59 +0900</pubDate>
    </item>
    <item>
      <title>가계부 앱 제작기</title>
      <link>https://wodyios.tistory.com/92</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로젝트의 시작 (Project. Strict Money Checking)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호땡이와 함께 분당선 도곡역 테라로사 카페에서 만난 2024년 9월. 이 글을 작성하는 2025년 2월이 된 지금, 그날 어떤 이야기를 나누었는지는 가물가물하다.   하지만 확실한 건, 우리는 소비 습관에 대해 이야기하다가 자연스럽게 &amp;lsquo;가계부 앱을 만들어보자&amp;rsquo;는 얘기로 흘러갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 Swift 6의 Strict Concurrency에 대해 이야기하고 있었다. Strict Concurrency는 컴파일 타임에 데이터 레이스를 방지해 코드의 안정성을 보장하는 새로운 패러다임이다. 그렇다면, Strict Concurrency가 코드의 안정성을 보장하듯 우리의 소비 습관도 체계적으로 관리할 수 있지 않을까? ㅋㅋㅎ 그렇게 우리의 프로젝트가 시작되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;![[스크린샷 2025-02-14 오후 9.29.14.png | 300]]&lt;/p&gt;
&lt;center&gt;&lt;span style=&quot;color: #7f7f7f;&quot;&gt;개발밖에 모르는 우리는 GitHub을 파고 프로젝트 설정을 하자고 했던 기억이 난다. ㅋㅋ&lt;/span&gt;&lt;/center&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자동 등록, 정말 편할까?  &lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;i&gt;&amp;ldquo;요즘 누가 직접 가계부를 써? 토스나 뱅크샐러드에서 카드 내역이 자동 등록되는데!&amp;rdquo;&lt;/i&gt;  &lt;br /&gt;대부분의 핀테크 가계부 앱에서 자동 등록 기능을 제공한다. 하지만 우리는 오히려 &amp;lsquo;자동 등록이 불편하다&amp;rsquo;는 점에서 출발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가계부를 쓰는 이유는 소비를 줄이기 위해서다. 그런데 카드 내역이 자동으로 입력되면 소비를 직접 인지하는 과정이 사라진다. 결국 &amp;ldquo;이번 달에 얼마나 썼지?&amp;rdquo; 하고 돌아보는 순간이 사라지고, 돈이 어디로 빠져나갔는지 흐릿해진다. 자동 등록이 오히려 소비 습관을 통제하는 데 방해가 되는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 기존 가계부 앱이 편리한 것도 아니었다. 여러 소비 내역을 한 번에 등록할 수 없고, 같은 금액을 같은 장소에서 소비해도 매번 일일이 입력해야 했다. 게다가 기능이 너무 많아 오히려 복잡했다. &amp;ldquo;필요한 기능만 담아, 사용자가 직접 소비를 인지할 수 있는 가계부를 만들어보자.&amp;rdquo; 이런 고민 끝에 Strict Money Checking 프로젝트가 시작되었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발 과정에서의 고민&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차별화된 핵심 기능을 제공하려면 기본 기능이 탄탄해야 했다. 그러나 SwiftUI로 한정된 시간 내에 가계부의 필수 기능을 구현하는 것은 쉽지 않았다. (계산을 틀리는 가계부 앱은 만들 수 없으니까 ..  )&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 달력&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가계부의 핵심 기능 중 하나인 달력은 주/월 전환과 좌우 무한 스와이프를 지원해야 했다. 주/월 전환은 비교적 단순했다. SwiftUI는 상태 기반 렌더링을 제공하므로, 표시할 데이터를 주 또는 월 단위로 변경하는 것만으로 자연스럽게 전환이 이루어졌다. 하지만 좌우 무한 스와이프 구현에서는 많은 시행착오를 겪었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 설계는 3개의 뷰를 배치하고 contentOffset을 조정하는 방식이었다. 예를 들어, 3월에서 2월로 스와이프하면 뷰의 위치를 초기화하고 데이터를 갱신해 자연스럽게 이어지는 것처럼 보이게 했다. 하지만 SwiftUI의 onScrollPhaseChange만으로는 스크롤 타이밍을 완벽하게 제어할 수 없어 빠른 스와이프 시 문제가 발생했다. 결국 UIPageViewController 기반 설계로 방향을 전환했다. UIViewControllerRepresentable을 활용해 SwiftUI에서 사용할 수 있도록 감싸고, 좌우 페이지를 동적으로 생성해 무한 스크롤을 구현했다. 최종적으로 안정적인 동작을 확보했지만, 첫 번째 접근법을 최적화하는 데 너무 많은 시간을 쏟은 점은 아쉬움으로 남았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[!warning] contentOffset 초기화 타이밍을 맞추기 위해 onScrollPhaseChange 뷰모디파이어에서 oldPhase == .decelerating &amp;amp;&amp;amp; newPhase == .idle 시점을 활용했으나, 빠르게 스와이프할 경우 정상적으로 감지되지 않는 문제가 있었다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬리제이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한국뿐만 아니라 해외 가계부 시장도 궁금했다. 그래서 처음부터 영어와 일본어 로컬라이제이션을 함께 진행했다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문자&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Xcode 15부터는 String Catalog를 통해 로컬라이제이션을 쉽게 적용할 수 있다. 우리는 한국어를 key 값으로 사용하고, 번역은 ChatGPT를 활용했다. 문자열 로컬라이제이션 자체는 큰 고민 없이 진행했지만, 결국 수작업이 많았다. (노가다였음  )&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;날짜&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜 형식은 언어에 따라 다르게 표시된다. 예: 2024년 12월 24일 (금) &amp;rarr; December 24, 2024 (Fri)&lt;br /&gt;처음에는 DateFormatter의 locale을 현재 국가에 맞추고, setLocalizedDateFormatFromTemplate을 활용하면 해결될 것이라 생각했다. 하지만 국가별로 세부적인 분기가 필요해 복잡해졌다. 이를 해결하기 위해 String Catalog에 &amp;ldquo;현재 날짜&amp;rdquo; key를 추가하고, 각 언어별 형식을 미리 설정한 후, dateFormat으로 적용했다.&lt;/p&gt;
&lt;h5&gt;년/월/일 설정&lt;/h5&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// String Catalog 설정 
// 한국: yyyy년 M월 d일 (E)
// 영어: MMM d, yyyy (E)
// 일본어: yyyy年 M月 d日 (E)

let formatter = DateFormatter()
formatter.locale = Locale(identifier: Locale.preferredLanguages.first!)
formatter.dateFormat = String(smcKey: &quot;현재날짜&quot;, table: .recordSpending)
return formatter&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;요일 설정&lt;/h5&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let formatter = DateFormatter()
formatter.locale = Locale(identifier: Locale.preferredLanguages.first!)
formatter.setLocalizedDateFormatFromTemplate(&quot;E&quot;)
return formatter&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;금액 단위&lt;/h4&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;한국 앱스토어에서 다운로드 &amp;rarr; 기본 통화는 원(₩)&lt;/li&gt;
&lt;li&gt;일본 앱스토어에서 다운로드 &amp;rarr; 기본 통화는 엔(&amp;yen;)&lt;/li&gt;
&lt;li&gt;미국 앱스토어에서 다운로드 &amp;rarr; 기본 통화는 달러($)&lt;/li&gt;
&lt;li&gt;이후 언어 변경 시에도 금액 단위는 유지&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;추가적으로, 달러($)는 소수점 두 자리까지 표기해야 한다는 점을 늦게 깨달았다. 이를 반영하기 위해 금액 단위를 Int에서 Double로 변경하고, 영어권 사용자를 위해 금액 입력 키보드에서 소수점 입력을 지원하도록 UI도 수정했다. (근데, 영어를 지원하는 다른 가계부 앱들은 왜 대부분 소수점 입력을 제공하지 않을까..  )&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;돌아보며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱을 배포한 지 일주일이 지났고, 현재까지 약 30명이 다운로드했으며 일일 평균 9명이 사용 중이다. 별다른 마케팅 없이 이 정도의 유입이 있었다는 점에서, 실제 사용자들에게 가치 있는 앱을 만들었다는 점이 의미가 있다고 생각한다. 아직은 기본적인 기능만 제공하고 있지만, 앞으로 지속적인 개선을 통해 완성도를 높여갈 계획이다. 특히, AB 테스트를 돌려보며 다양한 시도를 해보고 싶다. 직접 원하는 방향으로 기능을 실험하고 최적화하는 과정도 하나의 재미있는 도전이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트는 1.0 버전 배포까지 비교적 순조롭게 진행되었지만, 개인적으로 바쁜 시기가 있어 약 1~2주간 개발을 진행하지 못한 적이 있었다. 다행히도, 함께하는 동료들의 열정적인 모습에 다시 동기부여를 얻어 집중할 수 있었다. 이번 경험을 통해, 주변의 좋은 동료들이 얼마나 중요한지 다시 한번 깨달았다. 앞으로도 함께 성장하며 더 나은 서비스를 만들어가고 싶다! 잘 부탁해요  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 SwiftUI는 코드 단 몇 줄로 멋진 애니메이션과 UI를 구현할 수 있는 강력한 도구다. 잘 활용해서 아름답고 직관적인 앱을 만들어보자!!&lt;/p&gt;</description>
      <category>Retrospect</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/92</guid>
      <comments>https://wodyios.tistory.com/92#entry92comment</comments>
      <pubDate>Wed, 19 Feb 2025 23:20:55 +0900</pubDate>
    </item>
    <item>
      <title>@Published protocol에 정의하기</title>
      <link>https://wodyios.tistory.com/78</link>
      <description>&lt;h1 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hv_highlighter&quot;&gt;문제 상황&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine으로 작성한 코드를 테스트하는 중인데 @Published 프로퍼티 래퍼가 말썽입니다.&amp;nbsp;&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;ViewModel에 @Published 프로퍼티 래퍼 타입을 정의하고 ViewController에서 이 타입을 바인딩하여 ViewModel에 변화가 일어나면 &amp;nbsp;ViewController가 감지할 수 있게 하기 위해서 사용했습니다.&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 책 목록을 보여주는 화면의 로직을 담당하는 ViewModel과 이를 바인딩하고 있는 ViewController입니다.&amp;nbsp;&lt;b&gt;bookList&lt;/b&gt;에 변화가 일어난다면 &lt;b&gt;bookList&lt;/b&gt; publisher는 &lt;b&gt;BookListViewController&lt;/b&gt;에 UI를 업데이트하라고 전달합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667575559210&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class BookListViewModel: BaseViewModel {

    @Published var bookList: [Book]

    init(bookList: [Book] = []) {
        self.bookList = bookList
    }
}

final class BookListViewController: BaseViewController {
    var viewModel: BookListViewModel!

    var cancellables: Set&amp;lt;AnyCancellable&amp;gt; = []

    func bind() {
        viewModel.$bookList
            .receive(on: DispatchQueue.main)
            .sink { [weak self] bookList in
                // UI 
                self.activityIndicator.stopAnimating()
            }
            .store(in: &amp;amp;cancellables)
    }
}&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;여기서 더 나아가서, 프로토콜 프로그래밍의 다형성을 위해서 viewModel의 비즈니스 로직들을 프로토콜로 정의하려고 했습니다. ViewController가 해당 프로토콜을 채택한 어떠한 ViewModel을 받을 수 있게 하기 위해서 입니다. 그래야 테스트 코드를 작성할 때 목 데이터를 쉽게 작성할 수 있기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667576160985&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol BookListBusinessLogic {
    @Published var bookList: [Book]
}

final class BookListViewModel: BaseViewModel, BookListBusinessLogic {

    @Published var bookList: [Book]

    init(bookList: [Book] = []) {
        self.bookList = bookList
    }
}

final class BookListViewController: BaseViewController {

    let viewModel: BookListBusinessLogic

    init(viewModel: BookListBusinessLogic = BookListViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
}&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;하지만&amp;nbsp;@Published 프로퍼티 래퍼 타입을 프로토콜에 정의했더니 해당 기능은 지원하지 않는다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mrETN/btrQp55z12N/xBqEaJk3a6dhHnZCEXkIvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mrETN/btrQp55z12N/xBqEaJk3a6dhHnZCEXkIvk/img.png&quot; data-alt=&quot;컴파일 에러 : 너 선언하면 안된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mrETN/btrQp55z12N/xBqEaJk3a6dhHnZCEXkIvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmrETN%2FbtrQp55z12N%2FxBqEaJk3a6dhHnZCEXkIvk%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;1384&quot; height=&quot;196&quot; data-origin-width=&quot;1384&quot; data-origin-height=&quot;196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컴파일 에러 : 너 선언하면 안된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hv_highlighter&quot;&gt;해결책&lt;/span&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ViewController에서 ViewModel의 @Published 타입에 바인딩하기 위해 필요한 타입을 알아야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667576422935&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;viewModel.$bookList
    .receive(on: DispatchQueue.main)
    .sink { [weak self] bookList in
        guard let self = self else { return }
        // UI
        self.activityIndicator.stopAnimating()
    }
    .store(in: &amp;amp;cancellables)&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;b&gt;$bookList&lt;/b&gt;에 바인딩을 하고 있고 이 타입은 &lt;b&gt;Publisher&lt;/b&gt;입니다. 다시 생각하면 ViewController가 실제로 필요한 것은 @Published 프로퍼티 래퍼 타입이 아니라 &lt;b&gt;bookList&lt;/b&gt;의 Publisher 타입입니다. ([Book]의 Publisher)&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;프로토콜에는 Publisher 타입을 정의하고 실제 구현에서 &lt;b&gt;bookList&lt;/b&gt;의 Publisher를 반환하는 코드를 작성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1667576641074&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protocol BookListBusinessLogic {
    var bookListPublisher: Published&amp;lt;[Book]&amp;gt;.Publisher { get }
}

final class BookListViewModel: BaseViewModel, BookListBusinessLogic {

    @Published var bookList: [Book]

    var bookListPublisher: Published&amp;lt;[Book]&amp;gt;.Publisher { $bookList }

    init(bookList: [Book] = []) {
        self.bookList = bookList
    }
}&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;ViewController에선 ViewModel을 프로토콜 타입으로 정의하고 &lt;b&gt;bookListPublisher&lt;/b&gt;에 바인딩시킵니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1667576874042&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class BookListViewController: BaseViewController {

    let viewModel: BookListBusinessLogic

    init(viewModel: BookListBusinessLogic = BookListViewModel()) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    func bind() {
        viewModel.bookListPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] bookList in
                guard let self = self else { return }
                // UI
                self.activityIndicator.stopAnimating()
            }
            .store(in: &amp;amp;cancellables)
    }
}&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;이를 통해 ViewModel과 ViewController의 바인딩 상태는 유지한 채 다형성을 성공적으로 적용시킬 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드를 작성할 때 &lt;b&gt;BookListBusinessLogic&lt;/b&gt;를 따르는 &lt;b&gt;MockBookListViewModel&lt;/b&gt;를 만들어 &lt;b&gt;BookListViewController&lt;/b&gt;에 주입할 수 있습니다.&amp;nbsp;&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;하지만, 단점도 있습니다. @Published 프로퍼티 래퍼 타입은 양방향 바인딩을 위한 녀석입니다. SwiftUI와 다르게 UIKit은 UI 컴포넌트에 직접 바인딩이 어려운 명령형 프로그래밍으로 이루어져 있습니다. 즉, 유저가 버튼을 누르면 연결된 데이터를 직접 바꿔주어야 하고, 데이터가 바뀌면 직접 UI를 업데이트해야 합니다. 만약&amp;nbsp;ViewController가 @Published 녀석에 대해 모르도록 숨겨버린다면 양방향 바인딩의 장점은 사라지고 단방향 데이터 흐름을 가지게 됩니다.&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Combine에서 테스트 코드를 작성하다 작성한 글입니다. 틀린 점이 있다면 지적 부탁드려요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽어주셔서 감사합니다.&lt;/p&gt;</description>
      <category>iOS</category>
      <category>@Published</category>
      <category>Protocol</category>
      <category>다형성</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/78</guid>
      <comments>https://wodyios.tistory.com/78#entry78comment</comments>
      <pubDate>Sat, 5 Nov 2022 00:59:57 +0900</pubDate>
    </item>
    <item>
      <title>ViewController 생명주기의 몰랐던 점</title>
      <link>https://wodyios.tistory.com/74</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hv_highlighter&quot;&gt;ViewController 생명주기 호출 순서&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발을 한다면 ViewController 생명주기는 모두가 알고 있을 거예요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;viewDidLoad &lt;br /&gt;-&amp;gt; viewWillAppear &lt;br /&gt;-&amp;gt; viewDidAppear &lt;br /&gt;-&amp;gt; viewWillDisappear &lt;br /&gt;-&amp;gt; viewDidDisappear&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;다른 뷰컨트롤러로 넘어가는 상황에서 현재 뷰컨트롤러의 viewWillDisppear가 먼저 호출될까 전환되는 뷰컨트롤러의 viewWillAppear가 먼저 호출될까? 도 디버깅을 해본다면 바로 알 수 있습니다.&lt;/span&gt;&lt;/span&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;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;caret-color: #000000;&quot;&gt;테스트를 위해서 뷰컨트롤러의 &lt;span style=&quot;color: #000000;&quot;&gt;모든 생명주기에 print문을 찍어볼게요.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666610128608&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        print(&quot;✨ FirstViewController viewDidLoad&quot;)
    }

    @IBAction func buttonDidTap(_ sender: Any) {
        
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        print(&quot;✨ FirstViewController viewWillAppear&quot;)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        print(&quot;✨ FirstViewController viewDidAppear&quot;)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        print(&quot;✨ FirstViewController viewWillDisappear&quot;)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        print(&quot;✨ FirstViewController viewDidDisappear&quot;)
    }
}

final class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        title = &quot;두번째&quot;
        view.backgroundColor = .yellow
        print(&quot;  SecondViewController viewDidLoad  &quot;)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        print(&quot;  SecondViewController viewWillAppear  &quot;)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        print(&quot;  SecondViewController viewDidAppear  &quot;)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        print(&quot;  SecondViewController viewWillDisappear  &quot;)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        print(&quot;  SecondViewController viewDidDisappear  &quot;)
    }
}&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;그리고 &amp;nbsp;FirstViewController에서 SecondViewController로 네비게이션 push를 해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.44.56.png&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daiJU5/btrPuj3pz37/5bDF3k8Ho8l6wJ8cxJRsRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daiJU5/btrPuj3pz37/5bDF3k8Ho8l6wJ8cxJRsRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daiJU5/btrPuj3pz37/5bDF3k8Ho8l6wJ8cxJRsRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaiJU5%2FbtrPuj3pz37%2F5bDF3k8Ho8l6wJ8cxJRsRk%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;500&quot; height=&quot;236&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.44.56.png&quot; data-origin-width=&quot;866&quot; data-origin-height=&quot;408&quot;/&gt;&lt;/span&gt;&lt;/figure&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;FirstViewController의 viewWillDisappear가 먼저 호출되고 다음 SecondViewController의 viewWillAppear가 호출되는 것을 볼 수 있어요. 이 호출 순서가 중요할까? 라고 생각할지 모르겠지만 두 화면에서 각각 애니메이션을 처리하고 있을 때 부드러운 애니메이션 효과를 주기 위해 필요합니다.&amp;nbsp;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.45.55.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/coyeBF/btrPs22qqyX/QKhbKwlwi3GXW8c5kj8BVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/coyeBF/btrPs22qqyX/QKhbKwlwi3GXW8c5kj8BVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/coyeBF/btrPs22qqyX/QKhbKwlwi3GXW8c5kj8BVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcoyeBF%2FbtrPs22qqyX%2FQKhbKwlwi3GXW8c5kj8BVK%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;500&quot; height=&quot;129&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.45.55.png&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirstViewController와 SecondViewController 각각을 생각해보면, 모두&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;viewDidLoad &lt;br /&gt;-&amp;gt; viewWillAppear &lt;br /&gt;-&amp;gt; viewDidAppear &lt;br /&gt;-&amp;gt; viewWillDisappear &lt;br /&gt;-&amp;gt; viewDidDisappear&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;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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hv_highlighter&quot;&gt;viewWillAppear 다음에 항상 viewWillDisappear 호출된다?&lt;/span&gt; &amp;nbsp;❎&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewWillAppear 메소드가 호출된 후에 viewWillDisappear 메소드가 항상 호출되지 않습니다.그리고,&amp;nbsp;viewWillDisappear 메소드 다음에도 항상 viewDidDisappear 메소드가 호출되지 않습니다.&amp;nbsp;&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;네비게이션 push를 통한 화면전환에서 스와이프 제스처를 이용해서 뒤로 가려다 취소를 한 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 상황을 녹화해봤어요. SecondViewController에서 뒤로가기 버튼을 눌러 뒤로가는 것이 아닌, 제스처를 통해서 뒤로가려고 했다가 중간에 취소한 케이스입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z2u9S/btrPtjQlUjY/RJyRCr2vs88OjFHezwbdU1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z2u9S/btrPtjQlUjY/RJyRCr2vs88OjFHezwbdU1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z2u9S/btrPtjQlUjY/RJyRCr2vs88OjFHezwbdU1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/Z2u9S/btrPtjQlUjY/RJyRCr2vs88OjFHezwbdU1/img.gif&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;300&quot; height=&quot;649&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.19.14.png&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C9WzS/btrPtge3kSS/PPm1xZ4y2wDa5cyUTVkADk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C9WzS/btrPtge3kSS/PPm1xZ4y2wDa5cyUTVkADk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C9WzS/btrPtge3kSS/PPm1xZ4y2wDa5cyUTVkADk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC9WzS%2FbtrPtge3kSS%2FPPm1xZ4y2wDa5cyUTVkADk%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;500&quot; height=&quot;171&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.19.14.png&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&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;먼저, FirstViewController를 보면, 호출 순서는 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;viewWillAppear &lt;br /&gt;-&amp;gt; viewWillDisappear &lt;br /&gt;-&amp;gt; viewDidDisappear&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;뷰컨트롤러가 보여질려고 했지만 취소되어 사라집니다. viewWillAppear가 호출된 이후에 viewDidAppear가 호출되지 않았습니다!&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;다음, SecondViewController를 보면 호출 순서는 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;viewWillDisapppear &lt;br /&gt;-&amp;gt; viewWillAppear &lt;br /&gt;-&amp;gt; viewDidAppear&amp;nbsp;&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;뷰컨트롤러가 사라지려고 했지만 취소되어 다시 보입니다.&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewWillAppear 뒤에 항상 viewDidAppear가 호출되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewWillDisappear 뒤에 항상 viewDidDisappear가 호출되지 않는다.&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;또 더 유추해본다면 뷰컨트롤러의 생명주기는 서로에게 영향을 미칩니다. appear가 호출되면 뷰가 사라지기 위해 disappear가 무조건 호출되어야하고,&amp;nbsp;disappear가 호출된다면 해당 뷰가 다시 appear되기 위해선 appear가 호출되어야 합니다.&amp;nbsp;&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;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;will과 did의 상관없이 appear과 disappear의 관계는 항상 붙어다니는 관계&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;will과 did의 관계도 생각해보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;will 다음에 항상 did는 올 수도 있고 안 올수도 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;did 전에는 항상 will이 와야합니다.&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;viewDidAppear 전에는 항상 viewWillAppear가 호출되어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;viewDidDisappear 전에는 항상 viewWillDisappear가 호출되어야한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;span class=&quot;hv_highlighter&quot;&gt;modal present 화면 전환에서는?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 상황에서 SecondViewController를 ThirdViewController라고 생각하고만 바꿔서&amp;nbsp;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;화면전환은 모달로 다시 테스트해보겠습니다!&lt;/span&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;modal present하는 상황은 presentation이 fullscreen인 상황과 아닌 상황 2가지로 나눠서 볼게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지 상황을 나누는 이유는 fullscreen이 아닌 경우 FirstViewController의 disappear 생명주기 메소드가 호출되지 않기 때문이에요. 뷰컨트롤러가 완전히 사라지지 않고 뒤에 스냅샷으로 남아있기 때문에 다른 케이스입니다.&amp;nbsp;&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;먼저, fullscreen부터 실험을 해볼게요. 이 케이스는 일반적인 케이스입니다! 그니까 원래 생각했던 방식대로 뷰컨트롤러 생명주기가 실행돼요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FirstViewController와&amp;nbsp;ThirdViewController 모두 생각했던 일반적으로 실행됩니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;viewDidLoad &lt;br /&gt;-&amp;gt; viewWillAppear &lt;br /&gt;-&amp;gt; viewDidAppear &lt;br /&gt;-&amp;gt; viewWillDisappear &lt;br /&gt;-&amp;gt; viewDidDisappear&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.44.15.png&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prsFA/btrPt7aUPLu/40z89Ck1zdzDknVW41yEX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prsFA/btrPt7aUPLu/40z89Ck1zdzDknVW41yEX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prsFA/btrPt7aUPLu/40z89Ck1zdzDknVW41yEX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprsFA%2FbtrPt7aUPLu%2F40z89Ck1zdzDknVW41yEX1%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;500&quot; height=&quot;264&quot; data-filename=&quot;스크린샷 2022-10-24 오후 8.44.15.png&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 fullscreen이 아닌 모달인 경우에는 어떻게 될까요?&amp;nbsp;&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;b&gt;모달을 닫으려는 제스처를 취소하는 경우&lt;/b&gt; viewWillAppear 이후 viewDidAppear 함수가 호출되지 않고, viewWillDisappear 이후 viewDidDisappear 함수가 호출되지 않습니다! 아래 영상같은 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cauUZH/btrPsAd4pYf/UfaTebvY8TAFyfntDp6gmK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cauUZH/btrPsAd4pYf/UfaTebvY8TAFyfntDp6gmK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cauUZH/btrPsAd4pYf/UfaTebvY8TAFyfntDp6gmK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/cauUZH/btrPsAd4pYf/UfaTebvY8TAFyfntDp6gmK/img.gif&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;300&quot; height=&quot;1920&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WPI6a/btrPt7aU3P2/vNMy6TiTKLbve7pMQ8RyB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WPI6a/btrPt7aU3P2/vNMy6TiTKLbve7pMQ8RyB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WPI6a/btrPt7aU3P2/vNMy6TiTKLbve7pMQ8RyB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWPI6a%2FbtrPt7aU3P2%2FvNMy6TiTKLbve7pMQ8RyB0%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;500&quot; height=&quot;178&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간 네모 박스를 친 부분을 보신다면&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;viewWillDisappear &lt;br /&gt;-&amp;gt; viewWillAppear &lt;br /&gt;-&amp;gt; viewDidAppear&amp;nbsp;&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;위 호출 순서를 볼 수 있습니다.&amp;nbsp;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사실을 알고 난 후, viewDidAppear에 애니메이션 효과를 시작하고, viewWillDisappear에서 사라지는 애니메이션 효과를 주어 더 부드럽게 효과를 개선할 수 있었습니다.&amp;nbsp;&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;b&gt;개선 전, 뒤로가기 제스쳐에서 행성이 남아있음&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2022-10-24 20.59.44 2.gif&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n90oX/btrPrnziFpn/X1tkvP2fGjOZpbWh4xOkM0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n90oX/btrPrnziFpn/X1tkvP2fGjOZpbWh4xOkM0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n90oX/btrPrnziFpn/X1tkvP2fGjOZpbWh4xOkM0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/n90oX/btrPrnziFpn/X1tkvP2fGjOZpbWh4xOkM0/img.gif&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;300&quot; height=&quot;649&quot; data-filename=&quot;2022-10-24 20.59.44 2.gif&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;개선 후,&lt;/b&gt; &lt;b&gt;뒤로가기 제스쳐에 애니메이션 효과 적용&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dM3xFq/btrPs3G2sUl/NlkBPDsPWHBZVU3kbwA3h0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dM3xFq/btrPs3G2sUl/NlkBPDsPWHBZVU3kbwA3h0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dM3xFq/btrPs3G2sUl/NlkBPDsPWHBZVU3kbwA3h0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dM3xFq/btrPs3G2sUl/NlkBPDsPWHBZVU3kbwA3h0/img.gif&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;300&quot; height=&quot;1920&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;hv_highlighter&quot;&gt;UIAdaptivePresentationControllerDelegate&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제스쳐를 이용한 뒤로가기를 어떻게 감지할 수 있는지에 대해서도 알면 좋을 것 같아 더 찾아봤습니다&lt;span style=&quot;color: #000000;&quot;&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A set of methods that, in conjunction with a presentation controller, determine how to respond to trait changes in your app.&amp;nbsp;&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;해당 프로토콜을 UIPresentationController 객체에 채택한다면, 제스처로 뒤로가는 상황을 알 수 있습니다!&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;&amp;nbsp;&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>
      <category>UIAdaptivePresentationControllerDelegate</category>
      <category>UIViewController 생명주기</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/74</guid>
      <comments>https://wodyios.tistory.com/74#entry74comment</comments>
      <pubDate>Mon, 24 Oct 2022 21:12:21 +0900</pubDate>
    </item>
    <item>
      <title>[CoreAnimation] Layer에 CoreAnimation을 무한으로 체이닝할 수 있을까?</title>
      <link>https://wodyios.tistory.com/73</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;p data-ke-size=&quot;size16&quot;&gt;오늘은 코어애니메이션을 체이닝할 수 있는지에 대해 알아보려고 합니다. 그냥 체이닝이 아니라 무한 체이닝입니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(살짝 스포하자면 &lt;span style=&quot;color: #ee2323;&quot;&gt;실패&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2022-10-23 20.09.08.gif&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFlwVM/btrPoy0p1Rm/VHZJUfvqt6FoGKUFpgz6Zk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFlwVM/btrPoy0p1Rm/VHZJUfvqt6FoGKUFpgz6Zk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFlwVM/btrPoy0p1Rm/VHZJUfvqt6FoGKUFpgz6Zk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bFlwVM/btrPoy0p1Rm/VHZJUfvqt6FoGKUFpgz6Zk/img.gif&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;250&quot; height=&quot;510&quot; data-filename=&quot;2022-10-23 20.09.08.gif&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&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;질문이 나오게 된 배경&amp;nbsp;&lt;/h3&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3824&quot; data-origin-height=&quot;2474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvhXD3/btrPmFldyqu/KYn0FaP4DrUJcWmjsYFnt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvhXD3/btrPmFldyqu/KYn0FaP4DrUJcWmjsYFnt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvhXD3/btrPmFldyqu/KYn0FaP4DrUJcWmjsYFnt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvhXD3%2FbtrPmFldyqu%2FKYn0FaP4DrUJcWmjsYFnt0%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;700&quot; height=&quot;453&quot; data-origin-width=&quot;3824&quot; data-origin-height=&quot;2474&quot;/&gt;&lt;/span&gt;&lt;/figure&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;별을 구성하고 있는 모서리를 알아야 UIBezierPath로 경로를 그려줄 수 있어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이미지로 추출해서 layer의 contents로 넣어줘도 가능하지만 정밀한 컨트롤이 불가능할 것 같아서 직접 만들어주었어요.&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;위에 보이는 모서리를 이용해서 path를 한땀한땀 만들어줬어요. 생각해보면 애니메이션은 개발의 영역이 아닌 것 같기도 합니당.. 이렇게 말한 이유는 나중에도 생각할 수 있지만... 일단 넘어가서!&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;UIBezierPath의 Extension함수로 만들었습니다!&lt;/p&gt;
&lt;pre id=&quot;code_1666523725925&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;extension UIBezierPath {
    static var starPath: UIBezierPath {
        let path = UIBezierPath()
        path.move(to: .init(x: 10, y: 0))
        path.addLine(to: .init(x: 7.35, y: 6.36))
        path.addLine(to: .init(x: 0.49, y: 6.91))
        path.addLine(to: .init(x: 5.72, y: 11.39))
        path.addLine(to: .init(x: 4.12, y: 18.09))
        path.addLine(to: .init(x: 10, y: 14.5))
        path.addLine(to: .init(x: 15.88, y: 18.09))
        path.addLine(to: .init(x: 14.28, y: 11.39))
        path.addLine(to: .init(x: 19.51, y: 6.91))
        path.addLine(to: .init(x: 12.65, y: 6.36))
        path.close()
        return path
    }
}&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;a href=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot;&gt;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666523992406&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 - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 &quot; data-og-description=&quot;  iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포 - GitHub - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot; data-og-url=&quot;https://github.com/wody-d/woody-iOS-tip&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/m4vFE/hyQhC1dogW/jgtkdWb6QJEqt7UQ3MItrK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/m4vFE/hyQhC1dogW/jgtkdWb6QJEqt7UQ3MItrK/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 - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포 - GitHub - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고&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;그럼 이제 별이 빛날 배경을 만들기 위해서 샘플코드를 작성했습니다!&lt;/p&gt;
&lt;pre id=&quot;code_1666523803906&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class ViewController: UIViewController {

    lazy var starBackgroundView = StarBackgroundView(frame: view.bounds)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black

        view.addSubview(starBackgroundView)
    }
}

final class StarBackgroundView: UIView {

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

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
}&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;pre id=&quot;code_1666524067166&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class StarBackgroundView: UIView {
    let starSize = CGSize(width: 10, height: 10)
    let starCount: Int = 100
    var starLayers = [CALayer]()
	
    override init(frame: CGRect) {
        super.init(frame: frame)

        for _ in 0..&amp;lt;starCount {
            let astar = CAShapeLayer()
            astar.frame = .init(origin: generateRandomPosition(), size: starSize)
            astar.path = UIBezierPath.starPath.cgPath
            astar.fillColor = UIColor.systemYellow.cgColor
            astar.shadowPath = UIBezierPath.starPath.cgPath
            astar.masksToBounds = false
            astar.shadowColor = UIColor.red.cgColor
            astar.shadowOpacity = 2
            astar.shadowOffset = .zero
            astar.shadowRadius = 4
            starLayers.append(astar)
        }
    }
    
	override func draw(_ rect: CGRect) {
	starLayers.forEach {
	    layer.addSublayer($0)
	}
    
    // ✅ 랜덤 위치 생성 함수
    func generateRandomPosition() -&amp;gt; CGPoint {
    	let height = frame.height
    	let width = frame.width
	
    	return CGPoint(
    	    x: CGFloat(arc4random()).truncatingRemainder(dividingBy: width),
    	    y: CGFloat(arc4random()).truncatingRemainder(dividingBy: height)
    	)
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별의 frame이 필요했기에 사이즈를 가로 세로가 10로 정했고, 별의 개수는 100개로 했습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 레이어의 속성들을 초기화를 해주었는데 여기서 중요한 점은 frame의 origin 속성입니다.! 랜덤한 값을 넣어줘야 매번 다른 위치에 별이 생성되니까요. path는 처음에 만들어준 별 경로를 할당했고 색깔, 그림자 등등을 정의했어요.&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;배경 뷰가 초기화될 때 별 별의 개수만큼 추가해주었어요. 이때 for문을 이용해서 별을 추가해주었지만 처음에는 CAReplicatorLayer로 레이어를 복제하려고 했습니다. 하지만 CAReplicatorLayer는 원하는 위치에 랜덤배치가 되지 않았어요. instanceTransform 프로퍼티는 복제된 이전의 위치를 기반으로 다음 레이어가 복제되기 때문에, 원하는 구현이 아니었습니다.. 그래서 for문을 이용하기로 했어요.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;1988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OagbC/btrPh9On0HG/kYz036al6tNe5bJRm5y3rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OagbC/btrPh9On0HG/kYz036al6tNe5bJRm5y3rK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OagbC/btrPh9On0HG/kYz036al6tNe5bJRm5y3rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOagbC%2FbtrPh9On0HG%2FkYz036al6tNe5bJRm5y3rK%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;250&quot; height=&quot;471&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;1988&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 준비였고,&amp;nbsp;이제 Core Animation을 주도록 할게요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;opacity, fillColor 2가지에 대한 애니메이션을 줬고 어떻게 줬는지에 대한 설명은 생략하도록 하겠습니다. 궁금하시다면, 다른 글을 읽고 오시면 좋을 것 같아요!&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;아래 코드를 draw 메소드에 추가해줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1666525011147&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;starLayers.forEach {
    let opacityAnimation = CABasicAnimation(keyPath: &quot;opacity&quot;)
    let duration: CGFloat = 1
    opacityAnimation.fromValue = 0.2
    opacityAnimation.toValue = 0.8
    opacityAnimation.timeOffset = duration * drand48()
    opacityAnimation.duration = duration
    opacityAnimation.repeatCount = .infinity
    opacityAnimation.autoreverses = true
    opacityAnimation.delegate = self
    opacityAnimation.isRemovedOnCompletion = false

    $0.add(opacityAnimation, forKey: &quot;opacity&quot;)

    let colorAnimation = CAKeyframeAnimation(keyPath: &quot;fillColor&quot;)
    colorAnimation.duration = duration
    colorAnimation.values = [UIColor.clear.cgColor, UIColor.systemYellow.cgColor, UIColor.white.cgColor]
    colorAnimation.timeOffset = duration * drand48()
    colorAnimation.repeatCount = .infinity
    colorAnimation.autoreverses = true
    colorAnimation.delegate = self
    $0.add(colorAnimation, forKey: &quot;color&quot;)
}&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;&amp;nbsp;&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHbkJa/btrPpWNwhob/pWnGLA4pPwJl1g8nezkq7k/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHbkJa/btrPpWNwhob/pWnGLA4pPwJl1g8nezkq7k/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHbkJa/btrPpWNwhob/pWnGLA4pPwJl1g8nezkq7k/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bHbkJa/btrPpWNwhob/pWnGLA4pPwJl1g8nezkq7k/img.gif&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;250&quot; height=&quot;510&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 별들은 &lt;span style=&quot;color: #ee2323;&quot;&gt;매번 같은 duration, 같은 timeOffset, 같은 speed를&lt;/span&gt; 가진 채 반짝이고 있습니다. 저는 duration, timeOffset과 speed를 매번 다르게 주고 싶었어요. 별은 매번 일정하게 반짝이는 것이 아니라 매번 랜덤하게 반짝이니까요. (어쩔때는 오랫동안 별의 빛을 유지하기도 하다가 또 어쩔때는 금방 깜박이고..)&amp;nbsp;&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;Core Animation 효과를 무한으로 체이닝할 수 있을까?&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;가장 처음 든 생각은 현재 repeatCount 프로퍼티를 infinity로 줬는데 이것을 0으로 준 후, 애니메이션이 끝날 때마다 새로운 애니메이션을 추가하는 방법이었어요. 애니메이션이 끝나는 시점은 2가지 방법으로 알 수가 있었습니다.&amp;nbsp;&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;먼저, delegate을 채택하고 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;func animationDidStop(_ anim: CAAnimation, finished flag: Bool) &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메소드에서 알 수 있습니다. 따라서 해당 메소드에서 새로운 애니메이션을 추가하면 돼요. 그래서 아래와 같이 추가를 해줬습니다.&amp;nbsp;&lt;/span&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;pre id=&quot;code_1666528031572&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// opacity 애니메이션만 일단 적용 
// 전체코드 아님 주의! 

opacityAnimation.delegate = self
   
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
	if let opacity = anim.value(forKey: &quot;opacity&quot;) as? String, opacity == &quot;opacityValue&quot; {
		let opacityAnimation = CABasicAnimation(keyPath: &quot;opacity&quot;)
		let duration: CGFloat = 1
		opacityAnimation.fromValue = 0.2
		opacityAnimation.toValue = 0.8
		opacityAnimation.timeOffset = duration * drand48()
		opacityAnimation.duration = duration
		opacityAnimation.repeatCount = 0
		opacityAnimation.autoreverses = true
		opacityAnimation.delegate = self
		opacityAnimation.setValue(&quot;opacityValue&quot;, forKey: &quot;opacity&quot;)
		starLayers.forEach {
			$0.add(opacityAnimation, forKey: nil)
		}
	}
}&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;&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;애니메이션을 레이어에 추가하는 스레드가 먹통인가? &quot;그럼 애니메이션만 추가하는 아토믹한 명령으로 만들어보자&quot;라는 생각으로 CATransaction을 이용했어요.&lt;/p&gt;
&lt;pre id=&quot;code_1666528754467&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CATransaction.begin()
starLayers.forEach {
    $0.add(opacityAnimation, forKey: nil)
}
CATransaction.commit()&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;이번엔 되긴 했어요. 근데 duration을 무시하고 빠른 속도로 깜박입니다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rg9ZQ/btrPkruiafM/Q4IWZoIKZcTzzyMVMK5Or0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rg9ZQ/btrPkruiafM/Q4IWZoIKZcTzzyMVMK5Or0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rg9ZQ/btrPkruiafM/Q4IWZoIKZcTzzyMVMK5Or0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/rg9ZQ/btrPkruiafM/Q4IWZoIKZcTzzyMVMK5Or0/img.gif&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;250&quot; height=&quot;510&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;p data-ke-size=&quot;size16&quot;&gt;모르겠습니다. 고민은 정말 많이 했는데 그 원인을 찾기가 쉽지 않았습니다. 검색해도 나오지가 않더라구요. 이유가 있을까요...? iOS 커뮤니티와 오픈채팅방에도 여쭤봤지만 답이 없었습니다.. 클린 코드 책에서는 &quot;말이 안되는 실패는 잠정적인 스레드 문제로 취급하라&quot; 고 합니다. 스레드를 디버깅하는 방식은 더 공부해봐야할 것 같아 Main 쓰레드에서 코드를 작성해보았지만 그마저도 실행되지 않았습니다. &amp;nbsp;&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;UIView의 애니메이션으로 구현하면 매번 프로퍼티값을 랜덤하게 설정해줄 수 있기 때문에 원하는 애니메이션을 만들 수는 있습니다. 하지만 별이 정말 많아지면, cpu에 부담을 많이 줄 수 있습니다. Core 애니메이션으로 구현하면 매번 프로퍼티값을 랜덤하게 설정할 수가 없어요. 트레이드오프가 있네요... (사실 제가 코어 애니메이션으로 못하고 있는 것 같습니다만.. 아신다면 댓글에 이유를 남겨주세요!..!)&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;1. beginTime 프로퍼티와 CAAnimationGroup을 이용하는 방법이 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. CATransaction의 setCompletionBlock 프로퍼티를 이용하는 방법도 있습니다.&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;무한으로 체이닝하려고 하기 때문에 위와 같은 이슈가 발생하는 것입니다. 이 이슈로 오늘 6시간을 보냈습니다. 더 찾아보고 싶지만 이제 지금 하고 있는 프로젝트를 개발하러 가야겠습니다... 아쉽지만 다음에 찾아보도록 할게요. 오늘도 읽어주셔서 감사합니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>iOS</category>
      <category>CAAnimationGroup</category>
      <category>CATransaction</category>
      <category>CoreAnimation</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/73</guid>
      <comments>https://wodyios.tistory.com/73#entry73comment</comments>
      <pubDate>Sun, 23 Oct 2022 21:49:58 +0900</pubDate>
    </item>
    <item>
      <title>[CoreAnimation] 물체를 path 따라 움직이기 (feat. 우주비행사 날리기)</title>
      <link>https://wodyios.tistory.com/72</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BEIX0/btrPivjq33A/ar5G7P7zXlGaszKMhTvgP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BEIX0/btrPivjq33A/ar5G7P7zXlGaszKMhTvgP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BEIX0/btrPivjq33A/ar5G7P7zXlGaszKMhTvgP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBEIX0%2FbtrPivjq33A%2Far5G7P7zXlGaszKMhTvgP1%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;300&quot; height=&quot;300&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&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;p data-ke-size=&quot;size16&quot;&gt;먼저 지난번에 알아본 CAKeyframeAnimation와 UIBezierPath의 개념에 대한 이해가 필요해요.&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;&lt;a href=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666439931903&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 - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 &quot; data-og-description=&quot;  iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포 - GitHub - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot; data-og-url=&quot;https://github.com/wody-d/woody-iOS-tip&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b2JSLG/hyQhG3hg4w/RtsKqFAUzYFoySsaoMokZK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:19_uibezierpath.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b2JSLG/hyQhG3hg4w/RtsKqFAUzYFoySsaoMokZK/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 - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포 - GitHub - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고&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;&lt;a href=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:20_core_animation.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:20_core_animation.md&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666439944388&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 - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 &quot; data-og-description=&quot;  iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포 - GitHub - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 &quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:20_core_animation.md&quot; data-og-url=&quot;https://github.com/wody-d/woody-iOS-tip&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dtyzyM/hyQbAKhIIi/FBB8uubQnxxIIkLhC0xKR1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:20_core_animation.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/wody-d/woody-iOS-tip/blob/main/TIL_2022:10:20_core_animation.md&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dtyzyM/hyQbAKhIIi/FBB8uubQnxxIIkLhC0xKR1/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 - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고 오늘 배운 것들을 모아둔 레포 - GitHub - wody-d/woody-iOS-tip:   iOS에 대한 소소한 팁들과 개발하다 마주친 버그 해결기, 그리고&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;h3 data-ke-size=&quot;size23&quot;&gt;경로 그리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 view 하나를 준비합니다. 위 view위에 layer를 추가해줄려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666440216812&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class ViewController: UIViewController { 

	lazy var animationView =  AnimationView(
	    frame: .init(
	        origin: .init(x: 50, y: 100),
	        size: .init(width: view.frame.width-100, height: 300)
	    )
	)
	
	override func viewDidLoad() {
	   super.viewDidLoad()
	   view.backgroundColor = .white
	   view.addSubview(animationView)
	}
}

final class AnimationView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .black
    }

    required init?(coder: NSCoder) {
        fatalError(&quot;init(coder:) has not been implemented&quot;)
    }
}&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;물체를 원하는 경로로 날리기 위해서는 경로를 그려줘야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로는 UIBezierPath를 이용해서 간단한 곡선을 그려볼게요.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666440114723&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let path = UIBezierPath()
path.move(to: .init(x: 0, y: 0))
path.addQuadCurve(
    to: .init(x: bounds.width/2, y: bounds.height/2),
    controlPoint: .init(x: 0, y: bounds.height/2)
)
path.addQuadCurve(
    to: .init(x: bounds.width, y: bounds.height),
    controlPoint: .init(x: bounds.width, y: bounds.height/2)
)
path.move(to: .init(x: bounds.width, y: bounds.height))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로를 만들어주었으면 이제 레이어를 추가해주어야해요.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666440298364&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func draw(_ rect: CGRect) {
   let lineLayer = CAShapeLayer()
   lineLayer.frame = bounds
   lineLayer.path = path.cgPath
   lineLayer.strokeColor = UIColor.white.cgColor
   lineLayer.fillColor = UIColor.clear.cgColor
   self.layer.addSublayer(lineLayer)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;1037&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QNnDF/btrPgWuob8M/zoHGCiZC8kqBItN1sANT4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QNnDF/btrPgWuob8M/zoHGCiZC8kqBItN1sANT4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QNnDF/btrPgWuob8M/zoHGCiZC8kqBItN1sANT4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQNnDF%2FbtrPgWuob8M%2FzoHGCiZC8kqBItN1sANT4K%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;300&quot; height=&quot;565&quot; data-origin-width=&quot;551&quot; data-origin-height=&quot;1037&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;Shake 버튼과 아래 슬라이더는 일단 무시해주세요. 여러가지 실험을 위한 재료들입니다.&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;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;pre id=&quot;code_1666440676732&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class AnimationView: UIView {
    let imageView: UIImageView = UIImageView(image: .init(named: &quot;astronaut&quot;))

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .black
        addSubview(imageView)
        imageView.frame = CGRect(
            origin: .init(x: -20, y: -20),
            size: .init(width: 40, height: 40)
        )
       
       // path 코드 생략...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp5bQH/btrPjl8a7tn/GtZ3L7qApdghiURZUBJAEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp5bQH/btrPjl8a7tn/GtZ3L7qApdghiURZUBJAEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp5bQH/btrPjl8a7tn/GtZ3L7qApdghiURZUBJAEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp5bQH%2FbtrPjl8a7tn%2FGtZ3L7qApdghiURZUBJAEk%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;400&quot; height=&quot;342&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;499&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우주비행사한테 애니메이션을 주려면&amp;nbsp;애니메이션을 실행해줄 CAKeyframeAnimation 객체가 필요합니다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;또 움직일 경로가 필요한데&amp;nbsp;이미 위에서 path는 준비해두었네요!&lt;/span&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;이 때, CAKeyframeAnimation의 keypath는 우주비행사 이미지를 움직이는 것이므로 position이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;position은 해당 객체의 위치를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;position은 객체의 위치를 항상 나타내지는 않습니다. 위의 경우 우주비행사 이미지에 새로 추가한 레이어가 없고 anchorPoint 또한 (0.5, 0.5) 이기 때문에 우주비행사 이미지의 중앙을 나타냅니다. 자세한 내용은 아래 내용을 참고해주세요!&lt;br /&gt;https://wodyios.tistory.com/71&lt;/blockquote&gt;
&lt;pre id=&quot;code_1666441002608&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func draw(_ rect: CGRect) {
	let lineLayer = CAShapeLayer()
	lineLayer.frame = bounds
	lineLayer.path = path.cgPath
	lineLayer.strokeColor = UIColor.white.cgColor
	lineLayer.fillColor = UIColor.clear.cgColor
	self.layer.addSublayer(lineLayer)
	
	// 아래 코드 추가 
	let astronautAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
	astronautAnimation.repeatCount = 0
	astronautAnimation.path = path.cgPath
	astronautAnimation.duration = 1
    
	imageView.layer.add(astronautAnimation, forKey: nil)
	imageView.layer.position = path.currentPoint
}&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666441784221&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func draw(_ rect: CGRect) {

    let lineLayer = CAShapeLayer()
    lineLayer.frame = bounds
    lineLayer.path = path.cgPath

    lineLayer.strokeColor = UIColor.white.cgColor
    lineLayer.fillColor = UIColor.clear.cgColor
    self.layer.addSublayer(lineLayer)

    let astronautAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
    astronautAnimation.path = path.cgPath
    astronautAnimation.duration = 2
    let lineAnimation = CABasicAnimation(keyPath: &quot;strokeEnd&quot;)
    lineAnimation.fromValue = 0
    lineAnimation.toValue = 1
    lineAnimation.duration = 2

    CATransaction.begin()
    lineLayer.add(lineAnimation, forKey: nil)
    imageView.layer.add(astronautAnimation, forKey: nil)
    imageView.layer.position = path.currentPoint
    CATransaction.commit()
}&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;위 코드에서 CATransaction을 사용한 이유는 2개의 애니메이션을 함께 실행하는 것을 보장하기 위해서입니다. CATransaction은 타입메소드로 되어 있기 때문에 CATransaction.begin()과 commit()과 같이 사용할 수 있어요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 Core Animation을 레이어에 추가하면 스레드의 다음 런루프에 애니메이션이 실행이 돼요. 애니메이션이 실행될 때, &lt;span&gt;부분적으로 실행되다가 중단되지 않을 것을 보장하기 위해&amp;nbsp;&lt;/span&gt;transaction을 만드는데,&amp;nbsp;&lt;span&gt;스레드의 런루프마다 자동으로 생성되는 transaction을&lt;/span&gt;&amp;nbsp;implicit transaction이라 해요. 반대로, CATransaction와 같이 명시적으로 transaction을 만들어줄 수가 있는데 이를 explicit transaction이라고 합니다! 저는 지금 explicit transaction을 만들었습니다!&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;transaction은 inner와 outer로 구분될 수도 있습니다. CATransaction안에 또다른 CATransaction을 만들어줄 수가 있기 때문에 2번의 begin()을 하면, 2번의 commit()이 필요합니다.&amp;nbsp;자세한 내용은 아래 문서를 확인해주세요!&lt;br /&gt;https://developer.apple.com/documentation/quartzcore/catransaction&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FRMRC/btrPf3OKKVc/B3oHhlhKkKWPL6mhuaahc0/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FRMRC/btrPf3OKKVc/B3oHhlhKkKWPL6mhuaahc0/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FRMRC/btrPf3OKKVc/B3oHhlhKkKWPL6mhuaahc0/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/FRMRC/btrPf3OKKVc/B3oHhlhKkKWPL6mhuaahc0/img.gif&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;300&quot; height=&quot;649&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시간 조절하기 (timeOffset, speed)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 애니메이션은 view가 로드되는 순간 실행돼요.&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;먼저 슬라이더를 추가하고 슬라이더의 값이 변할 때마다 CoreAnimation을 주면 되겠다고 생각했습니다.&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;그래서, 슬라이더의 값에 따라 원하는 위치에 있기 위해서는&lt;b&gt; timeOffset이라는 프로퍼티&lt;/b&gt;를 사용해야 했어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;timeOffset 프로퍼티는 애니메이션이 실행될 때 몇 초 후 애니메이션이 그리고 있는 레이어에서부터 시작할 수 있도록 도와주는 프로퍼티입니다! (여기서 이해가 안될 수도 있는데 아래에서 더 자세하게 설명할게요)&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;a href=&quot;https://developer.apple.com/documentation/quartzcore/1395996-cacurrentmediatime)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CAMediaTiming&lt;/a&gt; 문서를 좀만 읽어가다 보면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이어에&lt;b&gt; speed 프로퍼티&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;그래서 speed = 0으로 하고 timeoffset에 슬라이더의 값 * duration을 할당했습니다. duration을 곱해준 이유는 duration동안 애니메이션이 실행되기 때문입니다! 생각한 것을 코드로 구현했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1666444304678&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 슬라이더 추가 코드 생략

func changeValue(_ value: Float) {
	let pathAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
	pathAnimation.path = path.cgPath
	let duration: CGFloat = 1
	pathAnimation.duration = duration
	pathAnimation.speed = 0
	pathAnimation.timeOffset = duration * Double(value)
	
	imageView.layer.add(pathAnimation, forKey: nil)
	imageView.layer.position = path.currentPoint
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dF4feN/btrPkquEdjH/CD0q978adlijk8OMtSQoa1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dF4feN/btrPkquEdjH/CD0q978adlijk8OMtSQoa1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dF4feN/btrPkquEdjH/CD0q978adlijk8OMtSQoa1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dF4feN/btrPkquEdjH/CD0q978adlijk8OMtSQoa1/img.gif&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;300&quot; height=&quot;649&quot; data-origin-width=&quot;888&quot; data-origin-height=&quot;1920&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 방법? (beginTime, isRemovedOnCompletion, fillMode)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법이 최선일까 더 찾아보다가 신기한 방법을 찾아냈어요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;speed = 0 을 하지 않아도, &lt;b&gt;beginTime&lt;/b&gt;&amp;nbsp;&lt;b&gt;isRemovedOnCompletion, fillMode&amp;nbsp;&lt;/b&gt;&amp;nbsp;3가지 프로퍼티만으로 작동하더라구요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666444601482&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func changeValue(_ value: Float) {
    let pathAnimation = CAKeyframeAnimation(keyPath: &quot;position&quot;)
    pathAnimation.path = path.cgPath
    let duration: CGFloat = 1
    pathAnimation.duration = duration
    pathAnimation.timeOffset = duration * Double(value)
    pathAnimation.beginTime = 1 // ✅
    pathAnimation.isRemovedOnCompletion = false // ✅
    pathAnimation.fillMode = .forwards // ✅
    
    imageView.layer.add(pathAnimation, forKey: nil)
    imageView.layer.position = path.currentPoint
}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isRemovedOnCompletion 프로퍼티는&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;fillMode 프로퍼티는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션이 끝났을 때, 다시 원래대로 돌아갈까 아니면 끝난 시점 레이어로 보여줄까에 관한 프로퍼티예요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 처음에 실행되는 애니메이션에서 우주비행사가 오른쪽아래로 이동했을 때 다시 왼쪽위모서리로 되돌릴까 아니면 그대로 남길까에 관한 프로퍼티인거죠. 이 프로퍼티는 isRemovedOnCompletion = false일 때에만 실행돼요. 애니메이션이 레이어에서 제거되면 애니메이션 에 관한 정보가 남아있지 않기 때문에 유효하지 않아요.&amp;nbsp;&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;beginTime 프로퍼티는 애니메이션이 시작되는 시간을 의미해요.&amp;nbsp;&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;프로퍼티들의 정의를 다 읽어보고 fillMode와 isRemovedOnCompletion프로퍼티는 납득이 갔어요.&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;&quot;애니메이션을 남겨서 이동된 위치정보를 가지고 있게하기 위해 설정한 것이구나&quot;&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;그런데, 도저히 beginTime은 이해가 가지 않았습니다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beginTime = 0 으로 설정하면 애니메이션이 의도대로 동작하지 않고 0이 아닌 다른 값으로 설정하면 의도대로 동작하더라구요.&amp;nbsp;&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;a href=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Customizing the Timing of an Animation 문서&lt;/a&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;먼저 알아야할 개념은 레이어마다 각자의 시간을 가지고 있다는 사실이에요. 레이어 각각의 시간차이는 유저가 느끼지 못할 정도로 작기 때문에 시간차를 준 애니메이션이 아니고선 시간차이에 관해서는 신경쓰지말라고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째로는 부모와의 상대적인 시간 개념도 있어요.&amp;nbsp;즉, frame과 bound의 관계처럼 말이에요.&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;그래서 결국 부모와 local 시간 중에 무엇이 더 중요하냐면, 레이어 각각이 가지고 있는 local 시간이 중요합니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 레이어의 현재 시간(local time)을 구하기 위해 부모의 시간을 자신의 시간(local time)으로 변환하는 과정이 필요해요.&amp;nbsp;&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;여기까지 읽어보았을 때, beginTime을 0으로 설정하면 자동으로 부모의 시간을 자신의 시간으로 변환해서 애니메이션을 실행합니다. 하지만, 0이 아닌 0.1초, 1초, 5초 등의 시간으로 설정하면 beginTime은 레이어의 절대적인 시간을 설정하는 것이므로 부모 레이어의 시간과 비교해보았을 때, 너어무 비슷해서 실행하고 바로 멈추는 것 같아요. (사실 이게 오작동 아닐까요?)&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;p data-ke-size=&quot;size16&quot;&gt;애니메이션이 아래와 같이 동작하는게 정삭적일 텐데,&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Normal : &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | A-&amp;gt;B-&amp;gt;C-&amp;gt;D-&amp;gt;E&lt;br /&gt;beginTime : &amp;nbsp; &amp;nbsp; | &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;A-&amp;gt;B-&amp;gt;C-&amp;gt;D-&amp;gt;E&lt;br /&gt;timeOffset : &amp;nbsp; &amp;nbsp; | C-&amp;gt;D-&amp;gt;E-&amp;gt;A-&amp;gt;B&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beginTime이 부모 레이어의 시간과 너무 맞지 않아,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Normal : &amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; | A-&amp;gt;B-&amp;gt;C-&amp;gt;D-&amp;gt;E&lt;br /&gt;beginTime+timeOffset : &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;C &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&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;조금 더 찾아봐야겠지만 일단 이렇게 결론을 내려봤어요. 더 찾아보고 놀라운 내용이 있다면 글을 다시 작성하도록 하겠습니다!&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;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;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;h3 data-ke-size=&quot;size23&quot;&gt;Ref&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;a href=&quot;https://stackoverflow.com/questions/27643002/ios-swift-animation-controlled-by-gesture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/27643002/ios-swift-animation-controlled-by-gesture&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666445344995&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;iOS, Swift: animation controlled by gesture&quot; data-og-description=&quot;I trying to find a way for connecting user interaction (pan gesture) with animation. I want to control animation progress by dragging view (or just by finger panning on the screen). I think I can ...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/27643002/ios-swift-animation-controlled-by-gesture&quot; data-og-url=&quot;https://stackoverflow.com/questions/27643002/ios-swift-animation-controlled-by-gesture&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bpLJFW/hyQhAWkcmd/kqCcieAI0ykdcbme5Lo57K/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/27643002/ios-swift-animation-controlled-by-gesture&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/27643002/ios-swift-animation-controlled-by-gesture&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bpLJFW/hyQhAWkcmd/kqCcieAI0ykdcbme5Lo57K/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;iOS, Swift: animation controlled by gesture&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I trying to find a way for connecting user interaction (pan gesture) with animation. I want to control animation progress by dragging view (or just by finger panning on the screen). I think I can ...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming/1427654-begintime&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/quartzcore/camediatiming/1427654-begintime&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666445349630&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;Apple Developer Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming/1427654-begintime&quot; data-og-url=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming/1427654-begintime&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming/1427654-begintime&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming/1427654-begintime&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.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;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/catransaction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/quartzcore/catransaction&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666445353154&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;Apple Developer Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/quartzcore/catransaction&quot; data-og-url=&quot;https://developer.apple.com/documentation/quartzcore/catransaction&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/catransaction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/quartzcore/catransaction&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.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;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/quartzcore/camediatiming&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666445356453&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;Apple Developer Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming&quot; data-og-url=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/quartzcore/camediatiming&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666445362754&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;Advanced Animation Tricks&quot; data-og-description=&quot;Advanced Animation Tricks There are many ways to configure your property-based or keyframe animations to do more for you. Apps that need to perform multiple animations together or sequentially can use more advanced behaviors to synchronize the timing of th&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&quot; data-og-url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Advanced Animation Tricks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Advanced Animation Tricks There are many ways to configure your property-based or keyframe animations to do more for you. Apps that need to perform multiple animations together or sequentially can use more advanced behaviors to synchronize the timing of th&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/1166405/how-to-specify-an-begintime-for-an-animation-by-using-cftimeinterval&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/1166405/how-to-specify-an-begintime-for-an-animation-by-using-cftimeinterval&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666445873039&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;How to specify an beginTime for an animation by using CFTimeInterval?&quot; data-og-description=&quot;For my understanding, beginTime can be used to say &amp;quot;hey, start at exactly 12:00 'o clock&amp;quot;. But how would I tell this with an CFTimeInterval type? I thought that this one is nothing more than a kind...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/1166405/how-to-specify-an-begintime-for-an-animation-by-using-cftimeinterval&quot; data-og-url=&quot;https://stackoverflow.com/questions/1166405/how-to-specify-an-begintime-for-an-animation-by-using-cftimeinterval&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c0D41K/hyQaoXLmL2/LVr5bmwYvHY6HB3SSn3Aak/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/1166405/how-to-specify-an-begintime-for-an-animation-by-using-cftimeinterval&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/1166405/how-to-specify-an-begintime-for-an-animation-by-using-cftimeinterval&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c0D41K/hyQaoXLmL2/LVr5bmwYvHY6HB3SSn3Aak/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;How to specify an beginTime for an animation by using CFTimeInterval?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;For my understanding, beginTime can be used to say &quot;hey, start at exactly 12:00 'o clock&quot;. But how would I tell this with an CFTimeInterval type? I thought that this one is nothing more than a kind...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;</description>
      <category>iOS</category>
      <category>CATransaction</category>
      <category>CoreAnimation</category>
      <category>UIBezierPath</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/72</guid>
      <comments>https://wodyios.tistory.com/72#entry72comment</comments>
      <pubDate>Sat, 22 Oct 2022 22:43:33 +0900</pubDate>
    </item>
    <item>
      <title>[CoreAnimation] AnchorPoint, position의 관계 (feat. SwiftUI에선 anchortPoint가 없다..?)</title>
      <link>https://wodyios.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;뷰의 레이아웃을 위치시킬 때, 오토레이아웃을 사용하면 사실 frame, anchorPoint 등 레이아웃 관련 프로퍼티는 크게 신경 쓰지 않아도 됩니다. 그런데, 이번 프로젝트에서 frame을 이용해야 하는 조금 어려운 애니메이션을 구현해보는 구현 내용이 있었고 구현을 하다보니 이전에 공부하다 헷갈렸던 내용과 똑같은 곳에서 헷갈렸습니다.&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;layer프로퍼티에 있는 position은 무엇을 나타내는 값일까요? 라는 질문이 나오기까지의 과정을 설명하겠습니다.&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;먼저, Core Animation Basics 문서를 읽다보면 중간에 아래 그림이 나옵니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-21 오후 9.25.32.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;1582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIW0Og/btrPgBXB0Tq/t9cHhkkKYYTK0ynK6l8cKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIW0Og/btrPgBXB0Tq/t9cHhkkKYYTK0ynK6l8cKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIW0Og/btrPgBXB0Tq/t9cHhkkKYYTK0ynK6l8cKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIW0Og%2FbtrPgBXB0Tq%2Ft9cHhkkKYYTK0ynK6l8cKK%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;600&quot; height=&quot;634&quot; data-filename=&quot;스크린샷 2022-10-21 오후 9.25.32.png&quot; data-origin-width=&quot;1498&quot; data-origin-height=&quot;1582&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&quot;&lt;b&gt;anchor point&lt;/b&gt;가 layer의 &lt;b&gt;position&lt;/b&gt;에 얼마나 영향을 미치는지&quot;에 대한 사진입니다.&amp;nbsp;그리고 위에 부가설명이 적혀있었습니다.&amp;nbsp;&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;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Figure 1-5&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;demonstrates how changing the anchor point from its default value to a different value affects the&amp;nbsp;&lt;/span&gt;position&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;property of a layer. Even though the layer has not moved within its parents&amp;rsquo; bounds, moving the anchor point from the center of the layer to the layer&amp;rsquo;s bounds origin changes the value in the&amp;nbsp;&lt;/span&gt;position&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;property&lt;/span&gt;&lt;/span&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;위 말을 마지막 문장을 보시면, &quot;앵커포인트를 (0.5, 0.5), 즉 중앙에서 다른 곳으로 옮기는 행위는 position 프로퍼티 값을 바꾼다&quot; 라고 이해가 됩니다. 하지만 실제로 코드를 작성해보면 그렇지 않았습니다.&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;아래 코드에서 position 프로퍼티 값의 결과는 (0,0)으로 일정했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1666364412632&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let layer = CAShapeLayer()
layer.path = UIBezierPath(
	rect: .init(origin: .init(x: view.center.x-100, y: view.center.y-100), 
    size: .init(width: 200, height: 200)
)).cgPath
layer.fillColor = UIColor.blue.cgColor

print(layer.position)
layer.anchorPoint = .init(x: 0, y: 0)
print(layer.position)

view.layer.addSublayer(layer)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;2104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cduwmT/btrPhpo0Hmj/SHiiWzgTKajF7t7wsMiS2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cduwmT/btrPhpo0Hmj/SHiiWzgTKajF7t7wsMiS2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cduwmT/btrPhpo0Hmj/SHiiWzgTKajF7t7wsMiS2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcduwmT%2FbtrPhpo0Hmj%2FSHiiWzgTKajF7t7wsMiS2K%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;300&quot; height=&quot;565&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;2104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.00.58.png&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK94pb/btrPh81BMYV/xIQF3IIrbvx1YI6aLIoqv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK94pb/btrPh81BMYV/xIQF3IIrbvx1YI6aLIoqv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK94pb/btrPh81BMYV/xIQF3IIrbvx1YI6aLIoqv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK94pb%2FbtrPh81BMYV%2FxIQF3IIrbvx1YI6aLIoqv1%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;242&quot; height=&quot;164&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.00.58.png&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;164&quot;/&gt;&lt;/span&gt;&lt;/figure&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;먼저, position의 정의를 다시 알아봤습니다.&amp;nbsp;&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;position은 레이어의 프로퍼티이고 자신의 부모 레이어의 좌표계내에서의 위치를 나타낸다고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Discussion에서 새로운 레이어는 기본적으로 position이 (0.0, 0.0)으로 설정된다고 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGaiNb/btrPfnTOzIQ/MPKRulFw7C2nndfJ7UhyZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGaiNb/btrPfnTOzIQ/MPKRulFw7C2nndfJ7UhyZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGaiNb/btrPfnTOzIQ/MPKRulFw7C2nndfJ7UhyZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGaiNb%2FbtrPfnTOzIQ%2FMPKRulFw7C2nndfJ7UhyZ0%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;1914&quot; height=&quot;442&quot; data-origin-width=&quot;1914&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&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;아, 그럼 새로운 레이어를 추가했기 때문에 (0,0)으로 초기화되었고 새 레이어를 추가해서 예제를 만들면 안되겠다는 사실을 알았습니다.&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;pre id=&quot;code_1666365402928&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let view1 = UIView(frame: .init(
    origin: .init(x: view.center.x-100, y: view.center.y-100),
    size: .init(width: 200, height: 200)
))
view1.backgroundColor = .blue
view.addSubview(view1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 작성한 후, 레이어의 앵커포인트를 (0,0)으로 수정했습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666365502222&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;view1.layer.anchorPoint = .init(x: 0, y: 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그랬더니 웬걸, 뷰가 &lt;span style=&quot;color: #ee2323;&quot;&gt;오른쪽 아래&lt;/span&gt;로 배치되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.18.32.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;2104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lJ8fc/btrPhmeG4LI/k32NErBACZt7c5FbpDK9l1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lJ8fc/btrPhmeG4LI/k32NErBACZt7c5FbpDK9l1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lJ8fc/btrPhmeG4LI/k32NErBACZt7c5FbpDK9l1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlJ8fc%2FbtrPhmeG4LI%2Fk32NErBACZt7c5FbpDK9l1%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;300&quot; height=&quot;565&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.18.32.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;2104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;position을 앵커포인트가 바뀌기 전과 바뀐 후와 비교해보니, (195.0, 422.0)로 일정했습니다. 해당 좌표는 핸드폰 중앙입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.19.47.png&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IdOkX/btrPfXAec93/kORpKyaY4AMZybgqvZSsWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IdOkX/btrPfXAec93/kORpKyaY4AMZybgqvZSsWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IdOkX/btrPfXAec93/kORpKyaY4AMZybgqvZSsWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIdOkX%2FbtrPfXAec93%2FkORpKyaY4AMZybgqvZSsWK%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;272&quot; height=&quot;130&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.19.47.png&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;color: #ee2323;&quot;&gt;position은 anchorPoint를 변하게해도 일정하다&lt;/span&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사진에선, anchorPoint가 (0.5, 0.5) -&amp;gt; (0,0)으로 바뀌었을 때,&amp;nbsp;position은 (100.0, 100.0) -&amp;gt; (40.0, 60.0) 으로 좌표값이 변했습니다. 하지만 이것은 완전히 상대적 값입니다. &quot;position은 이제 anchorPoint가 바뀌면서 중앙에서 왼쪽 위로 변했다&quot;라고 사진에서는 보여 준것입니다. 그것을 이해가 쉽게 하기 위해 좌표를 통해 보여준 것입니다. 처음에 봤을 때, 아마 모든 사람들이 position 값이 변하는 줄 알았을 것이라고 생각합니다.. 이 생각이 맞는 지 실험을 해보았습니다.&amp;nbsp;&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;위 상황처럼 뷰의 anchorPoint를 (0.0, 0.0)으로 수정한 상황에서, position을 (0,0)으로 수정해봤습니다. (혹시나, 레이어와 뷰가 따로 노는가 싶어서 레이어의 테두리에도 색깔을 주었습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1666366348168&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let view1 = UIView(frame: .init(
    origin: .init(x: view.center.x-100, y: view.center.y-100),
    size: .init(width: 200, height: 200)
))
view1.backgroundColor = .blue
view.addSubview(view1)
view1.layer.anchorPoint = .init(x: 0, y: 0)
view1.layer.position = .init(x: 0, y: 0)
view1.layer.borderColor = UIColor.red.cgColor
view1.layer.borderWidth = 4&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;스크린샷 2022-10-22 오전 12.31.50.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;2104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vytHS/btrPfPvINEp/KZeb484J4Zz2GrhpkPC0k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vytHS/btrPfPvINEp/KZeb484J4Zz2GrhpkPC0k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vytHS/btrPfPvINEp/KZeb484J4Zz2GrhpkPC0k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvytHS%2FbtrPfPvINEp%2FKZeb484J4Zz2GrhpkPC0k0%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;300&quot; height=&quot;565&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.31.50.png&quot; data-origin-width=&quot;1118&quot; data-origin-height=&quot;2104&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&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;u&gt;position은 anchorPoint를 명시적으로 나타내는 좌표라고 할 수 있습니다. 따라서 position을 변경하면 뷰는 움직입니다. 하지만 anchorPoint를 바꾼다고 뷰가 움직이지는 않습니다.&amp;nbsp;&lt;/u&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;이 anchorPoint는 transform에서 의미를 가집니다. 해당 문서를 더 읽어보면 아래의 사진을 보실 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;anchor point가 transform 영향을 어떻게 미치는지에 대한 사진으로 회전축이라고 생각하시면 됩니다. 이 회전축은 뷰의 비율(0~1)로 생각하고 정의를 하고 비율이므로 실제 anchorPoint의 좌표는 position이 되는 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.35.44.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;1406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYesr/btrPgLzj3sf/XRcrJCZ3e1Oo6mCBNJKwhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYesr/btrPgLzj3sf/XRcrJCZ3e1Oo6mCBNJKwhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYesr/btrPgLzj3sf/XRcrJCZ3e1Oo6mCBNJKwhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYesr%2FbtrPgLzj3sf%2FXRcrJCZ3e1Oo6mCBNJKwhK%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;600&quot; height=&quot;584&quot; data-filename=&quot;스크린샷 2022-10-22 오전 12.35.44.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;1406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SwiftUI?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 아주 조금만 더 나아가서, SwiftUI에서도 position과 anchorPoint라는 개념이 있는 지 궁금해져서 조금 더 찾아봤습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;position이라는 뷰모디파이어는 찾을 수 있었습니다. 하지만, anchorPoint는 찾을 수 없었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1666367144243&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Text(&quot;Position by passing a CGPoint()&quot;)
    .position(CGPoint(x: 175, y: 100))
    .border(Color.gray)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 SwiftUI에서는 뷰의 anchorPoint를 바꾸기 위해서는 어떻게 할까요? 라는 질문이 자연스레 나올 수 있습니다.. 네.. 이게 정상입니다..&amp;nbsp;&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;그래서 검색해봤고.. 스택오버플로우에서 하나의 예시를 찾을 수 있었습니다. 하나의 예시라고 한 이유는 SwiftUI에서는 관점을 바꿔서 생각해야했기 때문입니다. (그래서 다양한 방법으로 애니메이션을 줄 수 있기 때문입니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/57442995/anchor-point-in-swiftui&quot;&gt;https://stackoverflow.com/questions/57442995/anchor-point-in-swiftui&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666367692184&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;Anchor point in SwiftUI&quot; data-og-description=&quot;I'm playing with SwiftUI animations. I wanted to make a square bigger modifying its frame with an animation this way: struct ContentView: View { let squareWidth = CGFloat(100) let squareHe...&quot; data-og-host=&quot;stackoverflow.com&quot; data-og-source-url=&quot;https://stackoverflow.com/questions/57442995/anchor-point-in-swiftui&quot; data-og-url=&quot;https://stackoverflow.com/questions/57442995/anchor-point-in-swiftui&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b0pjcG/hyQby6tjz3/iiP4aaF2NfBJlYNGiorKQ1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/57442995/anchor-point-in-swiftui&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://stackoverflow.com/questions/57442995/anchor-point-in-swiftui&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b0pjcG/hyQby6tjz3/iiP4aaF2NfBJlYNGiorKQ1/img.png?width=316&amp;amp;height=316&amp;amp;face=0_0_316_316');&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;Anchor point in SwiftUI&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;I'm playing with SwiftUI animations. I wanted to make a square bigger modifying its frame with an animation this way: struct ContentView: View { let squareWidth = CGFloat(100) let squareHe...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;stackoverflow.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;(UIKit언어로) anchorPoint가 (0.5, 0.5)일 때,&lt;/p&gt;
&lt;pre id=&quot;code_1666367804855&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;VStack {
    Color.green
        .frame(width: bigger ? self.squareWidth * 2 : self.squareWidth, height: self.squareHeight)
        .animation(.default, value: bigger)
    Button(action: {
            self.bigger.toggle()
    }) {
        Text(&quot;Click me!&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsSqLk/btrPfODBVvZ/sNMiKRreZJ1znKco1JX5Hk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsSqLk/btrPfODBVvZ/sNMiKRreZJ1znKco1JX5Hk/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsSqLk/btrPfODBVvZ/sNMiKRreZJ1znKco1JX5Hk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dsSqLk/btrPfODBVvZ/sNMiKRreZJ1znKco1JX5Hk/img.gif&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;400&quot; height=&quot;233&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(UIKit언어로) anchorPoint가 (0.0, 0.5) 일 때,  &lt;/p&gt;
&lt;pre id=&quot;code_1666367682598&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HStack {
    VStack {
        Color.green
            .frame(width: bigger ? self.squareWidth * 2 : self.squareWidth)
            .frame(height: self.squareHeight)
            .animation(.default, value: bigg
        Button(&quot;Click me!&quot;) { self.bigger.toggle() }
            .animation(.default, value: bigger)
  
    Space()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddYrJA/btrPfDWjTMq/DcavWZ6CU24tKJadkcGMT1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddYrJA/btrPfDWjTMq/DcavWZ6CU24tKJadkcGMT1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddYrJA/btrPfDWjTMq/DcavWZ6CU24tKJadkcGMT1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/ddYrJA/btrPfDWjTMq/DcavWZ6CU24tKJadkcGMT1/img.gif&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;400&quot; height=&quot;233&quot; data-origin-width=&quot;674&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;505&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmsdKA/btrPfDIROkM/EzdVVG8DZda6JNfTdDSmA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmsdKA/btrPfDIROkM/EzdVVG8DZda6JNfTdDSmA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmsdKA/btrPfDIROkM/EzdVVG8DZda6JNfTdDSmA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmsdKA%2FbtrPfDIROkM%2FEzdVVG8DZda6JNfTdDSmA1%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;500&quot; height=&quot;281&quot; data-origin-width=&quot;897&quot; data-origin-height=&quot;505&quot;/&gt;&lt;/span&gt;&lt;/figure&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;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666366947958&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;Core Animation Basics&quot; data-og-description=&quot;Core Animation Basics Core Animation provides a general purpose system for animating views and other visual elements of your app. Core Animation is not a replacement for your app&amp;rsquo;s views. Instead, it is a technology that integrates with views to provide &quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3&quot; data-og-url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/CoreAnimationBasics/CoreAnimationBasics.html#//apple_ref/doc/uid/TP40004514-CH2-SW3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Core Animation Basics&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Core Animation Basics Core Animation provides a general purpose system for animating views and other visual elements of your app. Core Animation is not a replacement for your app&amp;rsquo;s views. Instead, it is a technology that integrates with views to provide&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/calayer/1410791-position&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/quartzcore/calayer/1410791-position&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1666368020059&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;Apple Developer Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/quartzcore/calayer/1410791-position&quot; data-og-url=&quot;https://developer.apple.com/documentation/quartzcore/calayer/1410791-position&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/quartzcore/calayer/1410791-position&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/quartzcore/calayer/1410791-position&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swiftui/view/position(x:y:)&quot;&gt;https://developer.apple.com/documentation/swiftui/view/position(x:y:)&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1666368023802&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;Apple Developer Documentation&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.apple.com&quot; data-og-source-url=&quot;https://developer.apple.com/documentation/swiftui/view/position(x:y:)&quot; data-og-url=&quot;https://developer.apple.com/documentation/swiftui/view/position(x:y:)&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/swiftui/view/position(x:y:)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.apple.com/documentation/swiftui/view/position(x:y:)&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;Apple Developer Documentation&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.apple.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;</description>
      <category>iOS</category>
      <author>jaelee</author>
      <guid isPermaLink="true">https://wodyios.tistory.com/71</guid>
      <comments>https://wodyios.tistory.com/71#entry71comment</comments>
      <pubDate>Sat, 22 Oct 2022 01:00:35 +0900</pubDate>
    </item>
  </channel>
</rss>