SwiftUI

SwiftUI - 커스텀 뷰 생성

iosos 2024. 1. 10. 18:17

SwiftUI 뷰

  • 뷰 : View 프로토콜을 따르는 구조체로 선언
  • View 프로토콜을 따르는 구조체는 body 프로퍼티를 가지고 있어야 되며, body 안에 뷰가 선언되어야 함
  • 텍스트 레이블, 텍스트 필드, 메뉴 토글, 레이아웃 매니저 뷰 등 다양한 뷰가 내장되어 있음
  • 각 뷰는 View 프로토콜을 따르는 독립적인 객체
  • 커스텀 뷰의 크기와 복잡성 또는 커스텀 뷰에 캡슐화된 자식 뷰의 개수와는 관계없이, 하나의 커스텀 뷰는 사용자 인터페이스 모양과 동작을 정의하는 하나의 객체

 

 

기본 뷰 생성

  • 기본 ContentView
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
    }
}
  • View 프로토콜을 따르며, 필수 요소인 body 프로퍼티 가지고 있음
  • 내장 컴포넌트인 Text 뷰의 인스턴스가 body 프로퍼티 포함

 

 

 

뷰 추가

  • body 프로퍼티는 하나의 뷰를 변환하도록 구성되어 있어 다음의 예제와 같이 뷰를 추가하면 구문 오류 발생
import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
        Text("Hello, world!")   // 유효하지 않은 구조
    }
}

→ 뷰를 추가하기 위해서는 스택(stack)이나 폼(form) 같은 컨테이너 뷰에 뷰들을 배치해야 함

 

 

  • 수직 스택 (VStack) 사용 → 뷰들이 수직 방향으로 배치
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
            Text("Hello, world!")
        }
    }
}

 

 

 

  • SwiftUI 뷰는 기본적으로 부모 뷰와 자식 뷰 형태의 계층 구조가 됨
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            VStack {
                Text("Text1")
                Text("Text2")
                HStack{
                    Text("Text3")
                    Text("Text4")
                }
            }
            Text("Text5")
        }
    }
}

 

 

 

 

 

  • 컨테이너에 포함된 여러 뷰를 연결하면 하나의 뷰로 간주됨
import SwiftUI

struct ContentView: View {
    var body: some View {        
        Text("Hello, ") + Text("how ") + Text("are you?")
    }
}

→ body 프로퍼티의 클로저는 반환 구문이 없음 : 단일 표현식이기 때문

 

 

 

 

  • 클로저에 별도의 표현식이 추가되면 return 구문 추가해야 함
import SwiftUI

struct ContentView: View {
    var body: some View {    
        var myString = "Welcome to SwiftUI"

        return VStack{
            Text("Hello, world!")
            Text("Goodbye, world!")
        }
    }
}

 

 

 

 

하위 뷰로 작업하기

  • 뷰는 최대한 작고 가볍게 → 재사용 가능한 컴포넌트 생성 권장
  • 뷰 선언부를 더 쉽게 관리하여 레이아웃이 더 효율적으로 랜더링 가능
  • 이전 코드에서 HStack 뷰를 MyHStackView라는 이름의 하위 뷰로 나눌 수 있음
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            VStack {
                Text("Text1")
                Text("Text2")
                MyHStackView()
            }
            Text("Text5")
        }
    }
}

struct MyHStackView : View{
    var body: some View {
        HStack {
            Text("Text3")
            Text("Text4")
        }
    }
}

 

 

 

프로퍼티로서의 뷰

  • 하위 뷰 생성 외에도 뷰를 계층구조하는 방법
  • 프로퍼티를 뷰에 할당
struct ContentView : View {
    var body: some View {
        VStack {
            Text("Main Title")
                .font(.largeTitle)
            HStack{
                Text("Car Image")
                Image(systemName: "car.fill")
            }
        }
    }
}

→ 위와 같은 구조에서 선언부 일부를 프로퍼티 값으로 이동하고 이름으로 참조

 

 

 

  • HStack을 carStack 이름의 프로퍼티에 할당하고 참조
struct ContentView : View {
    
    let carStack = HStack{
        Text("Car Image")
        Image(systemName: "car.fill")
    }
    
    var body: some View {
        
        VStack {
            Text("Main Title")
                .font(.largeTitle)
            carStack
        }
    }
}

 

 

 

 

 

 

뷰 변경

  • 수정자(modifier)를 뷰에 적용하여 커스터마이징 가능
  • 수정자는 뷰의 인스턴스에서 호출하는 메서드 형태를 취함
  • 원래의 뷰를 다른 뷰를 감싸는 방식으로 수정자들이 연결되어 뷰를 변경함
// 폰트와 포그라운드 색상 변경
Text("Main Title")
    .font(.headline)
    .foregroundStyle(.red)

// 이미지 뷰가 허용하는 공간 안에 이미지를 정배율로 표현
Image(systemName: "car.fill")
     .resizable()
     .aspectRatio(contentMode: .fit)

// 커스텀 뷰의 Text 뷰들의 폰트를 largeTitle 폰트로 변경
MyHStackView()
    .font(.largeTitle)

 

 

 

 

텍스트 스타일로 작업

  • 앞의 예제에서는 텍스트 폰트를 내장된 스타일은 largeTitle을 사용함
  • 텍스트 크기를 사용자가 선택 가능
  • settings → display & Brightness → Text Size 에서 슬라이더로 조정 가능
  • 제공되는 스타일
    • headline
    • subheadline
    • body
    • callout
    • footnote
    • caption

 

 

  • 커스텀 폰트 → 사용자가 선택한 텍스트 크기와 상관없이 고정된 크기
