공부/iOS

Editing NavigationSplitView 예제에서 모르는 키워드 정리(작성중)

안토니1 2022. 11. 26. 03:45

배경

새롭게 등장한 NavigationSplitView와 NavigationStack을 공부하며 찾아봤던 내용을 정리했습니다.

예제 코드에서 키워드를 뽑아냈으며 헷갈리거나 모르는 개념을 정리해봤습니다.

목차

Group

VStack과 비슷한 역할을 하는 Struct입니다.

다음과 같이 VStack 처럼 사용할 수 있으며, 이 경우 VStack과 가장 큰 차이점은 Group은 11개 이상의 child를 가질 수 있다는 점 입니다.

Group {
    Text("SwiftUI")
    Text("Combine")
    Text("Swift System")
}
.font(.headline)

View Builder를 사용하여 뷰 그룹을 작성하기 때문에 Group의 이니셜라이저를 사용하여 조건부에서 다른 종류의 뷰를 생성한 다음 선택적으로 수정자를 적용할 수 있습니다.

Group {
    if isLoggedIn {
        WelcomeView()
    } else {
        LoginView()
    }
}
.navigationBarTitle("Start")

ViewBuilder

Closure에서 View를 구성하는 custom parameter attribute. Closure에서 (Child) View를 구성한다.

두가지 방법으로 사용된다.

  1. @ViewBuiler Prameter

image

HStack은 생성자에 @ViewBuilder content 파라미터(Closure타입)가 있습니다.

이 처럼 커스텀으로 파라미터를 받는 생성자를 정의할 때 사용할 수 있습니다.

struct CustomHStack<Content>: View where Content: View {

    let content: () -> Content
    let color: Color

    init(color: Color = .clear,
         @ViewBuilder content: @escaping () -> Content) {
        self.color = color
        self.content = content
    }

    var body: some View {
        HStack {
            content()
        }
        .background(color)
    }
}
  1. @ViewBuilder computed property, Method

computed property 가 너무 길어지거나 Method로 빼고 싶은 경우에 다음과 같이 쓸 수 있다.

struct ContentView: View {

    var body: some View {
        VStack{
            manyTexts
            generateTexts()
        }

    }

    @ViewBuilder
    var manyTexts: some View {
        Text("computed Property 1")
        Text("computed Property 2")
        Text("computed Property 3")
        Text("computed Property 4")
        Text("computed Property 5")
    }

    @ViewBuilder
    func generateTexts() -> some View {
        Text("Method 1")
        Text("Method 2")
        Text("Method 3")
        Text("Method 4")
        Text("Method 5")
    }
}

예제 코드

// 2022-08-03: updated to work by following "Bringing robust navigation structure to your SwiftUI app"
// https://developer.apple.com/documentation/swiftui/bringing_robust_navigation_structure_to_your_swiftui_app
// Tested with Xcode Version 14.0 beta 4 (iOS 16.0 beta 4)
import SwiftUI
import Combine

@main
struct SplitNav_2022_08_02App: App {
    @StateObject var navigationModel = NavigationModel(sidebarDestination: .subreddit(subreddit: .games))

    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(navigationModel)
        }
    }
}

// Based on https://developer.apple.com/documentation/swiftui/bringing_robust_navigation_structure_to_your_swiftui_app
final class NavigationModel: ObservableObject {
    @Published var sidebarDestination: Destination?
    @Published var detailPath: [Destination]


    @Published var detailNavigation: Destination?
    @Published var columnVisibility: NavigationSplitViewVisibility


    init(sidebarDestination: Destination? = nil, detailPath: [Destination] = [], detailNavigation: Destination? = nil, columnVisibility: NavigationSplitViewVisibility = .automatic) {
        self.sidebarDestination = sidebarDestination
        self.detailPath = detailPath
        self.detailNavigation = detailNavigation
        self.columnVisibility = columnVisibility
    }

    var selectedDetail: Destination? {
        get { detailPath.first }
        set { detailPath = [newValue].compactMap { $0 } }
    }
}

enum HomeDestination: String, CaseIterable, Hashable {
    case hot, best, trending, new, top, rising
}
enum SubredditDestination: String, CaseIterable, Hashable {
    case news, diablo, pics, wtf, games, movies
}

enum UserDestination: String, CaseIterable, Hashable {
    case profile, inbox, posts, comments, saved
}

enum Destination: Hashable {
    case home(home: HomeDestination)
    case subreddit(subreddit: SubredditDestination)
    case user(user: UserDestination)
    case post(post: Post)

