archivedData(withRootObject:)' was deprecated in iOS 12.0 - Swift 5 and Flutter - swift

I have a project which was written (iOS part) with Swift 4. I update it to 5 but now I have this warnings. I've tried to fix it as it's suggest but I get different error each time. Could you help me with it?
Code
func saveData(data: BloodData) {
if let defaults = getUserDefaults() {
BloodData.registerClassName()
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: data)
defaults.set(encodedData, forKey: dataKey)
defaults.synchronize()
}
}
Warning
archivedData(withRootObject:)' was deprecated in iOS 12.0: Use +archivedDataWithRootObject:requiringSecureCoding:error: instead
Code
func getData() -> BloodData? {
BloodData.registerClassName()
if let defaults = getUserDefaults(), let data = defaults.data(forKey: dataKey), let decodedData = NSKeyedUnarchiver.unarchiveObject(with: data) as? BloodData {
return computeData(data: decodedData)
}
return nil
}
Warning
'unarchiveObject(with:)' was deprecated in iOS 12.0: Use +unarchivedObjectOfClass:fromData:error: instead
Here's a computeData func:
private func computeData(data: BloodData) -> BloodData? {
let today = Date()
let calendar = Calendar.current
let date1 = calendar.date(bySettingHour: 12, minute: 00, second: 00, of: calendar.startOfDay(for: data.readingDate))!
let date2 = calendar.date(bySettingHour: 12, minute: 00, second: 00, of: calendar.startOfDay(for: today))!
let components = calendar.dateComponents([.day], from: date1, to: date2)
let dayDiff = components.day ?? 0
let newBloodData = BloodData(
whiteCells: data.whiteCells - dayDiff,
fullBlood: data.fullBlood - dayDiff,
platelet: data.platelet - dayDiff,
redCells1: data.redCells1 - dayDiff,
redCells2: data.redCells2 - dayDiff,
bloodPlasma: data.bloodPlasma - dayDiff,
readingDate: date2
)
return newBloodData
}

When coding in Swift most of the time the errors are not very helpful.
The first one is pretty self explanatory, except that the suggested method might throw an error. You can remove the throw keyword and ignore the error with try? if you would like to but it is better to catch and handle the errors:
func save(blood: BloodData, forKey defaultName: String = "BloodDataKey") { // or forKey defaultName: String = dataKey
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: blood, requiringSecureCoding: false) else { return }
UserDefaults.standard.set(data, forKey: defaultName)
}
The second error doesn't help at all. You can use NSKeyedUnarchiver's method unarchiveTopLevelObjectWithData and it throws as well:
func getBlood(forKey defaultName: String = "BloodDataKey") -> BloodData? { // or forKey defaultName: String = dataKey
guard let data = UserDefaults.standard.data(forKey: defaultName) else { return nil }
guard let bloodData = (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? BloodData else { return nil }
return computeData(data: bloodData)
}

Related

Prevent lost of data on AppStorage when changing a struct

I have a data model that handles a structure and the data the app uses. I'm saving that data using AppStorage.
I recently needed to add an extra value to the struct, and when I did that, all the data saved was gone.
is there any way to prevent this? I can't find anything on Apple's documentation, or other Swift or SwiftUI sites about this.
Here's my data structure and how I save it.
let dateFormatter = DateFormatter()
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
//var starred: Int = 0 // if I add this, it wipes all the data the app has saved
}
final class DataModel: ObservableObject {
#AppStorage("myappdata") public var notes: [NoteItem] = []
init() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
func sortList() {
self.notes = self.notes.sorted(by: {
$0.date.compare($1.date) == .orderedDescending
})
}
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
I certainly agree that UserDefaults (AppStorage) is no the best choice for this but whatever storage solution you choose you are going to need a migration strategy. So here are two routes you can take to migrate a changed json struct.
The first one is to add a custom init(from:) to your struct and handle the new property separately
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
text = try container.decode(String.self, forKey: .text)
date = try container.decode(Date.self, forKey: .date)
tags = try container.decode([String].self, forKey: .tags)
if let value = try? container.decode(Int.self, forKey: .starred) {
starred = value
} else {
starred = 0
}
}
The other option is to keep the old version of the struct with another name and use it if the decoding fails for the ordinary struct and then convert the result to the new struct
extension NoteItem {
static func decode(string: String) -> [NoteItem]? {
guard let data = string.data(using: .utf8) else { return nil }
if let result = try? JSONDecoder().decode([NoteItem].self, from: data) {
return result
} else if let result = try? JSONDecoder().decode([NoteItemOld].self, from: data) {
return result.map { NoteItem(id: $0.id, text: $0.text, date: $0.date, tags: $0.tags, starred: 0)}
}
return nil
}
}

Updating screen fields when background task ends

