我现在所拥有的是:页面加载,API 被调用并返回一个项目列表。当 API 返回时,我想为每个单独的项目调用 API 以请求附加数据。我被困在最后一部分了。
这是我到目前为止所拥有的。
struct MainView: View {
@Environment(\.dismiss) var dismiss
let source: String
let sourceId: String
@EnvironmentObject var startupViewModel: StartupViewModel
@StateObject var vm = PlaceViewModel(placeService: PlaceService())
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(Array((vm.attractions).enumerated()), id: \.element.id) { index, place in
PlaceCardView(place: place)
}
}
}
}
}
.task {
vm.fetchPlace(source: source, sourceId: sourceId)
}
.onChange(of: vm.place != nil) {
if let placeId = vm.place?.id {
vm.fetchAttractions(placeId: placeId, page: 1)
}
}
}
.environmentObject(vm)
}
}
如您所见,当页面加载时,我首先调用
fetchPlace()
。然后,当 vm.place
发生变化时,我调用 vm.fetchAttractions()
。
以下是在我的视图模型中进行这些调用的方式:
class PlaceViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
let placeService: PlaceServiceProtocol
@Published var place: Place?
@Published var attractions: [Place] = []
init(placeService: PlaceServiceProtocol) {
self.placeService = placeService
}
func fetchPlace(source: String, sourceId: String) {
placeService.getPlace(source: source, sourceId: sourceId)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { data in
print("Received:")
print(data)
}, receiveValue: {[weak self] data in
self?.place = data.data?.place
}).store(in: &cancellables)
print(cancellables)
}
func fetchAttractions(placeId: Int, page: Int, completion: ((_ response: [Place]) -> Void)? = nil) {
placeService.getAttractions(placeId: placeId, page: page)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { data in
print("Received \(data)")
}, receiveValue: {[weak self] data in
guard let newAttractions = data.data?.attractions else {
return
}
if (newAttractions.isEmpty) {
self?.hasMoreAttractions = false
}
self?.attractions.append(contentsOf: newAttractions)
completion?(newAttractions)
}).store(in: &cancellables)
print(cancellables)
}
}
这是我的网络层(来自本教程):
protocol PlaceServiceProtocol {
func getPlace(source: String, sourceId: String) -> AnyPublisher<Response, Error>
func getAttractions(placeId: Int, page: Int) -> AnyPublisher<Response, Error>
}
class PlaceService: PlaceServiceProtocol {
let apiClient = URLSessionAPIClient<PlaceEndpoint>()
func getPlace(source: String, sourceId: String) -> AnyPublisher<Response, Error> {
return apiClient.request(.getPlace(source: source, sourceId: sourceId))
}
func getAttractions(placeId: Int, page: Int) -> AnyPublisher<Response, Error> {
return apiClient.request(.getAttractions(placeId: placeId, page: page))
}
}
enum PlaceEndpoint: APIEndpoint {
case getPlace(source: String, sourceId: String)
case getAttractions(placeId: Int, page: Int)
var path: String {
switch self {
case .getPlace:
return "/place/get"
case .getAttractions:
return "/place/attractions/get"
}
}
var method: HTTPMethod {
switch self {
case .getPlace:
return .post
case .getAttractions:
return .post
}
}
var headers: [String: String]? {
switch self {
case .getPlace:
return nil
case .getAttractions:
return nil
}
}
var parameters: [String: Any]? {
switch self {
case .getPlace(let source, let sourceId):
return ["source": source, "source_id": sourceId]
case .getAttractions(let placeId, let page):
return ["place_id": placeId, "page": page]
}
}
}
protocol APIClient {
associatedtype EndpointType: APIEndpoint
func request<T: Decodable>(_ endpoint: EndpointType) -> AnyPublisher<T, Error>
}
class URLSessionAPIClient<EndpointType: APIEndpoint>: APIClient {
func request<T: Decodable>(_ endpoint: EndpointType) -> AnyPublisher<T, Error> {
let url = URL(string: BuildConfiguration.shared.baseURL)!.appendingPathComponent(endpoint.path)
var request = URLRequest(url: url)
let keychain = KeychainSwift()
request.httpMethod = endpoint.method.rawValue
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
// Authorization
request.addValue("Bearer " + (keychain.get("token") ?? ""), forHTTPHeaderField: "Authorization")
do {
// convert parameters to Data and assign dictionary to httpBody of request
request.httpBody = try JSONSerialization.data(withJSONObject: endpoint.parameters ?? [], options: .prettyPrinted)
} catch let error {
print(error.localizedDescription)
}
endpoint.headers?.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }
return URLSession.shared.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> JSONDecoder.Input in
print(response)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
return data
}
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
调用
fetchAttractions()
后,我想再次调用 API 以获取列表中每个景点的附加数据。
最好的方法是什么?我尝试过循环浏览
fetchAttractions()
内的每个景点,但是在滚动时遇到了滞后。
尝试这样的事情:
.task(id: place.id) {
details = await controller.fetchDetails(for: place)
}