SwiftUI:按下带有导航链接的屏幕会再次导致列表变红

时间:2020-07-22 15:43:04

标签: ios swiftui swiftui-list swiftui-navigationlink

我有一个列表视图屏幕。最初加载列表时,日志将在控制台上正确打印。当我从列表中选择一个项目时,详细信息屏幕将按预期方式打开,但是我看到列表项目日志再次被打印。这意味着列表项将再次呈现。

struct ItunesSearchView: View {

    @ObservedObject private var viewModel = ItunesSearchViewModel()
    @State var selectedItem: Int? = nil
    @State private var keyboardHeight: CGFloat = 0

    func makeItemDetailView(_ index: Int) -> some View {
        ItemDetailView(item: viewModel.results[index])
    }

    func makeItemView(_ index: Int, _ width: CGFloat) -> some View {
        ItunesItemView(item: viewModel.results[index])
                .frame(width: width, height: 100)
    }

    func renderSingleColumnList(_ size: CGSize) -> some View {
        List {
            ForEach(0..<viewModel.results.count, id: \.self) { row in
                ZStack {
                    if row < self.viewModel.count {
                        NavigationLink(destination: self.makeItemDetailView(row)) {
                            self.makeItemView(row, size.width - 8)
                        }
                    } else {
                        EmptyView().frame(width: size.width - 8, height: 100)
                    }
                }
            }
        }.padding(.leading, size.width < size.height ? -16 : -22)
    }

    func renderTwoColumnsList(_ size: CGSize) -> some View {
        let count = viewModel.results.count
        let halfCount = count % 2 == 0 ? count / 2 : count / 2 + 1
        return List {
            ForEach(0..<halfCount, id: \.self) { row in
                ZStack {
                    if row < self.viewModel.count {
                        HStack(spacing: 16) {
                            ForEach(0..<2, id: \.self) { column in
                                NavigationLink(destination: self.makeItemDetailView(row * 2 + column)) {
                                    self.makeItemView(row * 2 + column, size.width / 2 - 8)
                                }
                            }
                        }.frame(width: size.width, height: 100)
                    } else {
                        EmptyView().frame(width: size.width - 8, height: 100)
                    }
                }
            }
        }.padding(.leading, size.width < size.height ? -16 : -22)
    }

    func isPortrait(_ size: CGSize) -> Bool {
        size.width < size.height
    }

    func searchBarTopPadding(_ size: CGSize) -> CGFloat {
        isPortrait(size) ? 8 : 16
    }

    var body: some View {
        GeometryReader { geometry in
            NavigationView {
                VStack(alignment: .center) {
                    SearchBar(
                            text: self.$viewModel.searchText
                    ).padding(.top, self.searchBarTopPadding(geometry.size))
                    LoadingView(isShowing: self.$viewModel.isLoading) {
                        ZStack {
                            if self.isPortrait(geometry.size) {
                                self.renderSingleColumnList(geometry.size)
                            } else {
                                self.renderTwoColumnsList(geometry.size)
                            }
                        }
                    }
                }.navigationBarTitle(
                        "iTunes Search", displayMode: .inline
                ).keyboardAdaptive()
            }
        }.navigationViewStyle(StackNavigationViewStyle())
    }
}

这是列表项视图:

struct ItunesItemView: View {

    @ObservedObject private var viewModel = ItunesItemViewModel()

    init(item: ItunesItem) {
        viewModel.item = item
        viewModel.getImage()
    }

    var body: some View {
        GeometryReader { geometry in
            HStack(alignment: .center, spacing: 16) {
                ItemImageView(
                        imageData: self.$viewModel.imageData,
                        kind: self.viewModel.kind
                ).frame(width: 100, height: 100)
                VStack(alignment: .leading, spacing: 4) {
                    Text(self.viewModel.trackName).lineLimit(1).font(.headline)
                    Text(self.viewModel.artistName).lineLimit(1).font(.body)
                    Text(self.viewModel.collectionName).lineLimit(1).font(.caption)
                }.background(Color.clear)
                Spacer().background(Color.clear)
            }.background(self.viewModel.isItemVisited ? Color.gray : Color.green)
        }
    }
}

这是我的项目视图模型:

public class BaseItunesItemViewModel: ObservableObject {

    internal let apiClient = ImageApiClient()
    internal var task: Cancellable?

    internal var log: OSLog {
        OSLog.baseItemViewModel
    }

    let historyManager = HistoryManager.sharedInstance
    var item: ItunesItem?

    @Published var imageData = Data() {
        willSet {
            self.objectWillChange.send()
        }
        didSet {
            os_log("ImageData is set", log: log, type: .info)
        }
    }

    var isItemVisited: Bool {
        guard let item = item else {
            os_log("isItemVisited: Failed to get item", log:log, type: .error)
            return false
        }
        guard let id = item.trackId else {
            os_log("isItemVisited: Failed to get item id", log: log, type: .error)
            return false
        }
        return historyManager.isVisited(id: id)
    }

    func setItemVisited() {
        guard let item = item else {
            os_log("setItemVisited: Failed to get item", log: log, type: .error)
            return
        }
        guard let id = item.trackId else {
            os_log("setItemVisited: Failed to get item id", log: log, type: .error)
            return
        }
        os_log("setItemVisited: Item %{public}@ is visited", log: log, type: .error, "\(id)")
        historyManager.setVisited(id: id)
    }

    func getImage(url: String?) {
        task?.cancel()
        guard let url = url else {
            os_log("getImage: Failed to get image url", log: .itemViewModel, type: .info)
            return
        }
        if let data = UIImageCache.sharedInstance.imageData(for: url) {
            imageData = data
            os_log("getImage: Returned cached image data for %{public}@", log: log, type: .info, url)
            return
        }
        task = apiClient.getImage(url: url).sink(receiveCompletion: { [weak self, url] completion in
            self?.onCompletion(url, completion)
        }, receiveValue: { [weak self] data in
            UIImageCache.sharedInstance.setImageData(data: data, url: url)
            self?.imageData = data
        })
    }

    private func onCompletion(_ url: String, _ completion: Subscribers.Completion<Error>) {
        switch completion {
        case .finished:
            os_log("getImage: Fetched image for %{public}@", log: log, type: .info, url)
        case .failure(let error):
            os_log("getImage: Failed to fetch image for %{public}@ : %{public}@",
                    log: .itemViewModel,
                    type: .error,
                    url,
                    error.localizedDescription
            )
        }
    }
}

public final class ItunesItemViewModel: BaseItunesItemViewModel {

    override var log: OSLog {
        .itemViewModel
    }

    var trackName: String {
        item?.trackName ?? "???"
    }

    var artistName: String {
        item?.artistName ?? "???"
    }

    var collectionName: String {
        item?.collectionName ?? "???"
    }

    var kind: String {
        item?.kind ?? "???"
    }

    func getImage() {
        getImage(url: item?.artworkUrl100)
    }
}

为什么会这样?我该如何预防?

简化的日志:

GET https://itunes.apple.com/search?term=Tarkan&media=music&entity=song&country=tr&limit=100
[ItunesSearchViewModel] Received search results
[ItunesSearchViewModel] Finished receiving search results
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemDetailViewModel] setItemVisited: Item 1086960912 is visited
[ItemDetailViewModel] ImageData is set
[ItemDetailViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg
[ItemViewModel] getImage: Returned cached image data for https://source/100x100bb.jpg

0 个答案:

没有答案