Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SwiftUI] List Item을 NavigationLink로 만들 경우 #20

Closed
Brandnew-one opened this issue Jan 20, 2023 · 0 comments
Closed

[SwiftUI] List Item을 NavigationLink로 만들 경우 #20

Brandnew-one opened this issue Jan 20, 2023 · 0 comments
Assignees
Labels

Comments

@Brandnew-one
Copy link
Owner

LazyVStack - NavigationLink

LazyVstack안에 NavigationLink가 있는 경우 발생한 버그에 대해 정리해보고자 한다.

Simulator Screen Recording - iPhone 14 Pro - 2023-01-20 at 20 37 32

현재 사용자가 입력한 결과값에 따라 Media List를 받아오고 각 Cell을 클릭하면 DetailView로 Push 되고 있는 형태의 앱이다.

struct MediaListView: View {
  @StateObject
  var viewModel: MediaListViewModel

  let appContainer: AppContainer

  var body: some View {
    NavigationView {
      ScrollView {
        LazyVStack {
          ForEach(viewModel.output.medias, id: \.id) { media in
            NavigationLink(
              destination: {
                NavigationLazyView(
                  MediaDetailView(
                    viewModel: appContainer.mediaDetailViewModel(media)
                  )
                )
              },
              label: {
                MediaListItemView(media: media)
              }
            )
          }
        }
        .padding(.top, 20)
        .padding([.leading, .trailing], 12)
      }
      .navigationTitle("TV Search")
      .navigationBarTitleDisplayMode(.inline)
      .searchable(text: $viewModel.input.searchMediaSub.value)
    }
  }
}

SwiftUI의 NavigationLink는 사용자가 push하기 이전뷰에서도 push될 뷰를 미리 load하고 있는 문제점(?)이 있는데 이를 방지하고자 push되는 시점에서 뷰가 그려질 수 있도록 NavigaitonLazyView를 사용하고 있다.

하지만 위와 같이 코드를 작성하고, 기존에 리스트에 있는 cell을 클릭하는 시점에 사용자가 키보드를 통해 다른 값을 입력하면 자동으로 navigation pop이 되는 문제점이 있다.

입력값이 변화함에 따라서 기존의 list가 가지고 있던 cell이 사라지면서 navigationLink도 사라져 버린것이다!

이런 문제를 어떻게 해결할 수 있을까?

이런 문제가 발생하는 이유는 list의 cell이 navigationLink라는 View로 만들어졌기 때문이다. 따라서 문제를 해결하기 위해서 List의 cell Item 자체가 navigationLink가 되어서는 안된다.

import SwiftUI

struct MediaListView: View {
  @StateObject
  var viewModel: MediaListViewModel

  let appContainer: AppContainer

  var body: some View {
    NavigationView {
      ScrollView {
        LazyVStack {
          // Empty View
          NavigationLink(
            isActive: $viewModel.output.isNavigationShow,
            destination: {
              if let media = viewModel.output.selectedMedia {
                NavigationLazyView(
                  MediaDetailView(
                    viewModel: appContainer.mediaDetailViewModel(media)
                  )
                )
              }
            },
            label: { }
          )
          // List Cell Item
          ForEach(viewModel.output.medias, id: \.id) { media in
            MediaListItemView(media: media)
              .wrapToButton { viewModel.action(.navigationTapped(media)) }
          }
        }
        .padding(.top, 20)
        .padding([.leading, .trailing], 12)
      }
      .navigationTitle("TV Search")
      .navigationBarTitleDisplayMode(.inline)
      .searchable(text: $viewModel.input.searchMediaSub.value)
    }
  }
}

해결방법은 간단하다. List외부에 Empty View Label을 가지는 NavigationLink를 만들고, 해당 NavigationLink를 통해서 화면전환을 하면된다.

코드가 변경된 부분을 보자. 기존에는 List의 Item 자체가 NavigationLink였지만 단순히 Button 기능을 하는 버튼으로 변경된 것을 확인할 수 있다.

import Combine
import Foundation

final class MediaListViewModel: ViewModelType {
  private let searchMediaUsecase: SearchMediaUseCase

  struct Input {
    var searchMediaSub = BindingSubject<String>(value: "")
    fileprivate let searchButtonSub = PassthroughSubject<Void, Never>()
    fileprivate let navigationSub = PassthroughSubject<Media, Never>()
  }

  enum Action {
    case searchButtonTapped
    case navigationTapped(Media)
  }

  func action(_ action: Action) {
    switch action {
    case .searchButtonTapped:
      input.searchButtonSub.send()
    case .navigationTapped(let media):
      input.navigationSub.send(media)
    }
  }

  struct Output {
    var medias: [Media] = []
    var isNavigationShow: Bool = false
    var selectedMedia: Media?
  }

  var input = Input()
  @Published var output = Output()
  var cancellables = Set<AnyCancellable>()

  init(searchMediaUsecase: SearchMediaUseCase) {
    self.searchMediaUsecase = searchMediaUsecase
    transform()
  }

  func transform() {
    input.searchMediaSub.subject
      .debounce(for: 0.5, scheduler: RunLoop.main)
      .flatMap { [weak self] search -> AnyPublisher<MediaPage, Never> in
        guard let self = self else {
          return Just(MediaPage(page: -1, totalPages: -1, medias: [])).eraseToAnyPublisher()
        }
        return self.searchMediaUsecase.tvExcute(query: search, page: 1)
          .replaceError(with: MediaPage(page: -1, totalPages: -1, medias: []))
          .eraseToAnyPublisher()
      }
      .map { $0.medias }
      .sink(receiveValue: { [weak self] in
        guard let self = self else { return }
        self.output.medias = $0
      })
      .store(in: &cancellables)

    input.navigationSub
      .sink(receiveValue: { [weak self] in
        guard let self = self else { return }
        self.output.selectedMedia = $0
        self.output.isNavigationShow = true
      })
      .store(in: &cancellables)
  }
}

뷰모델의 프레젠테이션 로직을 확인해보면, 사용자가 DetailView를 보기 위해서 버튼을 누르면 해당 버튼을 그릴 때 사용되었던 Media를 subject를 통해 넘겨주고 navigationLink의 isActive를 변경한다.
Simulator Screen Recording - iPhone 14 Pro - 2023-01-20 at 21 17 30

문제가 해결된것을 확인할 수 있다.

전체 프로젝트는 여기서 확인할 수 있습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant