From dac67bd529a7a0c0d865fa56e9d5d0d6c9a0c872 Mon Sep 17 00:00:00 2001 From: Jace Kalms Date: Mon, 19 Feb 2018 16:30:46 +0100 Subject: [PATCH 1/9] Update example in readme to correctly handle special characters Characters like ampersands aren't encoded when they are part of the value of a query parameter. We now suggest the use of URLComponents to create the URL. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 582a906..e7d8ce9 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,10 @@ do { let encoder = JSONEncoder() let data = try encoder.encode(container) let json = String.init(data: data, encoding: .utf8)! - let jsonEncoded = json.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! - let url = URL(string: "things:///add-json?data=\(jsonEncoded)")! + var components = URLComponents.init(string: "things:///add-json")! + let queryItem = URLQueryItem.init(name: "data", value: json) + components.queryItems = [queryItem] + let url = components.url! UIApplication.shared.open(url, options: [:], completionHandler: nil) } catch { From 76678450ce1cd594e591b0948aec74b803c5d973 Mon Sep 17 00:00:00 2001 From: Jace Kalms Date: Mon, 16 Apr 2018 14:09:31 +0200 Subject: [PATCH 2/9] Add support for Things 3.5 features Including: - Updating objects - Completion and cancel dates --- README.md | 6 +- ThingsJSON.swift | 218 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 205 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e7d8ce9..60e388c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Things JSON Coder -This repo contains a Swift file that allows the creation of the JSON required to be passed to the `add-json` command of Things’ URL scheme. +This repo contains a Swift file that allows the creation of the JSON required to be passed to the `json` command of Things’ URL scheme. ## Installation @@ -33,6 +33,9 @@ There are two wrapper enums used to package objects into arrays. Associated valu * `TJSProject.Item` – This enum has cases for todo and heading objects. Only todo and heading objects can be items inside a project. +#### Dates +Dates should be formatted according to ISO8601. Setting the JSON encoder’s `dateEncodingStrategy` to `ThingsJSONDateEncodingStrategy()` is the easiest way to do this (see example below). + ## Example Create two todos and a project, encode them into JSON and send to Things’ add command. @@ -52,6 +55,7 @@ let container = TJSContainer(items: [.todo(todo1), .project(project)]) do { let encoder = JSONEncoder() + encoder.dateEncodingStrategy = ThingsJSONDateEncodingStrategy() let data = try encoder.encode(container) let json = String.init(data: data, encoding: .utf8)! var components = URLComponents.init(string: "things:///add-json")! diff --git a/ThingsJSON.swift b/ThingsJSON.swift index fe75d85..b8029eb 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -94,14 +94,38 @@ class TJSContainer : Codable { class TJSModelItem { fileprivate var type: String = "" + /// The operation to perform on the object. + var operation: Operation + + /// The ID of the item to update. + var id: String? + private enum CodingKeys: String, CodingKey { case type + case operation + case id case attributes } + enum Operation: String, Codable { + /// Create a new item. + case create = "create" + /// Update an existing item. + /// + /// Requires id to be set. + case update = "update" + } + + init(operation: Operation, id: String? = nil) { + self.operation = operation + self.id = id + } + fileprivate func attributes(_ type: T.Type, from decoder: Decoder) throws -> KeyedDecodingContainer { let container = try decoder.container(keyedBy: CodingKeys.self) let decodedType = try container.decode(String.self, forKey: .type) + self.operation = try container.decodeIfPresent(Operation.self, forKey: .operation) ?? .create + self.id = try container.decodeIfPresent(String.self, forKey: .id) guard decodedType == self.type else { let description = String.init(format: "Expected to decode a %@ but found a %@ instead.", self.type, decodedType) let errorContext = DecodingError.Context.init(codingPath: [CodingKeys.type], debugDescription: description) @@ -114,6 +138,8 @@ class TJSModelItem { fileprivate func attributes(_ type: T.Type, for encoder: Encoder) throws -> KeyedEncodingContainer { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.type, forKey: .type) + try container.encode(self.operation, forKey: .operation) + try container.encodeIfPresent(self.id, forKey: .id) return container.nestedContainer(keyedBy: T.self, forKey: .attributes) } } @@ -125,72 +151,110 @@ class TJSModelItem { class TJSTodo : TJSModelItem, Codable { var title: String? var notes: String? + var prependNotes: String? + var appendNotes: String? var when: String? var deadline: String? var tags: [String]? + var addTags: [String]? var checklistItems: [TJSChecklistItem]? + var prependChecklistItems: [TJSChecklistItem]? + var appendChecklistItems: [TJSChecklistItem]? var listID: String? var list: String? var heading: String? var completed: Bool? var canceled: Bool? + var creationDate: Date? + var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case notes + case prependNotes = "prepend-notes" + case appendNotes = "append-notes" case when case deadline case tags + case addTags = "add-tags" case checklistItems = "checklist-items" + case prependChecklistItems = "prepend-checklist-items" + case appendChecklistItems = "append-checklist-items" case listID = "list-id" case list case heading case completed case canceled + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new todo configured with the provided values. - init(title: String? = nil, + init(operation: Operation = .create, + id: String? = nil, + title: String? = nil, notes: String? = nil, + prependNotes: String? = nil, + appendNotes: String? = nil, when: String? = nil, deadline: String? = nil, tags: [String]? = nil, + addTags: [String]? = nil, checklistItems: [TJSChecklistItem]? = nil, + prependChecklistItems: [TJSChecklistItem]? = nil, + appendChecklistItems: [TJSChecklistItem]? = nil, listID: String? = nil, list: String? = nil, heading: String? = nil, completed: Bool? = nil, - canceled: Bool? = nil) { + canceled: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation, id: id) self.type = "to-do" self.title = title self.notes = notes + self.prependNotes = prependNotes + self.appendNotes = appendNotes self.when = when self.deadline = deadline self.tags = tags + self.addTags = addTags self.checklistItems = checklistItems + self.prependChecklistItems = prependChecklistItems + self.appendChecklistItems = appendChecklistItems self.listID = listID self.list = list self.heading = heading self.completed = completed self.canceled = canceled + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new todo configured with same values as the provided todo. convenience init(_ todo: TJSTodo) { - self.init(title: todo.title, + self.init(id: todo.id, + title: todo.title, notes: todo.notes, + prependNotes: todo.prependNotes, + appendNotes: todo.appendNotes, when: todo.when, deadline: todo.deadline, tags: todo.tags, + addTags: todo.addTags, checklistItems: todo.checklistItems, + prependChecklistItems: todo.prependChecklistItems, + appendChecklistItems: todo.appendChecklistItems, listID: todo.listID, list: todo.list, heading: todo.heading, completed: todo.completed, - canceled: todo.canceled) + canceled: todo.canceled, + creationDate: todo.creationDate, + completionDate: todo.completionDate) } /// Creates a new instance by decoding from the given decoder. @@ -200,15 +264,22 @@ class TJSTodo : TJSModelItem, Codable { do { title = try attributes.decodeIfPresent(String.self, forKey: .title) notes = try attributes.decodeIfPresent(String.self, forKey: .notes) + prependNotes = try attributes.decodeIfPresent(String.self, forKey: .prependNotes) + appendNotes = try attributes.decodeIfPresent(String.self, forKey: .appendNotes) when = try attributes.decodeIfPresent(String.self, forKey: .when) deadline = try attributes.decodeIfPresent(String.self, forKey: .deadline) tags = try attributes.decodeIfPresent([String].self, forKey: .tags) + addTags = try attributes.decodeIfPresent([String].self, forKey: .addTags) checklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .checklistItems) + prependChecklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .prependChecklistItems) + appendChecklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .appendChecklistItems) listID = try attributes.decodeIfPresent(String.self, forKey: .listID) list = try attributes.decodeIfPresent(String.self, forKey: .list) heading = try attributes.decodeIfPresent(String.self, forKey: .heading) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } catch TJSError.invalidType(let expectedType, let errorContext) { throw DecodingError.typeMismatch(expectedType, errorContext) @@ -220,15 +291,22 @@ class TJSTodo : TJSModelItem, Codable { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(notes, forKey: .notes) + try attributes.encodeIfPresent(prependNotes, forKey: .prependNotes) + try attributes.encodeIfPresent(appendNotes, forKey: .appendNotes) try attributes.encodeIfPresent(when, forKey: .when) try attributes.encodeIfPresent(deadline, forKey: .deadline) try attributes.encodeIfPresent(tags, forKey: .tags) + try attributes.encodeIfPresent(addTags, forKey: .addTags) try attributes.encodeIfPresent(checklistItems, forKey: .checklistItems) + try attributes.encodeIfPresent(prependChecklistItems, forKey: .prependChecklistItems) + try attributes.encodeIfPresent(appendChecklistItems, forKey: .appendChecklistItems) try attributes.encodeIfPresent(listID, forKey: .listID) try attributes.encodeIfPresent(list, forKey: .list) try attributes.encodeIfPresent(heading, forKey: .heading) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } } @@ -239,67 +317,95 @@ class TJSTodo : TJSModelItem, Codable { class TJSProject : TJSModelItem, Codable { var title: String? var notes: String? + var prependNotes: String? + var appendNotes: String? var when: String? var deadline: String? var tags: [String]? + var addTags: [String]? var areaID: String? var area: String? var items: [Item]? var completed: Bool? var canceled: Bool? + var creationDate: Date? + var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case notes + case prependNotes = "prepend-notes" + case appendNotes = "append-notes" case when case deadline case tags + case addTags = "add-tags" case areaID = "area-id" case area case items case completed case canceled + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new project configured with the provided values. - init(title: String? = nil, + init(operation: Operation = .create, + id: String? = nil, + title: String? = nil, notes: String? = nil, + prependNotes: String? = nil, + appendNotes: String? = nil, when: String? = nil, deadline: String? = nil, tags: [String]? = nil, + addTags: [String]? = nil, areaID: String? = nil, area: String? = nil, items: [Item]? = nil, completed: Bool? = nil, - canceled: Bool? = nil) { + canceled: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation, id: id) self.type = "project" self.title = title self.notes = notes + self.prependNotes = prependNotes + self.appendNotes = appendNotes self.when = when self.deadline = deadline self.tags = tags + self.addTags = addTags self.areaID = areaID self.area = area self.items = items self.completed = completed self.canceled = canceled + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new project configured with same values as the provided project. convenience init(_ project: TJSProject) { - self.init(title: project.title, + self.init(id: project.id, + title: project.title, notes: project.notes, + prependNotes: project.prependNotes, + appendNotes: project.appendNotes, when: project.when, deadline: project.deadline, tags: project.tags, + addTags: project.addTags, areaID: project.areaID, area: project.area, items: project.items, completed: project.completed, - canceled: project.canceled) + canceled: project.canceled, + creationDate: project.creationDate, + completionDate: project.completionDate) } /// Creates a new instance by decoding from the given decoder. @@ -309,14 +415,19 @@ class TJSProject : TJSModelItem, Codable { do { title = try attributes.decodeIfPresent(String.self, forKey: .title) notes = try attributes.decodeIfPresent(String.self, forKey: .notes) + prependNotes = try attributes.decodeIfPresent(String.self, forKey: .prependNotes) + appendNotes = try attributes.decodeIfPresent(String.self, forKey: .appendNotes) when = try attributes.decodeIfPresent(String.self, forKey: .when) deadline = try attributes.decodeIfPresent(String.self, forKey: .deadline) tags = try attributes.decodeIfPresent([String].self, forKey: .tags) + addTags = try attributes.decodeIfPresent([String].self, forKey: .addTags) areaID = try attributes.decodeIfPresent(String.self, forKey: .areaID) area = try attributes.decodeIfPresent(String.self, forKey: .area) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) items = try attributes.decodeIfPresent([Item].self, forKey: .items) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } catch TJSError.invalidType(let expectedType, let errorContext) { throw DecodingError.typeMismatch(expectedType, errorContext) @@ -328,14 +439,19 @@ class TJSProject : TJSModelItem, Codable { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(notes, forKey: .notes) + try attributes.encodeIfPresent(prependNotes, forKey: .prependNotes) + try attributes.encodeIfPresent(appendNotes, forKey: .appendNotes) try attributes.encodeIfPresent(when, forKey: .when) try attributes.encodeIfPresent(deadline, forKey: .deadline) try attributes.encodeIfPresent(tags, forKey: .tags) + try attributes.encodeIfPresent(addTags, forKey: .addTags) try attributes.encodeIfPresent(areaID, forKey: .areaID) try attributes.encodeIfPresent(area, forKey: .area) try attributes.encodeIfPresent(items, forKey: .items) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } /// A child item of a project. @@ -382,27 +498,38 @@ class TJSProject : TJSModelItem, Codable { class TJSHeading : TJSModelItem, Codable { var title: String? var archived: Bool? + var creationDate: Date? + var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case archived + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new heading configured with the provided values. - init(title: String? = nil, - archived: Bool? = nil) { + init(operation: Operation = .create, + title: String? = nil, + archived: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation) self.type = "heading" self.title = title self.archived = archived + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new heading configured with same values as the provided heading. convenience init(_ heading: TJSHeading) { self.init(title: heading.title, - archived: heading.archived) + archived: heading.archived, + creationDate: heading.creationDate, + completionDate: heading.completionDate) } /// Creates a new instance by decoding from the given decoder. @@ -411,6 +538,8 @@ class TJSHeading : TJSModelItem, Codable { let attributes = try self.attributes(CodingKeys.self, from: decoder) title = try attributes.decodeIfPresent(String.self, forKey: .title) archived = try attributes.decodeIfPresent(Bool.self, forKey: .archived) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } /// Encodes this value into the given encoder. @@ -418,6 +547,8 @@ class TJSHeading : TJSModelItem, Codable { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(archived, forKey: .archived) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } } @@ -429,31 +560,42 @@ class TJSChecklistItem : TJSModelItem, Codable { var title: String? var completed: Bool? var canceled: Bool? + var creationDate: Date? + var completionDate: Date? private enum CodingKeys: String, CodingKey { case title case completed case canceled + case creationDate = "creation-date" + case completionDate = "completion-date" } /// Create and return a new checklist item configured with the provided values. - init(title: String? = nil, + init(operation: Operation = .create, + title: String? = nil, completed: Bool? = nil, - canceled: Bool? = nil) { + canceled: Bool? = nil, + creationDate: Date? = nil, + completionDate: Date? = nil) { - super.init() + super.init(operation: operation) self.type = "checklist-item" self.title = title self.completed = completed self.canceled = canceled + self.creationDate = creationDate + self.completionDate = completionDate } /// Create and return a new checklist item configured with same values as the provided checklist item. convenience init (_ checklistItem: TJSChecklistItem) { self.init(title: checklistItem.title, completed: checklistItem.completed, - canceled: checklistItem.canceled) + canceled: checklistItem.canceled, + creationDate: checklistItem.creationDate, + completionDate: checklistItem.completionDate) } /// Creates a new instance by decoding from the given decoder. @@ -463,6 +605,8 @@ class TJSChecklistItem : TJSModelItem, Codable { title = try attributes.decodeIfPresent(String.self, forKey: .title) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) + creationDate = try attributes.decodeIfPresent(Date.self, forKey: .creationDate) + completionDate = try attributes.decodeIfPresent(Date.self, forKey: .completionDate) } /// Encodes this value into the given encoder. @@ -471,6 +615,8 @@ class TJSChecklistItem : TJSModelItem, Codable { try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled) + try attributes.encodeIfPresent(creationDate, forKey: .creationDate) + try attributes.encodeIfPresent(completionDate, forKey: .completionDate) } } @@ -480,3 +626,39 @@ class TJSChecklistItem : TJSModelItem, Codable { private enum TJSError : Error { case invalidType(expectedType: Any.Type, errorContext: DecodingError.Context) } + + +// Mark: - Date Formatting + +/// A date encoding strategy to format a date according to ISO8601. +/// +/// Use to with a JSONEncoder to correctly format dates. +func ThingsJSONDateEncodingStrategy() -> JSONEncoder.DateEncodingStrategy { + if #available(iOS 10, OSX 10.12, *) { + return .iso8601 + } + else { + return .formatted(isoDateFormatter()) + } +} + +/// A date decoding strategy to format a date according to ISO8601. +/// +/// Use to with a JSONDecoder to correctly format dates. +func ThingsJSONDateDecodingStrategy() -> JSONDecoder.DateDecodingStrategy { + if #available(iOS 10, OSX 10.12, *) { + return .iso8601 + } + else { + return .formatted(isoDateFormatter()) + } +} + +private func isoDateFormatter() -> DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + return dateFormatter +} From 28b5183df0891431f373ac711983416dfa709d76 Mon Sep 17 00:00:00 2001 From: Fernando Lemos Date: Thu, 26 Jul 2018 15:36:00 +0200 Subject: [PATCH 3/9] Make all internal members public This works better for the case where ThingsJSON.swift is added to a framework target, but the members need to be accessed from an app target. --- ThingsJSON.swift | 126 +++++++++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index b8029eb..79b5151 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -27,24 +27,24 @@ import Foundation // MARK: Container /// The container holding the array of items to be encoded to JSON. -class TJSContainer : Codable { +public class TJSContainer : Codable { /// The array of items that will be encoded or decoded from the JSON. - var items = [Item]() + public var items = [Item]() /// Create and return a new ThingsJSON object configured with the provided items. - init(items: [Item]) { + public init(items: [Item]) { self.items = items } /// Creates a new instance by decoding from the given decoder. - required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.items = try container.decode([Item].self) } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { try self.items.encode(to: encoder) } @@ -53,12 +53,12 @@ class TJSContainer : Codable { /// This is an enum that wraps a TJSTodo or TJSProject object and handles its encoding /// and decoding to JSON. This is required because there is no way of specifiying a /// strongly typed array that contains more than one type. - enum Item : Codable { + public enum Item : Codable { case todo(TJSTodo) case project(TJSProject) /// Creates a new instance by decoding from the given decoder. - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() do { @@ -74,7 +74,7 @@ class TJSContainer : Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { switch self { case .todo(let todo): try todo.encode(to: encoder) @@ -91,14 +91,14 @@ class TJSContainer : Codable { /// The superclass of all the Things JSON model items. /// /// Do not instantiate this class itself. Instead use one of the subclasses. -class TJSModelItem { +public class TJSModelItem { fileprivate var type: String = "" /// The operation to perform on the object. - var operation: Operation + public var operation: Operation /// The ID of the item to update. - var id: String? + public var id: String? private enum CodingKeys: String, CodingKey { case type @@ -107,7 +107,7 @@ class TJSModelItem { case attributes } - enum Operation: String, Codable { + public enum Operation: String, Codable { /// Create a new item. case create = "create" /// Update an existing item. @@ -116,7 +116,7 @@ class TJSModelItem { case update = "update" } - init(operation: Operation, id: String? = nil) { + public init(operation: Operation, id: String? = nil) { self.operation = operation self.id = id } @@ -148,25 +148,25 @@ class TJSModelItem { // MARK: - /// Represents a to-do in Things. -class TJSTodo : TJSModelItem, Codable { - var title: String? - var notes: String? - var prependNotes: String? - var appendNotes: String? - var when: String? - var deadline: String? - var tags: [String]? - var addTags: [String]? - var checklistItems: [TJSChecklistItem]? - var prependChecklistItems: [TJSChecklistItem]? - var appendChecklistItems: [TJSChecklistItem]? - var listID: String? - var list: String? - var heading: String? - var completed: Bool? - var canceled: Bool? - var creationDate: Date? - var completionDate: Date? +public class TJSTodo : TJSModelItem, Codable { + public var title: String? + public var notes: String? + public var prependNotes: String? + public var appendNotes: String? + public var when: String? + public var deadline: String? + public var tags: [String]? + public var addTags: [String]? + public var checklistItems: [TJSChecklistItem]? + public var prependChecklistItems: [TJSChecklistItem]? + public var appendChecklistItems: [TJSChecklistItem]? + public var listID: String? + public var list: String? + public var heading: String? + public var completed: Bool? + public var canceled: Bool? + public var creationDate: Date? + public var completionDate: Date? private enum CodingKeys: String, CodingKey { case title @@ -190,7 +190,7 @@ class TJSTodo : TJSModelItem, Codable { } /// Create and return a new todo configured with the provided values. - init(operation: Operation = .create, + public init(operation: Operation = .create, id: String? = nil, title: String? = nil, notes: String? = nil, @@ -235,7 +235,7 @@ class TJSTodo : TJSModelItem, Codable { } /// Create and return a new todo configured with same values as the provided todo. - convenience init(_ todo: TJSTodo) { + public convenience init(_ todo: TJSTodo) { self.init(id: todo.id, title: todo.title, notes: todo.notes, @@ -258,7 +258,7 @@ class TJSTodo : TJSModelItem, Codable { } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) do { @@ -287,7 +287,7 @@ class TJSTodo : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(notes, forKey: .notes) @@ -314,7 +314,7 @@ class TJSTodo : TJSModelItem, Codable { // MARK: - /// Represents a project in Things. -class TJSProject : TJSModelItem, Codable { +public class TJSProject : TJSModelItem, Codable { var title: String? var notes: String? var prependNotes: String? @@ -409,7 +409,7 @@ class TJSProject : TJSModelItem, Codable { } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) do { @@ -435,7 +435,7 @@ class TJSProject : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(notes, forKey: .notes) @@ -459,12 +459,12 @@ class TJSProject : TJSModelItem, Codable { /// This is an enum that wraps a TJSTodo or TJSHeading object and handles its encoding /// and decoding to JSON. This is required because there is no way of specifiying a /// strongly typed array that contains more than one type. - enum Item : Codable { + public enum Item : Codable { case todo(TJSTodo) case heading(TJSHeading) /// Creates a new instance by decoding from the given decoder. - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() do { @@ -480,7 +480,7 @@ class TJSProject : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { switch self { case .todo(let todo): try todo.encode(to: encoder) @@ -495,11 +495,11 @@ class TJSProject : TJSModelItem, Codable { // MARK: - /// Represents a heading in Things. -class TJSHeading : TJSModelItem, Codable { - var title: String? - var archived: Bool? - var creationDate: Date? - var completionDate: Date? +public class TJSHeading : TJSModelItem, Codable { + public var title: String? + public var archived: Bool? + public var creationDate: Date? + public var completionDate: Date? private enum CodingKeys: String, CodingKey { case title @@ -509,7 +509,7 @@ class TJSHeading : TJSModelItem, Codable { } /// Create and return a new heading configured with the provided values. - init(operation: Operation = .create, + public init(operation: Operation = .create, title: String? = nil, archived: Bool? = nil, creationDate: Date? = nil, @@ -525,7 +525,7 @@ class TJSHeading : TJSModelItem, Codable { } /// Create and return a new heading configured with same values as the provided heading. - convenience init(_ heading: TJSHeading) { + public convenience init(_ heading: TJSHeading) { self.init(title: heading.title, archived: heading.archived, creationDate: heading.creationDate, @@ -533,7 +533,7 @@ class TJSHeading : TJSModelItem, Codable { } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) title = try attributes.decodeIfPresent(String.self, forKey: .title) @@ -543,7 +543,7 @@ class TJSHeading : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(archived, forKey: .archived) @@ -556,12 +556,12 @@ class TJSHeading : TJSModelItem, Codable { // MARK: - /// Represents a checklist item in Things. -class TJSChecklistItem : TJSModelItem, Codable { - var title: String? - var completed: Bool? - var canceled: Bool? - var creationDate: Date? - var completionDate: Date? +public class TJSChecklistItem : TJSModelItem, Codable { + public var title: String? + public var completed: Bool? + public var canceled: Bool? + public var creationDate: Date? + public var completionDate: Date? private enum CodingKeys: String, CodingKey { case title @@ -572,7 +572,7 @@ class TJSChecklistItem : TJSModelItem, Codable { } /// Create and return a new checklist item configured with the provided values. - init(operation: Operation = .create, + public init(operation: Operation = .create, title: String? = nil, completed: Bool? = nil, canceled: Bool? = nil, @@ -590,7 +590,7 @@ class TJSChecklistItem : TJSModelItem, Codable { } /// Create and return a new checklist item configured with same values as the provided checklist item. - convenience init (_ checklistItem: TJSChecklistItem) { + public convenience init (_ checklistItem: TJSChecklistItem) { self.init(title: checklistItem.title, completed: checklistItem.completed, canceled: checklistItem.canceled, @@ -599,7 +599,7 @@ class TJSChecklistItem : TJSModelItem, Codable { } /// Creates a new instance by decoding from the given decoder. - required convenience init(from decoder: Decoder) throws { + public required convenience init(from decoder: Decoder) throws { self.init() let attributes = try self.attributes(CodingKeys.self, from: decoder) title = try attributes.decodeIfPresent(String.self, forKey: .title) @@ -610,7 +610,7 @@ class TJSChecklistItem : TJSModelItem, Codable { } /// Encodes this value into the given encoder. - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var attributes = try self.attributes(CodingKeys.self, for: encoder) try attributes.encodeIfPresent(title, forKey: .title) try attributes.encodeIfPresent(completed, forKey: .completed) @@ -633,7 +633,7 @@ private enum TJSError : Error { /// A date encoding strategy to format a date according to ISO8601. /// /// Use to with a JSONEncoder to correctly format dates. -func ThingsJSONDateEncodingStrategy() -> JSONEncoder.DateEncodingStrategy { +public func ThingsJSONDateEncodingStrategy() -> JSONEncoder.DateEncodingStrategy { if #available(iOS 10, OSX 10.12, *) { return .iso8601 } @@ -645,7 +645,7 @@ func ThingsJSONDateEncodingStrategy() -> JSONEncoder.DateEncodingStrategy { /// A date decoding strategy to format a date according to ISO8601. /// /// Use to with a JSONDecoder to correctly format dates. -func ThingsJSONDateDecodingStrategy() -> JSONDecoder.DateDecodingStrategy { +public func ThingsJSONDateDecodingStrategy() -> JSONDecoder.DateDecodingStrategy { if #available(iOS 10, OSX 10.12, *) { return .iso8601 } From a5adf918a53f4be53dd83b8b4b6e7753cc1f5f4d Mon Sep 17 00:00:00 2001 From: Fabian Renner Date: Fri, 23 Feb 2018 10:10:04 +0100 Subject: [PATCH 4/9] Some enhanced Swift initializing --- README.md | 6 +++--- ThingsJSON.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 60e388c..2a3092b 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ do { let encoder = JSONEncoder() encoder.dateEncodingStrategy = ThingsJSONDateEncodingStrategy() let data = try encoder.encode(container) - let json = String.init(data: data, encoding: .utf8)! - var components = URLComponents.init(string: "things:///add-json")! - let queryItem = URLQueryItem.init(name: "data", value: json) + let json = String(data: data, encoding: .utf8)! + var components = URLComponents(string: "things:///add-json")! + let queryItem = URLQueryItem(name: "data", value: json) components.queryItems = [queryItem] let url = components.url! UIApplication.shared.open(url, options: [:], completionHandler: nil) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index 79b5151..9e5f201 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -127,8 +127,8 @@ public class TJSModelItem { self.operation = try container.decodeIfPresent(Operation.self, forKey: .operation) ?? .create self.id = try container.decodeIfPresent(String.self, forKey: .id) guard decodedType == self.type else { - let description = String.init(format: "Expected to decode a %@ but found a %@ instead.", self.type, decodedType) - let errorContext = DecodingError.Context.init(codingPath: [CodingKeys.type], debugDescription: description) + let description = String(format: "Expected to decode a %@ but found a %@ instead.", self.type, decodedType) + let errorContext = DecodingError.Context(codingPath: [CodingKeys.type], debugDescription: description) let expectedType = Swift.type(of: self) throw TJSError.invalidType(expectedType: expectedType, errorContext: errorContext) } From b7d52beda02ac677f0f7ab1c9b0f5999cb7f57d8 Mon Sep 17 00:00:00 2001 From: Fernando Lemos Date: Thu, 26 Jul 2018 16:25:42 +0200 Subject: [PATCH 5/9] Allow specifying tags by ID --- ThingsJSON.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index 9e5f201..8d79b0e 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -155,6 +155,7 @@ public class TJSTodo : TJSModelItem, Codable { public var appendNotes: String? public var when: String? public var deadline: String? + public var tagIDs: [String]? public var tags: [String]? public var addTags: [String]? public var checklistItems: [TJSChecklistItem]? @@ -175,6 +176,7 @@ public class TJSTodo : TJSModelItem, Codable { case appendNotes = "append-notes" case when case deadline + case tagIDs = "tag-ids" case tags case addTags = "add-tags" case checklistItems = "checklist-items" @@ -198,6 +200,7 @@ public class TJSTodo : TJSModelItem, Codable { appendNotes: String? = nil, when: String? = nil, deadline: String? = nil, + tagIDs: [String]? = nil, tags: [String]? = nil, addTags: [String]? = nil, checklistItems: [TJSChecklistItem]? = nil, @@ -220,6 +223,7 @@ public class TJSTodo : TJSModelItem, Codable { self.appendNotes = appendNotes self.when = when self.deadline = deadline + self.tagIDs = tagIDs self.tags = tags self.addTags = addTags self.checklistItems = checklistItems @@ -243,6 +247,7 @@ public class TJSTodo : TJSModelItem, Codable { appendNotes: todo.appendNotes, when: todo.when, deadline: todo.deadline, + tagIDs: todo.tagIDs, tags: todo.tags, addTags: todo.addTags, checklistItems: todo.checklistItems, @@ -268,6 +273,7 @@ public class TJSTodo : TJSModelItem, Codable { appendNotes = try attributes.decodeIfPresent(String.self, forKey: .appendNotes) when = try attributes.decodeIfPresent(String.self, forKey: .when) deadline = try attributes.decodeIfPresent(String.self, forKey: .deadline) + tagIDs = try attributes.decodeIfPresent([String].self, forKey: .tagIDs) tags = try attributes.decodeIfPresent([String].self, forKey: .tags) addTags = try attributes.decodeIfPresent([String].self, forKey: .addTags) checklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .checklistItems) @@ -295,6 +301,7 @@ public class TJSTodo : TJSModelItem, Codable { try attributes.encodeIfPresent(appendNotes, forKey: .appendNotes) try attributes.encodeIfPresent(when, forKey: .when) try attributes.encodeIfPresent(deadline, forKey: .deadline) + try attributes.encodeIfPresent(tagIDs, forKey: .tagIDs) try attributes.encodeIfPresent(tags, forKey: .tags) try attributes.encodeIfPresent(addTags, forKey: .addTags) try attributes.encodeIfPresent(checklistItems, forKey: .checklistItems) @@ -321,6 +328,7 @@ public class TJSProject : TJSModelItem, Codable { var appendNotes: String? var when: String? var deadline: String? + var tagIDs: [String]? var tags: [String]? var addTags: [String]? var areaID: String? @@ -338,6 +346,7 @@ public class TJSProject : TJSModelItem, Codable { case appendNotes = "append-notes" case when case deadline + case tagIDs = "tag-ids" case tags case addTags = "add-tags" case areaID = "area-id" @@ -358,6 +367,7 @@ public class TJSProject : TJSModelItem, Codable { appendNotes: String? = nil, when: String? = nil, deadline: String? = nil, + tagIDs: [String]? = nil, tags: [String]? = nil, addTags: [String]? = nil, areaID: String? = nil, @@ -377,6 +387,7 @@ public class TJSProject : TJSModelItem, Codable { self.appendNotes = appendNotes self.when = when self.deadline = deadline + self.tagIDs = tagIDs self.tags = tags self.addTags = addTags self.areaID = areaID @@ -397,6 +408,7 @@ public class TJSProject : TJSModelItem, Codable { appendNotes: project.appendNotes, when: project.when, deadline: project.deadline, + tagIDs: project.tagIDs, tags: project.tags, addTags: project.addTags, areaID: project.areaID, @@ -419,6 +431,7 @@ public class TJSProject : TJSModelItem, Codable { appendNotes = try attributes.decodeIfPresent(String.self, forKey: .appendNotes) when = try attributes.decodeIfPresent(String.self, forKey: .when) deadline = try attributes.decodeIfPresent(String.self, forKey: .deadline) + tagIDs = try attributes.decodeIfPresent([String].self, forKey: .tagIDs) tags = try attributes.decodeIfPresent([String].self, forKey: .tags) addTags = try attributes.decodeIfPresent([String].self, forKey: .addTags) areaID = try attributes.decodeIfPresent(String.self, forKey: .areaID) @@ -443,6 +456,7 @@ public class TJSProject : TJSModelItem, Codable { try attributes.encodeIfPresent(appendNotes, forKey: .appendNotes) try attributes.encodeIfPresent(when, forKey: .when) try attributes.encodeIfPresent(deadline, forKey: .deadline) + try attributes.encodeIfPresent(tagIDs, forKey: .tagIDs) try attributes.encodeIfPresent(tags, forKey: .tags) try attributes.encodeIfPresent(addTags, forKey: .addTags) try attributes.encodeIfPresent(areaID, forKey: .areaID) From 71911a83c9adc6f45523217bacff7b4ff3f83df9 Mon Sep 17 00:00:00 2001 From: Jason Bruder Date: Mon, 20 Feb 2023 13:00:13 -0500 Subject: [PATCH 6/9] Update ThingsJSON.swift Resolves "Enum case 'invalidType' has 2 associated values; matching them as a tuple is deprecated" warning. --- ThingsJSON.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index 8d79b0e..2bb01c2 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -66,7 +66,7 @@ public class TJSContainer : Codable { let todo = try container.decode(TJSTodo.self) self = .todo(todo) } - catch TJSError.invalidType(_) { + catch TJSError.invalidType(_, _) { // If it's the wrong type, try a project let project = try container.decode(TJSProject.self) self = .project(project) @@ -486,7 +486,7 @@ public class TJSProject : TJSModelItem, Codable { let todo = try container.decode(TJSTodo.self) self = .todo(todo) } - catch TJSError.invalidType(_) { + catch TJSError.invalidType(_, _) { // If it's the wrong type, try a heading let heading = try container.decode(TJSHeading.self) self = .heading(heading) From c3e85c913f6073f500f943156457d266f1967b26 Mon Sep 17 00:00:00 2001 From: Jace Kalms Date: Mon, 27 Feb 2023 11:46:15 +0100 Subject: [PATCH 7/9] Tweak unused parameters --- ThingsJSON.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index 2bb01c2..1eedc84 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -66,7 +66,7 @@ public class TJSContainer : Codable { let todo = try container.decode(TJSTodo.self) self = .todo(todo) } - catch TJSError.invalidType(_, _) { + catch TJSError.invalidType(expectedType: _, errorContext: _) { // If it's the wrong type, try a project let project = try container.decode(TJSProject.self) self = .project(project) @@ -486,7 +486,7 @@ public class TJSProject : TJSModelItem, Codable { let todo = try container.decode(TJSTodo.self) self = .todo(todo) } - catch TJSError.invalidType(_, _) { + catch TJSError.invalidType(expectedType: _, errorContext: _) { // If it's the wrong type, try a heading let heading = try container.decode(TJSHeading.self) self = .heading(heading) From 61649b06dbd7eaac0f401ba5b2e466cbe8bbff8e Mon Sep 17 00:00:00 2001 From: Jace Kalms Date: Mon, 27 Feb 2023 11:46:35 +0100 Subject: [PATCH 8/9] Remove unnecessary #available checks for very old OS's --- ThingsJSON.swift | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index 1eedc84..2612a6b 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -648,31 +648,12 @@ private enum TJSError : Error { /// /// Use to with a JSONEncoder to correctly format dates. public func ThingsJSONDateEncodingStrategy() -> JSONEncoder.DateEncodingStrategy { - if #available(iOS 10, OSX 10.12, *) { - return .iso8601 - } - else { - return .formatted(isoDateFormatter()) - } + return .iso8601 } /// A date decoding strategy to format a date according to ISO8601. /// /// Use to with a JSONDecoder to correctly format dates. public func ThingsJSONDateDecodingStrategy() -> JSONDecoder.DateDecodingStrategy { - if #available(iOS 10, OSX 10.12, *) { - return .iso8601 - } - else { - return .formatted(isoDateFormatter()) - } -} - -private func isoDateFormatter() -> DateFormatter { - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - - return dateFormatter + return .iso8601 } From 0862eaaba247974f998749a893b3d6c94784e84c Mon Sep 17 00:00:00 2001 From: Jace Kalms Date: Mon, 27 Feb 2023 11:46:56 +0100 Subject: [PATCH 9/9] Add the heading-id parameter to TJSTodo --- ThingsJSON.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ThingsJSON.swift b/ThingsJSON.swift index 2612a6b..7677215 100644 --- a/ThingsJSON.swift +++ b/ThingsJSON.swift @@ -163,6 +163,7 @@ public class TJSTodo : TJSModelItem, Codable { public var appendChecklistItems: [TJSChecklistItem]? public var listID: String? public var list: String? + public var headingID: String? public var heading: String? public var completed: Bool? public var canceled: Bool? @@ -184,6 +185,7 @@ public class TJSTodo : TJSModelItem, Codable { case appendChecklistItems = "append-checklist-items" case listID = "list-id" case list + case headingID = "heading-id" case heading case completed case canceled @@ -208,6 +210,7 @@ public class TJSTodo : TJSModelItem, Codable { appendChecklistItems: [TJSChecklistItem]? = nil, listID: String? = nil, list: String? = nil, + headingID: String? = nil, heading: String? = nil, completed: Bool? = nil, canceled: Bool? = nil, @@ -232,6 +235,7 @@ public class TJSTodo : TJSModelItem, Codable { self.listID = listID self.list = list self.heading = heading + self.headingID = headingID self.completed = completed self.canceled = canceled self.creationDate = creationDate @@ -255,6 +259,7 @@ public class TJSTodo : TJSModelItem, Codable { appendChecklistItems: todo.appendChecklistItems, listID: todo.listID, list: todo.list, + headingID: todo.headingID, heading: todo.heading, completed: todo.completed, canceled: todo.canceled, @@ -281,6 +286,7 @@ public class TJSTodo : TJSModelItem, Codable { appendChecklistItems = try attributes.decodeIfPresent([TJSChecklistItem].self, forKey: .appendChecklistItems) listID = try attributes.decodeIfPresent(String.self, forKey: .listID) list = try attributes.decodeIfPresent(String.self, forKey: .list) + headingID = try attributes.decodeIfPresent(String.self, forKey: .headingID) heading = try attributes.decodeIfPresent(String.self, forKey: .heading) completed = try attributes.decodeIfPresent(Bool.self, forKey: .completed) canceled = try attributes.decodeIfPresent(Bool.self, forKey: .canceled) @@ -309,6 +315,7 @@ public class TJSTodo : TJSModelItem, Codable { try attributes.encodeIfPresent(appendChecklistItems, forKey: .appendChecklistItems) try attributes.encodeIfPresent(listID, forKey: .listID) try attributes.encodeIfPresent(list, forKey: .list) + try attributes.encodeIfPresent(headingID, forKey: .headingID) try attributes.encodeIfPresent(heading, forKey: .heading) try attributes.encodeIfPresent(completed, forKey: .completed) try attributes.encodeIfPresent(canceled, forKey: .canceled)