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

SwiftUI Project10 : 영화 캐릭터 정보 앱 #4 - 디테일뷰

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

 

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

 

디테일 화면 구현하기

전 포스팅에서 내비게이션 링크까지 연결을 했습니다. 이제 실제로 상세 화면을 구현할 차례입니다. 

 

[SwiftUI View] 템플릿을 통해서 ProductDetailView.swift라고 이름 짓고 다음 코드를 채워 넣어주겠습니다. 우선 아래 코드를 넣어주고 하나하나씩 구현하도록 할게요.

//  ProductDetailView.swift

import SwiftUI

struct ProductDetailView: View {
    // 정보를 전달받기 위한 프로퍼티 선언
    let product: Product
    
    var body: some View {
        VStack(spacing: 0) {
            productImage //영화의 이미지가 올 부분
            orderView // 영화 정보를 출력하고 그 영화를 구매 하기 위한 뷰
        }
        .edgesIgnoringSafeArea(.top)
    }
}

edgesIgnoringSafeArea(.top)수식어를 넣어서 윗부분까지 채워주도록 했고, 이제 productImage의 뷰를 표현해보겠습니다.

productImage 

영화의 이미지를 나타내는 부분

//  ProductDetailView.swift

var body: some View {
        VStack {
            productImage //영화의 이미지가 올 부분
            orderView // 영화 정보를 출력하고 그 영화를 구매 하기 위한 뷰
        }
        .edgesIgnoringSafeArea(.top)
    }
}

private extension ProductDetailView {
    var productImage: some View {
            Image(self.product.ImageName)
                .resizable()
                .scaledToFill()
    }
}

orderView

orderView에는 여러 코드가 들어가기 때문에 역할에 따라 여러 개의 뷰로 나누어 주도록 하겠습니다.

//  ProductDetailView.swift

private extension ProductDetailView {

    var productImage: some View {
            Image(self.product.ImageName)
                .resizable()
                .scaledToFill()
        }
    }
    var orderView: some View {
        GeometryReader {
            VStack(alignment: .leading) {
                self.productDescripsion //영화제목과 즐겨찾기 버튼 이미지 추가
                Spacer()
                // 아래 코드는 잠시 주석 처리
                //                self.priceInfo // 가격
                //                self.orderButton // 주문버튼
            }
            .frame(height: $0.size.height + 10)
            .padding(30)
            .background(Color.white)
            .cornerRadius(20)
            .shadow(color: Color.black.opacity(0.4), radius: 10, x: 0, y: -5)
        }
    }

ProductDescription

자 이제 productDescripsion부분을 구현해주겠습니다. 이곳에는 캐릭터명, 즐겨찾기 버튼, 설명 등을 표시하는 뷰가 됩니다.

 var productDescripsion: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text(product.name)
                    .font(.largeTitle)
                    .foregroundColor(.black)
                Spacer()
                Image(systemName: "heart") // 즐겨찾기 버튼
                    .font(.title2)
                    .foregroundColor(.lightRed)
                    .frame(width: 32, height: 32)
            }
            Text(product.description) //상품 설명
                .foregroundColor(.gray)
        }
    }
}

한 가지 함수를 작성해줘야 하는데 한 문장으로 길게 된 텍스트를 화면에 적절하게 나태 나게 위해 두 줄로 나누어 주는 기능을 수행하도록 하겠습니다. 이 코드는 아래 게시글에서 자세하게 확인할 수 있습니다. 이번 포스팅에서는 설명하지 않겠습니다. :)

 

 

아래와 같이 함수를 작성해주고 Text 부분을 Text(splitText(product.description))으로 수정해줍니다.

//  ProductDetailView.swift

import SwiftUI


struct ProductDetailView: View { ... }

private extension ProductDetailView {
    
    // MARK : View
    var productImage: some View { ...}
    
    var orderView: some View { ... }
    
    var productDescripsion: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                ...
            }
            Text(splitText(product.description))
                .foregroundColor(.gray)
        }
    }
    /* 한 문장으로 길게 구성된 텍스트를 화면에 적장하게 나타내기위해 두 줄로 나누워 주는 기능 */
    
    func splitText(_ text: String) -> String {
        guard !text.isEmpty else { return text }
        let centerIdx = text.index(text.startIndex, offsetBy: text.count / 2)
        let centerSpaceIdx = text[..<centerIdx].lastIndex(of: " ")
            ?? text[centerIdx...].firstIndex(of: " ")
            ?? text.index(before: text.endIndex)
        let afterSpaceIdx = text.index(after: centerSpaceIdx)
        let lhsString = text[..<afterSpaceIdx].trimmingCharacters(in: .whitespaces)
        let rhsString = text[afterSpaceIdx...].trimmingCharacters(in: .whitespaces)
        return String(lhsString + "\n" + rhsString)
    }
}

현재까지의 코드

<hide/>
//  ProductDetailView.swift

import SwiftUI

struct ProductDetailView: View {
    // 정보를 전달받기 위한 프로퍼티 선언
    let product: Product
    
    // MARK : Body
    var body: some View {
        VStack(spacing: 0) {
            productImage // 이미지가 올 부분
            orderView // 영화 정보를 출력하고 그 상품을 주문하기 위한 뷰
        }
        .edgesIgnoringSafeArea(.top)
    }
}

private extension ProductDetailView {
    
