SwiftUI 뷰
사용자 인터페이스 레이아웃은 뷰 사용과 생성, 그리고 결합을 통해 SwiftUI로 구성된다. 가장 먼저 살펴봐야 할 중요한 단계는 View 라는 용어의 의미를 이해하는 것이다. SwiftUI에서 뷰는 View 프로토콜을 따르는 구조체로 선언된다.
View 프로토콜을 따르도록 하기 위해 구조체는 body 프로퍼티를 가지고 있어야 하며, 이 body 프로퍼티 안에 뷰가 선언돼야 한다.
- SwiftUI에는 사용자 인터페이스를 구축할 때 사용될 수 있는 다양한 뷰(텍스트 레이블, 텍스트 필드, 메뉴, 토글, 레이아웃 매니저 뷰 등)가 내장되어 있다.
- 각각의 뷰는 View 프로토콜을 따르는 독립적인 객체다.
- SwiftUI로 앱을 만들게 되면 커스텀 뷰를 생성하기 위해 이들 뷰를 사용하게 될 것이다.
- 재사용 가능한 뷰 컴포넌트를 캡슐화하는 하위 뷰(예를 들어, 텍스트 레이블, 텍스트 필드, 메뉴, 토글, 레이아웃 매니저 뷰 등)부터 전체 화면에 대한 사용자 인터페이스를 캡슐화한 뷰까지 다양한 영역에 걸쳐 커스텀 뷰를 만들 수 있다.
- 커스텀 뷰의 크기와 복잡성 또는 커스텀 뷰에 캡슐화된 자식 뷰의 개수와는 관계없이, 하나의 커스텀 뷰는 사용자 인터페이스 모양과 동작을 정의하는 하나의 객체일 뿐이다.
기본 뷰 생성하기
Xcode에서 커스텀 뷰는 SwiftUI View 파일에 포함된다. 새로운 SwiftUI 프로젝트를 생성하면 Xcode는 Text 뷰 컴포넌트 하나로 구성된 하나의 커스텀 뷰를 가진 단일 SwiftUI View 파일을 생성할 것이다.
- 기본적으로 ContentView.swift가 만들어지며, 다음과 같은 코드가 포함되어 있다.
import SwiftUI struct ContentView: View { var body: some View { VStavk { Image(systemName: "globe") .imagescale(.large) .foregroundColor(.accentColor) Text("Hello, World!") } } } struct ContentView_Previews: PreviewProvider { static var previews: some view { ContentView() } } - ContentView는 View 프로토콜을 따르도록 선언되어 있다. 또한, 필수 요소인 body 프로퍼티도 가지며, "Hello, World!"라는 문자열로 초기화된 내장 컴포넌트인 Text 뷰의 인스턴스가 body 프로퍼티에 포함되어 있다.
- 이 파일의 두 번재 구조체는 ContentView의 인스턴스를 생성하기 위해 필요하며, 프리뷰 캔버스에 나타나게 한다.
뷰 추가하기
body 프로퍼티에 원하는 뷰를 배치하여 다른 뷰가 추가될 수 있다. 하지만 body 프로퍼티는 단 하나의 뷰를 반환하도록 구성되어 있다.
- SwiftUI 뷰는 기본적으로 부모 뷰와 자식 뷰 형태의 계층 구조가 된다. 이것은 여러 계층의 뷰들이 복잡한 사용자 인터페이스를 생성할 수 있게 한다.

