How does a view obtain data using a view model and Network API - api

I'm trying to fetch some data with this helper file:
https://gist.github.com/jbfbell/e011c5e4c3869584723d79927b7c4b68
Here's a snippet of the important code:
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)
}
}
}
}
Extension 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
}
My goal is to fetch the data from responseJSON after the data of the url request is loaded.
My ViewModel currently looks like this:
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
}
}
So in my ViewModel the didSet gets called once but not when it should store the data. The results of AlphaVantageRequest always prints out properly, but not in my ViewModel. How can I achieve to have the loaded data also in my ViewModel?

When you use a view model which is an ObservableObject, your view wants to observe published properties, usually a viewState (MVVM terminology):
class CompanyViewModel: ObservableObject {
enum ViewState {
case undefined
case value(Company)
}
#Published var viewState: ViewState = .undefined
viewState completely describes how your view will be rendered. Note, that it can be undefined - which your view should be able to handle.
Adding a loading(Company?) case would also be a good idea. Your view can then render a loading indicator. Note that loading also provides an optional company value. You can then render a "refresh", in which case you already have a company value while also drawing a loading indicator.
In order to fetch some data from an endpoint, you may use the following abstraction:
public protocol HTTPClient: class {
func publisher(for request: URLRequest) -> AnyPublisher<HTTPResponse, Swift.Error>
}
This can be implemented by a simple wrapper around URLSession with 5 lines of code. A conforming type may however do much more: it may handle authentication, authorization, it may retry requests, refresh access tokens, or present user interfaces where the user needs to authenticate, etc. This simple protocol is sufficient for all this.
So, how does your ViewModel get the data?
It makes sense to introduce another abstraction: "UseCase" which performs this task, and not let the view model directly use the HTTP client.
A "use case" is simply an object that performs a task, taking an input and producing an output or error. You can name it how you want, "DataProvider", "ContentProvider" or something like this. "Use Case" is a well known term, though.
Conceptually, it has a similar API as an HTTP client, but semantically it sits on a higher level:
public protocol UseCase {
associatedtype Input: Encodable
associatedtype Output: Decodable
associatedtype Error
func callAsFunction(with input: Input) -> AnyPublisher<Output, Error>
}
Lets create us a "GetCompany" use case:
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 { $0 } // 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)
}
}
So, this Use Case clearly accesses our HTTP client. We can implement this accessing CoreData, or read from file, or using a mock, etc. The API is always the same, and the view model does not care. The beauty here is, you can switch it out and swap in another one, the view model still works and also your view. (In order to make this really cool, you would create a AnyUseCase generic type, which is very easy, and here you have your dependency injection).
Now lets see how the view model may look like and how it uses the Use Case:
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)")
}
}
}
The load function triggers the use case, which calls the underlying http client to load the company data.
When the UseCase returns a company, it will be assigned the view state. Observers (the view, or ViewController) will get notified about the change and can preform an update.
You can experiment with code in playground. Here are the missing peaces:
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()

Related

How to return a Flux in async/reactive webclient request with subscribe method

