iOS Dev/SwiftUI

[SwiftUI] 6. Composing Complex Interfaces

Mosu(정종인) 2023. 5. 4. 01:50
반응형

https://developer.apple.com/tutorials/swiftui/composing-complex-interfaces

 

Composing Complex Interfaces | Apple Developer Documentation

The category view for Landmarks shows a vertically scrolling list of horizontally scrolling landmarks. As you build this view and connect it to your existing views, you’ll explore how composed views can adapt to different device sizes and orientations.

developer.apple.com

 

Section 1. Add a Category View

Views 그룹에 Categories 그룹을 만들고 그 안에 CategoryHome.swift를 추가한다.

코드는 다음 내용으로 채운다.

import SwiftUI

struct CategoryHome: View {
    var body: some View {
        NavigationStack {
            Text("Hello, World!")
                .navigationTitle("Featured")
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
    }
}

NavigationView 말고 NavigationStack으로 사용했다.

.navigationTitle 모디파이어를 사용하면 해당 뷰가 들어있는 Navigation 요소의 title을 정의할 수 있다.

 

Section 2. Create a Category List

json파일에서 Category 데이터를 읽어오기 위해 기존 코드를 많이 수정해야 한다.

먼저 Landmark.swift 부터 수정한다.

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    var isFavorite: Bool
    
    var category: Category // ✅
    enum Category: String, CaseIterable, Codable { // ✅
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }
    
    private var imageName: String
    var image: Image {
        return Image(imageName)
    }
    
    private var coordinates: Coordinates
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
    }
    
    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

 

다음은 ModelData.swift이다.

final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
    var hikes: [Hike] = load("hikeData.json")
    
    var categories: [String: [Landmark]] { // ✅
        Dictionary(
            grouping: landmarks,
            by: { $0.category.rawValue }
        )
    }
}

Dictionary 부분이 흥미로웠는데, landmarks 데이터를 그룹핑하는데, key를 각각의 데이터의 category의 rawValue로 주었다. 진짜 아는 만큼 보이는 것 같다..

 

다시 CategoryHome으로 돌아와서 modelData를 받아주고, List를 생성한다.

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData

    var body: some View {
        NavigationStack {
            List {
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    Text(key)
                }
            }
            .navigationTitle("Featured")
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
            .environmentObject(ModelData())
    }
}

 

Section 3. Create a Category Row

CategoryItem.swift 파일과 CategoryRow.swift 파일을 생성한다.

먼저 CategoryItem.swift코드는 다음과 같이 작성한다.

import SwiftUI

struct CategoryItem: View {
    var landmark: Landmark

    var body: some View {
        VStack(alignment: .leading) {
            landmark.image
                .resizable()
                .frame(width: 155, height: 155)
                .cornerRadius(5)
            Text(landmark.name)
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

struct CategoryItem_Previews: PreviewProvider {
    static var previews: some View {
        CategoryItem(landmark: ModelData().landmarks[0])
    }
}

다음, CategoryRow.swift 코드는 다음과 같이 작성한다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]

    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        CategoryItem(landmark: landmark)
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

struct CategoryRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks

    static var previews: some View {
        CategoryRow(
            categoryName: landmarks[0].category.rawValue,
            items: Array(landmarks.prefix(4))
        )
    }
}

여기까지 CategoryRow는 이런 모양이다.

Section 4. Complete the Category View

CategoryHome.swift를 다음과 같이 업데이트한다.

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData

    var body: some View {
        NavigationStack {
            List {
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key] ?? []) // ✅
                }
            }
            .navigationTitle("Featured")
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
            .environmentObject(ModelData())
    }
}

 

이제 특집이 있는 Landmark도 구분하기 위해 Landmark.swift 모델을 수정해야한다.

import Foundation
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    var isFavorite: Bool
    var isFeatured: Bool // ✅
    
    var category: Category
    enum Category: String, CaseIterable, Codable {
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }
    
    private var imageName: String
    var image: Image {
        return Image(imageName)
    }
    
    private var coordinates: Coordinates
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: coordinates.latitude, longitude: coordinates.longitude)
    }
    
    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

ModelData.swift도 손봐준다.

import Foundation
import Combine

final class ModelData: ObservableObject {
    @Published var landmarks: [Landmark] = load("landmarkData.json")
    var hikes: [Hike] = load("hikeData.json")
    
    var features: [Landmark] { // ✅
        landmarks.filter { $0.isFeatured }
    }
    
    var categories: [String: [Landmark]] {
        Dictionary(
            grouping: landmarks,
            by: { $0.category.rawValue }
        )
    }
}

다시 CategoryHome.swift로 돌아와서 다음 내용을 수정한다.

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData

    var body: some View {
        NavigationStack {
            List {
                modelData.features[0].image // ✅
                    .resizable()
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key] ?? [])
                }
                .listRowInsets(EdgeInsets()) // ✅
            }
            .navigationTitle("Featured")
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
            .environmentObject(ModelData())
    }
}

.listRowInsets(EdgeInsets())를 하게 되면 모든 Edge에 대한 Inset이 0으로 초기화가 되면서 요소를 꽉 채울 수 있다.

 

Section 5. Add Navigation Between Sections

이제 각각 다른 뷰에서 같은 디테일 뷰로 navigate할 수 있도록 할 것이다.

먼저 CategoryRow.swift로 가서 다음을 수정한다.

import SwiftUI

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]

    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        NavigationLink { // ✅
                            LandmarkDetail(landmark: landmark)
                        } label: {
                            CategoryItem(landmark: landmark)
                        }
                    }
                }
            }
            .frame(height: 185)
        }
    }
}

 

다음은 CategoryItem.swift이다.

import SwiftUI

struct CategoryItem: View {
    var landmark: Landmark

    var body: some View {
        VStack(alignment: .leading) {
            landmark.image
                .renderingMode(.original) // ✅
                .resizable()
                .frame(width: 155, height: 155)
                .cornerRadius(5)
            Text(landmark.name)
                .foregroundColor(.primary) // ✅
                .font(.caption)
        }
        .padding(.leading, 15)
    }
}

이 두가지를 바꾸는 이유? 먼저 image는 NavigationLink의 Label로 표현될 때 템플릿 이미지로 렌더링되고, text는 accent color로 렌더링 되게 된다. 이를 바꾸려면 renderingMode를 바꾸거나 foregroundColor를 미리 주면 된다.

이제 맨 처음 ContentView.swift로 돌아가서 TabView를 만들어준다.

import SwiftUI

struct ContentView: View {
    @State private var selection: Tab = .featured
    enum Tab {
        case featured
        case list
    }

    var body: some View {
        TabView(selection: $selection) {
            CategoryHome()
                .tabItem {
                    Label("Featured", systemImage: "star")
                }
                .tag(Tab.featured)
            
            LandmarkList()
                .tabItem {
                    Label("List", systemImage: "list.bullet")
                }
                .tag(Tab.list)
        }
    }
}

잘 되는 것을 확인할 수 있다.

 

Check Your Understanding

답은 3 3 2

반응형