I am using some APIs to get data. These are initiated as session.dataTask and I am using classes to encapsulate the API calls, methods and returned properties for each different API. How should I configure my code so as to update the relevant screen labels and subViews when the API sessions have concluded and the data is available?
The relevant section of the AstronomicalTimes class init is:
init (date: Date, lat: Float, long: Float) {
let coreURL = "https://api.sunrise-sunset.org/json?"
let position = "lat=\(lat)&lng=\(long)"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = "&date=" + dateFormatter.string(from: date)
//let dateString = "&date=2020-06-21"
let urlString = coreURL + position + dateString + "&formatted=0"
let session = URLSession.shared
let url = URL(string: urlString)!
let request = URLRequest(url: url)
session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error {
let nsError = error as NSError
print("Astronomical Times API call failed with error \(nsError.code)")
return
}
if let response = response as? HTTPURLResponse {
print("Astronomical Times API call response is \(response.statusCode)")
}
if let data = data {
do {
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: data)
print("Astronomical times successfully parsed")
self.fillFields(astronomicalTimesResponse.results) //completes all class properties from parsed data
} catch {
print("Error while tide details parsing: \(error)")
}
}
}).resume()
A label is assigned the result of the API call in viewDidLoad() with:
currentAstronomicalTimes = AstronomicalTimes(date: savedDate, lat: currentSelection.station.lat, long: currentSelection.station.long)
lblAstDawn.text = currentAstronomicalTimes.strings.astronomicalTwilightBegin
Clearly this doesn't work as the screen is rendered with the labels and subViews blank before the API returns the data. I can't figure out how to signal the ViewController when the API has completed and then how to redraw the labels etc. I have tried updating the viewController fields in the API call closure expression but I can't update the UILabels from another class (and I think this approach is messy as the label update logic should really be in the ViewController)
Any help appreciated.
UPDATE after Rob's comments:
I have changed my class definition as advised and it successfully loads the data from the API. The class definition is as below, note I add a function which takes the loaded data and turns it into time strings and date() for ease of use in viewController (these all appear to be correctly populated after the API call)
import Foundation
enum AstronomicalTimesError: Error {
case invalidResponse(Data?, URLResponse?)
}
class AstronomicalTimes {
//structures for decoding daylight times
struct AstronomicalTimesResponse: Decodable {
public var results: AstronomicalTimes
public var status: String
}
struct AstronomicalTimes: Decodable {
var sunrise = String()
var sunset = String()
var solarNoon = String()
var dayLength = 0
var civilTwilightBegin = String()
var civilTwilightEnd = String()
var nauticalTwilightBegin = String()
var nauticalTwilightEnd = String()
var astronomicalTwilightBegin = String()
var astronomicalTwilightEnd = String()
private enum CodingKeys : String, CodingKey {
case sunrise = "sunrise"
case sunset = "sunset"
case solarNoon = "solar_noon"
case dayLength = "day_length"
case civilTwilightBegin = "civil_twilight_begin"
case civilTwilightEnd = "civil_twilight_end"
case nauticalTwilightBegin = "nautical_twilight_begin"
case nauticalTwilightEnd = "nautical_twilight_end"
case astronomicalTwilightBegin = "astronomical_twilight_begin"
case astronomicalTwilightEnd = "astronomical_twilight_end"
}
}
//used to hold string values to enter to label, i.e. time strings for labels
var strings = AstronomicalTimes()
//struct and variable used to hold specific date/times for gradient calculation
struct Times {
var sunrise = Date()
var sunset = Date()
var solarNoon = Date()
var dayLength = 0
var civilTwilightBegin = Date()
var civilTwilightEnd = Date()
var nauticalTwilightBegin = Date()
var nauticalTwilightEnd = Date()
var astronomicalTwilightBegin = Date()
var astronomicalTwilightEnd = Date()
}
var times = Times()
let date: Date
let latitude: Float
let longitude: Float
init (date: Date, latitude: Float, longitude: Float) {
self.date = date
self.latitude = latitude
self.longitude = longitude
}
func start(completion: #escaping (Result<AstronomicalTimesResponse, Error>) -> Void) {
var components = URLComponents(string: "https://api.sunrise-sunset.org/json")!
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // just in case your end user isn't using Gregorian calendar
dateFormatter.dateFormat = "yyyy-MM-dd"
components.queryItems = [
URLQueryItem(name: "lat", value: "\(latitude)"),
URLQueryItem(name: "lng", value: "\(longitude)"),
URLQueryItem(name: "date", value: dateFormatter.string(from: date)),
URLQueryItem(name: "formatted", value: "0")
]
let session = URLSession.shared
let url = components.url!
let request = URLRequest(url: url)
session.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
}
return
}
do {
print("Astronomical times api completed with status code ", httpResponse.statusCode)
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
DispatchQueue.main.async {
completion(.success(astronomicalTimesResponse))
self.fillFields(astronomicalTimesResponse.results)
}
} catch let jsonError {
DispatchQueue.main.async {
completion(.failure(jsonError))
}
}
}.resume()
}
func fillFields(_ input: AstronomicalTimes) -> Void {
//formats output fields into Date() or String (HH:mm) format
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" //Your date format
times.sunrise = dateFormatter.date(from: input.sunrise) ?? Date()
times.sunset = dateFormatter.date(from: input.sunset) ?? Date()
times.solarNoon = dateFormatter.date(from: input.solarNoon) ?? Date()
times.dayLength = input.dayLength
times.civilTwilightBegin = dateFormatter.date(from: input.civilTwilightBegin) ?? Date()
times.civilTwilightEnd = dateFormatter.date(from: input.civilTwilightEnd) ?? Date()
times.nauticalTwilightBegin = dateFormatter.date(from: input.nauticalTwilightBegin) ?? Date()
times.nauticalTwilightEnd = dateFormatter.date(from: input.nauticalTwilightEnd) ?? Date()
times.astronomicalTwilightBegin = dateFormatter.date(from: input.astronomicalTwilightBegin) ?? Date()
times.astronomicalTwilightEnd = dateFormatter.date(from: input.astronomicalTwilightEnd) ?? Date()
let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "HH:mm"
strings.sunrise = timeFormatter.string(from: times.sunrise)
strings.sunset = timeFormatter.string(from: times.sunset)
strings.solarNoon = timeFormatter.string(from: times.solarNoon)
strings.dayLength = input.dayLength
strings.civilTwilightBegin = timeFormatter.string(from: times.civilTwilightBegin)
strings.civilTwilightEnd = timeFormatter.string(from: times.civilTwilightEnd)
strings.nauticalTwilightBegin = timeFormatter.string(from: times.nauticalTwilightBegin)
strings.nauticalTwilightEnd = timeFormatter.string(from: times.nauticalTwilightEnd)
strings.astronomicalTwilightBegin = timeFormatter.string(from: times.astronomicalTwilightBegin)
strings.astronomicalTwilightEnd = timeFormatter.string(from: times.astronomicalTwilightEnd)
}
}
I then call this from a function within viewController:
func getAstronomicalTimes(date: Date, latitude: Float, longitude: Float) -> Void {
let astronomicalTimes = AstronomicalTimes(date: date, latitude: latitude, longitude: longitude)
astronomicalTimes.start { result in
switch result {
case .success(let astronomicalTimesResponse):
print("astronomical times response ", astronomicalTimesResponse)
print("label", astronomicalTimes.strings.astronomicalTwilightBegin)
self.lblAstDawn.text = astronomicalTimes.strings.astronomicalTwilightBegin
case .failure(let error):
print(error)
}
}
}
This function is called within viewDidLoad():
getAstronomicalTimes(date: savedDate, latitude: currentSelection.station.lat, longitude: currentSelection.station.long)
However, the getAstronomicalTimes(date:latitude:longitude) does not update the lblAstDawn.text as I had hoped for.
Any clues as to where I am getting this wrong?
You need to supply a completion handler to your AstronomicalTimes request, so it can tell your view controller when the data has been retrieved, and the view controller can then update the various fields.
Thus:
enum AstronomicalTimesError: Error {
case invalidResponse(Data?, URLResponse?)
}
class AstronomicalTimes {
let date: Date
let latitude: Float // generally we use Double, but for your purposes, this might be adequate
let longitude: Float
init (date: Date, latitude: Float, longitude: Float) {
self.date = date
self.latitude = latitude
self.longitude = longitude
}
func start(completion: #escaping (Result<AstronomicalTimesResponse, Error>) -> Void) {
var components = URLComponents(string: "https://api.sunrise-sunset.org/json")!
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // just in case your end user isn't using Gregorian calendar
dateFormatter.dateFormat = "yyyy-MM-dd"
components.queryItems = [
URLQueryItem(name: "lat", value: "\(latitude)"),
URLQueryItem(name: "lng", value: "\(longitude)"),
URLQueryItem(name: "date", value: dateFormatter.string(from: date)),
URLQueryItem(name: "formatted", value: "0")
]
let session = URLSession.shared
let url = components.url!
let request = URLRequest(url: url)
session.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
}
return
}
do {
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
DispatchQueue.main.async {
completion(.success(astronomicalTimesResponse))
}
} catch let jsonError {
DispatchQueue.main.async {
completion(.failure(jsonError))
}
}
}.resume()
}
}
Then your viewDidLoad might do something like:
override viewDidLoad() {
super.viewDidLoad()
let astronomicalTimes = AstronomicalTimes(date: someDate, latitude: someLatitude, longitude: someLongitude)
astronomicalTimes.start { result in
switch result {
case .success(let astronomicalTimesResponse):
// populate your fields here
case .failure(let error):
print(error)
}
}
}

