Get build date and time in Swift
Asked Answered
O

7

20

I'm using __DATE__ and __TIME__ in Objective-C to get the build date and time of my app. I can't find a way to get this information in Swift. Is it possible?

Overstride answered 8/11, 2014 at 1:10 Comment(2)
There are lots of things you can't say in Swift. No big deal. Use a helper class written in Objective-C...Deemphasize
#43751360Mullens
V
15

You can use #line, #column, and #function.


Original answer:

Create a new Objective-C file in your project, and when Xcode asks, say yes to creating the bridging header.

In this new Objective-C file, add the following the the .h file:

NSString *compileDate();
NSString *compileTime();

And in the .m implement these functions:

NSString *compileDate() {
    return [NSString stringWithUTF8String:__DATE__];
}

NSString *compileTime() {
    return [NSString stringWithUTF8String:__TIME__];
}

Now go to the bridging header and import the .h we created.

Now back to any of your Swift files:

println(compileDate() + ", " + compileTime())
Visitor answered 8/11, 2014 at 2:13 Comment(6)
As a note, there's unfortunately no way that I know of to accurately use __LINE__ or __FILE__ in Swift. This same approach will just return the line/file of the Objective-C code, obviously.Visitor
Actually, there is a way. Check out this post from Apple: developer.apple.com/swift/blog/?id=15Overstride
I was looking for this but somehow I am not able to create the correct header file (I am not very familiar with Objective-C). When I create the Objective-C file, the bridging header is created, but not a separate header file. If I create a separate header file, the code above does not work for this. Maybe the answer can be made a bit more clear and have examples of the complete .h, .m and bridging-header file. This would be awesome! Thanks!Urbanite
@DavidPotter Note: These have changed to #file, #line, #column, #function in Swift 3.Misanthrope
There doesn't seem to exist a #date in Swift, nor anywhere in Apple's documentation. Can you add a link / full example of its use?Danley
@Danley no. I was just trying to update the syntax in a really old answer. Didn't actually check that #date is a thing. I've updated the answer.Visitor
A
34

You can get the build date and time without reverting to objective-C. When the app is built, the Info.plist file placed in the bundle is always created from the one in your project. So the creation date of that file matches the build date and time. You can always read files in your app's bundle and get their attributes. So you can get the build date in Swift by accessing its Info.plist file attributes:

 var buildDate:NSDate 
 {
     if let infoPath = NSBundle.mainBundle().pathForResource("Info.plist", ofType: nil),
        let infoAttr = try? NSFileManager.defaultManager().attributesOfItemAtPath(infoPath),
        let infoDate = infoAttr["NSFileCreationDate"] as? NSDate
     { return infoDate }
     return NSDate()
 }

Note: this is the post that got me to use the bridging header when I initially had this problem. I found this "Swiftier" solution since then so I thought I'd share it for future reference.

[EDIT] added compileDate variable to get the latest compilation date even when not doing a full build. This only has meaning during development since you're going to have to do a full build to release the application on the app store but it may still be of some use. It works the same way but uses the bundled file that contains the actual code instead of the Info.plist file.

var compileDate:Date
{
    let bundleName = Bundle.main.infoDictionary!["CFBundleName"] as? String ?? "Info.plist"
    if let infoPath = Bundle.main.path(forResource: bundleName, ofType: nil),
       let infoAttr = try? FileManager.default.attributesOfItem(atPath: infoPath),
       let infoDate = infoAttr[FileAttributeKey.creationDate] as? Date
    { return infoDate }
    return Date()
}
Aquamanile answered 17/7, 2016 at 12:58 Comment(9)
This is not giving a consistent result with __DATE__ since when you update your app info.plist will remain the same while __DATE__ gets proper updated by the time of the update.Oscan
I inferred that the OP is looking for build date and time. I'm not sure what you mean by "when you update your app". You're either rebuilding it, which will always change the creation date of the info.plist, or you're not rebuilding it and the date won't change (as expected).Aquamanile
Yeah, I meant rebuild. The thing is that if you go to simple rebuild via Xcode it will output same time like before. Going Product > Clean will force to update Info.plist. Can you confirm that?Oscan
I can confirm (xCode 8.2.1), Product > Clean ... then Build will update the Info.plist even if you make no changes to any files.Aquamanile
That's what I failed to did - to change any file.. I've just rebuilt and that caused no update to a compile date produced by your function. My bad. Thanks!Oscan
"CFBundleName" only works if you did not set it to something else yourself. By default it's set to the Product Name (in build settings), which in turn is set to the Target Name. Better use "CFBundleExecutable", which is set to the Executable Name, which is exactly what you want. If you set the Product Name this will become the executable name too. I don't know where you can set $(EXECUTABLE_NAME) though.Isoagglutination
Turns out $(EXECUTABLE_NAME) is a concatenation $EXECUTABLE_PREFIX, $PRODUCT_NAME and $EXECUTABLE_SUFFIX, which can be set in Build Settings. Changing the value of "CFBundleExecutable" in the info.plist results in issues. It is probably just meant so you can get the executable name while running, which is exactly what we need.Isoagglutination
+1 for the compileDate() method, exactly what all other solutions keep missing. This is super useful when building directly to a device.Altamirano
does this reliably work when the app is downloaded from the AppStore?Embryonic
V
15

