How to get original (EXIF?) creation date/time of PHAsset? - ios

PHAsset has a creationDate property that gives the creation date of the asset in UTC.
If I take a photo taken at 10:52 PM UTC-6, the creationDate property is 03:52 AM.
How am I supposed to know the 'true' time of the photo taken? No timezone information is supplied with the creationDate property, so I can't adjust this back to 10:52 PM - I have no idea what time zone to just when adjusting it...
I know I can get the EXIF creation date (or attempt to at least) using PHImageManager requestImageDataForAsset and using the data returned there to obtain an EXIF creation date which actually is 10:52 PM, but this results in very slow for performance when the photos are in iCloud (I need this creation date value for all assets in the user's library). Additionally I cannot figure out how to get the EXIF creation date for videos.
I also know there are methods of obtaining a timezone from a CLLocation, which I could use to adjust the creationDate - but these methods are either rate limited and/or inaccurate
Is there another, easier way, to get this original creation date value?
To recap:
A photo is taken at 10:52 PM in UTC-6
PHAsset creation date is 03:52 AM in UTC
I want to know how to get 10:52 PM UTC-6 date/time.

You can try using PHContentEditingInput.
It will be probably slow for iCloud images too but it is worth trying as I don't think there are any other ways to do it :(
This approach does not require ImageIO import and needs less code so maybe it would be somewhat better than PHImageManager requestImageDataForAsset
You can use this PHAsset category to try it quickly:
https://github.com/zakkhoyt/PHAsset-Utility

Can get exif details as below :
let options = PHContentEditingInputRequestOptions()
asset.requestContentEditingInput(with: options) { input, _ in
guard let url = input?.fullSizeImageURL else { return }
guard let image = CIImage(contentsOf: url) else { return }
guard let exif = image.properties["{Exif}"] as? [String: Any] else { return }
print(exif["DateTimeOriginal"] ?? "")
print(exif["SubsecTimeDigitized"] ?? "")
}
This prints:
2021:01:04 11:47:07
177

Related

How can I get GNU Lib C TZ format output from NSTimeZone?

I need to set the timezone information of a remote clock to the one on the iOS device.
The remote clock only supports GNU lib C TZ format of:
std offset dst [offset],start[/time],end[/time]
e.g: EST+5EDT,M3.2.0/2,M11.1.0/2
So I need to produce a string similar to above from NSTimeZone.local time zone in Swift. Can't seem to access the current timezone rules as they would be in the IANA TZ database to produce the output.
Can this be done without the horrifying idea of caching a local copy of the TZ database in the app?
Update:
I haven't been able to find anything useful even through other programming languages. The best I was able to find was essentially parsing the tzfile in linux and making my own NSDictionary containing the info.
This was a fun exploration, largely because fitting the data into just the right format is pretty complex. Problem components:
We need the "current" TZ database rule that applies for a given time zone. This is a bit of a loaded concept, because:
Darwin platforms don't actually use the TZ database directly for most applications, but instead use ICU's time zone database, which comes in a different format and is more complex. Even if you produce a string in this format, it's not necessarily descriptive of the actual time behavior on device
While it is possible to read and parse the TZ database on iOS dynamically, the TZ database itself is not guaranteed to store information in the format needed here. rfc8536, the RFC governing the Time Zone Information Format says the following about the format you want:
The TZ string in a version 3 TZif file MAY use the following extensions to POSIX TZ strings. These extensions are described using the terminology of Section 8.3 of the "Base Definitions" volume of [POSIX].
Example: <-03>3<-02>,M3.5.0/-2,M10.5.0/-1
Example: EST5EDT,0/0,J365/25
While spelunking through the iOS TZ database, I found some database entries that do offer a rule at the end of the file in this format, but they appear to be a minority. You could parse these dynamically, but it's likely not worth it
So, we need to use APIs to produce a string in this format.
In order to produce a "rule" that is at least approximately correct on a given date, you need to know information about DST transitions around that date. This is an extremely thorny topic, because DST rules change all the time, and don't always make as much sense as you'd hope. At the very least:
Many time zones in the Northern hemisphere observe DST beginning in the spring and ending in the fall
Many time zones in the Southern hemisphere observe DST beginning in the fall and ending in the spring
Some time zones don't observe DST (are in standard time year-round)
Some time zones don't observe DST and are in daylight time year-round
Because the rules are so complex, the rest of this answer assumes you're okay with producing a "good enough" answer that represents a specific date in time, and is willing to send further strings to your clock some time in the future when corrections are needed. e.g., to describe "now", we will be assuming that producing a rule based off of the last DST transition (if any) and the next DST transition (if any) is "good enough", but this may not work for all situations in many time zones
Foundation provides DST transition information on TimeZone in the form of TimeZone.nextDaylightSavingTimeTransition/TimeZone.nextDaylightSavingTimeTransition(after:). Frustratingly, however, there's no way to get information about previous DST transitions, so we'll need to rectify that:
Foundation's localization support (including calendars and time zones) is based directly on the ICU library, which ships internally on all Apple platforms. ICU does provide a way to get information about previous DST transitions, but Foundation just doesn't offer this as API, so we'll need to expose it ourselves
ICU is a semi-private library on Apple platforms. The library is guaranteed to be present, and Xcode will offer you libicucore.tbd to link against in <Project> > <Target> > Build Phases > Link Binary with Libraries, but the actual headers and symbols are not directly exposed to apps. You can successfully link against libicucore, but you'll need to forward-declare the functionality we need in an Obj-C header imported into Swift
Somewhere in the Swift project, we need to expose the following ICU functionality:
#include <stdint.h>
typedef void * _Nonnull UCalendar;
typedef double UDate;
typedef int8_t UBool;
typedef uint16_t UChar;
typedef enum UTimeZoneTransitionType {
UCAL_TZ_TRANSITION_NEXT,
UCAL_TZ_TRANSITION_NEXT_INCLUSIVE,
UCAL_TZ_TRANSITION_PREVIOUS,
UCAL_TZ_TRANSITION_PREVIOUS_INCLUSIVE,
} UTimeZoneTransitionType;
typedef enum UCalendarType {
UCAL_TRADITIONAL,
UCAL_DEFAULT,
UCAL_GREGORIAN,
} UCalendarType;
typedef enum UErrorCode {
U_ZERO_ERROR = 0,
} UErrorCode;
UCalendar * _Nullable ucal_open(const UChar *zoneID, int32_t len, const char *locale, UCalendarType type, UErrorCode *status);
void ucal_setMillis(const UCalendar * _Nonnull cal, UDate date, UErrorCode * _Nonnull status);
UBool ucal_getTimeZoneTransitionDate(const UCalendar * _Nonnull cal, UTimeZoneTransitionType type, UDate * _Nonnull transition, UErrorCode * _Nonnull status);
These are all forward declarations / constants, so no need to worry about implementation (since we get that by linking against libicucore).
You can see the values in UTimeZoneTransitionType — TimeZone.nextDaylightSavingTimeTransition just calls ucal_getTimeZoneTransitionDate with a value of UCAL_TZ_TRANSITION_NEXT, so we can offer roughly the same functionality by calling the method with UCAL_TZ_TRANSITION_PREVIOUS:
extension TimeZone {
func previousDaylightSavingTimeTransition(before: Date) -> Date? {
// We _must_ pass a status variable for `ucal_open` to write into, but the actual initial
// value doesn't matter.
var status = U_ZERO_ERROR
// `ucal_open` requires the time zone identifier be passed in as UTF-16 code points.
// `String.utf16` doesn't offer a contiguous buffer for us to pass directly into `ucal_open`
// so we have to create our own by copying the values into an `Array`, then
let timeZoneIdentifier = Array(identifier.utf16)
guard let calendar = Locale.current.identifier.withCString({ localeIdentifier in
ucal_open(timeZoneIdentifier, // implicit conversion of Array to a pointer, but convenient!
Int32(timeZoneIdentifier.count),
localeIdentifier,
UCAL_GREGORIAN,
&status)
}) else {
// Figure out some error handling here -- we failed to find a "calendar" for this time
// zone; i.e., there's no time zone date for this time zone.
//
// With more enum cases copied from `UErrorCode` you may find a good way to report an
// error here if needed. `u_errorName` turns a `UErrorCode` into a string.
return nil
}
// `UCalendar` functions operate on the calendar's current timestamp, so we have to apply
// `date` to it. `UDate`s are the number of milliseconds which have passed since January 1,
// 1970, while `Date` offers its time interval in seconds.
ucal_setMillis(calendar, before.timeIntervalSince1970 * 1000.0, &status)
var result: UDate = 0
guard ucal_getTimeZoneTransitionDate(calendar, UCAL_TZ_TRANSITION_PREVIOUS, &result, &status) != 0 else {
// Figure out some error handling here -- same as above (check status).
return nil
}
// Same transition but in reverse.
return Date(timeIntervalSince1970: result / 1000.0)
}
}
So, with all of this in place, we can fill out a crude method to produce a string in the format you need:
extension TimeZone {
struct Transition {
let abbreviation: String
let offsetFromGMT: Int
let date: Date
let components: DateComponents
init(for timeZone: TimeZone, on date: Date, using referenceCalendar: Calendar) {
abbreviation = timeZone.abbreviation(for: date) ?? ""
offsetFromGMT = timeZone.secondsFromGMT(for: date)
self.date = date
components = referenceCalendar.dateComponents([.month, .weekOfMonth, .weekdayOrdinal, .hour, .minute, .second], from: date)
}
}
func approximateTZEntryRule(on date: Date = Date(), using calendar: Calendar? = nil) -> String? {
var referenceCalendar = calendar ?? Calendar(identifier: .gregorian)
referenceCalendar.timeZone = self
guard let year = referenceCalendar.dateInterval(of: .year, for: date) else {
return nil
}
// If no prior DST transition has ever occurred, we're likely in a time zone which is either
// standard or daylight year-round. We'll cap the definition here to the very start of the
// year.
let previousDSTTransition = Transition(for: self, on: previousDaylightSavingTimeTransition(before: date) ?? year.start, using: referenceCalendar)
// Same with the following DST transition -- if no following DST transition will ever come,
// we'll cap it to the end of the year.
let nextDSTTransition = Transition(for: self, on: nextDaylightSavingTimeTransition(after: date) ?? year.end, using: referenceCalendar)
let standardToDaylightTransition: Transition
let daylightToStandardTransition: Transition
if isDaylightSavingTime(for: date) {
standardToDaylightTransition = previousDSTTransition
daylightToStandardTransition = nextDSTTransition
} else {
standardToDaylightTransition = nextDSTTransition
daylightToStandardTransition = previousDSTTransition
}
let standardAbbreviation = daylightToStandardTransition.abbreviation
let standardOffset = formatOffset(daylightToStandardTransition.offsetFromGMT)
let daylightAbbreviation = standardToDaylightTransition.abbreviation
let startDate = formatDate(components: standardToDaylightTransition.components)
let endDate = formatDate(components: daylightToStandardTransition.components)
return "\(standardAbbreviation)\(standardOffset)\(daylightAbbreviation),\(startDate),\(endDate)"
}
/* These formatting functions can be way better. You'll also want to actually cache the
DateComponentsFormatter somewhere.
*/
func formatOffset(_ dateComponents: DateComponents) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = .dropTrailing
return formatter.string(from: dateComponents) ?? ""
}
func formatOffset(_ seconds: Int) -> String {
return formatOffset(DateComponents(second: seconds))
}
func formatDate(components: DateComponents) -> String {
let month = components.month ?? 0
let week = components.weekOfMonth ?? 0
let day = components.weekdayOrdinal ?? 0
let offset = formatOffset(DateComponents(hour: components.hour, minute: components.minute, second: components.second))
return "M\(month).\(week).\(day)/\(offset)"
}
}
Note that there's lots to improve here, especially in clarity and performance. (Formatters are notoriously expensive, so you'll definitely want to cache them.) This also currently only produces dates in the expanded form "Mm.w.d" and not Julian days, but that can be bolted on. The code also assumes that it's "good enough" to restrict unbounded rules to the current calendar year, since this is what the GNU C library docs seem to imply about e.g. time zones which are always in standard/daylight time. (This also doesn't recognize well-known time zones like GMT/UTC, which might be sufficient to just write out as "GMT".)
I have not extensively tested this code for various time zones, and the above code should be considered a basis for additional iteration. For my time zone of America/New_York, this produces "EST-5EDT,M3.3.2/3,M11.2.1/1", which appears correct to me at first glance, but many other edge cases might be good to explore:
Boundary conditions around the start/end of the year
Giving a date which exactly matches a DST transition (consider TRANSITION_PREVIOUS vs. TRANSITION_PREVIOUS_INCLUSIVE)
Time zones which are always standard/daylight
Non-standard daylight/timezone offsets
There's a lot more to this, and in general, I'd recommend trying to find an alternative method of setting a time on this device (preferably using named time zones), but this might hopefully at least get you started.