I am using spring hexagonal architecture (port and adapter) as my application need to read the stream of data from the source topic, process/transforms the data, and send it to destination topic.
My application need to do the following actions.
Read the data (which will have the call back url)
Make an http call with the url in the incoming data (using webclient)
Get the a actual data and it needs to be transformed into another format.
Send the transformed data to the outgoing topic.
Here is my code,
public Flux<TargeData> getData(Flux<Message<EventInput>> message)
{
return message
.flatMap(it -> {
Event event = objectMapper.convertValue(it.getPayload(), Event.class);
String eventType = event.getHeader().getEventType();
String callBackURL = "";
if (DISTRIBUTOR.equals(eventType)) {
callBackURL = event.getHeader().getCallbackEnpoint();
WebClient client = WebClient.create();
Flux<NodeInput> nodeInputFlux = client.get()
.uri(callBackURL)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptTypes = new ArrayList<>();
acceptTypes.add(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(acceptTypes);
})
.exchangeToFlux(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(NodeInput.class);
}
return Flux.empty();
});
nodeInputFlux.subscribe( nodeInput -> {
SourceData source = objectMapper.convertValue(nodeInput, SourceData.class);
// return Flux.fromIterable(this.TransformImpl.transform(source));
});
}
return Flux.empty();
});
}
The commented line in the above code is giving the compilation as subscribe method does not allow return types.
I need a solution "without using block" here.
Please help me here, Thanks in advance.
I think i understood the logic. What do you may want is this:
public Flux<TargeData> getData(Flux<Message<EventInput>> message) {
return message
.flatMap(it -> {
// 1. marshall and unmarshall operations are CPU expensive and could harm event loop
return Mono.fromCallable(() -> objectMapper.convertValue(it.getPayload(), Event.class))
.subscribeOn(Schedulers.parallel());
})
.filter(event -> {
// 2. Moving the if-statement yours to a filter - same behavior
String eventType = event.getHeader().getEventType();
return DISTRIBUTOR.equals(eventType);
})
// Here is the trick 1 - your request below return Flux of SourceData the we will flatten
// into a single Flux<SourceData> instead of Flux<List<SourceData>> with flatMapMany
.flatMap(event -> {
// This WebClient should not be created here. Should be a singleton injected on your class
WebClient client = WebClient.create();
return client.get()
.uri(event.getHeader().getCallbackEnpoint())
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(SourceData.class);
}
return Flux.empty();
});
})
// Here is the trick 2 - supposing that transform return a Iterable of TargetData, then you should do this and will have Flux<TargetData>
// and flatten instead of Flux<List<TargetData>>
.flatMapIterable(source -> this.TransformImpl.transform(source));
}

SwiftUI OpenWeatherApi App show no content