You can use #line, #column, and #function.


Original answer:

Create a new Objective-C file in your project, and when Xcode asks, say yes to creating the bridging header.

In this new Objective-C file, add the following the the .h file:

NSString *compileDate();
NSString *compileTime();

And in the .m implement these functions:

NSString *compileDate() {
    return [NSString stringWithUTF8String:__DATE__];
}

NSString *compileTime() {
    return [NSString stringWithUTF8String:__TIME__];
}

Now go to the bridging header and import the .h we created.

Now back to any of your Swift files:

println(compileDate() + ", " + compileTime())
Visitor answered 8/11, 2014 at 2:13 Comment(6)
As a note, there's unfortunately no way that I know of to accurately use __LINE__ or __FILE__ in Swift. This same approach will just return the line/file of the Objective-C code, obviously.Visitor
Actually, there is a way. Check out this post from Apple: developer.apple.com/swift/blog/?id=15Overstride
I was looking for this but somehow I am not able to create the correct header file (I am not very familiar with Objective-C). When I create the Objective-C file, the bridging header is created, but not a separate header file. If I create a separate header file, the code above does not work for this. Maybe the answer can be made a bit more clear and have examples of the complete .h, .m and bridging-header file. This would be awesome! Thanks!Urbanite
@DavidPotter Note: These have changed to #file, #line, #column, #function in Swift 3.Misanthrope
There doesn't seem to exist a #date in Swift, nor anywhere in Apple's documentation. Can you add a link / full example of its use?Danley
@Danley no. I was just trying to update the syntax in a really old answer. Didn't actually check that #date is a thing. I've updated the answer.Visitor
O
12

Swift 5 version of Alain T's answer:

var buildDate: Date {
    if let infoPath = Bundle.main.path(forResource: "Info", ofType: "plist"),
        let infoAttr = try? FileManager.default.attributesOfItem(atPath: infoPath),
        let infoDate = infoAttr[.modificationDate] as? Date {
        return infoDate
    }
    return Date()
}
Oriflamme answered 30/12, 2019 at 23:36 Comment(2)
Should this work on MacOS too? I'm getting nil for infoPathLon
@JimB. https://mcmap.net/q/661677/-how-to-get-ios-app-archive-date-using-swift-duplicateMullens
I
10

A Tamperproof, Swift-Only Approach:

  1. Add a new Run Script build phase to your app and MAKE SURE it is set to run before the Compile Sources phase.

  2. Add this as the code in that script:

#!/bin/bash

