원문 : App Design and Layout - Composing Complex Interfaces

Composing Complex Interfaces

Landmark에 대한 홈 화면은 각 카테고리에서 수평 스크롤하는 랜드마크가 있는 카테고리 목록을 보여줍니다. 기본 네비게이션을 만들어, 합성된 뷰가 어떻게 다른 기기 크기와 회전에 적용할 수 있는지 살펴 볼 것입니다.

이 프로젝트를 빌드하기 위해 다음 단계를 따르거나, 완성된 프로젝트를 다운로드 받아 탐색해보세요.

프로젝트 파일 다운로드

Section 1. 홈 뷰 추가하기(Add a Home View)

이제 랜드마크(Landmarks) 앱에 필요한 모든 뷰를 가지고 있으므로, 홈(뷰들을 통합하기 위한 뷰)을 추가할 때입니다. 홈 뷰는 다른 모든 뷰들을 포함할 뿐만 아니라, 랜드마크를 탐색하고 보여주는 것을 제공합니다.

1 단계

Home.swift라는 새 파일에 CategoryHome이라는 사용자정의 뷰를 만듭니다.

import SwiftUI

struct CategoryHome: View {
    var body: some View {
        Text("Landmarks Content")
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

2 단계

랜드마크 목록 대신에 새로운 CategoryHome 뷰가 보이도록 장면(scene) 델리게이터(delegate)를 수정합니다.

import SwiftUI
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Use a UIHostingController as window root view controller
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIHostingController(rootView: CategoryHome().environmentObject(UserData()))
        self.window = window
        window.makeKeyAndVisible()
    }
}

홈 뷰는 Landmark 앱의 루트 역할을 하므로, 모든 다른 뷰들을 보여주는 방법이 필요합니다.

3 단계

Landmark에서 다른 뷰들을 관리하기 위한 NavigationView를 추가합니다.

앱의 계층구조로 탐색하도록 만들기 위해 NavigationButton 인스턴스와 관련된 수식어와 함께 네비게이션 뷰를 사용합니다.

import SwiftUI

struct CategoryHome: View {
    var body: some View {
        NavigationView {
            Text("Landmarks Content")
        }
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

4 단계

네비게이션바의 제목을 Featured로 설정합니다.

import SwiftUI

struct CategoryHome: View {
    var body: some View {
        NavigationView {
            Text("Landmarks Content")
                .navigationBarTitle(Text("Featured"))
        }
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

Section 2. 카테고리 목록 만들기(Create a Categories List)

Landmark 앱은 모든 카테고리를 쉽게 탐색할수 있는 수직 열(column)으로 정렬된 별도의 행(row)으로 보여줍니다. 수직과 수평 스택으로 결합하고 목록에 스크롤을 추가하는 작업을 수행합니다.

1 단계

Dictionary 구조체의 init(grouping:by:) 초기화를 사용해서 카테고리를 그룹화하고 랜드마크의 category 속성을 입력합니다. 

시작 프로젝트 파일은 미리 정의된 샘플 랜드마크의 각 카테고리를 포함하고 있습니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var body: some View {
        NavigationView {
            Text("Landmarks Content")
                .navigationBarTitle(Text("Featured"))
        }
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

2 단계

List를 사용해서 Landmarks에 있는 카테고리를 보여줍니다.

Landmark.Category 케이스 이름은 목록에 있는 각 항목의 식별자 이며, 열거형이기 때문에 다른 카테고리들 간에 반드시 유일해야 합니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var body: some View {
        NavigationView {
            List {
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    Text(key)
                }
            }
            .navigationBarTitle(Text("Featured"))
        }
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

Section 3. 랜드마크의 행 추가(Add Rows of Landmarks)

Landmark는 각 카테고리를 수평 스크롤하는 행(row)을 보여줍니다. 행을 보여주는 새로운 뷰 타입을 추가하고나서, 새로운 뷰에 카테고리에 대한 모든 랜드마크를 보여줍니다.

https://docs-assets.developer.apple.com/published/1a4d5203bd/add-rows-landmarks.mp4

1 단계

한 행의 컨텐츠를 가질 새로운 사용자정의 뷰를 정의합니다.

이 뷰는 랜드마크와 함께 보여지는 랜드마크의 특정 카테고리에 대한 정보를 저장하기 위해 필요합니다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        Text(self.categoryName)
            .font(.headline)
    }
}

#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
    static var previews: some View {
        CategoryRow(
            categoryName: landmarkData[0].category.rawValue,
            items: Array(landmarkData.prefix(3))
        )
    }
}
#endif

2 단계

새로운 행(row) 타입에 카테고리 정보를 전달하기 위해 CategoryHome의 본문을 업데이트합니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var body: some View {
        NavigationView {
            List {
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    CategoryRow(categoryName: key, items: self.categories[key]!)
                }
            }
            .navigationBarTitle(Text("Featured"))
        }
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

3 단계

HStack에 있는 카테고리의 랜드마크를 보여줍니다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            ForEach(self.items) { landmark in
                Text(landmark.name)
            }
        }
    }
}

