Forming String from Struct via CustomStringConvertible - ios

I am trying to build a URL query as something similar
limit=20&offset=0
if there is no limit then URL query should look like
offset=0
I tried to form the string as follows, but my struct items are optionals; therefore, I am confused to come up with a string.
struct Filter {
var limit: Int?
var offset: Int?
}
extension Filter:CustomStringConvertible {
var description: String {
return "limit=\(limit)&offset=\(offset)"
}
}

If you really want to stick to the String version, here is my take:
extension Filter: CustomStringConvertible {
var description: String {
[
limit.map { "limit=\($0)" },
offset.map { "offset=\($0)" }
].compactMap { $0 }.joined(separator: "&")
}
}
But as there already are comments that guide you toward the good solution, I figured I might provide you with an example. Let's start with building query items for the Filter object.
extension Filter {
var queryItems: [URLQueryItem] {
var items = [URLQueryItem]()
if let limit = limit {
items.append(URLQueryItem(name: "limit", value: limit))
}
if let offset = offset {
items.append(URLQueryItem(name: "offset", value: offset))
}
return items
}
}
and now its super easy to build URL using URLComponents
let filter: Filter
var components = URLComponents()
components.scheme = "https" // example scheme
components.host = "api.github.com" // example url
components.path = "/search/repositories" // example path
components.queryItems = filter.queryItems
let url = components.url
John Sundell has a great article on Constructing URLs in Swift

Related

How can I handle Ineheritence for my data model Structs to have a Main Struct with different sub structs that represent different types?

I have various types of Worksheet structs that I use to populate a view, where the user fills out entries and these are then saved.
So far I've just used each struct individually, but I realized it would be more efficient to have a main 'Worksheet' struct that holds the common properties (id, date) so that I can reuse some views rather than creating a different view for every single type of Worksheet.
For example, here is how I read data of a certain worksheet:
if let diaryArray = vm.readData(of: [Diary](), from: "diary_data") {
let sortedArray = diaryArray.sorted(by: {$0.date > $1.date})
vm.diaries = sortedArray
}
I'd like this to be something like
if let diaryArray = vm.readData(of: [T](), from: "\(path)_data") {
let sortedArray = diaryArray.sorted(by: {$0.date > $1.date})
vm.worksheets = sortedArray
}
So, I won't know what type of worksheet it is yet, but that it is a worksheet that has a date property for sure, so I can sort it by that, with this approach I could just reuse this view and function instead of having to write this for every type.
I've tried two ways so far to achieve this with my structs:
firstly, using protocols:
protocol Worksheet: Codable, Identifiable, Hashable {
var id: String { get set }
var date: Date { get set }
}
extension Worksheet {
static func parseVehicleFields(id: String, date: Date) -> (String, Date) {
let id = ""
let date = Date()
return (id, date)
}
}
struct Diary: Worksheet {
var id: String
var date: Date
var title: String
var situation: String
var thoughts: String
var emotions: String
var physicalSensations: String
var behaviours: String
var analysis: String
var alternative: String
var outcome: String
init() {
self.id = ""
self.date = Date()
self.title = ""
self.situation = ""
self.thoughts = ""
self.emotions = ""
self.physicalSensations = ""
self.behaviours = ""
self.analysis = ""
self.alternative = ""
self.outcome = ""
}
}
struct Goal: Worksheet {
var mainGoal: String
var notificationActive: Bool
var notificationDate: Date
var obstacles: String
var positiveBehaviours: String
var immediateActions: String
var goalAchieved: Bool
init() {
self.mainGoal = ""
self.notificationActive = false
self.notificationDate = Date()
self.obstacles = ""
self.positiveBehaviours = ""
self.immediateActions = ""
self.goalAchieved = false
}
}
It seems close, but I get the error of 'Type 'any Worksheet' cannot conform to 'Decodable''
when trying this:
if let array = vm.readData(of: [any Worksheet](), from: "\(path)_data") {
let sortedArray = array.sorted(by: {$0.date > $1.date})
vm.worksheets = sortedArray
}
Here is the save function for reference:
func saveWorksheetProtocol<T: Worksheet>(for type: [T], to path: String) {
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let documentURL = (directoryURL?.appendingPathComponent(path).appendingPathExtension("json"))!
let jsonEncoder = JSONEncoder()
let data = try? jsonEncoder.encode(type)
do {
try data?.write(to: documentURL, options: .noFileProtection)
} catch {
print("Error...Cannot save data!!!See error: \(error.localizedDescription)")
}
}
It works if I use Diary as the parameter, but that defeats the point of trying to make this Generic to Worksheets, not their specific types.
Alternatively, I tried a method that creates a major Struct type of Worksheet, that contains the other types. This actually works nicely, except I have to hold empty data for EVERY type of worksheet, if I want to create an array of only 'Diary' entries for example, and this seems redundant and inefficient:
struct Worksheet: Codable, Identifiable, Hashable {
var id: String
var date: Date
var diary: Diary2
var goal: Goal2
var experiment: Experiment2
init() {
self.id = ""
self.date = Date()
self.diary = Diary2()
self.goal = Goal2()
self.experiment = Experiment2()
}
}
struct Diary: Codable, Hashable {
var title: String
var situation: String
var thoughts: String
var emotions: String
var physicalSensations: String
var behaviours: String
var analysis: String
var alternative: String
var outcome: String
init() {
self.title = ""
self.situation = ""
self.thoughts = ""
self.emotions = ""
self.physicalSensations = ""
self.behaviours = ""
self.analysis = ""
self.alternative = ""
self.outcome = ""
}
}
struct Goal: Codable, Hashable {
var mainGoal: String
var notificationActive: Bool
var notificationDate: Date
var obstacles: String
var positiveBehaviours: String
var immediateActions: String
var goalAchieved: Bool
init() {
self.mainGoal = ""
self.notificationActive = false
self.notificationDate = Date()
self.obstacles = ""
self.positiveBehaviours = ""
self.immediateActions = ""
self.goalAchieved = false
}
}
So here I can just find a specific 'Worksheet', sort it, and then later access the values of whichever one it is, i.e. worksheet.diary.title, worksheet.goal.title etc. But this means each entry has redundant values of every other, and I tried making each type optional, but I could not then use them with bindings, as I was getting unresolvable unwrap errors, if the type i.e. 'Diary?' was optional in the Worksheet struct:
TextField("", text: $entry.diary!.title, axis: .vertical) <- error would be here, even when checking if diary exist first.
.lineLimit(20)
.disabled(!editMode)
So I am looking for a way to handle my Worksheet types more efficiently, so that I can reduce repetitive code for functions and views where possible.

