원문 : 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 앱에 대한 루트 뷰 인가요?
- SceneDelegate
- Landmarks
- CategoryHome
[정답 : 3]
Landmarks 앱의 홈 화면은 랜드마크의 카테고리가 보여지므로, 전체 앱에서 그 이름이 그 역할을 나타냅니다.
퀴즈 2. CategoryHome 뷰는 앱의 나머지 코드들을 어떻게 활용하나요?
- 다른 랜드마크에 대한 이미지 어셋(assets)을 재사용합니다.
- 수식어에 대한 동일한 구문을 사용하고 뷰에 대해 동일한 명명규칙을 사용합니다.
- 네비게이션 계층구조에 있는 모든 랜드마크 뷰를 연결합니다.
[정답 : 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 |
Animating Views and Transitions (0) | 2019.08.13 |
Drawing Paths and Shapes (0) | 2019.08.13 |
Handling User Input (0) | 2019.08.13 |
Building Lists and Navigation (0) | 2019.07.23 |
Creating and Combining Views (0) | 2019.07.23 |