정점 N개와 간선 N개를 가진 트리는 한 개의 사이클을 가지고 있는 트리이다. 따라서 정점 u에서 v로 가는 경로가 사이클을 거쳐가게 된다면 경로는 2개가 되고, 경로에 사이클이 없다면 경로는 1개가 되는 것이다.
따라서 사이클에 속해있는 정점을 파악한 뒤 유니온 파인드를 수행하다. 연결하려 하는 두 정점의 경로가 사이클에 속한 경로라면, 정점을 연결하지 않고 넘어간 뒤, 쿼리가 주어질 때, 두 정점의 부모가 같다면 1 아니면 2를 출력하면 정답이다.
주어진 예제를 통해 보면 정점 1 - 2 - 3으로 사이클이 이루어진다. 따라서 정점 1, 2, 3 사이의 간선을 없애고 쿼리에서 서로 다른 컴포넌트의 경로를 물어보면 해당 경로는 사이클을 통한 경로가 있다는 뜻이므로 2를 출력, 같은 컴포넌트 내의 경로를 물어보면 1을 출력하면 된다.
문제의 핵심은 가중치가 주어지는 간선은 오로지 한 개라는 것. 즉 가중치가 있는 간선을 거쳐가는 경로의 개수를 찾는 문제다. 가중치가 없는 간선들을 먼저 연결해 준 뒤 마지막으로 가중치가 존재하는 간선을 연결할 때, 연결하려는 두 컴포넌트의 개수를 서로 곱해주면 해당 간선을 거쳐가는 간선의 개수를 구할 수 있게 된다. 예제를 기준으로 그림으로 설명하면 다음과 같다.
2번 정점과 3번 정점을 연결할 때, 1-3, 1-4, 1-5, 2-3, 2-4, 2-5의 경로에서 2-3 간선을 이용해야만 한다. 즉 2번 정점이 속한 컴포넌트의 정점개수와 3번 정점이 속한 컴포넌트의 정점 개수를 서로 곱해주면 정답이다.
단, 예제입력 2와 같이 가중치가 존재하는 간선을 연결할 때 이미 두 정점이 같은 컴포넌트인 경우 비용 0인 간선으로 우회가 가능하기에 이때는 정답이 0이다.
따라서 유니온 파인드 알고리즘을 통해 각 컴포넌트의 개수를 핸들링하여 정답을 출력해 주면 된다.
구조체는 클래스와 마찬자기로메서드를가질 수 있는데,자기 자신(self)의변경이 일어나는 메서드작성 시 mutating 키워드를 명시해주어야 한다.
Lectrue 3에서 구조체의 대표적인 특징인 copy on write이 있다고 했다. 구조체는 값타입(value type)으로 객체의 할당 혹은 인자로 넘겨줄 경우 값 복사가 일어나지만, 정확히는 사본과 원본의 차이점이 발생할 때 복사가 이루어진다고 했다. 여기서 사본생성의 시점 혹은 이후에 사본 생성 여부를 가늠하게 해주는 장치가 바로 mutating 키워드이다.
ObservableObject
swiftUI의 핵심기능 중 하나이다. 해당 프로토콜이 적용된 객체에 변경이 감지되면 구독하고 있는 Subscriber에게 변경을 알린다.
가장 많이 사용되는 부분이 MVVM구조의 ViewModel에 해당된다. View에서 ObservableObject를 구독(Subscribe) 하고 ViewModel에서 알리는 변경점을 통해 View의 body의 필요한 부분의 재빌드를 통해 데이터 바인딩이 일어난다.
ObjectWillChange
ObservableObject를 채택하게 되면 우리는 ObjectWillChange라고 불리는 Publisher를 얻게 된다. 해당 속성을 통해 변경점을 알릴 수 있다.
send()
ObservableObject가 가지고 있는 속성 중 변경이 일어나면, ObjectWillChange.send()를 호출하여 변경사항을 subscriber에게 알리게 된다.
@Published
ObservableObject의 Publisher를 통해 변경점을 알리는 일을 자동으로 해주는 키워드이다. ObservableObject 내에 속성 중에서 변경점을 알리고 싶은 속성에 해당 키워드를 적용하게 되면 변경점이 일어날 경우 자동으로 ObjectWillChange.send()가 호출된다. 주로 ViewModel에 생성된 Model 객체에 사용하게 된다. 따라서 Model의 변경을 자동으로 감지할 수 있고, 이를 통해 Model과 View의 바인딩 작업을 손쉽게 할 수 있다.
@ObservedObject
ObservableObject를 구독(subscribe) 받기 위해 사용하는 키워드이다. 생성된 객체에 해당 키워드를 적용하면 된다. 주로 View에서 ObservableObject를 채택한 ViewModel을 구독하기 위해 사용하게 된다. 이를 통해 ViewModel은 Model과 View의 바인딩이 가능해진다.
UI구성의 내용이 담긴 ContentView는 결국 View 구조체이며, 이러한 구조체 안에는 여러 가지 변수, 메서드를 담아낼 수 있다.
눈여겨볼 것은 body 변수인데, 해당 변수는 View도 아닌, some View라고 정의되어 있다.
강의에서는 "something that behave like a View"라고 설명했다. 즉 "View처럼 행동하는 무언가" 정도로 해석이 가능하다.
그렇다면 왜 View가 아니라 View처럼 행동하는 무언가일까?
교수님은 해당 부분을 레고에 비유하였다.
우리가 레고로 집을 만든다고 하였을 때, 작은 단위의 레고를 모아 의자, 소파, 테이블과 같은 작은 단위의 레고를 만들고, 그것들이 모여 거실, 방, 지붕과 같은 큰 단위로 결합하게 된다. SwiftUI 또한 작은 단위의 View가 결합되어 하나의 View를 구성하게끔 동작한다는 것이다. 즉 수많은 종류의 View 중에 어떤 형태의 View가 반환될지 모르기에 some View라는 키워드로 치환된 것이다.
조금 더 해당 표현에 대해 자세한 설명을 하자면, 화면상에 단 하나의 View만 존재하게 될 경우, 위와 같은 코드로 작성하게 된다. 하지만 해당 코드는 swiftUI에 의해 몇 가지 키워드가 치환된 형태이다. 조금 더 명시적인 형태로 작성하게 되면 아래의 코드로 적을 수 있다.
import SwiftUI
struct ContentView: View {
var body: Text {
return Text("Hello!")
}
}
ContentView의 body는 Text라는 View구조체이며, 이는 곧 "Hello"라 적힌 Text View를 반환한다는 것. 하지만 예시보다 복잡한 여러 형태의 View가 조합되면 개발자는 이에 대한 명시를 하기 어려워지므로 some View라는 형태로 적게 된 것이며, 해당 부분에 대한 치환은 컴파일러가 알아서 처리하게 하는 것이다.
이처럼 body와 같이 여러 가지 View를 결합하는 View를 Combiner View라고 소개하였으며, 해당 부분의 some View는 대부분 Combiner View를 의미한다고 한다.
함수형 프로그래밍
ContentView의 body는 변수로 선언이 되어있지만, 메모리에 저장되지 않는다고 한다. body에 접근할 때마다 body 뒤에 이어진 { ... } 블록의 해당 함수 내용을 수행하고 반환되는 결과를 받아 오는 것이다. 이는 swift의 큰 특징 중 하나인 함수형 프로그래밍에 의한 표현이다.
결론
some View라는 표현은 개발자가 편하기 UI 작성하기 위한 표현이며, 이는 최종적으로 컴파일러가 body의 closure에 정의된 함수 내용을 수행하여 반환되는 특정한 View로 치환하게 된다는 이야기이다.
Zstack
View에는 여러 가지 종류의 View가 있으며 이들 중 다른 View를 결합하는 combiner View가 있다 했다. Zstack 또한 강력한 기능을 가진 Combiner View라고 소개되었는데, content { ... } 인자 내에 단순히 View를 나열하기만 하면 스크린 - 사용자 방향 순서로 view를 결합하는 기능을 수행한다. 강의에서는 이렇게 나열을 통해 View를 결합하는 것을 View Builder Machanism이라는 표현으로 소개하였다.
Zstack에는 나열한 것을 View로 생성하는 것 이외에도 또 다른 강력한 기능이 있는데, 바로 ZStack에 적용한 속성들이 Zstack 하위의 View에 상속된다는 점이다.
예시의 padding 이외에도 foregroundColor 등 View가 가진 공통 속성들의 적용이 가능하며, 당연하게도 하위 View에 직접 속성을 적용하게 된다면 오버라이딩과 같은 효과를 줄 수 있다.