Test fails when asserting dates - ios

I am trying to verify that two items I am storing are the same. However, while testing I am getting an error when checking a Date property.
Note: my Item class implements the Equatable protocol.
This is my setUp method:
class InputViewControllerTests: XCTestCase {
var sut: InputViewController!
var placemark: MockPlacemark!
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "Main",
bundle: nil)
sut = storyboard
.instantiateViewController(
withIdentifier: "InputViewController")
as! InputViewController
_ = sut.view
}
}
This is the extension of my test class:
extension InputViewControllerTests {
class MockGeocoder: CLGeocoder {
var completionHandler: CLGeocodeCompletionHandler?
override func geocodeAddressString(
_ addressString: String,
completionHandler: #escaping CLGeocodeCompletionHandler) {
self.completionHandler = completionHandler
}
}
class MockPlacemark : CLPlacemark {
var mockCoordinate: CLLocationCoordinate2D?
override var location: CLLocation? {
guard let coordinate = mockCoordinate else
{ return CLLocation() }
return CLLocation(latitude: coordinate.latitude,
longitude: coordinate.longitude)
}
}
}
This is my test:
func test_Save_UsesGeocoderToGetCoordinateFromAddress() {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
let timestamp = 1456095600.0
let date = Date(timeIntervalSince1970: timestamp)
sut.titleTextField.text = "Foo"
sut.dateTextField.text = dateFormatter.string(from: date)
sut.locationTextField.text = "Bar"
sut.addressTextField.text = "Infinite Loop 1, Cupertino"
sut.descriptionTextField.text = "Baz"
let mockGeocoder = MockGeocoder()
sut.geocoder = mockGeocoder
sut.itemManager = ItemManager()
sut.save()
placemark = MockPlacemark()
let coordinate = CLLocationCoordinate2DMake(37.3316851,
-122.0300674)
placemark.mockCoordinate = coordinate
mockGeocoder.completionHandler?([placemark], nil)
let item = sut.itemManager?.item(at: 0)
let testItem = ToDoItem(title: "Foo",
itemDescription: "Baz",
timestamp: timestamp,
location: Location(name: "Bar",
coordinate: coordinate))
XCTAssertEqual(item, testItem)
}
This is the implementation of the save() method:
class InputViewController: UIViewController {
// ...
#IBAction func save() {
guard let titleString = titleTextField.text,
titleString.characters.count > 0 else { return }
let date: Date?
if let dateText = self.dateTextField.text,
dateText.characters.count > 0 {
date = dateFormatter.date(from: dateText)
} else {
date = nil
}
let descriptionString = descriptionTextField.text
if let locationName = locationTextField.text,
locationName.characters.count > 0 {
if let address = addressTextField.text,
address.characters.count > 0 {
geocoder.geocodeAddressString(address) {
[unowned self] (placeMarks, error) -> Void in
let placeMark = placeMarks?.first
let item = ToDoItem(
title: titleString,
itemDescription: descriptionString,
timestamp: date?.timeIntervalSince1970,
location: Location(
name: locationName,
coordinate: placeMark?.location?.coordinate))
self.itemManager?.add(item)
}
}
}
}
}
I am having trouble trying to figure out what is wrong with this. The error I am getting is:
test_Save_UsesGeocoderToGetCoordinateFromAddress()] failed: XCTAssertEqual failed: ("Optional(ToDo.ToDoItem(title: "Foo", itemDescription: Optional("Baz"), timestamp: Optional(1456030800.0), location: Optional(ToDo.Location(name: "Bar", coordinate: Optional(__C.CLLocationCoordinate2D(latitude: 37.331685100000001, longitude: -122.03006739999999))))))") is not equal to ("Optional(ToDo.ToDoItem(title: "Foo", itemDescription: Optional("Baz"), timestamp: Optional(1456095600.0), location: Optional(ToDo.Location(name: "Bar", coordinate: Optional(__C.CLLocationCoordinate2D(latitude: 37.331685100000001, longitude: -122.03006739999999))))))") -
As it can be clearly seen, the problem is that the timestamp is not the same in both, and I have no idea why it is changing.
EDIT: As #ganzogo found, there is a difference of exactly 18 hours between this too items. I am living in Ecuador which is GTM-5. Perhaps this could be a cue to figure out the problem.