timestamp=$(date +%s)
echo "import Foundation;let appBuildDate: Date = Date(timeIntervalSince1970: $timestamp)" > ${PROJECT_DIR}/Path/To/Some/BuildTimestamp.swift
  1. Create the file BuildTimestamp.swift at some path in your project, then make sure the output path in the script above matches where that file exists, relative to the project's root folder.

  2. In Build Settings, search for "Enable User Script Sandboxing" and turn that OFF. (Otherwise you'll get a permissions error when the script tries to run.)

  3. You now have a global appBuildDate that can be used anywhere in your project. (Build the project once before using the variable so that the script creates it in the file you specified.)

  4. Optional: if you'd like the date to update in incremental builds, be sure to uncheck the "based on dependency analysis" checkbox in the Run Script phase you created.

Advantages:

  1. It's automatic.

  2. It can't be affected by users changing the modification/creation date of various files in the app bundle (a concern on macOS).

  3. It doesn't need the old __TIME__ and __DATE__ from C.

  4. It's already a Date and ready to be used, as-is.

Immortalize answered 22/3, 2022 at 0:58 Comment(3)
This is the most elegant way to accomplish it. Just awesome!Disharmonious
Very nice solution, works well.Sclerometer
Latest Xcode complains: "Operation not permitted", "Sandbox: bash(15341) deny file-write-data". To solve it go to Build Settings / User Script Sandboxing and set it to "No".Cacao
S
4

A slight variation on previous answers, checking the executable creation date instead. This seems to work on macOS too (tested with a Catalyst app).

/// Returns the build date of the app.
public static var buildDate: Date
{
    if let executablePath = Bundle.main.executablePath,
        let attributes = try? FileManager.default.attributesOfItem(atPath: executablePath),
        let date = attributes[.creationDate] as? Date
    {
        return date
    }
    return Date()
}
Slatternly answered 1/1, 2021 at 11:44 Comment(1)
@Brian Hong s solution didn't work for me for iOS. Because info.plist's date can be outdated without a clean. But this solution here contains always the latest build date.Coltish
K
1

All the older answers here are not good, as they do not provide a steady and reliable way to get the actual build date. For instance, getting the file date of a file inside the app is not good because the file date could change without invalidating the app's code signature.

The official build date is added by Xcode to the app's Info.plist – that's the one you should be using.

E.g, with this code (sorry, it's in ObjC, but transscribing it to Swift shouldn't be so hard):

+ (NSDate *)buildDate {
    static NSDate *result = nil;
    if (result == nil) { 
        NSDictionary *infoDictionary = NSBundle.mainBundle.infoDictionary;
        NSString *s = [infoDictionary valueForKey:@"BuildDateString"];
        NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init];
        NSDate *d = [formatter dateFromString:s];
        result = d;
    }
    return result;
}

And this is the script you'll have to run from your project's Build Phases in order to add the BuildDateString to your Info.plist:

#!/bin/sh
infoplist="$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH"
builddate=`date +%Y-%m-%dT%H:%M:%S%z`
if [[ -n "$builddate" ]]; then
    # if BuildDateString doesn't exist, add it
    /usr/libexec/PlistBuddy -c "Add :BuildDateString string $builddate" "${infoplist}"
    # and if BuildDateString already existed, update it
    /usr/libexec/PlistBuddy -c "Set :BuildDateString $builddate" "${infoplist}"
fi
Krever answered 7/7, 2021 at 15:6 Comment(1)
How are you getting a BuildDateString added to the plist? Xcode isn't automatically doing that for me, at least not for macOS apps.Nils
P
0

Relying on the creation date of the Info.plist won't work. The retrieved result in some scenarios can be the date-timestamp of when the app gets installed into your computer, which is what actually happened to me.

Here are my two thoughts:

  1. Use contentModificationDateKey instead. Still, this might be unreliable if one copied this one to FAT or NTFS volume, ruining the timestamp information.

  2. Find a method to get the CFDate value of kseccodeinfotimestamp. This is not tamperable. See the following example:

(It'll return nil if not signed by Apple Developer ID Application, etc. Ad-hoc signatures will let it throw nil, too.)

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.

import Foundation

let url = URL.init(fileURLWithPath: "/Users/shikisuen/Library/Input Methods/vChewing.app/")

func getCodeSignedDate(bundleURL: URL) -> Date? {
  var code: SecStaticCode?
  var information: CFDictionary?
  let status4Code = SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(rawValue: 0), &code)
  guard status4Code == 0, let code = code else { 
    NSLog("Error from getCodeSignedDate(): Failed from retrieving status4Code.")
    return nil
  }
  let status = SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &information)
  guard status == noErr else { 
    NSLog("Error from getCodeSignedDate(): Failed from retrieving code signing intelligence.")
    return nil
  }
  guard let dictionary = information as? [String: NSObject] else { return nil }
  guard dictionary[kSecCodeInfoIdentifier as String] != nil else {
    NSLog("Error from getCodeSignedDate(): Target not signed.")
    return nil
  }
  guard let infoDate = dictionary[kSecCodeInfoTimestamp as String] as? Date else {
    NSLog("Error from getCodeSignedDate(): Target signing timestamp is missing.")
    return nil
  }
  return infoDate as Date
}

if let infoDate = getCodeSignedDate(bundleURL: url) {
  let dateFormatter = DateFormatter()
  dateFormatter.dateFormat = "yyyyMMdd.HHmm"
  dateFormatter.timeZone = .init(secondsFromGMT: +28800) ?? .current
  let strDate = dateFormatter.string(from: infoDate)
  print(strDate)
}
Pipkin answered 20/7, 2023 at 14:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.