    // MARK : View
    var productImage: some View {
            Image(self.product.ImageName)
                .resizable()
                .scaledToFill()
    }
    var orderView: some View {
        GeometryReader {
            VStack(alignment: .leading) {
                self.productDescripsion //영화제목과 즐겨찾기 버튼 이미지 추가
                Spacer()
//                self.priceInfo // 가격
//                self.orderButton // 주문버튼
            }
            //geometryReader가 차지하고있는 뷰 높이보다 VStack의 높이가 10 크도록 지정해줌
            .frame(height: $0.size.height + 10)
            .padding(30)
            .background(Color.white)
            .cornerRadius(20)
            .shadow(color: Color.black.opacity(0.4), radius: 10, x: 0, y: -4)
        }
    }
    var productDescripsion: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {
                Text(product.name)
                    .font(.largeTitle)
                    .foregroundColor(.black)
                Spacer()
                Image(systemName: "heart") // 즐겨찾기 버튼
                    .font(.title2)
                    .foregroundColor(.lightRed)
                    .frame(width: 32, height: 32)
            }
            Text(splitText(product.description))
                .foregroundColor(.gray)
        }
    }
    
    // MARK : Func
    /* 한 문장으로 길게 구성된 텍스트를 화면에 적장하게 나타내기위해 두 줄로 나누워 주는 기능 */
    
    func splitText(_ text: String) -> String {
        guard !text.isEmpty else { return text }
        let centerIdx = text.index(text.startIndex, offsetBy: text.count / 2)
        let centerSpaceIdx = text[..<centerIdx].lastIndex(of: " ")
            ?? text[centerIdx...].firstIndex(of: " ")
            ?? text.index(before: text.endIndex)
        let afterSpaceIdx = text.index(after: centerSpaceIdx)
        let lhsString = text[..<afterSpaceIdx].trimmingCharacters(in: .whitespaces)
        let rhsString = text[afterSpaceIdx...].trimmingCharacters(in: .whitespaces)
        return String(lhsString + "\n" + rhsString)
    }
}
// MARK : Preview

struct ProductDetailView_Previews: PreviewProvider {
    static var previews: some View {
        ProductDetailView(product: ProductList[0])
    }
}

PriceInfo

이제 orderView에서 주석 처리한 priceInfo부분을 활성화시킨 뒤, 아래와 같이 코드를 작성해주겠습니다. productprice부분은 Int 정수 이기 때문에 Text("product.price")로 바로 가져오게 되면 컴파일 오류가 생깁니다. 그렇기 때문에 반드시 문자열 보간을 사용하여 호출해야 합니다.

//  ProductDetailView.swift

var priceInfo: some View {
    HStack {
        Text("₩") + Text("\(product.price)").font(.title).fontWeight(.medium)
        Spacer()
    }
    .foregroundColor(.black)
}

orderButton

주문을 위한 버튼을 만들어 보겠습니다. 마찬가지로 orderView에서 주석 처리한 부분을 활성화시켜줍니다.

//  ProductDetailView.swift

var orderButton: some View {   // 주문하는 버튼
    Button(action: {
        // some action
    }) {
        Capsule()
            .fill(Color.lightRed)
            // 너비는 주어진 공간을 최대로 사용, 높이는 최소30, 최대 60으로 지정
            .frame(maxWidth: .infinity, minHeight: 30, maxHeight: 60)
            .overlay(Text("주문하기")
                        .font(.system(size: 23)).fontWeight(.medium)
                        .foregroundColor(.white))
            
            .padding(.vertical, 10)
    }
}

PriewView / Home View 코드 수정

프리뷰와 Home뷰 에도 아래와 같이 코드를 추가해줘야 정상적으로 실행이 됩니다.

//  ProductDetailView.swift

struct ProductDetailView_Previews: PreviewProvider {
    static var previews: some View {
        ProductDetailView(product: ProductList[0])
    }
}
import SwiftUI

struct Home: View {
    let movie: Movie
    
    var body: some View {
        NavigationView {
            List(movie.products) { product in
                NavigationLink(
                    destination: ProductDetailView(product: product)
                    //내비게이션 back 버튼 숨김
                    .navigationBarBackButtonHidden(true)) {
                    ProductRow(product: product)
                }
            }
            .navigationBarTitle("지브리 스튜디오")
            
        }
    }
}

앱을 실행해보면 크기가 제각각 다른 것을 확인할 수 있죠? 이유는 이미지 크기가 전부 동일하지 않기 때문입니다. 이것을 해결하려면 productImage프로퍼티에서 이미지를 GeometryReader로 감싸줘야 합니다.

productImage

//  ProductDetailView.swift

var productImage: some View {
    GeometryReader{ geo in
        Image(self.product.ImageName)
            .resizable()
            .scaledToFill()
           // 이미지 중앙을 기준으로 양옆을 자름
            .frame(width: geo.size.width, height: geo.size.height)
            .clipped()
            
    }
}

이제 모든 뷰가 동일한 크기로 구현되었고, 위 코드에서 추가한 frame 부분을 한번 비교해보도록 하겠습니다. 

왼 : scaleToFill()만 적용   /   오 : Geometry Frame을 적용

 

GitHub 현재 까지의 코드 확인

10. 디테일뷰 완성

 

읽어주셔서 감사합니다🤟

 

 

 


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


서근


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