    var caseName: String {
        switch self {
        case .home:
            return "Home"
        case .subreddit:
            return "Subreddit"
        case .user:
            return "Account"
        case .post:
            return "Post"
        }
    }
}

struct MainView: View {
    @EnvironmentObject var navigationModel: NavigationModel

    var body: some View {
        NavigationSplitView(
            columnVisibility: $navigationModel.columnVisibility
        ) {
            SidebarView()
        } content: {
            Group {
                switch navigationModel.sidebarDestination {
                case .home(let destination):
                    HomeView(destination: destination)
                        .navigationTitle(navigationModel.sidebarDestination!.caseName.capitalized)
                case .subreddit(let subreddit):
                    SubredditView(subreddit: subreddit)
                case .user(let destination):
                    AccountView(destination: destination)
                        .navigationTitle(navigationModel.sidebarDestination!.caseName.capitalized)
                case .post(let post):
                    PostView(post: post)
                case .none:
                    EmptyView()
                }
            }
            .onDisappear {
                if navigationModel.selectedDetail == nil {
                    navigationModel.sidebarDestination = nil
                }
            }

        } detail: {
            NavigationStack {
                Group {
                    if case .subreddit = navigationModel.sidebarDestination {
                        if let detailNavigation = navigationModel.selectedDetail {
                            if case .post(let post) = detailNavigation {
                                PostView(post: post)
                            }
                        } else {
                            Text("Please select a post")
                        }
                    } else {
                        EmptyView()
                    }
                }.navigationDestination(for: Destination.self) { destination in
                    switch destination {
                    case .user(let userDestination):
                        AccountView(destination: userDestination)
                    default:
                        Text("Not supported here")
                    }
                }
            }
        }
    }
}

struct SidebarView: View {
    @EnvironmentObject var navigationModel: NavigationModel

    var body: some View {
        List(selection: $navigationModel.sidebarDestination) {
            Section("Home") {
                ForEach(HomeDestination.allCases, id: \.self) { homeItem in
                    NavigationLink(value: Destination.home(home: homeItem)) {
                        Label(homeItem.rawValue.capitalized, systemImage: "globe")
                    }
                }
            }

            Section("Subreddit") {
                ForEach(SubredditDestination.allCases, id: \.self) { subreddit in
                    NavigationLink(value: Destination.subreddit(subreddit: subreddit)) {
                        Label(subreddit.rawValue.capitalized, systemImage: "globe")
                    }
                }
            }


            Section("Account") {
                ForEach(UserDestination.allCases, id: \.self) { userDestination in
                    NavigationLink(value: Destination.user(user: userDestination)) {
                        Label(userDestination.rawValue.capitalized, systemImage: "globe")
                    }
                }
            }
        }
        .navigationTitle("Categories")
    }
}


struct Post: Identifiable, Hashable {
    let id = UUID()
    let title = "A post title"
    let preview = "Some wall of text to represent the preview of a post that nobody will read if the title is not a clickbait"
}

extension Post {
    static var posts: [Post] = [Post(), Post(), Post(), Post(), Post(), Post(), Post(), Post()]
}

struct SubredditView: View {
    let subreddit: SubredditDestination
    @EnvironmentObject var navigationModel: NavigationModel

    var body: some View {
        List(Post.posts, selection: $navigationModel.selectedDetail) { post in
            NavigationLink(value: Destination.post(post: post)) {
                HStack {
                    VStack(alignment: .leading) {
                        Text(post.title)
                            .font(.title3)
                            .fontWeight(.semibold)
                        Text(post.preview)
                            .font(.callout)
                    }
                }
            }
        }.navigationTitle(subreddit.rawValue.capitalized)
    }
}

struct PostView: View {
    let post: Post

    var body: some View {
        VStack {
            Text(post.title)
                .font(.title)
            Text(post.preview)
            NavigationLink(value: Destination.user(user: .comments)) {
                Text("See some sub navigation")
            }
        }
    }
}

struct AccountView: View {
    let destination: UserDestination

    var body: some View {
        Text(destination.rawValue.capitalized)
    }
}

struct HomeView: View {
    let destination: HomeDestination

    var body: some View {
        Text(destination.rawValue.capitalized)
    }
}

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        MainView()
    }
}

출처: Bringing robust navigation structure to your SwiftUI app 를 참고한 코드입니다.

https://gist.github.com/Dimillian/c10ffac65a1a37b337a07b5cf0773cea