Swift: post only specific codable objects - swift

I am trying to work out how to upload only certain values via a put request. Essentially, I have an app whereby when the user accesses a specific screen, there are a bunch of fields, some of whose values will be pre-populated from an API (precisely which values are already populated is unpredictable). Any blank fields are editable and the user can put their own values in and upload them via a put request. However, I only want to upload new values (that were essentially 'nil' in the original API response). I do not want to override the values that came with the initial response and 're-upload' them. Here is the codable struct which essentially represents the body of the request:
struct UpdateRequest: Codable {
let total: Double?
let discrep: Double?
let percent: Double?
let pre: Double?
let target: Double?
let req: Double?
let dep: Double?
}
So all values are optional.
Here is my update method which is called once the user has completed the 'blanks' and pressed a specific button:
func updateVals() {
let args = ["id": viewModel.id]
guard let body = convertValues() else { return }
API.client.post(.order, with: args, using: .put, posting: body, expecting: MessageResponse.self) { (success, response) in
switch success {
case .failure:
DispatchQueue.asyncMain {
let viewController = WarningViewController.detailsUpdatedFailure()
self.present(viewController, animated: true, completion: nil)
}
case .success:
DispatchQueue.asyncMain {
let viewController = WarningViewController.detailsUpdatedSuccess()
self.present(viewController, animated: true, completion: nil)
}
}
}
}
}
And here is convertedValues() :
func convertValues() -> UpdateRequest? {
// In here I have a bunch of logic to convert values to specific units which is not relevant to this question
{
return UpdateRequest(
total: convertedTotal
discrep: convertedDiscrep
percent: convertedPercent
pre: convertedPre
target: convertedTarget
req: convertedReq
dep: convertedDep
)
}
return nil
}
However, if 'total' for example was in the initial API response I do not want to upload it to this request - as this end point is essentially the same as the 'get' endpoint from which I am populating these initial values.
Obviously I can write logic to check if the values were in the response, but how can I only upload new values to this UpdateRequest without uploading 'nil' values for the ones I don't need?

All you need to do is to override encode(to:) in UpdateRequest and use encodeIfPresent since that method will not include values that are nil.
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(total, forKey: .total)
try container.encodeIfPresent(discrep, forKey: .discrep)
//... and so on
}
Example with only total set
let req = UpdateRequest(total: 123.5, discrep: nil, percent: nil, pre: nil, target: nil, req: nil, dep: nil)
do {
let data = try JSONEncoder().encode(req)
print(String(data: data, encoding: .utf8))
} catch {
print(error)
}
outputs
Optional("{\"total\":123.5}")

Related

How to struct's value such that all views can access its values in Swift/SwiftUI?