How to safely force unwrap time in dictionary

I have an issue. I have a dictionary type [String: Any]
my code that works is
dict["start"] = "\(start.hour!):\(start.minute!)"
if let end = end {
dict["end"] = "\(end.hour!):\(end.minute!)"
}
But as I use swiftlint it throws me an error for force unwrapping. Value must be saved so if let is not good here :)
that is mostly a semantic issue, but you could do something like this:
if let startHour = start.hour,
let startMinute = start.minute {
dict["start"] = "\(startHour):\(startMinute)"
if let end = end,
let endHour = end.hour,
let endMinute = end.minute {
dict["end"] = "\(endHour):\(endMinute)"
}
}
...or something similar – as there are various ways in Swift to safely unwrap an optional.
You can try the following demo code. I hope it will help you.
import Foundation
let calendar = Calendar.current
let dateComponents = DateComponents(hour: 12, minute: 20, second: 55)
func getHoursMinutesFrom(time: DateComponents) -> (hour: Int,minute: Int) {
switch (time.hour, time.minute) {
case let (.some(hour), .some(minutes)):
return (hour,minutes)
default:
return (0,0)
}
}
print(getHoursMinutesFrom(time: dateComponents))

Saving an object with date to user defaults - date won't be the same

I am saving and restoring a simple struct that contains a date. The reason for using this string format is that RESTful API is returning this date format. Meaning that it shouldn't be changed for this code example below.
extension DateFormatter {
static let yyyyMMdd: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
struct Person: Codable, Equatable {
let birthDate: Date
private enum CodingKeys: String, CodingKey {
case birthDate
}
}
func storePerson(_ person: Person) {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(DateFormatter.yyyyMMdd)
guard let encodedPerson = try? encoder.encode(person) else {
return
}
UserDefaults.standard.set(encodedPerson, forKey: "savedPerson")
}
func fetchPerson() -> Person {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.yyyyMMdd)
guard let savedPerson = UserDefaults.standard.data(forKey: "savedPerson"),
let person = try? decoder.decode(Person.self, from: savedPerson) else {
fatalError()
}
return person
}
let person = Person(birthDate: Date())
storePerson(person)
let storedPerson = fetchPerson()
print("\(person == storedPerson)") // this will be false
print("\(person.birthDate)")
print("\(storedPerson.birthDate)") // same date as above, but with no minutes
Why is the date not the same? The encoder and decoder are using the same dateEncodingStrategy