Getting MPMediaPlaylist last modified date

Looking for a way to get the last modified date of music playlists.This is how I get the playlists and trying to get the property value:
if let playlists = MPMediaQuery.playlists().collections as? [MPMediaPlaylist] {
for playlist in playlists {
let date = playlist.value(forProperty: "modificationDateTime") as? Any
print(date)
}
}
Property is from the class header: https://github.com/xybp888/iOS-Header/blob/master/13.0/PrivateFrameworks/MusicLibrary.framework/MIPPlaylist.hAny date property returns nil, other properties, such as "cloudGlobalID", works fine.
Any help would be much appreciated.
dateModified property returns the date I was looking for.
dateCreated also returns when the playlist was created or added (for Apple Music playlists) to the library.

How can I get 2 Time Zone from EKEvent? There are two startTimeZone, but not endTimeZone

I am making a calendar app and want to create a view for each event, instead of using EventKitUI. When I get the data with like;
let eventStore = EKEventStore()
if let val = eventStore.eventWithIdentifier(id) {
print(val)
}
It shows
startTimeZone = US/Pacific (PST) offset -28800;
startTimeZone = America/New_York (EST) offset -18000 ...
When I get timezone data with;
print(val.timeZone)
It shows only first one. I need to second one too!
Optional(US/Pacific (PST) offset -28800)
How can I get it? The second timeZone must be for endDate.
I should have tested... I think it is a kind of bug. Anyway, I wrote the answer for the people like me.
val.valueForKey("startTimeZone") //you can get first one
val.valueForKey("endTimeZone") //you can get time zone for end.

