Project10의 첫 번째 포스팅입니다.
영화 캐릭터 정보 앱
이번 프로젝트에서는 지브리 스튜디오의 캐릭터의 정보를 앱에 나타내면서 SwiftUI
기능 사용해보겠습니다.
뷰 구성하기
우선 아래 이미지 파일을 다운받아서 Assets
폴더에 업로드해주겠습니다.
그리고 위 이미지 같은 뷰를 작성해보겠습니다. 이 뷰는 HStack
이 최상위에서 뷰를 감싸고 있고 VStack
이 자식 뷰로 캐릭터 정보를 감싼 형태입니다.
// Home
HStack {
Image("Sosuke")
.resizable()
.scaledToFill()
.frame(width: 150)
.clipped()
VStack(alignment: .leading){
HStack {
Text("소스케")
.font(.headline)
.fontWeight(.medium)
Spacer()
Text("벼랑위의 포뇨")
.font(.footnote).foregroundColor(.gray)
}
.padding(.bottom, 10)
Text("5살 남자 아이. 본작품의 주인공. 벼랑 위에 있는 2~3층 정도 되는 주택에서 엄마와 함께 살고 있다.")
.font(.footnote)
.foregroundColor(.secondary)
.lineLimit(3)
Spacer()
// 1
HStack(spacing: 0) {
Text("₩").font(.footnote)
+ Text("214000").font(.headline)
Spacer()
Image(systemName: "heart")
.imageScale(.large)
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
Image(systemName: "cart")
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
}
}
// 2
.padding([.leading, .bottom], 10)
.padding([.trailing, .top])
}
.frmae(height: 150)
위 코드에서 1번을 보게 되면 Text(.init())
으로 symbol 이미지를 가져왔습니다. 물론 Image(systemName: "")
으로 가져 올 수도 있지만 Text
도 이런 방식으로 가져올 수 있습니다.
padding
을 사용하는 법은 다양하게 있지만 위에서는 적용하고 싶은 위치를 배열로 지정해 여백을 주고 있습니다.
화면을 보면 테두리도 없고 어딘가 부족해 보입니다. 여기서 background
와 cornerRadius
, shadow
를 넣어보도록 하겠습니다. 넣기 전에 한 가지 알아야 할 것이 있는데 바로 Color.primary
입니다. Color.primary
는 다크 모드와 라이트 모드일 때 자동적으로 색을 테마에 맞게 바꿔주는 효과를 가지고 있습니다.
// Home
HStack {
...
}
.frame(height: 150)
.background(Color.primary.colorInvert()) //다크모드 라이트모드
.cornerRadius(10)
.shadow(color: Color.primary.opacity(0.3), radius: 1, x: 2, y: 2)
.padding(.vertical, 10)
뷰 추출하기
이제 밑바탕이 될 뷰를 완성되었습니다. 복잡하지 않은 뷰 인데도 뭔가 너무 깁니다.
또 동일한 뷰를 여러 번 반복해서 작성하려면 그 길이가 더 길어지겠죠? 부분 수정을 할 때도 힘들 테고요😭
이럴 땐 각 항목을 별도의 프로퍼티나 뷰로 추출해서 뷰의 핵심인 main body
에서는 간결하게 유지하는 것이 가장 좋습니다.
위에 뷰를 보기 쉽게 나눠보도록 하겠습니다.
뷰 나누기
1. 최상위 뷰 인 HStack
에 커맨드 + 클릭해서 VStack
감싸줍니다.
2. HStack
에 다시 한번 커멘드 + 클릭해서 [Extract SubView
]를 선택합니다.
3. ExtractedView()
의 이름을 ProductRow()
라고 재지정해주고, Home
의 body
프로퍼티에 ProductRow
를 3번 반복되도록 작성하겠습니다.
// Home
VStack {
ProductRow()
ProductRow()
ProductRow()
}
그럼 다음 그림과 같은 결과를 볼 수 있게 됩니다. ProductRow
만 불러오면 쉽게 재사용을 할 수 있다는 말이죠. 앞으로 이 기능을 적극적으로 사용해보도록 하겠습니다.
ProductRow 파일 생성
이제 ProductRow
라는 새로운 파일로 위 코드를 옮겨서 저장하도록 하겠습니다. [SwiftUI View
] 템플릿을 선택해서 ProductRow.swift
라고 이름을 정해주고 Home
그룹 안으로 넣어줍니다.
여기까지 했으면 Home
에는 ProductRow()
가 세 번 반복되는 코드만 있으면 됩니다.
// Home View
struct Home: View {
var body: some View {
VStack {
ProductRow()
ProductRow()
ProductRow()
}
}
}
프로퍼티로 추출
ProductRow
의 뷰도 세밀하게 나누어 보도록 하겠습니다. 이번에는 [Extract SubView
]로 추출 하지 않습니다.
1. 캐릭터 이미지
캐릭터 이미지를 [productImage
] 프로퍼티로 추출합니다. 그리고 body
에서는 productImage
를 호출해줍니다.
//ProductRow
var productImage: some View {
Image("Sosuke")
.resizable()
.scaledToFill()
.frame(width: 150)
.clipped()
}
2. 캐릭터 정보
이번에는 VStack
을 [ProductDescription
] 프로퍼티라고 이름짓고 코드를 전부 옮겨줍니다.
// Home
HStack {
Image("Sosuke")
.resizable()
.scaledToFill()
.frame(width: 150)
.clipped()
VStack(alignment: .leading){
HStack {
Text("소스케")
.font(.headline)
.fontWeight(.medium)
Spacer()
Text("벼랑위의 포뇨")
.font(.footnote).foregroundColor(.gray)
}
.padding(.bottom, 10)
Text("5살 남자 아이. 본작품의 주인공. 벼랑 위에 있는 2~3층 정도 되는 주택에서 엄마와 함께 살고 있다.")
.font(.footnote)
.foregroundColor(.secondary)
.lineLimit(3)
Spacer()
// 1
HStack(spacing: 0) {
Text("₩").font(.footnote)
+ Text("214000").font(.headline)
Spacer()
Image(systemName: "heart")
.imageScale(.large)
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
Image(systemName: "cart")
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
}
}
// 2
.padding([.leading, .bottom], 10)
.padding([.trailing, .top])
}
.frmae(height: 150)
잊지 말고 body
에 ProductDescription
을 추가해줘야 합니다.
현재까지의 body
화면입니다.
// ProductRow
var body: some View {
HStack {
productImage
productDescription
}
...
}
3. 캐릭터 정보 세분화
ProductDescription
을 한번 더 구분하여 나누어 주겠습니다. 해당 프로퍼티에서 HStack
을 떼어 내서 footerView
라고 이름 지어줍니다.
// ProductRow
var footerView: some View {
HStack(spacing: 0) {
Text("₩").font(.footnote)
+ Text("214000").font(.headline)
Spacer()
Image(systemName: "heart")
.imageScale(.large)
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
Image(systemName: "cart")
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
}
}
// ProductDescription
var productDescription: some View {
VStack(alignment: .leading){
Text("소스케")
...
footerView
}
.padding([.leading, .bottom], 10)
.padding([.trailing, .top])
}
4. 현재까지의 형태
여기까지 했다면 코드 형태는 다음과 같습니다.
// ProductRow
struct ProductRow: View {
var body: some View {
HStack {
productImage
productDescription
}
.frame(height: 150)
.background(Color.primary.colorInvert()) //다크모드 라이트모드
.cornerRadius(10)
.shadow(color: Color.primary.opacity(0.3), radius: 1, x: 2, y: 2)
.padding(.vertical, 10)
}
var productImage: some View {...}
var productDescription: some View {...}
var footerView: some View {...}
}
결과는 동일하지만 이렇게 뷰를 나누게 되면 body
뷰에서는 전체 레이아웃이 어떤 형태를 가지고 있는지 한눈에 쉽게 알 수 있고, 개별 뷰에 대해서 상세하고 살펴볼 필요가 있을 때는 집중에서 볼 수 있습니다.
5. Extension
한 단계 나아가서 ProductRow
에 대한 익스텐션을 만들어 확장시켜주고 body
를 제외한 나머지 프로퍼티들을 옮겨주겠습니다.
// ProductRow
private extension ProductRow {
var productImage: some View {...}
var productDescription: some View {...}
var footerView: some View {...}
}
6. 최종 형태
// ProductRow
struct ProductRow: View {
var body: some View {...}
}
private extension ProductRow {
var productImage: some View {...}
var productDescription: some View {...}
var footerView: some View {...}
}
//프리뷰
struct ProductRow_preview: PreviewProvider {
static var previews: some View {
ProductRow()
}
}
이렇게 나누는 이유는 기본 타입에서는 프로퍼티를 정의하고 뷰 프로토콜의 핵심인 body
를 구현하는 작업만 담당하고, 그 외에는 확장 영역으로 명확하게 구분해 줌으로써 앞으로 코드를 관리하게 수월하게 하도록 하기 위함입니다. 기능의 차이는 전혀 없습니다.
전체코드
// ProductRow
<hide/>
import SwiftUI
struct ProductRow: View {
var body: some View {
HStack {
productImage
productDescription
}
.frame(height: 150)
.background(Color.primary.colorInvert()) //다크모드 라이트모드
.cornerRadius(10)
.shadow(color: Color.primary.opacity(0.3), radius: 1, x: 2, y: 2)
.padding(.vertical, 10)
}
}
private extension ProductRow {
var productImage: some View {
Image("Sosuke")
.resizable()
.scaledToFill()
.frame(width: 150)
.clipped()
}
var productDescription: some View {
VStack(alignment: .leading){
HStack {
Text("소스케")
.font(.headline)
.fontWeight(.medium)
Spacer()
Text("벼랑위의 포뇨")
.font(.footnote).foregroundColor(.gray)
}
.padding(.bottom, 10)
Text("5살 남자 아이. 본작품의 주인공. 벼랑 위에 있는 2~3층 정도 되는 주택에서 엄마와 함께 살고 있다.")
.font(.footnote)
.foregroundColor(.secondary)
.lineLimit(3)
Spacer()
footerView
}
.padding([.leading, .bottom], 10)
.padding([.trailing, .top])
}
var footerView: some View {
HStack(spacing: 0) {
Text("₩").font(.footnote)
+ Text("214000").font(.headline)
Spacer()
Image(systemName: "heart")
.imageScale(.large)
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
Image(systemName: "cart")
.foregroundColor(.lightRed)
.frame(width: 32, height: 32)
}
}
}
struct ProductRow_preview: PreviewProvider {
static var previews: some View {
ProductRow()
}
}
읽어주서서 감사합니다🤟
'PROJECT > Simple' 카테고리의 다른 글
SwiftUI Project10 : 영화 캐릭터 정보 앱 #3 - JSON (0) | 2021.04.19 |
---|---|
SwiftUI Project10 : 영화 캐릭터 정보 앱 #2 - 모델생성 (0) | 2021.04.17 |
SwiftUI Project9 : CustomTabView (geometryReader) (0) | 2021.03.26 |
SwiftUI Project8 : WebView and Image (0) | 2021.03.21 |
SwiftUI Project7 : Stack / ScrollView / Link (0) | 2021.03.11 |