Obtain Model Identifier string on OS X - macos

Every Mac has a model identifier, for example "Macmini5,1". (These are shown in the System Information app.)
How can I programatically obtain this model identifier string?

Swift 4+ using IOKit
import IOKit
func getModelIdentifier() -> String? {
let service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("IOPlatformExpertDevice"))
var modelIdentifier: String?
if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data {
modelIdentifier = String(data: modelData, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters)
}
IOObjectRelease(service)
return modelIdentifier
}

You can use sysctl
#import <Foundation/Foundation.h>
#import <sys/sysctl.h>
NSString *ModelIdentifier()
{
NSString *result=#"Unknown Mac";
size_t len=0;
sysctlbyname("hw.model", NULL, &len, NULL, 0);
if (len) {
NSMutableData *data=[NSMutableData dataWithLength:len];
sysctlbyname("hw.model", [data mutableBytes], &len, NULL, 0);
result=[NSString stringWithUTF8String:[data bytes]];
}
return result;
}

You can also use IOKit.framework. I think it's best choice.
This simple code example shows how to read model identifier from I/O Kit registry to NSString:
- (NSString *)modelIdentifier {
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("IOPlatformExpertDevice"));
CFStringRef model = IORegistryEntryCreateCFProperty(service,
CFSTR("model"),
kCFAllocatorDefault,
0);
NSString *modelIdentifier = [[NSString alloc] initWithData:(__bridge NSData *)model
encoding:NSUTF8StringEncoding];
CFRelease(model);
IOObjectRelease(service);
return modelIdentifier;
}
Strings "IOPlatformExpertDevice" and "model" in code above is used to read model identifier from I/O Kit registry. ioreg command line tool is your friend, when you want to find information from I/O Kit registry. This image shows those strings in ioreg output:
I hope this helps to use IOKit.framework.

Answer from Ryan H is correct except improper conversion from null-terminated string to Swift String, giving result with \0 symbol in the end, which you may not expect, performing full match. This is corrected version:
static private func modelIdentifier() -> String? {
let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
defer { IOObjectRelease(service) }
if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data {
return modelData.withUnsafeBytes { (cString: UnsafePointer<UInt8>) -> String in
return String(cString: cString)
}
}
return nil
}

