# App

SwiftUI 기반으로 프로젝트를 생성하면 다음과 같이 기본적으로 앱의 진입점이 되는 구조체가 정의된다.

import SwiftUI

@main
struct swiftui_playgroundApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@main 키워드를 통해 컴파일러가 메인함수를 합성한다. App 프로토콜을 채택하며 필수 구현 대상으로 body 속성이 있다. body 속성은 Scene프로토콜을 채택한 Scene 객체이다. Scene은 뷰 계층을 담고 있는 컨테이너이다. 뷰를 표시하는 시점 등을 조절한다.

WindowGroup은 기본적으로 제공되는 씬 중 하나이며 윈도우그룹 내에서 실행할 화면을 리턴하여 초기 화면이 결정된다.

# Scene

씬은 뷰 계층을 담고 있는 컨테이너이며, 플랫폼과 현재 앱 상태에 따라서 뷰 계층을 표시하는 시점, 방식을 자동으로 결정해준다.

씬 레벨에서 할 수 있는 일은 다음과 같은 것들이 있다.

  1. 코드 가독성을 위해 커스텀 씬을 구현한다.
  2. 씬의 상태에 따라 원하는 작업을 실행한다.

커스텀 씬을 구현할 때에는 Scene 프로토콜을 채택하고 Scene 프로토콜을 채택한 body속성을 구현해주면 된다.

@main
struct swiftui_playgroundApp: App {
    var body: some Scene {
//        WindowGroup {
//            ContentView()
//        }
        MyScene() // 커스텀 씬 리턴
    }
}

// 커스텀 씬 정의
struct MyScene: Scene {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

씬의 단계를 처리하는 코드는 다음과 같다.

struct MyScene: Scene {
    @Environment(\.scenePhase) private var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) { oldValue, newValue in
            print(oldValue, newValue)
        }
    }
}

씬 페이지라는 시스템 객체를 사용하며 @Environment 키워드를 통해 구현하며, 씬의 onChange 함수에 씬 페이지 객체를 전달한 뒤 클로저 파라미터를 통해 씬 상태 변화를 체크할 수 있다.

씬은 크게 active, inactive, background 세 가지 속성을 갖는다.

# View

스위프트 UI에서 뷰는 View 프로토콜을 채택한 인스턴스를 의미한다. UIKit에서는 뷰 클래스를 상속받기 때문에 상대적으로 무거운데, 스위프트 UI는 상속받는 멤버가 없어서 가볍다. 또한 값 타입 기반이기 때문에 RC와 관련된 오버헤드도 없다.

뷰는 아래와 같이 계산속성 형태로 멤버를 구현하면 된다.

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Button(action: {
                print("HI")
            }, label: {
                Text("Button")
            })
        }
        .padding()
    }
}

body 속성

뷰의 body속성에는 반드시 하나의 뷰만 리턴되어야 한다. 위의 예시 코드에서 ImageButton뷰를 각각 리턴하는 방식이 아닌 VStack 내에 감싸 리턴하는 이유가 바로 위와 같은 문제로 인한 것이다.

뷰가 그려지는 주기

SwiftUI에서 뷰는 내부 라이프사이클에 맞추어 반복적으로 그려진다. 뷰 인스턴스가 새로 그려지지만 뷰 자체가 오브젝트가 아닌 불분명 타입 기반의 프로토콜 타입으로 호출 단에서 처리되기에 뷰가 가볍다.

다만 인스턴스가 새로 생성되는 만큼 이니셜라이저가 가벼워야 하므로 생성자 함수 전체를 구현하지 않고 멤버와이즈 이니셜라이저 기반으로 필요한 속성들에 대한 초기화만 진행한다.

# Modifier

SwiftUI에서는 뷰 뒤에 붙는 속성 조정자들을 모디파이어(Modifier)라고 부른다. 뷰 자체에 대한 속성값 조정에도 사용하고 뷰 라이프사이클 오버라이딩에도 사용한다.

var body: some View {
    VStack {
        // ...
    }
    .onAppear {
        print("onAppear!")
    }
    .onDisappear {
        print("onDisappear!")
    }

모든 모디파이어는 View 프로토콜을 채택하는 요소를 리턴한다.

Text("\(string)")
    .bold()
    .font(.caption)

bold 모디파이어를 통해 볼드체가 적용된 뷰를 리턴하고, 볼드체가 적용된 뷰를 받아 캡션 폰트가 적용된 폰트를 새로 리턴하게 된다.

모디파이어는 뷰 프로토콜을 채택하는 모든 요소에 적용 가능하기 때문에, 공통적으로 적용하려는 속성은 상위 뷰에 적용하면 한꺼번에 처리 가능하다.

var body: some View {
    VStack {
        Text("\(string)")
            .font(.caption)
        Text("HI!!!")
            .font(.subheadline)
        Button(action: {
            string = "\(Int.random(in: 1...10))"
        }, label: {
            Text("Button")
        })
    }
    .bold()
}

VStack에 볼드 모디파이어를 적용하면 하위 요소인 두개의 텍스트뷰와 버튼에 모두 볼드체가 적용된다. 상위 요소에 등록한 모디파이어가 먼저 적용되고, 하위 요소에 등록한 모디파이어가 그 다음 적용된다.

따라서 공통적으로 적용하다가 하나의 자식 요소만 다른 속성값을 부여하고 싶다면 해당 요소에 새로운 모디파이어를 다시 등록해주면 된다.

var body: some View {
    VStack {
        Text("BOLD TEXT")
        Text("BOLD TEXT")
        Text("LIGHT TEXT") // light weight 적용
    }
    .bold()
}