Currently I am decoding a JSON response from an API and storing it into a struct "IPGeolocation". I want to be able to store this data in a variable or return an instance of this struct such that I can access the values in views.
Struct:
struct IPGeolocation: Decodable {
var location: Coordinates
var date: String
var sunrise: String
var sunset: String
var moonrise: String
var moonset: String
}
struct Coordinates: Decodable{
var latitude: Double
var longitude: Double
}
URL extension with function getResult:
extension URL {
func getResult<T: Decodable>(completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: self) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}
do {
completion(.success(try data.decodedObject()))
} catch {
completion(.failure(error))
}
}.resume()
}
}
Function that retrieves and decodes the data:
func getMoonTimes(lat: Double, long: Double) -> Void{
urlComponents.queryItems = queryItems
let url = urlComponents.url!
url.getResult { (result: Result<IPGeolocation, Error>) in
switch result {
case let .success(result):
print("Printing returned results")
print(result)
case let .failure(error):
print(error)
}
}
}
My goal is to take the decoded information and assign it to my struct to be used in views after. The results variable is already an IPGeolocation struct once the function runs. My question lies in the best way to store it or even return it if necessary.
Would it make sense to have getResult return an IPGeolocation? Are there better/different ways?
Thanks!
EDIT: I made changes thanks to help from below comments from Leo Dabus.
func getMoonTimes(completion: #escaping (IPGeolocation?,Error?) -> Void) {
print("STARTING FUNC")
let locationViewModel = LocationViewModel()
let apiKey = "AKEY"
let latitudeString:String = String(locationViewModel.userLatitude)
let longitudeString:String = String(locationViewModel.userLongitude)
var urlComponents = URLComponents(string: "https://api.ipgeolocation.io/astronomy?")!
let queryItems = [URLQueryItem(name: "apiKey", value: apiKey),
URLQueryItem(name: "lat", value: latitudeString),
URLQueryItem(name: "long", value: longitudeString)]
urlComponents.queryItems = queryItems
urlComponents.url?.getResult { (result: Result<IPGeolocation, Error>) in
switch result {
case let .success(geolocation):
completion(geolocation, nil)
case let .failure(error):
completion(nil, error)
}
}
}
To call this method from my view:
struct MoonPhaseView: View {
getMoonTimes(){geolocation, error in
guard let geolocation = geolocation else {
print("error:", error ?? "nil")
return
}
}
...
...
...
IMO it would be better to return result and deal with the error. Note that you are assigning the same name result which you shouldn't. Change it to case let .success(geolocation). What you need is to add a completion handler to your method because the request runs asynchronously and return an option coordinate and an optional error as well:
Note that I am not sure if you want to get only the location (coordinates) or the whole structure with your method. But the main point is to add a completion handler to your method and pass the property that you want or the whole structure.
func getMoonTimes(for queryItems: [URLQueryItem], completion: #escaping (Coordinates?,Error?) -> Void) {
urlComponents.queryItems = queryItems
urlComponents.url?.getResult { (result: Result<IPGeolocation, Error>) in
switch result {
case let .success(geolocation):
completion(geolocation.location, nil)
case let .failure(error):
completion(nil, error)
}
}
}
Usage:
getMoonTimes(for: queryItems) { location, error in
guard let location = location else {
print("error:", error ?? "nil")
return
}
print("Location:", location)
print("Latitude:", location.latitude)
print("Longitude:", location.longitude)
}

HTTP post request and save response in app

I'm totally new to swift and iOS programming so I'm a little lost on how to do this and even in what files I should be doing this too.
I'm trying to do a http post request to get calendar events and save them in the app to later use and display.
I made a model class with this code.
import UIKit
class Event {
var id: Int
var init_date: String
var end_date: String
var title: String
var description: String
var color_code: String
var all_day: Int
init?(id: Int, init_date: String, end_date: String, title: String, description: String, color_code: String, all_day: Int) {
//Initialization should fail if these are false
if id < 0 || init_date.isEmpty || end_date.isEmpty || title.isEmpty {
return nil
}
//Initialize stored properties
self.id = id
self.init_date = init_date
self.end_date = end_date
self.title = title
self.description = description
self.color_code = color_code
self.all_day = all_day
}
}
But now I don't know what the next step would be. I need this to be downloaded immediately once the app is opened for the first time and not when it's not being opened for the first time. Do I create a new method in the ViewController.swift for the download?
Right now I haven't added anything to the ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
What should I do next?
At this point you need to create a function that handles the POST request you are making.
Once completed, place this function inside your appDelegate main function didFinishLaunchingWithOptions. This is the function that executes on appStart
On a successful function call save the data (presumably json) into a Global Variable or whatever you need for you app.
TIP:
On you class
class Event: Codable {
}
make sure to add Codable like above
Below is an example of what your post request will look like
func myPostRequest(completionHandler: #escaping (Bool?, String?) -> Void){
guard let url = URL(string:"") else { return }
let parameters = ["": ""]
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil
else {
print(error as Any)
return
}
if let httpResponse = response as? HTTPURLResponse {
if (httpResponse.statusCode == 200) {
if let data = data {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]]
//print("^^^^^^^^^^^^^^",json)
for x in json ?? [] {
//here is where you will parse your data from the post request
}
completionHandler(true, nil)
return
}
} else {
completionHandler(false, "No Response From Server")
print("Failure response: STATUS CODE != 200")
}
} else {
completionHandler(false, "Database Connection Error")
print("Error \(error!)")
}
}
task.resume()
} catch let error {
completionHandler(false, "failure")
print("POSTERROR: \(error.localizedDescription)")
}
}
I use Alamofire, you can add it to your project via:
Pods
Swift Package Manager
When you add the framework you can use it:
import Alamofire
Then you need to make your class with the protocol Codable to pass the data to your class.
class Event: Codable { }
Then you need to call the url and store the response in a variable:
override func viewDidAppear(_ animated: Bool) {
AF.request("your API rest url").responseData { (resData) in
guard let data = resData.data else { return }//Check if the data is valid
do {
let decoder = JSONDecoder()//Initialize a Json decoder variable
let decodedData = try decoder.decode(Event.self, from: data)//Decode the response data to your decodable class
//Print the values
print(decodedData.headers)
print(decodedData.id)
print(decodedData.init_date)
print(decodedData.end_date)
} catch {
print(error.localizedDescription)
}
}
}

