ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SwiftUI] 6. Composing Complex Interfaces
    iOS Dev/SwiftUI 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

    반응형

    댓글

Designed by Tistory.