swift: convert long/int64 to date

I want to convert 1529704800000 to a Date object but I don't get the right date. I'm getting the value out of a json. I'm converting the value on this way:
class Example: Decodable {
var id: Int64?
var date: Date?
init(json: [String: Any]) {
id = json["id"] as? Int64 ?? -1
var dateTime = (json["date"] as AnyObject? as? Int64) ?? 0
date = Date(timeIntervalSince1970: (TimeInterval(dateTime / 1000)))
}
static func fetchReportsForUser(authorId: Int64) -> [Report]? {
let urlString = "http://localhost:8080/test-application/rest/example/"
let url = URL(string: urlString)!
var examples = [Example]()
let group = DispatchGroup()
group.enter()
DispatchQueue.global(qos: .default).async {
URLSession.shared.dataTask(with: url) { (data, response, error) -> Void in
if error != nil {
print(error!)
return
}
guard let data = data else {
return
}
if(data.isEmpty) {
group.leave()
return
}
do {
examples = try JSONDecoder().decode([Example].self, from: data)
} catch let err {
print(err)
}
group.leave()
}.resume()
}
group.wait()
return examples
}
}
When I do it like that I still get 50472-09-04 16:00:00 +0000 as date. I tried it with Double instead of Int64 but I got the same result.
Just let JSONDecoder do the job by using the appropriate date decoding strategy
let json = """
{"id" : 1, "date" : 1529704800000}
"""
struct Example : Decodable {
let id : Int
let date : Date
}
let data = Data(json.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
do {
let result = try decoder.decode(Example.self, from: data)
print(result) // Example(id: 1, date: 2018-06-22 22:00:00 +0000)
} catch {
print(error)
}
Note: Don't make an asynchronous task synchronous. Learn to understand asynchronous data processing and use a completion handler.
Your integer seems to represent milliseconds rather than seconds, this it is giving a date thousands of years in the future!
Dividing it by 1000 (removing the last 3 zeros) gives 22 June 2018 10pm UTC.
Also, try changing the lines where you cast from the json to a date:
if let dateTime = json["date"] as? Int {
date = Date(timeIntervalSince1970: dateTime/1000)
}