You can get the same output from the system_profiler command. It has an -xml option that you can use. NSTask can run the command for you and you can parse the result.
Sample code:
#import <Foundation/Foundation.h>
NSString *ModelIdentifier() {
NSPipe *pipe=[NSPipe pipe];
NSTask *task=[[NSTask alloc] init];
[task setLaunchPath:#"/usr/sbin/system_profiler"];
[task setArguments:#[#"-xml", #"SPHardwareDataType"]];
[task setStandardOutput:pipe];
[task launch];
NSData *outData=[[pipe fileHandleForReading] readDataToEndOfFile];
NSString *outString=[[NSString alloc] initWithData:outData encoding:NSUTF8StringEncoding];
return [outString propertyList][0][#"_items"][0][#"machine_model"];
}

CFStringRef model = IORegistryEntryCreateCFProperty(service,
CFSTR("model"),
kCFAllocatorDefault,
0); ? type is ok ?
I think code maybe like this:
CFSDataRef model = IORegistryEntryCreateCFProperty(service,
CFSTR("model"),
kCFAllocatorDefault,
0);

With iOS 16 apps that also build to Mac Catalyst any solution here that uses kIOMasterPortDefault (such as Ryan H's) will generate a build error:
'kIOMasterPortDefault' is unavailable in Mac Catalyst
Attempting to switch kIOMasterPortDefault to kIOMainPortDefault may also give a build error depending on which versions of Catalyst you are targetting – and I found trying to use #available to get around that didn't work either.
If you run into that situation then try the following, which is a reformulation of Parag Bafna's answer into Swift:
var modelIdentifier: String {
#if targetEnvironment(macCatalyst)
var size = 0
sysctlbyname("hw.model", nil, &size, nil, 0)
var modelIdentifier: [CChar] = Array(repeating: 0, count: size)
sysctlbyname("hw.model", &modelIdentifier, &size, nil, 0)
return String(cString: modelIdentifier)
#else
// Handle iOS
#endif
}

Swift version of Parag Bafna excellent answer
var deviceName: String {
var str = "Unknown Device"
var len = 0
sysctlbyname("hw.model", nil, &len, nil, 0)
if len > 0 {
var data = Data(count: len)
sysctlbyname("hw.model", &data, &len, nil, 0)
if let s = String(bytes: data, encoding: .utf8) {
str = s
}
}
return str
}

Related

Does iOS 13 has new way of getting device notification token?

So my friend got this email from OneSignal
Due to a change that may occur as part of the upcoming iOS 13 release, you must update to the latest version of the iOS SDK before building your app with Xcode 11. All of OneSignal’s wrapper SDKs including React Native, Unity, and Flutter have been updated as well.
The reason for this is that Xcode 11, which is being released alongside iOS 13, breaks a common technique that apps and libraries like OneSignal were using to get a push token for the device. If you do not use our new SDK then new users will not be able to subscribe to notifications from your app.
And I got curious about it.
This is the way we got the device notification token on iOS 12
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
var token = ""
for i in 0..<deviceToken.count {
token = token + String(format: "%02.2hhx", arguments: [deviceToken[i]])
}
print("Notification token = \(token)")
}
Whats the proper way to get it on iOS 13?
Should I do the new way for my currently developing apps or the old way is still fine?
You may use this method to fetch the device token on iOS 13 onwards:
Objective-C:
+ (NSString *)stringFromDeviceToken:(NSData *)deviceToken {
NSUInteger length = deviceToken.length;
if (length == 0) {
return nil;
}
const unsigned char *buffer = deviceToken.bytes;
NSMutableString *hexString = [NSMutableString stringWithCapacity:(length * 2)];
for (int i = 0; i < length; ++i) {
[hexString appendFormat:#"%02x", buffer[i]];
}
return [hexString copy];
}
Swift 5.0 (Untested)
class func string(fromDeviceToken deviceToken: Data?) -> String? {
let length = deviceToken?.count ?? 0
if length == 0 {
return nil
}
let buffer = UInt8(deviceToken?.bytes ?? 0)
var hexString = String(repeating: "\0", count: length * 2)
for i in 0..<length {
hexString += String(format: "%02x", buffer[i])
}
return hexString
}
Taken from OneSignal blog
The way you do it is fine and it should continue to work on iOS 13. But some developers do it like this. To convert Data into base-16 strings, they call description, which returns something like
<124686a5 556a72ca d808f572 00c323b9 3eff9285 92445590 3225757d b83997ba>
And then they trim < and > and remove spaces.
On iOS 13 the description called on token data returns something like
{ length = 32, bytes = 0xd3d997af 967d1f43 b405374a 13394d2f ... 28f10282 14af515f }
Which obviously makes this way broken.
Another example of wrong implementation (already edited to include correct implementation as well).
Some more examples might be found in this thread.
The same code for Swift 5 but bit shorter variant. Verified at iOS 13.
func getStringFrom(token:NSData) -> String {
return token.reduce("") { $0 + String(format: "%02.2hhx", $1) }
}
Correctly capture iOS 13 Device Token in Xamarin.iOS
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
//DeviceToken = Regex.Replace(deviceToken.ToString(), "[^0-9a-zA-Z]+", "");
//Replace the above line whick worked up to iOS12 with the code below:
byte[] bytes = deviceToken.ToArray<byte>();
string[] hexArray = bytes.Select(b => b.ToString("x2")).ToArray();
DeviceToken = string.Join(string.Empty, hexArray);
}
Here is what's going on here:
First we have to grab all the bytes in the device token by calling
the ToArray() method on it.
Once we have the bytes which is an array of bytes or, byte[], we
call LINQ Select which applies an inner function that takes each
byte and returns a zero-padded 2 digit Hex string. C# can do this
nicely using the format specifier x2. The LINQ Select function
returns an IEnumerable, so it’s easy to call ToArray() to
get an array of string or string[].
Now just call Join() method on an array of string and we end up with
a concatenated string.
Reference: https://dev.to/codeprototype/correctly-capture-ios-13-device-token-in-xamarin-1968
Solution 2: This also works fine
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
byte[] result = new byte[deviceToken.Length];
Marshal.Copy(deviceToken.Bytes, result, 0, (int)deviceToken.Length);
var token = BitConverter.ToString(result).Replace("-", "");
}
Nice solution in C# with Xamarin:
// In AppDelegate class:
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
var bytes = deviceToken.ToArray();
var deviceTokenString = string.Concat(bytes.Select(b => $"{b:x2}"));
// TODO: handle deviceTokenString
}
func getStringFrom(deviceToken: Data) -> String {
var token = ""
for i in 0..<deviceToken.count {
token += String(format: "%02.2hhx", arguments: [deviceToken[i]])
}
return token
}
You can have look in the below code as I was also stuck on this problem. Here is the code by which you can get the device token in below iOS 13 and above.
NSString *str = [NSString stringWithFormat:#"%#", devTokendata]; // devTokendata is NSData
str = [str stringByReplacingOccurrencesOfString:#" " withString:#""];
str = [str stringByReplacingOccurrencesOfString:#"<" withString:#""];
str = [str stringByReplacingOccurrencesOfString:#">" withString:#""];
if (#available(iOS 13, *)) {
str = [self hexadecimalStringFromData:devToken];
NSLog(#"APNS Token: %#",str);
}
-(NSString *)deviceTokenFromData:(NSData *)data
{
NSUInteger dataLength = data.length;
if (dataLength == 0) {
return nil;
}
const unsigned char *dataBuffer = (const unsigned char *)data.bytes;
NSMutableString *hexString = [NSMutableString stringWithCapacity:(dataLength * 2)];
for (int i = 0; i < dataLength; ++i) {
[hexString appendFormat:#"%02x", dataBuffer[i]];
}
return [hexString copy];
}
use deviceToken.debugDescription

Detecting screen recording settings on macOS Catalina

What's is a reliable way to detect if user has enabled this API?
CGWindowListCreateImage returns a valid object even if screen recording API is disabled. There are multiple combinations possible (kCGWindowListOptionIncludingWindow, kCGWindowListOptionOnScreenBelowWindow) and only some will return NULL.
- (CGImageRef)createScreenshotImage
{
NSWindow *window = [[self view] window];
NSRect rect = [window frame];
rect.origin.y = NSHeight([[window screen] frame]) - NSMaxY([window frame]);
CGImageRef screenshot = CGWindowListCreateImage(
rect,
kCGWindowListOptionIncludingWindow,
//kCGWindowListOptionOnScreenBelowWindow,
0,//(CGWindowID)[window windowNumber],
kCGWindowImageBoundsIgnoreFraming);//kCGWindowImageDefault
return screenshot;
}
The only reliable way is through CGDisplayStreamCreate which is risky as Apple always changes privacy settings every year.
- (BOOL)canRecordScreen
{
if (#available(macOS 10.15, *)) {
CGDisplayStreamRef stream = CGDisplayStreamCreate(CGMainDisplayID(), 1, 1, kCVPixelFormatType_32BGRA, nil, ^(CGDisplayStreamFrameStatus status, uint64_t displayTime, IOSurfaceRef frameSurface, CGDisplayStreamUpdateRef updateRef) {
;
});
BOOL canRecord = stream != NULL;
if (stream) {
CFRelease(stream);
}
return canRecord;
} else {
return YES;
}
}
All of the solutions presented here have a flaw in one way or another. The root of the problem is that there's no correlation between your permission to know about a window (via the name in the window list), your permission to know about the process owner of the window (such as WindowServer and Dock). Your permission to view the pixels on screen is a combination of two sparse sets of information.
Here is a heuristic that covers all the cases as of macOS 10.15.1:
BOOL canRecordScreen = YES;
if (#available(macOS 10.15, *)) {
canRecordScreen = NO;
NSRunningApplication *runningApplication = NSRunningApplication.currentApplication;
NSNumber *ourProcessIdentifier = [NSNumber numberWithInteger:runningApplication.processIdentifier];
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger numberOfWindows = CFArrayGetCount(windowList);
for (int index = 0; index < numberOfWindows; index++) {
// get information for each window
NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, index);
NSString *windowName = windowInfo[(id)kCGWindowName];
NSNumber *processIdentifier = windowInfo[(id)kCGWindowOwnerPID];
// don't check windows owned by this process
if (! [processIdentifier isEqual:ourProcessIdentifier]) {
// get process information for each window
pid_t pid = processIdentifier.intValue;
NSRunningApplication *windowRunningApplication = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
if (! windowRunningApplication) {
// ignore processes we don't have access to, such as WindowServer, which manages the windows named "Menubar" and "Backstop Menubar"
}
else {
NSString *windowExecutableName = windowRunningApplication.executableURL.lastPathComponent;
if (windowName) {
if ([windowExecutableName isEqual:#"Dock"]) {
// ignore the Dock, which provides the desktop picture
}
else {
canRecordScreen = YES;
break;
}
}
}
}
}
CFRelease(windowList);
}
If canRecordScreen is not set, you'll need to put up some kind of dialog that warns the user that they'll only be able to see the menubar, desktop picture, and the app's own windows. Here's how we presented it in our app xScope.
And yes, I'm still bitter that these protections were introduced with little regard to usability.
Apple provides direct low level api to check for access and grant access. No need to use tricky workarounds.
/* Checks whether the current process already has screen capture access */
#available(macOS 10.15, *)
public func CGPreflightScreenCaptureAccess() -> Bool
Use the above functions to check for screen capture access.
if access is not given use the below function to prompt for access
/* Requests event listening access if absent, potentially prompting */
#available(macOS 10.15, *)
public func CGRequestScreenCaptureAccess() -> Bool
Screenshot taken from documentation
#marek-h posted a good example that can detect the screen recording setting without showing privacy alert.
Btw, #jordan-h mentioned that this solution doesn't work when the app presents an alert via beginSheetModalForWindow.
I found that SystemUIServer process is always creating some windows with names: AppleVolumeExtra, AppleClockExtra, AppleBluetoothExtra ...
We can't get the names of these windows, before the screen recording is enabled in Privacy preferences. And when we can get one of these names at least, then it means that the user has enabled screen recording.
So we can check the names of the windows (created by SystemUIServer process) to detect the screen recording preference, and it works fine on macOS Catalina.
#include <AppKit/AppKit.h>
#include <libproc.h>
bool isScreenRecordingEnabled()
{
if (#available(macos 10.15, *)) {
bool bRet = false;
CFArrayRef list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID);
if (list) {
int n = (int)(CFArrayGetCount(list));
for (int i = 0; i < n; i++) {
NSDictionary* info = (NSDictionary*)(CFArrayGetValueAtIndex(list, (CFIndex)i));
NSString* name = info[(id)kCGWindowName];
NSNumber* pid = info[(id)kCGWindowOwnerPID];
if (pid != nil && name != nil) {
int nPid = [pid intValue];
char path[PROC_PIDPATHINFO_MAXSIZE+1];
int lenPath = proc_pidpath(nPid, path, PROC_PIDPATHINFO_MAXSIZE);
if (lenPath > 0) {
path[lenPath] = 0;
if (strcmp(path, "/System/Library/CoreServices/SystemUIServer.app/Contents/MacOS/SystemUIServer") == 0) {
bRet = true;
break;
}
}
}
}
CFRelease(list);
}
return bRet;
} else {
return true;
}
}
I'm not aware of an API that's specifically for getting the screen recording permission status. Besides creating a CGDisplayStream and checking for nil, the Advances in macOS Security WWDC presentation also mentioned that certain metadata from the CGWindowListCopyWindowInfo() API will not be returned unless permission is granted. So something like this does seem to work, although it has the same issue of relying on implementation details of that function:
private func canRecordScreen() -> Bool {
guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
return windows.allSatisfy({ window in
let windowName = window[kCGWindowName as String] as? String
return windowName != nil
})
}
As of Nov19 chockenberry has correct answer.
As #onelittlefish pointed out the kCGWindowName is being omitted in case user has not enabled the screen recording access in privacy pane. This method also doesn't trigger the privacy alert.
- (BOOL)canRecordScreen
{
if (#available(macOS 10.15, *)) {
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger numberOfWindows = CFArrayGetCount(windowList);
NSUInteger numberOfWindowsWithName = 0;
for (int idx = 0; idx < numberOfWindows; idx++) {
NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
NSString *windowName = windowInfo[(id)kCGWindowName];
if (windowName) {
numberOfWindowsWithName++;
} else {
//no kCGWindowName detected -> not enabled
break; //breaking early, numberOfWindowsWithName not increased
}
}
CFRelease(windowList);
return numberOfWindows == numberOfWindowsWithName;
}
return YES;
}
The most favorable answer is not exactly right, he left out some sences, like sharing state.
we can find the answer in WWDC(https://developer.apple.com/videos/play/wwdc2019/701/?time=1007)
Here are some excerpts from WWDC:
the window name and sharing state are not available, unless the user has preapproved the app for screen recording. And this is because some apps put sensitive data such as account names or more likely web page URLs in the window's name.
- (BOOL)ScreeningRecordPermissionCheck {
if (#available(macOS 10.15, *)) {
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
NSUInteger numberOfWindows = CFArrayGetCount(windowList);
NSUInteger numberOfWindowsWithInfoGet = 0;
for (int idx = 0; idx < numberOfWindows; idx++) {
NSDictionary *windowInfo = (NSDictionary *)CFArrayGetValueAtIndex(windowList, idx);
NSString *windowName = windowInfo[(id)kCGWindowName];
NSNumber* sharingType = windowInfo[(id)kCGWindowSharingState];
if (windowName || kCGWindowSharingNone != sharingType.intValue) {
numberOfWindowsWithInfoGet++;
} else {
NSNumber* pid = windowInfo[(id)kCGWindowOwnerPID];
NSString* appName = windowInfo[(id)kCGWindowOwnerName];
NSLog(#"windowInfo get Fail pid:%lu appName:%#", pid.integerValue, appName);
}
}
CFRelease(windowList);
if (numberOfWindows == numberOfWindowsWithInfoGet) {
return YES;
} else {
return NO;
}
}
return YES;
}
As of MacOS 10.15.7 the heuristics of obtaining window-names for visible windows, and so know we have screen-capture permission, doesn't always work. Sometimes we just don't find valid windows we can query, and would wrongly deduce we don't have permissions.
However, I found another way to directly query (using sqlite) the Apple TCC database - the model where permissions are persisted. The screen-recording permissions are to be found in the "System level" TCC database ( residing in /Library/Application Support/com.apple.TCC/TCC.db). If you open the database using sqlite, and query: SELECT allowed FROM access WHERE client="com.myCompany.myApp" AND service="kTCCServiceScreenCapture" you'll get your answer.
Two downsides comparing to other answers:
to open this TCC.db database, your app must have "Full Disk Access" permission. It doesn't need to run with 'root' privileges, and root privileges won't help if you don't have the "Full disk access".
it takes about 15 millisec to run, which is slower than querying the window list.
The up side -- it's a direct query of the actual thing, and does not rely on any windows, or processes to exist at the time of query.
Here's some draft code to do this:
NSString *client = #"com.myCompany.myApp";
sqlite3 *tccDb = NULL;
sqlite3_stmt *statement = NULL;
NSString *pathToSystemTCCDB = #"/Library/Application Support/com.apple.TCC/TCC.db";
const char *pathToDBFile = [pathToSystemTCCDB fileSystemRepresentation];
if (sqlite3_open(pathToDBFile, &tccDb) != SQLITE_OK)
return nil;
const char *query = [[NSString stringWithFormat: #"SELECT allowed FROM access WHERE client=\"%#\" AND service=\"kTCCServiceScreenCapture\"",client] UTF8String];
if (sqlite3_prepare_v2(tccDb, query , -1, &statement, nil) != SQLITE_OK)
return nil;
BOOL allowed = NO;
while (sqlite3_step(statement) == SQLITE_ROW)
allowed |= (sqlite3_column_int(statement, 0) == 1);
if (statement)
sqlite3_finalize(statement);
if (tccDb)
sqlite3_close(tccDb);
return #(allowed);
}
Working for me.
Code from: https://gist.github.com/code4you2021/270859c71f90720d880ccb2474f4e7df
import Cocoa
struct ScreenRecordPermission {
static var hasPermission: Bool {
permissionCheck()
}
static func permissionCheck() -> Bool {
if #available(macOS 10.15, *) {
let runningApplication = NSRunningApplication.current
let processIdentifier = runningApplication.processIdentifier
guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID)
as? [[String: AnyObject]],
let _ = windows.first(where: { window -> Bool in
guard let windowProcessIdentifier = (window[kCGWindowOwnerPID as String] as? Int).flatMap(pid_t.init),
windowProcessIdentifier != processIdentifier,
let windowRunningApplication = NSRunningApplication(processIdentifier: windowProcessIdentifier),
windowRunningApplication.executableURL?.lastPathComponent != "Dock",
let _ = window[String(kCGWindowName)] as? String
else {
return false
}
return true
})
else {
return false
}
}
return true
}
static func requestPermission() {
if #available(macOS 10.15, *) {
CGWindowListCreateImage(CGRect(x: 0, y: 0, width: 1, height: 1), .optionOnScreenOnly, kCGNullWindowID, [])
}
}
}
# how to use
# print("hasPermission: ", ScreenRecordPermission.hasPermission)
The above answer is not working fine. Below is the correct answer.
private var canRecordScreen : Bool {
guard let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly], kCGNullWindowID) as? [[String: AnyObject]] else { return false }
return windows.allSatisfy({ window in
let windowName = window[kCGWindowName as String] as? String
let isSharingEnabled = window[kCGWindowSharingState as String] as? Int
return windowName != nil || isSharingEnabled == 1
})
}

How to get an ALAsset URL from a PHAsset?

You can do it sneakily† using the undocumented PHAsset.ALAssetURL property, but I'm looking for something documented.
† In Objective-C, this will help
#interface PHAsset (Sneaky)
#property (nonatomic, readonly) NSURL *ALAssetURL;
#end
Create the assetURL by leveraging the localidentifier of the PHAsset.
Example:
PHAsset.localidentifier returns 91B1C271-C617-49CE-A074-E391BA7F843F/L0/001
Now take the 32 first characters to build the assetURL, like:
assets-library://asset/asset.JPG?id=91B1C271-C617-49CE-A074-E391BA7F843F&ext=JPG
You might change the extension JPG depending on the UTI of the asset (requestImageDataForAsset returns the UTI), but in my testing the extensions of the assetURL seems to be ignored anyhow.
I wanted to be able to get a URL for an asset too. However, I have realised that the localIdentifier can be persisted instead and used to recover the PHAsset.
PHAsset* asset = [PHAsset fetchAssetsWithLocalIdentifiers:#[localIdentifier] options:nil].firstObject;
Legacy asset URLs can be converted using:
PHAsset* legacyAsset = [PHAsset fetchAssetsWithALAssetUrls:#[assetUrl] options:nil].firstObject;
NSString* convertedIdentifier = legacyAsset.localIdentifier;
(before that method gets obsoleted...)
(Thanks holtmann - localIdentifier is hidden away in PHObject.)
Here is working code tested on iOS 11 both simulator and device
PHFetchResult *result = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:nil];
[result enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
PHAsset *asset = (PHAsset *)obj;
[asset requestContentEditingInputWithOptions:nil completionHandler:^(PHContentEditingInput * _Nullable contentEditingInput, NSDictionary * _Nonnull info) {
NSLog(#"URL:%#", contentEditingInput.fullSizeImageURL.absoluteString);
NSString* path = [contentEditingInput.fullSizeImageURL.absoluteString substringFromIndex:7];//screw all the crap of file://
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isExist = [fileManager fileExistsAtPath:path];
if (isExist)
NSLog(#"oh yeah");
else {
NSLog(#"damn");
}
}];
}];
Read the bottom!
The resultHandler for PHImageManager.requestImage returns 2 objects: result and info.
You can get the original filename for the PHAsset (like IMG_1043.JPG) as well as its full path on the filesystem with:
let url = info?["PHImageFileURLKey"] as! URL
This should work right, but for some reason it doesn't. So basically, you have to copy your image to a file then access that then delete it.
The PHImageFileURLKey is usable to get the original file name, but you cannot actually access that file. It probably has to do with the fact that code in the background can access the file while other apps can delete it.
Here is a PHAsset extension written in Swift that will retrieve the URL.
extension PHAsset {
func getURL(completionHandler : #escaping ((_ responseURL : URL?) -> Void)){
if self.mediaType == .image {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
self.requestContentEditingInput(with: options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [AnyHashable : Any]) -> Void in
completionHandler(contentEditingInput!.fullSizeImageURL as URL?)
})
} else if self.mediaType == .video {
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.version = .original
PHImageManager.default().requestAVAsset(forVideo: self, options: options, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [AnyHashable : Any]?) -> Void in
if let urlAsset = asset as? AVURLAsset {
let localVideoUrl: URL = urlAsset.url as URL
completionHandler(localVideoUrl)
} else {
completionHandler(nil)
}
})
}
}
}

NSURL from PHAsset

I'm converting our app over to use the Photos Framework of iOS8, the ALAsset framework is clearly a second class citizen under iOS8.
I'm having a problem is that our architecture really wants an NSURL that represents the location of the media on "disk." We use this to upload the media to our servers for further processing.
This was easy with ALAsset:
ALAssetRepresentation *rep = [asset defaultRepresentation];
self.originalVideo = rep.url;
But I'm just not seeing this ability in PHAsset. I guess I can call:
imageManager.requestImageDataForAsset
and then write it out to a temp spot in the file system but that seems awfully heavyweight and wasteful, not to mention potentially slow.
Is there a way to get this or am I going to have refactor more of my app to only use NSURLs for iOS7 and some other method for iOS8?
If you use [imageManager requestAVAssetForVideo...], it'll return an AVAsset. That AVAsset is actually an AVURLAsset, so if you cast it, you can access it's -url property.
I'm not sure if you can create a new asset out of this, but it does give you the location.
SWIFT 2.0 version
This function returns NSURL from PHAsset (both image and video)
func getAssetUrl(mPhasset : PHAsset, completionHandler : ((responseURL : NSURL?) -> Void)){
if mPhasset.mediaType == .Image {
let options: PHContentEditingInputRequestOptions = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = {(adjustmeta: PHAdjustmentData) -> Bool in
return true
}
mPhasset.requestContentEditingInputWithOptions(options, completionHandler: {(contentEditingInput: PHContentEditingInput?, info: [NSObject : AnyObject]) -> Void in
completionHandler(responseURL : contentEditingInput!.fullSizeImageURL)
})
} else if mPhasset.mediaType == .Video {
let options: PHVideoRequestOptions = PHVideoRequestOptions()
options.version = .Original
PHImageManager.defaultManager().requestAVAssetForVideo(mPhasset, options: options, resultHandler: {(asset: AVAsset?, audioMix: AVAudioMix?, info: [NSObject : AnyObject]?) -> Void in
if let urlAsset = asset as? AVURLAsset {
let localVideoUrl : NSURL = urlAsset.URL
completionHandler(responseURL : localVideoUrl)
} else {
completionHandler(responseURL : nil)
}
})
}
}
If you have a PHAsset, you can get the url for said asset like this:
[asset requestContentEditingInputWithOptions:editOptions
completionHandler:^(PHContentEditingInput *contentEditingInput, NSDictionary *info) {
NSURL *imageURL = contentEditingInput.fullSizeImageURL;
}];
Use the new localIdentifier property of PHObject. (PHAsset inherits from this).
It provides similar functionality to an ALAsset URL, namely that you can load assets by calling the method
+[PHAsset fetchAssetsWithLocalIdentifiers:identifiers options:options]
All the above solutions won't work for slow-motion videos. A solution that I found handles all video asset types is this:
func createFileURLFromVideoPHAsset(asset: PHAsset, destinationURL: NSURL) {
PHCachingImageManager().requestAVAssetForVideo(self, options: nil) { avAsset, _, _ in
let exportSession = AVAssetExportSession(asset: avAsset!, presetName: AVAssetExportPresetHighestQuality)!
exportSession.outputFileType = AVFileTypeMPEG4
exportSession.outputURL = destinationURL
exportSession.exportAsynchronouslyWithCompletionHandler {
guard exportSession.error == nil else {
log.error("Error exporting video asset: \(exportSession.error)")
return
}
// It worked! You can find your file at: destinationURL
}
}
}
See this answer here.
And this one here.
In my experience you'll need to first export the asset to disk in order to get a fully accessible / reliable URL.
The answers linked to above describe how to do this.
Just want to post the hidden gem from a comment from #jlw
#rishu1992 For slo-mo videos, grab the AVComposition's
AVCompositionTrack (of mediaType AVMediaTypeVideo), grab its first
segment (of type AVCompositionTrackSegment), and then access its
sourceURL property. – jlw Aug 25 '15 at 11:52
In speking of url from PHAsset, I had once prepared a util func on Swift 2 (although only for playing videos from PHAsset). Sharing it in this answer, might help someone.
static func playVideo (view:UIViewController, asset:PHAsset)
Please check this Answer
Here's a handy PHAsset category:
#implementation PHAsset (Utils)
- (NSURL *)fileURL {
__block NSURL *url = nil;
switch (self.mediaType) {
case PHAssetMediaTypeImage: {
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.synchronous = YES;
[PHImageManager.defaultManager requestImageDataForAsset:self
options:options
resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
url = info[#"PHImageFileURLKey"];
}];
break;
}
case PHAssetMediaTypeVideo: {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[PHImageManager.defaultManager requestAVAssetForVideo:self
options:nil
resultHandler:^(AVAsset *asset, AVAudioMix *audioMix, NSDictionary *info) {
if ([asset isKindOfClass:AVURLAsset.class]) {
url = [(AVURLAsset *)asset URL];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
break;
}
default:
break;
}
return url;
}
#end
I had similiar problem with video files, what worked for me was:
NSString* assetID = [asset.localIdentifier substringToIndex:(asset.localIdentifier.length - 7)];
NSURL* videoURL = [NSURL URLWithString:[NSString stringWithFormat:#"assets-library://asset/asset.mov?id=%#&ext=mov", assetID]];
Where asset is PHAsset.

Get the Username(s) stored in Keychain, using only the ServiceName? OR: Where are you supposed to store the Username?

So the OS X Keychain has three pieces of information:
ServiceName (the name of my app)
Username
Password
I obviously always know the ServiceName. Is there a way to find any saved Username(s) for that ServiceName? (Finding the password is easy once you know the Username.)
I would much prefer to use a nice Cocoa wrapper such as EMKeychain to do this. But EMKeychain requires the UserName to get any keychain item!
+ (EMGenericKeychainItem *)genericKeychainItemForService:(NSString *)serviceNameString withUsername:(NSString *)usernameString;
How are you expected to fully utilize saving credentials in the Keychain, if you need the Username to find the credentials? Is the best practice to save the Username in the .plist file or something?
SecKeychainFindGenericPassword only returns a single keychain item. To find all generic passwords for a specific service, you need to run a query on the keychain. There are several ways to do this, based on what version of OS X you target.
If you need to run on 10.5 or below, you'll need to use SecKeychainSearchCreateFromAttributes. It's a rather horrible API. Here is a rough cut of a method that returns a dictionary mapping usernames to passwords.
- (NSDictionary *)genericPasswordsWithService:(NSString *)service {
OSStatus status;
// Construct a query.
const char *utf8Service = [service UTF8String];
SecKeychainAttribute attr = { .tag = kSecServiceItemAttr,
.length = strlen(utf8Service),
.data = (void *)utf8Service };
SecKeychainAttribute attrList = { .count = 1, .attr = &attr };
SecKeychainSearchRef *search = NULL;
status = SecKeychainSearchCreateFromAttributes(NULL, kSecGenericPasswordItemClass, &attrList, &search);
if (status) {
report(status);
return nil;
}
// Enumerate results.
NSMutableDictionary *result = [NSMutableDictionary dictionary];
while (1) {
SecKeychainItemRef item = NULL;
status = SecKeychainSearchCopyNext(search, &item);
if (status)
break;
// Find 'account' attribute and password value.
UInt32 tag = kSecAccountItemAttr;
UInt32 format = CSSM_DB_ATTRIBUTE_FORMAT_STRING;
SecKeychainAttributeInfo info = { .count = 1, .tag = &tag, .format = &format };
SecKeychainAttributeList *attrList = NULL;
UInt32 length = 0;
void *data = NULL;
status = SecKeychainItemCopyAttributesAndData(item, &info, NULL, &attrList, &length, &data);
if (status) {
CFRelease(item);
continue;
}
NSAssert(attrList->count == 1 && attrList->attr[0].tag == kSecAccountItemAttr, #"SecKeychainItemCopyAttributesAndData is messing with us");
NSString *account = [[[NSString alloc] initWithBytes:attrList->attr[0].data length:attrList->attr[0].length encoding:NSUTF8StringEncoding] autorelease];
NSString *password = [[[NSString alloc] initWithBytes:data length:length encoding:NSUTF8StringEncoding] autorelease];
[result setObject:password forKey:account];
SecKeychainItemFreeAttributesAndData(attrList, data);
CFRelease(item);
}
CFRelease(search);
return result;
}
For 10.6 and later, you can use the somewhat less inconvenient SecItemCopyMatching API:
- (NSDictionary *)genericPasswordsWithService:(NSString *)service {
NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys:
kSecClassGenericPassword, kSecClass,
(id)kCFBooleanTrue, kSecReturnData,
(id)kCFBooleanTrue, kSecReturnAttributes,
kSecMatchLimitAll, kSecMatchLimit,
service, kSecAttrService,
nil];
NSArray *itemDicts = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)q, (CFTypeRef *)&itemDicts);
if (status) {
report(status);
return nil;
}
NSMutableDictionary *result = [NSMutableDictionary dictionary];
for (NSDictionary *itemDict in itemDicts) {
NSData *data = [itemDict objectForKey:kSecValueData];
NSString *password = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
NSString *account = [itemDict objectForKey:kSecAttrAccount];
[result setObject:password forKey:account];
}
[itemDicts release];
return result;
}
For 10.7 or later, you can use my wonderful LKKeychain framework (PLUG!). It doesn't support building attribute-based queries, but you can simply list all passwords and filter out the ones you don't need.
- (NSDictionary *)genericPasswordsWithService:(NSString *)service {
LKKCKeychain *keychain = [LKKCKeychain defaultKeychain];
NSMutableDictionary *result = [NSMutableDictionary dictionary];
for (LKKCGenericPassword *item in [keychain genericPasswords]) {
if ([service isEqualToString:item.service]) {
[result setObject:item.password forKey:item.account];
}
}
return result;
}
(I didn't try running, or even compiling any of the above code samples; sorry for any typos.)
You don't need the username. You do with EMKeychain, but that's an artificial distinction that that class imposes; the underlying Keychain Services function does not require a username to find a keychain item.
When using SecKeychainFindGenericPassword directly, pass 0 and NULL for the username parameters. It will return a keychain item that exists on that service.
However, that will return only one item. If the user has multiple keychain items on the same service, you won't know that, or which one you got (the documentation says it returns the “first” matching item, with no specification of what it considers “first”). If you want any and all items for that service, you should create a search and use that.
Generic passwords have a unique key of the service name and the username. Thus, to fetch a single generic keychain entry, you will need to provide both. However, you can iterate over all generic keychain entries for your given service using the SecKeychainFindGenericPassword function.
(Disclaimer: I don't know anything about doing this in EMKeychain.)

Resources