视图如何使用视图模型和网络获取数据API
How does a view obtain data using a view model and Network API
我正在尝试使用此帮助程序文件获取一些数据:
https://gist.github.com/jbfbell/e011c5e4c3869584723d79927b7c4b68
这是重要代码的片段:
Class
/// Base class for requests to the Alpha Vantage Stock Data API. Intended to be subclasssed, but can
/// be used directly if library does not support a new api.
class AlphaVantageRequest : ApiRequest {
private static let alphaApi = AlphaVantageRestApi()
let method = "GET"
let path = ""
let queryStringParameters : Array<URLQueryItem>
let api : RestApi = AlphaVantageRequest.alphaApi
var responseJSON : [String : Any]? {
didSet {
if let results = responseJSON {
print(results)
}
}
}
}
扩展 ApiRequest
/// Makes asynchronous call to fetch response from server, stores response on self
///
/// - Returns: self to allow for chained method calls
public func callApi() -> ApiRequest {
guard let apiRequest = createRequest() else {
print("No Request to make")
return self
}
let session = URLSession(configuration: URLSessionConfiguration.ephemeral)
let dataTask = session.dataTask(with: apiRequest) {(data, response, error) in
guard error == nil else {
print("Error Reaching API, \(String(describing: apiRequest.url))")
return
}
self.receiveResponse(data)
}
dataTask.resume()
return self
}
我的目标是在加载 url 请求的数据后从 responseJSON 中获取数据。
我的 ViewModel 目前看起来像这样:
class CompanyViewModel: ObservableObject {
var companyOverviewRequest: ApiRequest? {
didSet {
if let response = companyOverviewRequest?.responseJSON {
print(response)
}
}
}
private var searchEndpoint: SearchEndpoint
init(companyOverviewRequest: AlphaVantageRequest? = nil,
searchEndpoint: SearchEndpoint) {
self.companyOverviewRequest = CompanyOverviewRequest(symbol: searchEndpoint.symbol)
}
func fetchCompanyOverview() {
guard let request = self.companyOverviewRequest?.callApi() else { return }
self.companyOverviewRequest = request
}
}
所以在我的 ViewModel 中,didSet 被调用一次,但不是在它应该存储数据的时候。 AlphaVantageRequest 的结果总是正确打印出来,但不是在我的 ViewModel 中。我怎样才能在我的 ViewModel 中也加载数据?
当您使用作为 ObservableObject 的视图模型时,您的视图想要观察 已发布 属性,通常是 viewState
(MVVM 术语):
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
@Published var viewState: ViewState = .undefined
viewState
完整描述了您的视图将如何呈现。请注意,它可以是 undefined
- 您的视图应该能够处理。
添加 loading(Company?)
案例也是一个好主意。然后您的视图可以呈现加载指示器。请注意,加载还提供可选的公司值。然后您可以呈现“刷新”,在这种情况下,您已经拥有公司价值,同时还绘制了加载指示器。
为了从端点获取一些数据,您可以使用以下抽象:
public protocol HTTPClient: class {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error>
}
这可以通过使用 5 行代码围绕 URLSession 进行简单包装来实现。然而,符合类型可以做更多的事情:它可以处理身份验证、授权,它可以重试请求、刷新访问令牌,或者在用户需要身份验证的地方呈现用户界面等。这个简单的协议足以满足所有这些需求。
那么,你的ViewModel是如何获取数据的呢?
引入另一个抽象是有意义的:执行此任务的“UseCase”,而不是让视图模型直接使用 HTTP 客户端。
“用例”只是执行任务、接受输入并产生输出或错误的对象。您可以随意命名,“DataProvider”、“ContentProvider”或类似名称。不过,“用例”是一个众所周知的术语。
从概念上讲,它与 HTTP 客户端具有相似的 API,但在语义上它位于更高的级别:
public protocol UseCase {
associatedtype Input: Encodable
associatedtype Output: Decodable
associatedtype Error
func callAsFunction(with input: Input) -> AnyPublisher<Output, Error>
}
让我们创建一个“GetCompany”用例:
struct Company: Codable {
var name: String
var id: Int
}
struct GetCompanyUseCase: UseCase {
typealias Input = Int
typealias Output = Company
typealias Error = Swift.Error
private let httpClient: HTTPClient
init(httpClient: HTTPClient) {
self.httpClient = httpClient
}
func callAsFunction(with id: Int) -> AnyPublisher<Company, Swift.Error> {
let request = composeURLRequest(input: id)
return httpClient.publisher(for: request)
.tryMap { httpResponse in
switch httpResponse {
case .success(_, let data):
return data
default:
throw "invalid status code"
}
}
.decode(type: Company.self, decoder: JSONDecoder())
.map { [=13=] } // no-op, usually you receive a "DTO.Company" value and transform it into your Company type.
.eraseToAnyPublisher()
}
private func composeURLRequest(input: Int) -> URLRequest {
let url = URL(string: "https://api.my.com/companies?id=\(input)")!
return URLRequest(url: url)
}
}
所以,这个用例显然访问了我们的 HTTP 客户端。我们可以实现访问 CoreData,或从文件读取,或使用模拟等。 API 始终相同,视图模型不关心。这里的美妙之处在于,你可以将它切换出来并换入另一个,视图模型仍然有效,你的视图也是如此。 (为了让它变得非常酷,您可以创建一个 AnyUseCase
泛型类型,这非常简单,这里是您的依赖注入)。
现在让我们看看视图模型的外观以及它如何使用用例:
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
@Published var viewState: ViewState = .undefined
let getCompany: GetCompanyUseCase
var getCompanyCancellable: AnyCancellable?
init(getCompany: GetCompanyUseCase) {
self.getCompany = getCompany
}
func load() {
self.getCompanyCancellable =
self.getCompany(with: 1)
.sink { (completion) in
print(completion)
} receiveValue: { (company) in
self.viewState = .value(company)
print("company set to: \(company)")
}
}
}
load
函数触发用例,调用底层http客户端加载公司数据。
当UseCasereturns一个公司时,它会被分配视图状态。观察者(视图,或 ViewController)将收到有关更改的通知,并可以执行更新。
您可以在 playground 中试验代码。以下是遗失的和平:
import Foundation
import Combine
extension String: Swift.Error {}
public enum HTTPResponse {
case information(response: HTTPURLResponse, data: Data)
case success(response: HTTPURLResponse, data: Data)
case redirect(response: HTTPURLResponse, data: Data)
case clientError(response: HTTPURLResponse, data: Data)
case serverError(response: HTTPURLResponse, data: Data)
case custom(response: HTTPURLResponse, data: Data)
}
class MockHTTPClient: HTTPClient {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error> {
let json = #"{"id": 1, "name": "Some Corporation"}"#.data(using: .utf8)!
let url = URL(string: "https://api.my.com/companies")!
let httpUrlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let response: HTTPResponse = .success(response: httpUrlResponse, data: json)
return Just(response)
.mapError { _ in "no error" }
.eraseToAnyPublisher()
}
}
Assemble:
let httpClient = MockHTTPClient()
let getCompany = GetCompany(httpClient: httpClient)
let viewModel = CompanyViewModel(getCompany: getCompany)
viewModel.load()
我正在尝试使用此帮助程序文件获取一些数据: https://gist.github.com/jbfbell/e011c5e4c3869584723d79927b7c4b68
这是重要代码的片段:
Class
/// Base class for requests to the Alpha Vantage Stock Data API. Intended to be subclasssed, but can
/// be used directly if library does not support a new api.
class AlphaVantageRequest : ApiRequest {
private static let alphaApi = AlphaVantageRestApi()
let method = "GET"
let path = ""
let queryStringParameters : Array<URLQueryItem>
let api : RestApi = AlphaVantageRequest.alphaApi
var responseJSON : [String : Any]? {
didSet {
if let results = responseJSON {
print(results)
}
}
}
}
扩展 ApiRequest
/// Makes asynchronous call to fetch response from server, stores response on self
///
/// - Returns: self to allow for chained method calls
public func callApi() -> ApiRequest {
guard let apiRequest = createRequest() else {
print("No Request to make")
return self
}
let session = URLSession(configuration: URLSessionConfiguration.ephemeral)
let dataTask = session.dataTask(with: apiRequest) {(data, response, error) in
guard error == nil else {
print("Error Reaching API, \(String(describing: apiRequest.url))")
return
}
self.receiveResponse(data)
}
dataTask.resume()
return self
}
我的目标是在加载 url 请求的数据后从 responseJSON 中获取数据。
我的 ViewModel 目前看起来像这样:
class CompanyViewModel: ObservableObject {
var companyOverviewRequest: ApiRequest? {
didSet {
if let response = companyOverviewRequest?.responseJSON {
print(response)
}
}
}
private var searchEndpoint: SearchEndpoint
init(companyOverviewRequest: AlphaVantageRequest? = nil,
searchEndpoint: SearchEndpoint) {
self.companyOverviewRequest = CompanyOverviewRequest(symbol: searchEndpoint.symbol)
}
func fetchCompanyOverview() {
guard let request = self.companyOverviewRequest?.callApi() else { return }
self.companyOverviewRequest = request
}
}
所以在我的 ViewModel 中,didSet 被调用一次,但不是在它应该存储数据的时候。 AlphaVantageRequest 的结果总是正确打印出来,但不是在我的 ViewModel 中。我怎样才能在我的 ViewModel 中也加载数据?
当您使用作为 ObservableObject 的视图模型时,您的视图想要观察 已发布 属性,通常是 viewState
(MVVM 术语):
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
@Published var viewState: ViewState = .undefined
viewState
完整描述了您的视图将如何呈现。请注意,它可以是 undefined
- 您的视图应该能够处理。
添加 loading(Company?)
案例也是一个好主意。然后您的视图可以呈现加载指示器。请注意,加载还提供可选的公司值。然后您可以呈现“刷新”,在这种情况下,您已经拥有公司价值,同时还绘制了加载指示器。
为了从端点获取一些数据,您可以使用以下抽象:
public protocol HTTPClient: class {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error>
}
这可以通过使用 5 行代码围绕 URLSession 进行简单包装来实现。然而,符合类型可以做更多的事情:它可以处理身份验证、授权,它可以重试请求、刷新访问令牌,或者在用户需要身份验证的地方呈现用户界面等。这个简单的协议足以满足所有这些需求。
那么,你的ViewModel是如何获取数据的呢?
引入另一个抽象是有意义的:执行此任务的“UseCase”,而不是让视图模型直接使用 HTTP 客户端。 “用例”只是执行任务、接受输入并产生输出或错误的对象。您可以随意命名,“DataProvider”、“ContentProvider”或类似名称。不过,“用例”是一个众所周知的术语。 从概念上讲,它与 HTTP 客户端具有相似的 API,但在语义上它位于更高的级别:
public protocol UseCase {
associatedtype Input: Encodable
associatedtype Output: Decodable
associatedtype Error
func callAsFunction(with input: Input) -> AnyPublisher<Output, Error>
}
让我们创建一个“GetCompany”用例:
struct Company: Codable {
var name: String
var id: Int
}
struct GetCompanyUseCase: UseCase {
typealias Input = Int
typealias Output = Company
typealias Error = Swift.Error
private let httpClient: HTTPClient
init(httpClient: HTTPClient) {
self.httpClient = httpClient
}
func callAsFunction(with id: Int) -> AnyPublisher<Company, Swift.Error> {
let request = composeURLRequest(input: id)
return httpClient.publisher(for: request)
.tryMap { httpResponse in
switch httpResponse {
case .success(_, let data):
return data
default:
throw "invalid status code"
}
}
.decode(type: Company.self, decoder: JSONDecoder())
.map { [=13=] } // no-op, usually you receive a "DTO.Company" value and transform it into your Company type.
.eraseToAnyPublisher()
}
private func composeURLRequest(input: Int) -> URLRequest {
let url = URL(string: "https://api.my.com/companies?id=\(input)")!
return URLRequest(url: url)
}
}
所以,这个用例显然访问了我们的 HTTP 客户端。我们可以实现访问 CoreData,或从文件读取,或使用模拟等。 API 始终相同,视图模型不关心。这里的美妙之处在于,你可以将它切换出来并换入另一个,视图模型仍然有效,你的视图也是如此。 (为了让它变得非常酷,您可以创建一个 AnyUseCase
泛型类型,这非常简单,这里是您的依赖注入)。
现在让我们看看视图模型的外观以及它如何使用用例:
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
@Published var viewState: ViewState = .undefined
let getCompany: GetCompanyUseCase
var getCompanyCancellable: AnyCancellable?
init(getCompany: GetCompanyUseCase) {
self.getCompany = getCompany
}
func load() {
self.getCompanyCancellable =
self.getCompany(with: 1)
.sink { (completion) in
print(completion)
} receiveValue: { (company) in
self.viewState = .value(company)
print("company set to: \(company)")
}
}
}
load
函数触发用例,调用底层http客户端加载公司数据。
当UseCasereturns一个公司时,它会被分配视图状态。观察者(视图,或 ViewController)将收到有关更改的通知,并可以执行更新。
您可以在 playground 中试验代码。以下是遗失的和平:
import Foundation
import Combine
extension String: Swift.Error {}
public enum HTTPResponse {
case information(response: HTTPURLResponse, data: Data)
case success(response: HTTPURLResponse, data: Data)
case redirect(response: HTTPURLResponse, data: Data)
case clientError(response: HTTPURLResponse, data: Data)
case serverError(response: HTTPURLResponse, data: Data)
case custom(response: HTTPURLResponse, data: Data)
}
class MockHTTPClient: HTTPClient {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error> {
let json = #"{"id": 1, "name": "Some Corporation"}"#.data(using: .utf8)!
let url = URL(string: "https://api.my.com/companies")!
let httpUrlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let response: HTTPResponse = .success(response: httpUrlResponse, data: json)
return Just(response)
.mapError { _ in "no error" }
.eraseToAnyPublisher()
}
}
Assemble:
let httpClient = MockHTTPClient()
let getCompany = GetCompany(httpClient: httpClient)
let viewModel = CompanyViewModel(getCompany: getCompany)
viewModel.load()