Swift prevent Optional("") or nil in string concatenate result | Create query string from object

I have struct like this:
struct Request {
var page: Int
var name: String?
var favoriteName: String?
var favoriteId: Int?
}
Then I convert it to Dictionary
func toDict() -> [String:Any] {
var dict = [String:Any]()
let otherSelf = Mirror(reflecting: self)
for child in otherSelf.children {
if let key = child.label {
dict[key] = child.value
}
}
return dict
}
If I loop it to modify and concatenate using those Dict key & value to create query string:
var queryString: String {
var output: String = ""
for (key,value) in toDict() {
output += "\(key)" + "=\(value)&"
}
output = String(output.dropLast())
return output
}
Does anyone know how to prevent nil, Optional(""), Optional(25) string added in the concatenation process ?
current result: page=20&name=nil&favoriteName=Optional("")&favoriteId=Optional(25)
expected result: page=20&name=&favoriteName=&favoriteId=25
Edit: Thank you everyone , wasn't really expecting so much answer tho. Let me edit the title to help future developers search.
The best practice is to use the URLComponents and URLQueryItem structs.
This is my approach to solving your problem.
First I added an enum to avoid having hardcoded strings.
struct Request {
var page: Int
var name: String?
var favoriteName: String?
var favoriteId: Int?
}
enum RequestValues: String {
case page
case name
case favoriteName
case favoriteId
}
Then I made this helper function to return the non nil vars from the Request instance as an array of URLQueryItem.
func createQuery(request: Request) -> [URLQueryItem] {
var queryItems: [URLQueryItem] = []
queryItems.append(URLQueryItem(name: RequestValues.page.rawValue, value: "\(request.page)"))
if let name = request.name {
queryItems.append(URLQueryItem(name: RequestValues.name.rawValue, value: name))
}
if let favoriteName = request.favoriteName {
queryItems.append(URLQueryItem(name: RequestValues.favoriteName.rawValue, value: favoriteName))
}
if let favoriteId = request.favoriteId {
queryItems.append(URLQueryItem(name: RequestValues.favoriteId.rawValue, value: "\(favoriteId)"))
}
return queryItems
}
Then you can get the query string like this:
let queryString = queryItems.compactMap({ element -> String in
guard let value = element.value else {
return ""
}
let queryElement = "\(element.name)=\(value)"
return queryElement
})
this will give you the expected result in your question.
page=20&name=&favoriteName=&favoriteId=25
But you should use the URLComponents struct to build your url as such.
func buildURL() -> URL? {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "google.com"
urlComponents.queryItems = queryItems
urlComponents.path = "/api/example"
urlComponents.url
guard let url = urlComponents.url else {
print("Could not build url")
return nil
}
return url
}
This would will give you the url with the query.
It would look like this :
https://google.com/api/example?page=5&name=nil&favoriteName=Hello&favoriteId=9
In model have optioanl var.
First, make your dictionary value optional
func toDict() -> [String: Any?] {
and then use the default value
var queryString: String {
var output: String = ""
for (key,value) in toDict() {
output += "\(key)" + "=\(value ?? "")&" // <== Here
}
output = String(output.dropLast())
return output
}
You can also prevent nil value in string
var queryString: String {
var output: String = ""
for (key,value) in toDict() where value != nil { //< Here
output += "\(key)" + "=\(value ?? "")&" //< Here
}
output = String(output.dropLast())
return output
}
Check if value is nil before insert it in the output, if the value is nil you can provide a default value:
var queryString: String {
var output: String = ""
for (key,value) in toDict() {
output += "\(key)"
if let value = value{
output += "=\(value)&"
}else{
output += "=DefaultValue&" // Replace DefaultValue with what is good for you
}
}
output = String(output.dropLast())
return output
}
This is more concise:
var queryString: String {
var output: String = ""
for (key,value) in toDict() {
output += "\(key)" + "=\(value ?? "DefaultValue")&"
}
output = String(output.dropLast())
return output
}
Instead of manually creating your string you should use URLComponents to generate your URL and use URLQueryItems for your parameters. I would make your structure conform to Encodable and encode NSNull in case the value is nil. Then you just need to pass an empty string to your query item when its value is NSNull:
This will force encoding nil values:
extension Request: Codable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(page, forKey: .page)
try container.encode(name, forKey: .name)
try container.encode(favoriteName, forKey: .favoriteName)
try container.encode(favoriteId, forKey: .favoriteId)
}
}
let request = Request(page: 20, name: nil, favoriteName: nil, favoriteId: 25)
do {
let dictiony = (try JSONSerialization.jsonObject(with: JSONEncoder().encode(request))) as? [String: Any] ?? [:]
let queryItems: [URLQueryItem] = dictiony.reduce(into: []) {
$0.append(.init(name: $1.key, value: $1.value is NSNull ? "" : String(describing: $1.value)))
}
print(queryItems)
var components = URLComponents()
components.scheme = "https"
components.host = "www.google.com"
components.path = "/search"
components.queryItems = queryItems
if let url = components.url {
print(url)
}
} catch {
print(error)
}
This will print
[name=, favoriteName=, page=20, favoriteId=25]
https://www.google.com/search?favoriteName=&name=&page=20&favoriteId=25