#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
    static var previews: some View {
        CategoryRow(
            categoryName: landmarkData[0].category.rawValue,
            items: Array(landmarkData.prefix(3))
        )
    }
}
#endif

4 단계

frame(width:height:)로 높이를 지정하고 스크롤뷰를 스택으로 감싸서 행의 공간을 제공합합니다.

더 큰 샘플링 데이터로 뷰 미리보기를 업데이트 하면 스크롤 동작이 옳은지 쉽게 확인할수 있습니다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(self.categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(showsHorizontalIndicator: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(self.items) { landmark in
                        Text(landmark.name)
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
    static var previews: some View {
        CategoryRow(
            categoryName: landmarkData[0].category.rawValue,
            items: Array(landmarkData.prefix(4))
        )
    }
}
#endif

Section 4. 홈 뷰 구성하기(Compose the Home View)

Landmarks 앱의 홈 페이지는 사용자가 상세보기를 위해 탭을 하기 전에 랜드마크에 대해 간단한 보여주는 것이 필요합니다.

카테고리와 기능 뷰에 대한 랜드마크의 익숙한 미리보기를 만들기 위해 뷰 작성과 조합하기(Creating and Combining Views)에서 만들었던 Landmark 뷰의 일부를 재사용합니다.

1 단계

CategoryRow 다음에 새로운 사용자정의 뷰 CategoryItem을 만들고 랜드마크 이름 텍스트를 저장하는 Text를 새로운 뷰로 교체합니다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(self.categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(showsHorizontalIndicator: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(self.items) { landmark in
                        CategoryItem(landmark: landmark)
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

struct CategoryItem: View {
    var landmark: Landmark
    var body: some View {
        VStack(alignment: .leading) {
            landmark
                .image(forSize: 155)
                .cornerRadius(5)
            Text(landmark.name)
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
    static var previews: some View {
        CategoryRow(
            categoryName: landmarkData[0].category.rawValue,
            items: Array(landmarkData.prefix(4))
        )
    }
}
#endif

2 단계

Home.swift에서, FeaturedLandmarks라는 간단한 뷰를 추가해서 isFeatured가 표시된 랜드마크만 보여줍니다.

나중에 튜토리얼에서 이 뷰를 대화형 회전메뉴로 바꿀 것입니다. 현재는, 축소되고 잘려진 미리보기 이미지와 함께 특정 랜드마크들 중 하나를 보여줍니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var featured: [Landmark] {
        landmarkData.filter { $0.isFeatured }
    }
    
    var body: some View {
        NavigationView {
            List {
                FeaturedLandmarks(landmarks: featured)
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    CategoryRow(categoryName: key, items: self.categories[key]!)
                }
            }
            .navigationBarTitle(Text("Featured"))
        }
    }
}

struct FeaturedLandmarks: View {
    var landmarks: [Landmark]
    var body: some View {
        landmarks[0].image(forSize: 250).resizable()
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

3 단계

두 종류의 랜드마크 미리보기에서 가장자리(edge) 인셋(inset)을 0으로 설정함으로써 컨텐츠가 화면 가장자리까지 확장될 수 있습니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var featured: [Landmark] {
        landmarkData.filter { $0.isFeatured }
    }
    
    var body: some View {
        NavigationView {
            List {
                FeaturedLandmarks(landmarks: featured)
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())
                
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    CategoryRow(categoryName: key, items: self.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
            }
            .navigationBarTitle(Text("Featured"))
        }
    }
}

struct FeaturedLandmarks: View {
    var landmarks: [Landmark]
    var body: some View {
        landmarks[0].image(forSize: 250).resizable()
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

Section 5. 섹션간에 네비게이션 추가하기(Add Navigation Between Sections)

이제 홈뷰에서 다른 카테고리된 랜드마크들이 모두 보이므로, 사용자가 앱내의 각 섹션으로 도달하는 방법이 필요합니다. 상세뷰, 즐겨찾기 목록, 사용자의 프로파일을 홈 뷰에서 모두 탐색이 가능하도록 하기위해 네비게이션과 프리젠테이션 API를 사용합니다.

1 단계

CategoryRow.swift에서, 기존 CategoryItem을 NavigationButton으로 감쌉니다.

카테고리 항목 자체는 버튼에 대한 라벨이고, 그 대상은 카드로 표현되는 랜드마크에 대한 랜드마크 상세 뷰입니다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(self.categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(showsHorizontalIndicator: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(self.items) { landmark in
                        NavigationButton(
                            destination: LandmarkDetail(
                                landmark: landmark
                            )
                        ) {
                            CategoryItem(landmark: landmark)
                        }
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

struct CategoryItem: View {
    var landmark: Landmark
    var body: some View {
        VStack(alignment: .leading) {
            landmark
                .image(forSize: 155)
                .cornerRadius(5)
            Text(landmark.name)
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
    static var previews: some View {
        CategoryRow(
            categoryName: landmarkData[0].category.rawValue,
            items: Array(landmarkData.prefix(4))
        )
    }
}
#endif

2 단계

renderingMode(_:)와 color(_:) 수식어를 적용해서 카테고리 항목들의 네비게이션 모양을 변경합니다.

네비게이션 버튼에 대한 라벨로 전달한 텍스트는 환경설정의 강조 색상을 사용해서 그려지고, 이미지는 템플릿 이미지로 그려질수 있습니다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(self.categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(showsHorizontalIndicator: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(self.items) { landmark in
                        NavigationButton(
                            destination: LandmarkDetail(
                                landmark: landmark
                            )
                        ) {
                            CategoryItem(landmark: landmark)
                        }
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

struct CategoryItem: View {
    var landmark: Landmark
    var body: some View {
        VStack(alignment: .leading) {
            landmark
                .image(forSize: 155)
                .renderingMode(.original)
                .cornerRadius(5)
            Text(landmark.name)
                .color(.primary)
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

#if DEBUG
struct CategoryRow_Previews: PreviewProvider {
    static var previews: some View {
        CategoryRow(
            categoryName: landmarkData[0].category.rawValue,
            items: Array(landmarkData.prefix(4))
        )
    }
}
#endif

3 단계

Home.swift에서, 탭바에 있는 프로필 아이콘을 탭한 후에 모달 뷰에서 사용자 프로필을 보여주는 프리젠테이션을 추가합니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var featured: [Landmark] {
        landmarkData.filter { $0.isFeatured }
    }
    
    var body: some View {
        NavigationView {
            List {
                FeaturedLandmarks(landmarks: featured)
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())
                
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    CategoryRow(categoryName: key, items: self.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
            }
            .navigationBarTitle(Text("Featured"))
            .navigationBarItems(trailing:
                PresentationButton(destination: Text("User Profile")) {
                    Image(systemName: "person.crop.circle")
                        .imageScale(.large)
                        .accessibility(label: Text("User Profile"))
                        .padding()
                }
            )
        }
    }
}

struct FeaturedLandmarks: View {
    var landmarks: [Landmark]
    var body: some View {
        landmarks[0].image(forSize: 250).resizable()
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

4 단계

랜드마크의 필터링 가능한 목록으로 이어지는 네비게이션 버튼을 추가해서 홈 화면을 마무리합니다.

import SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }
    
    var featured: [Landmark] {
        landmarkData.filter { $0.isFeatured }
    }
    
    var body: some View {
        NavigationView {
            List {
                FeaturedLandmarks(landmarks: featured)
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())
                
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    CategoryRow(categoryName: key, items: self.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
                
                NavigationButton(destination: LandmarkList()) {
                    Text("See All")
                }
            }
            .navigationBarTitle(Text("Featured"))
            .navigationBarItems(trailing:
                PresentationButton(destination: Text("User Profile")) {
                    Image(systemName: "person.crop.circle")
                        .imageScale(.large)
                        .accessibility(label: Text("User Profile"))
                        .padding()
                }
            )
        }
    }
}

struct FeaturedLandmarks: View {
    var landmarks: [Landmark]
    var body: some View {
        landmarks[0].image(forSize: 250).resizable()
    }
}

#if DEBUG
struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}
#endif

5 단계

LandmarkList.swift에서, 랜드마크 목록을 감싼 NavigationView를 제거하고 미리보기에 추가합니다.

앱의 맥락에서는, LandmarkList가 항상 Home.swift 안에 선언된 네비게이션 뷰 안에서 표현됩니다.

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject var userData: UserData

    var body: some View {

        List {
            Toggle(isOn: $userData.showFavoritesOnly) {
                Text("Favorites only")
            }

            ForEach(userData.landmarks) { landmark in
                if !self.userData.showFavoritesOnly || landmark.isFavorite {
                    NavigationButton(destination: LandmarkDetail(landmark: landmark)) {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Landmarks"))

    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            LandmarkList()
                .environmentObject(UserData())
        }
    }
}

이해하는지 확인하기(Check Your Understanding)

퀴즈 1. 어떤 뷰가 Landmarks 앱에 대한 루트 뷰 인가요?

  1. SceneDelegate
  2. Landmarks
  3. CategoryHome

[정답 : 3]
Landmarks 앱의 홈 화면은 랜드마크의 카테고리가 보여지므로, 전체 앱에서 그 이름이 그 역할을 나타냅니다.

퀴즈 2. CategoryHome 뷰는 앱의 나머지 코드들을 어떻게 활용하나요?

  1. 다른 랜드마크에 대한 이미지 어셋(assets)을 재사용합니다.
  2. 수식어에 대한 동일한 구문을 사용하고 뷰에 대해 동일한 명명규칙을 사용합니다.
  3. 네비게이션 계층구조에 있는 모든 랜드마크 뷰를 연결합니다.

[정답 : 3]
Landmarks 앱은 네비게이션 뷰를 포함한 모든 뷰를 합해놓은 것입니다.

퀴즈 3. 네비게이션 버튼으로 변환하는 옳은 코드는 무엇인가요?

1.

 NavigationButton(
    LandmarkCard(landmark: landmark),
    destination: LandmarksView(landmark: landmark)
)

2.

 NavigationButton(destination: LandmarksView(landmark: landmark)) {
    LandmarkCard(landmark: landmark)
}

3.

 NavigationButton {
    LandmarkCard(landmark: landmark)
}
.navigationDestination(LandmarksView(landmark: landmark))

[정답 : 2]
버튼에 있는 라벨은 대상과 함께 전달되는 뷰 생성 클로져에 있습니다.

'SwiftUI > Tutorials' 카테고리의 다른 글

Interfacing with UIKit  (0) 2019.08.13
Working with UI Controls  (0) 2019.08.13
Composing Complex Interfaces  (0) 2019.08.13
Animating Views and Transitions  (0) 2019.08.13
Drawing Paths and Shapes  (0) 2019.08.13
Handling User Input  (0) 2019.08.13
Posted by 까칠코더