struct ContentView : View {
    
    var body: some View {
        Text("Sample Text")
            .font(.custom("Copperplate", size: 70))
    }
}

 

 

 

수정자 순서

  • 수정자들의 순서가 중요함
Text("Sample Text")
            .border(Color.black)
            .padding()

→ 패딩을 적용시켜서 텍스트와 경계선 사이의 간격을 기대했지만, 없음

 

 

  • 패딩은 border 경계선 밖에 적용되었기 때문 → 순서를 바꿔야 됨
Text("Sample Text")
            .padding()
            .border(Color.black)

 

 

 

 

 

 

커스텀 수정자

  • 다음의 수정자는 뷰 선언부에 공통으로 필요한 수정자라고 가정
var body: some View {
        Text("Text 1")
            .font(.largeTitle)
            .background(.white)
            .border(.gray, width: 0.2)
            .shadow(color: .black, radius: 5, x: 0, y: 5)
}

 

 

  • 위와 같은 수정자 4개가 항상 필요할 때 계속 적용하는 것보다 이 커스텀 수정자를 묶어서 참조 가능함
  • 커스텀 수정자는 ViewModifier 프로토콜을 따르는 구조체로 선언 가능
struct StandardTitle : ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.largeTitle)
            .background(.white)
            .border(.gray, width: 0.2)
            .shadow(color: .black, radius: 5, x: 0, y: 5)
    }
}
var body: some View {
        Text("Text 1")
            .modifier(StandardTitle())
        Text("Text 2")
            .modifier(StandardTitle())
}

 

 

 

이벤트 처리

  • Button 뷰는 버튼 내용과 함께 클릭이 감지될 때 호출될 메서드로 선언되어야 함
  • ex) 뷰 전체를 하나의 버튼으로 지정할 수 있음
  • 아래는 Text 뷰를 감싸는 Button 뷰로, 클릭하면 buttonPressed() 메서드 호출되는 코드
struct ContentView : View {
    var body: some View {
        Button(action: buttonPressed, label: {
            Text("Click Me")
        })
    }
    
    func buttonPressed(){
        // 동작 코드 
    }
}

 

 

 

  • 액션 함수 지정 대신 클로저 사용 가능
struct ContentView : View {
    var body: some View {
        // 클로저 사용
        Button(action: {
            // 동작 코드
        }, label: {
            Text("Click Me")
        })
    }
}

 

 

 

  • 이미지 뷰를 버튼으로 만드는 경우
struct ContentView : View {
    var body: some View {
        Button(action: {print("Hello")}, label: {
            Image(systemName: "square.and.arrow.down")
        })
    }
}

 

 

 

 

 

onAppear 메서드와 onDisappear 메서드

  • 레이아웃 안에 뷰가 나타나거나 사라질 때 초기화 작업과 해제 작업을 수행하기 위해 지정된 뷰에 액션 메서드를 선언하기도 함
struct ContentView : View {
    var body: some View {
        Text("Hello World")
            .onAppear(perform: {
                // 뷰가 나타날 때 수행될 코드
            })
            .onDisappear(perform: {
                // 뷰가 사라질 떄 수행될 코드
            })
    }
}

 

 

 

 

커스텀 컨테이너 뷰 만들기

  • 하위 뷰의 한계 : 컨테이너 뷰의 콘텐트가 정적(static)이라는 것
  • 하위뷰가 레이아웃에 포함되는 시점에 동적으로 지정할 수가 없음
  • 아래는 3개의 텍스트 뷰가 VStack 안에 포함되며 임의의 간격과 폰트 설정으로 된 하위 뷰
struct MyVStack: View {
    var body: some View {
        VStack(spacing: 10){
            Text("Text Item 1")
            Text("Text Item 2")
            Text("Text Item 3")
        }
        .font(.largeTitle)
    }
}

→ 폰트와 간격만 같게 하고 싶을 때 다른 뷰들을 생성하지 못한다는 단점 : 유연성이 떨어짐 → ViewBuilder 클로저 속성 이용

 

 

 

  • ViewBuilder
    • 클로저 형태
    • 하위 뷰로 구성된 커스텀 뷰를 만들 때 사용
    • 레이아웃 선언부 내에 사용될 때까지 내용을 선언할 필요가 없음
    • 콘텐트 뷰들을 받아서 동적으로 만들어진 단일 뷰로 반환함
// ViewBuilder 사용
struct MyVStack<Content: View>: View {
    let content : () -> Content
    init(@ViewBuilder content : @escaping () -> Content){
        self.content = content
    }
    var body: some View {
        VStack(spacing: 10){
            content()
        }
        .font(.largeTitle)
    }
}
  • View 프로토콜을 따름
  • body에는 VStack 선언부 포함
  • 스택에 정적 뷰를 포함하는 대신 하위 뷰들은 초기화 메서드에 전달됨
  • ViewBuilder에 의해 처리되어 VStack에 하위뷰들로 포함
  • 커스텀 MyStack 뷰는 레이아웃 내에 사용될 서로 다른 하위 뷰들로 초기화 가능
struct ContentView : View {
    var body: some View {
        MyVStack{
            Text("Text 1")
            Text("Text 1")
            HStack{
                Image(systemName: "star.fill")
                Image(systemName: "star.fill")
                Image(systemName: "star")
            }
        }
    }
}