궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
PROJECT/Simple

SwiftUI Project10 : 영화 캐릭터 정보 앱 #1

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

 

 

Project10의 첫 번째 포스팅입니다.

영화 캐릭터 정보 앱

이번 프로젝트에서는 지브리 스튜디오의 캐릭터의 정보를 앱에 나타내면서 SwiftUI 기능 사용해보겠습니다.

뷰 구성하기

우선 아래 이미지 파일을 다운받아서 Assets 폴더에 업로드해주겠습니다.

캐릭터.zip
1.84MB

그리고 위 이미지 같은 뷰를 작성해보겠습니다. 이 뷰는 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을 사용하는 법은 다양하게 있지만 위에서는 적용하고 싶은 위치를 배열로 지정해 여백을 주고 있습니다.

화면을 보면 테두리도 없고 어딘가 부족해 보입니다. 여기서 backgroundcornerRadius, 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]를 선택합니다. 

Subview로 추출된 화면

3. ExtractedView()의 이름을 ProductRow()라고 재지정해주고, Homebody 프로퍼티에 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)

잊지 말고 bodyProductDescription을 추가해줘야 합니다.

 

현재까지의 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()
    }
}

 

읽어주서서 감사합니다🤟

 

 

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.