- 주목해야 할 점: 컨테이너에 포함된 여러 뷰를 서로 연결하면 하나의 뷰처럼 간주된다는 것이다.
- 따라서 다음의 코드도 유효한 뷰 선언부다.
struct ContentView: View { var body: some View { Text("Hello, ") + Text("how ") + Text("Are You?") } } - 앞의 예제에서 body 프로퍼티의 클로저는 반환 구문이 없다. 왜냐하면 단일 표현식으로 되어 있기 대문이다.
- 하지만, 다음의 예제와 같이 클로저에 변도의 표현식이 추가되면 return 구문을 추가해야 한다.
struct ConetentView: View { @State var fileopen: Bool = true var body: some View { var myString: String = "File closed" if (fileopen) { myString = "File open" } return VStack { HStack { Text(myString) .padding() Text("Goodbye, world") } } } } - 코드 에디터에 다음과 같은 구문 오류가 나타나면 일반적으로 return 문이 필요하다는 의미다.
Type '()' cannot conform to 'View' 쉽게 말해서, 위의 코드 예시는 body의 클로저 안에 여러 줄의 코드가 있다. myString 변수가 선언됐고, if 조건문이 실행되었으며 마지막에 VStack 뷰가 생성되는 형태다.
- 단일 표현식이 아니기 때문에 Swift는 마지막 줄이 반환값이라는 걸 자동으로 추론하지 않는다.
- 그래서 이게 반환값임을 알리기 위해 return이 필요한 것이다.
하위 뷰로 작업하기
애플은 최대한 뷰를 작고 가볍게 하라고 권장한다. 이것은 재사용할 수 있는 컴포넌트 생성을 권장하고, 뷰 선언부를 더 쉽게 관리하도록 하며, 레이아웃이 더 효율적으로 렌더링되도록 한다.
만약 우리가 만든 커스텀 뷰의 선언부가 크고 복잡하다면 하위 뷰로 나눌 수 있는 부분을 찾아야 한다.
- 매우 간단한 예제로, 이전의 코드에서 HStack 뷰를 다음과 같이 MyHStackView라는 이름의 하위 뷰로 나눌 수 있다.
struct ContentView: View { var body: some View { VStack { Text("Text 1") Text("Text 2") MyHStackView() } Text("Text 5") } } struct MyHStackView: View { var body: some View { HStack { Text("Text 3") Text("Text 4") } } } 프로퍼티로서의 뷰
하위 뷰를 생성하는 것 외에도 복잡한 뷰 계층 구조를 구성하는 방법으로 프로퍼티를 뷰에 할당할 수도 있을 것이다.
struct ContentView: View { var body: some View { VStack { Text("Main Title") .font(.largeTitle) HStack { Text("Car Image") Image(systemName: "car.fill") } } } } - 앞의 선언부 중 일부는 프로퍼티 값으로 이동하고 이름으로 참조될 수 있다. 다음의 선언부는 HStack을 carStack이라는 이름의 프로퍼티에 할당하고 VStack 레이아웃에서 참조한다.
struct ContentView: View { let carStack = HStack { Text("Car Image") Image(systemName: "car.fill") } var body: some View { VStack { Text("Main Title") .font(.largetitle) carStack } } } 뷰 변경하기
SwiftUI와 함께 제공되는 모든 뷰는 커스터마이징이 필요 없을 정도로 완전히 정확하게 우리가 원하는 모양과 동작을 하는 건 아니기 때문에 수정자(modifier)를 뷰에 적용하여 변경할 수 있다.
모든 SwiftUI 뷰에는 뷰의 모양과 동작을 변경하는 데 사용될 수 있는 수정자들이 있다. 이들 수정자는 뷰의 인스턴스에서 호출되는 메서드 형태를 취하며, 원래의 뷰를 다른 뷰로 감싸는 방식으로 필요한 변경을 한다. 이 말은 동일한 뷰에 여러가지를 변경하기 위해 수정자들이 연결될 수 있다는 의미다.
Text("Text 1") .font(.headline) .foregroundColor(.red) Image(systemName: "car.fill") .resizable() .aspectRatio(contentMode: .fit) MyHStackView() .font(.largeTitle) - 위와 같이 수정자는 커스텀 하위 뷰에도 적용할 수 있다. 이렇게 하면 이 뷰의 모든 Text 뷰들에 적용된다.
텍스트 스타일로 작업하기
애플이 권장하는 방식. 텍스트 스타일을 이용하여 뷰의 폰트를 선언했다면 텍스트 크기는 사용자가 지정한 폰트 크기에 따라 동적으로 맞춰진다. 거의 예외 없이, 내장된 ios 앱들은 사용자에 의해 선택된 폰트 크기 설정을 따라 텍스트를 표시하며, 다른 앱들도 사용자가 선택한 텍스트 크기를 따르라고 애플은 권장한다.
- Large Title
- Body
- Title, Title2, Title3
- Callout
- Headline
- Footnote
- Subheadline
- Caption, Caption2
딱 맞는 스타일이 없다면 폰트 종류와 크기를 선언해 커스텀 폰트를 적용할 수 있다. 하지만 이렇게 하면 사용자가 선택한 텍스트 크기와는 상관없이 고정된 크기로 표시될 것이다.
수정자 순서
수정자들을 연결할 때 수정자들이 적용되는 순서가 중요하다는 점을 알아야 한다.
Text("Sample Text") .border(Color.black) .padding() - border 수정자는 뷰 주변에 검정색 경계선을 그리며, padding 수정자는 뷰 주변의 여백을 추가한다. 하지만 글자 주변에는 패딩이 안 생긴다. 원하는 그림을 그리기 위해서는 다음과 같이 수정해야 한다.
Text("Sample Text") .padding() .border(Color.black) - 만약 수정자를 연결해서 작업했는데 기대한 효과가 나타나지 않는다면 뷰에 적용되는 순서 때문일 수 있다.
커스텀 수정자
SwiftUI는 나만의 커스텀 수정자를 생성할 수 있게도 해준다. 이것은 뷰에 자주 적용되는 대표적인 수정자들을 갖고자 할 때 유용하다.
Text("Text 1") .font(.largeTitle) .backgroung(Color.white) .border(Color.gray, width: 0.2) .shadow(color: Color.black, radius: 5, x: 0, y: 5) - 이와 같은 모양으로 표시해야 할 텍스트가 나올 때마다 4개의 수정자를 계속 적용하는 것보다 더 좋은 방법은 -> 이들을 커스텀 수정자로 묶어서 필요할 때마다 참조하는 것이다.
- 커스텀 수정자는 ViewModifier 프로토콜을 따르는 구조체로 선언된다.
struct StandardTitle: ViewModifier { func body(content: Content) -> some View { content .font(.largeTitle) .background(Color.white) .border(Color.gray, width: 0.2) .shadow(color: Color.black, radius: 5, x: 0, y: 5) } } - 필요한 곳에서 modifier() 메서드를 통해 커스텀 수정자를 전달하여 적용한다.
- ViewModifier
- SwiftUI가 수정자를 정의할 수 있게 해주는 프로토콜
- body(content: )메서드를 반드시 구현해야 함.
- content
- 수정자 안에 들어갈 원본 뷰를 의미.
- 예를 들어 Text("Text 1").modifier(StandardTitle())라고 쓰면 "Text 1" 이게 content가 됨.
Text("Text 1") .modifier(StandardTitle()) Text("Text 2") .modifier(StandardTitle()) - 커스텀 수정자를 구현했으니 StandardTitle 구조체를 수정하면 이 수정자를 사용하는 모든 뷰에 자동으로 적용될 것이다.
- 이렇게 하면 여러 뷰에 적용한 수정자를 수동으로 수정할 필요가 없어진다.