how to pass value to struct dynamically

I am trying to form an URL dynamically.
where I am using Struct to construct it form resultant url
Constants.Swift
import Foundation
private struct Domains {
static let Local = "localhost IP"
static let QA = "QA.Environment"
static let UAT = "http://test-UAT.com"
}
// HardCoded URLRoutes
private struct URLRoutes{
static let query = "query=bitcoin&"
static let date = "date=2019-04-04&"
static let sortBy = "sortBy=Date&"
}
private static let constructionURL = Domain.local+URLRoutes.query + URLRoutes.date + URLRoutes.sortBy + API.Key
static var ResultantURL: String {
return constructionURL
}
I am trying to make this dynamic to pass value from other class to form the dynamic url.
private struct URLRoutes{
var query : String
var date : String?
var sortBy : String?
}
From another Class trying to access the resultant URL
url = URL(string: URLRoutes.ResultantURL)!
but how can I construct the formate of url from the another class?
static let query = "query=bitcoin&"
static let date = "date=2019-04-04&"
static let sortBy = "sortBy=Date&"
Your inputs will guide me.
Here's playground code that does what you want:
struct API {
static let Key = "ABC123"
}
struct URLRoutes{
var query : String
var date : String?
var sortBy : String?
var constructionURL: String {
return query + (date ?? "") + (sortBy ?? "") + API.Key
}
}
let query = "query=bitcoin&"
let date = "date=2019-04-04&"
let sortBy = "sortBy=Date&"
let myRoute = URLRoutes(query: query, date: date, sortBy: sortBy)
print(myRoute.constructionURL)
However, this isn't really ideal and doesn't use the constructs that Apple provides. Here's another approach:
struct URLRoute {
var queryItems:[URLQueryItem]
init(query: String, date:String?, sortBy:String?) {
queryItems = [
URLQueryItem(name: "query", value: query),
URLQueryItem(name: "date", value: date),
URLQueryItem(name: "sortBy", value: sortBy),
URLQueryItem(name: "api_key", value: API.Key)
]
}
var constructionURL:String {
get {
var component = URLComponents(string: "")
component?.queryItems = queryItems
return component?.string ?? ""
}
}
}
let betterRoute = URLRoute(query: "bitcoin", date: "2019-04-04", sortBy: "Date")
print(betterRoute.constructionURL)
You can use URLComponents to do lots of heavy lifting for you in creating valid URLs.

