Jekyll2022-01-30T23:17:53+09:00https://zetablog.io/feed.xmlZeta의 좌충우돌 삽질 분투기수학, 과학, 컴퓨터 과학 등 원하는 내용에 대한 포스팅을 합니다. 한 번한 삽칠을 되풀이하지 않기 위해, 그리고 다른 분들도 같은 삽질을 하지 않으시도록 블로그 형태로 기록을 합니다.
Zetajaeho.lee@snu.ac.krSwiftUI: 프랙탈과 애니메이션2020-04-25T00:00:00+09:002020-04-25T00:00:00+09:00https://zetablog.io/posts/fractals-in-swiftui<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/banner-fractal-tree.png" width="75%" alt="Fractal Tree" />
</p>
<h1 id="도입">도입</h1>
<blockquote>
<p>프랙탈(fractal) 또는 프랙털은 일부 작은 조각이 전체와 비슷한 기하학적 형태를
말한다.
이런 특징을 자기 유사성이라고 하며, 다시 말해 자기 유사성을 갖는 기하학적
구조를 프랙탈 구조라고 한다.
– <a href="https://ko.wikipedia.org/wiki/%ED%94%84%EB%9E%99%ED%83%88">한글 위키백과</a></p>
</blockquote>
<p>프랙탈 자체에 관하여는 위키백과가 훌륭히 서술을 하고 있으니, 여기서는 자세히
설명하지 않겠다.
코딩의 관점에서 프랙탈은 기하적으로 재귀적(recursive)인 형태를 만드는 것이기
때문에, 반복문이나 재귀 호출을 연습할 때 흔히 쓰이는 예제이다.
(흔히 <a href="https://www.acmicpc.net/problem/2438"><em>별찍기</em></a> 문제라고 불리우는 것들 중
<a href="https://www.acmicpc.net/problem/2447">어려운</a>
<a href="https://www.acmicpc.net/problem/2448">축에</a>
<a href="https://www.acmicpc.net/problem/10993">속하는</a>
<a href="https://www.acmicpc.net/problem/10994">문제들</a>)</p>
<p>본 글에서는 Mandelbrot 집합 같은 프랙탈이 아닌, 단순한 Sierpinski 삼각형이나
카펫의 형태를 다룬다.
먼저 명령형(imperative) 방식과 선언형(declarative) 방식을 비교한 후,
재귀(recursion)에 대해 간단히 소개한다.
이후 SwiftUI의 재귀적 View 구성으로 Sierpinski 카펫과 삼각형을 구성해본다.
그러나 이 방식은 성능면에서 한계가 있는데,
<a href="https://developer.apple.com/documentation/swiftui/shape"><code class="language-plaintext highlighter-rouge">Shape</code></a>를 사용한
다른 방식으로 접근하여 fractal tree (본 글의 맨 위 스크린 샷)를 구현해본다.
나아가, SwiftUI의
<a href="https://developer.apple.com/documentation/swiftui/animatable"><code class="language-plaintext highlighter-rouge">Animatable</code></a> 을
활용하여 fractal tree의 애니메이션을 구현한다.
완성된 결과는 아래 GIF와 같으며, 전체 결과물은
<a href="https://github.com/Zeta611/SwiftUI-Fractals">GitHub</a>에서 확인할 수 있다.</p>
<p><img src="/assets/images/2020-04-25/animating-fractal-tree.gif" alt="Animating Fractal Tree" class="shadow" /></p>
<h1 id="명령형과-선언형">명령형과 선언형</h1>
<p>진행하기에 앞서, 본 글에서는 명령형과 절차적 프로그래밍을 같은 의미로 혼용하여
사용할 것이라는 것을 알린다.
(후자는 함수나 <code class="language-plaintext highlighter-rouge">for</code> 문과 같은 control flow를 사용한다는 의미를 포함하기에
약간의 차이가 있지만, 본 글의 맥락에서는 큰 문제가 되지 않는다.)</p>
<h2 id="절차적으로-프랙탈-별찍기">절차적으로 프랙탈 ‘별찍기’</h2>
<p>아래 코드는 Sierpinski 삼각형 형태의 ‘별찍기’ 코드이다.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sierpinski</span><span class="p">(</span><span class="n">step</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
<span class="k">if</span> <span class="n">step</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[</span><span class="s">" * "</span><span class="p">,</span> <span class="s">" * * "</span><span class="p">,</span> <span class="s">"*****"</span><span class="p">]</span>
<span class="n">previous</span> <span class="o">=</span> <span class="n">sierpinski</span><span class="p">(</span><span class="n">step</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">mid</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">previous</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">//</span> <span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">mid</span><span class="p">):</span>
<span class="n">result</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="s">" "</span> <span class="o">*</span> <span class="n">mid</span> <span class="o">+</span> <span class="n">previous</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">*</span> <span class="n">mid</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">mid</span><span class="p">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">mid</span><span class="p">):</span>
<span class="n">result</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">previous</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="n">mid</span><span class="p">]</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">+</span> <span class="n">previous</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="n">mid</span><span class="p">])</span>
<span class="k">return</span> <span class="n">result</span>
<span class="n">m</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">input</span><span class="p">())</span>
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">sierpinski</span><span class="p">(</span><span class="n">m</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
</code></pre></div></div>
<p>Python을 모르더라도 Swift를 안다면 읽는데 무리가 없을 간단한 코드이다.
(SwiftUI 글에 왜 Python 코드냐면, 이미 필자가 Python 2로 예전에 짜둔 것이 있어서
그냥 Python 3로 옮겨왔다.)
위 코드에 4를 입력으로 주면 아래와 같이 출력된다:</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/python-output.png" width="50%" alt="Python 별찍기" />
</p>
<p>뭐 상당히 싱거운 결과긴 하지만, 위 코드에서 강조하고 싶은 것은 그
<strong>절차적인 방식</strong>이다.
위 코드는 <code class="language-plaintext highlighter-rouge">for</code> 문을 사용해 Sierpinski 삼각형의 구조를 쪼갠 후, list에 더하는
것을 반복한다.
그런데 다시 생각해보면, 우리는 어떤 구조 안에 그 구조가 다시 반복되는 형태를
만들고 싶었던 것 뿐이다.
Sierpinski 삼각형은 그 구조가 세 꼭짓점 쪽에 무한히 반복되는 것이고 말이다.</p>
<p>만약 우리의 언어가 MS사의 PowerPoint였다면, 다음 영상에서처럼 간단히 삼각형의
위치를 드래그해 정해주는 것만으로도 Sierpinski 삼각형을 구현할 수 있었을
것이다:</p>
<p align="center">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/b-Fa6HtvGtQ?start=582" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</p>
<p>(여담이지만, <a href="https://youtu.be/sdkxWqsk17c">PowerPoint는 Turing 완전하다!</a>)</p>
<p>Python 예시와 PowerPoint 예시의 차이점은, 후자는 선언적인(declarative) 성격을
가지는 반면 전자는 절차를 직접 명령했다는 것이다.</p>
<h2 id="for-문과-재귀-호출">for 문과 재귀 호출</h2>
<p>본격적으로 SwiftUI에서 재귀적인 View를 구현하기 전에, 간단한 <code class="language-plaintext highlighter-rouge">for</code> 문과 재귀
호출을 비교해보도록 하자.
1부터 <code class="language-plaintext highlighter-rouge">n</code>까지를 출력하는 코드는 Swift에서 다음과 같이 작성할 수 있다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">1</span><span class="o">...</span><span class="n">n</span> <span class="p">{</span>
<span class="nf">print</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>여기에 작성하기 민망할 정도로 간단한 코드이긴 하지만, OCaml과 같은 함수형
언어에서는 (물론 <code class="language-plaintext highlighter-rouge">for</code>문이 있기는 하지만 제한적이다) 보통 이런 간단한 작업을
포함한 모든 반복을 재귀 함수로 처리한다.
간단히 써보면,</p>
<div class="language-ocaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="k">rec</span> <span class="n">loop</span> <span class="n">n</span> <span class="o">=</span>
<span class="k">if</span> <span class="n">n</span> <span class="o"><</span> <span class="mi">1</span> <span class="k">then</span> <span class="bp">()</span> <span class="k">else</span> <span class="p">(</span>
<span class="n">loop</span> <span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
<span class="n">print_int</span> <span class="n">n</span><span class="p">;</span>
<span class="n">print_newline</span> <span class="bp">()</span>
<span class="p">)</span>
</code></pre></div></div>
<p>와 같다.
재귀 호출에서 핵심은, 1부터 <code class="language-plaintext highlighter-rouge">n</code>까지 출력을 <em>어떻게</em> 하는지가 아니라 <em>무엇을</em>
하는지 강조하는 것이다.
1부터 <code class="language-plaintext highlighter-rouge">n</code>까지 출력하는 것은 1부터 <code class="language-plaintext highlighter-rouge">n - 1</code>까지 출력하기를 한 후, <code class="language-plaintext highlighter-rouge">n</code>을 출력하는
것이다.
이때 필요한 것은 종료 조건인데, 입력이 1보다 작은 값은 아무것도 하지 않으면
된다.
OCaml이라는 언어가 생소한 독자분들이 대다수겠지만, 위 코드에서는 이러한
<strong>재귀적인 사고 방식</strong>이 명확하게 드러날 것이다.</p>
<h2 id="swiftui에서-재귀적-view-구성하기">SwiftUI에서 재귀적 View 구성하기</h2>
<p>다시 SwiftUI로 돌아와서, <a href="https://zetablog.ml/posts/nested-aligning-in-swiftui">SwiftUI의 가장 큰 특징은 UIKit과 달리 선언적으로
layout을 정의할 수 있다는 점</a>이다.
<code class="language-plaintext highlighter-rouge">var body: some View { }</code>안에 넣는 내용이 실제로 화면에 render되는 결과이다.</p>
<p>위의 <code class="language-plaintext highlighter-rouge">for</code> 문과 재귀 호출에 썼던 예시를 가져와서, SwiftUI에서 1부터 <code class="language-plaintext highlighter-rouge">n</code>을
‘출력’하려면 어떻게 해야 할까?
SwiftUI의
<a href="https://developer.apple.com/documentation/swiftui/foreach"><code class="language-plaintext highlighter-rouge">ForEach</code></a>를
사용하면 된다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">n</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span> <span class="p">{</span>
<span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..<</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="nv">$0</span> <span class="o">+</span> <span class="mi">1</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">ContentView(n: 5)</code>의 결과는 다음과 같다:
<img src="/assets/images/2020-04-25/foreach.png" alt="ForEach" class="shadow" /></p>
<p>그런데 오늘 날씨가 안 좋거나해서 기분이 꼬였다면, 이를 재귀로 구현하고 싶은
마음이 들 수도 있다.
위에서 잠깐 보았던 OCaml 코드와 정확히 동일한 구조로 코드를 작성하면 된다!</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">n</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">n</span> <span class="o"><</span> <span class="mi">1</span> <span class="p">{</span>
<span class="kt">EmptyView</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="kt">ContentView</span><span class="p">(</span><span class="nv">n</span><span class="p">:</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">n</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>다만 Swift(UI)에서는 OCaml과 다르게 <code class="language-plaintext highlighter-rouge">if-then-else</code>의 모든 가지를 갖출 필요가
없기 때문에, <code class="language-plaintext highlighter-rouge">VStack</code>안을 다음과 같이 간략화할 수 있다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">n</span> <span class="o">>=</span> <span class="mi">1</span> <span class="p">{</span>
<span class="kt">ContentView</span><span class="p">(</span><span class="nv">n</span><span class="p">:</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">n</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>결과는 <code class="language-plaintext highlighter-rouge">ForEach</code>를 썼을 때와 완전히 동일하다.</p>
<h1 id="sierpinski-카펫과-삼각형">Sierpinski 카펫과 삼각형</h1>
<h2 id="sierpinski-카펫">Sierpinski 카펫</h2>
<p>먼저 Sierpinski 카펫을 구현해보자.
자기 자신의 형태를 첫 줄에 세 개, 둘째 줄에 양 끝에 두 개, 마지막 줄에 세 개를
넣는 방법이 가장 먼저 생각날 것이다.
다만 둘째 줄에 가운데 <code class="language-plaintext highlighter-rouge">Spacer()</code>을 통해 두 view를 분리한다면, 첫 줄과 마지막
줄에 포함된 view와 크기가 다르게 나온다.
따라서 둘째 줄에도 자기 자신을 세 개 넣는 대신, 가운데의 view는 보이지 않게
<code class="language-plaintext highlighter-rouge">.hidden()</code>을 사용하여 크기를 맞춘다.</p>
<p>다음과 같이 구현하면:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Carpet</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">n</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">child</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Carpet</span><span class="p">(</span><span class="nv">n</span><span class="p">:</span> <span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">}</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">defaultRow</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span> <span class="n">child</span><span class="p">;</span> <span class="n">child</span><span class="p">;</span> <span class="n">child</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">middleRow</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span> <span class="n">child</span><span class="p">;</span> <span class="n">child</span><span class="o">.</span><span class="nf">hidden</span><span class="p">();</span> <span class="n">child</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">n</span> <span class="o">></span> <span class="mi">0</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">defaultRow</span><span class="p">;</span> <span class="n">middleRow</span><span class="p">;</span> <span class="n">defaultRow</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">asAnyView</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">Rectangle</span><span class="p">()</span>
<span class="o">.</span><span class="nf">aspectRatio</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nv">contentMode</span><span class="p">:</span> <span class="o">.</span><span class="n">fit</span><span class="p">)</span>
<span class="o">.</span><span class="nf">asAnyView</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">n = 0</code>부터 <code class="language-plaintext highlighter-rouge">5</code>까지 아래와 같은 결과가 나온다.</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/carpet0.png" width="30%" alt="Sierpinski Carpet 0" />
<img src="/assets/images/2020-04-25/carpet1.png" width="30%" alt="Sierpinski Carpet 1" />
<img src="/assets/images/2020-04-25/carpet2.png" width="30%" alt="Sierpinski Carpet 2" />
<br /><br />
<img src="/assets/images/2020-04-25/carpet3.png" width="30%" alt="Sierpinski Carpet 3" />
<img src="/assets/images/2020-04-25/carpet4.png" width="30%" alt="Sierpinski Carpet 4" />
<img src="/assets/images/2020-04-25/carpet5.png" width="30%" alt="Sierpinski Carpet 4" />
</p>
<p>이때 <code class="language-plaintext highlighter-rouge">.asAnyView()</code>는 <code class="language-plaintext highlighter-rouge">AnyView</code>로 감싸주는 extension이다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">asAnyView</span><span class="p">()</span> <span class="o">-></span> <span class="kt">AnyView</span> <span class="p">{</span>
<span class="kt">AnyView</span><span class="p">(</span><span class="k">self</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>이는 Swift의
<a href="https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html">opaque 반환값</a>을
요구하는 함수에서는 반환값이 모두 일치해야하기 때문에 <code class="language-plaintext highlighter-rouge">AnyView</code>로 타입을 지워준
것이다.
그렇지 않다면 위 코드의 <code class="language-plaintext highlighter-rouge">if</code> 문 안에서는 <code class="language-plaintext highlighter-rouge">VStack</code>으로 감싸진 <code class="language-plaintext highlighter-rouge">some View</code>,
<code class="language-plaintext highlighter-rouge">else</code> 문에서는 <code class="language-plaintext highlighter-rouge">.aspectRatio(...)</code>가 반환하는 또 다른 종류의 <code class="language-plaintext highlighter-rouge">some View</code>를
반환하여 문제가 된다.</p>
<p>다시 코드 내용으로 돌아가서, 위 코드의 종료 조건은 <code class="language-plaintext highlighter-rouge">else</code> 문의 <code class="language-plaintext highlighter-rouge">Rectangle</code>을
그리는 코드이다.
위 결과를 보면 알겠지만, 실제로 사각형을 그리는 부분은 재귀 호출에서 마지막
단계이고, 그 전 단계들은 모두 ‘배치’를 하는 과정이다.
실제로 Xcode에서 확인할 독자들이 계신다면, <code class="language-plaintext highlighter-rouge">n = 4</code>인 경우 그냥 Canvas에서는
render에 5초가 넘게 걸려 오류가 나기 때문에 Live Preview를 사용해서 확인을
하였다.</p>
<p>그리고
<a href="https://github.com/Zeta611/SwiftUI-Fractals/blob/master/Fractals/Shapes/Carpet.swift">GitHub에 있는 버전</a>은
위의 코드에서 변경점이 다소 있다.
해당 버전은 사각형의 크기에 비례하는 spacing을 주고, 각 사격형이 회전했을 때
서로 겹치지 않도록 <code class="language-plaintext highlighter-rouge">Rectangle</code>이 아니라 후술할 custom <code class="language-plaintext highlighter-rouge">Shape</code>을 사용하였다.
또한 <code class="language-plaintext highlighter-rouge">n</code>이 커졌을 때 사각형들의 끝이 일렬로 오지 않는 현상을 최대한 해결하기
위해 (위에서 사용하지 않았던) <code class="language-plaintext highlighter-rouge">Spacer()</code>과 <code class="language-plaintext highlighter-rouge">GeometryReader</code>의 조합을 사용하여
구현했다.
<code class="language-plaintext highlighter-rouge">GeometryReader</code> 등 layout에 관하여는
<a href="https://zetablog.ml/posts/nested-aligning-in-swiftui">이 포스팅</a>에서 자세히
확인할 수 있다.</p>
<h2 id="shape-protocol">Shape Protocol</h2>
<p>SwiftUI에서는 삼각형을 기본적으로 제공하지 않는다.
이를 위해서는 <code class="language-plaintext highlighter-rouge">Shape</code> protocol을 따르는 <code class="language-plaintext highlighter-rouge">struct</code>를 만들어주면 된다.
<code class="language-plaintext highlighter-rouge">Shape</code> protocol을 따르기 위해서는</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span>
</code></pre></div></div>
<p>함수를 구현해주면 된다.
주어진 <code class="language-plaintext highlighter-rouge">CGRect</code> 안에서 자유롭게 그림을 그리면 된다!
이 <em>그림</em>의 type은 <code class="language-plaintext highlighter-rouge">Path</code>인데, 여기서는 세 method만 알면 된다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="k">mutating</span> <span class="kd">func</span> <span class="nf">move</span><span class="p">(</span><span class="n">to</span> <span class="nv">p</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">)</span>
<span class="kd">public</span> <span class="k">mutating</span> <span class="kd">func</span> <span class="nf">addLine</span><span class="p">(</span><span class="n">to</span> <span class="nv">p</span><span class="p">:</span> <span class="kt">CGPoint</span><span class="p">)</span>
<span class="kd">public</span> <span class="k">mutating</span> <span class="kd">func</span> <span class="nf">closeSubpath</span><span class="p">()</span>
</code></pre></div></div>
<p>굉장히 직관적인 이름을 가지는 함수들인데, 그래도 간단히 설명을 붙이자면
<code class="language-plaintext highlighter-rouge">move(to:)</code> 함수는 주어진 <code class="language-plaintext highlighter-rouge">CGPoint</code>로 <code class="language-plaintext highlighter-rouge">Path</code>를 그릴 ‘펜’을 옮기고,
<code class="language-plaintext highlighter-rouge">addLine(to:)</code> 함수는 현재 ‘펜’의 위치에서 주어진 <code class="language-plaintext highlighter-rouge">CGPoint</code>까지 선을 긋고,
<code class="language-plaintext highlighter-rouge">closeSubpath</code>는 현재의 부분 경로의 시작점까지 선을 그려서 닫는 것이다.
직접 사람이 펜을 가지고 그림을 그리는 것과 같은 방법이다.</p>
<h2 id="정다각형-구현">정다각형 구현</h2>
<p><code class="language-plaintext highlighter-rouge">path(in:)</code> 함수를 구현하기 위해서는 먼저 그림을 그릴 도화지, 즉 <code class="language-plaintext highlighter-rouge">CGRect</code>의
범위 안에서 그려야 한다.
만약 여기를 벗어난다면 <code class="language-plaintext highlighter-rouge">Shape</code>의 <code class="language-plaintext highlighter-rouge">frame</code>을 벗어나는 획들이 나올
것이다(<code class="language-plaintext highlighter-rouge">Shape</code>도 <code class="language-plaintext highlighter-rouge">View</code>이다).
따라서 주어진 <code class="language-plaintext highlighter-rouge">CGRect</code>의 변 중 작은 것의 절반을 반지름으로 하는 내접원의
내접하는 정다각형을 그리면 될 것이다:</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/inner-polygon.png" width="75%" alt="Inner Polygon" />
</p>
<p>이를 위하여, 다음과 같이 <code class="language-plaintext highlighter-rouge">CGRect</code>에 간단한 computed property 둘을 추가하였다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">CoreGraphics</span>
<span class="kd">extension</span> <span class="kt">CGRect</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">localCenter</span><span class="p">:</span> <span class="kt">CGPoint</span> <span class="p">{</span> <span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">width</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">minSide</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="nf">min</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">localCenter</code>는 <code class="language-plaintext highlighter-rouge">CGRect</code>의 중심점을 해당 <code class="language-plaintext highlighter-rouge">CGRect</code>의 좌표계에서 표시한 값이다.
즉, 그냥 <code class="language-plaintext highlighter-rouge">width</code>와 <code class="language-plaintext highlighter-rouge">height</code> 각각의 절반을 좌표로 가지는 <code class="language-plaintext highlighter-rouge">CGPoint</code>이다.
또한 <code class="language-plaintext highlighter-rouge">minSide</code>는 <code class="language-plaintext highlighter-rouge">width</code>와 <code class="language-plaintext highlighter-rouge">height</code> 중 작은 값이다.</p>
<p>그리고 원에서 등간격으로 벌어진 <code class="language-plaintext highlighter-rouge">CGPoint</code>들을 쉽게 찍을 수 있도록
<code class="language-plaintext highlighter-rouge">CGPoint</code>에도 유용한 method 둘을 추가하였다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">CGPoint</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">offset</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="mi">0</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGPoint</span> <span class="p">{</span>
<span class="kt">CGPoint</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">x</span> <span class="o">+</span> <span class="k">self</span><span class="o">.</span><span class="n">x</span><span class="p">,</span> <span class="nv">y</span><span class="p">:</span> <span class="n">y</span> <span class="o">+</span> <span class="k">self</span><span class="o">.</span><span class="n">y</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">centeredConcyclic</span><span class="p">(</span><span class="nv">radius</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGPoint</span> <span class="p">{</span>
<span class="nf">offset</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="n">radius</span> <span class="o">*</span> <span class="nf">cos</span><span class="p">(</span><span class="n">angle</span><span class="p">),</span> <span class="nv">y</span><span class="p">:</span> <span class="n">radius</span> <span class="o">*</span> <span class="nf">sin</span><span class="p">(</span><span class="n">angle</span><span class="p">))</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">offset(x:y:)</code> 함수는 그냥 <code class="language-plaintext highlighter-rouge">x</code>와 <code class="language-plaintext highlighter-rouge">y</code>만큼 떨어진 새로운 점을 반환한다.
이때 유의할 점은, <strong>iOS 좌표계의 y축은 화면 위에서 아래로 향하는 방향</strong>이다.
일반적으로 우리가 생각하는 직교 좌표계를 $x$축 대칭한 형태다.
이는 다음 함수인 <code class="language-plaintext highlighter-rouge">centeredConcyclic(radius:angle:)</code>을 사용할 때 유의해야하는
부분인데, <code class="language-plaintext highlighter-rouge">angle</code>은 반시계 방향이 아니라 시계 방향으로 도는 각도이다.
(수학의 극좌표에 익숙하지 않다면 오히려 다행인 것이, 일반적으로 수학에서는
반시계 방향을 기준으로 생각한다.)
극좌표계와 직교좌표계의 변환 공식에서 회전각을 시계 방향으로 생각하면 된다.
즉, $x = r \cos \theta, y = r \sin \theta$를 쓸 때 $\theta$는 시계 방향으로
회전한 각도이다.
정리하자면, <code class="language-plaintext highlighter-rouge">centeredConcyclic(radius:angle:)</code>은 주어진 <code class="language-plaintext highlighter-rouge">CGPoint</code>를
중심으로 하는 반지름 <code class="language-plaintext highlighter-rouge">radius</code>의 원에서, $x$축으로부터 시계 방향으로 회전한
<code class="language-plaintext highlighter-rouge">CGPoint</code>이다.</p>
<p>이제 정다각형을 나타내는 <code class="language-plaintext highlighter-rouge">struct RegularPolygon</code>을 구현할 모든 준비가 되었다.
<code class="language-plaintext highlighter-rouge">RegularPolygon</code>은 변의 개수인 <code class="language-plaintext highlighter-rouge">sides</code>를 받아서 만들도록 구현한다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">RegularPolygon</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">sides</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">radius</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">minSide</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">origin</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">localCenter</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="n">sides</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">cyclicPoint</span> <span class="o">=</span> <span class="n">origin</span><span class="o">.</span><span class="nf">centeredConcyclic</span><span class="p">(</span>
<span class="nv">radius</span><span class="p">:</span> <span class="n">radius</span><span class="p">,</span>
<span class="nv">angle</span><span class="p">:</span> <span class="mi">2</span> <span class="o">*</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> <span class="o">/</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">sides</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">i</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">cyclicPoint</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">cyclicPoint</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">path</span><span class="o">.</span><span class="nf">closeSubpath</span><span class="p">()</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>거의 자명한 코드인데, <code class="language-plaintext highlighter-rouge">for</code> 문 안에서는 $x$축에서부터 시계 방향으로 <code class="language-plaintext highlighter-rouge">sides</code>개의
<code class="language-plaintext highlighter-rouge">CGPoint</code>들을 등간격으로 찍는다.
<code class="language-plaintext highlighter-rouge">cyclicPoint</code>는 한 바퀴에 해당하는 $2\pi$를 <code class="language-plaintext highlighter-rouge">sides</code>등분 한 것이므로 <code class="language-plaintext highlighter-rouge">2 * .pi *
CGFloat(i) / CGFloat(sides)</code>만큼 회전한 것이다.
(이때 <code class="language-plaintext highlighter-rouge">.pi</code>는 <code class="language-plaintext highlighter-rouge">CGFloat.pi</code>인데, <code class="language-plaintext highlighter-rouge">Double.pi</code>도 있고 <code class="language-plaintext highlighter-rouge">Float.pi</code>도 있다.)
<code class="language-plaintext highlighter-rouge">i</code>가 <code class="language-plaintext highlighter-rouge">0</code>인 경우는 처음 시작할 때이므로 <code class="language-plaintext highlighter-rouge">move(to:)</code>로 선을 긋지 않고 이동만
하며, 이후로는 <code class="language-plaintext highlighter-rouge">addLine(to:)</code>로 선을 긋는다.
마지막 꼭짓점을 찍은 후 첫 번째 꼭짓점까지 다시 선분을 연결하기 위해
<code class="language-plaintext highlighter-rouge">closeSubpath()</code>를 호출하면 정다각형이 완성된다.
실은 위의 보라색 오각형이 SwiftUI에서 <code class="language-plaintext highlighter-rouge">RegularPolygon</code>을 사용해 그린 것이다.</p>
<h2 id="sierpinski-삼각형">Sierpinski 삼각형</h2>
<p><code class="language-plaintext highlighter-rouge">Shape</code>을 구현했으니 Sierpinski 삼각형은 금방이다.
특히 Sierpinski 삼각형은 자기 자신의 형태를 위에 하나, 아래에 양 옆으로 두 개만
포함하므로 간단하다.
바로 구현을 제시하자면 아래와 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Triangle</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">child</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Triangle</span><span class="p">(</span><span class="nv">step</span><span class="p">:</span> <span class="n">step</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">}</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">bottomRow</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">HStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">child</span><span class="p">;</span> <span class="n">child</span> <span class="p">}</span>
<span class="p">}</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">step</span> <span class="o">></span> <span class="mi">0</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">GeometryReader</span> <span class="p">{</span> <span class="n">geometry</span> <span class="k">in</span>
<span class="kt">VStack</span><span class="p">(</span><span class="nv">spacing</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">child</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">bottomRow</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">aspectRatio</span><span class="p">(</span><span class="nv">contentMode</span><span class="p">:</span> <span class="o">.</span><span class="n">fit</span><span class="p">)</span>
<span class="o">.</span><span class="nf">asAnyView</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="kt">RegularPolygon</span><span class="p">(</span><span class="nv">sides</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span>
<span class="o">.</span><span class="nf">rotationEffect</span><span class="p">(</span><span class="o">.</span><span class="nf">radians</span><span class="p">(</span><span class="o">.</span><span class="n">pi</span> <span class="o">/</span> <span class="mi">6</span><span class="p">))</span>
<span class="o">.</span><span class="nf">aspectRatio</span><span class="p">(</span><span class="nv">contentMode</span><span class="p">:</span> <span class="o">.</span><span class="n">fit</span><span class="p">)</span>
<span class="o">.</span><span class="nf">asAnyView</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">child</code>는 <code class="language-plaintext highlighter-rouge">step</code>을 하나 줄인 단계의 <code class="language-plaintext highlighter-rouge">Triangle</code>이고, <code class="language-plaintext highlighter-rouge">bottomRow</code>는 아랫줄에 올
<code class="language-plaintext highlighter-rouge">child</code> 두 개의 <code class="language-plaintext highlighter-rouge">HStack</code>이다.
여기서 <code class="language-plaintext highlighter-rouge">Triangle</code>의 비율을 유지하기 위해 <code class="language-plaintext highlighter-rouge">GeometryReader</code>을 사용해 명시적으로
윗줄의 <code class="language-plaintext highlighter-rouge">child</code>는 아랫줄의 <code class="language-plaintext highlighter-rouge">bottomRow</code>의 너비를 각각 전체 너비의 절반, 그리고
동일하게 설정해줬다.
또한 <code class="language-plaintext highlighter-rouge">.aspectRatio(contentMode: .fit)</code>으로 늘어나지 않고 작은 너비에 맞게 비율을
조절하였다.
이는 남은 공간을 모두 채우도록 view를 늘이는 <code class="language-plaintext highlighter-rouge">ContentMode.fill</code>와 함께
<code class="language-plaintext highlighter-rouge">enum ContentMode</code>의 두 <code class="language-plaintext highlighter-rouge">case</code> 중 하나다.</p>
<p>마지막으로 종료 조건은 <code class="language-plaintext highlighter-rouge">else</code> 문의 <code class="language-plaintext highlighter-rouge">RegularPolygon(sides: 3)</code>로 시작하는
View인데, <code class="language-plaintext highlighter-rouge">rotationEffect(.radians(.pi / 6))</code>은 <code class="language-plaintext highlighter-rouge">RegularPolygon</code>의 첫 꼭짓점이
(<code class="language-plaintext highlighter-rouge">RegularPolygon</code>의 중심을 원점으로 하는 local 좌표계의) $x$축 상에 존재하기
때문에 회전시켜준 것이다.
이의 유용한 부작용으로는, <code class="language-plaintext highlighter-rouge">RegularPolygon</code>의 크기에 비례하는 margin이 생긴다는
점이다.
이것은 위에서 Sierpinski 카펫을 다시 <code class="language-plaintext highlighter-rouge">RegularPolygon(sides: 4)</code>로 구현한
이유기도 하다.
또한
<a href="https://github.com/Zeta611/SwiftUI-Fractals/blob/master/Fractals/Shapes/Triangle.swift">GitHub의 버전</a>에서는
각 <code class="language-plaintext highlighter-rouge">RegularPolygon(sides: 3)</code>가 회전할 수 있게 <code class="language-plaintext highlighter-rouge">angle</code>을 주었는데, 처음
<code class="language-plaintext highlighter-rouge">RegularPolygon</code>을 설계할 때 주어진 <code class="language-plaintext highlighter-rouge">CGRect</code>의 <em>내접원에 내접하는</em> 정다각형으로
하였기 때문에 회전하여도 서로 겹치지 않게 된다.
서로 겹치지 않는다는 것은 아래 GIF를 통해 확인할 수 있다:</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/rotating-squares.gif" width="50%" alt="Rotating Squares" />
</p>
<p>구현한 Sierpinski 삼각형은 <code class="language-plaintext highlighter-rouge">step = 0</code>부터 <code class="language-plaintext highlighter-rouge">9</code>에 대해서 아래와 같다:</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/triangle0.png" width="23%" alt="Sierpinski Triangle 0" />
<img src="/assets/images/2020-04-25/triangle1.png" width="23%" alt="Sierpinski Triangle 1" />
<img src="/assets/images/2020-04-25/triangle2.png" width="23%" alt="Sierpinski Triangle 2" />
<img src="/assets/images/2020-04-25/triangle3.png" width="23%" alt="Sierpinski Triangle 3" />
<br /><br />
<img src="/assets/images/2020-04-25/triangle4.png" width="23%" alt="Sierpinski Triangle 4" />
<img src="/assets/images/2020-04-25/triangle5.png" width="23%" alt="Sierpinski Triangle 5" />
<img src="/assets/images/2020-04-25/triangle6.png" width="23%" alt="Sierpinski Triangle 6" />
<img src="/assets/images/2020-04-25/triangle7.png" width="23%" alt="Sierpinski Triangle 7" />
<br /><br />
<img src="/assets/images/2020-04-25/triangle8.png" width="100%" alt="Sierpinski Triangle 8" />
<br /><br />
<img src="/assets/images/2020-04-25/triangle9.png" width="100%" alt="Sierpinski Triangle 9" />
</p>
<p>(GitHub에서 프로젝트를 바로 받아 Xcode로 실행해보면 <code class="language-plaintext highlighter-rouge">step = 9</code>까지는 성능 상의
이유로 늘리지 못하게 해놓았지만, 확인하고 싶다면 <code class="language-plaintext highlighter-rouge">ContentView</code>의 <code class="language-plaintext highlighter-rouge">maxStep</code>을
수정하면 된다.)</p>
<h1 id="fractal-tree와-애니메이션">Fractal Tree와 애니메이션</h1>
<p>마지막으로, fractal tree의 구현을 알아보자.
이를 위해서는 위의 Sierpinski 카펫이나 삼각형과는 접근을 좀 다르게 해야하는데,
선분 등은 <code class="language-plaintext highlighter-rouge">Shape</code> 자체에서 구현을 해야 하기 때문이다.
(아마도, 시도해보지는 않았지만) <code class="language-plaintext highlighter-rouge">path(in:)</code> 자체를 재귀함수로 구현할 수
있겠지만, 위에서 보았듯이 성능 상의 이유로 fractal tree의 구현은 직접
반복문으로 구현하였다.
사실, 위의 두 Sierpinski 도형도 (마찬가지로 시도하지 않았지만) <code class="language-plaintext highlighter-rouge">path(in:)</code>
자체에서 반복문으로 구현할 수 있을 것이다.
다만 애초의 <em>선언적인</em> 방식으로 구성하고 싶었던 목적과는 맞지 않을 뿐이다.</p>
<h2 id="queue">Queue</h2>
<p>Fractal tree의 각 꼭짓점에 도달하면, 다시 해당 꼭짓점에서 fractal tree를 그려
나가야 한다.
그리고 우리는 모든 가지의 깊이를 동일하게 맞추고 싶기 때문에, 너비 우선
탐색으로 각 꼭짓점에 방문하여 fractal tree를 구현하면 좋을 것 같다.
너비 우선 탐색(breadth-first search, 이하 BFS)이란, 자식 꼭짓점에 방문하기
전에 같은 계층의 부모 꼭짓점들을 모두 다 방문하는 것이다.
이 방식으로는 깊이 $n$의 꼭짓점들은 모두 깊이 $n + 1$ 꼭짓점들보다 먼저
방문하게 된다.
예컨대 fractal tree에서는 다음과 같은 순서로 방문하는 것이다:</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/bfs.png" width="75%" alt="BFS" />
</p>
<p>사실 우리는 각 꼭짓점에 정확히 세 개의 변들이 연결된 것을 알고 있고, 정확히는
탐색을 하는 것이 아니라 <em>생성</em>을 하는 것이므로 완전한 BFS 알고리즘을 구현할
필요가 없다.</p>
<p>일단 BFS를 위해서
<a href="https://en.wikipedia.org/wiki/Queue_(abstract_data_type)">queue</a>를 구현해야
한다.
Swift에는 안타깝게도 일반적인 queue 자료 구조가 없기 때문에 직접 구현해야 한다.
여기서는 queue에 들어갈 원소의 적당한 상한 값을 이미 알 수 있으므로 (생성하고
싶은 깊이를 미리 정하므로), 그리고 병목은 자료의 수가 아니라 SwiftUI 그
자체이므로, 연결 리스트가 아니라 배열 기반의 구현을 하였다.</p>
<p>개략적으로 설명하자면, 먼저 담을 수 있는 최대 크기를 정한 후 해당 크기의
<code class="language-plaintext highlighter-rouge">Array</code>를 초기화한다.
원소를 queue에 추가(enqueue)할 때마다 <code class="language-plaintext highlighter-rouge">tail</code> index를 하나씩 증가시키고, 반대로
원소를 제거(dequeue)할 때마다 <code class="language-plaintext highlighter-rouge">head</code> index를 하나씩 증가시켜 queue의 처음
위치와 마지막 위치를 표시한다.
Index를 증가시키는데 배열의 끝에 도달하면, 다시 index 0으로 ‘감싼다’.
이때 <code class="language-plaintext highlighter-rouge">tail</code>은 마지막 원소의 다음 자리를 표시하며, queue가 비어있을 때는
<code class="language-plaintext highlighter-rouge">head</code>와 <code class="language-plaintext highlighter-rouge">tail</code>이 같을 때이다.
따라서 이 queue 구현은 <code class="language-plaintext highlighter-rouge">Array</code>의 크기보다 하나 작은 수의 원소를 담을 수 있다.</p>
<p>실제 Swift 구현은 아래와 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Queue</span><span class="o"><</span><span class="kt">Element</span><span class="o">></span> <span class="p">{</span>
<span class="c1">/// A queue can store at most `capacity - 1` elements</span>
<span class="k">let</span> <span class="nv">capacity</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">container</span><span class="p">:</span> <span class="p">[</span><span class="kt">Element</span><span class="p">?]</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">head</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">tail</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">isEmpty</span><span class="p">:</span> <span class="kt">Bool</span> <span class="p">{</span> <span class="n">head</span> <span class="o">==</span> <span class="n">tail</span> <span class="p">}</span>
<span class="k">mutating</span> <span class="kd">func</span> <span class="nf">enqueue</span><span class="p">(</span><span class="n">_</span> <span class="nv">newElement</span><span class="p">:</span> <span class="kt">Element</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Bool</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">head</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">tail</span> <span class="o">==</span> <span class="n">capacity</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">||</span> <span class="n">head</span> <span class="o">==</span> <span class="n">tail</span> <span class="o">+</span> <span class="mi">1</span> <span class="p">{</span>
<span class="c1">// The queue is full</span>
<span class="k">return</span> <span class="kc">false</span>
<span class="p">}</span>
<span class="n">container</span><span class="p">[</span><span class="n">tail</span><span class="p">]</span> <span class="o">=</span> <span class="n">newElement</span>
<span class="n">tail</span> <span class="o">=</span> <span class="n">tail</span> <span class="o">==</span> <span class="n">capacity</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="n">tail</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">return</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="k">mutating</span> <span class="kd">func</span> <span class="nf">dequeue</span><span class="p">()</span> <span class="o">-></span> <span class="kt">Element</span><span class="p">?</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">head</span> <span class="o">==</span> <span class="n">tail</span> <span class="p">{</span>
<span class="c1">// The queue is empty</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="n">container</span><span class="p">[</span><span class="n">head</span><span class="p">]</span> <span class="k">else</span> <span class="p">{</span>
<span class="nf">assertionFailure</span><span class="p">(</span><span class="s">"container should not be empty as head != tail"</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="n">head</span> <span class="o">=</span> <span class="n">head</span> <span class="o">==</span> <span class="n">capacity</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">?</span> <span class="mi">0</span> <span class="p">:</span> <span class="n">head</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="n">_</span> <span class="nv">array</span><span class="p">:</span> <span class="p">[</span><span class="kt">Element</span><span class="p">]</span> <span class="o">=</span> <span class="p">[],</span> <span class="nv">capacity</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">precondition</span><span class="p">(</span>
<span class="n">capacity</span> <span class="o">></span> <span class="n">array</span><span class="o">.</span><span class="n">count</span><span class="p">,</span>
<span class="s">"capacity must be greater than the number of elements in the array"</span>
<span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">capacity</span> <span class="o">=</span> <span class="n">capacity</span>
<span class="n">container</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Element</span><span class="p">?](</span><span class="nv">repeating</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span> <span class="nv">count</span><span class="p">:</span> <span class="n">capacity</span><span class="p">)</span>
<span class="k">for</span> <span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="k">in</span> <span class="n">array</span><span class="o">.</span><span class="nf">enumerated</span><span class="p">()</span> <span class="p">{</span>
<span class="n">container</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">e</span>
<span class="p">}</span>
<span class="n">head</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">tail</span> <span class="o">=</span> <span class="n">array</span><span class="o">.</span><span class="n">count</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Queue</code>는 <code class="language-plaintext highlighter-rouge">Element</code>에 대한 generic으로 구현을 하였고, 내부적으로 가지고 있는
배열 <code class="language-plaintext highlighter-rouge">container</code>은 <code class="language-plaintext highlighter-rouge">Element?</code>의 <code class="language-plaintext highlighter-rouge">Array</code>로, 처음 입력된 값을 빼고는 <code class="language-plaintext highlighter-rouge">nil</code>로
초기화되어 있다.</p>
<p>함수 구현은 위에 설명한 것과 거의 같은데, <code class="language-plaintext highlighter-rouge">enqueue(_:)</code>와 <code class="language-plaintext highlighter-rouge">dequeue()</code> 함수에서는
각각 overflow와 underflow의 경우 예외 처리를 해주었다.
<code class="language-plaintext highlighter-rouge">enqueue(_:)</code>의 경우 반환값이 <code class="language-plaintext highlighter-rouge">false</code>인 경우 queue가 가득 차서 새로운
<code class="language-plaintext highlighter-rouge">Element</code>를 넣지 못한 것이고, <code class="language-plaintext highlighter-rouge">dequeue()</code>의 경우 반환값이 <code class="language-plaintext highlighter-rouge">nil</code>이면 queue에
원소가 없는 경우이다.
또한 dequeue를 한다고 실제 값을 지우는 것은 아니고 <code class="language-plaintext highlighter-rouge">head</code> index만 조절할
뿐이다.
초기화는 편의를 위해 바로 <code class="language-plaintext highlighter-rouge">array: [Element]</code>로부터 만들 수 있게 하였다.</p>
<p>전체적으로 굉장히 단순한 구현이지만, 우리의 목적에는 충분히 부합한다.
Fractal tree 구현을 하기 전에 마지막으로, 유향 선분을 나타내는
<code class="language-plaintext highlighter-rouge">struct DirectedLineSegment</code>를 구현한다.</p>
<h2 id="directedlinesegment">DirectedLineSegment</h2>
<p><code class="language-plaintext highlighter-rouge">CoreGraphics</code>에 <code class="language-plaintext highlighter-rouge">CGVector</code>이 있기는 한데, 이는 크기와 방향만을 가지기 때문에
시작점과 끝점이 필요한 우리의 요구와는 맞지 않다.
따라서 직접 유향 선분을 나타내는 <code class="language-plaintext highlighter-rouge">DirectedLineSegment</code>을 구현하였다.
이는 fractal tree를 만들 때 특정 선분의 방향으로 연장하고 회전하는 것을
반복해야 하기 때문에 유용하다.</p>
<p>가지고 있는 변수는 시작점 <code class="language-plaintext highlighter-rouge">start</code>와 끝점 <code class="language-plaintext highlighter-rouge">end</code>으로 단 두 개이다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">CoreGraphics</span>
<span class="kd">struct</span> <span class="kt">DirectedLineSegment</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">start</span><span class="p">:</span> <span class="kt">CGPoint</span>
<span class="k">var</span> <span class="nv">end</span><span class="p">:</span> <span class="kt">CGPoint</span>
<span class="k">var</span> <span class="nv">dx</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="n">end</span><span class="o">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">start</span><span class="o">.</span><span class="n">x</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">dy</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="n">end</span><span class="o">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">start</span><span class="o">.</span><span class="n">y</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">magnitude</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="nf">sqrt</span><span class="p">(</span><span class="n">dx</span> <span class="o">*</span> <span class="n">dx</span> <span class="o">+</span> <span class="n">dy</span> <span class="o">*</span> <span class="n">dy</span><span class="p">)</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">direction</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="p">{</span> <span class="nf">atan2</span><span class="p">(</span><span class="n">dy</span><span class="p">,</span> <span class="n">dx</span><span class="p">)</span> <span class="p">}</span>
<span class="c1">/// Returns a point extended in the direction rotated clockwise by `angle`</span>
<span class="c1">/// with a distance `multiple` times the magnitude of this line segment.</span>
<span class="kd">func</span> <span class="nf">extended</span><span class="p">(</span><span class="nv">multiple</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGPoint</span> <span class="p">{</span>
<span class="n">end</span><span class="o">.</span><span class="nf">centeredConcyclic</span><span class="p">(</span>
<span class="nv">radius</span><span class="p">:</span> <span class="n">multiple</span> <span class="o">*</span> <span class="n">magnitude</span><span class="p">,</span>
<span class="nv">angle</span><span class="p">:</span> <span class="n">direction</span> <span class="o">+</span> <span class="n">angle</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">dx</code>, <code class="language-plaintext highlighter-rouge">dy</code>, <code class="language-plaintext highlighter-rouge">magnitude</code>, <code class="language-plaintext highlighter-rouge">direction</code>은 각각 $x$ 변위, $y$ 변위, 길이, 그리고
방향이다.
<code class="language-plaintext highlighter-rouge">direction</code>은 위에서 강조하였듯이 시계 방향으로의 각도로,
<a href="https://en.wikipedia.org/wiki/Atan2"><code class="language-plaintext highlighter-rouge">atan2</code></a>로 쉽게 구할 수 있다.
(필자도 처음 시도할 때는 그냥 <code class="language-plaintext highlighter-rouge">atan</code>과 <code class="language-plaintext highlighter-rouge">switch</code>로 구현하려고 했는데, 좌표축도
뒤집혀 있고 <code class="language-plaintext highlighter-rouge">CGFloat</code> 연산의 부정확함에 따른 굉장히 작은 음수/양수 오차의
등장으로 삽질을 하다가 <code class="language-plaintext highlighter-rouge">atan2</code>를 기억하였다.)</p>
<p><code class="language-plaintext highlighter-rouge">extended(multiple:angle:)</code> 함수는 유향 선분의 끝점에서부터 <code class="language-plaintext highlighter-rouge">angle</code>만큼 시계
방향으로 회전한 후, 유향 선분의 길이의 <code class="language-plaintext highlighter-rouge">multiple</code> 배만큼 나아간 위치의
<code class="language-plaintext highlighter-rouge">CGPoint</code>를 반환한다.
구현은 위에서 만든 <code class="language-plaintext highlighter-rouge">centeredConcyclic(radius:angle:)</code>로 간단히 할 수 있다.</p>
<h2 id="fractal-tree">Fractal Tree</h2>
<p>Fractal tree는 반시계 방향, 일직선, 그리고 시계 방향 세 방향으로 선분 긋기를
반복하면 된다.
이때 선분의 길이는 매번 절반씩 짧아지게 된다.
이때 처음 시작점은 주어진 <code class="language-plaintext highlighter-rouge">CGRect</code>의 중심에서 아래로 높이의 $\frac 16$만큼
떨어진 곳으로, 첫 선분들의 길이는 <code class="language-plaintext highlighter-rouge">CGRect</code>의 변 중 짧은 것의 $\frac 14$로
하였다.
정삼각형 기준으로 생각한다면 변의 길이가 딱 맞는 것보다 짧은 것인데, 이는
fractal tree의 각 선분들이 떨어져 있는 각을 $\frac 23\pi$보다 작은 경우, 특히
$\frac \pi 2$인 경우 <code class="language-plaintext highlighter-rouge">CGRect</code> 밖으로 나가지 않게 하기 위함이다.
그렇다면 현재까지의 코드는 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Tree</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">/</span> <span class="mi">3</span>
<span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">step</span> <span class="o">></span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Path</span><span class="p">()</span> <span class="p">}</span>
<span class="k">let</span> <span class="nv">origin</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">localCenter</span><span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">y</span><span class="p">:</span> <span class="n">rect</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">6</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">radius</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">minSide</span> <span class="o">/</span> <span class="mi">4</span>
<span class="k">let</span> <span class="nv">turnAngles</span> <span class="o">=</span> <span class="p">[</span><span class="o">-</span><span class="n">angle</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">angle</span><span class="p">]</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>이때 <code class="language-plaintext highlighter-rouge">step</code>과 <code class="language-plaintext highlighter-rouge">angle</code>은 각각 진행할 깊이, 각 선분이 분리된 (라디안) 각이다.
이제 <code class="language-plaintext highlighter-rouge">origin</code>에서 <code class="language-plaintext highlighter-rouge">radius</code> 길이의 선분을 <code class="language-plaintext highlighter-rouge">turnAngles</code>의 각 원소만큼 시계
방향으로 회전하여 그으면 깊이 1에 대한 fractal tree가 된다.
이는 <code class="language-plaintext highlighter-rouge">.centeredConcyclic(radius:angle:)</code>을 사용하여 쉽게 할 수 있다.
다만, 여기서 <code class="language-plaintext highlighter-rouge">angle</code> 값은 위에서 설명하였듯이 원의 중심에서 $x$축 방향이 0인
기준이기 때문에 fractal tree의 가운데 선분이 위로 향하기 위해서는
<code class="language-plaintext highlighter-rouge">turnAngles</code>의 값에 각각 시계 방향으로 $\frac\pi 2$ 회전하여 <code class="language-plaintext highlighter-rouge">CGFloat.pi / 2</code>를
빼서 <code class="language-plaintext highlighter-rouge">.centeredConcyclic(radius:angle:)</code>의 <code class="language-plaintext highlighter-rouge">angle</code> 인자로 넣어야 한다.
이렇게 만든 선분은 위에서 구현한 <code class="language-plaintext highlighter-rouge">DirectedLineSegment</code>으로 표현할 것이다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">initialSegments</span><span class="p">:</span> <span class="p">[</span><span class="kt">DirectedLineSegment</span><span class="p">]</span> <span class="o">=</span> <span class="n">turnAngles</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span>
<span class="c1">// (- pi / 2), as the +y direction is downwards</span>
<span class="k">let</span> <span class="nv">angle</span> <span class="o">=</span> <span class="nv">$0</span> <span class="o">-</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">end</span> <span class="o">=</span> <span class="n">origin</span>
<span class="o">.</span><span class="nf">centeredConcyclic</span><span class="p">(</span><span class="nv">radius</span><span class="p">:</span> <span class="n">radius</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">origin</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>첫 세 선분들 <code class="language-plaintext highlighter-rouge">initialSegments</code>를 먼저 화면에 그려보고 싶다면,</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">step</span> <span class="o">></span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Path</span><span class="p">()</span> <span class="p">}</span>
<span class="k">let</span> <span class="nv">origin</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">localCenter</span><span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">y</span><span class="p">:</span> <span class="n">rect</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">6</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">radius</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">minSide</span> <span class="o">/</span> <span class="mi">4</span>
<span class="k">let</span> <span class="nv">turnAngles</span> <span class="o">=</span> <span class="p">[</span><span class="o">-</span><span class="n">angle</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">angle</span><span class="p">]</span>
<span class="k">let</span> <span class="nv">initialSegments</span><span class="p">:</span> <span class="p">[</span><span class="kt">DirectedLineSegment</span><span class="p">]</span> <span class="o">=</span> <span class="n">turnAngles</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span>
<span class="c1">// (- pi / 2), as the +y direction is downwards</span>
<span class="k">let</span> <span class="nv">angle</span> <span class="o">=</span> <span class="nv">$0</span> <span class="o">-</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">end</span> <span class="o">=</span> <span class="n">origin</span>
<span class="o">.</span><span class="nf">centeredConcyclic</span><span class="p">(</span><span class="nv">radius</span><span class="p">:</span> <span class="n">radius</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">origin</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">for</span> <span class="n">segment</span> <span class="k">in</span> <span class="n">initialSegments</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">start</span><span class="p">)</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
</code></pre></div></div>
<p>와 같이 <code class="language-plaintext highlighter-rouge">initialSegments</code>의 각 <code class="language-plaintext highlighter-rouge">segment</code>별로 <code class="language-plaintext highlighter-rouge">path</code>를 그려주면 된다.
결과는 다음과 같다:</p>
<p><img src="/assets/images/2020-04-25/first-fractal-tree.png" alt="First Fractal Tree" class="shadow" /></p>
<p>이때 preview를 위해서는 <code class="language-plaintext highlighter-rouge">.stroke(lineWidth: 0.5)</code>와
<code class="language-plaintext highlighter-rouge">.aspectRatio(contentMode: .fit)</code>을 적용해주었다.</p>
<p>이제 주어진 임의의 <code class="language-plaintext highlighter-rouge">step</code> 깊이의 fractal tree를 그려야 한다.
<code class="language-plaintext highlighter-rouge">step</code>에 대해서 그려야하는 선분의 길이는 $3 + 3^2 + \cdots + 3^\texttt{step} =
\frac{3 (3^\texttt{step} - 1)}{2}$이다.
정수의 지수를 빠르게 계산하기 위해서 필자는 다음과 같은 <code class="language-plaintext highlighter-rouge">Int</code> extension을
만들었다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Int</span> <span class="p">{</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="nf">pow</span><span class="p">(</span><span class="n">_</span> <span class="nv">base</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="n">_</span> <span class="nv">exponent</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Int</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">base</span> <span class="o">=</span> <span class="n">base</span>
<span class="k">var</span> <span class="nv">exponent</span> <span class="o">=</span> <span class="n">exponent</span>
<span class="k">var</span> <span class="nv">result</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">while</span> <span class="kc">true</span> <span class="p">{</span>
<span class="k">if</span> <span class="o">!</span><span class="n">exponent</span><span class="o">.</span><span class="nf">isMultiple</span><span class="p">(</span><span class="nv">of</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">result</span> <span class="o">*=</span> <span class="n">base</span>
<span class="p">}</span>
<span class="n">exponent</span> <span class="o">/=</span> <span class="mi">2</span>
<span class="k">if</span> <span class="n">exponent</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span> <span class="k">break</span> <span class="p">}</span>
<span class="n">base</span> <span class="o">*=</span> <span class="n">base</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">result</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>지수 <code class="language-plaintext highlighter-rouge">exponent</code>를 절반씩 분할하여 계산하는 방법으로, 로그 시간 시간 복잡도를
가진다.
어차피 너무 큰 <code class="language-plaintext highlighter-rouge">step</code>에 대해서는 그림을 그리는 것이 병목이기도 하고, 따라서
미리 3의 거듭 제곱들을 dictionary로 저장해도 되지만 필자는 일반성을 위해
위와 같이 runtime에 계산하기로 하였다.</p>
<p>이제 <code class="language-plaintext highlighter-rouge">initialSegments</code>를 구현한 <code class="language-plaintext highlighter-rouge">Queue</code>에 넣고, 각 segment를 꺼낸 후 새로운 세
개의 segment들을 넣는 것을 $\frac{3 (3^\texttt{step} - 1)}{2}$회 반복하면 된다.
미리 <code class="language-plaintext highlighter-rouge">Queue</code>의 크기를 정해줘야 하는데, 아무리 커도 $3^\texttt{step} +
3^\texttt{step + 1} = 4 \times 3^\texttt{step}$보다는 작으므로 이를 넉넉한
상한으로 잡자.
이 과정은 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">power</span> <span class="o">=</span> <span class="kt">Int</span><span class="o">.</span><span class="nf">pow</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="kt">Int</span><span class="p">(</span><span class="n">step</span><span class="p">))</span>
<span class="k">var</span> <span class="nv">queue</span> <span class="o">=</span> <span class="kt">Queue</span><span class="p">(</span><span class="n">initialSegments</span><span class="p">,</span> <span class="nv">capacity</span><span class="p">:</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">power</span><span class="p">)</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">for</span> <span class="n">_</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="mi">3</span> <span class="o">*</span> <span class="p">(</span><span class="n">power</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">segment</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="nf">dequeue</span><span class="p">()</span><span class="o">!</span>
<span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">start</span><span class="p">)</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">nextPoints</span> <span class="o">=</span> <span class="n">turnAngles</span>
<span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">segment</span><span class="o">.</span><span class="nf">extended</span><span class="p">(</span><span class="nv">multiple</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
<span class="k">for</span> <span class="n">point</span> <span class="k">in</span> <span class="n">nextPoints</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">queue</span><span class="o">.</span><span class="nf">enqueue</span><span class="p">(</span>
<span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">point</span><span class="p">)</span>
<span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
<span class="nf">assertionFailure</span><span class="p">(</span><span class="s">"Queue overflow"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">nextPoints</code>는 각 선분의 끝에서 나아갈 새로운 세 선분의 꼭짓점들이다.
처음에 <code class="language-plaintext highlighter-rouge">dequeue</code>는 계산한 것이 맞다면 반드시 <code class="language-plaintext highlighter-rouge">dequeue</code>할 수 있음이 보장되며,
<code class="language-plaintext highlighter-rouge">enqueue</code>의 경우는 <code class="language-plaintext highlighter-rouge">queue</code>의 크기가 충분하다면 성공할 것이다.
(위에서 <code class="language-plaintext highlighter-rouge">enqueue</code>가 성공 여부를 <code class="language-plaintext highlighter-rouge">Bool</code>로 반환하도록 구현하였다.)
지금까지의 코드를 하나로 합치면 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Tree</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">/</span> <span class="mi">3</span>
<span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">step</span> <span class="o">></span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Path</span><span class="p">()</span> <span class="p">}</span>
<span class="k">let</span> <span class="nv">origin</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">localCenter</span><span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">y</span><span class="p">:</span> <span class="n">rect</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">6</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">radius</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">minSide</span> <span class="o">/</span> <span class="mi">4</span>
<span class="k">let</span> <span class="nv">turnAngles</span> <span class="o">=</span> <span class="p">[</span><span class="o">-</span><span class="n">angle</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">angle</span><span class="p">]</span>
<span class="k">let</span> <span class="nv">initialSegments</span><span class="p">:</span> <span class="p">[</span><span class="kt">DirectedLineSegment</span><span class="p">]</span> <span class="o">=</span> <span class="n">turnAngles</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span>
<span class="c1">// (- pi / 2), as the +y direction is downwards</span>
<span class="k">let</span> <span class="nv">angle</span> <span class="o">=</span> <span class="nv">$0</span> <span class="o">-</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">end</span> <span class="o">=</span> <span class="n">origin</span>
<span class="o">.</span><span class="nf">centeredConcyclic</span><span class="p">(</span><span class="nv">radius</span><span class="p">:</span> <span class="n">radius</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">origin</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">power</span> <span class="o">=</span> <span class="kt">Int</span><span class="o">.</span><span class="nf">pow</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="kt">Int</span><span class="p">(</span><span class="n">step</span><span class="p">))</span>
<span class="k">var</span> <span class="nv">queue</span> <span class="o">=</span> <span class="kt">Queue</span><span class="p">(</span><span class="n">initialSegments</span><span class="p">,</span> <span class="nv">capacity</span><span class="p">:</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">power</span><span class="p">)</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">for</span> <span class="n">_</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="mi">3</span> <span class="o">*</span> <span class="p">(</span><span class="n">power</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">segment</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="nf">dequeue</span><span class="p">()</span><span class="o">!</span>
<span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">start</span><span class="p">)</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">nextPoints</span> <span class="o">=</span> <span class="n">turnAngles</span>
<span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">segment</span><span class="o">.</span><span class="nf">extended</span><span class="p">(</span><span class="nv">multiple</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
<span class="k">for</span> <span class="n">point</span> <span class="k">in</span> <span class="n">nextPoints</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">queue</span><span class="o">.</span><span class="nf">enqueue</span><span class="p">(</span>
<span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">point</span><span class="p">)</span>
<span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
<span class="nf">assertionFailure</span><span class="p">(</span><span class="s">"Queue overflow"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>이렇게 작성한 결과, <code class="language-plaintext highlighter-rouge">Tree(step: 7, angle: .pi * 2 / 3)</code>를 그리면 다음과 같다:</p>
<p><img src="/assets/images/2020-04-25/example-fractal-tree.png" alt="Example Fractal Tree" class="shadow" /></p>
<p>마찬가지로 preview를 위해 <code class="language-plaintext highlighter-rouge">.stroke(lineWidth: 0.5)</code>와
<code class="language-plaintext highlighter-rouge">.aspectRatio(contentMode: .fit)</code>을 적용해주었다.</p>
<h2 id="demonstration-view">Demonstration View</h2>
<p>필자는 직접 <code class="language-plaintext highlighter-rouge">step</code>이랑 <code class="language-plaintext highlighter-rouge">angle</code>을 조절할 수 있는 UI를 만들었다.
이는 <code class="language-plaintext highlighter-rouge">Tree</code>에 직접 구현하지 않고, 위의 <code class="language-plaintext highlighter-rouge">Carpet</code>과 <code class="language-plaintext highlighter-rouge">Triangle</code>까지 포함할 수
있도록 wrapper의 형태로 구성하였다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Demonstration</span><span class="o"><</span><span class="kt">F</span><span class="o">></span><span class="p">:</span> <span class="kt">View</span> <span class="k">where</span> <span class="kt">F</span><span class="p">:</span> <span class="kt">Fractal</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">maxStep</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">let</span> <span class="nv">maxAngle</span><span class="p">:</span> <span class="kt">Double</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">step</span> <span class="o">=</span> <span class="mi">2</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">angle</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span> <span class="p">{</span>
<span class="kt">F</span><span class="p">(</span><span class="nv">step</span><span class="p">:</span> <span class="n">step</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="o">.</span><span class="nf">drawingGroup</span><span class="p">()</span>
<span class="kt">Stepper</span><span class="p">(</span><span class="s">"Step </span><span class="se">\(</span><span class="n">step</span><span class="se">)</span><span class="s">"</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="err">$</span><span class="n">step</span><span class="p">,</span> <span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="n">maxStep</span><span class="p">)</span>
<span class="o">.</span><span class="nf">fixedSize</span><span class="p">()</span>
<span class="kt">Slider</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="err">$</span><span class="n">angle</span><span class="p">,</span> <span class="nv">in</span><span class="p">:</span> <span class="mi">0</span><span class="o">...</span><span class="n">maxAngle</span><span class="p">)</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">([</span><span class="o">.</span><span class="n">horizontal</span><span class="p">,</span> <span class="o">.</span><span class="n">bottom</span><span class="p">])</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">navigationBarTitle</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="kt">F</span><span class="o">.</span><span class="n">name</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">maxStep</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">maxAngle</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxStep</span> <span class="o">=</span> <span class="n">maxStep</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxAngle</span> <span class="o">=</span> <span class="n">maxAngle</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>이때 <code class="language-plaintext highlighter-rouge">Fractal</code> protocol은 다음과 같이 정의되었다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protocol</span> <span class="kt">Fractal</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">static</span> <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Fractal</code>는 <code class="language-plaintext highlighter-rouge">Carpet</code>, <code class="language-plaintext highlighter-rouge">Triangle</code>, 그리고 <code class="language-plaintext highlighter-rouge">Tree</code>가 따르도록 할 것이다.
먼저 <code class="language-plaintext highlighter-rouge">Demonstration<F></code>를 보자면, 주어진 <code class="language-plaintext highlighter-rouge">F</code>와 <code class="language-plaintext highlighter-rouge">Stepper</code>, <code class="language-plaintext highlighter-rouge">Slider</code>가 <code class="language-plaintext highlighter-rouge">VStack</code>을
이루는 간단한 구조이다.
<code class="language-plaintext highlighter-rouge">Stepper</code>는 <code class="language-plaintext highlighter-rouge">step</code>을, <code class="language-plaintext highlighter-rouge">Slider</code>는 <code class="language-plaintext highlighter-rouge">angle</code>을 조절하는 control이다.</p>
<p><code class="language-plaintext highlighter-rouge">Tree</code>는 <code class="language-plaintext highlighter-rouge">Fractal</code>에 따르도록 wrapper를 만들었다.
위에 구현한 <code class="language-plaintext highlighter-rouge">Tree</code>는 <code class="language-plaintext highlighter-rouge">TreeShape</code>로 변경하고, 아래와 같이 새로운 <code class="language-plaintext highlighter-rouge">Tree</code>
wrapper를 만들었다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Tree</span><span class="p">:</span> <span class="kt">Fractal</span> <span class="p">{</span>
<span class="kd">static</span> <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span> <span class="s">"Fractal Tree"</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">/</span> <span class="mi">3</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">TreeShape</span><span class="p">(</span><span class="nv">step</span><span class="p">:</span> <span class="n">step</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="nv">lineWidth</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">)</span>
<span class="o">.</span><span class="nf">aspectRatio</span><span class="p">(</span><span class="nv">contentMode</span><span class="p">:</span> <span class="o">.</span><span class="n">fit</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">struct</span> <span class="kt">TreeShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="c1">// 원래의 Tree를 여기로 옮겼다</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>이를 <code class="language-plaintext highlighter-rouge">Demonstration</code>에 감싸
<code class="language-plaintext highlighter-rouge">Demonstration<Tree>(maxStep: 10, 8, maxAngle: .pi * 2 / 3)</code>와 같이 하면,
다음과 같이 control로 fractal tree를 조절할 수 있다:</p>
<p align="center" class="shadow">
<img src="/assets/images/2020-04-25/fractal-control.png" width="75%" alt="Fractal Control" />
</p>
<h2 id="애니메이션">애니메이션</h2>
<p>위의 <code class="language-plaintext highlighter-rouge">Demonstration<Tree></code>에서 한 가지 아쉬운 점이 있는데, <code class="language-plaintext highlighter-rouge">step</code>을 증가시킬 때
fractal tree이 바로 바뀐다는 점이다.
우리의 fractal tree는 엄연히 나무인데 자라는 모습을 볼 수 없다니!
<code class="language-plaintext highlighter-rouge">step</code>을 증가시키거나 감소시키면 부드럽게 fractal tree의 가지가 자라고 줄어드는
것을 볼 수 있도록 애니메이션을 구현해 보았다.</p>
<p>SwiftUI에서 애니메이션은 <code class="language-plaintext highlighter-rouge">Animatable</code> protocol을 따르면 된다.
이는 애니메이션이 가능한 <code class="language-plaintext highlighter-rouge">var animatableData</code>를 구현하면 되는데, <code class="language-plaintext highlighter-rouge">Shape</code>는
<code class="language-plaintext highlighter-rouge">Animatable</code>을 이미 따르고 있다.
<code class="language-plaintext highlighter-rouge">animatableData</code>는 <code class="language-plaintext highlighter-rouge">VectorArithmetic</code>을 따르는 type에 해당하면 되는데,
<code class="language-plaintext highlighter-rouge">Double</code>, <code class="language-plaintext highlighter-rouge">Float</code>, <code class="language-plaintext highlighter-rouge">CGFloat</code>는 이미 여기에 해당한다.</p>
<p>우리는 <code class="language-plaintext highlighter-rouge">step</code>을 <code class="language-plaintext highlighter-rouge">animatableData</code>로 하고 싶지만, 바로 적용할 수는 없다.
<code class="language-plaintext highlighter-rouge">step</code>은 <code class="language-plaintext highlighter-rouge">Int</code>이기 때문이다.
그렇다고 <code class="language-plaintext highlighter-rouge">step</code>을 무작정 <code class="language-plaintext highlighter-rouge">Double</code>로 바꾼다고 되는 것도 아니다.
SwiftUI가 <code class="language-plaintext highlighter-rouge">step = 2.136</code>와 같은 값을 그릴 수 있도록 알려줘야 하기 때문이다.</p>
<p><code class="language-plaintext highlighter-rouge">step</code>에 따라 마지막 leaf node들에 대하여 길이를 연속적으로 증가시키면 될
것이다.
먼저 <code class="language-plaintext highlighter-rouge">step</code>으로 바로 <code class="language-plaintext highlighter-rouge">Double</code>로 만들지 말고, <code class="language-plaintext highlighter-rouge">step</code>은 <code class="language-plaintext highlighter-rouge">Int</code>로 그대로 두고
<code class="language-plaintext highlighter-rouge">Double</code>인 <code class="language-plaintext highlighter-rouge">_step</code>을 만든 후 이를 <code class="language-plaintext highlighter-rouge">animatableData</code>로 해준다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">TreeShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">_step</span><span class="p">:</span> <span class="kt">Double</span>
<span class="k">var</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">/</span> <span class="mi">3</span>
<span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">Double</span> <span class="p">{</span>
<span class="k">get</span> <span class="p">{</span> <span class="n">_step</span> <span class="p">}</span>
<span class="k">set</span> <span class="p">{</span> <span class="n">_step</span> <span class="o">=</span> <span class="n">newValue</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Leaf node인 경우 <code class="language-plaintext highlighter-rouge">path(in:)</code>의 <code class="language-plaintext highlighter-rouge">while</code> 문의 <code class="language-plaintext highlighter-rouge">path.addLine(to: segment.end)</code>
부분을 다르게 처리해주면 된다.
기존과 달라진 부분을 적어보면:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ...</span>
<span class="k">let</span> <span class="nv">roundedStep</span> <span class="o">=</span> <span class="n">_step</span><span class="o">.</span><span class="nf">rounded</span><span class="p">(</span><span class="o">.</span><span class="n">up</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">power</span> <span class="o">=</span> <span class="kt">Int</span><span class="o">.</span><span class="nf">pow</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="kt">Int</span><span class="p">(</span><span class="n">roundedStep</span><span class="p">))</span>
<span class="k">var</span> <span class="nv">queue</span> <span class="o">=</span> <span class="kt">Queue</span><span class="p">(</span><span class="n">initialSegments</span><span class="p">,</span> <span class="nv">capacity</span><span class="p">:</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">power</span><span class="p">)</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="mi">3</span> <span class="o">*</span> <span class="p">(</span><span class="n">power</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">isLeafNode</span> <span class="o">=</span> <span class="n">i</span> <span class="o">>=</span> <span class="p">(</span><span class="n">power</span> <span class="o">-</span> <span class="mi">3</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">segment</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="nf">dequeue</span><span class="p">()</span><span class="o">!</span>
<span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">start</span><span class="p">)</span>
<span class="k">if</span> <span class="n">isLeafNode</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span>
<span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="nf">extended</span><span class="p">(</span>
<span class="nv">multiple</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">roundedStep</span> <span class="o">-</span> <span class="n">_step</span><span class="p">),</span>
<span class="nv">angle</span><span class="p">:</span> <span class="o">-.</span><span class="n">pi</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ...</span>
</code></pre></div></div>
<p>여기서 <code class="language-plaintext highlighter-rouge">roundedStep</code>는 <code class="language-plaintext highlighter-rouge">_step</code>을 올림한 값이며, <code class="language-plaintext highlighter-rouge">isLeafNode</code>는 현재 반복문의
index <code class="language-plaintext highlighter-rouge">i</code>가 마지막 leaf node에 대한 것인지 확인하는 flag이다.
만약 <code class="language-plaintext highlighter-rouge">isLeafNode</code>라면, <code class="language-plaintext highlighter-rouge">path</code>에서 연장할 길이가 원래보다 짧아야 할 것이다.
이는 <code class="language-plaintext highlighter-rouge">extended</code>를 사용하여, 반대 방향 (<code class="language-plaintext highlighter-rouge">angle: -.pi</code>)으로 0과 1 사이의 값인
<code class="language-plaintext highlighter-rouge">roundedStep - _step</code> 배 이동한 점까지만 선을 긋도록 수정하였다.</p>
<p>이렇게 구현한 <code class="language-plaintext highlighter-rouge">Tree</code>의 전체 코드는 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Tree</span><span class="p">:</span> <span class="kt">Fractal</span> <span class="p">{</span>
<span class="kd">static</span> <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span> <span class="s">"Fractal Tree"</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="k">var</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">/</span> <span class="mi">3</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">TreeShape</span><span class="p">(</span><span class="nv">step</span><span class="p">:</span> <span class="n">step</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="o">.</span><span class="nf">stroke</span><span class="p">(</span><span class="nv">lineWidth</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">)</span>
<span class="o">.</span><span class="nf">animation</span><span class="p">(</span><span class="n">step</span> <span class="o"><=</span> <span class="mi">6</span> <span class="p">?</span> <span class="o">.</span><span class="k">default</span> <span class="p">:</span> <span class="o">.</span><span class="k">none</span><span class="p">)</span>
<span class="o">.</span><span class="nf">aspectRatio</span><span class="p">(</span><span class="nv">contentMode</span><span class="p">:</span> <span class="o">.</span><span class="n">fit</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">Tree</span> <span class="p">{</span>
<span class="kd">struct</span> <span class="kt">TreeShape</span><span class="p">:</span> <span class="kt">Shape</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">_step</span><span class="p">:</span> <span class="kt">Double</span>
<span class="c1">// var sides: Int = 3</span>
<span class="k">var</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">CGFloat</span> <span class="o">=</span> <span class="o">.</span><span class="n">pi</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">/</span> <span class="mi">3</span>
<span class="k">var</span> <span class="nv">animatableData</span><span class="p">:</span> <span class="kt">Double</span> <span class="p">{</span>
<span class="k">get</span> <span class="p">{</span> <span class="n">_step</span> <span class="p">}</span>
<span class="k">set</span> <span class="p">{</span> <span class="n">_step</span> <span class="o">=</span> <span class="n">newValue</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">path</span><span class="p">(</span><span class="k">in</span> <span class="nv">rect</span><span class="p">:</span> <span class="kt">CGRect</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Path</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">step</span> <span class="o">></span> <span class="mi">0</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Path</span><span class="p">()</span> <span class="p">}</span>
<span class="k">let</span> <span class="nv">origin</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">localCenter</span><span class="o">.</span><span class="nf">offset</span><span class="p">(</span><span class="nv">y</span><span class="p">:</span> <span class="n">rect</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">6</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">radius</span> <span class="o">=</span> <span class="n">rect</span><span class="o">.</span><span class="n">minSide</span> <span class="o">/</span> <span class="mi">4</span>
<span class="k">let</span> <span class="nv">turnAngles</span> <span class="o">=</span> <span class="p">[</span><span class="o">-</span><span class="n">angle</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">angle</span><span class="p">]</span>
<span class="k">let</span> <span class="nv">initialSegments</span><span class="p">:</span> <span class="p">[</span><span class="kt">DirectedLineSegment</span><span class="p">]</span> <span class="o">=</span> <span class="n">turnAngles</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span>
<span class="c1">// (- pi / 2), as the +y direction is downwards</span>
<span class="k">let</span> <span class="nv">angle</span> <span class="o">=</span> <span class="nv">$0</span> <span class="o">-</span> <span class="kt">CGFloat</span><span class="o">.</span><span class="n">pi</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">end</span> <span class="o">=</span> <span class="n">origin</span>
<span class="o">.</span><span class="nf">centeredConcyclic</span><span class="p">(</span><span class="nv">radius</span><span class="p">:</span> <span class="n">radius</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="n">angle</span><span class="p">)</span>
<span class="k">return</span> <span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">origin</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// Lower bound for the queue size</span>
<span class="c1">// < 3^step + 3^(step + 1) = 4 * 3^step</span>
<span class="c1">// Total number of dequeue count =</span>
<span class="c1">// 3 + 3^2 + ... + 3^step = 3 * (3^step - 1) / 2</span>
<span class="k">let</span> <span class="nv">roundedStep</span> <span class="o">=</span> <span class="n">_step</span><span class="o">.</span><span class="nf">rounded</span><span class="p">(</span><span class="o">.</span><span class="n">up</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">power</span> <span class="o">=</span> <span class="kt">Int</span><span class="o">.</span><span class="nf">pow</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="kt">Int</span><span class="p">(</span><span class="n">roundedStep</span><span class="p">))</span>
<span class="k">var</span> <span class="nv">queue</span> <span class="o">=</span> <span class="kt">Queue</span><span class="p">(</span><span class="n">initialSegments</span><span class="p">,</span> <span class="nv">capacity</span><span class="p">:</span> <span class="mi">4</span> <span class="o">*</span> <span class="n">power</span><span class="p">)</span>
<span class="k">var</span> <span class="nv">path</span> <span class="o">=</span> <span class="kt">Path</span><span class="p">()</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="mi">3</span> <span class="o">*</span> <span class="p">(</span><span class="n">power</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">isLeafNode</span> <span class="o">=</span> <span class="n">i</span> <span class="o">>=</span> <span class="p">(</span><span class="n">power</span> <span class="o">-</span> <span class="mi">3</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span>
<span class="k">let</span> <span class="nv">segment</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="nf">dequeue</span><span class="p">()</span><span class="o">!</span>
<span class="n">path</span><span class="o">.</span><span class="nf">move</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">start</span><span class="p">)</span>
<span class="k">if</span> <span class="n">isLeafNode</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span>
<span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="nf">extended</span><span class="p">(</span>
<span class="nv">multiple</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">roundedStep</span> <span class="o">-</span> <span class="n">_step</span><span class="p">),</span>
<span class="nv">angle</span><span class="p">:</span> <span class="o">-.</span><span class="n">pi</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">path</span><span class="o">.</span><span class="nf">addLine</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">nextPoints</span> <span class="o">=</span> <span class="n">turnAngles</span>
<span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">segment</span><span class="o">.</span><span class="nf">extended</span><span class="p">(</span><span class="nv">multiple</span><span class="p">:</span> <span class="mf">0.5</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
<span class="k">for</span> <span class="n">point</span> <span class="k">in</span> <span class="n">nextPoints</span> <span class="p">{</span>
<span class="k">guard</span> <span class="n">queue</span><span class="o">.</span><span class="nf">enqueue</span><span class="p">(</span>
<span class="kt">DirectedLineSegment</span><span class="p">(</span><span class="nv">start</span><span class="p">:</span> <span class="n">segment</span><span class="o">.</span><span class="n">end</span><span class="p">,</span> <span class="nv">end</span><span class="p">:</span> <span class="n">point</span><span class="p">)</span>
<span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
<span class="nf">assertionFailure</span><span class="p">(</span><span class="s">"Queue overflow"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">path</span>
<span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">step</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">angle</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">step</span> <span class="o">=</span> <span class="n">step</span>
<span class="k">self</span><span class="o">.</span><span class="n">_step</span> <span class="o">=</span> <span class="kt">Double</span><span class="p">(</span><span class="n">step</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">angle</span> <span class="o">=</span> <span class="kt">CGFloat</span><span class="p">(</span><span class="n">angle</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h1 id="finale">Finale</h1>
<p>제목이 ‘프랙탈과 애니메이션’이지만, 글을 작성하다보니 애니메이션의 부분이 짧게
마무리가 되었다.
하지만 그만큼 SwiftUI에서 애니메이션을 쉽게 넣을 수 있다는 것을 알 수 있다.</p>
<p><a href="https://github.com/zeta611/swiftui-fractals">GitHub</a>에는 위의 구현 이외에도,
<code class="language-plaintext highlighter-rouge">Combine</code>과 <code class="language-plaintext highlighter-rouge">Timer</code>, 그리고 <code class="language-plaintext highlighter-rouge">EnvironmentObject</code>를 사용해 자동으로
<code class="language-plaintext highlighter-rouge">F: Fractal</code>의 매개변수를 조절하는 코드가 <code class="language-plaintext highlighter-rouge">Demonstration</code>에 들어가 있다.
나아가, <code class="language-plaintext highlighter-rouge">step</code>이 큰 경우에는 iOS 기기의 한계로 인해 애니메이션이 실행될 경우
느리거나 앱이 종료하는 문제가 있기 때문에, 애니메이션을 진행할 최대 <code class="language-plaintext highlighter-rouge">step</code>을
제한하고 <code class="language-plaintext highlighter-rouge">Toggle</code>을 비활성화시키는 코드 등이 들어 있다.
또한 위에서 언급하였듯이 Sierpinski 카펫의 경우 <code class="language-plaintext highlighter-rouge">RegularPolygon</code>으로 다시
구현이 되었으며, Sierpinski 삼각형과 마찬가지로 각 도형이 회전하도록
<code class="language-plaintext highlighter-rouge">Fractal</code>의 <code class="language-plaintext highlighter-rouge">angle</code>을 활용하게 구현이 되어 있다.</p>
<p>지금까지 SwiftUI에서 fractal을 구현하는 선언적인 방식과 <code class="language-plaintext highlighter-rouge">Shape</code>를 사용한 두
가지 방법과, <code class="language-plaintext highlighter-rouge">Animatable</code>을 사용한 애니메이션에 대해 알아보았다.</p>Zetajaeho.lee@snu.ac.krSwiftUI의 재귀적인 View 구조로 Sierpinski 프랙탈을 구현한 후, Shape와 Animatable을 통해 프랙탈 트리를 만들어본다.SwiftUI: 중첩된 View 정렬하기2020-04-14T00:00:00+09:002020-04-14T00:00:00+09:00https://zetablog.io/posts/nested-aligning-in-swiftui<p align="center" class="shadow">
<img src="/assets/images/2020-04-14/iphone-final.png" width="45%" alt="iPhone final" />
</p>
<h1 id="swiftui의-view-정렬">SwiftUI의 View 정렬</h1>
<p>Apple은 지난 WWDC 2019에서 Swift의 기능을 극한까지 활용한 UI framework인
SwiftUI를 내놓았다.
오히려 역으로, SwiftUI를 위해
<a href="https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md">Swift에</a>
<a href="https://github.com/apple/swift-evolution/blob/master/proposals/0244-opaque-result-types.md">새로운</a>
<a href="https://github.com/apple/swift-evolution/blob/master/proposals/0255-omit-return.md">기능들을</a>
<a href="https://github.com/apple/swift-evolution/pull/1046">넣기까지</a> 하였다.
이로써 Objective-C 위에 만들어진 UIKit에서 써야했던 절차적인
방식–<code class="language-plaintext highlighter-rouge">@objc func</code>와 <code class="language-plaintext highlighter-rouge">#selector</code>–을 property wrapper <code class="language-plaintext highlighter-rouge">@State</code>와 <code class="language-plaintext highlighter-rouge">@Binding</code>
등을 사용한 반응형 view를 설계할 수 있게 되었다.
또한 <a href="/posts/clean-architecture-in-ios-is-too-much">지난 포스트</a>에서
소개하였듯 이런 SwiftUI의 특징 덕분에 기존의 MVC, VIPER 등의 디자인 패턴이
obsolete하게 되었다.</p>
<p>SwiftUI에서 간단한 layout은 <code class="language-plaintext highlighter-rouge">HStack</code>, <code class="language-plaintext highlighter-rouge">VStack</code>, 그리고 <code class="language-plaintext highlighter-rouge">ZStack</code>을 활용하여
<strong>매우</strong> 간단하게 구성할 수 있다.
그러나 이 세상에 ‘완벽한 도구’가 있을 수는 없는 법.
SwiftUI를 처음 접하고 30분 정도 실험하다 보면, 분명히 난관에 맞닥뜨릴 것이다.
그러곤, “SwiftUI의 마법으로 이 문제를 해결할 수 있을거야!”라고 생각한다면
오산이다.
SwiftUI에서는 UIKit의 autolayout으로 간단히 해결할 수 있는 문제를 다루기가
까다로운 경우가 많기 때문이다.
특히 구체적인 수치를 기반으로 view를 짜야 한다면 말이다.</p>
<p>하지만, 프로그래밍의 세계에서
<a href="https://en.wikipedia.org/wiki/List_of_unsolved_problems_in_computer_science">불가능?!</a>은
없는 법.
이번 포스트에서는 SwiftUI에서 까다로운 view 정렬에 대해서 알아본다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p>iOS에서 다음과 같은 코드를 실행하면:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">text1</span> <span class="o">=</span> <span class="s">""</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">text2</span> <span class="o">=</span> <span class="s">""</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span> <span class="p">{</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text1</span><span class="p">)</span>
<span class="p">}</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">)</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text2</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">textFieldStyle</span><span class="p">(</span><span class="kt">RoundedBorderTextFieldStyle</span><span class="p">())</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/2020-04-14/iphone-init.png" alt="iPhone init" class="shadow" /></p>
<p>위와 같이 Title과 Long Title의 끝이 맞지 않는 것을 볼 수 있다.
가장 간단하고도 빠른 해결법은 Title과 Long Title의 frame을 직접 정해주는
것이다.
이 경우, <code class="language-plaintext highlighter-rouge">.frame(width: 76, alignment: .trailing)</code>를 각 <code class="language-plaintext highlighter-rouge">Text</code>에 적용하면 아래
그림과 같이 된다:</p>
<p><img src="/assets/images/2020-04-14/iphone-try1.png" alt="iPhone try 1" class="shadow" /></p>
<p>그러나 이런 방식으로는, <code class="language-plaintext highlighter-rouge">Text</code>의 내용이 바뀌거나, 다른 글꼴을 쓰도록 고칠
때마다 새로운 frame 너비를 직접 찾아줘야 한다.
자동으로 ‘Title’과 ‘Long Title’의 끝을 맞출 수는 없을까?</p>
<p>또 다른 간단한 방식으로는 VStack과 HStack의 위치를 뒤바꿔 다음과 같이 하는
것이다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">24</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">)</span>
<span class="p">}</span>
<span class="kt">VStack</span> <span class="p">{</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>그러나 이 경우에도 <code class="language-plaintext highlighter-rouge">VStack</code>의 <code class="language-plaintext highlighter-rouge">spacing</code> 값을 직접 조정해줘야 한다는 문제가
있다.
나아가, 의미상으로도 <code class="language-plaintext highlighter-rouge">Text(Title)</code>은 <code class="language-plaintext highlighter-rouge">TextField("Title", text: $text)</code>의
header이기 때문에 같은 group에 묶여 있으면 좋을 것이다.</p>
<p>이러한 정렬은 macOS에서 <code class="language-plaintext highlighter-rouge">Picker</code>를 쓸 때도 문제가 된다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">selection1</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">selection2</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">integers</span> <span class="o">=</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">Picker</span><span class="p">(</span><span class="s">"Title"</span><span class="p">,</span> <span class="nv">selection</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="err">$</span><span class="n">selection1</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">ForEach</span><span class="p">(</span><span class="n">integers</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Selection </span><span class="se">\(</span><span class="nv">$0</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">Picker</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">,</span> <span class="nv">selection</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="err">$</span><span class="n">selection2</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">ForEach</span><span class="p">(</span><span class="n">integers</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Selection </span><span class="se">\(</span><span class="nv">$0</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="/assets/images/2020-04-14/mac-init.png" alt="Mac init" class="shadow" /></p>
<p>다만 macOS의 <code class="language-plaintext highlighter-rouge">Picker</code>의 경우 이 포스트에서 제시할 해결법을 적용하려면
<code class="language-plaintext highlighter-rouge">.labelsHidden()</code>을 적용한 후 별도의 <code class="language-plaintext highlighter-rouge">Text</code>로 앞에 “Title”과 “Long Title”의
부분을 넣어주어야 한다.</p>
<h2 id="alignments">Alignments</h2>
<p>우리가 하고 싶은 것은, “Title”과 “Long Title”의 오른쪽 끝을 같은 선에 정렬하는
것이다.
이런 상황에서 alignment를 쓸 수 있다.
사실 alignment는 SwiftUI에서 HStack, VStack, 그리고 ZStack을 써보았다면
들어보거나 직접 사용하였을 것이다.
<code class="language-plaintext highlighter-rouge">.trailing</code>, <code class="language-plaintext highlighter-rouge">.bottom</code> 등이 각각 <code class="language-plaintext highlighter-rouge">HorizontalAlignment</code>와 <code class="language-plaintext highlighter-rouge">VerticalAlignment</code>의
static 변수이다.
이외에도 직접 Alignment를 정의해줄 수 있는데, 이를 위해서는
<a href="https://developer.apple.com/documentation/swiftui/alignmentid"><code class="language-plaintext highlighter-rouge">AlignmentID</code>의 문서</a>를
통해 방법을 어렴풋이 알 수 있다.
인용하자면,</p>
<blockquote>
<p>Types conforming to AlignmentID have a corresponding alignment guide value,
typically declared as a static constant property of HorizontalAlignment or
VerticalAlignment.
이라고 한다.</p>
</blockquote>
<p>“Title”과 “Long Title”의 가로 위치를 조절하고 싶으므로 <code class="language-plaintext highlighter-rouge">HorizontalAlignment</code>를
사용하면 된다.
먼저 새로운 alignment를 위한 <code class="language-plaintext highlighter-rouge">AlignmentID</code>를 만들어야 하는데,
<code class="language-plaintext highlighter-rouge">LabelTrailingAlignment</code>라고 이름 짓자.
<code class="language-plaintext highlighter-rouge">AlignmentID</code>에 conform하기 위해서는
<code class="language-plaintext highlighter-rouge">static func defaultValue(in context: ViewDimensions) -> CGFloat</code>을 구현해주면
된다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">HorizontalAlignment</span> <span class="p">{</span>
<span class="kd">private</span> <span class="kd">enum</span> <span class="kt">LabelTrailingAlignment</span><span class="p">:</span> <span class="kt">AlignmentID</span> <span class="p">{</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="nf">defaultValue</span><span class="p">(</span><span class="k">in</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">ViewDimensions</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGFloat</span> <span class="p">{</span>
<span class="c1">// Does not matter</span>
<span class="n">context</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>주석에 적어두었는데, <code class="language-plaintext highlighter-rouge">context[.trailing]</code>은 필요하기 때문에 적어놓았지만 우리는
사용하지 않을 dummy value이다.</p>
<p>이제 새로운 alignment를 통해 <code class="language-plaintext highlighter-rouge">VStack</code> 안의 <code class="language-plaintext highlighter-rouge">HStack</code>에 쌓여 있는 view를 기준으로
맨 바깥의 <code class="language-plaintext highlighter-rouge">VStack</code>을 정렬할 수 있다.
무슨 말이냐면,</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// HorizontalAlignment+labelTrailingAlignment.swift</span>
<span class="kd">extension</span> <span class="kt">HorizontalAlignment</span> <span class="p">{</span>
<span class="c1">// 위에 정의한 private enum 생략</span>
<span class="kd">static</span> <span class="k">let</span> <span class="nv">labelTrailingAlignment</span> <span class="o">=</span> <span class="kt">HorizontalAlignment</span><span class="p">(</span>
<span class="kt">LabelTrailingAlignment</span><span class="o">.</span><span class="k">self</span>
<span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ContentView.swift</span>
<span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">text</span> <span class="o">=</span> <span class="s">""</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$0</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span> <span class="p">}</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$0</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span> <span class="p">}</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">textFieldStyle</span><span class="p">(</span><span class="kt">RoundedBorderTextFieldStyle</span><span class="p">())</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>와 같이 하는 것이다.
여기서 중요한 부분은 <code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code> 뒤에 붙인
<code class="language-plaintext highlighter-rouge">.alignmentGuide</code> method인데, 이 부분이 우리가 새로 정의한
<code class="language-plaintext highlighter-rouge">.labelTrailingAlignment</code>의 값을 정해주는 부분이다.
맨 바깥 <code class="language-plaintext highlighter-rouge">VStack</code>의 정렬 기준을 <code class="language-plaintext highlighter-rouge">.labelTrailingAlignment</code>으로 해주고 안의
view들은 두 <code class="language-plaintext highlighter-rouge">Text</code>만 이 alignment 기준을 지정하였으므로 제대로 정렬이 되는
것이다.
이때 <code class="language-plaintext highlighter-rouge">alignmentGuide</code>의 signature은 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@inlinable</span> <span class="kd">public</span> <span class="kd">func</span> <span class="nf">alignmentGuide</span><span class="p">(</span>
<span class="n">_</span> <span class="nv">g</span><span class="p">:</span> <span class="kt">HorizontalAlignment</span><span class="p">,</span>
<span class="nv">computeValue</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">ViewDimensions</span><span class="p">)</span> <span class="o">-></span> <span class="kt">CGFloat</span>
<span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span>
</code></pre></div></div>
<p>뒤에 인자로 사용한 <code class="language-plaintext highlighter-rouge">$0</code>의 type이 <code class="language-plaintext highlighter-rouge">ViewDimensions</code>인데, 이는 우리가 정렬을 위해
사용할 수 있는 view의 수치를 담고 있다.
우리는 <code class="language-plaintext highlighter-rouge">.labelTrailingAlignment</code>에게 <code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>의
끝을 정렬 기준으로 넘겨주고 싶기 때문에 <code class="language-plaintext highlighter-rouge">$0[.trailing]</code>을 반환한 것이다.
이 <strong>반환 값은 별게 아닌 <code class="language-plaintext highlighter-rouge">CGFloat</code>로, <code class="language-plaintext highlighter-rouge">$0[.trailing]</code>은 <code class="language-plaintext highlighter-rouge">Text</code>의 너비</strong>이다.
그렇기 때문에 사실 <code class="language-plaintext highlighter-rouge">$0[.trailing]</code> 대신 <code class="language-plaintext highlighter-rouge">$0.width</code>를 통해 직접 값을 넘겨주어도
동일한 결과를 얻을 수 있다.
나아가서 <code class="language-plaintext highlighter-rouge">ViewDimensions</code>에는 <code class="language-plaintext highlighter-rouge">height</code>의 값도 있으며,
<code class="language-plaintext highlighter-rouge">HorizontalAlignment.trailing</code> 이외에도 <code class="language-plaintext highlighter-rouge">subscript</code>로 <code class="language-plaintext highlighter-rouge">VerticalAlignment</code>와
<code class="language-plaintext highlighter-rouge">HorizontalAlignment</code>의 값들을 모두 읽어올 수 있다.</p>
<p>그런데 이 결과는 역시 우리가 원하는 모습과 거리가 있다:
<img src="/assets/images/2020-04-14/iphone-try2.png" alt="iPhone try 2" class="shadow" /></p>
<p>이 결과는 child view가 자신의 크기를 정한 이후, parent view는 반드시 그 크기를
수용해야하는 SwiftUI의 특징에서 기인한다.
Alignment가 정해지기 전 크기를 정하고, 그 후에 parent view가 정렬을 하기 때문에
child view의 일부가 화면을 벗어나는 것이다.
이를 해결하는 것은 지금까지 언급한 범위 안에서도 할 수 있다.
그러나 Preference를 사용한 좀 더 일반적이고 효율적인 방법이 있으니, 먼저 간단한
방법을 소개한 후 이를 소개하도록 한다.</p>
<h3 id="간단한-ad-hoc-해결법">간단한 Ad-Hoc 해결법</h3>
<p>SwiftUI는 parent view가 child view(s)에게 크기를 제시한 뒤, child view가 정한
크기를 반드시 수용한다.
그렇기 때문에 우리는 모종의 방법으로 child view에게 크기를 <strong>재조정</strong>하도록
요청해야 한다.
우리는 <code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>의 너비가 같도록 하고 싶다.
그러면, 둘 중 너비가 큰 값으로 이 둘의 크기를 정하면 된다.
(이는 뒤에 제시할 일반적인 방법에서도 마찬가지다.)</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">maxTextWidth</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">?</span>
</code></pre></div></div>
<p>을 <code class="language-plaintext highlighter-rouge">ContentView</code>에 도입하자.
그리고 예상하듯이</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 생략</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
<span class="c1">// 생략</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
</code></pre></div></div>
<p>과 같이 <code class="language-plaintext highlighter-rouge">frame</code>을 정해준다.
이때 <code class="language-plaintext highlighter-rouge">Text("Title")</code>의 너비가 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>보다 좁을 것이 분명한데,
“Title”이 <code class="language-plaintext highlighter-rouge">Text</code>의 넓은 <code class="language-plaintext highlighter-rouge">frame</code>에서 덩그러니 가운데에 있으면 안되기 때문에
<code class="language-plaintext highlighter-rouge">, alignment: .trailing</code>을 주었다.</p>
<p>이제 <code class="language-plaintext highlighter-rouge">maxTextWidth</code>에 적절한 값을 넣어주면, SwiftUI가 알아서 변화를 감지하여
재조정을 할 것이다.
지금까지 글을 유심히 읽으신 독자분들은 위에 강조처리한 <code class="language-plaintext highlighter-rouge">alignmentGuide</code>가 받는
closure의 반환값이 <code class="language-plaintext highlighter-rouge">CGFloat</code>라는 사실을 기억할 것이다.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="n">d</span> <span class="k">in</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">maxTextWidth</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">maxTextWidth</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">width</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="n">width</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">d</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span> <span class="c1">// d.width와 동일</span>
<span class="p">}</span>
</code></pre></div></div>
<p>를 <code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>의 <code class="language-plaintext highlighter-rouge">.frame</code> 바로 뒤에 붙여주면 된다.
여기에 지저분한 <code class="language-plaintext highlighter-rouge">DispatchQueue.main.async</code>가 들어간 이유는, SwiftUI가 view를
rendering하는 순간에 <code class="language-plaintext highlighter-rouge">@State</code>를 바꾸면 문제가 생기기 때문이다.
따라서 SwiftUI가 view를 rendering한 후에 <code class="language-plaintext highlighter-rouge">@State</code>를 바꾸려면 이와 같은
gimmick을 써야 한다.
궁금한 독자들은 이 비동기 코드를 주석처리한 후 실행하면, Xcode가 친절하게</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[SwiftUI] Modifying state during view update, this will cause undefined behavior.
</code></pre></div></div>
<p>라는 경고를 띄워준다.</p>
<p>지금까지의 코드는 아래와 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">text</span> <span class="o">=</span> <span class="s">""</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">maxTextWidth</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">?</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="n">d</span> <span class="k">in</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">maxTextWidth</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">maxTextWidth</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">width</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="n">width</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">d</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span>
<span class="p">}</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="n">d</span> <span class="k">in</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">maxTextWidth</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">maxTextWidth</span><span class="p">,</span> <span class="n">d</span><span class="o">.</span><span class="n">width</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="n">d</span><span class="o">.</span><span class="n">width</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="n">d</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span>
<span class="p">}</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">textFieldStyle</span><span class="p">(</span><span class="kt">RoundedBorderTextFieldStyle</span><span class="p">())</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>결과는 우리가 원하는 바와 같다:
<img src="/assets/images/2020-04-14/iphone-try3.png" alt="iPhone try 3" class="shadow" /></p>
<h3 id="문제점">문제점</h3>
<p>그러나 이는 suboptimal하다.
아래와 같이 <code class="language-plaintext highlighter-rouge">ContentView</code>에 <code class="language-plaintext highlighter-rouge">static</code> counter를 더한 후 <code class="language-plaintext highlighter-rouge">maxTextWidth</code>이 몇
번이나 정해지는지 보자:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">maxTextWidth</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">?</span> <span class="p">{</span>
<span class="k">didSet</span> <span class="p">{</span>
<span class="k">Self</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="nf">print</span><span class="p">(</span><span class="k">Self</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>앞으로 SwiftUI API 변화에 따라 얼마나 바뀔지는 모르겠지만, 필자의 Xcode에서는
무려 14번이나 이 값을 변경한다.
단 두 개의 <code class="language-plaintext highlighter-rouge">Text</code>를 정렬하기 위해서 이런 비효율은 좋지 않다.
나아가 우리는 잠재적으로 훨씬 많은 수의 view들을 정렬하고 싶기 때문에, 다른
해결책을 강구하여야 한다.
다행히, 이런 문제를 해결하는 일반적인 방법이 있다.</p>
<h2 id="preference-anchor-geometryreader">Preference, Anchor, GeometryReader</h2>
<p>일반적으로, SwiftUI에서 child view가 parent view에게 값을 전달하는 방식은
<code class="language-plaintext highlighter-rouge">Preference</code>를 매개로 한다.
특히 우리의 문제 상황에서는 child view가 parent view에게 자신의 크기를
전달해주면 좋을 것이다.
이런 상황에 <code class="language-plaintext highlighter-rouge">anchorPreference</code>와 <code class="language-plaintext highlighter-rouge">Anchor</code>을 쓸 수 있다.</p>
<p><strong>놀랍게도 <code class="language-plaintext highlighter-rouge">anchorPreference</code>는 개발자 문서가 없다.</strong>
Signature은 정말이지…</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@inlinable</span> <span class="kd">public</span> <span class="kd">func</span> <span class="n">anchorPreference</span><span class="o"><</span><span class="kt">A</span><span class="p">,</span> <span class="kt">K</span><span class="o">></span><span class="p">(</span>
<span class="n">key</span> <span class="nv">_</span><span class="p">:</span> <span class="kt">K</span><span class="o">.</span><span class="k">Type</span> <span class="o">=</span> <span class="kt">K</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
<span class="nv">value</span><span class="p">:</span> <span class="kt">Anchor</span><span class="o"><</span><span class="kt">A</span><span class="o">>.</span><span class="kt">Source</span><span class="p">,</span>
<span class="nv">transform</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Anchor</span><span class="o"><</span><span class="kt">A</span><span class="o">></span><span class="p">)</span> <span class="o">-></span> <span class="kt">K</span><span class="o">.</span><span class="kt">Value</span>
<span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="k">where</span> <span class="kt">K</span> <span class="p">:</span> <span class="kt">PreferenceKey</span>
</code></pre></div></div>
<p>당황스러운 마음을 추스리고, <code class="language-plaintext highlighter-rouge">Anchor</code>의 개발자 문서를 읽자면:</p>
<blockquote>
<p>An opaque value derived from an anchor source and a particular view.</p>
<p>It may be converted to a <code class="language-plaintext highlighter-rouge">Value</code> in the coordinate
space of a target view, using a <code class="language-plaintext highlighter-rouge">GeometryProxy</code> value to specify
the target view.</p>
</blockquote>
<p>뭔가 <code class="language-plaintext highlighter-rouge">GeometryProxy</code> (<code class="language-plaintext highlighter-rouge">GeometryReader</code>의 closure 인자가 받는 인자)와 같이 써서
<code class="language-plaintext highlighter-rouge">Value</code>를 얻을 수 있다고 한다.
이 <code class="language-plaintext highlighter-rouge">Value</code>는 generic인 <code class="language-plaintext highlighter-rouge">Anchor<Value></code>의 <code class="language-plaintext highlighter-rouge">Value</code>이다.
그런데 <code class="language-plaintext highlighter-rouge">Value</code>가 무엇인지 알기 위해서는 <code class="language-plaintext highlighter-rouge">Anchor.Source</code>의 문서를 읽어보아야
한다:</p>
<blockquote>
<p>A type-erased geometry value that produces an anchored value of
type <code class="language-plaintext highlighter-rouge">Value</code>.</p>
<p>Anchored geometry values are passed around the
view tree via preference keys, and then converted back into the
local coordinate space via a <code class="language-plaintext highlighter-rouge">GeometryProxy</code> value.</p>
</blockquote>
<p>유심히 보면 이 <code class="language-plaintext highlighter-rouge">Anchor.Source</code>라는 것은 다름 아닌 <code class="language-plaintext highlighter-rouge">Value</code> 그 자체인데, 다만
type이 지워진 상태이다.
이는 preference keys를 매개로 돌아다닌다는데, 이는 <code class="language-plaintext highlighter-rouge">PreferenceKey</code>를 말하는
것이다.
무슨 말인지 하나도 이해가 안되더라도, 조금만 참자.
곧 모든 것이 명확해질 것이다!</p>
<p>마지막으로 <code class="language-plaintext highlighter-rouge">PreferenceKey</code>의 문서를 보면:</p>
<blockquote>
<p>A named value produced by a view.</p>
<p>Views with multiple children
automatically combine all child values into a single value visible
to their ancestors.</p>
</blockquote>
<p>View가 만드는 값이라고 한다.
굉장히 모호하게 들리는 말이다.
“무슨 값이라는 것인가?”라고 물을 수밖에 없는데, 재밌게도 ‘값’이라고 할 수 밖에
없는 것이, 어떠한 값이어도 상관 없기 때문이다.</p>
<h3 id="preferencekey">PreferenceKey</h3>
<p>계속 강조하는 것이지만, SwiftUI에서 parent view는 child view가 정한 크기를
그대로 수용해야 하지만, 이러한 경직성을 극복하기 위해 child view가 parent
view에게 어떠한 종류의 값이든지 넘길 수 있게 되어 있다.
Child view의 ‘취향’을 넘겨주는 것이다.
그러면 parent view는 child view들의 ‘취향’들을 모두 취합하여 적절하게 사용한다.</p>
<p>이렇게 전달되는 preference는 아무 값이나 될 수 있기 때문에, ‘어떤’
preference인가를 나타내는 key가 <code class="language-plaintext highlighter-rouge">PreferenceKey</code>인 것이다.
각 child view는 다양한 ‘취향’들을 <code class="language-plaintext highlighter-rouge">PreferenceKey</code>를 key로 하는 key-value pair로
parent view에게 전달한다.
우리의 경우에는 child view의 크기를 parent view에게 전달하고 싶은 것인데,
child view는 (예컨대) 자신이 생성된 시각을 parent view에게 <code class="language-plaintext highlighter-rouge">Date</code>로 전달해줄
수도 있는 것이다.</p>
<h3 id="anchorsource">Anchor.Source</h3>
<p><code class="language-plaintext highlighter-rouge">Anchor</code>는 initializer가 없다.
앞에 나왔던 무시무시한 <code class="language-plaintext highlighter-rouge">anchorPreference</code>가 주는 값을 쓸 수 밖에 없다.
불행 중 다행으로, <code class="language-plaintext highlighter-rouge">Anchor<Value></code>는 두 종류밖에 없다.
<code class="language-plaintext highlighter-rouge">Anchor<CGRect></code>와 <code class="language-plaintext highlighter-rouge">Anchor<CGPoint></code>이다.
(물론 <code class="language-plaintext highlighter-rouge">Anchor<Int?></code>와 같이 무의미한 type을 만들 수는 있지만, 우리는
<code class="language-plaintext highlighter-rouge">Anchor<Int?>.Source</code>만 조작할 수 있을 뿐 <code class="language-plaintext highlighter-rouge">Anchor<Int?></code>에 접근할 수 있는
property나 method가 없으므로 실질적으로 두 종류이다.)</p>
<p>아까 보았던 <code class="language-plaintext highlighter-rouge">ViewDimensions</code>과 거의 역할이 유사한데, <code class="language-plaintext highlighter-rouge">Anchor<CGRect></code> 버전은
단순한 <code class="language-plaintext highlighter-rouge">CGFloat</code>가 아니라 나중에 <code class="language-plaintext highlighter-rouge">GeometryProxy</code>가 사용할 수 있는 값을 담고
있다.
우리는 child view의 크기를 전달하고 싶기 때문에 <code class="language-plaintext highlighter-rouge">.bounds</code> property를 사용할
것이다.
만약 child view의 특점 위치를 전달하고 싶으면, <code class="language-plaintext highlighter-rouge">Anchor<CGPoint>.Source</code>의
<code class="language-plaintext highlighter-rouge">.bottomLeading</code> 같은 값을 쓰면 된다.</p>
<p>아직도 이 복잡한 관계들이 파악이 쉽게 되지 않을 텐데, 실제 <code class="language-plaintext highlighter-rouge">anchorPreference</code>의
작동과 사용을 보면 더 명확해질 것이다.</p>
<h3 id="anchorpreference">anchorPreference</h3>
<p><code class="language-plaintext highlighter-rouge">anchorPreference</code>를 쓰기 위해서는 <code class="language-plaintext highlighter-rouge">.bounds</code> property를 child view의 ‘취향’으로
전달할 때 사용할 <code class="language-plaintext highlighter-rouge">PreferenceKey</code>를 만들어야 한다.
우리는 <code class="language-plaintext highlighter-rouge">bounds</code>를 전달할 것이니 <code class="language-plaintext highlighter-rouge">BoundsPreferenceKey</code>라는 이름을 사용하였다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">BoundsPreferenceKey</span><span class="p">:</span> <span class="kt">PreferenceKey</span> <span class="p">{</span>
<span class="kd">static</span> <span class="k">var</span> <span class="nv">defaultValue</span> <span class="o">=</span> <span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">]()</span>
<span class="kd">static</span> <span class="kd">func</span> <span class="nf">reduce</span><span class="p">(</span>
<span class="nv">value</span><span class="p">:</span> <span class="k">inout</span> <span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">],</span>
<span class="nv">nextValue</span><span class="p">:</span> <span class="p">()</span> <span class="o">-></span> <span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">]</span>
<span class="p">)</span> <span class="p">{</span>
<span class="n">value</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="nf">nextValue</span><span class="p">())</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>위에 구현한 내용은 <code class="language-plaintext highlighter-rouge">PreferenceKey</code> protocol에 conform하기 위한 필요 조건이다.
<code class="language-plaintext highlighter-rouge">defaultValue</code>는 말 그대로 기본값으로, <code class="language-plaintext highlighter-rouge">anchorPreference</code>를 전달하지 않는
view가 내뱉는 값이다.
이때 <code class="language-plaintext highlighter-rouge">BoundsPreference</code>는</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">BoundsPreference</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">bounds</span><span class="p">:</span> <span class="kt">Anchor</span><span class="o"><</span><span class="kt">CGRect</span><span class="o">></span>
<span class="p">}</span>
</code></pre></div></div>
<p>처럼 <code class="language-plaintext highlighter-rouge">Anchor<CGRect></code>를 감싸는 아주 단순한 struct이다.</p>
<p>여기서 <code class="language-plaintext highlighter-rouge">BoundsPreference</code>가 <code class="language-plaintext highlighter-rouge">PreferenceKey</code>를 따를 때 associatedtype이
<code class="language-plaintext highlighter-rouge">BoundsPreference</code>가 아니라 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>인 이유는 child view들이 각각
<code class="language-plaintext highlighter-rouge">BoundsPreference</code>를 parent view에게 전달하면 parent view는 이를 array로 묶어
처리할 것이기 때문이다.
(사실 <code class="language-plaintext highlighter-rouge">BoundsPreference?</code> 같은 type을 써도 되는데, 구현이 다소 복잡해진다.)
그래서 child view도 그냥 <code class="language-plaintext highlighter-rouge">BoundsPreference</code>가 아니라 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>로
전달해야 한다.
모순적으로 들릴 수도 있지만, child view도 자신의 child view들에게는 parent
view이기 때문이다.
<code class="language-plaintext highlighter-rouge">reduce</code> method는 parent view가 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>를 취합하는 방식이다.</p>
<p>위 과정을 요약한 도식은 다음과 같다 (아래 구현을 보면 알겠지만 완전히 정확한
표현은 아니지만, data flow는 동일하다.):
<img src="/assets/images/2020-04-14/preferencekey.png" alt="PreferenceKey" class="shadow" /></p>
<h3 id="geometryreader와-geometryproxy">GeometryReader와 GeometryProxy</h3>
<p>지금까지의 난관을 잘 헤쳐왔다면, 이제는 비교적 간단한 <code class="language-plaintext highlighter-rouge">GeometryReader</code>와
<code class="language-plaintext highlighter-rouge">GeometryProxy</code>만 남았다.
<code class="language-plaintext highlighter-rouge">GeometryReader</code>는 child view에게 자신의 size를 포함한 값들, 즉 geometry를
전달해주는 view이다.
Child view는 <code class="language-plaintext highlighter-rouge">GeometryProxy</code>를 매개로 하여 값을 읽어올 수 있다.
아주 간단한 예시로는,</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">GeometryReader</span> <span class="p">{</span> <span class="p">(</span><span class="nv">geometry</span><span class="p">:</span> <span class="kt">GeometryProxy</span><span class="p">)</span> <span class="k">in</span>
<span class="kt">Rectangle</span><span class="p">()</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span>
<span class="nv">width</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span>
<span class="nv">height</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span> <span class="o">/</span> <span class="mi">2</span>
<span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>와 같이 쓸 수 있다.</p>
<p><code class="language-plaintext highlighter-rouge">GeometryProxy</code>의 <code class="language-plaintext highlighter-rouge">size</code> property는 별도의 설명이 필요 없을 만큼 하는 일과
목적이 명확하다고 생각한다.
다만</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">subscript</span><span class="o"><</span><span class="kt">T</span><span class="o">></span><span class="p">(</span><span class="nv">anchor</span><span class="p">:</span> <span class="kt">Anchor</span><span class="o"><</span><span class="kt">T</span><span class="o">></span><span class="p">)</span> <span class="o">-></span> <span class="kt">T</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
</code></pre></div></div>
<p>이 문제인데, 이미 <code class="language-plaintext highlighter-rouge">Anchor</code>를 소개하였기 때문에 그리 복잡하지 않다.
위에서 <code class="language-plaintext highlighter-rouge">Anchor</code>의 가장 독특하고도 까다로운 점이 안에 <code class="language-plaintext highlighter-rouge">CGRect</code>와 <code class="language-plaintext highlighter-rouge">CGPoint</code>를
우리가 직접 조작하거나 만들 수 없다는 것이었다.
그럼 값을 어떻게 쓰는가?
바로 <strong><code class="language-plaintext highlighter-rouge">GeometryProxy</code>의 <code class="language-plaintext highlighter-rouge">[]</code> 연산자, 즉 <code class="language-plaintext highlighter-rouge">subscript</code>를 통해서 <code class="language-plaintext highlighter-rouge">Anchor<T></code>의
<code class="language-plaintext highlighter-rouge">T</code>를 꺼낼 수 있다!</strong>
위의 signature를 보면 알 수 있듯, <code class="language-plaintext highlighter-rouge">GeometryProxy</code>인 <code class="language-plaintext highlighter-rouge">geometry</code>와
<code class="language-plaintext highlighter-rouge">Anchor<CGRect></code>인 <code class="language-plaintext highlighter-rouge">anchor</code>가 있다면, <code class="language-plaintext highlighter-rouge">geometry[anchor.bounds]</code>와 같이 해당
<code class="language-plaintext highlighter-rouge">CGRect</code>를 꺼낼 수 있다.</p>
<p>이제 거의 다 왔다.
<code class="language-plaintext highlighter-rouge">GeometryReader</code>를 쓸 때 주의할 점은, <code class="language-plaintext highlighter-rouge">GeometryReader</code>는 자신에게 주어진 크기를
<strong>전부</strong> 사용하기 때문에 간혹 view가 화면을 꽉 채우는 경우가 있다.
이러한 경우 <code class="language-plaintext highlighter-rouge">GeometryReader</code>를 parent view의 background로 사용해도 되는
경우라면 문제를 해결할 수 있다.
아래의 구현에서는 화면을 채우는 문제와 무관하게 <code class="language-plaintext highlighter-rouge">GeometryReader</code>를 background로
사용할 것이다.</p>
<p><strong>이제 중첩된 view를 정렬하도록 하자!</strong></p>
<h2 id="finale">Finale</h2>
<p><code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>이 자신의 <code class="language-plaintext highlighter-rouge">.bounds</code>를 넘겨주면 된다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Text</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$0</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span> <span class="p">}</span>
<span class="o">.</span><span class="nf">anchorPreference</span><span class="p">(</span>
<span class="nv">key</span><span class="p">:</span> <span class="kt">BoundsPreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
<span class="nv">value</span><span class="p">:</span> <span class="o">.</span><span class="n">bounds</span>
<span class="p">)</span> <span class="p">{</span>
<span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">(</span><span class="nv">bounds</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)]</span>
<span class="p">}</span>
<span class="c1">// Text("Long Title")도 동일</span>
</code></pre></div></div>
<p>이렇게 전달된 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>를 취합하는 것은 바깥의 <code class="language-plaintext highlighter-rouge">VStack</code>에서 해주면
된다.
일반적인 preference 값이었으면 <code class="language-plaintext highlighter-rouge">VStack</code>에</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@inlinable</span> <span class="kd">public</span> <span class="kd">func</span> <span class="n">onPreferenceChange</span><span class="o"><</span><span class="kt">K</span><span class="o">></span><span class="p">(</span>
<span class="n">_</span> <span class="nv">key</span><span class="p">:</span> <span class="kt">K</span><span class="o">.</span><span class="k">Type</span> <span class="o">=</span> <span class="kt">K</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
<span class="n">perform</span> <span class="nv">action</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">K</span><span class="o">.</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Void</span>
<span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="k">where</span> <span class="kt">K</span> <span class="p">:</span> <span class="kt">PreferenceKey</span><span class="p">,</span> <span class="kt">K</span><span class="o">.</span><span class="kt">Value</span> <span class="p">:</span> <span class="kt">Equatable</span>
</code></pre></div></div>
<p>을 사용하여 <code class="language-plaintext highlighter-rouge">VStack {...}.onPreferenceChange(BoundsPreferenceKey.self) {...}</code>와
같이 사용하였겠지만, <code class="language-plaintext highlighter-rouge">BoundsPreferenceKey.Value</code>인 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>은
<code class="language-plaintext highlighter-rouge">BoundsPreference</code>의 <code class="language-plaintext highlighter-rouge">bounds: Anchor<CGRect></code>가 <code class="language-plaintext highlighter-rouge">Equatable</code>이 아니기 때문에 쓸
수 없다.
그러므로 일반적인 preference는 위 method를 쓰면 되는데, 이 경우는</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@inlinable</span> <span class="kd">public</span> <span class="kd">func</span> <span class="n">backgroundPreferenceValue</span><span class="o"><</span><span class="kt">Key</span><span class="p">,</span> <span class="kt">T</span><span class="o">></span><span class="p">(</span>
<span class="n">_</span> <span class="nv">key</span><span class="p">:</span> <span class="kt">Key</span><span class="o">.</span><span class="k">Type</span> <span class="o">=</span> <span class="kt">Key</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
<span class="kd">@ViewBuilder</span> <span class="n">_</span> <span class="nv">transform</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Key</span><span class="o">.</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-></span> <span class="kt">T</span>
<span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="k">where</span> <span class="kt">Key</span> <span class="p">:</span> <span class="kt">PreferenceKey</span><span class="p">,</span> <span class="kt">T</span> <span class="p">:</span> <span class="kt">View</span>
</code></pre></div></div>
<p>을 쓸 수 있다.
이 method는 preference를 가지고 background view를 만들 때 사용한다.
동일하게 overlay를 만드는 버전도 있다.
우리는 background를 만드려는 것이 아니지만, <code class="language-plaintext highlighter-rouge">GeometryReader</code>을 background로
하여 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>를 읽어올 수 있다!</p>
<p><code class="language-plaintext highlighter-rouge">VStack</code> 맨 뒤에 다음과 같이 작성해준다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">.</span><span class="nf">backgroundPreferenceValue</span><span class="p">(</span><span class="kt">BoundsPreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">values</span> <span class="k">in</span>
<span class="kt">GeometryReader</span> <span class="p">{</span> <span class="n">geometry</span> <span class="k">in</span>
<span class="k">self</span><span class="o">.</span><span class="nf">readWidth</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">values</span><span class="p">,</span> <span class="nv">in</span><span class="p">:</span> <span class="n">geometry</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>이때 <code class="language-plaintext highlighter-rouge">readWidth</code>는 <code class="language-plaintext highlighter-rouge">ContentView</code>에 더해준 <code class="language-plaintext highlighter-rouge">private</code> method로,</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">readWidth</span><span class="p">(</span>
<span class="n">from</span> <span class="nv">values</span><span class="p">:</span> <span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">],</span>
<span class="k">in</span> <span class="nv">geometry</span><span class="p">:</span> <span class="kt">GeometryProxy</span>
<span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="n">values</span>
<span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">geometry</span><span class="p">[</span><span class="nv">$0</span><span class="o">.</span><span class="n">bounds</span><span class="p">]</span><span class="o">.</span><span class="n">width</span> <span class="p">}</span>
<span class="o">.</span><span class="nf">max</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kt">Rectangle</span><span class="p">()</span>
<span class="o">.</span><span class="nf">hidden</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>와 같다.
차근차근 보자면, <code class="language-plaintext highlighter-rouge">VStack</code>은 child view에게서 받아온 preference 값 (이 경우에는
두 <code class="language-plaintext highlighter-rouge">Text</code>가 보내온 길이 2인 <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>)을 가지고 작업을 하기 위해
<code class="language-plaintext highlighter-rouge">.babackgroundPreferenceValue</code> method를 사용하였다.
우리의 ‘취향’은 <code class="language-plaintext highlighter-rouge">BoundsPreferenceKey</code>를 일종의 ‘id’, 즉 key로 사용하였기 때문에
이를 알려주고, <code class="language-plaintext highlighter-rouge">backgroundPreferenceValue</code>는 결국 background view를 만드는
함수이기 때문에 <code class="language-plaintext highlighter-rouge">GeometryReader</code>를 사용하면 된다.</p>
<p><code class="language-plaintext highlighter-rouge">GeometryReader</code>는 <code class="language-plaintext highlighter-rouge">GeometryProxy</code>를 받아 <code class="language-plaintext highlighter-rouge">View</code>를 반환하기 때문에, 우리는
dummy view를 반환하면서 preference를 가지고 <code class="language-plaintext highlighter-rouge">@State maxTextWidth</code>를 조작하면
된다.
<code class="language-plaintext highlighter-rouge">GeometryReader</code>의 closure 인자 안에서 직접 <code class="language-plaintext highlighter-rouge">@State maxTextWidth</code>를 조작하려고
하면 Swift compiler가 당황하기 때문에, 이 작업은 함수로 추출하여 진행한다.
이것이 <code class="language-plaintext highlighter-rouge">readWidth</code> 함수인데, <code class="language-plaintext highlighter-rouge">[BoundsPreference]</code>와 <code class="language-plaintext highlighter-rouge">GeometryProxy</code>를 받아서
실질적인 작업을 한다.</p>
<p>이전에 <code class="language-plaintext highlighter-rouge">.alignmentGuide</code> 안에서 하였던 것처럼 <code class="language-plaintext highlighter-rouge">DispatchQueue.main.async</code> 안에서
<code class="language-plaintext highlighter-rouge">maxTextWidth</code>를 바꿔야 한다.
<code class="language-plaintext highlighter-rouge">values</code>가 바로 <code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>이 <code class="language-plaintext highlighter-rouge">.anchorPreference</code>
method에서 전달해준 <code class="language-plaintext highlighter-rouge">Anchor<CGRect></code>를 감싸는 <code class="language-plaintext highlighter-rouge">BoundsPreference</code>들이 들어있는
값이다.
따라서 각각의 <code class="language-plaintext highlighter-rouge">BoundsPreference</code>를 먼저 <code class="language-plaintext highlighter-rouge">geometry</code>에 넣어 <code class="language-plaintext highlighter-rouge">CGRect</code>로 변환해준
후(<code class="language-plaintext highlighter-rouge">.map</code>안의 <code class="language-plaintext highlighter-rouge">geometry[$0.bounds]</code>), <code class="language-plaintext highlighter-rouge">width</code>들의 array로 변환한다.
이후 최대값을 골라 <code class="language-plaintext highlighter-rouge">self.maxTextWidth</code>에 넣으면 끝!</p>
<p>마지막으로 <code class="language-plaintext highlighter-rouge">GeometryReader</code>의 closure 인자가 요구하는 <code class="language-plaintext highlighter-rouge">View</code>를 반환해야 하는데,
<code class="language-plaintext highlighter-rouge">EmptyView</code>를 반환하면 SwiftUI가 너무 똑똑하게 무시하고 넘어가기 때문에…
<code class="language-plaintext highlighter-rouge">Rectangle().hidden()</code>을 반환한다.
이렇게 나온 결과물은
<img src="/assets/images/2020-04-14/iphone-finale.png" alt="iPhone finale" class="shadow" />
이다.
아까 <code class="language-plaintext highlighter-rouge">.alignmentGuide</code> 안에서 한 것과 똑같아 감흥이 없다면, <code class="language-plaintext highlighter-rouge">static</code> counter로
<code class="language-plaintext highlighter-rouge">@State maxTextWidth</code>가 얼마나 씌어지는지 세어보면 딱 두 번이 나온다.</p>
<p>마지막으로 전체 코드를 적기 전에, <code class="language-plaintext highlighter-rouge">Text("Title")</code>과 <code class="language-plaintext highlighter-rouge">Text("Long Title")</code>의
논리가 완벽히 일치하기 때문에 다음과 같이 추출하였다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">wrappedText</span><span class="p">(</span><span class="n">_</span> <span class="nv">text</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$0</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span> <span class="p">}</span>
<span class="o">.</span><span class="nf">anchorPreference</span><span class="p">(</span>
<span class="nv">key</span><span class="p">:</span> <span class="kt">BoundsPreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
<span class="nv">value</span><span class="p">:</span> <span class="o">.</span><span class="n">bounds</span>
<span class="p">)</span> <span class="p">{</span>
<span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">(</span><span class="nv">bounds</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)]</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>전체 <code class="language-plaintext highlighter-rouge">ContentView</code>의 코드는 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">maxTextWidth</span><span class="p">:</span> <span class="kt">CGFloat</span><span class="p">?</span> <span class="p">{</span>
<span class="k">didSet</span> <span class="p">{</span>
<span class="k">Self</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="nf">print</span><span class="p">(</span><span class="k">Self</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">text</span> <span class="o">=</span> <span class="s">""</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">VStack</span><span class="p">(</span><span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="nf">wrappedText</span><span class="p">(</span><span class="s">"Title"</span><span class="p">)</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="kt">HStack</span> <span class="p">{</span>
<span class="nf">wrappedText</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">)</span>
<span class="kt">TextField</span><span class="p">(</span><span class="s">"Long Title"</span><span class="p">,</span> <span class="nv">text</span><span class="p">:</span> <span class="err">$</span><span class="n">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">textFieldStyle</span><span class="p">(</span><span class="kt">RoundedBorderTextFieldStyle</span><span class="p">())</span>
<span class="o">.</span><span class="nf">padding</span><span class="p">()</span>
<span class="o">.</span><span class="nf">backgroundPreferenceValue</span><span class="p">(</span><span class="kt">BoundsPreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">values</span> <span class="k">in</span>
<span class="kt">GeometryReader</span> <span class="p">{</span> <span class="p">(</span><span class="nv">geometry</span><span class="p">:</span> <span class="kt">GeometryProxy</span><span class="p">)</span> <span class="k">in</span>
<span class="k">self</span><span class="o">.</span><span class="nf">readWidth</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">values</span><span class="p">,</span> <span class="nv">in</span><span class="p">:</span> <span class="n">geometry</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">wrappedText</span><span class="p">(</span><span class="n">_</span> <span class="nv">text</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">Text</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
<span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">maxTextWidth</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="o">.</span><span class="n">trailing</span><span class="p">)</span>
<span class="o">.</span><span class="nf">alignmentGuide</span><span class="p">(</span><span class="o">.</span><span class="n">labelTrailingAlignment</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$0</span><span class="p">[</span><span class="o">.</span><span class="n">trailing</span><span class="p">]</span> <span class="p">}</span>
<span class="o">.</span><span class="nf">anchorPreference</span><span class="p">(</span>
<span class="nv">key</span><span class="p">:</span> <span class="kt">BoundsPreferenceKey</span><span class="o">.</span><span class="k">self</span><span class="p">,</span>
<span class="nv">value</span><span class="p">:</span> <span class="o">.</span><span class="n">bounds</span>
<span class="p">)</span> <span class="p">{</span>
<span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">(</span><span class="nv">bounds</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)]</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">readWidth</span><span class="p">(</span>
<span class="n">from</span> <span class="nv">values</span><span class="p">:</span> <span class="p">[</span><span class="kt">BoundsPreference</span><span class="p">],</span>
<span class="k">in</span> <span class="nv">geometry</span><span class="p">:</span> <span class="kt">GeometryProxy</span>
<span class="p">)</span> <span class="o">-></span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">maxTextWidth</span> <span class="o">=</span> <span class="n">values</span>
<span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">geometry</span><span class="p">[</span><span class="nv">$0</span><span class="o">.</span><span class="n">bounds</span><span class="p">]</span><span class="o">.</span><span class="n">width</span> <span class="p">}</span>
<span class="o">.</span><span class="nf">max</span><span class="p">()</span>
<span class="p">}</span>
<span class="k">return</span> <span class="kt">Rectangle</span><span class="p">()</span>
<span class="o">.</span><span class="nf">hidden</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="updates">Updates</h2>
<p>macOS에서는 window의 크기 조절이 자유롭기 때문에, window가 너무 좁을 경우
<code class="language-plaintext highlighter-rouge">Text</code> 자체가 사라지는 경우가 있다.
이는 <code class="language-plaintext highlighter-rouge">wrappedText</code>의 <code class="language-plaintext highlighter-rouge">Text(text)</code>에 <code class="language-plaintext highlighter-rouge">.layoutPriority(1)</code>을 더해주면 된다.
<code class="language-plaintext highlighter-rouge">Text</code>가 가장 먼저 parent view가 제시한 공간을 쓸 수 있도록 우선 순위를 높여준
것이다.
기본값은 0이다.</p>
<p>본 포스트에서는 iOS 예시만 보였지만, macOS에서 <code class="language-plaintext highlighter-rouge">Picker</code>을 사용한 예시를 포함한
완성된 코드는 <a href="https://github.com/Zeta611/SwiftUI-Nested-Layout">GitHub</a>에서
확인할 수 있다.</p>Zetajaeho.lee@snu.ac.krSwiftUI의 Alignment, Preference, GeometryReader, 그리고 Anchor을 통해 중첩된 View들을 정렬하는 방법을 알아본다.Clean Architecture는 iOS에 과하다2020-04-08T00:00:00+09:002020-04-08T00:00:00+09:00https://zetablog.io/posts/clean-architecture-in-ios-is-too-much<p><img src="https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg" alt="Figure 1" class="shadow" /></p>
<h1 id="clean-architecture">Clean Architecture?</h1>
<p>몇 주 전 미루고 미루던
<a href="https://www.amazon.com/dp/0134494164">Clean Architecture</a>를 완독하였다.
그리고 기대가 컸던 탓인지 실망도 컸다.
우선, 평소에 읽는 서적들과 달리 정보의 밀도가 굉장히 낮았다.
이곳저곳 흥미있는 내용과 경험에서 나오는 조언이 있었지만, 책을 읽는 내내 같은
말을 반복한다는 느낌이 떠나지 않았다.
결국 핵심은 의존성 역전(Dependency Inversion)과 구조 사이의 선을 잘 그으라는
것.
책은 이를 중심으로 다양한 사례와 경험을 서술한다.
그 중 가장 유명하고 이 책의 제목을 본딴 ‘디자인 패턴’이 위 그림에 나온 Clean
Architecture이다.</p>
<p>이미 제목을 보았겠지만, 이 글은 Clean Architecture를 iOS에 적용하여 얼마나
코드가 깔끔해졌는지, 얼마나 정돈이 잘 되었는지를 설명하는 글이 아니다.
다만 들어가기 전에 오해하지 말아야 할 점은, 필자는 Clean Architecture가
잘못되었다고 주장하는 것이 아니라는 점이다.
서버나 Java로 작성된 엔터프라이즈 소프트웨어를 작성하는 것이 아니라 iOS 앱을
개발하는 것이라면, Clean Architecture는 적합하지 않다는 것이다.
특히 SwiftUI와 Combine을 통해 네이티브하게 선언형 프로그래밍과 데이터 중심의
설계가 가능해진 iOS 13 이상에서 말이다.
(벌써부터 SwiftUI 같은 ‘디테일’을 언급한다고, 눈살을 찌뿌리는 독자들이 있으리라
믿는다.)
또한, 본 글에서 다루는 ‘Clean Architecture’는 위 그림에 나오는 형태의 구성
자체를 말한다.
필자는 Uncle Bob (책과 위 디자인 패턴의 저자)이 주장하는 <strong>dependency 규칙들이나
layer를 나누는 일반적인 방법론</strong>에는 전적으로 동의한다.</p>
<h2 id="배경">배경</h2>
<p>자세한 내용은 <a href="https://www.amazon.com/dp/0134494164">동명의 책</a>이나
<a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">블로그 포스트</a>에
잘 정리되어 있으니 여기서는 간단히만 설명한다.</p>
<p>Entities는 프로그램의 핵심 대상들을 말하는데, 이 ‘핵심 대상’이란 UI나 DB 구조
등의 변화에 바뀌지 않는 가장 고수준의 물건(객체)을 말한다.
Use cases는 각 앱에 국한된 논리를 포함한다.
이들은 entities에 영향을 주지는 않지만, 마찬가지로 DB나 UI 등에 영향을 받지 않는
계층이다.</p>
<p>여기까지는 앱에서도 적용할 수 있는 내용이다.
Use cases는 앱에서 있는 다양한 동선–화면들의 흐름–을 나타낸다고 보거나, 각
화면을 use case로 볼 수도 있겠다.
다만 녹색과 파란색 계층의 구성이 필자가 iOS 앱에서 어울리지 않다고 생각하는
부분이다.</p>
<p>Controller는 UI 등에서 입력을 받아 use case의 interactor에 전달해주는 역할을
한다.
Presenter은 반대로 use case의 interactor로부터 UI에 전달해주는 역할을 한다.
사실, 대부분 iOS 앱들은 코드의 70% 이상이 이 controller-interactor-presenter
계층에 할애한다고 생각한다.
70%가 아니라 <em>90%</em>가 되어도 이상할 것이 없을 것이, 간단한 앱들은 대부분 API를
활용해 가공하는 것이 전부이기 때문이다.</p>
<p>아무튼 설명을 계속하자면, gateway는 DB, Web API 등에서 정보를 취합하는 창구
역할을 한다.
이 부분이 필요하다는 것에는 필자도 이견이 없다.
그렇지 않으면 (마음 한 구석이 찔리긴 하지만) 다른 계층에 DB 접근이나 네트워크
통신 코드가 들어가는데, 영 좋지 않다.</p>
<p>마지막으로 DB, devices, Web, UI, external interfaces는 별도의 설명이 필요
없다고 생각한다.
다시 iOS의 세계로 돌아와서, UI가 Devices와 Web, DB와 같은 계층이 있다는 점에서
다시 한 번 생각해보자.
과연 그런가?
UIKit의 <code class="language-plaintext highlighter-rouge">UIView</code>라면 납득이 가지만, SwiftUI의 <code class="language-plaintext highlighter-rouge">some View</code>라면?
또한, 이제 presenter라던지 controller와 같은 계층이 꼭 필요한가?
SwiftUI와 Combine의 등장에도 저 구조는 여전히 유효한가?</p>
<p>혹자는 Clean Architecture 책에 나와 있는 것처럼 프레임워크에 종속되면 안된다고
말할 수도 있다.
UIKit을 쓰던 SwiftUI를 쓰던 논리적으로 UI의 위계는 최하단 계층에 <strong>있어야
된다</strong>고 주장할 수도 있다.
그런데, 우리는 이미 Apple이 만든 기기에서 돌아가는, Apple이 만든 언어로,
Apple이 만든 도구로, Apple이 만든 프레임워크를 사용해 Apple 제품을 쓰는
사용자를 위해 앱을 만든다.</p>
<p>이렇게 쓰고 보니 조금 무섭기는 하지만, 그게 앱 개발의 현실이다.
우리는 화면에 데이터를 표시하기 위해 Apple이 만든 함수를 사용하고, 앱의
시작점도 Apple이 정해준 <code class="language-plaintext highlighter-rouge">AppDelegate</code>에서 시작한다.
<a href="https://developer.apple.com/videos/play/wwdc2019/212/">Apple이 새로운 제품을 지원하기 위해 앱의 시작점을 변형한다면</a>
우리는 별 수 없이 앱을 변형해야 한다.
이런 상황에서 우리가 할 수 있고, 해야 하는 최선은 <strong>주어진 프레임워크에서
효율적인 앱을 설계하는</strong> 나름의 방식을 찾는 것이다.
이것을 아키텍쳐라고 부르지는 않겠고, 디자인 패턴이라고 하겠다.
(Clean Architecture 책 앞부분에서 디자인 패턴과 아키텍쳐는 명확한 경계가 없다고
말했지만…)</p>
<p>아무튼, 지금까지 필자의 주장을 요약하자면 <em>iOS에 Clean Architecture를
곧이곧대로 적용하면 안된다</em>는 것이다.
iOS 개발을 할 때, 혹은 다른 특수한 목적으로 개발을 할 때에는 상황에 맞춰서
경계를 합치거나 변형을 해야 한다.
그것이 우리에게 주어진 프레임워크에게 종속되는 일이더라도 말이다 (적어도
Apple이 직접 만든 것들에 대해서는).</p>
<p>그런데, 여기로 내용이 끝난다면 이 글은 결국 필자의 ‘주장’ 그 이상도 이하도 아닐
것이다.
프로그래밍은 이념 싸움이 아니다.
아래에 새로 도입된 SwiftUI와 Combine을 활용하여 위의 Clean Architecture 패턴을
충실히 구현한 결과를 서술한다.</p>
<h1 id="clean-architecture의-구현">Clean Architecture의 구현</h1>
<p>실제 구현은 이와 같은 순서로 진행하지 않았지만, 이해를 위해 <code class="language-plaintext highlighter-rouge">Presenter</code>,
<code class="language-plaintext highlighter-rouge">Controller</code>, <code class="language-plaintext highlighter-rouge">Interactor</code>의 protocol을 먼저 나열한다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protocol</span> <span class="kt">Presenter</span> <span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
<span class="kd">associatedtype</span> <span class="kt">ViewModel</span>
<span class="k">var</span> <span class="nv">viewModel</span><span class="p">:</span> <span class="kt">ViewModel</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">protocol</span> <span class="kt">Controller</span> <span class="p">{</span>
<span class="kd">associatedtype</span> <span class="kt">InputPort</span>
<span class="kd">associatedtype</span> <span class="kt">Action</span>
<span class="k">var</span> <span class="nv">inputPort</span><span class="p">:</span> <span class="kt">InputPort</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="k">var</span> <span class="nv">actionHandler</span><span class="p">:</span> <span class="kt">PassthroughSubject</span><span class="o"><</span><span class="kt">Action</span><span class="p">,</span> <span class="kt">Never</span><span class="o">></span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">protocol</span> <span class="kt">Interactor</span> <span class="p">{</span>
<span class="kd">associatedtype</span> <span class="kt">OutputPort</span>
<span class="k">var</span> <span class="nv">outputPort</span><span class="p">:</span> <span class="kt">OutputPort</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Interactor</code>는 결과적으로 <code class="language-plaintext highlighter-rouge">Controller</code>의 <code class="language-plaintext highlighter-rouge">InputPort</code>가 되고, <code class="language-plaintext highlighter-rouge">Presenter</code>를
<code class="language-plaintext highlighter-rouge">OutputPort</code>로 가질 것이다.
또한 <code class="language-plaintext highlighter-rouge">Controller</code>는 view로부터 action을 전달 받을 것인데, 매개체는
<code class="language-plaintext highlighter-rouge">Controller</code>의 <code class="language-plaintext highlighter-rouge">actionHandler</code>가 될 것이다.
<code class="language-plaintext highlighter-rouge">Presenter</code>는 view에게 <code class="language-plaintext highlighter-rouge">ViewModel</code>을 제공할 것인데, 그 구체적인 방식은 view가
<code class="language-plaintext highlighter-rouge">Presenter</code>를 <code class="language-plaintext highlighter-rouge">ObservedObject</code>로 가지고 이벤트를 받는 것이다.</p>
<p>나아가서 <code class="language-plaintext highlighter-rouge">Interactor</code>는 그림에 나와 있듯이 이들과 직접 소통하지 않고
<code class="language-plaintext highlighter-rouge">InputPort</code>와 <code class="language-plaintext highlighter-rouge">OutputPort</code>라는 추상 계층을 사이로 소통한다.
이는 use case 계층의 <code class="language-plaintext highlighter-rouge">Interactor</code>를 나머지 둘과 분리하기 위함이다.
Associated type <code class="language-plaintext highlighter-rouge">InputPort</code>와 <code class="language-plaintext highlighter-rouge">OutputPort</code>는 다음과 같이 <code class="language-plaintext highlighter-rouge">MyInputPort</code>와
<code class="language-plaintext highlighter-rouge">MyOutputPort</code>로 정의되어 있다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protocol</span> <span class="kt">MyInputPort</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">execute</span><span class="p">(</span><span class="nv">request</span><span class="p">:</span> <span class="kt">MyRequest</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">protocol</span> <span class="kt">MyOutputPort</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">show</span><span class="p">(</span><span class="n">_</span> <span class="nv">response</span><span class="p">:</span> <span class="kt">MyResponse</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>보면 <code class="language-plaintext highlighter-rouge">MyInputPort</code>와 <code class="language-plaintext highlighter-rouge">MyOutputPort</code>는 각각 <code class="language-plaintext highlighter-rouge">MyRequest</code>와 <code class="language-plaintext highlighter-rouge">MyResponse</code>라는
정해진 메시지를 통해 데이터를 주고 받는다.
<code class="language-plaintext highlighter-rouge">MyRequest</code>와 <code class="language-plaintext highlighter-rouge">MyResponse</code>는 각각 <code class="language-plaintext highlighter-rouge">enum</code>과 <code class="language-plaintext highlighter-rouge">struct</code>로 하나는
<code class="language-plaintext highlighter-rouge">Controller</code>가 <code class="language-plaintext highlighter-rouge">InputPort</code>, 즉 <code class="language-plaintext highlighter-rouge">Interactor</code>에게 보낼 요청을 나열한 것과 다른
하나는 <code class="language-plaintext highlighter-rouge">Interactor</code>가 <code class="language-plaintext highlighter-rouge">OutputPort</code>, 즉 <code class="language-plaintext highlighter-rouge">Presenter</code>에게 넘겨주는 데이터이다.
<code class="language-plaintext highlighter-rouge">MyRequest</code>의 경우에는 요청을 나열한 것이기 때문에 각 요청에 대해 associated
value가 있는 경우가 많을 것이다.</p>
<p>실제 <code class="language-plaintext highlighter-rouge">Interactor</code>의 구현인 <code class="language-plaintext highlighter-rouge">MyInteractor</code>는 다음과 같은 모습을 가진다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">MyInteractor</span> <span class="p">:</span> <span class="kt">Interactor</span><span class="p">,</span> <span class="kt">MyInputPort</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">gateway</span><span class="p">:</span> <span class="kt">MyGateway</span>
<span class="k">let</span> <span class="nv">outputPort</span><span class="p">:</span> <span class="kt">MyOutputPort</span>
<span class="kd">func</span> <span class="nf">execute</span><span class="p">(</span><span class="nv">request</span><span class="p">:</span> <span class="kt">MyRequest</span><span class="p">)</span> <span class="p">{</span>
<span class="k">switch</span> <span class="n">request</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="nf">create</span><span class="p">(</span><span class="k">let</span> <span class="nv">foo</span><span class="p">):</span>
<span class="n">gateway</span><span class="o">.</span><span class="nf">create</span><span class="p">(</span><span class="n">foo</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">outputPort</span><span class="o">.</span><span class="nf">show</span><span class="p">(</span><span class="kt">MyResponse</span><span class="p">(</span><span class="n">gateway</span><span class="o">.</span><span class="nf">readDate</span><span class="p">()))</span>
<span class="p">}</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">outputPort</span><span class="p">:</span> <span class="kt">MyOutputPort</span><span class="p">,</span> <span class="nv">gateway</span><span class="p">:</span> <span class="kt">MyGateway</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">outputPort</span> <span class="o">=</span> <span class="n">outputPort</span>
<span class="k">self</span><span class="o">.</span><span class="n">gateway</span> <span class="o">=</span> <span class="n">gateway</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">MyGateway</code>는 실제 DB 혹은 API를 감싸는 protocol으로, interactor와의 dependency
inversion을 위함이다.
이 부분은 use case의 핵심 논리를 담고 있기 때문에 위의 예시보다 복잡할 것이다.</p>
<p>지금까지가 use case에 해당하는 interactor 단의 구조이다.
벌써부터 불필요한 boilerplate 코드가 산더미다.
Apple이 WWDC 2019에서 강조한, “우리 앱의 특색 있는 기능” 구현은 언제부터 할 수
있을까?</p>
<p>이제 <code class="language-plaintext highlighter-rouge">Controller</code>의 구현인 <code class="language-plaintext highlighter-rouge">MyController</code>를 보자.
기본적인 구조는 다음과 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">MyController</span> <span class="p">:</span> <span class="kt">Controller</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">inputPort</span><span class="p">:</span> <span class="kt">MyInputPort</span>
<span class="k">let</span> <span class="nv">actionHandler</span> <span class="o">=</span> <span class="kt">PassthroughSubject</span><span class="o"><</span><span class="kt">MyAction</span><span class="p">,</span> <span class="kt">Never</span><span class="o">></span><span class="p">()</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">cancellable</span><span class="p">:</span> <span class="kt">Cancellable</span><span class="p">?</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">awesomeMethod</span><span class="p">()</span> <span class="p">{}</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">inputPort</span><span class="p">:</span> <span class="kt">MyInputPort</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">inputPort</span> <span class="o">=</span> <span class="n">inputPort</span>
<span class="k">self</span><span class="o">.</span><span class="n">cancellable</span> <span class="o">=</span> <span class="n">actionHandler</span><span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">action</span> <span class="k">in</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">self</span> <span class="o">=</span> <span class="k">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">awesome</span><span class="p">:</span>
<span class="k">self</span><span class="o">.</span><span class="nf">awesomeMethod</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>다른 구현과 마찬가지로 <code class="language-plaintext highlighter-rouge">MyController</code>는 단순히 구현의 예시를 위한 stub
클래스이다.</p>
<p>Presenter는 SwiftUI를 사용할 때 특히 역할이 없는 부분이다.
다음과 같은 단순한 형태이다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">MyPresenter</span> <span class="p">:</span> <span class="kt">Presenter</span><span class="p">,</span> <span class="kt">MyOutputPort</span> <span class="p">{</span>
<span class="kd">@Published</span> <span class="k">var</span> <span class="nv">viewModel</span> <span class="o">=</span> <span class="kt">MyViewModel</span><span class="p">()</span>
<span class="kd">func</span> <span class="nf">show</span><span class="p">(</span><span class="n">_</span> <span class="nv">response</span><span class="p">:</span> <span class="kt">MyResponse</span><span class="p">)</span> <span class="p">{</span>
<span class="n">viewModel</span> <span class="o">=</span> <span class="kt">MyViewModel</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="nf">convertToData</span><span class="p">())</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Presenter</code>는 view가 사용하기 쉬운 형태로 response를 가공하여 넘겨주는 역할을
하지만, SwiftUI는 view가 상태의 함수로서 <code class="language-plaintext highlighter-rouge">viewModel</code>의 변화를 감지하여 자동으로
처리하기 때문에 그 필요성이 사라졌다.</p>
<p>마지막으로 이 모두를 취합하는 view인 <code class="language-plaintext highlighter-rouge">MyView</code>의 구조는 아래와 같다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">MyView</span> <span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">controller</span><span class="p">:</span> <span class="kt">MyController</span>
<span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">presenter</span><span class="p">:</span> <span class="kt">MyPresenter</span>
<span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
<span class="kt">EmptyView</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>지금까지의 객체들은 factory를 통해 한 곳에서 생성해줄 수 있다:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">MyViewFactory</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">gateway</span><span class="p">:</span> <span class="kt">MyGateway</span>
<span class="kd">func</span> <span class="nf">createView</span><span class="p">()</span> <span class="o">-></span> <span class="kt">MyView</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">presenter</span> <span class="o">=</span> <span class="kt">MyPresenter</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">interactor</span> <span class="o">=</span> <span class="kt">MyInteractor</span><span class="p">(</span><span class="nv">outputPort</span><span class="p">:</span> <span class="n">presenter</span><span class="p">,</span> <span class="nv">gateway</span><span class="p">:</span> <span class="n">gateway</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">controller</span> <span class="o">=</span> <span class="kt">MyController</span><span class="p">(</span><span class="nv">inputPort</span><span class="p">:</span> <span class="n">interactor</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">view</span> <span class="o">=</span> <span class="kt">MyView</span><span class="p">(</span><span class="nv">controller</span><span class="p">:</span> <span class="n">controller</span><span class="p">,</span> <span class="nv">presenter</span><span class="p">:</span> <span class="n">presenter</span><span class="p">)</span>
<span class="k">return</span> <span class="n">view</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>또한 여기서 제시한 구조는 메모리 cycle이 생기지 않도록 객체들의 소유권을 분배한
것으로, 각 class의 <code class="language-plaintext highlighter-rouge">deinit</code>에 메시지를 넣어 제대로 release되는지 확인해볼 수
있다.
Xcode의 디버그 메모리 그래프를 통해 각 객체의 관계를 확인하면, <code class="language-plaintext highlighter-rouge">MyController</code>와
<code class="language-plaintext highlighter-rouge">MyInteractor</code>는
<img src="/assets/images/2020-04-10/interactor-graph.png" alt="Interactor Graph" class="shadow" />
와 같고, <code class="language-plaintext highlighter-rouge">MyPresenter</code>는
<img src="/assets/images/2020-04-10/presenter-graph.png" alt="Presenter Graph" class="shadow" />
와 같다.
모두 일차적으로 SwiftUI view인 <code class="language-plaintext highlighter-rouge">MyView</code>에 의해 retain되어 있다.</p>
<p>지금까지의 코드는 모두 핵심 논리 없이 Clean Architecture의 boilerplate만을
구현해본 것이다.
Code generator 도구를 사용하면 boilerplate 코드 작성에 대한 부담이 줄어들 수도
있겠지만, 이는 근본적인 해결책이 아니다.
위에 구현한 것과 같이 Clean Architecture를 곧이곧대로 구현한 것은 아니지만,
<a href="https://www.objc.io/issues/13-architecture/viper/">VIPER</a>과 같은 Clean
Architecture의 변형도 ‘너무 복잡하다’는 비판을 피해가지는 못한다.</p>
<h2 id="대안">대안?</h2>
<p>그러나 Clean Architecture에서 배울 점은 분명히 있다.
동명의 책에서 강조하는 바처럼, 역할에 맞게 계층을 나누는 방법론을 적용하여
SwiftUI와 Combine을 활용한 모던 iOS 앱에 적합한 패턴을 구성할 수 있다.</p>
<p>필자는 위의 구조에서 필요 없는 부분인 presenter 계층을 제거한 채 view model에
대응되는 state를 사용하고, controller 계층을 제거하고 View가 직접 interactor에
action을 전달하는 방식을 채택하였다.
이러한 구성으로는 데이터가 단방향으로 흐른다 (unidirectional data flow).
View가 interactor에게 action을 보내면 interactor는 state를 업데이트하고,
state의 변화를 view가 감지하여 자동으로 rendering을 하는 방식이다.
이는 redux의 구성과 유사한데, 필자가 이 구조로 macOS를 위한 간단한 video
converter 앱을 만든 내용을 다음 포스팅에 소개하겠다.</p>Zetajaeho.lee@snu.ac.krSwiftUI와 Combine을 사용해 Clean Architecture를 구현해보고, 왜 Clean Architecture가 iOS 환경에 어울리지 않는지 주장한다.C언어, Python 2, PyPy2, Swift 4의 속도 비교2018-08-29T00:00:00+09:002018-08-29T00:00:00+09:00https://zetablog.io/posts/speed-comparison-of-c-vs-python-2-vs-pypy2-vs-swift-4-sieve-of-eratosthenes<p><img src="/assets/images/2018-08-29-1/programming languages.jpeg" alt="Figure 1" class="shadow" /></p>
<h1 id="swift의-실행-속도">Swift의 실행 속도?</h1>
<p>최근에 Swift로 에라토스테네스의 체를 구현하였는데, Python(의 PyPy 구현. PyPy에 대해서는 <a href="https://pypy.org">pypy.org</a>를 참고하자.)보다 실행 속도가 무려 10배 정도 느린 것을 확인하였다. 이에 따라 <em>1. Swift가 굉장히 비효율적인 언어</em>이거나, <em>2. 아직 Swift에 익숙하지 않아 최적화된 방식으로 작성하지 못한 것</em>이라고 생각하였다. 한편 Swift로 <a href="https://projecteuler.net">Project Euler</a>의 문제를 풀던 중, 런타임 에러가 발생하는데도 불구하고 에러 메시지가 표시되지 않는 문제가 있었다. 이에 AppCode (JetBrains 회사의 Swift/Objective-C IDE) configuration을 확인해보니 릴리즈 모드로 컴파일되고 있었고, 정상적으로 에러 메시지를 표시하려면 디버그 모드로 컴파일했다. 그런데 위에서 이 설정이 문제가 되었던 것이, 디버그 모드로 컴파일하면 디버깅이 가능하도록 에러 메시지가 표시되지만 실행 속도는 현저히 느려지기 때문이다. 다시 디버깅을 완료한 후 릴리즈 모드로 컴파일하니 속도가 10배 정도 빨라졌다.</p>
<p>속도 문제는 해결하였지만, PyPy와 Swift의 속도가 엇비슷한 것을 보고 Swift의 속도가 어느 정도 빠른 것인지, 약간의 구글링을 해보았다. 검색을 해보니 Swift가 아주 빠른 것은 아니지만, 점점 최적화를 하여 최신 버전인 Swift 4는 실행 속도가 상당히 개선되었다고 한다. 그래서 직접 C, PyPy, 그리고 Swift로 에라토스테네스의 체를 구현하여 실행 속도를 비교해보았다.
C는 Apple LLVM 9.1.0로 compile했으며, Python은 Python 2.7.13의 PyPy v6.0.0을, Swift는 Swift 4.1.2를 사용하였다. Python의 경우 CPython 2.7.15에 대해서도 함께 시간을 측정하였다. 모두 100 000 000까지의 소수를 구하는 체를 구현하였고, 출력 시간으로 인한 병목 현상을 없애기 위해 출력은 주석 처리하였다. CPython을 제외하고는 100번씩 진행한 평균 시간을 측정하였다. (CPython의 경우 100번을 진행하면 무려 30분 정도의 시간이 걸릴 것으로 예상되었다.)</p>
<h1 id="언어별-에라토스테네스의-체-구현">언어별 에라토스테네스의 체 구현</h1>
<h2 id="c-구현-기준-시간">C 구현 (기준 시간)</h2>
<p>C 언어는 비교적 기계어에 근접한 로우 레벨 언어임으로 실행 속도가 가장 빠를 것으로 기대할 수 있다. 아래 표에 100 000 000까지의 소수를 에라토스테네스의 체를 사용해 C언어로 구하는데 걸리는 시간이 나와있다. 최적화를 하지 않은 -O0 옵션 플래그와 -O1, -O2, -O3, 그리고 -Os 최적화 옵션 플래그를 적용한 경우에 대해서 측정하였다. (최적화 옵션에 대해서는 <a href="https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html">Using the GNU Compiler Collection (GCC) 3.10 Options That Control Optimization</a>을 참고하자.) 최적화를 하지 않았을 경우에는 평균 2초의 수행시간이 걸렸으며, 최적화를 하였을 경우에는 1.5초 정도의 시간이 걸렸다.</p>
<table>
<thead>
<tr>
<th style="text-align: center">최적화 옵션</th>
<th style="text-align: center">평균 실행 시간 [초]</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">-O0</td>
<td style="text-align: center">1.919 700</td>
</tr>
<tr>
<td style="text-align: center">-O1</td>
<td style="text-align: center">1.578 928</td>
</tr>
<tr>
<td style="text-align: center">-O2</td>
<td style="text-align: center">1.531 234</td>
</tr>
<tr>
<td style="text-align: center">-O3</td>
<td style="text-align: center">1.543 294</td>
</tr>
<tr>
<td style="text-align: center">-Os</td>
<td style="text-align: center">1.543 973</td>
</tr>
</tbody>
</table>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <stdio.h>
#include <stdlib.h>
#include <time.h>
</span>
<span class="kt">void</span> <span class="nf">sieve_eratosthenes</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">int</span> <span class="o">*</span><span class="n">sieve</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="n">n</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">));</span>
<span class="n">sieve</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">p</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span> <span class="n">p</span> <span class="o"><=</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">p</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">sieve</span><span class="p">[</span><span class="n">p</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="cm">/* printf("%d\n", p); */</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="n">p</span> <span class="o">*</span> <span class="n">p</span><span class="p">;</span> <span class="n">i</span> <span class="o"><=</span> <span class="n">n</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="n">p</span><span class="p">)</span>
<span class="n">sieve</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">100000000</span><span class="p">;</span>
<span class="kt">double</span> <span class="n">average</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="mi">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">clock_t</span> <span class="n">start</span> <span class="o">=</span> <span class="n">clock</span><span class="p">();</span>
<span class="n">sieve_eratosthenes</span><span class="p">(</span><span class="n">n</span><span class="p">);</span>
<span class="kt">clock_t</span> <span class="n">end</span> <span class="o">=</span> <span class="n">clock</span><span class="p">();</span>
<span class="n">average</span> <span class="o">+=</span> <span class="p">(</span><span class="kt">double</span><span class="p">)</span> <span class="p">(</span><span class="n">end</span> <span class="o">-</span> <span class="n">start</span><span class="p">)</span> <span class="o">/</span> <span class="n">CLOCKS_PER_SEC</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">printf</span><span class="p">(</span><span class="s">"%f</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">average</span> <span class="o">/</span> <span class="mi">100</span><span class="p">.);</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="python-구현">Python 구현</h2>
<p>Python의 경우, CPython보다 PyPy 구현이 훨씬 빠를 것으로 예상된다. PyPy로는 평균적으로 2.200 786초가 걸렸고, 기본적인 CPython으로는 무려 22.031 530초가 걸렸다. CPython은 code를 수정하여 1회만 측정하였다. PyPy의 경우 최적화하지 않은 C 언어보다 15% 가량, 최적화한 C 언어에 비해서는 40% 정도 느린 결과이다. CPython은 수행하는데 PyPy보다 10배 정도 오랜 시간이 걸렸다. 아래 표에 결과가 정리되어 있다.</p>
<table>
<thead>
<tr>
<th style="text-align: center">구현</th>
<th style="text-align: center">평균 실행 시간 [초]</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">CPython</td>
<td style="text-align: center">22.031 530</td>
</tr>
<tr>
<td style="text-align: center">PyPy</td>
<td style="text-align: center">2.200 786</td>
</tr>
</tbody>
</table>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">time</span>
<span class="k">def</span> <span class="nf">sieve_eratosthenes</span><span class="p">(</span><span class="n">n</span><span class="p">):</span>
<span class="n">sieve</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">n</span>
<span class="n">sieve</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">):</span>
<span class="k">if</span> <span class="n">sieve</span><span class="p">[</span><span class="n">p</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]:</span>
<span class="c1"># print p
</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="n">p</span> <span class="o">**</span> <span class="mi">2</span><span class="p">,</span> <span class="n">n</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">p</span><span class="p">):</span>
<span class="n">sieve</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">n</span> <span class="o">=</span> <span class="mi">100000000</span>
<span class="n">average</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">xrange</span><span class="p">(</span><span class="mi">100</span><span class="p">):</span>
<span class="n">start</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">sieve_eratosthenes</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
<span class="n">end</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="n">average</span> <span class="o">+=</span> <span class="n">end</span> <span class="o">-</span> <span class="n">start</span>
<span class="k">print</span> <span class="n">average</span> <span class="o">/</span> <span class="mf">100.</span>
</code></pre></div></div>
<h2 id="swift-구현">Swift 구현</h2>
<p>Swift로는 1.975 862초가 걸렸다. 최적화하지 않은 C 언어와 비슷한 수준의 수행 시간이 걸렸으며, 최적화한 C 언어에 비해서도 30% 정도밖에 느리지 않다.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">QuartzCore</span>
<span class="kd">func</span> <span class="nf">sieveEratosthenes</span><span class="p">(</span><span class="n">_</span> <span class="nv">n</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">sieve</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="nv">repeating</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">count</span><span class="p">:</span> <span class="n">n</span><span class="p">)</span>
<span class="n">sieve</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">p</span> <span class="k">in</span> <span class="mi">2</span><span class="o">...</span><span class="n">n</span> <span class="p">{</span>
<span class="k">if</span> <span class="n">sieve</span><span class="p">[</span><span class="n">p</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span> <span class="p">{</span>
<span class="c1">// print(p)</span>
<span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="nf">stride</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">p</span> <span class="o">*</span> <span class="n">p</span><span class="p">,</span> <span class="nv">through</span><span class="p">:</span> <span class="n">n</span><span class="p">,</span> <span class="nv">by</span><span class="p">:</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
<span class="n">sieve</span><span class="p">[</span><span class="n">i</span> <span class="o">-</span> <span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">let</span> <span class="nv">n</span> <span class="o">=</span> <span class="mi">100000000</span>
<span class="k">var</span> <span class="nv">average</span> <span class="o">=</span> <span class="mf">0.0</span>
<span class="k">for</span> <span class="n">_</span> <span class="k">in</span> <span class="mi">1</span><span class="o">...</span><span class="mi">100</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">start</span> <span class="o">=</span> <span class="kt">CACurrentMediaTime</span><span class="p">()</span>
<span class="nf">sieveEratosthenes</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">end</span> <span class="o">=</span> <span class="kt">CACurrentMediaTime</span><span class="p">()</span>
<span class="n">average</span> <span class="o">+=</span> <span class="n">end</span> <span class="o">-</span> <span class="n">start</span>
<span class="p">}</span>
<span class="nf">print</span><span class="p">(</span><span class="n">average</span> <span class="o">/</span> <span class="mf">100.0</span><span class="p">)</span>
</code></pre></div></div>Zetajaeho.lee@snu.ac.kr에라토스테네스의 체를 통해 C언어, Python 2의 CPython 2 구현 및 PyPy2 구현, Swift 4의 실행 속도를 비교해본다.GitHub Pages와 Jekyll로 블로그 이전하기2018-08-24T00:00:00+09:002018-08-24T00:00:00+09:00https://zetablog.io/posts/github-pages-migration<p>어렸을 때부터 지금까지 블로그를 다양한 곳에서 운영했었다. 처음에는 네이버에서, 이후에는 <a href="http://zetablog.tistory.com">http://zetablog.tistory.com</a>의 티스토리 블로그로 다양한 글을 연재했다. 물론 지금도 대부분의 글은 티스토리에 남아있고, 틈틈히 하나씩 여기로 이전할 생각이다. 이전에 Moon 테마(GitHub 저장소: <a href="https://github.com/TaylanTatli/Moon">https://github.com/TaylanTatli/Moon</a>, 블로그 적용 예시: <a href="https://taylantatli.github.io/Moon/">https://taylantatli.github.io/Moon/</a>)를 사용한 Jekyll 블로그를 방치해두다가, 이번에 새로 블로그를 시작하려고 Chalk 테마(GitHub 저장소: <a href="https://github.com/nielsenramon/chalk">https://github.com/nielsenramon/chalk</a>, 블로그 적용 예시: <a href="http://chalk.nielsenramon.com">http://chalk.nielsenramon.com</a>)를 사용해보기로 했다. 그 과정에 있었던 삽질을 여기에 기록하도록 한다.</p>
<p>사실, GitHub Pages로 블로그를 시작하는 것이 그리 어려운 작업은 아니지만, 내가 사용하기로 한 Chalk 테마는 GitHub Pages에서 공식으로 지원하지 않는 패키지를 사용해 이런 부가적인 과정이 필요하다고 한다. Chalk 테마를 사용하시려는 방문객이시라면 이 포스트가 부디 도움이 되길 바란다.</p>
<h1 id="github-저장소-설정">GitHub 저장소 설정</h1>
<p>GitHub Pages로 블로그를 만들기 위해서는 블로그를 만들 저장소를 만들어야 한다. 일반적으로는 계정명.github.io(내 경우 Zeta611.github.io)로 만들면 되는데, 나는 블로그 URL인 zetablog.ml로 저장소를 만들고 싶었다. 또한, 전자의 경우에는 블로그를 master branch에서만 만들 수 있다. 우리가 사용한 Chalk 테마는 master branch에 직접 블로그를 올릴 수 없기 때문에, 일이 복잡해진다. Chalk 테마의 경우, local에서 특정 branch(나의 경우 master branch)에서 블로그를 만든 후, 해당 경로에서</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run publish
</code></pre></div></div>
<p>를 실행하면 자동으로 gh-pages branch를 만들어 블로그를 만든 후 다시 원래 branch로 checkout한다. 이 때, remote의 origin/gh-pages로 직접 push까지 해준다. 즉, 우리가 만들 Chalk 테마는 origin/gh-pages로 블로그를 만들어주기 때문에, origin/master의 내용만을 블로그로 만들 수 있는 계정명.github.io로 저장소 이름을 설정하면 번거로울 수 있는 것이다. 저장소를 다른 이름으로 설정한다면 gh-pages를 포함해 원하는 branch에서 블로그를 publish할 수 있다. GitHub 저장소의 Settings tab으로 가면 아래의 GitHub Pages 설정을 볼 수 있는데, 여기의 Source에서 gh-pages branch를 선택하면 된다. 계정명.github.io의 경우에는 master branch밖에 선택이 불가하다.</p>
<p><img src="/assets/images/2018-08-24-1/gh-pages source.png" alt="Figure 1" class="shadow" /></p>
<p>사실 <a href="https://github.com/nielsenramon/chalk">https://github.com/nielsenramon/chalk</a>에서 시키는대로 하면 다 된다! 그런데 이 테마에서는 기본적으로 \(\LaTeX\) 지원이 되지를 않아, 이를 설정해주기 위해…</p>
<h1 id="latex-수식-입력-지원하기">LaTeX 수식 입력 지원하기</h1>
<h2 id="시행착오-1">시행착오 1</h2>
<p>여러 시행착오를 겪게 되었다. 해결법만이 필요하시다면, 바로 시행착오 2로 건너뛰시길 바란다. (만약 jekyll-latex을 사용하고 싶으시다면, 아래에 필자가 겪은 내용을 참고하셔도 좋다. 하지만 필자는 실패하였으니 혹시 해결법을 아신다면 댓글로 해결하신 방법을 부탁한다.)</p>
<p>모든 포스트의 확장자를 markdown(.md)에서 \(\LaTeX\)(.tex)으로 바꾼 후</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>jekyll-latex
</code></pre></div></div>
<p>으로 jekyll-latex를 설치하고, Gemfile에도 추가하였는데 블로그에 변함이 없다. 이후 _config.yml에도 추가해봤지만, 여전히 변함이 없다. 이후 만지작거리다가 Dependency Error가 생겨, Google에 검색을 하니 <a href="https://stackoverflow.com/questions/35401566/dont-have-jekyll-paginate-or-one-of-its-dependencies-installed">https://stackoverflow.com/questions/35401566/dont-have-jekyll-paginate-or-one-of-its-dependencies-installed</a>의 내용을 발견했다. 나도 마찬가지로 두 버전의 jekyll이 설치돼 있어서 최신 버전만 남겨 두었다. 그 과정에서 오타가 생겼는지,</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[!]</span> There was an error parsing <span class="sb">`</span>Gemfile<span class="sb">`</span>: Undefined <span class="nb">local </span>variable or method <span class="sb">`</span>wsource<span class="s1">' for Gemfile. Bundler cannot continue.
# from /Users/jay/zetablog.ml/Gemfile:1
# -------------------------------------------
> wsource "https://rubygems.org"
#
# -------------------------------------------
</span></code></pre></div></div>
<p>의 error가 생겨 다시 Gemfile을 수정하였다. 처음에는 wsource가 원래 명령어인줄로 알고</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem update <span class="nt">--system</span>
</code></pre></div></div>
<p>를 해보기도 했다. 그런데 저 error를 고치고 나니 새로운 error가 생긴다. 산 넘어 산이다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bundler could not find compatible versions <span class="k">for </span>gem <span class="s2">"kramdown"</span>:
In snapshot <span class="o">(</span>Gemfile.lock<span class="o">)</span>:
kramdown <span class="o">(=</span> 1.17.0<span class="o">)</span>
In Gemfile:
jekyll was resolved to 3.8.3, which depends on
kramdown <span class="o">(</span>~> 1.14<span class="o">)</span>
jekyll-latex was resolved to 1.0.0, which depends on
polytexnic <span class="o">(</span>~> 1.5<span class="o">)</span> was resolved to 1.5.6, which depends on
kramdown <span class="o">(</span>~> 1.14.0<span class="o">)</span>
Running <span class="sb">`</span>bundle update<span class="sb">`</span> will rebuild your snapshot from scratch, using only
the gems <span class="k">in </span>your Gemfile, which may resolve the conflict.
npm ERR! code ELIFECYCLE
npm ERR! errno 6
npm ERR! chalk@ publish: <span class="sb">`</span>bin/deploy<span class="sb">`</span>
npm ERR! Exit status 6
npm ERR!
npm ERR! Failed at the chalk@ publish script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
</code></pre></div></div>
<p>시키는대로 bundle update를 해본 후, npm run publish를 하지만 블로그는 그대로이다. 앗차, bundle을 다시 안했다. 그러고 나니, \(\LaTeX\) 원문 그대로 블로그에 올라간다. 그래서 Gemfile이랑 _config.yml의 jekyll-latex을 지웠다. 아래의 시행착오 2를 시도한다.</p>
<h2 id="시행착오-2">시행착오 2</h2>
<p><a href="https://helloworldpark.github.io/jekyll/update/2016/12/18/Github-and-Latex.html">https://helloworldpark.github.io/jekyll/update/2016/12/18/Github-and-Latex.html</a>를 참고하였다.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">async</span>
<span class="na">src=</span><span class="s">"https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"</span><span class="nt">></span>
<span class="nt"></script></span>
</code></pre></div></div>
<p>를 _layouts/post.html의 <article> 태그 다음에 넣어주었다. 다시</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run publish
</code></pre></div></div>
<p>을 하니 error message는 뜨지 않는다. 이렇게 문제가 바로 해결되는가 싶더니… 해결되었다! 결국 답은 가까운 MathJax에 있었다.</p>
<h1 id="cname으로-custom-domain-지정하기">CNAME으로 Custom Domain 지정하기</h1>
<p>그런데, 매번 블로그의 custom domain이 재설정되는 문제가 있었다. 위의 시행착오를 시도할 때마다 매번 GitHub 저장소의 Settings tab에 가서 custom domain을 zetablog.ml로 설정하는 것은 여간 귀찮은 일이 아니었다. 나아가 새로운 포스트를 옮길 때마다 이 작업을 반복하는 것은 말도 안되는 고역이지 않는가.</p>
<p>간단한 Googling을 통해 <a href="https://github.community/t5/GitHub-Pages/Github-forgets-about-my-custom-domain-every-time-I-update-the/td-p/470">https://github.community/t5/GitHub-Pages/Github-forgets-about-my-custom-domain-every-time-I-update-the/td-p/470</a>를 찾았다.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run publish
</code></pre></div></div>
<p>을 할 때마다 매번 CNAME이 덮어씌워진다는 것 같다. 잘 모르겠는데, 일단 _config.yml의 URL에 zetablog.ml을 추가해본다. 역시나 문제는 그대로다.</p>
<p>master branch의 root directory의 CNAME이 빈 file인데, 여기에 zetablog.ml을 추가하니, 문제가 해결되었다.</p>
<h1 id="마지막으로">마지막으로</h1>
<p>마지막으로, tag별로 모아보는 기능이 작동되지 않아 _my_tags의 markdown file들의 제목과 내용을 모두 tag 이름으로 바꿔서 넣어주면 된다. Tag당 markdown file 하나다.</p>
<p>Disqus 댓글 지원은 <a href="https://xho95.github.io/blog/jekyll/disqus/migration/2017/01/20/Add-Disqus-to-Jekyll.html">https://xho95.github.io/blog/jekyll/disqus/migration/2017/01/20/Add-Disqus-to-Jekyll.html</a>을 참고했다.</p>Zetajaeho.lee@snu.ac.kr블로그를 GitHub Pages와 Jekyll로 이전하면서 있었던 삽질을 기록한다.i의 i승? 복소수와 복소지수의 다가성2018-08-24T00:00:00+09:002018-08-24T00:00:00+09:00https://zetablog.io/posts/i-power-i<h1 id="복소수의-도입">복소수의 도입</h1>
<h2 id="허수의-실재">허수의 실재</h2>
<p>사람들은 \(x^2 = -1\)의 해를 비롯해, 실수로 나타낼 수 없는 이차방정식의 근을 나타내기 위해 허수를 도입했다. 영어로는 imaginary number인 <strong>허수</strong>는 실재하지 않는다는 의미를 가지고 있다. 제곱을 하여 -1이 되는 수가 어디 있겠냐마는, 나는 허수라는 이름이 굉장히 naive하게 지어졌다고 생각한다. ‘수’라는 개념 자체가 애초에 추상적이라는 사실을 떠올려보자. 거듭제곱해서 -1이 되는 \(i\)만큼이나 -1 자신, 그리고 0이나 무리수도 충분히 추상적인 개념이다. 파스칼을 비롯한 여러 수학자들도 음수를 받아들이는데 어려움을 겪었다고 한다. 허수까지 갈 것 없이, 실수 체계도 우리가 일상생활에서 사용하는 숫자—개수 세기—와 충분히 괴리가 크다.</p>
\[x + 1 = 0\]
<p>의 해도 음수를 고안하기 전까지는 해가 없다고 취급을 하였다. 지금의 입장에서는 부자연스럽지만, 저 방정식의 해를 허수라고 불러도 문제가 없을 것이다.</p>
<p>역사적으로, 수학자들은 수 체계를 확장할 때 기하적인 접근법을 사용했다. 음수와 무리수 등을 포함한 실수 체계도 수직선과 수를 일대일로 대응시켜 만들어진 산물이라고 볼 수 있다. 이미 복소수가 잘 정의된 지금의 시점에서 본다면, 복소수는 수평면을 수로 나타내기 위한 확장이라고 볼 수 있다. 이 관점으로는 해밀턴의 사원수까지도 수의 자연스러운 형태로 받아들일 수 있다. 여하튼, <strong>허수는 충분히 ‘자연스러운’ 수 체계의 확장이다.</strong> 실제로 양자 역학의 영역으로 들어간다면, 자연에서도 본질적으로 허수가 실재한다고 생각할 수 있다.</p>
<p>각설하고, 제곱해서 -1이 되는 수는 사실 두 개가 있다. 그 중 <strong>임의로</strong> 하나를 \(i\), 다른 하나를 \(-i\)로 잡은 것이다. (왜 두 개가 있느냐 하면, 제곱을 하면 -의 부호가 +가 되기 때문이다. 생각해보면 이 또한 음수의 특별한 성질 중 하나이며, 수학자들이 왜 처음에 음수를 받아들이기 힘들어했는지를 엿볼 수 있는 대목이다.) 따라서 우리는 <strong>허수의 ‘방향성’</strong>을 생각할 수 있다. 나아가, 허수와 실수를 결합한 형태인 <strong>복소수</strong>는 수평면 상의 한 점을 나타내는데 사용할 수 있다. 여기서 복소수는 실수 \(a\)와 허수 \(i\)의 실수 \(b\)배의 합인 \(a + bi\)의 꼴로 나타낼 수 있다.</p>
<h2 id="이차방정식의-해">이차방정식의 해</h2>
<p>위에서 간단히 살펴본 \(x^2 = -1\)뿐만이 아니라, <strong>임의의 복소계수 이차방정식의 해는 복소수로 나타낼 수 있다.</strong> (복소계수란 각 항의 계수가 복소수라는 의미이다. 또한, 실계수는 각 항의 계수가 실수라는 것을 말한다.) 즉, 복소수를 도입하더라도 그 해를 나타내는데 새로운 수 체계가 필요하지 않다. 일단은 실계수 이차방정식</p>
\[ax^2 + bx + c = 0\]
<p>을 보자. 일반적으로, 해 \(x\)는</p>
\[\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\]
<p>로 나타낼 수 있다. 여기서 \(b^2 - 4ac\)가 음수라면 \(x\)를 나타내기 위해 복소수의 도입이 불가피하다는 사실을 알 수 있다. 실계수 이차방정식의 해는 근호 안의 부분이 양수이나 음수이냐에 따라 실수가 될 수도, 복소수가 될 수도 있다. 따라서 이 부분은 해의 형태를 판별하는데에 있어서 중요한 의미를 가지며, 이를 <strong>판별식</strong>이라고 한다. 보통 \(D\)라는 기호로 나타낸다.</p>
<p>예를 들어 방정식 \(x^2+3x+3=0\)의 해는,</p>
\[\begin{align*}
x &= \frac{-3 \pm \sqrt{3^2 - 4\cdot 1 \cdot 3}}{2\cdot 1}\\
&= \frac{-3\pm \sqrt{-3}}{2}\\
&=\frac{-3\pm \sqrt{-1}\sqrt{3}}{2}\\
&=\frac{-3\pm \sqrt{3}i}{2}
\end{align*}\]
<p>으로 방정식에 들어간 모든 계수가 실수임에도 불구하고 그 해는 복소수라는 것을 알 수 있다.</p>
<h1 id="복소평면">복소평면</h1>
<h2 id="데카르트-좌표계와-극좌표계">데카르트 좌표계와 극좌표계</h2>
<p><img src="/assets/images/2018-08-24-2/complex plane.png" alt="Figure 1" class="shadow" /></p>
<p>위 그림에서 평면의 가로축은 실수(real의 앞 두 글자를 딴 \(Re\)), 세로축은 허수(imaginary의 앞 글자를 딴 \(Im\))를 나타낸다. 이렇게 만든 평면과 복소수를 대응시켜 나타낸 것을 <strong>복소평면</strong>(comlex plane)이라고 한다. 여기서 복소수의 <strong>절댓값</strong>(absolute value)과 <strong>편각</strong>(argument)을 도입하면 <strong>오일러(Euler)의 공식</strong>을 이해할 수 있다. 위 그림에서 \(r\)과 \(\varphi\)가 각각 절댓값과 편각에 해당한다. 실수축의 한 지점과 허수축의 한 지점을 정하면 복소수가 하나로 특정할 수 있듯이, 절댓값과 편각으로 복소수를 특정할 수도 있다. 그렇다면, 이 두 값으로 복소수를 어떻게 표기할 수 있을까? 바로 <strong>극좌표</strong>를 통해 나타낼 수 있다.</p>
<p>어떤 공간에서 한 점을 나타내는 방법은 한 가지가 아니며, 이렇게 한 점을 나타낼 수 있는 체계를 <strong>좌표계</strong>라고 한다. 예컨데, 어떤 <strong>직교좌표계</strong>, 혹은 <strong>데카르트 좌표계</strong>(Cartesian coordinate system)에서 (1, 3) 위치의 점을 찍었다고 하자. 2차원이기에 위치를 특정하기 위해서는 두 개의 정보가 필요하다. 그리고 이 둘은 독립적이어야한다. 하지만 이뿐이다! (1, 3)의 지점을 나타내기 위해, \(x\)좌표의 값과 \(y\)좌표의 값이 필요한 데카르트 좌표계가 필수적인 것은 아니다. 원점과의 거리와 가로축과의 각도가 주어저도 동일한 지점을 찍을 수 있을 것이다. 이렇게 점을 나타내는 좌표계를 <strong>극좌표계</strong>(polar coordinate system)라고 한다.</p>
<p>(1, 3)과 원점 (0, 0)까지의 거리는 피타고라스의 정리에 의해 \(\sqrt{1^2+3^2}=\sqrt{10}\)이며, 가로축과 (1, 3)이 이루는 각도는 \(\tan^{-1}{\frac{3}{1}}=\tan{3}\)이다. 따라서, 데카르트 좌표계에서 (1, 3)에 해당했던 지점은 극좌표계에서는 \(\left(\sqrt{10},\ \tan{3}\right)\)으로 나타낼 수 있다. 일반적으로 어떤 점이 데카르트 좌표계에서는 \((x, y)\)로, 극좌표계에서는 \(\left(r, \theta \right)\)으로 나타내어질 때, 이 둘 사이의 변환식은 다음과 같다.</p>
\[\begin{align*}
r &= \sqrt{x^2 + y^2}\\
\theta &= \arctan {\frac yx}\\
x &= r \cos \theta\\
y &= r \sin \theta
\end{align*}\]
<h2 id="오일러의-공식">오일러의 공식</h2>
<p>복소평면에서 어떤 점을 복소수로 나타낼 때, 단순히 절댓값과 편각의 순서쌍으로도 충분히 복소수를 나타낼 수 있다. 하지만, 이 둘의 연산으로 복소수를 ‘계산’할 수 있는데, 그 돌파구가 바로 <strong>오일러의 공식</strong>이다. 오일러의 공식을 유도하는데는 여러 가지 방법이 있는데, 여기서는 <strong>테일러(Taylor) 전개</strong>를 사용한 방법을 보인다. 이 외에도 <a href="http://zetablog.tistory.com/9">Euler (오일러) 공식의 세 가지 느슨한 증명</a>에 세 가지 다른 증명법이 소개되어 있다.</p>
<p>테일러 전개에 따르면,</p>
\[\exp x = 1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!} + \dots \\
\sin x = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \dots \\
\cos x = 1 - \frac{x^2}{2!} + \frac{x^4}{4!} - \frac{x^6}{6!} + \dots\]
<p>을 알 수 있다. 여기서 \(\exp ix\)는 \(i\sin x\)와 \(\cos x\)의 합으로 나타낼 수 있음을 확인할 수 있다. 즉, 아래와 같은 공식이 성립한다.</p>
\[\exp (ix) = e^{ix} = \cos x + i \sin x\]
<p>여기서 \(x\)에 \(\pi\)를 대입한 \(e^{i\pi} = -1\)이 오일러의 공식이다.</p>
<h2 id="i의-i승과-복소지수의-다가성">i의 i승과 복소지수의 다가성</h2>
<p>여기까지의 내용을 따라왔다면 아래의 등식을 이해할 수 있을 것이다.</p>
\[x + iy = re^{i\theta}\]
<p>이 때, \(\sqrt{x^2+y^2}\)이며, \(\tan\theta = \frac{y}{x}\)이다. 이런 형태로 복소수를 나타내는 방식을 <strong>극형식</strong>이라고 부른다. 이제 우리는 어떤 복소수이든 \(re^{i\theta}\)의 꼴로 나타낼 수 있게 되었다. 변환식에 \(i\)를 넣으면 극형식으로 \(1e^{i\cdot \frac{\pi}{2}}\)가 된다. 기하학적으로 보자면 원점과의 거리가 1, 가로축과 이루는 각이 라디안으로 \(\frac{\pi}{2}\)이라는 관찰과 부합한다. 이제 \(i\)의 \(i\)승, 즉 \(i^i\)를 변환식에 넣어보자.</p>
\[\begin{align*}
i^i &= \left( e^{\frac{i\pi}{2}} \right)^i\\
&=e^{\frac{i^2 \pi}{2}}\\
&=e^{-\frac{\pi}{2}}\\
&=0.2078\dots
\end{align*}\]
<p><strong>놀랍게도 허수의 허수승인 \(i^i\)의 값은 다름아닌 실수, 0.2078…가 나왔다.</strong> \((-1) \times (-1) = 1\)을 떠올릴 수 있는 대목이다.</p>
<p>여기서 끝이 아니다! <a href="http://zetablog.tistory.com/10">복소 로그 함수와 복소 지수, 그리고 다가성 (multivaluedness)</a>를 보면 알 수 있겠지만, 복소지수에는 여러 값을 배정할 수 있다. 그 말인즉슨, 0.2078…이 \(i^i\)의 유일한 정답이 아니라는 것이다. 이는 복소지수의 <strong>다가성</strong>(multivaluedness)에 기인한 성질인데, 위의 내용에서 \(i\)의 편각에 \(\frac{\pi}{2}\)가 아니라 \(\frac{5\pi}{2}\)이나 \(\frac{-3\pi}{2}\)를 넣어도 문제될 것이 없다. 정수 \(n\)에 대해서 \(\frac{(4n+1)\pi}{2}\)의 꼴이라면 항상 \(\frac{\pi}{2}\)과 같은 방향을 가리키고 있기 때문이다. 일반적으로 \(i^i\)는 아래와 같이 계산할 수 있다.</p>
\[\begin{align*}
i^i &= \left( e^{\frac{i(4n+1)\pi}{2}} \right)^i\\
&= e^{\frac{i^2 (4n+1)\pi}{2}}\\
&= e^{-\frac{(4n+1)\pi}{2}}\\
&= \left(\frac{0.2078\dots}{535.49\dots}\right) \frac 1n
\end{align*}\]
<p>이번 포스트에서는 \(i\)의 \(i\)승에 대한 해답을 찾는 것에 포커스를 맞추어 복소수에 대해서 알아보았다. 이와 관련된 내용에 대해 더 알고 싶다면, <a href="https://en.wikipedia.org/wiki/Complex_logarithm">https://en.wikipedia.org/wiki/Complex_logarithm</a>를 참고해보자.</p>Zetajaeho.lee@snu.ac.kr복소수에 대해 알아본 후, 지수의 정의역을 복소수로 확장해 i의 i승의 값을 구해본다.브라키스토크론 문제: 최단시간강하곡선 사이클로이드와 오일러-라그랑주 방정식2017-07-07T00:00:00+09:002017-07-07T00:00:00+09:00https://zetablog.io/posts/brachistochrone-euler-lagrange-equation<h2 id="개요">개요</h2>
<p>브라키스토크론(brachistochrone), 혹은 최단시간강하곡선 문제는 역사적으로 의미가 깊은 문제이다.
최단시간강하곡선은 어떤 물체가 한 점에서 다른 한 점까지 마찰 없이 미끄러져 내려갈 때 걸리는 시간을 가장 짧게 만드는 경로로, 놀랍게도 사이클로이드 곡선이 바로 최단시간강하곡선이다.
이 포스트에서는 다루지 않지만, 이 곡선 상에서는 어느 지점에 물체를 올려놓든지와 무관하게, 가장 낮은 지점까지 내려가는데 걸리는 시간이 모두 같다는 흥미로운 성질도 가지고 있다.
이 성질에 주목학 최단시간강하곡선, 혹은 사이클로이드 곡선의 다른 이름은 등시강하곡선(tautochrone)이다.</p>
<p>이 문제의 기원은 300여년을 거슬러 올라가 뉴턴, 베르누이, 로피탈 등의 수학자들의 시대에 있다.
사실 이 문제를 처음 생각해낸 것은 갈릴레오였을 것으로 생각되는데, 그는 1938년에 해당 문제에 대한 답이 원 호라고 결론을 지었다.
물론 이는 잘못된 답이다.
1969년, 요한 베르누이는 <em>Acta Eruditorum</em>에 최단시간강하곡선 문제를 내어 다른 수학자들이 도전하게 하였다.
그리하여 라이프니츠는 요한 베르누이에게 외국에 있는 수학자들도 이 문제를 풀 수 있도록 6개월보다 더 많은 시간을 달라고 요청하였다.
결국 다섯 명의 수학자로부터 다섯 개의 풀이가 나왔는데, 문제를 해결한 사람들은 뉴턴, 야콥 베르누이, 라이프니츠, 로피탈, 그리고 출제자 본인인 요한 베르누이였다.</p>
<p>이번 포스트에서는 이들이 풀어낸 방법과는 다르게 최단시간강하곡선 문제에 대한 답을 내고자 한다.
이를 위해서 오일러-라그랑주 방정식을 통해 미분방정식을 구한 후,—사이클로이드에 해당하는—곡선의 방정식을 유도할 것이다.</p>
<h2 id="식-세우기">식 세우기</h2>
<p><img src="/assets/images/2017-07-07-2/fig1.png" alt="Figure 1" class="shadow" /></p>
<p>연직 아래 방향, 즉 중력 방향으로 \(x\)-축을 설정한다.
곡선의 선소를 \(\mathrm{d} s\)라고 하면, 피타고라스의 정리에 의해 다음이 성립한다:</p>
\[\mathrm{d} s = \sqrt{1 + \left( \frac{\mathrm{d} y}{\mathrm{d} x} \right)^2} \mathrm{d} x = \sqrt{1 + (y')^2} \mathrm{d} x\]
<p>이 때, 미끄러져 내려오는 물체의 질량을 \(m\), 속력을 \(v\), 중력가속도를 \(g\)라고 두자.
그렇다면</p>
\[v = \frac{\mathrm d s}{\mathrm d t}\]
<p>이고, 역학적 에너지 보존 법칙에 의해</p>
\[\frac 12 m v^2 = mgx\]
<p>임을 알 수 있다.
이를 \(v\)에 대해 정리하면</p>
\[v = \frac{\mathrm d s}{\mathrm d t} = \sqrt{2gx}\]
<p>이 된다.
따라서,</p>
\[\mathrm d t = \frac{\mathrm d s}{\sqrt{2gx}}\]
<p>이다.
이제 곡선을 따라 \(\mathrm A\)에서 \(\mathrm B\)까지 내려오는데 걸리는 시간을 \(T = \int_\mathrm{A}^\mathrm{B} \mathrm d t\)라고 하자.
위 \(\mathrm d t\)를 대입하면,</p>
\[T = \int_0^{x_1} \sqrt{\frac{1 + (y')^2}{2gx}} \mathrm d x\\
\qquad = \frac{1}{2g} \int_0^{x_1} \sqrt{\frac{1 + (y')^2}{x}} \mathrm d x\]
<p>이 된다.</p>
<h2 id="오일러-라그랑주-방정식의-적용">오일러-라그랑주 방정식의 적용</h2>
<p>다음과 같이 함수 \(f\)를 정의하자:</p>
\[f(x, y, y') = \sqrt{\frac{1 + (y')^2}{x}}\]
<p>오일러-라그랑주 방정식은 다음과 같다:</p>
\[\frac{\partial f}{\partial y} - \frac{\mathrm d}{\mathrm d x} \left(\frac{\partial f}{\partial y'}\right) = 0\]
<p>일단 \(\frac{\partial f}{\partial y}\) 항은,</p>
\[\frac{\partial f}{\partial y} = \frac{\partial}{\partial y} \left(\sqrt{\frac{1 + (y')^2}{x}}\right) = 0\]
<p>이고, \(\frac{\partial f}{\partial y'}\) 항은,</p>
\[\frac{\partial f}{\partial y'} = \frac 12 \sqrt{\frac{x}{1 + (y')^2}} \cdot \frac{x \cdot 2y' - 0}{x^2} = \frac{y'}{\sqrt{x \left( 1 + (y')^2 \right)}}\]
<p>이다.
따라서, 오일러-라그랑주 방정식은 다음과 같다:</p>
\[\frac{\partial f}{\partial y} - \frac{\mathrm d}{\mathrm d x} \left(\frac{\partial f}{\partial y'}\right) = - \frac{\mathrm d}{\mathrm d x} \left(\frac{y'}{\sqrt{x \left( 1 + (y')^2 \right)}}\right) = 0\]
<h2 id="미분방정식-풀이">미분방정식 풀이</h2>
\[\frac{\mathrm d}{\mathrm d x} \left(\frac{y'}{\sqrt{x \left( 1 + (y')^2 \right)}}\right) = 0\]
<p>이므로, \(C\)가 상수일 때</p>
\[\frac{y'}{\sqrt{x \left( 1 + (y')^2 \right)}} = C\]
<p>이다.
양변을 제곱하여 좌변의 분모를 우변으로 이항시키면,</p>
\[(y')^2 = C^2 x \left(1 + (y')^2 \right) = C^2 x + C^2 x (y')^2\]
<p>이 되고, \(y'\)에 대해 정리하면,</p>
\[y' = \pm \sqrt{\frac{C^2 x}{1 - C^2 x}}\]
<p>이다.
\(\frac{1}{C^2} = a\)로 치환하자.
그러면</p>
\[y' = \pm \sqrt{\frac{x}{a - x}}\]
<p>가 되어</p>
\[\mathrm d y = \pm \sqrt{\frac{x}{a - x}} \mathrm d x\]
<p>이다.
\(x = a \sin^2 \frac \theta 2 = \frac a 2 (1 - \cos \theta)\)로 변수 치환을 하자.</p>
\[\mathrm d x = a \sin \frac \theta 2 \cos \frac \theta 2 \mathrm d \theta\]
<p>이므로,</p>
\[\qquad \mathrm d y = \pm \sqrt{\frac{a \sin^2 \frac \theta 2}{a - a \sin^2 \frac \theta 2}} \cdot a \sin \frac \theta 2 \cos \frac \theta 2 \mathrm d \theta\\
\quad = \pm \sqrt{\frac{\sin^2 \frac \theta 2}{\cos^2 \frac \theta 2}} \cdot a \sin \frac \theta 2 \cos \frac \theta 2 \mathrm d \theta\\
= \pm a \sin^2 \frac \theta 2 \mathrm d \theta \qquad \qquad\ \ \ \,\,\\
= \pm \frac a 2 (1 - \cos \theta) \mathrm d \theta \qquad \quad \\]
<p>이다.
양변을 적분하면,</p>
\[y = \pm \frac a 2 (\theta - \sin \theta) + D\]
<p>이 되는데 (\(D\)는 적분 상수), \(x = 0\), 즉 \(\theta = 0\)일 때 \(y = 0\)이므로 \(D = 0\)이다.
그리고 제 1사분면에 점 \(\mathrm B\)가 있기 때문에 부호는 \(+\)를 고른다.
결국</p>
\[\begin{cases}
x = \frac a 2 (1 - \cos \theta)\\
y = \frac a 2 (\theta - \sin \theta)
\end{cases}\]
<p>으로, 사이클로이드 곡선의 방정식이 나왔음을 알 수 있다.</p>Zetajaeho.lee@snu.ac.kr브라키스토크론(brachistochrone), 혹은 최단시간강하곡선을 오일러-라그랑주 방정식을 통해 유도하여, 이것이 사이클로이드와 일치함을 보인다.Fermat 점 (페르마 점)의 위치와 그 성질에 대한 두 가지 증명2017-07-07T00:00:00+09:002017-07-07T00:00:00+09:00https://zetablog.io/posts/fermat-point-proofs<p>Fermat 점 (페르마 점)은 Euclid 평면 (유클리드 평면) \(\mathbb E\)에서 주어진 세 점이 있을 때, 그 세 점들까지의 거리의 합이 최소인 점을 말한다.
본 포스트에서는</p>
<blockquote>
<p>Fermat 점에서 세 꼭짓점까지 이루는 직선들은 서로 120도를 이룬다</p>
</blockquote>
<p>는 명제를 조건부 극값 문제를 해결하는 해석적인 증명과 평면 기하적인 증명, 두 가지로 보인다.</p>
<h2 id="해석적인-증명">해석적인 증명</h2>
<p>본 증명은 Lagrange 승수법을 통해 조건부 극값 문제를 해결하여 주어진 명제를 증명한다.
가장 단순하면서도 계산이 복잡한 방식이다.</p>
<p>삼각형 \(\mathrm{ABC}\)와 그 내부의 점 \(\mathrm K\)를 잡자.
이 때,</p>
\[\mathrm{AB} = c, \mathrm{BC} = a, \mathrm{CA} = b,\\
\mathrm{AP} = x, \mathrm{BP} = y, \mathrm{CP} = z,\\
\angle \mathrm{APB} = \gamma = 2\pi - \alpha - \beta, \angle \mathrm{BPC} = \alpha, \angle \mathrm{CPA} = \beta\]
<p>으로 설정하고, 함수 \(f: \mathbb{E} \rightarrow \mathbb{R}\)는 \(f(P) = x + y + z\)로 정의하자.
Fermat 점을 찾는 문제가 \(f\)의 극값을 구하는 문제로 바뀌었다.
단, 주어진 세 점 \(\mathrm{A, B, C}\)는 삼각형을 이루므로 다음의 세 조건을 만족한다.</p>
\[x^2 + y^2 - 2xy \cos (2\pi - \alpha - \beta) = c^2,\\
y^2 + z^2 - 2yz \cos \alpha = a^2,\\
z^2 + x^2 - 2zx \cos \beta = b^2\]
<p>이제 다음과 같이 \(\mathbb E\)에서 \(\mathbb R\)로 가는 세 함수를 정의할 수 있다:</p>
\[g_1 (P) = x^2 + y^2 - 2xy \cos (\alpha + \beta) - c^2,\\
g_2 (P) = y^2 + z^2 - 2yz \cos \alpha - a^2,\\
g_3 (P) = z^2 + x^2 - 2zx \cos \beta - b^2\]
<p>Lagrange 승수법을 사용하기 위해 다음과 같은 새로운 함수 \(\tilde f: \mathbb{E} \rightarrow \mathbb{R}\)를 정의하자:</p>
\[\tilde f(P) = f(P) - \sum_{i = 1}^3 \lambda_i g_i (P)\]
<p>이제 좀 골치 아픈 작업이 남았다—\(\tilde f\)의 극값을 구하기 위해 \(x, y, z, \alpha, \beta\)에 대해서 식을 편미분하자.</p>
\[(1)\ \frac{\partial \tilde f}{\partial x} = 1 - 2 \lambda_1 x + 2 \lambda_1 y \cos (\alpha + \beta) - 2 \lambda_3 x + 2 \lambda_3 z \cos \beta = 0\\
(2)\ \frac{\partial \tilde f}{\partial y} = 1 - 2 \lambda_1 y + 2 \lambda_1 x \cos (\alpha + \beta) - 2 \lambda_2 y + 2 \lambda_2 z \cos \alpha = 0\\
(3)\ \frac{\partial \tilde f}{\partial z} = 1 - 2 \lambda_2 z + 2 \lambda_2 y \cos \alpha - 2 \lambda_3 z + 2 \lambda_3 x \cos \beta = 0\\
(4)\ \frac{\partial \tilde f}{\partial \alpha} = 2 \lambda_1 xy \sin (\alpha + \beta) + 2 \lambda_2 yz \sin \alpha = 0\\
(5)\ \frac{\partial \tilde f}{\partial \beta} = 2 \lambda_1 xy \sin (\alpha + \beta) + 2 \lambda_3 zx \sin \beta = 0\]
<p>항 하나가 완전히 일치하는 (4)와 (5)를 연립하면 \(\frac{\lambda_2 \sin \alpha}{\lambda_3 \sin \beta} = \frac x y\)이 된다.
이 때 \(x = k \lambda_2 \sin \alpha\)라고 두고 다시 (4)나 (5)에 대입하면, \(x, y, z\) 모두 \(k, \alpha, \beta\)만으로 표현할 수 있게 된다.</p>
\[x = k \lambda_2 \sin \alpha,\\
y = k \lambda_3 \sin \beta,\\
z = k \lambda_1 \sin (\alpha _ \beta)\]
<p>이렇게 다시 쓴 \(x, y, z\)를 (1)–(3)에 대입하여 식을 정리하고 \(\sin (\alpha + \beta) = \sin \alpha \cos \beta + \cos \alpha \sin \beta\)를 대입하면,</p>
\[\sin \alpha = X,\\
\sin \beta = X,\\
\sin (\alpha + \beta) = X\]
<p>가 나온다.
이 때, \(X\)는 식을 정리하면서 나온 어떤 값이다.
결국 \(\alpha = \beta = 120^\circ\)를 알 수 있다.</p>
<p>그런데, 이 증명에는 흠이 있는데, Lagrange 승수법의 한계상, 이미 \(f\)는 극값을 가진다는 것이 전제된다.
즉, 필요 조건일 뿐이지 충분 조건이 되지 않는다.
예컨데 삼각형의 한 내각이 120돌르 넘는다면, Fermat 점에서 각 꼭짓점들을 이은 직선들은 서로 이루는 각이 120도가 될 수 없다.</p>
<p>이에 우리는 이 문제를 해결할 수 있는 기하적인 증명을 제시한다.</p>
<h2 id="기하적인-증명">기하적인 증명</h2>
<p>삼각형 \(\mathrm{ABC}\)와 그 내부의 한 점 \(F'\)를 잡자.</p>
<p><img src="/assets/images/2017-07-07-1/fig1.png" alt="Figure 1" class="shadow" /></p>
<p>이 \(\triangle \mathrm{ABC}\)를 점 \(\mathrm B\)에 대해 반시계 방향으로 60도 회전한다.
이렇게 회전된 삼각형과 점을 각각 \(\triangle \mathrm{A'BC}, F''\)으로 두자.</p>
<p><img src="/assets/images/2017-07-07-1/fig2.png" alt="Figure 2" class="shadow" /></p>
<p>그렇다면 \(\mathrm{AF' + BF' + CF' = A'F'' + F''F' + F'C}\)이며, 이의 최솟값은 \(\mathrm{A'C}\)가 된다.
(Euclid 평면에서 최단거리는 직선이기 때문이다.)
\(\mathrm{A'B = BA = AA'}\)이므로 \(\triangle \mathrm{AA'B}\)는 정삼각형이다.
즉, \(\mathrm A'\)은 \(\triangle \mathrm{ABC}\) 외부에 \(\mathrm{AB}\)와 정삼각형을 이루도록 잡은 점이다.</p>
<p><img src="/assets/images/2017-07-07-1/fig3.png" alt="Figure 3" class="shadow" /></p>
<p>마찬가지로, \(\triangle \mathrm{BB'C}\)가 정삼각형이 되도록 \(\mathrm B'\)을 \(\triangle \mathrm{ABC}\) 밖에 잡을 수 있다.
따라서 \(\mathrm{AF' + BF' + CF'}\)이 최소인 \(\mathrm F'\)은 \(\mathrm{CA'}\)과 \(\mathrm{AB'}\)의 교점에 위치할 때이며, 이를 \(\mathrm F\)라고 하자.</p>
<p>또한, \(\mathrm{A'A = BA, \angle A'AC = \angle BAC', AC = AC'}\)이므로 \(\triangle \mathrm{A'AC} \equiv \triangle \mathrm{BAC}\ (SAS)\)이다.
결국 \(\angle \mathrm{ABF} = \angle \mathrm{AA'F}\)이어서 \(\mathrm{A, A', B, F}\)는 concyclic, 즉 한 원 위에 있다.
따라서 \(\mathrm{\angle AFB = 180^\circ - \angle AA'B = 120^\circ}\)이다.</p>
<p>그런데, 위 증명에서는 삼각형의 내각이 모두 120도 이하일 경우만을 고려한 것이다.
왜인지 알겠는가?
\(\mathrm{A, A', B, F}\)가 한 원 위에 있다고 하였을 때, \(\angle \mathrm{BAC}\)가 120도를 넘었더라면 네 점의 외접원 내부에 \(\mathrm A\)가 들어와 문제가 되기 때문이다.
이런 경우를 따로 고려해줘야한다.</p>
<p>간단히 개요만 설명한다.
\(\angle \mathrm{BAC}\)가 120도를 넘는다고 하자.
그렇다면 삼각형 내부의 어떤 점이든 각 꼭짓점까지의 거리 합이 \(\mathrm{AB + AC}\)보다 작은 점이 없다는 것은 위 증명을 조금 변형하여 알 수 있다.
이제 아래와 같은 lemma를 설정하자.</p>
<blockquote>
<p>삼각형 외부의 어떤 점이든, 각 꼭짓점까지의 거리 합은 그 삼각형 둘레에 존재하는 점에서 각 꼭짓점까지의 거리의 합보다 작다.</p>
</blockquote>
<p>삼각형의 각 변을 직선으로 연장하여 평면을 총 6구역으로 나눌 수 있는데, 각 구역에 존재하는 점에서 각 꼭짓점까지의 거리 합보다 작은 삼각형 둘레 상의 점을 쉽게 찾을 수 있다.
일반성을 잃지 않고 삼각형의 꼭짓점이 그 무한 영역의 끝이 아닌 경우와 삼각형의 변이 무한 영역의 경계인 경우 두 구역에 대해서만 확인해보면 된다.
이렇게 lemma를 증명하면, \(\mathrm A\)가 Fermat 점이 됨을 알 수 있다.</p>Zetajaeho.lee@snu.ac.krFermat 점 (페르마 점)은 Euclid 평면 (유클리드 평면)에서 주어진 세 점이 있을 때, 그 세점들까지의 거리의 합이 최소인 점이다. Fermat 점에서 세 꼭짓점까지 이루는 직선들은 서로 120도를 이룬다는 것을 해석적인 증명과 기하적인 증명, 두 가지로 보인다.우주를 보는 서로 다른 두 관점—소설 네 인생의 이야기 속 물리학2017-07-03T00:00:00+09:002017-07-03T00:00:00+09:00https://zetablog.io/posts/story-of-your-life<p>연초에 개봉한 영화 《컨택트》 (원제: Arrival)는 영화관을 나오고도 한동안 그 내용을 머릿속에서 곱씹어보게될 정도로 신선한 충격을 주었다.
《컨택트》는 외계인과의 조우를 다룬 전형적인 할리우드식 블록버스터이기를 거부한다.
반면 《인디펜던스 데이》나 《우주 전쟁》과 같은 영화를 기대했다면 조금은 실망스러울 수도 있겠다.
이들 영화가 인류에 적대적인 외계인의 침공을 다루었다면, 《컨택트》는 우주에 대한 서로 다른 두 관점을 지닌 외계인과의 소통을 주제로 삼고 있다.</p>
<p>사실 이 영화는 필자가 하드 SF 소설에 흥미를 가지게 되는 가장 큰 계기가 되었는데, 영화를 보고 바로 얼마 후 인터넷 서점에서 원작 소설인 〈네 인생의 이야기〉 (원제: Story of Your Life)를 담고 있는 중단편집 〈당신 인생의 이야기〉(원제: Stories of Your Life and Others)를 발견하게 된 것이다.
특히 작가 테드 창은 정말 독특한 이력을 가져, 잠깐 소개하지 않을 수 없다.
브라운 대학교에서 물리학과 컴퓨터 공학을 전공한 그는 1990년에 단편 〈바빌론의 탑〉으로 등단을 하는데, 이 작품으로 그는 저명한 SF 문학상인 네뷸러상을 최연소로 수상하게 된다.
이후 그는 발표하는 작품마다 각종 상을 휩쓸며 주목을 받고, ‘전 세계 과학소설계의 보물’이라는 찬사를 받게 된다.
하지만 가장 특이한 점은, 이런 그가 발표한 작품은 지금까지 중단편 15편뿐이라는 것이다.
그의 작품집은 〈당신 인생의 이야기〉가 유일할 정도로 그 수가 적다.</p>
<p>〈당신 인생의 이야기〉에는 다음의 총 여덟 작품이 수록되어 있다: 〈바빌론의 탑〉, 〈이해〉, 〈영으로 나누면〉, 〈네 인생의 이야기〉, 〈일흔두 글자〉, 〈인류 과학의 진화〉, 〈지옥은 신의 부재〉, 그리고 〈외모 지상주의에 관한 소고: 다큐멘터리〉.
이 중 필자는 〈네 인생의 이야기〉와 〈바빌론의 탑〉, 그리고 〈지옥은 신의 부재〉를 가장 흥미롭게 읽었다.
특히 〈바빌론의 탑〉에서 생생하게 묘사되는 이질적이면서도 사실적인 세계관이 인상깊었다.
〈지옥은 신의 부재〉는 종교적인 소재를 다루면서도 굉장히 과학적으로 지옥, 현세, 그리고 천국을 소개하고, 천사의 강림을 독창적으로 해석한 것이 참신하였다.
한 작품을 마칠 때마다 정말 많은 생각을 하게 되었는데, 기회가 되면 단편집에 담긴 다른 소설들도 소개하고 싶다.
여기서는 단편집에 담긴 여러 훌륭한 작품들 중 〈네 인생의 이야기〉의 줄거리와 더불어 과학적인 배경을 다루려고 한다.</p>
<p>인간은 기본적으로 원인과 결과, 즉 인과론적인 관점으로 우주를 바라본다.
유리잔을 던지는 행위를 취하면, 유리잔이 깨지는 결과가 따라올 것이다.
공을 언덕의 경사면에 놓으면 중력에 의해 굴러내려올 것이다.
과학에 큰 관심이 없더라도, \(F = ma\)라는 공식은 누구나 한 번쯤 보거나 들어보았을 것이다.
바로 뉴턴의 운동 제2법칙이다.
사실 좀 더 물리적인 의미를 가지는 식의 형태는 바로 \(a = \frac F m\)이다.
여기서 \(a\)는 가속도, \(F\)는 힘, \(m\)은 질량인데, “어떤 질량 \(m\)인 물체에 힘 \(F\)를 가하면(우변), 물체는 가속도 \(a\)로 가속한다(좌변)”는 의미를 내포하고 있다.
즉, 식의 우변은 원인, 좌변은 결과를 나타낸다.
이것은 모든 사건에는 원인이 있고, 그것에 따라서 결과가 발생한다는, 인간이 세상을 바라보는 지극히 직관적인 관점과 상통하는 것이다.</p>
<p>나아가 사람들이 서로 사회적인 상호 작용을 하면서 인간 관계를 만드는 것도 인과론적으로 볼 수 있다.
타인에게 어떤 행동을 하는지, 어떻게 대화를 하는지가 그 사람과의 관계를 형성하는 동력이 된다.
인간 관계에 있어서 어떤 선택을 하는지에 따라 결과는 확연히 달라진다.
이것은 모두 인간이 시간의 축에 종속되어 미래에 대한 정보를 알지 못한다는 것에서 연유된다.
인간은 미래가 불확정적이라고 인식하며, 그것이 바로 원인과 결과에 대한 관점의 핵심이다.
인과론의 전제는 원인에 대한 자유도가 서로 다른 결과로 이어질 수 있다는 것에 있다.
〈네 인생의 이야기〉는 이 관점에 질문을 던진다.
과연 인과론이 우주를 서술하는 유일한 관점인가?</p>
<p>그렇다면 인과론에 배치되는 관점은 무엇인가?
일단 위에서 언급한 \(F = ma\)를 상기시켜보자.
\(F = ma\)를 뉴턴의 제2법칙이라고 했는데, 그렇다면 제1법칙도 있을 것이다.
사실 뉴턴의 운동 법칙에는 총 세 개가 있다.
제1법칙은 ‘관성의 법칙’이다.
‘관성의 법칙’은 어떤 물체에 힘을 가하지 않는다면 운동 상태를 계속 유지한다는 것을 말한다.
제3법칙은 ‘작용과 반작용의 법칙’으로, 어떤 물체에 힘을 가하면 물체도 똑같은 크기를 가지는 힘을 반대 방향으로 힘을 가한 주체에게 가한다는 내용이다.
여튼 이 세 법칙을 기반으로 우리 우주의 역학 체계를 설명할 수 있는데—고전적으로는—, 이렇게 구축된 역학 체계를 뉴턴 역학이라고 한다.
뉴턴 역학은 앞서 말했듯이 자연을 인과적으로 서술하는 역학 체계이다.
그러나 물리학적으로는 이 인과론적인 역학 체계와 동치인 다른 역학 체계들이 있음이 알려져 있는데, 바로 라그랑주 역학이다.</p>
<p>라그랑주 역학은 우리 우주를 뉴턴 역학과는 사뭇 다르게 바라본다.
간단히 말해 라그랑주 역학은 물체가 라그랑지안이라는 물리량을 최소화시키기 위해 운동한다고 설명한다.
잠시 라그랑지안이 무엇인지 살펴보자면, 운동 에너지\(T\)에서 위치 에너지\(V\)를 뺀 값으로, \(L(q, \dot q, t) = T(q, \dot q, t) - V(q, t)\)로 표현된다.
조금 복잡해보이는가?
더 나가보자.
라그랑주 역학의 핵심 아이디어는 바로 해밀턴 원리, 혹은 최소 작용의 원리이다.
사실 ‘작용’이라는 것도 다음과 같은 물리량으로 정해진다:</p>
\[S = \int_{t_1}^{t_2} L(q, \dot q, t) \mathrm{d}t.\]
<p>라그랑주 역학은 위의 작용을 변분이라는 연산을 통해 최소화시키는 조건을 찾아내며, 이를 통해 운동 방정식을 이끌어낸다.
이렇게 얻은 운동 방정식은 실제로 뉴턴 역학을 통해 얻어낸 그것과 완벽히 일치한다.
다시 말해, 두 역학 체계는 동치라는 것이다.
굳이 위에서 수식을 언급한 이유는, 이 라그랑주 역학이라는 것이 전혀 간단한 체계가 아님을 직접적으로 보여주기 위함이다—물리학에 친숙한 독자들을 제외하면.
비록 그 착상은 놀라울만큼 단순하다: 운동은 어떤 물리량 작용을 최소로 유지하며 움직인다.
이것이 바로 최소 작용의 원리로, 운동은 자연의 내재적인 목적을 달성시키기 위해 진행된다는 의미를 가지고 있다.</p>
<p>조금 복잡하게 느껴진다면, 더 간단하게는 소설 속에서도 언급되는 페르마의 최소 시간 원리를 예시로 들 수 있다.
빛은 서로 다른 두 매질을 통과할 때 그 경계면에서 굴절률에 따라 꺾인다.
단순히 생각해보면 두 점을 잇는 최단 경로를 지날 것 같지만, 빛은 스넬의 법칙에 따라 속도가 변화하여 다른 경로로 이동하게 된다.
한편 이 경로는 이동 시간을 최소화하는 경로에 해당한다.
어떻게 보면 빛은 이동을 하기 전에 시간이 최소가 되는 경로를 미리 알고 이를 선택하는 것이다.
이것이 바로 페르마의 최소 시간 원리로, 빛은 시간을 최소화하는 경로를 통해 이동한다는 내용이다.
다르게 말해서 빛은 ‘이동 시간’이라는 값을 최소화하는 목적을 띤다.
라그랑주 역학은 이를 확장하여 목적론적인 관점을 역학 체계에 적용한다.</p>
<p>시간 종속적인 뉴턴 역학의 관점과 운동 그 자체를 조명하는 라그랑주 역학.
상충될 것만 같은 두 관점이, 같은 우주를 서술하는 서로 다른 방식이라는 것은 상당히 역설적이다.
인간은 어떤 행위를 하기 전에 그것이 어떤 결과를 불러 일으킬지 알지 못한다는 점에서 라그랑주 역학은 인간의 인식과 상당히 동떨어졌다는 생각까지 들게 한다.
라그랑주 역학은, 예를 들자면, 인간은 공공의 선을 최대화하기 위한 목적을 지니고 정해진대로 행동한다는 해석만큼이나 비상식적인 해석이다.
간단히는, 위에서 언급한 라그랑주 역학에 관한 수식들과 뉴턴 역학을 비교해보라!
그런데 과연 이것이 인간만의 상식이라면?
우주 어딘가에 있는 다른 외계 종족은 인과론이 아닌 목적론적인 관점을 가지고 있다면?</p>
<p>〈네 인생의 이야기〉는 목적론적인 관점을 상식으로 취하는 외계 종족인 헵타포드의 언어를 해석하며 관점, 나아가 사고 방식이 변화하게 되는 언어학자 루이즈의 이야기를 다룬다.
헵타포드의 언어는 인간의 언어처럼 시간 종속적이지 않은데, 루이즈는 이들의 언어를 해석하며 헵타포드의 사고를 체득하게 된다—언어를 통해 사고가 바뀐다는 설정은 언어적 상대성, 혹은 사피어-워프 가설에 바탕을 두고 있다고 한다.
이들의 관점으로 세상을 볼 수 있게된 루이즈는 미래를 ‘기억’할 수 있게 되다.
소설은 루이즈가 아직 태어나지 않은 딸에게 헵타포드가 지구에 방문한 일, 그녀가 그들의 언어를 해석한 것과 딸에게 앞으로 일어날 일들에 대해서 2인칭으로 이야기해주는 형식이다.
그녀의 딸의 이야기가 바로 소설의 제목, 네 인생의 이야기인 것이다.</p>
<p>그녀는 딸에게 일어날 일들에 대해서 말할 때에는 “… 기억해”로 문장을 구성할 뿐만이 아니라, 그녀에게 일어날 일들을 “…할 것이야”가 아닌 “…해”로 설명한다.
이는 미래에 일어날 사건을 현재 시제로 표현하여, 예측이 아니라 마치 자연의 불변하는 법칙을 언급하는 듯한 인상을 준다.
그녀에게, 그리고 헵타포드에게 우리 우주는 이미 정해진 미래를 가지고 있으며, 그들은 이미 주어진 사건들을 수행하며 운명을 따르는 것이다.
그렇다면 그들에게 언어란 어떤 의미를 가질까?
인간의 시점에서 보았을 때 ‘결과’가 정해져 있으면 의사소통이 불필요하다고 생각할 수 있다.
그러나 그들에게 언어는 수행문의 성격을 띤다.
언어를 통해 자신의 생각의 행위를 전달함으로써 미래를 실현하는 것이다.
대화를 하기 전에 이미 그들은 내용을 알고 있지만, 그것이 참이 되기 위해서는 그 대화가 직접 행해져야만 한다.</p>
<p>이런 배경에서, 〈네 인생의 이야기〉에서 가장 마음이 아프고 곱씹게 되는 내용은 루이즈의 딸이 젊은 나이에 사고를 당해 비극적으로 생을 마감했다는 것과, 루이즈는 그 사실을 딸이 태어나기도 전에 알고 있다는 것이다.
그녀는 단순히 이 사실을 알고 있는 것을 넘어 ‘기억’하고 있기에, 그 고통스러운 사건을 이미 경험했다고 볼 수 있다.
하지만 루이즈는 이 비극적인 미래를 바꾸려 하지 않는다.
그녀는 마치 빛이 이동 시간을 최소화하는 경로 상에서 나아가는 것처럼, 정해진 인생을 실현하기 위해 운명을 수행한다.
어떻게 보면 루이즈와 헵타포드는 속박된 기계라는 생각이 들겠지만, 루이즈는 이들에게 있어서 연대기를 실현시키는 것 자체가 바로 동기라고 한다.
이들에게 있어서 자유는 무의미하지만, 그렇다고 이돌의 삶이 강제적인 것은 아니다.</p>
<p>〈네 인생의 이야기〉는 구체적으로는 딸의 비극적인 인생을 포함한 자신의 삶 전체를 동시적으로 경험하게된 루이즈에 대해 이야기한다.
하지만 이는 모든 사람들의 인생에 대한 은유로 받아들일 수 있다고 생각한다.
우리는 흔히 미래에 대해서는 알지 못한다고 생각하며, 어느 정도는 맞는 말이다.
인과의 관점을 지닌 우리는 당장 내일조차 어떤 일들이 일어날지 알 방도가 없다.
그러나 우리는 언젠가는 떠내보낼 수 밖에 없는 인연들, 길을 따라 걷다 보면 마주치게될 무수한 슬픔들, 그리고 결국은 직면하게될 죽음의 존재를 이미 알고 있다.
그럼에도 불구하고 우리는 새로운 사람들을 만나고, 힘든 결정들을 내리고, 그 끝을 알고 있는 인생의 하루하루를 헤쳐간다.
비록 인간이 모든 사건들을 동시적으로 경험하는 헵타포드는 아닐지라도, 중요한 것은 ‘결과’가 아닌 ‘과정’이라는 것을 모두가 알고 있기 때문이다.</p>Zetajaeho.lee@snu.ac.kr영화 《컨택트》 (원제: Arrival)의 원작 소설인 〈네 인생의 이야기〉에는 감동적인 스토리뿐만이 아니라 하드 SF 답게 과학적으로 철학적인 내용도 담고 있다. 〈네 인생의 이야기〉의 내용뿐만이 아니라 최소 작용 원리, 라그랑주 역학 등에 기초한 소설의 배경에 대해서도 알아본다.