Can't access data outside of closure in swift

I am trying to extract an array from closure in swift 3 and its not working for me. I have my JSON parser in the class WeatherGetter and I am calling it in the view did load file in the viewcontroller.swift how to assign the weather_data array to some outside variable?
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "Insert API address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
//print(weather)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
//temps.updateValue(tempF, forKey: "\(date)//\(j)")
temps.append(tempF)
winds.append(windspeed)
}
}
//print(temps)
//print(winds)
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
}
//Do your stuff with isResponse variable.
}
You can assign it to a class property like this:
var weatherData: [[Double]]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
self.weatherData = weather_data
// reload or display data
}
}
You need to remember that the network request takes some time, so this is why you would call something like reloadData once you know you have received the response.
Say for example, the network response takes 100 milliseconds to respond. By the time the data has responded, all of the code in viewDidLoad will very likely be completely finished. So you need to respond to the data being received, when you receive it. If you have a bad mobile signal, it may take longer.
This is why you use callbacks/closures. They are called when the operation completes
UPDATE:
The code inside getWeather shows multiple errors for me and won't let me run it as is.
I managed to get a response from the weather API by modifying the code slightly and commenting alot out. Your main issue here is that you are not casting your JSON data to specific types.
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
guard error == nil, let data = data else {
print("ERROR")
return
}
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
if let weather = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:AnyObject] {
if let conditions1 = weather["data"] as? [String:AnyObject] {
print(conditions1)
}
}
} catch let jsonError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError)")
}
}
dataTask.resume()
See in the code above how I am optionally unwrapping the values whilst casting their types. This is what you need to do throughout your code and check you get the right data at each step along the way. Unfortunately the API response is too large for me to do it here.
Unwrapping JSON Swift
Swift Closures
iOS Networking with Swift - This is a free course which I highly recommend. This is how I learnt iOS networking.
As mentioned by #Scriptable, it takes a while for the response to be processed since it's asynchronous. What you can do is to add the OperationQueue.main.addOperation to assign the current process to the main queue. This will prioritize the processing of your network response. You can also put your reloadData in this part.
var weatherData: [Double]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
OperationQueue.main.addOperation {
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
// reloadData()
}
}
}

Swift3 Cannot assign value of type '()' to type '[Version]'

