SwiftUI 공식문서의 내용을 토대로 정리한 게시글 입니다.
SwiftUI의 UI 를 구성하는데 사용되는 중요한 요소중에는 View와 Modifier가 있다.
뷰(View)
버튼, 네비게이션 바, 텍스트, 이미지, 프로그레스 바 등 눈에 보이는 것들을 말한다. 다른 뷰들을 포함하는 컨테이너 뷰 역할도 할 수 있다.
View 프로토콜을 준수하는 구조체(struct)로 구현되어있다. 이 프로토콜을 준수하려면 body 프로퍼티를 포함해야 한다.
대표적으로 Text(), Image() 등이 있다.
수정자 (Modifiers)
뷰의 모양, 레이아웃, 동작 등을 변경하는데 사용되는 메소드이다.
View 프로토콜을 준수하는 모든 뷰에 적용될 수 있고, 적용 시 새로운 View 인스턴스를 반환한다.
대표적으로 .frame() .padding() 등이 있다.
View 종류를 설명하기 전에, 모든 View 에 주로 사용하는 Modifier 들을 먼저 정리하고
특정 뷰에서만 주로 사용하는 것은 따로 설명하겠다.
크기 조절: .frame(width: [너비], height: [높이])
뷰의 너비와 높이를 지정한다.
패딩 추가: .padding([.edges], [길이])
뷰 주변에 공간을 추가한다. 방향을 지정하지 않으면 모든 방향에 패딩이 적용되고 길이를 지정하지 않으면 기본값이 사용된다.
배경색 설정: .background([색상], alignment: [정렬])
뷰의 배경에 색상을 설정한다. 이미지를 배경으로 할 수도 있다.
요소 색 설정: .foregroundColor([색상])
뷰의 텍스트, 아이콘, 벡터, 버튼 등 요소들의 색상을 설정한다.
위치 조정: .offset(x: X축 오프셋, y: Y축 오프셋)
뷰의 위치를 X축과 Y축 방향으로 이동시킨다. 이는 뷰의 실제 위치를 변경하지 않고, 시각적으로만 이동시킨다.
회전 효과: .rotationEffect(.degrees([각도]))
뷰를 주어진 각도만큼 회전시킨다.
투명도 조정: .opacity([값])
뷰의 투명도를 설정한다. 값은 0.0(투명) ~ 1.0(불투명) 사이이다.
그림자 추가: .shadow(color: [색상], radius: [반경], x: [X축 오프셋], y: [Y축 오프셋])
뷰에 그림자를 추가한다.
테두리 추가: .border([Color], width: [너비])
뷰의 외곽에 테두리를 추가한다.
모양대로 자르기: .clipShape([Shape])
뷰의 모양을 지정한 Shape으로 잘라낸다. ex) .clipShape(Circle())
탭했을때 액션: .onTapGesture { 액션 }
뷰를 탭 가능한 인터랙티브 요소로 만들어 사용자와의 상호작용을 가능하게 한다. 탭 할 시 액션 부분에 작성한 코드가 실행된다.
나타났을 때 액션: onAppear { 액션 }
뷰가 화면에 나타났을 때 실행할 액션을 만들 수 있다.
표준 폰트 설정: .font(종류)
.title, .headline, .body 등이 있다.
SwiftUI 가 제공하는 표준 폰트 사이즈이다. 시스템 설정에 따라 자동으로 조정될 수 있어 접근성이 좋다.
커스텀 폰트 설정: .font(.system(size: 24, weight: .bold, design: .rounded))
커스텀으로 폰트 굵기, 사이즈, 디자인 등을 지정할 때 사용하면 된다.
굵기 변경: .fontWeight(굵기)
.light, regular, .semibold, .bold 등이 있다.
텍스트 정렬: .multilineTextAlignment(기준)
.center, .leading , .trailing 이 있다.
텍스트 최소크기 지정: .minimumScaleFactor(비율)
텍스트가 주어진 공간에 맞게 줄어들수 있도록 한다. 0.25 로 지정시 최소 25% 까지 크기가 축소 될 수 있음을 의미한다. 위젯이나 워치등 작은 화면의 뷰를 만들 때 반응형으로 만들 때 유용하다.\
최대 줄 수 지정: .lineLimit(줄 수)
텍스트 뷰가 표시할 수 있는 텍스트의 줄 수를 제한할 수 있다. 제한이 넘어가면 잘릴 수 있다.

struct WelcomeMessageView: View {
var body: some View {
Text("닉네임과 프로필 사진을 등록해주세요")
.font(.system(size: 24, weight: .bold)) // 커스텀 폰트 설정
.foregroundColor(.black) // 글자색 설정
.multilineTextAlignment(.center) // 텍스트 중앙 정렬
.frame(width: 300) // 너비 제한
}
}
이미지를 보여주는 뷰이다.
Image(이름)
프로젝트 파일 내부의 Assets 안에 넣은 사진의 이름을 문자열로 적으면 된다.
크기 조절 가능하게 하기: .resizeable()
이 메소드를 써줘야 frame 으로 크기 조절이 가능해진다.
이미지 비율 유지: .scaledToFit() 또는 .scaledToFill()
.scaledToFit()은 이미지의 원본 비율을 유지하면서 주어진 공간에 맞추어 이미지를 조절한다. 이미지가 주어진 공간을 모두 채우지 않을 수 있습니다.
.scaledToFill()도 이미지의 원본 비율을 유지하면서, 이미지가 주어진 공간을 완전히 채우도록 조절합니다. 이미지의 일부가 잘릴 수 있다.

