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