After your line:
let dateFormatter = DateFormatter()
Try this:
dateFormatter.timeZone = TimeZone(secondsFromGMT: 64800)
If that doesn't work, try:
dateFormatter.timeZone = TimeZone(secondsFromGMT: -64800)
:-)
But you're kind of defeating the purpose of a unit test if you're just hacking it until in passes. You really need to understand whether the testItem or the item is exhibiting the correct behaviour right now, and that will depend on your application.

In func
test_Save_UsesGeocoderToGetCoordinateFromAddress()
timestamp transform to sut.dateTextField.text,then
In sut.save()
sut.dateTextField.text
transform to timestamp, the minute and second are drop

Related

How do I avoid pyramid of doom - iOS?

I have a pyramid of doom in my code.
if places.count > 0 {
for i in 0..<places.count {
for j in 0..<places.count {
if let nameI = places[i]["name"] {
if let cityI = places[i]["city"] {
if let nameJ = places[j]["name"] {
if let cityJ = places[j]["city"] {
if let latI = places[i]["lat"] {
if let lonI = places[i]["lon"] {
if let latitudeI = Double(latI) {
if let longitudeI = Double(lonI) {
if let latJ = places[j]["lat"] {
if let lonJ = places[j]["lon"] {
if let latitudeJ = Double(latJ) {
if let longitudeJ = Double(lonJ) {
if(i != j) {
let coordinateI = CLLocation(latitude: latitudeI, longitude: longitudeI)
let coordinateJ = CLLocation(latitude: latitudeJ, longitude: longitudeJ)
let distanceInMeters = coordinateI.distance(from: coordinateJ) // result is in meters
let distanceInMiles = distanceInMeters/1609.344
var distances = [Distance]()
distances.append(Distance(
distanceInMiles: distanceInMiles,
distanceInMeters: distanceInMeters,
places: [
Place(name: nameI, city: cityI, lat: latitudeI, long: longitudeI, coordinate: coordinateI),
Place(name: nameJ, city: cityJ, lat: latitudeJ, long: longitudeJ, coordinate: coordinateJ),
]
))
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
How do I avoid it?
Is there a technique or rule I should follow?
In iOS, we have to use a lot of if-let. How would one avoid doing like me ?
Here's a first approximation. As you can see, there are lots of repeated patterns that can be extracted out.
for (i, placeDictI) in 0..<places.enumerated() {
guard
let nameI = placeDictI["name"],
let cityI = placeDictI["city"],
let latitudeI = placeDictI["lat"].map(Double.init),
let longitudeI = placeDictI["lon"].map(Double.init),
else { continue }
let coordinateI = CLLocation(latitude: latitudeI, longitude: longitudeI)
let placeI = Place(name: nameI, city: cityI, lat: latitudeI, long: longitudeI, coordinate: coordinateI)
for (j, placeDictJ) in places.enumerated() where i != j {
guard let nameJ = placeDictI["name"],
let cityJ = placeDictI["city"],
let latitudeJ = placeDictI["lat"].map(Double.init),
let longitudeJ = placeDictI["lon"].map(Double.init)
else { continue }
let coordinateJ = CLLocation(latitude: latitudeJ, longitude: longitudeJ)
let placeJ = Place(name: nameJ, city: cityJ, lat: latitudeJ, long: longitudeJ, coordinate: coordinateJ)
let distanceInMeters = coordinateI.distance(from: coordinateJ) // result is in meters // Editor's note: REALLY? I would have thought that a variable called "distanceInMeters" would store volume in litres! Silly me!
let distanceInMiles = distanceInMeters/1609.344
var distances = [Distance]()
distances.append(Distance(
distanceInMiles: distanceInMiles,
distanceInMeters: distanceInMeters,
places: [ placeI, placeJ ]
))
}
}
Here are the transformations I applied:
Don't check for places.count > 0. If it's 0, the loop won't do anything.
For every if statement whose block fully encompasses its parent block, I replaced it with a guard.
Merged adjacent guard statements with a comma.
Expressed failable type conversions as Optional.map(_:) expressions, rather than as separate let clauses in the guard statement
Changed the i != j check into a where condition on the inner for loop.
Changed this pattern:
for i in 0..<array.count {
use(array[i])
use(array[i])
use(array[i])
//...
}
to this pattern:
for (i, element) in array.enumerated() {
use(element)
use(element)
use(element)
//...
}
Seeing as there's no an obvious case of repetition, it's a good hint that the Dictionary unpacking into a Place belongs in a convenience initializer, that takes a dict that initailizes a Place? from it. Even better, just use the Codable system, and have the compiler synthesize it for you.

updating values in a realm database swift

I am trying to update the values on a realm database. If a user selects a row containing values I want to be able to update the values of that row. Here is my code but instead of updating, it creates another value in the database
func updateTodoList(todoList: TodoListModel, name: String, description: String, createdDate: Date, remiderDate: Date, photo: Data, isCompleted: Bool) -> Void {
try! database.write {
if name != "" {
todoList.name = name
} else {
todoList.name = "No extra information"
}
todoList.desc = description
todoList.createdDate = createdDate
todoList.remiderDate = remiderDate
todoList.photo = photo
todoList.isCompleted = false
}
}
my did select row
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let todoList = todoItems?[indexPath.row]
let storyBoard: UIStoryboard = UIStoryboard(name: "AddTodoListSB", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: Constants.ADD_TODO_SB) as! AddTodoListVC
newViewController.loadViewIfNeeded()
let min = Date()
let max = Date().addingTimeInterval(60 * 60 * 60 * 60)
guard let itemPhoto = UIImagePNGRepresentation(newViewController.imageView.image!) else {return}
newViewController.picker.minimumDate = min
newViewController.picker.maximumDate = max
// newViewController.showDateTimePicker(sender: <#T##AnyObject#>)
newViewController.picker.completionHandler = { date in
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
self.title = formatter.string(from: date)
let reminder = formatter.string(from: date)
TodoListFunctions.instance.updateTodoList(todoList: todoList!, name: newViewController.titleTxtField.text!, description: newViewController.moreInfoTxtView.text!, createdDate: (todoList?.createdDate)!, remiderDate: formatter.date(from: reminder)!, photo: itemPhoto, isCompleted: false)
}
tableView.reloadData()
self.present(newViewController, animated: true, completion: nil)
}
// TodolistModel
class TodoListModel: Object {
#objc dynamic var id = UUID().uuidString
#objc dynamic var name: String = ""
#objc dynamic var desc: String = "No Description"
#objc dynamic var photo: Data? = nil
#objc dynamic var createdDate: Date?
#objc dynamic var remiderDate: Date?
#objc dynamic var isCompleted = false
override static func primaryKey() -> String? {
return "id"
}
let parentCategory = LinkingObjects(fromType: CategoryModel.self, property: "items")
}
further codes would be supplied on request
To update an object it must have a primary key and after you edit it use
// if it doesn't exist it'll be added
database.add(editedObjc, update: true)
//
// create object 1 , note: r = database
let lista = TaskList()
lista.pid = 1
lista.name = "Whole List"
// create object 2
let lista2 = TaskList()
lista2.pid = 2
lista2.name = "Whole List 2"
// add to database by write
r.add([lista,lista2])
let stored = r.objects(TaskList.self)
print("before edit" , stored)
// edit name of object 2
lista2.name = "qqwwqwqwqwqwqwqwq"
// update the object after changing it's name
r.add(lista2, update: true)
let stored2 = r.objects(TaskList.self)
print("after edit" , stored2)

My weather data is not showing up in my view controller

I can't figure out why my view controller is not showing the data, even though I can see it in the output window.
Output:
Muḩāfaz̧at Al Jīzah
Clear
88.0
my code:
override func viewDidLoad() {
super.viewDidLoad()
loadCurrentWeather = currentWeatherData()
loadCurrentWeather.downloadWeatherData {
//setting uo UI to download data
self.updateTodayUI()
}
}
func updateTodayUI() {
locationLabel.text = loadCurrentWeather.cityName
weatherTypeLabel.text = loadCurrentWeather.weatherType
currentTempLabel.text = "\(loadCurrentWeather.currentTemp)"
weatherTypeImage.image = UIImage(named: loadCurrentWeather.weatherType)
}
My view controller in Xcode:
My view controller on iphone:
currentweatherData the code where I'm downloading the data form.
import UIKit
import Alamofire
class currentWeatherData {
var cityNameone: String!
var dateone: String!
var weatherTypeone: String!
var currentTempone: Double!
var cityName: String {
if cityNameone == nil {
cityNameone = ""
}
return cityNameone
}
var date: String {
if dateone == nil {
dateone = ""
}
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .none
let currentDate = dateFormatter.string(from: Date())
self.dateone = "Today, \(currentDate)"
return dateone
}
var weatherType: String{
if weatherTypeone == nil{
weatherTypeone = ""
}
return weatherTypeone
}
var currentTemp: Double {
if currentTempone == nil {
currentTempone = 0.0
}
return currentTempone
}
func downloadWeatherData(completed: DownloadComplete){
// to tell alamofire where to download the data
let weatherURL = URL (string: currentWeatherURL)!
Alamofire.request(weatherURL).responseJSON{ response in
let result = response.result
if let dictionary = result.value as? Dictionary<String, AnyObject>{
if let name = dictionary["name"] as? String {
self.cityNameone = name.capitalized
print(self.cityNameone ?? "No city name")
}
if let weather = dictionary["weather"] as? [Dictionary<String, AnyObject>]{
if let main = weather[0]["main"] as? String {
self.weatherTypeone = main.capitalized
print(self.weatherTypeone ?? "No weather type")
}
}
if let main = dictionary["main"] as? Dictionary<String, AnyObject> {
if let currentTemperature = main["temp"] as? Double {
let kelvintoFarenheit = (currentTemperature * (9/5) - 459.67)
let totalKelvinToFarenheit = Double(round(10 * kelvintoFarenheit/10))
self.currentTempone = totalKelvinToFarenheit
print(self.currentTempone ?? .nan)
}
}
}
}
completed()
}
}
Is problem with my code or my view controller? Is it something wrong with my constraints?
I can't seem to figure it out.
You are calling completed too early - before the JSON response arrives. You have to call it inside the closure of the responseJSON call instead:
Alamofire.request(weatherURL).responseJSON { response in
let result = response.result
// ...
completed()
}
I cannot see all of your code to troubleshoot, but you may have a concurrency issue. Try putting the call to updateTodayUI inside of viewDidLoad(_:) inside of an async block like this:
DispatchQueue.main.async {
updateTodayUI()
}
You can find more information on dispatch queues and concurrency in the documentation.

How can I rewrite this function so that it uses SwiftyJSON instead of JSON.swift?

I'm looking at the Ray Wenderlich tutorial http://www.raywenderlich.com/90971/introduction-mapkit-swift-tutorial and he is using there this function:
class func fromJSON(json: [JSONValue]) -> Artwork? {
// 1
var title: String
if let titleOrNil = json[16].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json[12].string
let discipline = json[15].string
// 2
let latitude = (json[18].string! as NSString).doubleValue
let longitude = (json[19].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}
Since I'm using SwiftyJSON in my project I would like to stay with that, so I thought about rewriting this function based on that.
If I understand correctly, this function takes one json node and creates Artwork object from it.
So how can I refer to a single json node with SwiftyJSON?
I tried doing:
class func fromJSON(JSON_: (data: dataFromNetworking))->Artwork?{
}
but it causes error use of undeclared type dataFromNetworking. On the other hand that's exactly how they use it in the documentation https://github.com/SwiftyJSON/SwiftyJSON
Could you help me with rewriting it?
My suggestion: separate the model layer from the presentation layer.
ArtworkModel
First of all you need a way to represent the data. A struct is perfect for this.
struct ArtworkModel {
let title: String
let locationName: String
let discipline: String
let coordinate: CLLocationCoordinate2D
init?(json:JSON) {
guard let
locationName = json[12].string,
discipline = json[15].string,
latitudeString = json[18].string,
latitude = Double(latitudeString),
longitueString = json[19].string,
longitude = Double(longitueString) else { return nil }
self.title = json[16].string ?? ""
self.locationName = locationName
self.discipline = discipline
self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
As you can see ArtworkModel is capable to initialize itself from a json.
The presentation layer
Now the Artwork (conform to MKAnnotation) becomes much easier.
class Artwork: NSObject, MKAnnotation {
private let artworkModel: ArtworkModel
init(artworkModel: ArtworkModel) {
self.artworkModel = artworkModel
super.init()
}
var title: String? { return artworkModel.title }
var subtitle: String? { return artworkModel.locationName }
var coordinate: CLLocationCoordinate2D { return artworkModel.coordinate }
}
Usage
You function now becomes
class func fromJSON(json: JSON) -> Artwork? {
guard let model = ArtworkModel(json: json) else { return nil }
return Artwork(artworkModel: model)
}
To use SwiftyJSON in this project first you have to change the method to retrieve the data from the property list file.
Note: This replacement is for Swift 2.
Replace the method loadInitialData() in ViewController with
func loadInitialData() {
do {
let fileName = NSBundle.mainBundle().pathForResource("PublicArt", ofType: "json")
let data = try NSData(contentsOfFile: fileName!, options: NSDataReadingOptions())
let jsonObject = JSON(data:data)
if let jsonData = jsonObject["data"].array {
for artworkJSON in jsonData {
if let artworkJSONArray = artworkJSON.array, artwork = Artwork.fromJSON(artworkJSONArray) {
artworks.append(artwork)
}
}
}
} catch let error as NSError {
print(error)
}
}
And then just exchange [JSONValue] in the method
class func fromJSON(json: [JSONValue]) -> Artwork? {
of the Artworkclass with [JSON], so it's now
class func fromJSON(json: [JSON]) -> Artwork? {
That's it.

Mapview - protection against coordinates found nil

I have mapview that downloads records off CloudKit. The coordinates of each record is based on forward geocoder, where users add the address (ex: New York, NY) and lats and lons are obtained
Current Model is as follow:
class Place: NSObject
{
var name: String
var address: String
var comment: String?
var photo: UIImage?
var rating: Int
var location: CLLocation?
var identifier: String
var record: CKRecord!
init(record: CKRecord)
{
self.record = record
self.name = record.valueForKey(placeName) as! String
self.address = record.valueForKey(placeAddress) as! String
self.comment = record.valueForKey(placeComment) as? String
if let photoAsset = record.valueForKey(placePhoto) as? CKAsset
{
self.photo = UIImage(data: NSData(contentsOfURL: photoAsset.fileURL)!)
}
self.rating = record.valueForKey(placeRating) as! Int
self.location = record.valueForKey(placeLocation) as? CLLocation
self.identifier = record.recordID.recordName
}
// MARK: Map Annotation
var coordinate: CLLocationCoordinate2D {
get {
return location!.coordinate
}
}
This is my method to place each pin on the mapview.
func placePins()
{
for place: Place in self.places
{
let location = CLLocationCoordinate2DMake(place.coordinate.latitude, place.coordinate.longitude)
let dropPin = CustomPointAnnotation(place: place)
dropPin.pinCustomImageName = "customPin"
dropPin.coordinate = location
dropPin.title = place.title
dropPin.subtitle = place.subtitle
dropPin.name = place.name
dropPin.image = place.photo
mapView.addAnnotation(dropPin)
}
}
How do i fix them to protect against any record that doesn't have coordinates since forward geocoder is not the most reliable way?
What about
for place: Place in self.places
{
if (place.location == nil) {
continue;
}
...
}
Not sure what is the issue there

Resources