Dictionary of Dictionaries of Dictionaries of Dictionaries syntax understanding

I need to map the doors of buildings into a single map, from which afterward I need to populate three pickers using this data.
Each door contains the following data: building, level, range, door number and other information that is less relevant. So I created the following map:
public var doorsMap: [String : [String : [String : [String: Door]]]] = [:]
and a have a list of doors that I need to populate this map with, the problem is that I can't understand what should be the right syntax to perform this task, I tried:
doorsMap[door.building]?[door.level]?[door.range]?[door.number] = door
but this doesn't create the inner sets of dictionaries. when I tried to do:
doorsMap[door.building]![door.level]![door.range]![door.number] = door
Obviously, I get the:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
because I try to unwrap a nil value.
So what would be the correct syntax in swift to populate this map from a list of doors?
A single assignment won't create the multiple, intermediate directories. You need to do it explicitly.
You could use something like this:
func add(door: Door) {
var building = self.doorsMap[door.building] ?? [String : [String:[String: Door]]]()
var level = building[door.level] ?? [String : [String: Door]]()
var range = level[door.range] ?? [String:Door]
range[door.number] = door
level[door.range] = range
building[door.level] = level
self.doorsMap[door.building] = building
}
Personally, I would look for a better data structure, perhaps use a struct to hold the doorsMap. This struct could have functions to handle the insertion and retrieval of doors.
Perhaps something like this:
struct Door {
let building: String
let level: String
let range: String
let number: String
}
struct DoorMap {
private var buildingsSet = Set<String>()
private var levelsSet = Set<String>()
private var rangesSet = Set<String>()
private var numberSet = Set<String>()
private var doorsArray = [Door]()
var buildings: [String] {
get {
return Array(buildingsSet).sorted()
}
}
var levels: [String] {
get {
return Array(levelsSet).sorted()
}
}
var ranges: [String] {
get {
return Array(rangesSet).sorted()
}
}
var numbers: [String] {
get {
return Array(numberSet).sorted()
}
}
var doors: [Door] {
get {
return doorsArray
}
}
mutating func add(door: Door) {
buildingsSet.insert(door.building)
levelsSet.insert(door.level)
rangesSet.insert(door.range)
numberSet.insert(door.number)
doorsArray.append(door)
}
func doorsMatching(building: String? = nil, level: String? = nil, range: String? = nil, number: String? = nil) -> [Door]{
let matches = doorsArray.filter { (potentialDoor) -> Bool in
var included = true
if let b = building {
if potentialDoor.building != b {
included = false
}
}
if let l = level {
if potentialDoor.level != l {
included = false
}
}
if let r = range {
if potentialDoor.range != r {
included = false
}
}
if let n = number {
if potentialDoor.number != n {
included = false
}
}
return included
}
return matches
}
}
var map = DoorMap()
let d1 = Door(building: "b1", level: "1", range: "r1", number: "1")
let d2 = Door(building: "b1", level: "2", range: "r1", number: "2")
let d3 = Door(building: "b2", level: "2", range: "r1", number: "2")
map.add(door: d1)
map.add(door: d2)
map.add(door: d3)
let b1Doors = map.doorsMatching(building:"b1")
let level2Doors = map.doorsMatching(level:"2")
let allBuildings = map.buildings()
Now, maybe you have more information on buildings and levels etc, so they could be structs too instead of just strings.

swift: cast incoming json array to dictionary and object