How to cache images in IOS App with expiry age using swift

In an iOS app, how can I cache an image with specified expiry age? There are examples on how to store and retrieve images, but how can I set an expiry period to auto delete old images?
As indicated by Fahri, you will need to manage the cache yourself (or using an open source library). You could easily create a cache directory to store your images. Then, at application launch, you parse this image cache directory to check image creation date, check time elapsed and remove those older than the specified age.
The below Swift code will do this parsing/removing job, I set the specified age to 30,000 (seconds)
// We list the stored images in Caches/Images and delete old ones
let cacheDirectory = NSFileManager.defaultManager().URLsForDirectory(.CachesDirectory, inDomains: .UserDomainMask).first! as NSURL
let filelist = try? filemanager.contentsOfDirectoryAtPath(cacheDirectory.path!)
var newDir = cacheDirectory.URLByAppendingPathComponent("Images")
var properties = [NSURLLocalizedNameKey, NSURLCreationDateKey, NSURLLocalizedTypeDescriptionKey]
var URLlist = try? filemanager.contentsOfDirectoryAtURL(newDir, includingPropertiesForKeys: properties, options: [])
if URLlist != nil {
for URLname in URLlist! {
let filePath = URLname.path!
let attrFile: NSDictionary? = try? filemanager.attributesOfItemAtPath(filePath)
let createdAt = attrFile![NSFileCreationDate] as! NSDate
let createdSince = fabs( createdAt.timeIntervalSinceNow )
#if DEBUG
print( "file created at \(createdAt), \(createdSince) seconds ago" )
#endif
if createdSince > 30000 {
let resultDelete: Bool
do {
try filemanager.removeItemAtPath(filePath)
resultDelete = true
} catch _ {
resultDelete = false
}
#if DEBUG
print("purging file =\(filePath), result= \(resultDelete)")
#endif
}
}
}
Web is web, iOS is iOS. If you want to create image cache with expiration, you have to implement it yourself, or use open source lib. I can give you the idea, it's not hard to implement. So, in addition to storing and retrieving functionality, you also need to add metadata management methods, using which you could know when the image was added, and what's the expiration date for that image, and when some events occur (app become active, going to background etc.) you should check the meta for your images, and delete the image if the expiration date passed. That's it, nothing hard. Good luck!
P.S.: In some git source projects I have seen the functionality your are looking for, check DFCache on github, maybe it suits your needs.