Im new to swift development and I can't make myself pass through this. I run my app and by pressing the "action" button I see no change on my text which should be the current temperature. And It's giving me an error "nw_protocol_get_quic_image_block_invoke dlopen libquic failed"
My code:
struct ContentView: View {
#State var temp = Int()
func CallApi(){
let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=budapest&appid=#########################################")
URLSession.shared.dataTask(with: url!){data, response, error in
if let data = data {
if let DecodedData = try? JSONDecoder().decode(Api.self, from: data){
self.temp = DecodedData.main.temp
}
}
}.resume()
}
struct Api: Codable, Hashable{
var main: DataStructre
}
struct DataStructre: Codable, Hashable {
var temp: Int
}
var body: some View {
Text("\(temp)")
Button("Idojaras"){CallApi()}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you use do/try/catch instead of try?, you can get a useful error message.
do {
let DecodedData = try JSONDecoder().decode(Api.self, from: data)
self.temp = DecodedData.main.temp
} catch {
print(error)
}
In this case, I get:
Parsed JSON number <304.31> does not fit in Int.
Changing the type of temp (in both the #State variable and the DataStructre) fixes the issue.
In general, I'd recommend pasting your JSON into app.quicktype.io, which will give you generated models with the correct types/structures.
Slightly unrelated to your issue, but you may want to look into using dataTaskPublisher and Combine in SwiftUI rather than dataTask (https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine). Also, function names in Swift are generally lowercased.

Alamofire 3.0.0-beta.3 Image Response Serialization

Can some one help me please, I'm trying to implement Alamofire image response serialization method : imageResponseSerializer,
Here is my code :
extension Alamofire.Request {
class func imageResponseSerializer() -> ResponseSerializer<UIImage, NSError> {
return ResponseSerializer<UIImage, NSError> { request, response, data, error in
guard let validData = data else {
let failureReason = "Data could not be serialized. Input data was nil."
let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
return .Failure(error)
}
if let image = UIImage(data: validData, scale: UIScreen.mainScreen().scale) {
return Result<UIImage, NSError>.Success(image)
}
else {
return .Failure(Error.errorWithCode(.JSONSerializationFailed, failureReason: "Unable to create image."))
}
}
}
func responseImage(completionHandler: (NSURLRequest?, NSHTTPURLResponse?, Result<UIImage, NSError>) -> Void) -> Self {
return response(responseSerializer: Request.imageResponseSerializer(), completionHandler: { request, response, result in
completionHandler(request, response, result)
})
}
}
Error : cannot call value of non-function type 'NSHTTPURLResponse?'
I'm using : Xcode 7.0.1, Swift 2 and Alamofire 3.0.0-beta.3
Thank you,
You should really check out AlamofireImage. 1) It has all this implemented already. 2) It has many other awesome features that you will most likely find handy.

how to set a default http header with alamofire

I want to set a header so that all requests include an authorized token header once the user logs in. I thought I could set the header in the shared alamofire manager by calling my setToken function but I don't think I'm doing it correctly. What's my mistake? Is there a better way of setting a default header for all alamofire requests?
class UserService : NSObject {
static let manager: Alamofire.Manager = Alamofire.Manager(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
class func setToken(token:String){
manager.session.configuration.HTTPAdditionalHeaders = [
"x-token": token
]
}
class func addFriend(user:LoggedUser, uname:String, callback:((success: Bool, errorMsg: String?)->Void)) {
let params:[String : AnyObject] = ["uname": uname]
let url = AppConfig.sharedInstance().baseURL() + "/user/\(user.userId!)/friends"
Alamofire.request(.POST, url , parameters:params, encoding:.JSON)
.responseJSON(options: .MutableContainers, completionHandler:{ (request, response, JSON, error) -> Void in
//token is not set in the request
}
}
Your code never make use of your custom manager, use following code:
class CustomManager: Manager {
static public let manager = CustomManager.generateManager()
class func generateManager()-> CustomManager {
var defaultHeaders = Alamofire.Manager.defaultHTTPHeaders ?? [:]
defaultHeaders["x-token""] = "token"
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.HTTPAdditionalHeaders = defaultHeaders
let manager = CustomManager(configuration: configuration)
return manager
}
}
class UserService : NSObject {
class func addFriend(user:LoggedUser, uname:String, callback:((success: Bool, errorMsg: String?)->Void)) {
let params:[String : AnyObject] = ["uname": uname]
let url = AppConfig.sharedInstance().baseURL() + "/user/\(user.userId!)/friends"
CustomManager.manager(.POST, url , parameters:params, encoding:.JSON)
.responseJSON(options: .MutableContainers, completionHandler:{ (request, response, JSON, error) -> Void in
// ...
}
}
If you were using a custom SessionManager in Alamofire 4, now in Alamofire 5 it's called Session and you can create it this way:
let configuration = URLSessionConfiguration.default
var defaultHeaders = HTTPHeaders.default
defaultHeaders["X-API-Version"] = "1.0"
configuration.headers = defaultHeaders
let yourManager = Alamofire.Session(configuration: configuration)
So my mistake was using the class function rather than the shared instance when I made the request.
So it should be
Alamofire.Manager.sharedInstance.request(.POST, url , parameters:params, encoding:.JSON)
rather than
Alamofire.request(.POST, url , parameters:params, encoding:.JSON)
Create a class that conforms to protocol RequestAdapter. You will have to implement this function func adapt(_ urlRequest: URLRequest) throws -> URLRequest {. Inside it do:
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
urlRequest.setValue("YourUserAgent", forHTTPHeaderField: "User-Agent")
return urlRequest
}
In this function you can add whatever headers you want.
And then do:
sessionManager.adapter = "Class that conforms to RequestAdapter", where sessionManager is your Alamofire SessionManager.

Customize binding in ASP.NET Web Api

I am stuck with following problem in ASP.NET Web Api. Let say I have following code in my ApiController:
public void Post(Person person)
{
// Handle the argument
}
What I would like to do is to accept following JSON request:
{
"person": {
"name": "John Doe",
"age": 27
}
}
I would like to go around creating some holding object for each model just to properly bind incoming data. In previous version of MVC, it was possible to define something like Prefix to solve this.
Let me report that I have been able to solve this implementing CustomJsonMediaTypeFormatter:
public class EmberJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
public override System.Threading.Tasks.Task<object> ReadFromStreamAsync(
Type type,
System.IO.Stream readStream,
System.Net.Http.HttpContent content,
IFormatterLogger formatterLogger)
{
return base.ReadFromStreamAsync(
typeof(JObject),
readStream,
content,
formatterLogger).ContinueWith<object>((task) =>
{
var data = task.Result as JObject;
var prefix= type.Name.ToLower();
if (data[prefix] == null)
{
return GetDefaultValueForType(type);
}
var serializer = JsonSerializer.Create(SerializerSettings);
return data[prefix].ToObject(type, serializer);
});
}
}
and replacing default JsonMediaTypeFormatter in GlobalConfiguration.