I am toying around/learning Swift and JSON atm and have problems correctly casting around my responses.
So, what I am trying to achieve is displaying a list of "posts" via Alamofire request. I have no problems displaying "first-level" json items like the post-ID or the "message", but when it comes to the author array I am kinda lost.
the json response comes in as an Array with no name, hence the first [
[
-{
__v: 1,
_id: "54428691a728c80424166ffb",
createDate: "2014-10-18T17:26:15.317Z",
message: "shshshshshshshhshshs",
-author: [
-{
_id: "54428691a728c80424166ffa",
userId: "543270679de5893d1acea11e",
userName: "foo"
}
]
}
Here is my corresponding VC:
Alamofire.request(.GET, "\(CurrentConfiguration.serverURL)/api/posts/\(CurrentConfiguration.currentUser.id)/newsfeed/\(CurrentConfiguration.currentMode)",encoding:.JSON)
.validate()
.responseJSON {(request, response, jsonData, error) in
let JSON = jsonData as? NSArray
self.loadPosts(JSON!)
}
tableView.delegate = self
tableView.dataSource = self
}
func loadPosts(posts:NSArray) {
for post in posts {
let id = post["_id"]! as NSString!
let message = post["message"]! as NSString!
var authorArray = post["author"]! as? [Author]!
println(authorArray)
var author:Author = Author()
author.userName = "TEST ME"
var postObj:Post = Post()
postObj.id = id
postObj.message = message
postObj.author = author
uppDatesCollection.append(postObj)
}
println(self.uppDatesCollection.count)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
my Models for Post
class Post {
var id:String!
var message:String!
var createDate: NSDate!
var author:Array<Author>!
init () {
}
}
and Author
class Author {
var id:String?
var userId:String?
var userName:String?
init () {
}
What is the best Approach here? Should you recast the returning Array as a Dictionary and then Access it via .valueforkey? Do you somehow iterate over the array to get this stuff out?
Obviously you can not say
author.name = authorArray[3] as String
Let's say you had Post class like so:
class Post : Printable {
var identifier:String!
var message:String!
var createDate: NSDate!
var authors:[Author]!
var description: String { return "<Post; identifier = \(identifier); message = \(message); createDate = \(createDate); authors = \(authors)" }
}
Your JSON has only one author in it, but given that it appears to be an array in the JSON, I assume it should be an array in the object model, too, as shown above. The Author class might be defined as so:
class Author : Printable {
var identifier:String!
var userId:String!
var userName:String!
var description: String { return "<Author; identifier = \(identifier); userId = \(userId); userName = \(userName)>" }
}
I wasn't sure why you made some of your optionals implicitly unwrapped (defined with !) and others not (defined with ?), so I just made them all implicitly unwrapped. Adjust as appropriate to your business rules.
Also, let's say your JSON looked like so (I wasn't quite sure what to make of the - in the JSON in your question, so I cleaned it up and added a second author):
[
{
"__v": 1,
"_id": "54428691a728c80424166ffb",
"createDate": "2014-10-18T17:26:15.317Z",
"message": "shshshshshshshhshshs",
"author": [
{
"_id": "54428691a728c80424166ffa",
"userId": "543270679de5893d1acea11e",
"userName": "foo"
},
{
"_id": "8434059834590834590834fa",
"userId": "345903459034594355cea11e",
"userName": "bar"
}
]
}
]
Then the routine to parse it might look like:
func loadPosts(postsJSON: NSArray) {
var posts = [Post]()
for postDictionary in postsJSON {
let post = Post()
let createDateString = postDictionary["createDate"] as String
post.message = postDictionary["message"] as String
post.identifier = postDictionary["_id"] as String
post.createDate = createDateString.rfc3339Date()
if let authorsArray = postDictionary["author"] as NSArray? {
var authors = [Author]()
for authorDictionary in authorsArray {
let author = Author()
author.userId = authorDictionary["userId"] as String
author.userName = authorDictionary["userName"] as String
author.identifier = authorDictionary["_id"] as String
authors.append(author)
}
post.authors = authors
}
posts.append(post)
}
// obviously, do something with `posts` array here, either setting some class var, return it, whatever
}
And this is my conversion routine from String to NSDate:
extension String {
/// Get NSDate from RFC 3339/ISO 8601 string representation of the date.
///
/// For more information, see:
///
/// https://developer.apple.com/library/ios/qa/qa1480/_index.html
///
/// :returns: Return date from RFC 3339 string representation
func rfc3339Date() -> NSDate? {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
return formatter.dateFromString(self)
}
}

Resources