NSDateFormatter still parsing instead having incorrect format

Having some problems parsing date. I have an array of supported formats and once I receive the date (string) from API, I try to parse it iterating through the formats until I get a valid NSDate object.
A snippet from Xcode Playground --
let dateString = "02/06/1987" // --> want to parse into this Feb 6, not Jun 2
let dateFormatIncorrect = "dd.MM.yyyy"
let dateFormatCorrect = "MM/dd/yyyy"
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = dateFormatIncorrect
let date = dateFormatter.dateFromString(dateString)! // "Jun 2, 1987, 12:00 AM"
dateFormatter.dateFormat = dateFormatCorrect
let date2 = dateFormatter.dateFromString(dateString)! // "Feb 6, 1987, 12:00 AM"
Why does it parse the date even though the format is clearly incorrect for a given string? Could not find anything in the docs regarding date formatter ignoring separators.
I realise the proper solution would be to have a fixed format returned from API but was wondering what is happening here?
Thanks.
It seems that NSDateFormatter is extremely lenient when parsing a date string.
Unfortunately, I could not find a reference for this, but even with
dateFormatIncorrect = "'aaa'dd'bbb'MM'ccc'yyyy'ddd'"
the date string "02/06/1987" is successfully parsed. There is a lenient property,
but that is false by default, and setting it explicitly makes no difference.
As a workaround, you could convert the parsed date back to a string, and only if
the result is equal to the original string, the date is accepted:
extension NSDateFormatter {
func checkedDateFromString(string : String) -> NSDate? {
if let date = self.dateFromString(string) {
if self.stringFromDate(date) == string {
return date
}
}
return nil
}
}
Using this custom extension,
dateFormatter.checkedDateFromString(dateString)
returns nil for the incorrect date format.
Generally, if you work with fixed date formats, you should also set the locale
to "en_US_POSIX"
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
(see What is the best way to deal with the NSDateFormatter locale "feechur"?). However, this makes no difference for this
particular problem.
Update for Swift 3:
extension DateFormatter {
func checkedDate(from: String) -> Date? {
if let date = date(from: from), string(from: date) == from {
return date
}
return nil
}
}
This could be related to the fact that NSDateFormatter will anyways respects the users settings when using fixed formats
Although in principle a format string specifies a fixed format, by
default NSDateFormatter still takes the user’s preferences (including
the locale setting) into account
So may be the locale defined in your preference uses '/' for separator and satisfies the 'incorrect format'. Even if that is not the case, apple noted in several places that NSDateFormatter might not act consistently. So try setting a fixed locale as below and see if that helps
NSLocale *locale = [[NSLocale alloc]
initWithLocaleIdentifier:#"en_US_POSIX"];
[dateFormatter setLocale:locale];
See these links for detail: apple tech note . Note directly related to separators, but that could be related.
Had a similar issue:
NSDateFormatter returns date object from invalid input
Filed a bug report at Apple.
Result: Will not be fixed, as the change could break working code, in addition it is more error tolerant and thus provides some kind of convenience.
Please know that our engineering team has determined that this issue
behaves as intended based on the information provided.
It appears that ICU’s udat_parseCalendar() is very lenient and still
is able to parse even if the string doesn’t exactly match the format.
We understand preferring that the formatter return nil in these cases
but (1) there’s no easy way for us to know that the input string
doesn’t match the format since ICU allows it and doesn’t throw an
error and (2) suddenly returning nil in these cases would almost
certainly be a bincompat issue.
In my case I had the option to either modify the unit tests and be more tolerant in case of invalid input or have an additional checkup (based on the recommended approach, which is the accepted answer for the post) whether the resulting NSDate's string fits to the input string.

Resources