Hello I am having trouble calling my methods to the controller properly as I am getting this error Cannot assign value of type '()' to type '[Version]'. I need help fixing this, thanks.
Swift 3 Method:
var versions : [Version] = []
func isActiveVersion() -> Bool {
let api = versionAPI()
versions = api.getVersionFromAPI(completion: ({_ in }))
for version in versions{
if version["version"] == "1.0.0" {
return true
}
}
}
Swift 3 Call
public class versionAPI {
var versions : [Version] = []
//---------------------------------
// MARK: ENDPOINTS
//---------------------------------
let getVersionEndPoint = "http://127.0.0.1:3000/api/v1/versions"
//---------------------------------
// MARK: REQUESTS
//---------------------------------
func getVersionFromAPI(completion: #escaping ([Version]) -> Void){
let url = URL(string: getVersionEndPoint)
let task = URLSession.shared.dataTask(with: url! as URL) { data, response, error in
guard let data = data, error == nil else {
completion([])
return
}
print(NSString(data: data, encoding: String.Encoding.utf8.rawValue)!)
self.parseVersionsToJSON(data: data)
completion(self.versions)
}
task.resume()
}
func parseVersionsToJSON(data: Data) {
do {
self.versions = []
if let json = try JSONSerialization.jsonObject(with: data) as? [[String:Any]] {
for dic in json {
let version = Version()
version.version = dic["version"] as! String
version.active = dic["active"] as! Bool
self.versions.append(version)
}
}
}
catch{
}
}
}
Your function getVersionFromAPI sets up an asynchronous task and then immediately returns before that task completes, returning void to its caller. This is why you get the error you report.
The [Version] value produced by the asynchronous task is passed by that task to the completion function passed to getVersionFromAPI. The completion function you pass {_ in } does nothing, so the list of versions is simply discarded.
You cannot simply call an asynchronous task, which will complete at some future time, from a synchronous task, getVersionFromAPI in your case, and have that asynchronous task somehow become synchronous and return its future result immediately.
You need to study asynchronous design. Then either redesign your code so the task done by getVersionFromAPI is itself asynchronous, or use one of the standard techniques to block your synchronous method from proceeding until the asynchronous one has completed.
If after revising your design you have trouble with your code ask a new question, showing your code, and someone will undoubtedly help you.
HTH
versions = api.getVersionFromAPI(completion: ({_ in }))
getVersionFromAPI does not return anything. Declare a global struct then pass the data into it and use DispatchQueue when finished to post a NotificationCentre
do {
GlobalStruct.versions = []
if let json = try JSONSerialization.jsonObject(with: data) as? [[String:Any]] {
for dic in json {
let version = Version()
version.version = dic["version"] as! String
version.active = dic["active"] as! Bool
GlobalStruct.versions.append(version)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "gotIt"), object: nil)
}
}
}
your Swift 3 ViewController should have the following:
var versions: [Version] = []
func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(myFunction), name: NSNotification.Name(rawValue: "gotIt"), object: nil)
let api = versionAPI()
api.getVersionFromAPI()
super.viewDidLoad()
}
func myFunction() {
versions = GlobalStruct.versions
if isActiveVersion {
.....
}
}

Swift: Returning a result from inside dispatch_async

So I have a block of code here which doesn't work. This is due to the fact that it found nil while trying to unwrap an optional value. Which is because it gets initialized inside the asynchronous method. My question is, how do I hold off on returning the function until it has fetched the result?
struct Domain {
var name: String?
var tld: String?
var combined: String {
get {
return self.name!+self.tld!
}
}
var whoIs: String {
get {
if self.whoIs.isEmpty {
var result: String?
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let whois_url = NSURL(string: SEARCH_URL+self.combined+"/whois")
result = NSString(contentsOfURL: whois_url!, encoding: NSUTF8StringEncoding, error: nil)
print(result!)
})
return result!
}
return self.whoIs
}
}
}
If you want to wait for the results of the block, simply replace dispatch_async with dispatch_sync:
dispatch_sync(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let whois_url = NSURL(string: SEARCH_URL+self.combined+"/whois")
result = NSString(contentsOfURL: whois_url!, encoding: NSUTF8StringEncoding, error: nil)
print(result!)
})
This would ensure that the method does not return until the content of URL is fetched into the result.