struct ProfileImageView: View {
let frameSize: CGFloat = 140
let borderColor: Color = .gray
let borderWidth: CGFloat = 4
let imageName: String
var body: some View {
ZStack {
Image(imageName)
.resizable()
.scaledToFit()
.clipShape(Circle()) // 원형으로 이미지를 자름
.overlay {
Circle().stroke(borderColor, lineWidth: borderWidth)
}
.frame(width: frameSize, height: frameSize)
}
}
}
//..
ProfileImageView(imageName: "exampleProfile") // 와 같이 사용
Circle()
.fill(Color.red)
Circle()
.fill(Color.gray)
.frame(width: 140, height: 140)
Circle()
.stroke(.black, lineWidth: 4)
.frame(width: 100) // 하나만 정해줘도 됨
RoundedRectangle()
모서리를 둥글게 할 수 있는 사각형이다.
RoundedRectangle(cornerRadius: 5)
RoundedRectangle(cornerRadius: 5)
.fill(Color.gray)
.frame(width: 300, height: 60)
VStack HStack ZStack
순서대로 수직, 수평, z축 방향으로 subviews 를 쌓는다.
VStack(spacing: 30)
내부의 subviews 간의 간격을 조절한다.
ZStack(alignment: .bottomLeading)
내부 subviews의 정렬을 조절한다. 스택 마다 다른 종류가 있고, 기본값은 center 이다.
HStack(spacing: 18) {
Image("profileImage")
Text("Ace")
VStack {
// ..
}
// ..
}
Spacer()
사용 가능한 전체 공간을 확장한다.
여러개 사용할 경우 사용 가능한 전체 공간을 1/n 하여 차지한다.
Spacer(minLength: 100)
VStack {
Text("Hi")
Spacer()
.frame(height: 40)
Text("닉네임과 프로필 사진을 등록해주세요")
Spacer()
}
사용자가 클릭하고 특정 액션을 행동하는 버튼을 만들 수 있다.
Button(action: {
// 버튼 클릭시 실행할 액션
}) {
// 버튼 모양을 나타낼 뷰
}
.disabled(조건)
Button(action: {
isReady.toggle() // 변수 토글
}) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(isReady ? Color.green : Color.gray) // 조건에 따른 색상 변경
.frame(width: 300, height: 60)
Text(user.name)
.foregroundStyle(.black) // 텍스트 색상 지정
.font(.title2) // 폰트 스타일 지정
}
}
작업의 진행 상태를 표시하는 데 주로 사용한다.
(다운로드, 로딩, 작업 진행도 등)

ProgressView("로딩 중..")

struct GameProgressBarView: View {
var currentIndex: Int // 현재 인덱스. 이 값은 상위 뷰 에서 매번 전달받아서 업데이트 된다.
var totalCount: Int // 총 카운트
var body: some View {
ProgressView(value: Float(currentIndex), total: Float(totalCount))
.progressViewStyle(LinearProgressViewStyle(tint: Color.blue)) // 프로그레스 바의 색상 지정
.scaleEffect(x: 1, y: 2, anchor: .center) // 프로그레스 바의 높이 조절
.animation(.linear(duration: 0.5), value: currentIndex)
.padding() // 적절한 패딩 추가
}
}
값의 범위를 게이지로 표시 하는데 사용된다.
(배터리 레벨, 속도계, 온도계 등)
Gauge(value: speed, in: 0...100) {
Text("Speed")
}
ScrollView {}
스크롤 뷰는 스크롤 가능한 콘텐츠 영역 내에 콘텐츠를 표시한다.
.vertical, .horizontal
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(0..<100) {
Text("Row \($0)")
}
}
}
}
다른 화면을 불러올 때 사용하는 컨테이너 뷰이다.
루트 뷰 위에 다른 뷰 스택을 쌓는다.
NavigationLink 를 사용하여 스택 상단에 뷰를 쌓을 수 있고, 자동으로 생성되는 뒤로가기나 스와이프 동작으로 뷰를 제거할 수 있다. (루트 뷰는 제거 불가능)
NavigationStack {
// ..
NavigationLink(destination: ScrollViewTest()) {
// 다음 페이지로 이동하는 버튼의 뷰
}
}

struct ViewTest: View {
var body: some View {
NavigationStack {
VStack {
Spacer()
.frame(height: 40)
Text("닉네임과 프로필 사진을 등록해주세요")
.font(.system(size: 24, weight: .bold))
.multilineTextAlignment(.center)
.frame(width: 300)
Spacer()
.frame(height: 50)
NavigationLink(destination: ScrollViewTest()) {
ZStack {
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 100, height: 50)
Text("다음")
.font(.title3)
.foregroundStyle(.black)
}
}
}
}
}
}
Tesk.sleep()
Task {
try await Task.sleep(nanoseconds: 500_000_000)
//.. 이 뒤에 넣는 코드는 0.5초 뒤에 실행된다.
}
// 블럭 밖은 비동기로 동시에 실행됨
withAnimation(애니메이션) {}
{} 블럭 내부의 값이 바뀌면, 그 값의 영향을 받는 모든 뷰에 지정한 애니메이션을 적용할 수 있다.
애니메이션: 애니메이션 종류 지정 가능.
.default: 기본.linear: 같은 속도.easeIn: 점점 빨라지는 속도.easeOut: 점점 느려지며 멈춤.spring: 스프링같은 애니메이션
struct ViewTest: View {
@State private var isAnimation = true
var body: some View {
VStack {
Rectangle()
.foregroundColor(isAnimation ? .green : .red)
.frame(width: 100, height: isAnimation ? 100 : 200)
.opacity(isAnimation ? 0.3 : 1)
.offset(x: isAnimation ? 0 : 50)
Button(isAnimation ? "나타나" : "사라져") {
withAnimation(.default) {
isAnimation.toggle()
}
}
}
}
}